From 2534fab7e7ddc67588792c55a426c78135af6bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= Date: Mon, 17 Jul 2023 17:22:19 +0200 Subject: [PATCH 001/260] Upgrade libjxl in snapcraft.yaml --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 2251e0548..693773d0b 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -272,7 +272,7 @@ parts: libjxl: source: https://github.com/libjxl/libjxl.git source-depth: 1 - source-tag: v0.8.1 + source-tag: v0.8.2 plugin: cmake build-environment: - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s From d0e851647a6262e10cdc35f931603e4160ea0fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= Date: Mon, 17 Jul 2023 17:29:07 +0200 Subject: [PATCH 002/260] Upgrade dav1d, libde265, libheif, libjxl in Linux build --- Telegram/build/docker/centos_env/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index e4b9ad10e..a7de4ed0d 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -196,7 +196,7 @@ RUN git clone -b v1.3.1 --depth=1 {{ GIT }}/xiph/opus.git \ FROM builder AS dav1d COPY --link --from=nasm {{ LibrariesPath }}/nasm-cache / -RUN git clone -b 1.0.0 --depth=1 {{ GIT }}/videolan/dav1d.git \ +RUN git clone -b 1.2.1 --depth=1 {{ GIT }}/videolan/dav1d.git \ && cd dav1d \ && meson build \ --buildtype=plain \ @@ -209,7 +209,7 @@ RUN git clone -b 1.0.0 --depth=1 {{ GIT }}/videolan/dav1d.git \ && rm -rf dav1d FROM builder AS libde265 -RUN git clone -b v1.0.11 --depth=1 {{ GIT }}/strukturag/libde265.git \ +RUN git clone -b v1.0.12 --depth=1 {{ GIT }}/strukturag/libde265.git \ && cd libde265 \ && cmake -GNinja . \ -DCMAKE_BUILD_TYPE=None \ @@ -254,7 +254,7 @@ RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/AOMediaCodec/libavif.git \ FROM builder AS libheif COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / -RUN git clone -b v1.15.1 --depth=1 {{ GIT }}/strukturag/libheif.git \ +RUN git clone -b v1.16.2 --depth=1 {{ GIT }}/strukturag/libheif.git \ && cd libheif \ && cmake -GNinja -B build . \ -DCMAKE_BUILD_TYPE=None \ @@ -279,7 +279,7 @@ COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / COPY --link --from=highway {{ LibrariesPath }}/highway-cache / -RUN git clone -b v0.8.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ +RUN git clone -b v0.8.2 --depth=1 {{ GIT }}/libjxl/libjxl.git \ && cd libjxl \ && cmake -GNinja -B build . \ -DCMAKE_BUILD_TYPE=None \ From 0534a2fb6213a2c05e06c4a1f7668a9c31b6c326 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Thu, 1 Jun 2023 07:20:11 +0400 Subject: [PATCH 003/260] Fix QGuiApplication::desktopFileName usage The Qt documentation says: This is the file name, without the full path or the trailing ".desktop" extension of the desktop entry that represents this application according to the freedesktop desktop entry specification. Qt 6.5.2 also automatically fixes it breaking all the current tdesktop and desktop-app usage expecting the file extension. --- .../platform/linux/integration_linux.cpp | 4 +--- .../platform/linux/main_window_linux.cpp | 7 +++--- .../linux/notifications_manager_linux.cpp | 2 +- .../platform/linux/specific_linux.cpp | 24 ++++++++++--------- Telegram/lib_base | 2 +- Telegram/lib_webview | 2 +- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index d69e06030..26ed0d9fe 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -209,9 +209,7 @@ void LinuxIntegration::initInhibit() { } void LinuxIntegration::LaunchNativeApplication() { - const auto appId = QGuiApplication::desktopFileName() - .chopped(8) - .toStdString(); + const auto appId = QGuiApplication::desktopFileName().toStdString(); const auto app = Glib::wrap( G_APPLICATION( diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index 98c6ff40a..c0b76494e 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -118,9 +118,7 @@ void XCBSetDesktopFileName(QWindow *window) { base::Platform::XCB::GetAtom(connection, "_KDE_NET_WM_DESKTOP_FILE"), }; - const auto filename = QGuiApplication::desktopFileName() - .chopped(8) - .toUtf8(); + const auto filename = QGuiApplication::desktopFileName().toUtf8(); for (const auto atom : filenameAtoms) { if (atom.has_value()) { @@ -244,7 +242,8 @@ void MainWindow::updateUnityCounter() { const auto launcherUrl = Glib::ustring( "application://" - + QGuiApplication::desktopFileName().toStdString()); + + QGuiApplication::desktopFileName().toStdString() + + ".desktop"); const auto counterSlice = std::min(Core::App().unreadBadge(), 9999); std::map dbusUnityProperties; diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index 456be21ba..210064ac0 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -591,7 +591,7 @@ bool NotificationData::init( _hints["category"] = Glib::Variant::create("im.received"); _hints["desktop-entry"] = Glib::Variant::create( - QGuiApplication::desktopFileName().chopped(8).toStdString()); + QGuiApplication::desktopFileName().toStdString()); _notificationClosedSignalId = _dbusConnection->signal_subscribe( signalEmitted, diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index f0f673c7f..9e9fe63ba 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -191,7 +191,9 @@ bool GenerateDesktopFile( if (!QDir(targetPath).exists()) QDir().mkpath(targetPath); const auto sourceFile = kDesktopFile.utf16(); - const auto targetFile = targetPath + QGuiApplication::desktopFileName(); + const auto targetFile = targetPath + + QGuiApplication::desktopFileName() + + u".desktop"_q; const auto sourceText = [&] { QFile source(sourceFile); @@ -332,7 +334,7 @@ bool GenerateServiceFile(bool silent = false) { QStandardPaths::GenericDataLocation) + u"/dbus-1/services/"_q; const auto targetFile = targetPath - + QGuiApplication::desktopFileName().chopped(8) + + QGuiApplication::desktopFileName() + u".service"_q; DEBUG_LOG(("App Info: placing .service file to %1").arg(targetPath)); @@ -344,7 +346,7 @@ bool GenerateServiceFile(bool silent = false) { target->set_string( group, "Name", - QGuiApplication::desktopFileName().chopped(8).toStdString()); + QGuiApplication::desktopFileName().toStdString()); target->set_string( group, @@ -455,7 +457,9 @@ void AutostartToggle(bool enabled, Fn done) { if (!enabled) { return QFile::remove( - autostart + QGuiApplication::desktopFileName()); + autostart + + QGuiApplication::desktopFileName() + + u".desktop"_q); } return GenerateDesktopFile( @@ -557,14 +561,13 @@ void start() { QGuiApplication::setDesktopFileName([&] { if (KSandbox::isFlatpak()) { - return qEnvironmentVariable("FLATPAK_ID") + u".desktop"_q; + return qEnvironmentVariable("FLATPAK_ID"); } if (KSandbox::isSnap()) { return qEnvironmentVariable("SNAP_INSTANCE_NAME") + '_' - + cExeName() - + u".desktop"_q; + + cExeName(); } if (!Core::UpdaterDisabled()) { @@ -579,14 +582,13 @@ void start() { md5Hash.data()); } - return u"org.telegram.desktop._%1.desktop"_q.arg( - md5Hash.constData()); + return u"org.telegram.desktop._%1"_q.arg(md5Hash.constData()); } - return u"org.telegram.desktop.desktop"_q; + return u"org.telegram.desktop"_q; }()); - LOG(("Launcher filename: %1").arg(QGuiApplication::desktopFileName())); + LOG(("App ID: %1").arg(QGuiApplication::desktopFileName())); if (!qEnvironmentVariableIsSet("XDG_ACTIVATION_TOKEN") && qEnvironmentVariableIsSet("DESKTOP_STARTUP_ID")) { diff --git a/Telegram/lib_base b/Telegram/lib_base index 9aaee8907..4efa613fe 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 9aaee890765618e9d9ea52fba0a03ab4baa7dfda +Subproject commit 4efa613fe3fb5e1db8a4c53b08852cfe538d2601 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 5c3e82bfe..ec24c7a96 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 5c3e82bfe71627ddeffee6ec44a7215bfb52e38e +Subproject commit ec24c7a96036268b2024ca9765a66c63e6b8396a From 59bb46aa406a0e446aec4984025045e86881b2f7 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 19 Jul 2023 22:58:22 +0400 Subject: [PATCH 004/260] Update Qt to 6.5.2 on Linux --- Telegram/build/docker/centos_env/Dockerfile | 4 ++-- snap/snapcraft.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index a7de4ed0d..88fe34eb2 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,6 +1,6 @@ {%- set GIT = "https://github.com" -%} {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} -{%- set QT = "6.5.1" -%} +{%- set QT = "6.5.2" -%} {%- set QT_TAG = "v" ~ QT -%} {%- set QT_PREFIX = "/usr/local/desktop-app/Qt-" ~ QT -%} {%- set OPENSSL_VER = "1_1_1" -%} @@ -59,7 +59,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin {{ GIT }}/desktop-app/patches.git \ - && git fetch --depth=1 origin 963f5b0c2a57f7e90239e64c9e14ed5f043e637e \ + && git fetch --depth=1 origin 523b061671d4dd2f29979c982a2d03c2cc8eaf4f \ && git reset --hard FETCH_HEAD \ && rm -rf .git diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 693773d0b..f6ccbea64 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -461,7 +461,7 @@ parts: - libxkbcommon-x11-0 - zlib1g override-pull: | - QT=6.5.1 + QT=6.5.2 git clone -b v${QT} --depth=1 https://code.qt.io/qt/qt5.git . git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools From 29d0c8c2eca5cbb196d8d66910aac0e200a736f1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 28 Apr 2023 17:43:57 +0400 Subject: [PATCH 005/260] Update API scheme to layer 159. --- Telegram/Resources/tl/api.tl | 31 +++++++++++-------- Telegram/SourceFiles/api/api_updates.cpp | 6 ++-- Telegram/SourceFiles/api/api_user_privacy.cpp | 21 ++++++------- Telegram/SourceFiles/api/api_user_privacy.h | 1 + Telegram/SourceFiles/data/data_poll.cpp | 18 +++++------ Telegram/SourceFiles/data/data_poll.h | 2 +- .../history/view/media/history_view_poll.cpp | 14 ++++----- .../polls/info_polls_results_inner_widget.cpp | 27 ++++++++-------- 8 files changed, 63 insertions(+), 57 deletions(-) diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 6a606b743..aa3934e16 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -370,7 +370,7 @@ updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector = Update; updateTheme#8216fba3 theme:Theme = Update; updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; updateLoginToken#564fe691 = Update; -updateMessagePollVote#106395c9 poll_id:long user_id:long options:Vector qts:int = Update; +updateMessagePollVote#24f40e77 poll_id:long peer:Peer options:Vector qts:int = Update; updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update; updateDialogFilterOrder#a5d72105 order:Vector = Update; updateDialogFilters#3504914f = Update; @@ -514,6 +514,7 @@ inputPrivacyKeyProfilePhoto#5719bacc = InputPrivacyKey; inputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey; inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey; inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey; +inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey; privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; privacyKeyChatInvite#500e6dfa = PrivacyKey; @@ -524,6 +525,7 @@ privacyKeyProfilePhoto#96151fed = PrivacyKey; privacyKeyPhoneNumber#d19ae46d = PrivacyKey; privacyKeyAddedByPhone#42ffd42b = PrivacyKey; privacyKeyVoiceMessages#697f414 = PrivacyKey; +privacyKeyAbout#a486b761 = PrivacyKey; inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; @@ -1135,7 +1137,7 @@ poll#86e18161 id:long flags:# closed:flags.0?true public_voters:flags.1?true mul pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters; -pollResults#dcb82ea3 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; +pollResults#7adf2420 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; chatOnlines#f041e250 onlines:int = ChatOnlines; @@ -1156,7 +1158,7 @@ codeSettings#ad253d78 flags:# allow_flashcall:flags.0?true current_number:flags. wallPaperSettings#1dc1bca4 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings; -autoDownloadSettings#8efab953 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true photo_size_max:int video_size_max:long file_size_max:long video_upload_maxbitrate:int = AutoDownloadSettings; +autoDownloadSettings#baa57628 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true photo_size_max:int video_size_max:long file_size_max:long video_upload_maxbitrate:int small_queue_active_operations_max:int large_queue_active_operations_max:int = AutoDownloadSettings; account.autoDownloadSettings#63cacf26 low:AutoDownloadSettings medium:AutoDownloadSettings high:AutoDownloadSettings = account.AutoDownloadSettings; @@ -1217,11 +1219,7 @@ themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:B webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; -messageUserVote#34d247b4 user_id:long option:bytes date:int = MessageUserVote; -messageUserVoteInputOption#3ca5b0ec user_id:long date:int = MessageUserVote; -messageUserVoteMultiple#8a65e557 user_id:long options:Vector date:int = MessageUserVote; - -messages.votesList#823f649 flags:# count:int votes:Vector users:Vector next_offset:flags.0?string = messages.VotesList; +messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; @@ -1346,7 +1344,7 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; -sponsoredMessage#fc25b828 flags:# recommended:flags.5?true show_peer_photo:flags.6?true random_id:bytes from_id:flags.3?Peer chat_invite:flags.4?ChatInvite chat_invite_hash:flags.4?string channel_post:flags.2?int start_param:flags.0?string message:string entities:flags.1?Vector sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage; +sponsoredMessage#daafff6b flags:# recommended:flags.5?true show_peer_photo:flags.6?true random_id:bytes from_id:flags.3?Peer chat_invite:flags.4?ChatInvite chat_invite_hash:flags.4?string channel_post:flags.2?int start_param:flags.0?string webpage:flags.9?SponsoredWebPage message:string entities:flags.1?Vector sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage; messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages; @@ -1378,7 +1376,7 @@ availableReaction#c077ec01 flags:# inactive:flags.0?true premium:flags.2?true re messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; -messagePeerReaction#8c79b63c flags:# big:flags.0?true unread:flags.1?true peer_id:Peer date:int reaction:Reaction = MessagePeerReaction; +messagePeerReaction#8c79b63c flags:# big:flags.0?true unread:flags.1?true my:flags.2?true peer_id:Peer date:int reaction:Reaction = MessagePeerReaction; groupCallStreamChannel#80eb48af channel:int scale:int last_timestamp_ms:long = GroupCallStreamChannel; @@ -1543,6 +1541,12 @@ chatlists.chatlistUpdates#93bd878d missing_peers:Vector chats:Vector bots.botInfo#e8a775b0 name:string about:string description:string = bots.BotInfo; +messagePeerVote#b6cc2d5c peer:Peer option:bytes date:int = MessagePeerVote; +messagePeerVoteInputOption#74cda504 peer:Peer date:int = MessagePeerVote; +messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = MessagePeerVote; + +sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1663,6 +1667,7 @@ account.getDefaultGroupPhotoEmojis#915860ae hash:long = EmojiList; account.getAutoSaveSettings#adcbbcda = account.AutoSaveSettings; account.saveAutoSaveSettings#d69b8361 flags:# users:flags.0?true chats:flags.1?true broadcasts:flags.2?true peer:flags.3?InputPeer settings:AutoSaveSettings = Bool; account.deleteAutoSaveExceptions#53bc0020 = Bool; +account.invalidateSignInCodes#ca8ae8ba codes:Vector = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1768,7 +1773,6 @@ messages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:fla messages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores; messages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores; messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats; -messages.getAllChats#875f74be except_ids:Vector = messages.Chats; messages.getWebPage#32ca8f91 url:string hash:int = WebPage; messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedDialogs#3b1adf37 flags:# force:flags.0?true folder_id:int order:Vector = Bool; @@ -1882,7 +1886,7 @@ messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:Inpu messages.setChatWallPaper#8ffacae1 flags:# peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; updates.getState#edd4882a = updates.State; -updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; +updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; photos.updateProfilePhoto#9e82039 flags:# fallback:flags.0?true bot:flags.1?InputUser id:InputPhoto = photos.Photo; @@ -1979,6 +1983,7 @@ channels.reorderPinnedForumTopics#2950a18f flags:# force:flags.0?true channel:In channels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates; channels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; +channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2075,4 +2080,4 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -// LAYER 158 +// LAYER 159 diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 59f8b098b..4a8ce5173 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -677,9 +677,11 @@ void Updates::getDifference() { api().request(MTPupdates_GetDifference( MTP_flags(0), MTP_int(_ptsWaiter.current()), - MTPint(), + MTPint(), // pts_limit + MTPint(), // pts_total_limit MTP_int(_updatesDate), - MTP_int(_updatesQts) + MTP_int(_updatesQts), + MTPint() // qts_limit )).done([=](const MTPupdates_Difference &result) { differenceDone(result); }).fail([=](const MTP::Error &error) { diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index 8c97479c6..6cd57e2ec 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -177,18 +177,13 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) { case Key::Calls: return MTP_inputPrivacyKeyPhoneCall(); case Key::Invites: return MTP_inputPrivacyKeyChatInvite(); case Key::PhoneNumber: return MTP_inputPrivacyKeyPhoneNumber(); - case Key::AddedByPhone: - return MTP_inputPrivacyKeyAddedByPhone(); - case Key::LastSeen: - return MTP_inputPrivacyKeyStatusTimestamp(); - case Key::CallsPeer2Peer: - return MTP_inputPrivacyKeyPhoneP2P(); - case Key::Forwards: - return MTP_inputPrivacyKeyForwards(); - case Key::ProfilePhoto: - return MTP_inputPrivacyKeyProfilePhoto(); - case Key::Voices: - return MTP_inputPrivacyKeyVoiceMessages(); + case Key::AddedByPhone: return MTP_inputPrivacyKeyAddedByPhone(); + case Key::LastSeen: return MTP_inputPrivacyKeyStatusTimestamp(); + case Key::CallsPeer2Peer: return MTP_inputPrivacyKeyPhoneP2P(); + case Key::Forwards: return MTP_inputPrivacyKeyForwards(); + case Key::ProfilePhoto: return MTP_inputPrivacyKeyProfilePhoto(); + case Key::Voices: return MTP_inputPrivacyKeyVoiceMessages(); + case Key::About: return MTP_inputPrivacyKeyAbout(); } Unexpected("Key in Api::UserPrivacy::KetToTL."); } @@ -214,6 +209,8 @@ std::optional TLToKey(mtpTypeId type) { case mtpc_inputPrivacyKeyProfilePhoto: return Key::ProfilePhoto; case mtpc_privacyKeyVoiceMessages: case mtpc_inputPrivacyKeyVoiceMessages: return Key::Voices; + case mtpc_privacyKeyAbout: + case mtpc_inputPrivacyKeyAbout: return Key::About; } return std::nullopt; } diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h index 39d8025b5..905d298e7 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.h +++ b/Telegram/SourceFiles/api/api_user_privacy.h @@ -29,6 +29,7 @@ public: Forwards, ProfilePhoto, Voices, + About, }; enum class Option { Everyone, diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index ce2cbee35..efe928960 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -139,19 +139,19 @@ bool PollData::applyResults(const MTPPollResults &results) { recentVoters, recent->v, ranges::equal_to(), - bareProj, - &MTPlong::v); + &PeerData::id, + peerFromMTP); if (recentChanged) { changed = true; recentVoters = ranges::views::all( recent->v - ) | ranges::views::transform([&](MTPlong userId) { - const auto user = _owner->user(userId.v); - return user->isMinimalLoaded() ? user.get() : nullptr; - }) | ranges::views::filter([](UserData *user) { - return user != nullptr; - }) | ranges::views::transform([](UserData *user) { - return not_null(user); + ) | ranges::views::transform([&](MTPPeer peerId) { + const auto peer = _owner->peer(peerFromMTP(peerId)); + return peer->isMinimalLoaded() ? peer.get() : nullptr; + }) | ranges::views::filter([](PeerData *peer) { + return peer != nullptr; + }) | ranges::views::transform([](PeerData *peer) { + return not_null(peer); }) | ranges::to_vector; } } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 72d2731d6..4f428479b 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -67,7 +67,7 @@ struct PollData { PollId id = 0; QString question; std::vector answers; - std::vector> recentVoters; + std::vector> recentVoters; std::vector sendingVotes; TextWithEntities solution; TimeId closePeriod = 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index b991e911f..c08d8fde5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -182,7 +182,7 @@ struct Poll::CloseInformation { }; struct Poll::RecentVoter { - not_null user; + not_null peer; mutable Ui::PeerUserpicView userpic; }; @@ -487,20 +487,20 @@ void Poll::updateRecentVoters() { _recentVoters, sliced, ranges::equal_to(), - &RecentVoter::user); + &RecentVoter::peer); if (changed) { auto updated = ranges::views::all( sliced - ) | ranges::views::transform([](not_null user) { - return RecentVoter{ user }; + ) | ranges::views::transform([](not_null peer) { + return RecentVoter{ peer }; }) | ranges::to_vector; const auto has = hasHeavyPart(); if (has) { for (auto &voter : updated) { const auto i = ranges::find( _recentVoters, - voter.user, - &RecentVoter::user); + voter.peer, + &RecentVoter::peer); if (i != end(_recentVoters)) { voter.userpic = std::move(i->userpic); } @@ -892,7 +892,7 @@ void Poll::paintRecentVoters( auto created = false; for (auto &recent : _recentVoters) { const auto was = !recent.userpic.null(); - recent.user->paintUserpic(p, recent.userpic, x, y, size); + recent.peer->paintUserpic(p, recent.userpic, x, y, size); if (!was && !recent.userpic.null()) { created = true; } diff --git a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp index c4086ff5a..5fc2308a5 100644 --- a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp +++ b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp @@ -143,12 +143,12 @@ private: QString loadForOffset; int leftToLoad = 0; int fullCount = 0; - std::vector> preloaded; + std::vector> preloaded; bool wasLoading = false; }; - bool appendRow(not_null user); - std::unique_ptr createRow(not_null user) const; + bool appendRow(not_null peer); + std::unique_ptr createRow(not_null peer) const; void addPreloaded(); bool addPreloadedPage(); void preloadedAdded(); @@ -163,7 +163,7 @@ private: QString _offset; mtpRequestId _loadRequestId = 0; QString _loadForOffset; - std::vector> _preloaded; + std::vector> _preloaded; rpl::variable _count = 0; rpl::variable _fullCount; rpl::variable _leftToLoad; @@ -227,16 +227,17 @@ void ListController::loadMoreRows() { _offset = data.vnext_offset().value_or_empty(); auto &owner = session().data(); owner.processUsers(data.vusers()); + owner.processChats(data.vchats()); auto add = limit - kLeavePreloaded; for (const auto &vote : data.vvotes().v) { vote.match([&](const auto &data) { - const auto user = owner.user(data.vuser_id().v); - if (user->isMinimalLoaded()) { + const auto peer = owner.peer(peerFromMTP(data.vpeer())); + if (peer->isMinimalLoaded()) { if (add) { - appendRow(user); + appendRow(peer); --add; } else { - _preloaded.push_back(user); + _preloaded.push_back(peer); } } }); @@ -393,17 +394,17 @@ void ListController::rowClicked(not_null row) { _showPeerInfoRequests.fire(row->peer()); } -bool ListController::appendRow(not_null user) { - if (delegate()->peerListFindRow(user->id.value)) { +bool ListController::appendRow(not_null peer) { + if (delegate()->peerListFindRow(peer->id.value)) { return false; } - delegate()->peerListAppendRow(createRow(user)); + delegate()->peerListAppendRow(createRow(peer)); return true; } std::unique_ptr ListController::createRow( - not_null user) const { - auto row = std::make_unique(user); + not_null peer) const { + auto row = std::make_unique(peer); row->setCustomStatus(QString()); return row; } From 918af601cf92c3931f548c32662f03c210827a1d Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 28 Apr 2023 20:16:22 +0400 Subject: [PATCH 006/260] Correctly handle reactions from channels. --- .../data/data_message_reactions.cpp | 113 ++++++++++++------ .../SourceFiles/data/data_message_reactions.h | 17 +-- Telegram/SourceFiles/history/history_item.cpp | 2 +- 3 files changed, 89 insertions(+), 43 deletions(-) diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index a0834888d..c0e7d9b87 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_account.h" #include "main/main_app_config.h" +#include "main/session/send_as_peers.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_histories.h" @@ -82,6 +83,30 @@ constexpr auto kTopReactionsLimit = 14; : config->get("reactions_user_max_default", 1); } +bool IsMyRecent( + const MTPDmessagePeerReaction &data, + const ReactionId &id, + not_null peer, + const base::flat_map< + ReactionId, + std::vector> &recent, + bool ignoreChosen) { + if (peer->id == peer->session().userPeerId()) { + return true; + } else if (!ignoreChosen) { + return data.is_my(); + } + const auto j = recent.find(id); + if (j == end(recent)) { + return false; + } + const auto k = ranges::find( + j->second, + peer, + &RecentReaction::peer); + return (k != end(j->second)) && k->my; +} + } // namespace PossibleItemReactionsRef LookupPossibleReactions( @@ -953,7 +978,6 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { Expects(!id.empty()); const auto history = _item->history(); - const auto self = history->session().user(); const auto myLimit = SentReactionsLimit(_item); if (ranges::contains(chosen(), id)) { return; @@ -973,7 +997,7 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { _recent.erase(j); } else { j->second.erase( - ranges::remove(j->second, self, &RecentReaction::peer), + ranges::remove(j->second, true, &RecentReaction::my), end(j->second)); if (j->second.empty()) { _recent.erase(j); @@ -982,9 +1006,14 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { } return removed; }), end(_list)); - if (_item->canViewReactions() || history->peer->isUser()) { + const auto peer = history->peer; + if (_item->canViewReactions() || peer->isUser()) { auto &list = _recent[id]; - list.insert(begin(list), RecentReaction{ self }); + const auto from = peer->session().sendAsPeers().resolveChosen(peer); + list.insert(begin(list), RecentReaction{ + .peer = from, + .my = true, + }); } const auto i = ranges::find(_list, id, &MessageReaction::id); if (i != end(_list)) { @@ -1018,13 +1047,16 @@ void MessageReactions::remove(const ReactionId &id) { _list.erase(i); } if (j != end(_recent)) { - j->second.erase( - ranges::remove(j->second, self, &RecentReaction::peer), - end(j->second)); - if (j->second.empty()) { + if (removed) { + j->second.clear(); _recent.erase(j); } else { - Assert(!removed); + j->second.erase( + ranges::remove(j->second, true, &RecentReaction::my), + end(j->second)); + if (j->second.empty()) { + _recent.erase(j); + } } } auto &owner = history->owner(); @@ -1034,7 +1066,8 @@ void MessageReactions::remove(const ReactionId &id) { bool MessageReactions::checkIfChanged( const QVector &list, - const QVector &recent) const { + const QVector &recent, + bool min) const { auto &owner = _item->history()->owner(); if (owner.reactions().sending(_item)) { // We'll apply non-stale data from the request response. @@ -1066,13 +1099,18 @@ bool MessageReactions::checkIfChanged( for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { const auto id = ReactionFromMTP(data.vreaction()); - if (ranges::contains(_list, id, &MessageReaction::id)) { - parsed[id].push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); + if (!ranges::contains(_list, id, &MessageReaction::id)) { + return; } + const auto peerId = peerFromMTP(data.vpeer_id()); + const auto peer = owner.peer(peerId); + const auto my = IsMyRecent(data, id, peer, _recent, min); + parsed[id].push_back({ + .peer = peer, + .unread = data.is_unread(), + .big = data.is_big(), + .my = my, + }); }); } return !ranges::equal(_recent, parsed, []( @@ -1081,7 +1119,7 @@ bool MessageReactions::checkIfChanged( return ranges::equal(a.second, b.second, []( const RecentReaction &a, const RecentReaction &b) { - return (a.peer == b.peer) && (a.big == b.big); + return (a.peer == b.peer) && (a.big == b.big) && (a.my == b.my); }); }); } @@ -1089,7 +1127,7 @@ bool MessageReactions::checkIfChanged( bool MessageReactions::change( const QVector &list, const QVector &recent, - bool ignoreChosen) { + bool min) { auto &owner = _item->history()->owner(); if (owner.reactions().sending(_item)) { // We'll apply non-stale data from the request response. @@ -1102,7 +1140,7 @@ bool MessageReactions::change( count.match([&](const MTPDreactionCount &data) { const auto id = ReactionFromMTP(data.vreaction()); const auto &chosen = data.vchosen_order(); - if (!ignoreChosen && chosen) { + if (!min && chosen) { order[id] = chosen->v; } const auto i = ranges::find(_list, id, &MessageReaction::id); @@ -1112,10 +1150,10 @@ bool MessageReactions::change( _list.push_back({ .id = id, .count = nowCount, - .my = (!ignoreChosen && chosen) + .my = (!min && chosen) }); } else { - const auto nowMy = ignoreChosen ? i->my : chosen.has_value(); + const auto nowMy = min ? i->my : chosen.has_value(); if (i->count != nowCount || i->my != nowMy) { i->count = nowCount; i->my = nowMy; @@ -1125,13 +1163,13 @@ bool MessageReactions::change( existing.emplace(id); }); } - if (!ignoreChosen && !order.empty()) { - const auto min = std::numeric_limits::min(); + if (!min && !order.empty()) { + const auto minimal = std::numeric_limits::min(); const auto proj = [&](const MessageReaction &reaction) { - return reaction.my ? order[reaction.id] : min; + return reaction.my ? order[reaction.id] : minimal; }; const auto correctOrder = [&] { - auto previousOrder = min; + auto previousOrder = minimal; for (const auto &reaction : _list) { const auto nowOrder = proj(reaction); if (nowOrder < previousOrder) { @@ -1156,21 +1194,28 @@ bool MessageReactions::change( } } } + const auto selfId = owner.session().userPeerId(); auto parsed = base::flat_map>(); for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { const auto id = ReactionFromMTP(data.vreaction()); const auto i = ranges::find(_list, id, &MessageReaction::id); - if (i != end(_list)) { - auto &list = parsed[id]; - if (list.size() < i->count) { - list.push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); - } + if (i == end(_list)) { + return; } + auto &list = parsed[id]; + if (list.size() >= i->count) { + return; + } + const auto peerId = peerFromMTP(data.vpeer_id()); + const auto peer = owner.peer(peerId); + const auto my = IsMyRecent(data, id, peer, _recent, min); + list.push_back({ + .peer = peer, + .unread = data.is_unread(), + .big = data.is_big(), + .my = my, + }); }); } if (_recent != parsed) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index c1aeb6f73..61b29107d 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -225,14 +225,14 @@ struct RecentReaction { not_null peer; bool unread = false; bool big = false; + bool my = false; - inline friend constexpr bool operator==( - const RecentReaction &a, - const RecentReaction &b) noexcept { - return (a.peer.get() == b.peer.get()) - && (a.unread == b.unread) - && (a.big == b.big); - } + friend inline auto operator<=>( + const RecentReaction &a, + const RecentReaction &b) = default; + friend inline bool operator==( + const RecentReaction &a, + const RecentReaction &b) = default; }; class MessageReactions final { @@ -247,7 +247,8 @@ public: bool ignoreChosen); [[nodiscard]] bool checkIfChanged( const QVector &list, - const QVector &recent) const; + const QVector &recent, + bool ignoreChosen) const; [[nodiscard]] const std::vector &list() const; [[nodiscard]] auto recent() const -> const base::flat_map> &; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 8cdf3d2c4..b0c2e55f0 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3199,7 +3199,7 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { const auto &recent = data.vrecent_reactions().value_or_empty(); if (min && hasUnreadReaction()) { // We can't update reactions from min if we have unread. - if (_reactions->checkIfChanged(list, recent)) { + if (_reactions->checkIfChanged(list, recent, min)) { updateReactionsUnknown(); } return false; From 429a3da3e58eb83640231c5759a46beec9d6e04a Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 1 May 2023 10:44:57 +0400 Subject: [PATCH 007/260] Update API scheme to layer 160. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/Resources/tl/api.tl | 46 +++++++++++++++++-- Telegram/SourceFiles/api/api_media.cpp | 3 +- Telegram/SourceFiles/api/api_sending.cpp | 2 + Telegram/SourceFiles/api/api_user_privacy.cpp | 4 ++ Telegram/SourceFiles/api/api_user_privacy.h | 1 + .../SourceFiles/boxes/edit_privacy_box.cpp | 7 ++- .../settings/settings_privacy_controllers.cpp | 22 ++++----- .../settings/settings_privacy_security.cpp | 4 ++ .../SourceFiles/storage/localimageloader.cpp | 7 ++- .../storage/serialize_document.cpp | 7 ++- 11 files changed, 83 insertions(+), 21 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 570b7b3ed..8bfb90dff 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -961,6 +961,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_everyone" = "Everybody"; "lng_edit_privacy_contacts" = "My contacts"; +"lng_edit_privacy_close_friends" = "Close friends"; "lng_edit_privacy_nobody" = "Nobody"; "lng_edit_privacy_exceptions" = "Add exceptions"; diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index aa3934e16..4d682710b 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -110,7 +110,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#8f97c628 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector = User; +user#8f97c628 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_available:flags2.3?true stories_unavailable:flags2.4?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -150,7 +150,7 @@ messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_ messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#9cb070d7 flags:# nopremium:flags.3?true spoiler:flags.4?true document:flags.0?Document ttl_seconds:flags.2?int = MessageMedia; +messageMediaDocument#4cf4d72d flags:# nopremium:flags.3?true spoiler:flags.4?true document:flags.0?Document alt_document:flags.5?Document ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; @@ -249,7 +249,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#93eadb53 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper = UserFull; +userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -410,6 +410,8 @@ updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector< updateUser#20529438 user_id:long = Update; updateAutoSaveSettings#ec05b097 = Update; updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; +updateStories#66fad7b5 stories:UserStories = Update; +updateReadStories#feb5345a user_id:long max_id:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -535,6 +537,7 @@ inputPrivacyValueDisallowAll#d66b66c9 = InputPrivacyRule; inputPrivacyValueDisallowUsers#90110467 users:Vector = InputPrivacyRule; inputPrivacyValueAllowChatParticipants#840649cf chats:Vector = InputPrivacyRule; inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector = InputPrivacyRule; +inputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule; privacyValueAllowContacts#fffe1bac = PrivacyRule; privacyValueAllowAll#65427b82 = PrivacyRule; @@ -544,6 +547,7 @@ privacyValueDisallowAll#8b73e763 = PrivacyRule; privacyValueDisallowUsers#e4621141 users:Vector = PrivacyRule; privacyValueAllowChatParticipants#6b134e8e chats:Vector = PrivacyRule; privacyValueDisallowChatParticipants#41c87565 chats:Vector = PrivacyRule; +privacyValueAllowCloseFriends#f7e8d89b = PrivacyRule; account.privacyRules#50a04e45 rules:Vector chats:Vector users:Vector = account.PrivacyRules; @@ -552,7 +556,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; -documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int = DocumentAttribute; +documentAttributeVideo#e9407793 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int preload_prefix_size:flags.2?int = DocumentAttribute; documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; @@ -1547,6 +1551,25 @@ messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = Mess sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; +storyViews#518b47d8 recent_viewers:Vector views_count:int = StoryViews; + +storyItemDeleted#51e6ee4f id:int = StoryItem; +storyItemSkipped#a1d8cf8f id:int date:int = StoryItem; +storyItem#8fcd96ac flags:# pinned:flags.5?true expired:flags.6?true id:int date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; + +userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; + +stories.allStoriesNotModified#47e0a07e state:string = stories.AllStories; +stories.allStories#5b1aa68c flags:# has_more:flags.0?true state:string user_stories:Vector users:Vector = stories.AllStories; + +stories.stories#4fe57df1 count:int stories:Vector users:Vector = stories.Stories; + +storyView#a71aacc2 user_id:long date:int = StoryView; + +stories.storyViewsList#fb3f77ac count:int views:Vector users:Vector = stories.StoryViewsList; + +stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1696,6 +1719,7 @@ contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_hi contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; contacts.exportContactToken#f8654027 = ExportedContactToken; contacts.importContactToken#13005788 token:string = User; +contacts.editCloseFriends#ba6705f0 id:Vector = Bool; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; @@ -2080,4 +2104,16 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -// LAYER 159 +stories.sendStory#8e826f5d flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector = Updates; +stories.deleteStories#b5d501d7 id:Vector = Vector; +stories.editStoryPrivacy#78981875 id:int privacy_rules:Vector = Updates; +stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; +stories.getAllStories#eeb0d625 flags:# next:flags.1?true state:flags.0?string = stories.AllStories; +stories.getUserStories#c946f3c0 flags:# pinned:flags.0?true user_id:InputUser offset_id:int limit:int = stories.Stories; +stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector = stories.Stories; +stories.getExpiredStories#8f792f2 offset_id:int limit:int = stories.Stories; +stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; +stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList; +stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; + +// LAYER 160 diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp index 7dd916140..72af0ba66 100644 --- a/Telegram/SourceFiles/api/api_media.cpp +++ b/Telegram/SourceFiles/api/api_media.cpp @@ -36,7 +36,8 @@ MTPVector ComposeSendingDocumentAttributes( MTP_flags(flags), MTP_int(duration), MTP_int(dimensions.width()), - MTP_int(dimensions.height()))); + MTP_int(dimensions.height()), + MTPint())); // preload_prefix_size } else { attributes.push_back(MTP_documentAttributeImageSize( MTP_int(dimensions.width()), diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index dfba1d930..e8d18c03c 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -453,11 +453,13 @@ void SendConfirmedFile( MTP_flags(Flag::f_document | (file->spoiler ? Flag::f_spoiler : Flag())), file->document, + MTPDocument(), // alt_document MTPint()); } else if (file->type == SendMediaType::Audio) { return MTP_messageMediaDocument( MTP_flags(MTPDmessageMediaDocument::Flag::f_document), file->document, + MTPDocument(), // alt_document MTPint()); } else { Unexpected("Type in sendFilesConfirmed."); diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index 6cd57e2ec..edaae7c97 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -82,6 +82,8 @@ TLInputRules RulesToTL(const UserPrivacy::Rule &rule) { switch (rule.option) { case Option::Everyone: return MTP_inputPrivacyValueAllowAll(); case Option::Contacts: return MTP_inputPrivacyValueAllowContacts(); + case Option::CloseFriends: + return MTP_inputPrivacyValueAllowCloseFriends(); case Option::Nobody: return MTP_inputPrivacyValueDisallowAll(); } Unexpected("Option value in Api::UserPrivacy::RulesToTL."); @@ -110,6 +112,8 @@ UserPrivacy::Rule TLToRules(const TLRules &rules, Data::Session &owner) { setOption(Option::Everyone); }, [&](const MTPDprivacyValueAllowContacts &) { setOption(Option::Contacts); + }, [&](const MTPDprivacyValueAllowCloseFriends &) { + setOption(Option::CloseFriends); }, [&](const MTPDprivacyValueAllowUsers &data) { const auto &users = data.vusers().v; always.reserve(always.size() + users.size()); diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h index 905d298e7..be8e452d9 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.h +++ b/Telegram/SourceFiles/api/api_user_privacy.h @@ -34,6 +34,7 @@ public: enum class Option { Everyone, Contacts, + CloseFriends, Nobody, }; struct Rule { diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index dfa6b3707..b30ecfb0c 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -116,6 +116,8 @@ QString EditPrivacyController::optionLabel(Option option) const { switch (option) { case Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now); case Option::Contacts: return tr::lng_edit_privacy_contacts(tr::now); + case Option::CloseFriends: + return tr::lng_edit_privacy_close_friends(tr::now); case Option::Nobody: return tr::lng_edit_privacy_nobody(tr::now); } Unexpected("Option value in optionsLabelKey."); @@ -182,10 +184,12 @@ bool EditPrivacyBox::showExceptionLink(Exception exception) const { switch (exception) { case Exception::Always: return (_value.option == Option::Contacts) + || (_value.option == Option::CloseFriends) || (_value.option == Option::Nobody); case Exception::Never: return (_value.option == Option::Everyone) - || (_value.option == Option::Contacts); + || (_value.option == Option::Contacts) + || (_value.option == Option::CloseFriends); } Unexpected("Invalid exception value."); } @@ -326,6 +330,7 @@ void EditPrivacyBox::setupContent() { { 0, st::settingsPrivacySkipTop, 0, 0 }); addOptionRow(Option::Everyone); addOptionRow(Option::Contacts); + addOptionRow(Option::CloseFriends); addOptionRow(Option::Nobody); const auto warning = addLabelOrDivider( content, diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 665c44ed4..9468c13f2 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -252,6 +252,7 @@ struct ForwardedTooltip { case Option::Everyone: return tr::lng_edit_privacy_forwards_sample_everyone(tr::now); case Option::Contacts: + case Option::CloseFriends: return tr::lng_edit_privacy_forwards_sample_contacts(tr::now); case Option::Nobody: return tr::lng_edit_privacy_forwards_sample_nobody(tr::now); @@ -783,15 +784,14 @@ rpl::producer CallsPeer2PeerPrivacyController::optionsTitleKey() const QString CallsPeer2PeerPrivacyController::optionLabel( EditPrivacyBox::Option option) const { switch (option) { - case Option::Everyone: { + case Option::Everyone: return tr::lng_edit_privacy_calls_p2p_everyone(tr::now); - }; - case Option::Contacts: { + case Option::Contacts: return tr::lng_edit_privacy_calls_p2p_contacts(tr::now); - }; - case Option::Nobody: { + case Option::CloseFriends: + return tr::lng_edit_privacy_close_friends(tr::now); // unused + case Option::Nobody: return tr::lng_edit_privacy_calls_p2p_nobody(tr::now); - }; } Unexpected("Option value in optionsLabelKey."); } @@ -1215,15 +1215,13 @@ auto ProfilePhotoPrivacyController::exceptionsDescription() const return _option.value( ) | rpl::map([](Option option) { switch (option) { - case Option::Everyone: { + case Option::Everyone: return tr::lng_edit_privacy_forwards_exceptions_everyone(); - }; - case Option::Contacts: { + case Option::Contacts: + case Option::CloseFriends: return tr::lng_edit_privacy_forwards_exceptions(); - }; - case Option::Nobody: { + case Option::Nobody: return tr::lng_edit_privacy_forwards_exceptions_nobody(); - }; } Unexpected("Option value in exceptionsDescription."); }) | rpl::flatten_latest(); diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index d9fff1232..164bb4443 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -80,6 +80,8 @@ QString PrivacyBase(Privacy::Key key, Privacy::Option option) { return tr::lng_edit_privacy_calls_p2p_everyone(tr::now); case Option::Contacts: return tr::lng_edit_privacy_calls_p2p_contacts(tr::now); + case Option::CloseFriends: + return tr::lng_edit_privacy_close_friends(tr::now); // unused case Option::Nobody: return tr::lng_edit_privacy_calls_p2p_nobody(tr::now); } @@ -88,6 +90,8 @@ QString PrivacyBase(Privacy::Key key, Privacy::Option option) { switch (option) { case Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now); case Option::Contacts: return tr::lng_edit_privacy_contacts(tr::now); + case Option::CloseFriends: + return tr::lng_edit_privacy_close_friends(tr::now); case Option::Nobody: return tr::lng_edit_privacy_nobody(tr::now); } Unexpected("Value in Privacy::Option."); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 3f39944fa..1e9d03bdb 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -918,7 +918,12 @@ void FileLoadTask::process(Args &&args) { if (video->supportsStreaming) { flags |= MTPDdocumentAttributeVideo::Flag::f_supports_streaming; } - attributes.push_back(MTP_documentAttributeVideo(MTP_flags(flags), MTP_int(video->duration), MTP_int(coverWidth), MTP_int(coverHeight))); + attributes.push_back(MTP_documentAttributeVideo( + MTP_flags(flags), + MTP_int(video->duration), + MTP_int(coverWidth), + MTP_int(coverHeight), + MTPint())); // preload_prefix_size if (args.generateGoodThumbnail) { goodThumbnail = video->thumbnail; diff --git a/Telegram/SourceFiles/storage/serialize_document.cpp b/Telegram/SourceFiles/storage/serialize_document.cpp index a44207a52..04d8936e7 100644 --- a/Telegram/SourceFiles/storage/serialize_document.cpp +++ b/Telegram/SourceFiles/storage/serialize_document.cpp @@ -192,7 +192,12 @@ DocumentData *Document::readFromStreamHelper( if (type == RoundVideoDocument) { flags |= MTPDdocumentAttributeVideo::Flag::f_round_message; } - attributes.push_back(MTP_documentAttributeVideo(MTP_flags(flags), MTP_int(duration), MTP_int(width), MTP_int(height))); + attributes.push_back(MTP_documentAttributeVideo( + MTP_flags(flags), + MTP_int(duration), + MTP_int(width), + MTP_int(height), + MTPint())); // preload_prefix_size } else { attributes.push_back(MTP_documentAttributeImageSize(MTP_int(width), MTP_int(height))); } From 89ca38ed29e5e69b53f893c9cf8be42040898101 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 3 May 2023 22:30:37 +0400 Subject: [PATCH 008/260] Start stories viewer with ComposeControls. --- Telegram/CMakeLists.txt | 16 +- Telegram/SourceFiles/api/api_updates.cpp | 7 +- .../chat_helpers/compose/compose_show.h | 2 +- Telegram/SourceFiles/data/data_session.cpp | 6 +- Telegram/SourceFiles/data/data_session.h | 5 + Telegram/SourceFiles/data/data_stories.cpp | 246 ++++++++++++++++++ Telegram/SourceFiles/data/data_stories.h | 74 ++++++ Telegram/SourceFiles/data/data_types.h | 2 + .../history_view_compose_controls.cpp | 10 +- .../controls/history_view_compose_controls.h | 2 +- .../media/stories/media_stories_delegate.cpp | 9 + .../media/stories/media_stories_delegate.h | 30 +++ .../media/stories/media_stories_header.cpp | 65 +++++ .../media/stories/media_stories_header.h | 43 +++ .../media/stories/media_stories_reply.cpp | 50 ++++ .../media/stories/media_stories_reply.h | 39 +++ .../media/stories/media_stories_slider.cpp | 18 ++ .../media/stories/media_stories_slider.h | 20 ++ .../media/stories/media_stories_view.cpp | 37 +++ .../media/stories/media_stories_view.h | 40 +++ .../media/view/media_view_overlay_widget.cpp | 142 +++++++++- .../media/view/media_view_overlay_widget.h | 37 ++- .../window/window_session_controller.cpp | 4 +- 23 files changed, 884 insertions(+), 20 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_stories.cpp create mode 100644 Telegram/SourceFiles/data/data_stories.h create mode 100644 Telegram/SourceFiles/media/stories/media_stories_delegate.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_delegate.h create mode 100644 Telegram/SourceFiles/media/stories/media_stories_header.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_header.h create mode 100644 Telegram/SourceFiles/media/stories/media_stories_reply.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_reply.h create mode 100644 Telegram/SourceFiles/media/stories/media_stories_slider.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_slider.h create mode 100644 Telegram/SourceFiles/media/stories/media_stories_view.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_view.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 991101b61..4b8b8b331 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -548,6 +548,8 @@ PRIVATE data/data_sparse_ids.h data/data_sponsored_messages.cpp data/data_sponsored_messages.h + data/data_stories.cpp + data/data_stories.h data/data_streaming.cpp data/data_streaming.h data/data_thread.cpp @@ -936,8 +938,6 @@ PRIVATE main/session/send_as_peers.h main/session/session_show.cpp main/session/session_show.h - media/system_media_controls_manager.h - media/system_media_controls_manager.cpp media/audio/media_audio.cpp media/audio/media_audio.h media/audio/media_audio_capture.cpp @@ -962,6 +962,16 @@ PRIVATE media/player/media_player_volume_controller.h media/player/media_player_widget.cpp media/player/media_player_widget.h + media/stories/media_stories_delegate.cpp + media/stories/media_stories_delegate.h + media/stories/media_stories_header.cpp + media/stories/media_stories_header.h + media/stories/media_stories_reply.cpp + media/stories/media_stories_reply.h + media/stories/media_stories_slider.cpp + media/stories/media_stories_slider.h + media/stories/media_stories_view.cpp + media/stories/media_stories_view.h media/streaming/media_streaming_audio_track.cpp media/streaming/media_streaming_audio_track.h media/streaming/media_streaming_common.h @@ -1007,6 +1017,8 @@ PRIVATE 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 menu/menu_antispam_validator.h menu/menu_item_download_files.cpp diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 4a8ce5173..62bf94ba7 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_scheduled_messages.h" #include "data/data_send_action.h" +#include "data/data_stories.h" #include "data/data_message_reactions.h" #include "inline_bots/bot_attach_web_view.h" #include "chat_helpers/emoji_interactions.h" @@ -2517,7 +2518,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updateTranscribedAudio: { const auto &data = update.c_updateTranscribedAudio(); _session->api().transcribes().apply(data); - } + } break; + + case mtpc_updateStories: { + _session->data().stories().apply(update.c_updateStories()); + } break; } } diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h index ba2f391df..a1ad76e6c 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h @@ -59,7 +59,7 @@ public: Data::FileOrigin origin, not_null photo) const = 0; - virtual void processChosenSticker(FileChosen chosen) const = 0; + virtual void processChosenSticker(FileChosen &&chosen) const = 0; [[nodiscard]] virtual Window::SessionController *resolveWindow( WindowUsage) const; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index bb1fc659f..42a8644d1 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_emoji_statuses.h" #include "data/data_forum_icons.h" #include "data/data_cloud_themes.h" +#include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_media_rotation.h" #include "data/data_histories.h" @@ -266,7 +267,8 @@ Session::Session(not_null session) , _emojiStatuses(std::make_unique(this)) , _forumIcons(std::make_unique(this)) , _notifySettings(std::make_unique(this)) -, _customEmojiManager(std::make_unique(this)) { +, _customEmojiManager(std::make_unique(this)) +, _stories(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -311,6 +313,8 @@ Session::Session(not_null session) } } }, _lifetime); + + _stories->loadMore(); }); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 16c56c23e..154b1b73f 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -63,6 +63,7 @@ class Stickers; class GroupCall; class NotifySettings; class CustomEmojiManager; +class Stories; struct RepliesReadTillUpdate { FullMsgId id; @@ -136,6 +137,9 @@ public: [[nodiscard]] CustomEmojiManager &customEmojiManager() const { return *_customEmojiManager; } + [[nodiscard]] Stories &stories() const { + return *_stories; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -1007,6 +1011,7 @@ private: const std::unique_ptr _forumIcons; const std::unique_ptr _notifySettings; const std::unique_ptr _customEmojiManager; + const std::unique_ptr _stories; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp new file mode 100644 index 000000000..0a0fe8b02 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -0,0 +1,246 @@ +/* +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 "data/data_stories.h" + +#include "api/api_text_entities.h" +#include "data/data_document.h" +#include "data/data_photo.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "apiwrap.h" + +// #TODO stories testing +#include "data/data_user.h" +#include "history/history.h" +#include "history/history_item.h" +#include "storage/storage_shared_media.h" + +namespace Data { +namespace { + +} // namespace + +bool StoriesList::unread() const { + return !items.empty() && readTill < items.front().id; +} + +Stories::Stories(not_null owner) : _owner(owner) { +} + +Stories::~Stories() { +} + +Session &Stories::owner() const { + return *_owner; +} + +void Stories::apply(const MTPDupdateStories &data) { + pushToFront(parse(data.vstories())); +} + +StoriesList Stories::parse(const MTPUserStories &stories) { + const auto &data = stories.data(); + const auto userId = UserId(data.vuser_id()); + const auto readTill = data.vmax_read_id().value_or_empty(); + const auto count = int(data.vstories().v.size()); + auto result = StoriesList{ + .user = _owner->user(userId), + .readTill = readTill, + .total = count, + }; + const auto &list = data.vstories().v; + result.items.reserve(list.size()); + for (const auto &story : list) { + story.match([&](const MTPDstoryItem &data) { + if (auto entry = parse(data)) { + result.items.push_back(std::move(*entry)); + } else { + --result.total; + } + }, [&](const MTPDstoryItemSkipped &) { + }, [&](const MTPDstoryItemDeleted &) { + --result.total; + }); + } + result.total = std::min(result.total, int(result.items.size())); + return result; +} + +std::optional Stories::parse(const MTPDstoryItem &data) { + const auto id = data.vid().v; + using MaybeMedia = std::optional< + std::variant, not_null>>; + const auto media = data.vmedia().match([&]( + const MTPDmessageMediaPhoto &data) -> MaybeMedia { + if (const auto photo = data.vphoto()) { + const auto result = _owner->processPhoto(*photo); + if (!result->isNull()) { + return result; + } + } + return {}; + }, [&](const MTPDmessageMediaDocument &data) -> MaybeMedia { + if (const auto document = data.vdocument()) { + const auto result = _owner->processDocument(*document); + if (!result->isNull() + && (result->isGifv() || result->isVideoFile())) { + return result; + } + } + return {}; + }, [](const auto &) { return MaybeMedia(); }); + if (!media) { + return {}; + } + auto caption = TextWithEntities{ + data.vcaption().value_or_empty(), + Api::EntitiesFromMTP( + &_owner->session(), + data.ventities().value_or_empty()), + }; + auto privacy = StoryPrivacy(); + + const auto date = data.vdate().v; + return StoryItem{ + .id = data.vid().v, + .media = *media, + .caption = std::move(caption), + .date = date, + .privacy = privacy, + }; +} + +void Stories::loadMore() { + if (_loadMoreRequestId || _allLoaded) { + return; + } + const auto api = &_owner->session().api(); + using Flag = MTPstories_GetAllStories::Flag; + _loadMoreRequestId = api->request(MTPstories_GetAllStories( + MTP_flags(_state.isEmpty() ? Flag(0) : Flag::f_next), + MTP_string(_state) + )).done([=](const MTPstories_AllStories &result) { + _loadMoreRequestId = 0; + + result.match([&](const MTPDstories_allStories &data) { + _owner->processUsers(data.vusers()); + _state = qs(data.vstate()); + _allLoaded = !data.is_has_more(); + for (const auto &single : data.vuser_stories().v) { + pushToBack(parse(single)); + } + }, [](const MTPDstories_allStoriesNotModified &) { + }); + }).fail([=] { + _loadMoreRequestId = 0; + }).send(); +} + +const std::vector &Stories::all() { + return _all; +} + +bool Stories::allLoaded() const { + return _allLoaded; +} + +// #TODO stories testing +StoryId Stories::generate( + not_null item, + std::variant< + v::null_t, + not_null, + not_null> media) { + if (v::is_null(media) + || !item->from()->isUser() + || !item->isRegular()) { + return {}; + } + const auto document = v::is>(media) + ? v::get>(media).get() + : nullptr; + if (document && !document->isVideoFile()) { + return {}; + } + using namespace Storage; + auto resultId = StoryId(); + const auto listType = SharedMediaType::PhotoVideo; + const auto itemId = item->id; + const auto peer = item->history()->peer; + const auto session = &peer->session(); + auto stories = StoriesList{ .user = item->from()->asUser() }; + const auto lifetime = session->storage().query(SharedMediaQuery( + SharedMediaKey(peer->id, MsgId(0), listType, itemId), + 32, + 32 + )) | rpl::start_with_next([&](SharedMediaResult &&result) { + stories.total = result.count.value_or(1); + if (!result.messageIds.contains(itemId)) { + result.messageIds.emplace(itemId); + } + stories.items.reserve(result.messageIds.size()); + auto index = StoryId(); + const auto owner = &peer->owner(); + for (const auto id : result.messageIds) { + if (const auto item = owner->message(peer, id)) { + if (id == itemId) { + resultId = ++index; + stories.items.push_back({ + .id = resultId, + .media = (document + ? StoryMedia{ not_null(document) } + : StoryMedia{ v::get>(media) }), + .caption = item->originalText(), + .date = item->date(), + }); + } else if (const auto media = item->media()) { + const auto photo = media->photo(); + const auto document = media->document(); + if (photo || (document && document->isVideoFile())) { + stories.items.push_back({ + .id = ++index, + .media = (document + ? StoryMedia{ not_null(document) } + : StoryMedia{ not_null(photo) }), + .caption = item->originalText(), + .date = item->date(), + }); + } + } + } + } + const auto i = ranges::find(_all, stories.user, &StoriesList::user); + if (i != end(_all)) { + *i = std::move(stories); + } else { + _all.push_back(std::move(stories)); + } + }); + return resultId; +} + +void Stories::pushToBack(StoriesList &&list) { + const auto i = ranges::find(_all, list.user, &StoriesList::user); + if (i != end(_all)) { + *i = std::move(list); + } else { + _all.push_back(std::move(list)); + } +} + +void Stories::pushToFront(StoriesList &&list) { + const auto i = ranges::find(_all, list.user, &StoriesList::user); + if (i != end(_all)) { + *i = std::move(list); + ranges::rotate(begin(_all), i, i + 1); + } else { + _all.insert(begin(_all), std::move(list)); + } +} + +} // namespace Data \ No newline at end of file diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h new file mode 100644 index 000000000..ea3a033f1 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories.h @@ -0,0 +1,74 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class PhotoData; +class DocumentData; + +namespace Data { + +class Session; + +struct StoryPrivacy { +}; + +struct StoryMedia { + std::variant, not_null> data; +}; + +struct StoryItem { + StoryId id = 0; + StoryMedia media; + TextWithEntities caption; + TimeId date = 0; + StoryPrivacy privacy; +}; + +struct StoriesList { + not_null user; + std::vector items; + int total = 0; +}; + +class Stories final { +public: + explicit Stories(not_null owner); + ~Stories(); + + void loadMore(); + void apply(const MTPDupdateStories &data); + + [[nodiscard]] const std::vector &all(); + [[nodiscard]] bool allLoaded() const; + + // #TODO stories testing + [[nodiscard]] StoryId generate( + not_null item, + std::variant< + v::null_t, + not_null, + not_null> media); + +private: + [[nodiscard]] StoriesList parse(const MTPUserStories &data); + [[nodiscard]] std::optional parse(const MTPDstoryItem &data); + + void pushToBack(StoriesList &&list); + void pushToFront(StoriesList &&list); + + const not_null _owner; + + std::vector _all; + QString _state; + bool _allLoaded = false; + + mtpRequestId _loadMoreRequestId = 0; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index e07df91e3..8f6ee551b 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -135,6 +135,8 @@ using PollId = uint64; using WallPaperId = uint64; using CallId = uint64; using BotAppId = uint64; +using StoryId = int32; + constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); struct PreparedPhotoThumb { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 4146e8eb8..75a46de3e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -2538,11 +2538,15 @@ bool ComposeControls::returnTabbedSelector() { } void ComposeControls::createTabbedPanel() { - auto descriptor = ChatHelpers::TabbedPanelDescriptor{ + using namespace ChatHelpers; + auto descriptor = TabbedPanelDescriptor{ .regularWindow = _regularWindow, - .nonOwnedSelector = _selector, + .ownedSelector = (_ownedSelector + ? object_ptr::fromRaw(_ownedSelector.release()) + : object_ptr(nullptr)), + .nonOwnedSelector = _ownedSelector ? nullptr : _selector.get(), }; - setTabbedPanel(std::make_unique( + setTabbedPanel(std::make_unique( _parent, std::move(descriptor))); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index c09b938d6..a24b5940a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -315,7 +315,7 @@ private: const not_null _session; Window::SessionController * const _regularWindow = nullptr; - const std::unique_ptr _ownedSelector; + std::unique_ptr _ownedSelector; const not_null _selector; rpl::event_stream _stickerOrEmojiChosen; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.cpp b/Telegram/SourceFiles/media/stories/media_stories_delegate.cpp new file mode 100644 index 000000000..721d9803c --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.cpp @@ -0,0 +1,9 @@ +/* +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/stories/media_stories_delegate.h" + diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h new file mode 100644 index 000000000..fd00b7cf9 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -0,0 +1,30 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace ChatHelpers { +class Show; +struct FileChosen; +} // namespace ChatHelpers + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Media::Stories { + +class Delegate { +public: + [[nodiscard]] virtual not_null storiesWrap() = 0; + [[nodiscard]] virtual auto storiesShow() + -> std::shared_ptr = 0; + [[nodiscard]] virtual auto storiesStickerOrEmojiChosen() + -> rpl::producer = 0; +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp new file mode 100644 index 000000000..08e81fccc --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -0,0 +1,65 @@ +/* +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/stories/media_stories_header.h" + +#include "base/unixtime.h" +#include "data/data_user.h" +#include "media/stories/media_stories_delegate.h" +#include "ui/controls/userpic_button.h" +#include "ui/text/format_values.h" +#include "ui/widgets/labels.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "styles/style_boxes.h" // defaultUserpicButton. + +namespace Media::Stories { + +Header::Header(not_null delegate) +: _delegate(delegate) { +} + +Header::~Header() { +} + +void Header::show(HeaderData data) { + if (_data == data) { + return; + } + const auto userChanged = (!_data || _data->user != data.user); + _data = data; + if (userChanged) { + _date = nullptr; + const auto parent = _delegate->storiesWrap(); + auto widget = std::make_unique(parent); + const auto raw = widget.get(); + parent->sizeValue() | rpl::start_with_next([=](QSize size) { + raw->setGeometry(50, 50, 600, 100); + }, raw->lifetime()); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto userpic = Ui::CreateChild( + raw, + data.user, + st::defaultUserpicButton); + userpic->move(0, 0); + const auto name = Ui::CreateChild( + raw, + data.user->firstName, + st::defaultFlatLabel); + name->move(100, 0); + raw->show(); + _widget = std::move(widget); + } + _date = std::make_unique( + _widget.get(), + Ui::FormatDateTime(base::unixtime::parse(data.date)), + st::defaultFlatLabel); + _date->move(100, 50); + _date->show(); +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h new file mode 100644 index 000000000..8df09a64f --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_header.h @@ -0,0 +1,43 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/userpic_view.h" + +namespace Ui { +class RpWidget; +class FlatLabel; +} // namespace Ui + +namespace Media::Stories { + +class Delegate; + +struct HeaderData { + not_null user; + TimeId date = 0; + + friend inline auto operator<=>(HeaderData, HeaderData) = default; +}; + +class Header final { +public: + explicit Header(not_null delegate); + ~Header(); + + void show(HeaderData data); + +private: + const not_null _delegate; + std::unique_ptr _widget; + std::unique_ptr _date; + std::optional _data; + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp new file mode 100644 index 000000000..cf2226bd1 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -0,0 +1,50 @@ +/* +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/stories/media_stories_reply.h" + +#include "chat_helpers/compose/compose_show.h" +#include "chat_helpers/tabbed_selector.h" +#include "history/view/controls/history_view_compose_controls.h" +#include "media/stories/media_stories_delegate.h" +#include "menu/menu_send.h" + +namespace Media::Stories { + +ReplyArea::ReplyArea(not_null delegate) +: _delegate(delegate) +, _controls(std::make_unique( + _delegate->storiesWrap(), + HistoryView::ComposeControlsDescriptor{ + .show = _delegate->storiesShow(), + .unavailableEmojiPasted = [=](not_null emoji) { + showPremiumToast(emoji); + }, + .mode = HistoryView::ComposeControlsMode::Normal, + .sendMenuType = SendMenu::Type::SilentOnly, + .stickerOrEmojiChosen = _delegate->storiesStickerOrEmojiChosen(), + } +)) { + _delegate->storiesWrap()->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + _controls->resizeToWidth(size.width() - 200); + _controls->move(100, size.height() - _controls->heightCurrent() - 20); + _controls->setAutocompleteBoundingRect({ QPoint() ,size }); + }, _lifetime); + + _controls->show(); + _controls->showFinished(); +} + +ReplyArea::~ReplyArea() { +} + +void ReplyArea::showPremiumToast(not_null emoji) { + // #TODO stories +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h new file mode 100644 index 000000000..4d13cff79 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -0,0 +1,39 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace HistoryView { +class ComposeControls; +} // namespace HistoryView + +namespace Media::Stories { + +class Delegate; + +struct ReplyAreaData { + not_null user; + + friend inline auto operator<=>(ReplyAreaData, ReplyAreaData) = default; +}; + +class ReplyArea final { +public: + explicit ReplyArea(not_null delegate); + ~ReplyArea(); + +private: + void showPremiumToast(not_null emoji); + + const not_null _delegate; + const std::unique_ptr _controls; + + rpl::lifetime _lifetime; + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp new file mode 100644 index 000000000..cc64c952e --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp @@ -0,0 +1,18 @@ +/* +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/stories/media_stories_slider.h" + +namespace Media::Stories { + +Slider::Slider() { +} + +Slider::~Slider() { +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.h b/Telegram/SourceFiles/media/stories/media_stories_slider.h new file mode 100644 index 000000000..bb908a9e2 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.h @@ -0,0 +1,20 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Media::Stories { + +class Slider final { +public: + Slider(); + ~Slider(); + + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp new file mode 100644 index 000000000..131a4d8d2 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -0,0 +1,37 @@ +/* +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/stories/media_stories_view.h" + +#include "media/stories/media_stories_delegate.h" +#include "media/stories/media_stories_header.h" +#include "media/stories/media_stories_slider.h" +#include "media/stories/media_stories_reply.h" + +namespace Media::Stories { + +View::View(not_null delegate) +: _delegate(delegate) +, _wrap(_delegate->storiesWrap()) +, _header(std::make_unique
(_delegate)) +, _slider(std::make_unique()) +, _replyArea(std::make_unique(_delegate)) { +} + +View::~View() = default; + +void View::show(const Data::StoriesList &list, int index) { + Expects(index < list.items.size()); + + const auto &item = list.items[index]; + _header->show({ + .user = list.user, + .date = item.date, + }); +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h new file mode 100644 index 000000000..884d44219 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -0,0 +1,40 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_stories.h" + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Media::Stories { + +class Header; +class Slider; +class ReplyArea; +class Delegate; + +class View final { +public: + explicit View(not_null delegate); + ~View(); + + void show(const Data::StoriesList &list, int index); + +private: + const not_null _delegate; + const not_null _wrap; + + std::unique_ptr
_header; + std::unique_ptr _slider; + std::unique_ptr _replyArea; + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index f19623e48..cfedf5ef8 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "ui/widgets/buttons.h" #include "ui/image/image.h" +#include "ui/layers/layer_manager.h" #include "ui/text/text_utilities.h" #include "ui/platform/ui_platform_utility.h" #include "ui/platform/ui_platform_window_title.h" @@ -45,6 +46,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_view.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/player/media_player_instance.h" @@ -54,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media.h" #include "data/data_media_types.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -331,6 +334,7 @@ OverlayWidget::OverlayWidget() , _widget(_surface->rpWidget()) , _fullscreen(Core::App().settings().mediaViewPosition().maximized == 2) , _windowed(Core::App().settings().mediaViewPosition().maximized == 0) +, _layerBg(std::make_unique(_body)) , _docDownload(_body, tr::lng_media_download(tr::now), st::mediaviewFileLink) , _docSaveAs(_body, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink) , _docCancel(_body, tr::lng_cancel(tr::now), st::mediaviewFileLink) @@ -2870,6 +2874,14 @@ void OverlayWidget::show(OpenRequest request) { } setSession(&photo->session()); + // #TODO stories testing + if (const auto storyId = (!contextPeer && contextItem) + ? contextItem->history()->owner().stories().generate( + contextItem, + photo) + : StoryId()) { + setContext(StoriesContext{ contextItem->from()->asUser(), storyId }); + } else if (contextPeer) { setContext(contextPeer); } else if (contextItem) { @@ -2888,6 +2900,14 @@ void OverlayWidget::show(OpenRequest request) { } else if (document) { setSession(&document->session()); + // #TODO stories testing + if (const auto storyId = contextItem + ? contextItem->history()->owner().stories().generate( + contextItem, + document) + : StoryId()) { + setContext(StoriesContext{ contextItem->from()->asUser(), storyId }); + } else if (contextItem) { setContext(ItemContext{ contextItem, contextTopicRootId }); } else { @@ -3808,6 +3828,91 @@ void OverlayWidget::switchToPip() { } } +not_null OverlayWidget::storiesWrap() { + return _body; +} + +std::shared_ptr OverlayWidget::storiesShow() { + class Show final : public ChatHelpers::Show { + public: + explicit Show(not_null widget) : _widget(widget) { + } + + void showBox( + object_ptr content, + Ui::LayerOptions options + = Ui::LayerOption::KeepOther) const override { + _widget->_layerBg->showBox( + std::move(content), + options, + anim::type::normal); + } + void hideLayer() const override { + _widget->_layerBg->hideAll(anim::type::normal); + } + not_null toastParent() const override { + return _widget->_body; + } + bool valid() const override { + return _widget->_storiesUser != nullptr; + } + operator bool() const override { + return valid(); + } + + Main::Session &session() const override { + return _widget->_storiesUser->session(); + } + bool paused(ChatHelpers::PauseReason reason) const override { + if (_widget->isHidden() + || (!_widget->_fullscreen + && !_widget->_window->isActiveWindow())) { + return true; + } else if (reason < ChatHelpers::PauseReason::Layer + && _widget->_layerBg->topShownLayer() != nullptr) { + return true; + } + return false; + } + rpl::producer<> pauseChanged() const override { + return rpl::never<>(); + } + + rpl::producer adjustShadowLeft() const override { + return rpl::single(false); + } + SendMenu::Type sendMenuType() const override { + return SendMenu::Type::SilentOnly; + } + + bool showMediaPreview( + Data::FileOrigin origin, + not_null document) const override { + return false; // #TODO stories + } + bool showMediaPreview( + Data::FileOrigin origin, + not_null photo) const override { + return false; // #TODO stories + } + + void processChosenSticker( + ChatHelpers::FileChosen &&chosen) const override { + _widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen)); + } + + private: + not_null _widget; + + }; + return std::make_shared(this); +} + +auto OverlayWidget::storiesStickerOrEmojiChosen() +-> rpl::producer { + return _storiesStickerOrEmojiChosen.events(); +} + void OverlayWidget::playbackToggleFullScreen() { Expects(_streamed != nullptr); @@ -4619,22 +4724,49 @@ void OverlayWidget::setContext( std::variant< v::null_t, ItemContext, - not_null> context) { + not_null, + StoriesContext> context) { if (const auto item = std::get_if(&context)) { _message = item->item; _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); + _stories = nullptr; + _storiesUser = nullptr; } else if (const auto peer = std::get_if>(&context)) { _peer = *peer; _history = _peer->owner().history(_peer); _message = nullptr; _topicRootId = MsgId(); + _stories = nullptr; + _storiesUser = nullptr; + } else if (const auto story = std::get_if(&context)) { + _message = nullptr; + _topicRootId = MsgId(); + _history = nullptr; + _peer = nullptr; + const auto &all = story->user->owner().stories().all(); + const auto i = ranges::find( + all, + story->user, + &Data::StoriesList::user); + Assert(i != end(all)); + const auto j = ranges::find( + i->items, + story->id, + &Data::StoryItem::id); + _storiesUser = story->user; + if (!_stories) { + _stories = std::make_unique( + static_cast(this)); + } + _stories->show(*i, j - begin(i->items)); } else { _message = nullptr; _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; + _stories = nullptr; } _migrated = nullptr; if (_history) { @@ -4704,6 +4836,14 @@ bool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) { if (v::is_null(entity.data) && !entity.item) { return false; } + // #TODO stories testing + if (const auto storyId = entity.item + ? entity.item->history()->owner().stories().generate( + entity.item, + entity.data) + : StoryId()) { + setContext(StoriesContext{ entity.item->from()->asUser(), storyId }); + } else if (const auto item = entity.item) { setContext(ItemContext{ item, entity.topicRootId }); } else if (_peer) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index b47a8de03..4d6d6d3f2 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_cloud_themes.h" // Data::CloudTheme. #include "media/view/media_view_playback_controls.h" #include "media/view/media_view_open_common.h" +#include "media/stories/media_stories_delegate.h" class History; @@ -32,6 +33,7 @@ class PopupMenu; class LinkButton; class RoundButton; class RpWindow; +class LayerManager; } // namespace Ui namespace Ui::GL { @@ -50,17 +52,20 @@ struct Preview; } // namespace Theme } // namespace Window -namespace Media { -namespace Player { +namespace Media::Player { struct TrackState; -} // namespace Player -namespace Streaming { +} // namespace Media::Player + +namespace Media::Streaming { struct Information; struct Update; struct FrameWithInfo; enum class Error; -} // namespace Streaming -} // namespace Media +} // namespace Media::Streaming + +namespace Media::Stories { +class View; +} // namespace Media::Stories namespace Media::View { @@ -69,7 +74,8 @@ class Pip; class OverlayWidget final : public ClickHandlerHost - , private PlaybackControls::Delegate { + , private PlaybackControls::Delegate + , private Stories::Delegate { public: OverlayWidget(); ~OverlayWidget(); @@ -217,6 +223,11 @@ private: void playbackPauseMusic(); void switchToPip(); + not_null storiesWrap() override; + std::shared_ptr storiesShow() override; + auto storiesStickerOrEmojiChosen() + -> rpl::producer override; + void hideControls(bool force = false); void subscribeToScreenGeometry(); @@ -268,10 +279,15 @@ private: not_null item; MsgId topicRootId = 0; }; + struct StoriesContext { + not_null user; + StoryId id = 0; + }; void setContext(std::variant< v::null_t, ItemContext, - not_null> context); + not_null, + StoriesContext> context); void refreshLang(); void showSaveMsgFile(); @@ -551,6 +567,11 @@ private: int _streamedCreated = 0; bool _showAsPip = false; + std::unique_ptr _stories; + UserData *_storiesUser = nullptr; + rpl::event_stream _storiesStickerOrEmojiChosen; + std::unique_ptr _layerBg; + const style::icon *_docIcon = nullptr; style::color _docIconColor; QString _docName, _docSize, _docExt; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 601463962..8ec809049 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -140,7 +140,7 @@ public: not_null photo) const override; void processChosenSticker( - ChatHelpers::FileChosen chosen) const override; + ChatHelpers::FileChosen &&chosen) const override; private: const base::weak_ptr _window; @@ -233,7 +233,7 @@ bool MainWindowShow::showMediaPreview( } void MainWindowShow::processChosenSticker( - ChatHelpers::FileChosen chosen) const { + ChatHelpers::FileChosen &&chosen) const { if (const auto window = _window.get()) { Ui::PostponeCall(window, [=, chosen = std::move(chosen)]() mutable { window->stickerOrEmojiChosen(std::move(chosen)); From 027bd89e5bf201292c98d0e1859d30ff170f98d7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 5 May 2023 14:40:51 +0400 Subject: [PATCH 009/260] Apply geometry constraints in stories viewer. --- Telegram/CMakeLists.txt | 2 + .../history_view_compose_controls.cpp | 58 ++++--- .../controls/history_view_compose_controls.h | 5 +- .../stories/media_stories_controller.cpp | 155 ++++++++++++++++++ .../media/stories/media_stories_controller.h | 91 ++++++++++ .../media/stories/media_stories_header.cpp | 33 ++-- .../media/stories/media_stories_header.h | 8 +- .../media/stories/media_stories_reply.cpp | 144 ++++++++++++++-- .../media/stories/media_stories_reply.h | 36 +++- .../media/stories/media_stories_slider.cpp | 58 ++++++- .../media/stories/media_stories_slider.h | 24 ++- .../media/stories/media_stories_view.cpp | 17 +- .../media/stories/media_stories_view.h | 20 +-- .../SourceFiles/media/view/media_view.style | 39 +++++ .../media/view/media_view_overlay_widget.cpp | 8 + 15 files changed, 607 insertions(+), 91 deletions(-) create mode 100644 Telegram/SourceFiles/media/stories/media_stories_controller.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_controller.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 4b8b8b331..1a76befc0 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -962,6 +962,8 @@ PRIVATE media/player/media_player_volume_controller.h media/player/media_player_widget.cpp media/player/media_player_widget.h + media/stories/media_stories_controller.cpp + media/stories/media_stories_controller.h media/stories/media_stories_delegate.cpp media/stories/media_stories_delegate.h media/stories/media_stories_header.cpp diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 75a46de3e..6c0f635e8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -355,6 +355,7 @@ public: rpl::producer title, rpl::producer description, rpl::producer page); + void previewUnregister(); [[nodiscard]] bool isDisplayed() const; [[nodiscard]] bool isEditingMessage() const; @@ -413,6 +414,7 @@ private: rpl::event_stream<> _replyCancelled; rpl::event_stream<> _forwardCancelled; rpl::event_stream<> _previewCancelled; + rpl::lifetime _previewLifetime; rpl::variable _editMsgId; rpl::variable _replyToId; @@ -677,17 +679,18 @@ void FieldHeader::resolveMessageData() { } void FieldHeader::previewRequested( - rpl::producer title, - rpl::producer description, - rpl::producer page) { + rpl::producer title, + rpl::producer description, + rpl::producer page) { + _previewLifetime.destroy(); std::move( title ) | rpl::filter([=] { return !_preview.cancelled; - }) | start_with_next([=](const QString &t) { + }) | rpl::start_with_next([=](const QString &t) { _title = t; - }, lifetime()); + }, _previewLifetime); std::move( description @@ -695,7 +698,7 @@ void FieldHeader::previewRequested( return !_preview.cancelled; }) | rpl::start_with_next([=](const QString &d) { _description = d; - }, lifetime()); + }, _previewLifetime); std::move( page @@ -704,8 +707,11 @@ void FieldHeader::previewRequested( }) | rpl::start_with_next([=](WebPageData *p) { _preview.data = p; updateVisible(); - }, lifetime()); + }, _previewLifetime); +} +void FieldHeader::previewUnregister() { + _previewLifetime.destroy(); } void FieldHeader::paintWebPage(Painter &p, not_null context) { @@ -996,10 +1002,6 @@ Main::Session &ComposeControls::session() const { } void ComposeControls::setHistory(SetHistoryArgs &&args) { - // Right now only single non-null set of history is supported. - // Otherwise initWebpageProcess should be updated / rewritten. - Expects(!_history && (*args.history)); - _showSlowmodeError = std::move(args.showSlowmodeError); _sendActionFactory = std::move(args.sendActionFactory); _slowmodeSecondsLeft = rpl::single(0) @@ -1014,6 +1016,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { } unregisterDraftSources(); _history = history; + _historyLifetime.destroy(); _header->setHistory(args); registerDraftSource(); _selector->setCurrentPeer(history ? history->peer.get() : nullptr); @@ -1025,9 +1028,12 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { updateControlsVisibility(); updateFieldPlaceholder(); updateAttachBotsMenu(); - //if (!_history) { - // return; - //} + + _sendAs = nullptr; + _silent = nullptr; + if (!_history) { + return; + } const auto peer = _history->peer; initSendAsButton(peer); if (peer->isChat() && peer->asChat()->noParticipantInfo()) { @@ -2134,7 +2140,7 @@ void ComposeControls::initSendAsButton(not_null peer) { updateControlsGeometry(_wrap->size()); orderControls(); } - }, _wrap->lifetime()); + }, _historyLifetime); updateSendAsButton(); } @@ -2218,7 +2224,7 @@ void ComposeControls::initVoiceRecordBar() { _voiceRecordBar->setStartRecordingFilter([=] { const auto error = [&]() -> std::optional { const auto peer = _history ? _history->peer.get() : nullptr; - if (!peer) { + if (peer) { if (const auto error = Data::RestrictionError( peer, ChatRestriction::SendVoiceMessages)) { @@ -2448,10 +2454,9 @@ void ComposeControls::updateMessagesTTLShown() { } bool ComposeControls::updateSendAsButton() { - Expects(_history != nullptr); - - const auto peer = _history->peer; - if (!_regularWindow + const auto peer = _history ? _history->peer.get() : nullptr; + if (!peer + || !_regularWindow || isEditingMessage() || !session().sendAsPeers().shouldChoose(peer)) { if (!_sendAs) { @@ -2467,7 +2472,7 @@ bool ComposeControls::updateSendAsButton() { st::sendAsButton); Ui::SetupSendAsButton( _sendAs.get(), - rpl::single(peer.get()), + rpl::single(peer), _regularWindow); return true; } @@ -2781,15 +2786,18 @@ bool ComposeControls::handleCancelRequest() { } void ComposeControls::initWebpageProcess() { - Expects(_history); + if (!_history) { + _preview = nullptr; + _header->previewUnregister(); + return; + } - auto &lifetime = _wrap->lifetime(); _preview = std::make_unique(_history, _field); _preview->paintRequests( ) | rpl::start_with_next(crl::guard(_header.get(), [=] { _header->update(); - }), lifetime); + }), _historyLifetime); session().changes().peerUpdates( Data::PeerUpdate::Flag::Rights @@ -2818,7 +2826,7 @@ void ComposeControls::initWebpageProcess() { updateControlsGeometry(_wrap->size()); } } - }, lifetime); + }, _historyLifetime); _header->previewRequested( _preview->titleChanges(), diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index a24b5940a..2f5b12491 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -389,10 +389,11 @@ private: std::unique_ptr _preview; - rpl::lifetime _uploaderSubscriptions; - Fn _raiseEmojiSuggestions; + rpl::lifetime _historyLifetime; + rpl::lifetime _uploaderSubscriptions; + }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp new file mode 100644 index 000000000..3fbbdb1ef --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -0,0 +1,155 @@ +/* +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/stories/media_stories_controller.h" + +#include "data/data_stories.h" +#include "media/stories/media_stories_delegate.h" +#include "media/stories/media_stories_header.h" +#include "media/stories/media_stories_slider.h" +#include "media/stories/media_stories_reply.h" +#include "ui/rp_widget.h" +#include "styles/style_media_view.h" +#include "styles/style_widgets.h" +#include "styles/style_boxes.h" // UserpicButton + +namespace Media::Stories { + +Controller::Controller(not_null delegate) +: _delegate(delegate) +, _wrap(_delegate->storiesWrap()) +, _header(std::make_unique
(this)) +, _slider(std::make_unique(this)) +, _replyArea(std::make_unique(this)) { + initLayout(); +} + +void Controller::initLayout() { + const auto headerHeight = st::storiesHeaderMargin.top() + + st::storiesHeaderPhoto.photoSize + + st::storiesHeaderMargin.bottom(); + const auto sliderHeight = st::storiesSliderMargin.top() + + st::storiesSlider.width + + st::storiesSliderMargin.bottom(); + const auto outsideHeaderHeight = headerHeight + sliderHeight; + const auto fieldMinHeight = st::storiesFieldMargin.top() + + st::storiesAttach.height + + st::storiesFieldMargin.bottom(); + const auto minHeightForOutsideHeader = st::storiesMaxSize.height() + + outsideHeaderHeight + + fieldMinHeight; + _layout = _wrap->sizeValue( + ) | rpl::map([=](QSize size) { + size = QSize( + std::max(size.width(), st::mediaviewMinWidth), + std::max(size.height(), st::mediaviewMinHeight)); + + auto layout = Layout(); + layout.headerLayout = (size.height() >= minHeightForOutsideHeader) + ? HeaderLayout::Outside + : HeaderLayout::Normal; + + const auto topSkip = (layout.headerLayout == HeaderLayout::Outside) + ? outsideHeaderHeight + : st::storiesFieldMargin.bottom(); + const auto bottomSkip = fieldMinHeight; + const auto maxWidth = size.width() - 2 * st::storiesSideSkip; + const auto availableHeight = size.height() - topSkip - bottomSkip; + const auto maxContentHeight = std::min( + availableHeight, + st::storiesMaxSize.height()); + const auto nowWidth = maxContentHeight * st::storiesMaxSize.width() + / st::storiesMaxSize.height(); + const auto contentWidth = std::min(nowWidth, maxWidth); + const auto contentHeight = (contentWidth < nowWidth) + ? (contentWidth * st::storiesMaxSize.height() + / st::storiesMaxSize.width()) + : maxContentHeight; + const auto addedTopSkip = (availableHeight - contentHeight) / 2; + layout.content = QRect( + (size.width() - contentWidth) / 2, + addedTopSkip + topSkip, + contentWidth, + contentHeight); + + if (layout.headerLayout == HeaderLayout::Outside) { + layout.header = QRect( + layout.content.topLeft() - QPoint(0, outsideHeaderHeight), + QSize(contentWidth, outsideHeaderHeight)); + layout.slider = QRect( + layout.header.topLeft() + QPoint(0, headerHeight), + QSize(contentWidth, sliderHeight)); + } else { + layout.slider = QRect( + layout.content.topLeft(), + QSize(contentWidth, sliderHeight)); + layout.header = QRect( + layout.slider.topLeft() + QPoint(0, sliderHeight), + QSize(contentWidth, headerHeight)); + } + layout.controlsWidth = std::max( + layout.content.width(), + st::storiesControlsMinWidth); + layout.controlsBottomPosition = QPoint( + (size.width() - layout.controlsWidth) / 2, + (layout.content.y() + + layout.content.height() + + fieldMinHeight + - st::storiesFieldMargin.bottom())); + layout.autocompleteRect = QRect( + layout.controlsBottomPosition.x(), + 0, + layout.controlsWidth, + layout.controlsBottomPosition.y()); + + return layout; + }); +} + +not_null Controller::wrap() const { + return _wrap; +} + +Layout Controller::layout() const { + Expects(_layout.current().has_value()); + + return *_layout.current(); +} + +rpl::producer Controller::layoutValue() const { + return _layout.value() | rpl::filter_optional(); +} + +std::shared_ptr Controller::uiShow() const { + return _delegate->storiesShow(); +} + +auto Controller::stickerOrEmojiChosen() const +->rpl::producer { + return _delegate->storiesStickerOrEmojiChosen(); +} + +void Controller::show(const Data::StoriesList &list, int index) { + Expects(index < list.items.size()); + + const auto &item = list.items[index]; + + const auto id = ShownId{ + .user = list.user, + .id = item.id, + }; + if (_shown == id) { + return; + } + _shown = id; + + _header->show({ .user = list.user, .date = item.date }); + _slider->show({ .index = index, .total = int(list.items.size()) }); + _replyArea->show({ .user = list.user }); +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h new file mode 100644 index 000000000..a50ff6c8a --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -0,0 +1,91 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace ChatHelpers { +class Show; +struct FileChosen; +} // namespace ChatHelpers + +namespace Data { +struct StoriesList; +} // namespace Data + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Media::Stories { + +class Header; +class Slider; +class ReplyArea; +class Delegate; + +struct ShownId { + UserData *user = nullptr; + StoryId id = 0; + + explicit operator bool() const { + return user != nullptr && id != 0; + } + friend inline auto operator<=>(ShownId, ShownId) = default; + friend inline bool operator==(ShownId, ShownId) = default; +}; + +enum class HeaderLayout { + Normal, + Outside, +}; + +struct Layout { + QRect content; + QRect header; + QRect slider; + int controlsWidth = 0; + QPoint controlsBottomPosition; + QRect autocompleteRect; + HeaderLayout headerLayout = HeaderLayout::Normal; + + friend inline auto operator<=>(Layout, Layout) = default; + friend inline bool operator==(Layout, Layout) = default; +}; + +class Controller final { +public: + explicit Controller(not_null delegate); + + [[nodiscard]] not_null wrap() const; + [[nodiscard]] Layout layout() const; + [[nodiscard]] rpl::producer layoutValue() const; + + [[nodiscard]] std::shared_ptr uiShow() const; + [[nodiscard]] auto stickerOrEmojiChosen() const + -> rpl::producer; + + void show(const Data::StoriesList &list, int index); + +private: + void initLayout(); + + const not_null _delegate; + + rpl::variable> _layout; + + const not_null _wrap; + const std::unique_ptr
_header; + const std::unique_ptr _slider; + const std::unique_ptr _replyArea; + + ShownId _shown; + + rpl::lifetime _lifetime; + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 08e81fccc..849e07e97 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -9,18 +9,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "data/data_user.h" -#include "media/stories/media_stories_delegate.h" +#include "media/stories/media_stories_controller.h" #include "ui/controls/userpic_button.h" #include "ui/text/format_values.h" #include "ui/widgets/labels.h" #include "ui/painter.h" #include "ui/rp_widget.h" -#include "styles/style_boxes.h" // defaultUserpicButton. +#include "styles/style_media_view.h" namespace Media::Stories { -Header::Header(not_null delegate) -: _delegate(delegate) { +Header::Header(not_null controller) +: _controller(controller) { } Header::~Header() { @@ -34,32 +34,37 @@ void Header::show(HeaderData data) { _data = data; if (userChanged) { _date = nullptr; - const auto parent = _delegate->storiesWrap(); + const auto parent = _controller->wrap(); auto widget = std::make_unique(parent); const auto raw = widget.get(); - parent->sizeValue() | rpl::start_with_next([=](QSize size) { - raw->setGeometry(50, 50, 600, 100); - }, raw->lifetime()); raw->setAttribute(Qt::WA_TransparentForMouseEvents); const auto userpic = Ui::CreateChild( raw, data.user, - st::defaultUserpicButton); - userpic->move(0, 0); + st::storiesHeaderPhoto); + userpic->show(); + userpic->move( + st::storiesHeaderMargin.left(), + st::storiesHeaderMargin.top()); const auto name = Ui::CreateChild( raw, data.user->firstName, - st::defaultFlatLabel); - name->move(100, 0); + st::storiesHeaderName); + name->move(st::storiesHeaderNamePosition); raw->show(); _widget = std::move(widget); + + _controller->layoutValue( + ) | rpl::start_with_next([=](const Layout &layout) { + raw->setGeometry(layout.header); + }, raw->lifetime()); } _date = std::make_unique( _widget.get(), Ui::FormatDateTime(base::unixtime::parse(data.date)), - st::defaultFlatLabel); - _date->move(100, 50); + st::storiesHeaderDate); _date->show(); + _date->move(st::storiesHeaderDatePosition); } } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h index 8df09a64f..3f0be2e5c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.h +++ b/Telegram/SourceFiles/media/stories/media_stories_header.h @@ -16,24 +16,26 @@ class FlatLabel; namespace Media::Stories { -class Delegate; +class Controller; struct HeaderData { not_null user; TimeId date = 0; friend inline auto operator<=>(HeaderData, HeaderData) = default; + friend inline bool operator==(HeaderData, HeaderData) = default; }; class Header final { public: - explicit Header(not_null delegate); + explicit Header(not_null controller); ~Header(); void show(HeaderData data); private: - const not_null _delegate; + const not_null _controller; + std::unique_ptr _widget; std::unique_ptr _date; std::optional _data; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index cf2226bd1..e4a24807c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -7,42 +7,156 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_reply.h" +#include "base/call_delayed.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/tabbed_selector.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/view/controls/compose_controls_common.h" #include "history/view/controls/history_view_compose_controls.h" -#include "media/stories/media_stories_delegate.h" +#include "inline_bots/inline_bot_result.h" +#include "media/stories/media_stories_controller.h" #include "menu/menu_send.h" +#include "styles/style_media_view.h" namespace Media::Stories { -ReplyArea::ReplyArea(not_null delegate) -: _delegate(delegate) +ReplyArea::ReplyArea(not_null controller) +: _controller(controller) , _controls(std::make_unique( - _delegate->storiesWrap(), + _controller->wrap(), HistoryView::ComposeControlsDescriptor{ - .show = _delegate->storiesShow(), + .show = _controller->uiShow(), .unavailableEmojiPasted = [=](not_null emoji) { showPremiumToast(emoji); }, .mode = HistoryView::ComposeControlsMode::Normal, .sendMenuType = SendMenu::Type::SilentOnly, - .stickerOrEmojiChosen = _delegate->storiesStickerOrEmojiChosen(), + .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), } )) { - _delegate->storiesWrap()->sizeValue( - ) | rpl::start_with_next([=](QSize size) { - _controls->resizeToWidth(size.width() - 200); - _controls->move(100, size.height() - _controls->heightCurrent() - 20); - _controls->setAutocompleteBoundingRect({ QPoint() ,size }); - }, _lifetime); - - _controls->show(); - _controls->showFinished(); + initGeometry(); + initActions(); } ReplyArea::~ReplyArea() { } +void ReplyArea::initGeometry() { + _controller->layoutValue( + ) | rpl::start_with_next([=](const Layout &layout) { + _controls->resizeToWidth(layout.content.width()); + const auto position = layout.controlsBottomPosition + - QPoint(0, _controls->heightCurrent()); + _controls->move(position.x(), position.y()); + _controls->setAutocompleteBoundingRect(layout.autocompleteRect); + }, _lifetime); +} + +void ReplyArea::send(Api::SendOptions options) { + // #TODO stories +} + +void ReplyArea::sendVoice(VoiceToSend &&data) { + // #TODO stories +} + +void ReplyArea::chooseAttach(std::optional overrideCompress) { + // #TODO stories +} + +void ReplyArea::initActions() { + _controls->cancelRequests( + ) | rpl::start_with_next([=] { + // #TODO stories + }, _lifetime); + + _controls->sendRequests( + ) | rpl::start_with_next([=](Api::SendOptions options) { + send(options); + }, _lifetime); + + _controls->sendVoiceRequests( + ) | rpl::start_with_next([=](VoiceToSend &&data) { + sendVoice(std::move(data)); + }, _lifetime); + + _controls->attachRequests( + ) | rpl::filter([=] { + return !_choosingAttach; + }) | rpl::start_with_next([=](std::optional overrideCompress) { + _choosingAttach = true; + base::call_delayed( + st::storiesAttach.ripple.hideDuration, + this, + [=] { chooseAttach(overrideCompress); }); + }, _lifetime); + + _controls->fileChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + _controller->uiShow()->hideLayer(); + //controller()->sendingAnimation().appendSending( + // data.messageSendingFrom); + //const auto localId = data.messageSendingFrom.localId; + //sendExistingDocument(data.document, data.options, localId); + }, _lifetime); + + _controls->photoChosen( + ) | rpl::start_with_next([=](ChatHelpers::PhotoChosen chosen) { + //sendExistingPhoto(chosen.photo, chosen.options); + }, _lifetime); + + _controls->inlineResultChosen( + ) | rpl::start_with_next([=](ChatHelpers::InlineChosen chosen) { + //controller()->sendingAnimation().appendSending( + // chosen.messageSendingFrom); + //const auto localId = chosen.messageSendingFrom.localId; + //sendInlineResult(chosen.result, chosen.bot, chosen.options, localId); + }, _lifetime); + + _controls->setMimeDataHook([=]( + not_null data, + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { + return false;// checkSendingFiles(data); + } else if (action == Ui::InputField::MimeAction::Insert) { + return false;/* confirmSendingFiles( + data, + std::nullopt, + Core::ReadMimeText(data));*/ + } + Unexpected("action in MimeData hook."); + }); + + _controls->lockShowStarts( + ) | rpl::start_with_next([=] { + }, _lifetime); + + _controls->show(); + _controls->finishAnimating(); + _controls->showFinished(); +} + +void ReplyArea::show(ReplyAreaData data) { + if (_data == data) { + return; + } + const auto userChanged = (_data.user != data.user); + _data = data; + if (!userChanged) { + if (_data.user) { + _controls->clear(); + } + return; + } + const auto user = data.user; + const auto history = user ? user->owner().history(user).get() : nullptr; + _controls->setHistory({ + .history = history, + }); + _controls->clear(); +} + void ReplyArea::showPremiumToast(not_null emoji) { // #TODO stories } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index 4d13cff79..571c9c88c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -7,31 +7,57 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/weak_ptr.h" + +namespace Api { +struct SendOptions; +} // namespace Api + namespace HistoryView { class ComposeControls; } // namespace HistoryView +namespace HistoryView::Controls { +struct VoiceToSend; +} // namespace HistoryView::Controls + namespace Media::Stories { -class Delegate; +class Controller; struct ReplyAreaData { - not_null user; + UserData *user = nullptr; + StoryId id = 0; friend inline auto operator<=>(ReplyAreaData, ReplyAreaData) = default; + friend inline bool operator==(ReplyAreaData, ReplyAreaData) = default; }; -class ReplyArea final { +class ReplyArea final : public base::has_weak_ptr { public: - explicit ReplyArea(not_null delegate); + explicit ReplyArea(not_null controller); ~ReplyArea(); + void show(ReplyAreaData data); + private: + using VoiceToSend = HistoryView::Controls::VoiceToSend; + + void initGeometry(); + void initActions(); + + void send(Api::SendOptions options); + void sendVoice(VoiceToSend &&data); + void chooseAttach(std::optional overrideSendImagesAsPhotos); + void showPremiumToast(not_null emoji); - const not_null _delegate; + const not_null _controller; const std::unique_ptr _controls; + ReplyAreaData _data; + bool _choosingAttach = false; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp index cc64c952e..2fdb53884 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp @@ -7,12 +7,68 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_slider.h" +#include "media/stories/media_stories_controller.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "styles/style_widgets.h" +#include "styles/style_media_view.h" + namespace Media::Stories { -Slider::Slider() { +Slider::Slider(not_null controller) +: _controller(controller) { } Slider::~Slider() { } +void Slider::show(SliderData data) { + if (_data == data) { + return; + } + _data = data; + + const auto parent = _controller->wrap(); + auto widget = std::make_unique(parent); + const auto raw = widget.get(); + + raw->paintRequest( + ) | rpl::filter([=] { + return (raw->width() >= st::storiesSlider.width); + }) | rpl::start_with_next([=](QRect clip) { + auto clipf = QRectF(clip); + auto p = QPainter(raw); + const auto single = st::storiesSlider.width; + const auto skip = st::storiesSliderSkip; + // width() == single * max + skip * (max - 1); + // max == (width() + skip) / (single + skip); + const auto max = (raw->width() + skip) / (single + skip); + Assert(max > 0); + const auto count = std::clamp(_data.total, 1, max); + const auto index = std::clamp(data.index, 0, count - 1); + const auto radius = st::storiesSlider.width / 2.; + const auto width = (raw->width() - (count - 1) * skip) + / float64(count); + auto hq = PainterHighQualityEnabler(p); + auto left = 0.; + for (auto i = 0; i != count; ++i) { + const auto rect = QRectF(left, 0, width, single); + p.setBrush((i == index) // #TODO stories + ? st::mediaviewPipControlsFgOver + : st::mediaviewPipPlaybackInactive); + p.setPen(Qt::NoPen); + p.drawRoundedRect(rect, radius, radius); + left += width + skip; + } + }, raw->lifetime()); + + raw->show(); + _widget = std::move(widget); + + _controller->layoutValue( + ) | rpl::start_with_next([=](const Layout &layout) { + raw->setGeometry(layout.slider - st::storiesSliderMargin); + }, raw->lifetime()); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.h b/Telegram/SourceFiles/media/stories/media_stories_slider.h index bb908a9e2..0dd18ef08 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.h +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.h @@ -7,13 +7,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace Ui { +class RpWidget; +} // namespace Ui + namespace Media::Stories { +class Controller; + +struct SliderData { + int index = 0; + int total = 0; + + friend inline auto operator<=>(SliderData, SliderData) = default; + friend inline bool operator==(SliderData, SliderData) = default; +}; + class Slider final { public: - Slider(); + explicit Slider(not_null controller); ~Slider(); + void show(SliderData data); + +private: + const not_null _controller; + + std::unique_ptr _widget; + + SliderData _data; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 131a4d8d2..6fcf4fd1c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_view.h" +#include "media/stories/media_stories_controller.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" #include "media/stories/media_stories_slider.h" @@ -15,23 +16,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media::Stories { View::View(not_null delegate) -: _delegate(delegate) -, _wrap(_delegate->storiesWrap()) -, _header(std::make_unique
(_delegate)) -, _slider(std::make_unique()) -, _replyArea(std::make_unique(_delegate)) { +: _controller(std::make_unique(delegate)) { } View::~View() = default; void View::show(const Data::StoriesList &list, int index) { - Expects(index < list.items.size()); + _controller->show(list, index); +} - const auto &item = list.items[index]; - _header->show({ - .user = list.user, - .date = item.date, - }); +QRect View::contentGeometry() const { + return _controller->layout().content; } } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 884d44219..133652e82 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -7,18 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "data/data_stories.h" - -namespace Ui { -class RpWidget; -} // namespace Ui +namespace Data { +struct StoriesList; +} // namespace Data namespace Media::Stories { -class Header; -class Slider; -class ReplyArea; class Delegate; +class Controller; class View final { public: @@ -26,14 +22,10 @@ public: ~View(); void show(const Data::StoriesList &list, int index); + [[nodiscard]] QRect contentGeometry() const; private: - const not_null _delegate; - const not_null _wrap; - - std::unique_ptr
_header; - std::unique_ptr _slider; - std::unique_ptr _replyArea; + const std::unique_ptr _controller; }; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c250e4616..fc2b6b18f 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -10,6 +10,7 @@ using "ui/basic.style"; using "ui/widgets/widgets.style"; using "ui/menu_icons.style"; using "media/player/media_player.style"; +using "boxes/boxes.style"; mediaviewOverDuration: 150; @@ -403,3 +404,41 @@ pipVolumeIcon2: icon {{ "player/player_volume_on", mediaviewPipControlsFg }}; pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOver }}; speedSliderDividerSize: size(2px, 8px); + +storiesMaxSize: size(405px, 720px); +storiesSlider: MediaSlider(mediaviewPlayback) { + width: 2px; + seekSize: size(2px, 2px); +} +storiesSliderMargin: margins(8px, 7px, 8px, 10px); +storiesSliderSkip: 4px; +storiesHeaderMargin: margins(12px, 3px, 12px, 8px); +storiesHeaderPhoto: UserpicButton(defaultUserpicButton) { + size: size(28px, 28px); + photoSize: 28px; +} +storiesHeaderName: FlatLabel(defaultFlatLabel) { + textFg: mediaviewPipControlsFgOver; // #TODO stories + style: semiboldTextStyle; +} +storiesHeaderNamePosition: point(50px, 2px); +storiesHeaderDate: FlatLabel(defaultFlatLabel) { + textFg: mediaviewPipControlsFg; // #TODO stories +} +storiesHeaderDatePosition: point(50px, 19px); +storiesControlsMinWidth: 200px; +storiesFieldMargin: margins(0px, 14px, 0px, 16px); +storiesAttach: IconButton(defaultIconButton) { + width: 44px; + height: 46px; + + icon: icon {{ "chat/input_attach", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_attach", historyComposeIconFgOver }}; + + rippleAreaPosition: point(2px, 3px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +storiesSideSkip: 145px; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index cfedf5ef8..edb309b69 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1543,6 +1543,14 @@ void OverlayWidget::recountSkipTop() { } void OverlayWidget::resizeContentByScreenSize() { + if (_stories) { + const auto content = _stories->contentGeometry(); + _x = content.x(); + _y = content.y(); + _w = content.width(); + _h = content.height(); + return; + } recountSkipTop(); const auto availableWidth = width(); const auto countZoomFor = [&](int outerw, int outerh) { From 7717de19abefac256f95d26dcd4bc4d3e53ec038 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 7 May 2023 00:20:21 +0400 Subject: [PATCH 010/260] Implement stories switching, photo "animation". --- Telegram/SourceFiles/data/data_stories.cpp | 4 +- Telegram/SourceFiles/data/data_stories.h | 18 ++ .../stories/media_stories_controller.cpp | 179 ++++++++++++- .../media/stories/media_stories_controller.h | 42 ++- .../media/stories/media_stories_delegate.h | 12 + .../media/stories/media_stories_header.cpp | 8 + .../media/stories/media_stories_slider.cpp | 123 +++++++-- .../media/stories/media_stories_slider.h | 19 ++ .../media/stories/media_stories_view.cpp | 24 ++ .../media/stories/media_stories_view.h | 14 + .../SourceFiles/media/view/media_view.style | 15 +- .../media/view/media_view_overlay_widget.cpp | 248 ++++++++++++------ .../media/view/media_view_overlay_widget.h | 6 +- 13 files changed, 581 insertions(+), 131 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 0a0fe8b02..c5634e4ac 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -179,7 +179,6 @@ StoryId Stories::generate( 32, 32 )) | rpl::start_with_next([&](SharedMediaResult &&result) { - stories.total = result.count.value_or(1); if (!result.messageIds.contains(itemId)) { result.messageIds.emplace(itemId); } @@ -214,6 +213,9 @@ StoryId Stories::generate( } } } + stories.total = std::max( + result.count.value_or(1), + int(result.messageIds.size())); const auto i = ranges::find(_all, stories.user, &StoriesList::user); if (i != end(_all)) { *i = std::move(stories); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index ea3a033f1..26f1f76a2 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -15,10 +15,13 @@ namespace Data { class Session; struct StoryPrivacy { + friend inline bool operator==(StoryPrivacy, StoryPrivacy) = default; }; struct StoryMedia { std::variant, not_null> data; + + friend inline bool operator==(StoryMedia, StoryMedia) = default; }; struct StoryItem { @@ -27,12 +30,27 @@ struct StoryItem { TextWithEntities caption; TimeId date = 0; StoryPrivacy privacy; + + friend inline bool operator==(StoryItem, StoryItem) = default; }; struct StoriesList { not_null user; std::vector items; int total = 0; + + friend inline bool operator==(StoriesList, StoriesList) = default; +}; + +struct FullStoryId { + UserData *user = nullptr; + StoryId id = 0; + + explicit operator bool() const { + return user != nullptr && id != 0; + } + friend inline auto operator<=>(FullStoryId, FullStoryId) = default; + friend inline bool operator==(FullStoryId, FullStoryId) = default; }; class Stories final { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 3fbbdb1ef..f88ebf8d3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -7,17 +7,95 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_controller.h" +#include "base/timer.h" +#include "base/power_save_blocker.h" #include "data/data_stories.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" #include "media/stories/media_stories_slider.h" #include "media/stories/media_stories_reply.h" +#include "media/audio/media_audio.h" #include "ui/rp_widget.h" #include "styles/style_media_view.h" #include "styles/style_widgets.h" #include "styles/style_boxes.h" // UserpicButton namespace Media::Stories { +namespace { + +constexpr auto kPhotoProgressInterval = crl::time(100); +constexpr auto kPhotoDuration = 5 * crl::time(1000); + +} // namespace + +class Controller::PhotoPlayback final { +public: + explicit PhotoPlayback(not_null controller); + + [[nodiscard]] bool paused() const; + void togglePaused(bool paused); + +private: + void callback(); + + const not_null _controller; + + base::Timer _timer; + crl::time _started = 0; + crl::time _paused = 0; + +}; + +Controller::PhotoPlayback::PhotoPlayback(not_null controller) +: _controller(controller) +, _timer([=] { callback(); }) +, _started(crl::now()) +, _paused(_started) { +} + +bool Controller::PhotoPlayback::paused() const { + return _paused != 0; +} + +void Controller::PhotoPlayback::togglePaused(bool paused) { + if (!_paused == !paused) { + return; + } else if (paused) { + const auto now = crl::now(); + if (now - _started >= kPhotoDuration) { + return; + } + _paused = now; + _timer.cancel(); + } else { + _started += crl::now() - _paused; + _paused = 0; + _timer.callEach(kPhotoProgressInterval); + } + callback(); +} + +void Controller::PhotoPlayback::callback() { + const auto now = crl::now(); + const auto elapsed = now - _started; + const auto finished = (now - _started >= kPhotoDuration); + if (finished) { + _timer.cancel(); + } + using State = Player::State; + const auto state = finished + ? State::StoppedAtEnd + : _paused + ? State::Paused + : State::Playing; + _controller->updatePhotoPlayback({ + .state = state, + .position = elapsed, + .receivedTill = kPhotoDuration, + .length = kPhotoDuration, + .frequency = 1000, + }); +} Controller::Controller(not_null delegate) : _delegate(delegate) @@ -28,12 +106,14 @@ Controller::Controller(not_null delegate) initLayout(); } +Controller::~Controller() = default; + void Controller::initLayout() { const auto headerHeight = st::storiesHeaderMargin.top() + st::storiesHeaderPhoto.photoSize + st::storiesHeaderMargin.bottom(); const auto sliderHeight = st::storiesSliderMargin.top() - + st::storiesSlider.width + + st::storiesSliderWidth + st::storiesSliderMargin.bottom(); const auto outsideHeaderHeight = headerHeight + sliderHeight; const auto fieldMinHeight = st::storiesFieldMargin.top() @@ -42,6 +122,7 @@ void Controller::initLayout() { const auto minHeightForOutsideHeader = st::storiesMaxSize.height() + outsideHeaderHeight + fieldMinHeight; + _layout = _wrap->sizeValue( ) | rpl::map([=](QSize size) { size = QSize( @@ -137,8 +218,19 @@ void Controller::show(const Data::StoriesList &list, int index) { Expects(index < list.items.size()); const auto &item = list.items[index]; + const auto guard = gsl::finally([&] { + if (v::is>(item.media.data)) { + _photoPlayback = std::make_unique(this); + } else { + _photoPlayback = nullptr; + } + }); + if (_list != list) { + _list = list; + } + _index = index; - const auto id = ShownId{ + const auto id = Data::FullStoryId{ .user = list.user, .id = item.id, }; @@ -152,4 +244,87 @@ void Controller::show(const Data::StoriesList &list, int index) { _replyArea->show({ .user = list.user }); } +void Controller::ready() { + if (_photoPlayback) { + _photoPlayback->togglePaused(false); + } +} + +void Controller::updateVideoPlayback(const Player::TrackState &state) { + updatePlayback(state); +} + +void Controller::updatePhotoPlayback(const Player::TrackState &state) { + updatePlayback(state); +} + +void Controller::updatePlayback(const Player::TrackState &state) { + _slider->updatePlayback(state); + updatePowerSaveBlocker(state); + if (Player::IsStoppedAtEnd(state.state)) { + if (!jumpFor(1)) { + _delegate->storiesJumpTo({}); + } + } +} + +bool Controller::jumpAvailable(int delta) const { + if (delta == -1) { + // Always allow to jump back for one. + // In case of the first story just jump to the beginning. + return _list && !_list->items.empty(); + } + const auto index = _index + delta; + return index >= 0 && index < _list->total; +} + +bool Controller::jumpFor(int delta) { + if (!_index && delta == -1) { + if (!_list || _list->items.empty()) { + return false; + } + _delegate->storiesJumpTo({ + .user = _list->user, + .id = _list->items.front().id + }); + return true; + } + const auto index = _index + delta; + if (index < 0 || index >= _list->total) { + return false; + } else if (index < _list->items.size()) { + // #TODO stories load more + _delegate->storiesJumpTo({ + .user = _list->user, + .id = _list->items[index].id + }); + } + return true; +} + +bool Controller::paused() const { + return _photoPlayback + ? _photoPlayback->paused() + : _delegate->storiesPaused(); +} + +void Controller::togglePaused(bool paused) { + if (_photoPlayback) { + _photoPlayback->togglePaused(paused); + } else { + _delegate->storiesTogglePaused(paused); + } +} + +void Controller::updatePowerSaveBlocker(const Player::TrackState &state) { + const auto block = !Player::IsPausedOrPausing(state.state) + && !Player::IsStoppedOrStopping(state.state); + base::UpdatePowerSaveBlocker( + _powerSaveBlocker, + block, + base::PowerSaveBlockType::PreventDisplaySleep, + [] { return u"Stories playback is active"_q; }, + [=] { return _wrap->window()->windowHandle(); }); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index a50ff6c8a..2d9abb80c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -7,6 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_stories.h" + +namespace base { +class PowerSaveBlocker; +} // namespace base + namespace ChatHelpers { class Show; struct FileChosen; @@ -20,6 +26,10 @@ namespace Ui { class RpWidget; } // namespace Ui +namespace Media::Player { +struct TrackState; +} // namespace Media::Player + namespace Media::Stories { class Header; @@ -27,17 +37,6 @@ class Slider; class ReplyArea; class Delegate; -struct ShownId { - UserData *user = nullptr; - StoryId id = 0; - - explicit operator bool() const { - return user != nullptr && id != 0; - } - friend inline auto operator<=>(ShownId, ShownId) = default; - friend inline bool operator==(ShownId, ShownId) = default; -}; - enum class HeaderLayout { Normal, Outside, @@ -59,6 +58,7 @@ struct Layout { class Controller final { public: explicit Controller(not_null delegate); + ~Controller(); [[nodiscard]] not_null wrap() const; [[nodiscard]] Layout layout() const; @@ -69,9 +69,22 @@ public: -> rpl::producer; void show(const Data::StoriesList &list, int index); + void ready(); + + void updateVideoPlayback(const Player::TrackState &state); + + [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool jumpFor(int delta); + [[nodiscard]] bool paused() const; + void togglePaused(bool paused); private: + class PhotoPlayback; + void initLayout(); + void updatePhotoPlayback(const Player::TrackState &state); + void updatePlayback(const Player::TrackState &state); + void updatePowerSaveBlocker(const Player::TrackState &state); const not_null _delegate; @@ -82,7 +95,12 @@ private: const std::unique_ptr _slider; const std::unique_ptr _replyArea; - ShownId _shown; + Data::FullStoryId _shown; + std::optional _list; + int _index = 0; + std::unique_ptr _photoPlayback; + + std::unique_ptr _powerSaveBlocker; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index fd00b7cf9..3a2d700f6 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -12,12 +12,21 @@ class Show; struct FileChosen; } // namespace ChatHelpers +namespace Data { +struct FullStoryId; +} // namespace Data + namespace Ui { class RpWidget; } // namespace Ui namespace Media::Stories { +enum class JumpReason { + Finished, + User, +}; + class Delegate { public: [[nodiscard]] virtual not_null storiesWrap() = 0; @@ -25,6 +34,9 @@ public: -> std::shared_ptr = 0; [[nodiscard]] virtual auto storiesStickerOrEmojiChosen() -> rpl::producer = 0; + virtual void storiesJumpTo(Data::FullStoryId id) = 0; + [[nodiscard]] virtual bool storiesPaused() = 0; + virtual void storiesTogglePaused(bool paused) = 0; }; } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 849e07e97..af11254a5 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -18,6 +18,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_media_view.h" namespace Media::Stories { +namespace { + +constexpr auto kNameOpacity = 1.; +constexpr auto kDateOpacity = 0.6; + +} // namespace Header::Header(not_null controller) : _controller(controller) { @@ -50,6 +56,7 @@ void Header::show(HeaderData data) { raw, data.user->firstName, st::storiesHeaderName); + name->setOpacity(kNameOpacity); name->move(st::storiesHeaderNamePosition); raw->show(); _widget = std::move(widget); @@ -63,6 +70,7 @@ void Header::show(HeaderData data) { _widget.get(), Ui::FormatDateTime(base::unixtime::parse(data.date)), st::storiesHeaderDate); + _date->setOpacity(kDateOpacity); _date->show(); _date->move(st::storiesHeaderDatePosition); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp index 2fdb53884..57f16ddd1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp @@ -8,21 +8,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_slider.h" #include "media/stories/media_stories_controller.h" +#include "media/view/media_view_playback_progress.h" +#include "media/audio/media_audio.h" #include "ui/painter.h" #include "ui/rp_widget.h" #include "styles/style_widgets.h" #include "styles/style_media_view.h" namespace Media::Stories { +namespace { + +constexpr auto kOpacityInactive = 0.4; +constexpr auto kOpacityActive = 1.; + +} // namespace Slider::Slider(not_null controller) -: _controller(controller) { +: _controller(controller) +, _progress(std::make_unique()) { } Slider::~Slider() { } void Slider::show(SliderData data) { + resetProgress(); + data.total = std::max(data.total, 1); + data.index = std::clamp(data.index, 0, data.total - 1); + if (_data == data) { return; } @@ -32,43 +45,101 @@ void Slider::show(SliderData data) { auto widget = std::make_unique(parent); const auto raw = widget.get(); + _rects.resize(_data.total); + + raw->widthValue() | rpl::filter([=](int width) { + return (width >= st::storiesSliderWidth); + }) | rpl::start_with_next([=](int width) { + layout(width); + }, raw->lifetime()); + raw->paintRequest( ) | rpl::filter([=] { - return (raw->width() >= st::storiesSlider.width); + return (raw->width() >= st::storiesSliderWidth); }) | rpl::start_with_next([=](QRect clip) { - auto clipf = QRectF(clip); - auto p = QPainter(raw); - const auto single = st::storiesSlider.width; - const auto skip = st::storiesSliderSkip; - // width() == single * max + skip * (max - 1); - // max == (width() + skip) / (single + skip); - const auto max = (raw->width() + skip) / (single + skip); - Assert(max > 0); - const auto count = std::clamp(_data.total, 1, max); - const auto index = std::clamp(data.index, 0, count - 1); - const auto radius = st::storiesSlider.width / 2.; - const auto width = (raw->width() - (count - 1) * skip) - / float64(count); - auto hq = PainterHighQualityEnabler(p); - auto left = 0.; - for (auto i = 0; i != count; ++i) { - const auto rect = QRectF(left, 0, width, single); - p.setBrush((i == index) // #TODO stories - ? st::mediaviewPipControlsFgOver - : st::mediaviewPipPlaybackInactive); - p.setPen(Qt::NoPen); - p.drawRoundedRect(rect, radius, radius); - left += width + skip; - } + paint(QRectF(clip)); }, raw->lifetime()); raw->show(); _widget = std::move(widget); + _progress->setValueChangedCallback([=](float64, float64) { + _widget->update(_activeBoundingRect); + }); + _controller->layoutValue( ) | rpl::start_with_next([=](const Layout &layout) { raw->setGeometry(layout.slider - st::storiesSliderMargin); }, raw->lifetime()); } +void Slider::updatePlayback(const Player::TrackState &state) { + _progress->updateState(state); +} + +void Slider::resetProgress() { + _progress->updateState({}); +} + +void Slider::layout(int width) { + const auto single = st::storiesSliderWidth; + const auto skip = st::storiesSliderSkip; + // width == single * max + skip * (max - 1); + // max == (width + skip) / (single + skip); + const auto max = (width + skip) / (single + skip); + Assert(max > 0); + const auto count = std::clamp(_data.total, 1, max); + const auto one = (width - (count - 1) * skip) / float64(count); + auto left = 0.; + for (auto i = 0; i != count; ++i) { + _rects[i] = QRectF(left, 0, one, single); + if (i == _data.index) { + const auto from = int(std::floor(left)); + const auto size = int(std::ceil(left + one)) - from; + _activeBoundingRect = QRect(from, 0, size, single); + } + left += one + skip; + } + for (auto i = count; i != _rects.size(); ++i) { + _rects[i] = QRectF(); + } +} + +void Slider::paint(QRectF clip) { + auto p = QPainter(_widget.get()); + auto hq = PainterHighQualityEnabler(p); + + p.setBrush(st::mediaviewControlFg); + p.setPen(Qt::NoPen); + const auto radius = st::storiesSliderWidth / 2.; + for (auto i = 0; i != int(_rects.size()); ++i) { + if (_rects[i].isEmpty()) { + break; + } else if (!_rects[i].intersects(clip)) { + continue; + } else if (i == _data.index) { + const auto progress = _progress->value(); + const auto full = _rects[i].width(); + const auto min = _rects[i].height(); + const auto activeWidth = std::max(full * progress, min); + const auto inactiveWidth = full - activeWidth + min; + const auto activeLeft = _rects[i].left(); + const auto inactiveLeft = activeLeft + activeWidth - min; + p.setOpacity(kOpacityInactive); + p.drawRoundedRect( + QRectF(inactiveLeft, 0, inactiveWidth, min), + radius, + radius); + p.setOpacity(kOpacityActive); + p.drawRoundedRect( + QRectF(activeLeft, 0, activeWidth, min), + radius, + radius); + } else { + p.setOpacity(kOpacityInactive); + p.drawRoundedRect(_rects[i], radius, radius); + } + } +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.h b/Telegram/SourceFiles/media/stories/media_stories_slider.h index 0dd18ef08..140df471f 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.h +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.h @@ -11,6 +11,14 @@ namespace Ui { class RpWidget; } // namespace Ui +namespace Media::View { +class PlaybackProgress; +} // namespace Media::View + +namespace Media::Player { +struct TrackState; +} // namespace Media::Player + namespace Media::Stories { class Controller; @@ -30,13 +38,24 @@ public: void show(SliderData data); + void updatePlayback(const Player::TrackState &state); + private: + void resetProgress(); + + void layout(int width); + void paint(QRectF clip); + const not_null _controller; + const std::unique_ptr _progress; std::unique_ptr _widget; + std::vector _rects; + QRect _activeBoundingRect; SliderData _data; + }; } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 6fcf4fd1c..cfe82693c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -25,8 +25,32 @@ void View::show(const Data::StoriesList &list, int index) { _controller->show(list, index); } +void View::ready() { + _controller->ready(); +} + QRect View::contentGeometry() const { return _controller->layout().content; } +void View::updatePlayback(const Player::TrackState &state) { + _controller->updateVideoPlayback(state); +} + +bool View::jumpAvailable(int delta) const { + return _controller->jumpAvailable(delta); +} + +bool View::jumpFor(int delta) const { + return _controller->jumpFor(delta); +} + +bool View::paused() const { + return _controller->paused(); +} + +void View::togglePaused(bool paused) { + _controller->togglePaused(paused); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 133652e82..96f4e0042 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -11,6 +11,10 @@ namespace Data { struct StoriesList; } // namespace Data +namespace Media::Player { +struct TrackState; +} // namespace Media::Player + namespace Media::Stories { class Delegate; @@ -22,8 +26,18 @@ public: ~View(); void show(const Data::StoriesList &list, int index); + void ready(); + [[nodiscard]] QRect contentGeometry() const; + void updatePlayback(const Player::TrackState &state); + + [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool jumpFor(int delta) const; + + [[nodiscard]] bool paused() const; + void togglePaused(bool paused); + private: const std::unique_ptr _controller; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index fc2b6b18f..19dd46778 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -406,11 +406,8 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); storiesMaxSize: size(405px, 720px); -storiesSlider: MediaSlider(mediaviewPlayback) { - width: 2px; - seekSize: size(2px, 2px); -} -storiesSliderMargin: margins(8px, 7px, 8px, 10px); +storiesSliderWidth: 2px; +storiesSliderMargin: margins(8px, 7px, 8px, 11px); storiesSliderSkip: 4px; storiesHeaderMargin: margins(12px, 3px, 12px, 8px); storiesHeaderPhoto: UserpicButton(defaultUserpicButton) { @@ -418,14 +415,14 @@ storiesHeaderPhoto: UserpicButton(defaultUserpicButton) { photoSize: 28px; } storiesHeaderName: FlatLabel(defaultFlatLabel) { - textFg: mediaviewPipControlsFgOver; // #TODO stories + textFg: mediaviewControlFg; style: semiboldTextStyle; } -storiesHeaderNamePosition: point(50px, 2px); +storiesHeaderNamePosition: point(50px, 0px); storiesHeaderDate: FlatLabel(defaultFlatLabel) { - textFg: mediaviewPipControlsFg; // #TODO stories + textFg: mediaviewControlFg; } -storiesHeaderDatePosition: point(50px, 19px); +storiesHeaderDatePosition: point(50px, 16px); storiesControlsMinWidth: 200px; storiesFieldMargin: margins(0px, 14px, 0px, 16px); storiesAttach: IconButton(defaultIconButton) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index edb309b69..13615c184 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -251,18 +251,14 @@ struct OverlayWidget::Streamed { Streamed( not_null document, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback); Streamed( not_null photo, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback); Streaming::Instance instance; - PlaybackControls controls; + std::unique_ptr controls; std::unique_ptr powerSaveBlocker; bool withSound = false; @@ -289,21 +285,15 @@ struct OverlayWidget::PipWrap { OverlayWidget::Streamed::Streamed( not_null document, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback) -: instance(document, origin, std::move(waitingCallback)) -, controls(controlsParent, controlsDelegate) { +: instance(document, origin, std::move(waitingCallback)) { } OverlayWidget::Streamed::Streamed( not_null photo, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback) -: instance(photo, origin, std::move(waitingCallback)) -, controls(controlsParent, controlsDelegate) { +: instance(photo, origin, std::move(waitingCallback)) { } OverlayWidget::PipWrap::PipWrap( @@ -542,7 +532,9 @@ OverlayWidget::OverlayWidget() Core::App().calls().currentGroupCallValue(), _1 || _2 ) | rpl::start_with_next([=](bool call) { - if (!_streamed || videoIsGifOrUserpic()) { + if (!_streamed + || !_document + || (_document->isAnimation() && !_document->isVideoMessage())) { return; } else if (call) { playbackPauseOnCall(); @@ -583,7 +575,10 @@ void OverlayWidget::setupWindow() { return Flag::None | Flag(0); } const auto inControls = (_over != OverNone) && (_over != OverVideo); - if (inControls || (_streamed && _streamed->controls.dragging())) { + if (inControls + || (_streamed + && _streamed->controls + && _streamed->controls->dragging())) { return Flag::None | Flag(0); } else if ((_w > _widget->width() || _h > _widget->height()) && (widgetPoint.y() > st::mediaviewHeaderTop) @@ -881,10 +876,10 @@ QSize OverlayWidget::videoSize() const { return flipSizeByRotation(_streamed->instance.info().video.size); } -bool OverlayWidget::videoIsGifOrUserpic() const { - return _streamed - && (!_document - || (_document->isAnimation() && !_document->isVideoMessage())); +bool OverlayWidget::streamingRequiresControls() const { + return !_stories + && _document + && (!_document->isAnimation() || _document->isVideoMessage()); } QImage OverlayWidget::videoFrame() const { @@ -979,13 +974,13 @@ void OverlayWidget::documentUpdated(not_null document) { updateDocSize(); _widget->update(_docRect); } - } else if (_streamed) { + } else if (_streamed && _streamed->controls) { const auto ready = _documentMedia->loaded() ? _document->size : _document->loading() ? std::clamp(_document->loadOffset(), int64(), _document->size) : 0; - _streamed->controls.setLoadingProgress(ready, _document->size); + _streamed->controls->setLoadingProgress(ready, _document->size); } } @@ -1013,7 +1008,10 @@ void OverlayWidget::updateDocSize() { } void OverlayWidget::refreshNavVisibility() { - if (_sharedMediaData) { + if (_stories) { + _leftNavVisible = _stories->jumpAvailable(-1); + _rightNavVisible = _stories->jumpAvailable(1); + } else if (_sharedMediaData) { _leftNavVisible = _index && (*_index > 0); _rightNavVisible = _index && (*_index + 1 < _sharedMediaData->size()); } else if (_userPhotosData) { @@ -1029,7 +1027,7 @@ void OverlayWidget::refreshNavVisibility() { } bool OverlayWidget::contentCanBeSaved() const { - if (hasCopyMediaRestriction()) { + if (_stories || hasCopyMediaRestriction()) { return false; } else if (_photo) { return _photo->hasVideo() || _photoMedia->loaded(); @@ -1108,7 +1106,7 @@ void OverlayWidget::updateControls() { QPoint(), QSize(st::mediaviewIconOver, st::mediaviewIconOver)); _saveVisible = contentCanBeSaved(); - _rotateVisible = !_themePreviewShown; + _rotateVisible = !_themePreviewShown && !_stories; const auto navRect = [&](int i) { return QRect(width() - st::mediaviewIconSize.width() * i, height() - st::mediaviewIconSize.height(), @@ -1181,8 +1179,8 @@ void OverlayWidget::refreshCaptionGeometry() { _groupThumbs = nullptr; _groupThumbsRect = QRect(); } - const auto captionBottom = (_streamed && !videoIsGifOrUserpic()) - ? (_streamed->controls.y() - st::mediaviewCaptionMargin.height()) + const auto captionBottom = (_streamed && _streamed->controls) + ? (_streamed->controls->y() - st::mediaviewCaptionMargin.height()) : _groupThumbs ? _groupThumbsTop : height() - st::mediaviewCaptionMargin.height(); @@ -1523,9 +1521,9 @@ void OverlayWidget::contentSizeChanged() { } void OverlayWidget::recountSkipTop() { - const auto bottom = (!_streamed || videoIsGifOrUserpic()) + const auto bottom = (!_streamed || !_streamed->controls) ? height() - : (_streamed->controls.y() - st::mediaviewCaptionPadding.bottom()); + : (_streamed->controls->y() - st::mediaviewCaptionPadding.bottom()); const auto skipHeightBottom = (height() - bottom); _skipTop = std::min( std::max( @@ -1869,12 +1867,12 @@ void OverlayWidget::toggleFullScreen(bool fullscreen) { } void OverlayWidget::activateControls() { - if (!_menu && !_mousePressed) { + if (!_menu && !_mousePressed && !_stories) { _controlsHideTimer.callOnce(st::mediaviewWaitHide); } if (_fullScreenVideo) { - if (_streamed) { - _streamed->controls.showAnimated(); + if (_streamed && _streamed->controls) { + _streamed->controls->showAnimated(); } } if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) { @@ -1888,16 +1886,23 @@ void OverlayWidget::activateControls() { } void OverlayWidget::hideControls(bool force) { - if (!force) { + if (_stories) { + _controlsState = ControlsShown; + _controlsOpacity = anim::value(1); + _helper->setControlsOpacity(1.); + return; + } else if (!force) { if (!_dropdown->isHidden() - || (_streamed && _streamed->controls.hasMenu()) + || (_streamed + && _streamed->controls + && _streamed->controls->hasMenu()) || _menu || _mousePressed) { return; } } - if (_fullScreenVideo) { - _streamed->controls.hideAnimated(); + if (_fullScreenVideo && _streamed && _streamed->controls) { + _streamed->controls->hideAnimated(); } if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return; @@ -2959,7 +2964,7 @@ void OverlayWidget::displayPhoto(not_null photo) { refreshMediaViewer(); _staticContent = QImage(); - if (_photo->videoCanBePlayed()) { + if (!_stories && _photo->videoCanBePlayed()) { initStreaming(); } @@ -3133,7 +3138,7 @@ void OverlayWidget::displayDocument( } refreshFromLabel(); _blurred = false; - if (_showAsPip && _streamed && !videoIsGifOrUserpic()) { + if (_showAsPip && _streamed && _streamed->controls) { switchToPip(); } else { displayFinished(); @@ -3348,20 +3353,12 @@ void OverlayWidget::applyVideoSize() { bool OverlayWidget::createStreamingObjects() { Expects(_photo || _document); + const auto origin = fileOrigin(); + const auto callback = [=] { waitingAnimationCallback(); }; if (_document) { - _streamed = std::make_unique( - _document, - fileOrigin(), - _body, - static_cast(this), - [=] { waitingAnimationCallback(); }); + _streamed = std::make_unique(_document, origin, callback); } else { - _streamed = std::make_unique( - _photo, - fileOrigin(), - _body, - static_cast(this), - [=] { waitingAnimationCallback(); }); + _streamed = std::make_unique(_photo, origin, callback); } if (!_streamed->instance.valid()) { _streamed = nullptr; @@ -3375,12 +3372,12 @@ bool OverlayWidget::createStreamingObjects() { || _document->isVideoFile() || _document->isVoiceMessage() || _document->isVideoMessage()); - - if (videoIsGifOrUserpic()) { - _streamed->controls.hide(); - } else { + if (streamingRequiresControls()) { + _streamed->controls = std::make_unique( + _body, + static_cast(this)); + _streamed->controls->show(); refreshClipControllerGeometry(); - _streamed->controls.show(); } return true; } @@ -3569,7 +3566,7 @@ void OverlayWidget::initThemePreview() { } void OverlayWidget::refreshClipControllerGeometry() { - if (!_streamed || videoIsGifOrUserpic()) { + if (!_streamed || !_streamed->controls) { return; } @@ -3584,13 +3581,15 @@ void OverlayWidget::refreshClipControllerGeometry() { const auto controllerWidth = std::min( st::mediaviewControllerSize.width(), width() - 2 * skip); - _streamed->controls.resize( + _streamed->controls->resize( controllerWidth, st::mediaviewControllerSize.height()); - _streamed->controls.move( + _streamed->controls->move( (width() - controllerWidth) / 2, - controllerBottom - _streamed->controls.height() - st::mediaviewCaptionPadding.bottom()); - Ui::SendPendingMoveResizeEvents(&_streamed->controls); + (controllerBottom + - _streamed->controls->height() + - st::mediaviewCaptionPadding.bottom())); + Ui::SendPendingMoveResizeEvents(_streamed->controls.get()); } void OverlayWidget::playbackControlsPlay() { @@ -3614,7 +3613,7 @@ void OverlayWidget::playbackControlsFromFullScreen() { } void OverlayWidget::playbackControlsToPictureInPicture() { - if (!videoIsGifOrUserpic()) { + if (_streamed && _streamed->controls) { switchToPip(); } } @@ -3775,7 +3774,7 @@ void OverlayWidget::playbackControlsSpeedChanged(float64 speed) { Core::App().settings().setVideoPlaybackSpeed(speed); Core::App().saveSettingsDelayed(); } - if (_streamed && !videoIsGifOrUserpic()) { + if (_streamed && _streamed->controls) { DEBUG_LOG(("Media playback speed: %1 to _streamed.").arg(speed)); _streamed->instance.setSpeed(speed); } @@ -3921,10 +3920,66 @@ auto OverlayWidget::storiesStickerOrEmojiChosen() return _storiesStickerOrEmojiChosen.events(); } +void OverlayWidget::storiesJumpTo(Data::FullStoryId id) { + Expects(_stories != nullptr); + + if (!id) { + close(); + return; + } + const auto &all = id.user->owner().stories().all(); + const auto i = ranges::find( + all, + not_null(id.user), + &Data::StoriesList::user); + if (i == end(all)) { + close(); + return; + } + const auto j = ranges::find(i->items, id.id, &Data::StoryItem::id); + if (j == end(i->items)) { + close(); + return; + } + setContext(StoriesContext{ i->user, id.id }); + clearStreaming(); + _streamingStartPaused = false; + const auto &data = j->media.data; + if (const auto photo = std::get_if>(&data)) { + displayPhoto(*photo); + } else { + displayDocument(v::get>(data)); + } +} + +bool OverlayWidget::storiesPaused() { + return _streamed + && !_streamed->instance.player().failed() + && !_streamed->instance.player().finished() + && _streamed->instance.player().active() + && _streamed->instance.player().paused(); +} + +void OverlayWidget::storiesTogglePaused(bool paused) { + if (!_streamed + || _streamed->instance.player().failed() + || _streamed->instance.player().finished() + || !_streamed->instance.player().active()) { + return; + } else if (_streamed->instance.player().paused()) { + _streamed->instance.resume(); + updatePlaybackState(); + playbackPauseMusic(); + } else { + _streamed->instance.pause(); + updatePlaybackState(); + } +} + void OverlayWidget::playbackToggleFullScreen() { Expects(_streamed != nullptr); - if (!videoShown() || (videoIsGifOrUserpic() && !_fullScreenVideo)) { + if (!videoShown() || (!_streamed->controls && !_fullScreenVideo)) { return; } _fullScreenVideo = !_fullScreenVideo; @@ -3936,10 +3991,12 @@ void OverlayWidget::playbackToggleFullScreen() { setZoomLevel( _fullScreenVideo ? kZoomToScreenLevel : _fullScreenZoomCache, true); - if (!_fullScreenVideo) { - _streamed->controls.showAnimated(); + if (_streamed->controls) { + if (!_fullScreenVideo) { + _streamed->controls->showAnimated(); + } + _streamed->controls->setInFullScreen(_fullScreenVideo); } - _streamed->controls.setInFullScreen(_fullScreenVideo); _touchbarFullscreenToggled.fire_copy(_fullScreenVideo); updateControls(); update(); @@ -3981,14 +4038,19 @@ void OverlayWidget::playbackPauseMusic() { void OverlayWidget::updatePlaybackState() { Expects(_streamed != nullptr); - if (videoIsGifOrUserpic()) { + if (!_streamed->controls && !_stories) { return; } const auto state = _streamed->instance.player().prepareLegacyState(); if (state.position != kTimeUnknown && state.length != kTimeUnknown) { - _streamed->controls.updatePlayback(state); - updatePowerSaveBlocker(state); - _touchbarTrackState.fire_copy(state); + if (_streamed->controls) { + _streamed->controls->updatePlayback(state); + _touchbarTrackState.fire_copy(state); + updatePowerSaveBlocker(state); + } + if (_stories) { + _stories->updatePlayback(state); + } } } @@ -4050,9 +4112,15 @@ void OverlayWidget::paint(not_null renderer) { renderer->paintTransformedVideoFrame(contentGeometry()); if (_streamed->instance.player().ready()) { _streamed->instance.markFrameShown(); + if (_stories) { + _stories->ready(); + } } } else { validatePhotoCurrentImage(); + if (_stories && !_blurred) { + _stories->ready(); + } const auto fillTransparentBackground = (!_document || (!_document->sticker() && !_document->isVideoMessage())) && _staticContentTransparent; @@ -4077,7 +4145,9 @@ void OverlayWidget::paint(not_null renderer) { const auto opacity = _fullScreenVideo ? 0. : _controlsOpacity.current(); if (opacity > 0) { paintControls(renderer, opacity); - renderer->paintFooter(footerGeometry(), opacity); + if (!_stories) { + renderer->paintFooter(footerGeometry(), opacity); + } if (!_caption.isEmpty()) { renderer->paintCaption(captionGeometry(), opacity); } @@ -4510,6 +4580,10 @@ void OverlayWidget::handleKeyPress(not_null e) { const auto key = e->key(); const auto modifiers = e->modifiers(); const auto ctrl = modifiers.testFlag(Qt::ControlModifier); + if (_stories && key == Qt::Key_Space && _down != OverVideo) { + _stories->togglePaused(!_stories->paused()); + return; + } if (_streamed) { // Ctrl + F for full screen toggle is in eventFilter(). const auto toggleFull = (modifiers.testFlag(Qt::AltModifier) || ctrl) @@ -4833,7 +4907,9 @@ void OverlayWidget::setSession(not_null session) { } bool OverlayWidget::moveToNext(int delta) { - if (!_index) { + if (_stories) { + return _stories->jumpFor(delta); + } else if (!_index) { return false; } auto newIndex = *_index + delta; @@ -4928,6 +5004,9 @@ void OverlayWidget::handleMousePress( || _over == OverMore || _over == OverVideo) { _down = _over; + if (_over == OverVideo && _stories) { + _stories->togglePaused(true); + } } else if (!_saveMsg.contains(position) || !isSaveMsgShown()) { _pressed = true; _dragging = 0; @@ -4950,9 +5029,12 @@ bool OverlayWidget::handleDoubleClick( if (_over != OverVideo || !_streamed || button != Qt::LeftButton) { return false; + } else if (_stories) { + toggleFullScreen(_windowed); + } else { + playbackToggleFullScreen(); + playbackPauseResume(); } - playbackToggleFullScreen(); - playbackPauseResume(); return true; } @@ -5090,11 +5172,11 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverLeftNav); } else if (_rightNavVisible && _rightNav.contains(pos)) { updateOverState(OverRightNav); - } else if (_from && _nameNav.contains(pos)) { + } else if (!_stories && _from && _nameNav.contains(pos)) { updateOverState(OverName); - } else if (_message && _message->isRegular() && _dateNav.contains(pos)) { + } else if (!_stories && _message && _message->isRegular() && _dateNav.contains(pos)) { updateOverState(OverDate); - } else if (_headerHasLink && _headerNav.contains(pos)) { + } else if (!_stories && _headerHasLink && _headerNav.contains(pos)) { updateOverState(OverHeader); } else if (_saveVisible && _saveNav.contains(pos)) { updateOverState(OverSave); @@ -5104,10 +5186,14 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverIcon); } else if (_moreNav.contains(pos)) { updateOverState(OverMore); - } else if (documentContentShown() && finalContentRect().contains(pos)) { - if ((_document->isVideoFile() || _document->isVideoMessage()) && _streamed) { + } else if (contentShown() && finalContentRect().contains(pos)) { + if (_stories) { updateOverState(OverVideo); - } else if (!_streamed && !_documentMedia->loaded()) { + } else if (_streamed + && _document + && (_document->isVideoFile() || _document->isVideoMessage())) { + updateOverState(OverVideo); + } else if (!_streamed && _document && !_documentMedia->loaded()) { updateOverState(OverIcon); } else if (_over != OverNone) { updateOverState(OverNone); @@ -5163,7 +5249,9 @@ void OverlayWidget::handleMouseRelease( } else if (_over == OverMore && _down == OverMore) { InvokeQueued(_widget, [=] { showDropdown(); }); } else if (_over == OverVideo && _down == OverVideo) { - if (_streamed) { + if (_stories) { + _stories->togglePaused(false); + } else if (_streamed) { playbackPauseResume(); } } else if (_pressed) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 4d6d6d3f2..ad7506ee1 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -26,6 +26,7 @@ class History; namespace Data { class PhotoMedia; class DocumentMedia; +struct FullStoryId; } // namespace Data namespace Ui { @@ -227,6 +228,9 @@ private: std::shared_ptr storiesShow() override; auto storiesStickerOrEmojiChosen() -> rpl::producer override; + void storiesJumpTo(Data::FullStoryId id) override; + bool storiesPaused() override; + void storiesTogglePaused(bool paused) override; void hideControls(bool force = false); void subscribeToScreenGeometry(); @@ -458,7 +462,7 @@ private: void applyVideoSize(); [[nodiscard]] bool videoShown() const; [[nodiscard]] QSize videoSize() const; - [[nodiscard]] bool videoIsGifOrUserpic() const; + [[nodiscard]] bool streamingRequiresControls() const; [[nodiscard]] QImage videoFrame() const; // ARGB (changes prepare format) [[nodiscard]] QImage currentVideoFrameImage() const; // RGB (may convert) [[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV From ae94cd2d42aedff0e96a560d403c8eb088ad2c28 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 9 May 2023 14:31:48 +0400 Subject: [PATCH 011/260] Allow navigating to stories of sibling users. --- Telegram/CMakeLists.txt | 2 + .../icons/mediaview/stories_next.png | Bin 0 -> 438 bytes .../icons/mediaview/stories_next@2x.png | Bin 0 -> 806 bytes .../icons/mediaview/stories_next@3x.png | Bin 0 -> 1260 bytes Telegram/SourceFiles/data/data_stories.cpp | 37 ++- Telegram/SourceFiles/data/data_stories.h | 5 +- .../stories/media_stories_controller.cpp | 137 +++++++-- .../media/stories/media_stories_controller.h | 31 ++- .../media/stories/media_stories_delegate.h | 1 + .../media/stories/media_stories_sibling.cpp | 262 ++++++++++++++++++ .../media/stories/media_stories_sibling.h | 48 ++++ .../media/stories/media_stories_slider.cpp | 4 +- .../media/stories/media_stories_view.cpp | 34 ++- .../media/stories/media_stories_view.h | 25 +- .../SourceFiles/media/view/media_view.style | 10 +- .../media/view/media_view_overlay_opengl.cpp | 31 ++- .../media/view/media_view_overlay_opengl.h | 4 +- .../media/view/media_view_overlay_widget.cpp | 151 +++++++--- .../media/view/media_view_overlay_widget.h | 8 +- .../platform/platform_overlay_widget.h | 2 + 20 files changed, 699 insertions(+), 93 deletions(-) create mode 100644 Telegram/Resources/icons/mediaview/stories_next.png create mode 100644 Telegram/Resources/icons/mediaview/stories_next@2x.png create mode 100644 Telegram/Resources/icons/mediaview/stories_next@3x.png create mode 100644 Telegram/SourceFiles/media/stories/media_stories_sibling.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_sibling.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1a76befc0..5a095f698 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -970,6 +970,8 @@ PRIVATE media/stories/media_stories_header.h media/stories/media_stories_reply.cpp media/stories/media_stories_reply.h + media/stories/media_stories_sibling.cpp + media/stories/media_stories_sibling.h media/stories/media_stories_slider.cpp media/stories/media_stories_slider.h media/stories/media_stories_view.cpp diff --git a/Telegram/Resources/icons/mediaview/stories_next.png b/Telegram/Resources/icons/mediaview/stories_next.png new file mode 100644 index 0000000000000000000000000000000000000000..dd997b2d08ee91a322d9a8e0a1287e7313a292e2 GIT binary patch literal 438 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgfOu^H|F~mY} z>l8yF7Xy*Dw_#DbbA#3vckFG--XXGit?-P6W{XYT(j~NbfA^=XacR1HcA0|y&;R~$ zD*l&6w$7fAEO9J3v+wbYWt_gT*DL4QPrtTp?S-Q}uXf3?I9@N^yd;r}>4b!|c*L}+ z%T~4=;BjU%Okb|3wrW+bb*^{L@TjkBId7NkUZ@RBthw@U?T^W&wI6F3|82jyugaxe0TgDQu6{1- HoD!M<{so-R literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/mediaview/stories_next@2x.png b/Telegram/Resources/icons/mediaview/stories_next@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..82b819e9e5425e6beded44dc875b5ce9e67c4eb4 GIT binary patch literal 806 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H12pw9Za9ARA#{=S=g zS5Y`wt$sJp9qZP#NuNLa78$9Z+_U%Z{x7x@E}Lsd7w?Q&8M5m7>!V2@AHS}$kvsnQ zk(;rU6f|E&s_&1#iZ`*Y1^FWSdH z*=pDE#~bH4WZivR_Or&0L)bE6%je~1x(*q%cSp=VtHvR`j?u$FFmjRl=OPU?%PrP( z`z%{jexAAV`m4&!E71%>wkC7^f=f-9{`oMdG>CP#9-HquSzVZcQ(;+8XZYST#+M5$ zuB<+?7AUDV-TTh7;0ZIhTALP{GAJ$h{(^ri%Y**=Cm3c*Z#u(TTDI(OI?u-IC495r zmU$=H%|E}9HLhoEnD#zX%@Yia!R?2$XLU>qc3<)2mD^&EbzfG!{gk}_zW2&|y>0IT zb?h0Ern=1jsj7QnPUeRdaSINnl+IDe`&N??r3_b$z5b-^S8m0XQ9m;oXTI3YwBP=F frlZnt^}KrKi}F3np1FM2Kxx#|)z4*}Q$iB}0g+Os literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/mediaview/stories_next@3x.png b/Telegram/Resources/icons/mediaview/stories_next@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3550c8cce46ff4f8984db7b426c134d4579001a1 GIT binary patch literal 1260 zcmVPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*ISe5n>#OUK@`VbHzE;N zp;Ax~;?YxSM4?qERK5VwiEki?QX%03i1-8=jaKbdC`d>=lN$vR_x{+j&p0!C@0mSo z&pFvSr(n+PS!@0O^H?*p_Bu^XjZdQo>Y@jJ6@j9=yL)_me0X?xaBy&7V4$ zo}Sg!)vK#3M>>Y)C_xnpMS$g3(%;{|y}kYR_GZB7QH4U(qC7vc+>-6>?Hd~#@9*y( z_%lRNjtLCq2I(v~Jw45G&%h=07(J!*(2O+Kr8kJv==4V3Lvr24HM3XSbn~>V#F)JAe@wH>P#X zXP`dXK=N8|IHDSQ3!RTz1F-ylvL>EgMW0>#c{U&U-fpnP6uNpAG_K z(wXbGK33GMtgK8;O&K3KRX#mEy}Z1%wY51(**EU!=wPKC92{7QaIE$}-9A4*&&(5i3#RpRlWaL00TQs4HW)X+n~4$S%tV>5 zK`?GmFmRiloy~{aMk)+9D8$4n?w*KrTq|B!T3T9LThrCb?g3^BySuwKeecu8$PKnq zdjJ&lzH16|r!15k6b!^JFE4#Y!(KLpn`dwZ3I<~L_xJYTJX+VaoL06my@9+zC8ypvxmzQ-D z96JsV54*a$^hd5WW(tKKf{o4{+){_B$m6Sr;7X?ycWnv73U20W3lxjEXa-}Ic+rHI zcxWIPD~2mAG$0$y3CRj#X~7)9@Z0|Wer5S8C0TC~`3(MMgx>%_#!uYZr!YD?s?hl^ z7Q_miz}C(EsF4bB_x1Jt&@1iP)lN5txT~yxeieBsvADRXsxEG z#JYQp1Y@$Z)UnQ8CgJk#?k=~ujUZ4Hhe&@=f>*UVsqdO{zFd#Ul`|azZlz6Nfg9VJ z)T=-nPhi;wNcy|k?2sB79VU`xTxpmVSXVl$ zs0Ke-lRW^V`IGpP$msR#-N91G0;Q+NBsp_&Nb^s*GS0fFvIajWi0N?;;Ep4bqNFlJ zA!=id; const auto peer = item->history()->peer; const auto session = &peer->session(); - auto stories = StoriesList{ .user = item->from()->asUser() }; + auto full = std::vector(); const auto lifetime = session->storage().query(SharedMediaQuery( SharedMediaKey(peer->id, MsgId(0), listType, itemId), 32, @@ -182,21 +182,33 @@ StoryId Stories::generate( if (!result.messageIds.contains(itemId)) { result.messageIds.emplace(itemId); } - stories.items.reserve(result.messageIds.size()); auto index = StoryId(); const auto owner = &peer->owner(); for (const auto id : result.messageIds) { if (const auto item = owner->message(peer, id)) { + const auto user = item->from()->asUser(); + if (!user) { + continue; + } + const auto i = ranges::find( + full, + not_null(user), + &StoriesList::user); + auto &stories = (i == end(full)) + ? full.emplace_back(StoriesList{ .user = user }) + : *i; if (id == itemId) { resultId = ++index; stories.items.push_back({ .id = resultId, .media = (document ? StoryMedia{ not_null(document) } - : StoryMedia{ v::get>(media) }), + : StoryMedia{ + v::get>(media) }), .caption = item->originalText(), .date = item->date(), }); + ++stories.total; } else if (const auto media = item->media()) { const auto photo = media->photo(); const auto document = media->document(); @@ -209,18 +221,21 @@ StoryId Stories::generate( .caption = item->originalText(), .date = item->date(), }); + ++stories.total; } } } } - stories.total = std::max( - result.count.value_or(1), - int(result.messageIds.size())); - const auto i = ranges::find(_all, stories.user, &StoriesList::user); - if (i != end(_all)) { - *i = std::move(stories); - } else { - _all.push_back(std::move(stories)); + for (auto &stories : full) { + const auto i = ranges::find( + _all, + stories.user, + &StoriesList::user); + if (i != end(_all)) { + *i = std::move(stories); + } else { + _all.push_back(std::move(stories)); + } } }); return resultId; diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 26f1f76a2..373f90b95 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -46,9 +46,12 @@ struct FullStoryId { UserData *user = nullptr; StoryId id = 0; - explicit operator bool() const { + [[nodiscard]] bool valid() const { return user != nullptr && id != 0; } + explicit operator bool() const { + return valid(); + } friend inline auto operator<=>(FullStoryId, FullStoryId) = default; friend inline bool operator==(FullStoryId, FullStoryId) = default; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index f88ebf8d3..c653ffb7f 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_stories.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" +#include "media/stories/media_stories_sibling.h" #include "media/stories/media_stories_slider.h" #include "media/stories/media_stories_reply.h" +#include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" #include "ui/rp_widget.h" #include "styles/style_media_view.h" @@ -25,6 +27,7 @@ namespace { constexpr auto kPhotoProgressInterval = crl::time(100); constexpr auto kPhotoDuration = 5 * crl::time(1000); +constexpr auto kSiblingMultiplier = 0.448; } // namespace @@ -115,12 +118,15 @@ void Controller::initLayout() { const auto sliderHeight = st::storiesSliderMargin.top() + st::storiesSliderWidth + st::storiesSliderMargin.bottom(); - const auto outsideHeaderHeight = headerHeight + sliderHeight; + const auto outsideHeaderHeight = headerHeight + + sliderHeight + + st::storiesSliderOutsideSkip; const auto fieldMinHeight = st::storiesFieldMargin.top() + st::storiesAttach.height + st::storiesFieldMargin.bottom(); - const auto minHeightForOutsideHeader = st::storiesMaxSize.height() + const auto minHeightForOutsideHeader = st::storiesFieldMargin.bottom() + outsideHeaderHeight + + st::storiesMaxSize.height() + fieldMinHeight; _layout = _wrap->sizeValue( @@ -134,9 +140,10 @@ void Controller::initLayout() { ? HeaderLayout::Outside : HeaderLayout::Normal; - const auto topSkip = (layout.headerLayout == HeaderLayout::Outside) - ? outsideHeaderHeight - : st::storiesFieldMargin.bottom(); + const auto topSkip = st::storiesFieldMargin.bottom() + + (layout.headerLayout == HeaderLayout::Outside + ? outsideHeaderHeight + : 0); const auto bottomSkip = fieldMinHeight; const auto maxWidth = size.width() - 2 * st::storiesSideSkip; const auto availableHeight = size.height() - topSkip - bottomSkip; @@ -187,6 +194,16 @@ void Controller::initLayout() { layout.controlsWidth, layout.controlsBottomPosition.y()); + const auto siblingSize = layout.content.size() * kSiblingMultiplier; + const auto siblingTop = layout.content.y() + + (layout.content.height() - siblingSize.height()) / 2; + layout.siblingLeft = QRect( + { -siblingSize.width() / 3, siblingTop }, + siblingSize); + layout.siblingRight = QRect( + { size.width() - (2 * siblingSize.width() / 3), siblingTop }, + siblingSize); + return layout; }); } @@ -214,11 +231,19 @@ auto Controller::stickerOrEmojiChosen() const return _delegate->storiesStickerOrEmojiChosen(); } -void Controller::show(const Data::StoriesList &list, int index) { - Expects(index < list.items.size()); +void Controller::show( + const std::vector &lists, + int index, + int subindex) { + Expects(index >= 0 && index < lists.size()); + Expects(subindex >= 0 && subindex < lists[index].items.size()); - const auto &item = list.items[index]; + showSiblings(lists, index); + + const auto &list = lists[index]; + const auto &item = list.items[subindex]; const auto guard = gsl::finally([&] { + _started = false; if (v::is>(item.media.data)) { _photoPlayback = std::make_unique(this); } else { @@ -228,7 +253,7 @@ void Controller::show(const Data::StoriesList &list, int index) { if (_list != list) { _list = list; } - _index = index; + _index = subindex; const auto id = Data::FullStoryId{ .user = list.user, @@ -240,11 +265,34 @@ void Controller::show(const Data::StoriesList &list, int index) { _shown = id; _header->show({ .user = list.user, .date = item.date }); - _slider->show({ .index = index, .total = int(list.items.size()) }); + _slider->show({ .index = _index, .total = list.total }); _replyArea->show({ .user = list.user }); } +void Controller::showSiblings( + const std::vector &lists, + int index) { + showSibling(_siblingLeft, (index > 0) ? &lists[index - 1] : nullptr); + showSibling( + _siblingRight, + (index + 1 < lists.size()) ? &lists[index + 1] : nullptr); +} + +void Controller::showSibling( + std::unique_ptr &sibling, + const Data::StoriesList *list) { + if (!list || list->items.empty()) { + sibling = nullptr; + } else if (!sibling || !sibling->shows(*list)) { + sibling = std::make_unique(this, *list); + } +} + void Controller::ready() { + if (_started) { + return; + } + _started = true; if (_photoPlayback) { _photoPlayback->togglePaused(false); } @@ -262,25 +310,28 @@ void Controller::updatePlayback(const Player::TrackState &state) { _slider->updatePlayback(state); updatePowerSaveBlocker(state); if (Player::IsStoppedAtEnd(state.state)) { - if (!jumpFor(1)) { + if (!subjumpFor(1)) { _delegate->storiesJumpTo({}); } } } -bool Controller::jumpAvailable(int delta) const { - if (delta == -1) { - // Always allow to jump back for one. - // In case of the first story just jump to the beginning. - return _list && !_list->items.empty(); - } +bool Controller::subjumpAvailable(int delta) const { const auto index = _index + delta; + if (index < 0) { + return _siblingLeft && _siblingLeft->shownId().valid(); + } else if (index >= _list->total) { + return _siblingRight && _siblingRight->shownId().valid(); + } return index >= 0 && index < _list->total; } -bool Controller::jumpFor(int delta) { - if (!_index && delta == -1) { - if (!_list || _list->items.empty()) { +bool Controller::subjumpFor(int delta) { + const auto index = _index + delta; + if (index < 0) { + if (_siblingLeft->shownId().valid()) { + return jumpFor(-1); + } else if (!_list || _list->items.empty()) { return false; } _delegate->storiesJumpTo({ @@ -288,10 +339,8 @@ bool Controller::jumpFor(int delta) { .id = _list->items.front().id }); return true; - } - const auto index = _index + delta; - if (index < 0 || index >= _list->total) { - return false; + } else if (index >= _list->total) { + return _siblingRight->shownId().valid() && jumpFor(1); } else if (index < _list->items.size()) { // #TODO stories load more _delegate->storiesJumpTo({ @@ -302,6 +351,22 @@ bool Controller::jumpFor(int delta) { return true; } + +bool Controller::jumpFor(int delta) { + if (delta == -1) { + if (const auto left = _siblingLeft.get()) { + _delegate->storiesJumpTo(left->shownId()); + return true; + } + } else if (delta == 1) { + if (const auto right = _siblingRight.get()) { + _delegate->storiesJumpTo(right->shownId()); + return true; + } + } + return false; +} + bool Controller::paused() const { return _photoPlayback ? _photoPlayback->paused() @@ -316,6 +381,30 @@ void Controller::togglePaused(bool paused) { } } +void Controller::repaintSibling(not_null sibling) { + if (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) { + _delegate->storiesRepaint(); + } +} + +SiblingView Controller::siblingLeft() const { + if (const auto value = _siblingLeft.get()) { + return { value->image(), _layout.current()->siblingLeft }; + } + return {}; +} + +SiblingView Controller::siblingRight() const { + if (const auto value = _siblingRight.get()) { + return { value->image(), _layout.current()->siblingRight }; + } + return {}; +} + +rpl::lifetime &Controller::lifetime() { + return _lifetime; +} + void Controller::updatePowerSaveBlocker(const Player::TrackState &state) { const auto block = !Player::IsPausedOrPausing(state.state) && !Player::IsStoppedOrStopping(state.state); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 2d9abb80c..dc80c4667 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -35,7 +35,9 @@ namespace Media::Stories { class Header; class Slider; class ReplyArea; +class Sibling; class Delegate; +struct SiblingView; enum class HeaderLayout { Normal, @@ -50,6 +52,8 @@ struct Layout { QPoint controlsBottomPosition; QRect autocompleteRect; HeaderLayout headerLayout = HeaderLayout::Normal; + QRect siblingLeft; + QRect siblingRight; friend inline auto operator<=>(Layout, Layout) = default; friend inline bool operator==(Layout, Layout) = default; @@ -68,16 +72,26 @@ public: [[nodiscard]] auto stickerOrEmojiChosen() const -> rpl::producer; - void show(const Data::StoriesList &list, int index); + void show( + const std::vector &lists, + int index, + int subindex); void ready(); void updateVideoPlayback(const Player::TrackState &state); - [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool subjumpAvailable(int delta) const; + [[nodiscard]] bool subjumpFor(int delta); [[nodiscard]] bool jumpFor(int delta); [[nodiscard]] bool paused() const; void togglePaused(bool paused); + void repaintSibling(not_null sibling); + [[nodiscard]] SiblingView siblingLeft() const; + [[nodiscard]] SiblingView siblingRight() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + private: class PhotoPlayback; @@ -86,6 +100,13 @@ private: void updatePlayback(const Player::TrackState &state); void updatePowerSaveBlocker(const Player::TrackState &state); + void showSiblings( + const std::vector &lists, + int index); + void showSibling( + std::unique_ptr &sibling, + const Data::StoriesList *list); + const not_null _delegate; rpl::variable> _layout; @@ -94,11 +115,15 @@ private: const std::unique_ptr
_header; const std::unique_ptr _slider; const std::unique_ptr _replyArea; + std::unique_ptr _photoPlayback; Data::FullStoryId _shown; std::optional _list; int _index = 0; - std::unique_ptr _photoPlayback; + bool _started = false; + + std::unique_ptr _siblingLeft; + std::unique_ptr _siblingRight; std::unique_ptr _powerSaveBlocker; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index 3a2d700f6..256aff2f2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -37,6 +37,7 @@ public: virtual void storiesJumpTo(Data::FullStoryId id) = 0; [[nodiscard]] virtual bool storiesPaused() = 0; virtual void storiesTogglePaused(bool paused) = 0; + virtual void storiesRepaint() = 0; }; } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp new file mode 100644 index 000000000..006485088 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -0,0 +1,262 @@ +/* +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/stories/media_stories_sibling.h" + +#include "base/weak_ptr.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "media/stories/media_stories_controller.h" +#include "media/streaming/media_streaming_instance.h" +#include "media/streaming/media_streaming_player.h" + +namespace Media::Stories { +namespace { + +constexpr auto kGoodFadeDuration = crl::time(200); + +} // namespace + +class Sibling::Loader { +public: + virtual ~Loader() = default; + + virtual QImage blurred() = 0; + virtual QImage good() = 0; +}; + +class Sibling::LoaderPhoto final : public Sibling::Loader { +public: + LoaderPhoto( + not_null photo, + Data::FileOrigin origin, + Fn update); + + QImage blurred() override; + QImage good() override; + +private: + const not_null _photo; + const Fn _update; + std::shared_ptr _media; + rpl::lifetime _waitingLoading; + +}; + +class Sibling::LoaderVideo final + : public Sibling::Loader + , public base::has_weak_ptr { +public: + LoaderVideo( + not_null video, + Data::FileOrigin origin, + Fn update); + + QImage blurred() override; + QImage good() override; + +private: + void waitForGoodThumbnail(); + bool updateAfterGoodCheck(); + void streamedFailed(); + + const not_null _video; + const Data::FileOrigin _origin; + const Fn _update; + std::shared_ptr _media; + std::unique_ptr _streamed; + rpl::lifetime _waitingGoodGeneration; + bool _checkingGoodInCache = false; + bool _failed = false; + +}; + +Sibling::LoaderPhoto::LoaderPhoto( + not_null photo, + Data::FileOrigin origin, + Fn update) +: _photo(photo) +, _update(std::move(update)) +, _media(_photo->createMediaView()) { + _photo->load(origin, LoadFromCloudOrLocal, true); +} + +QImage Sibling::LoaderPhoto::blurred() { + if (const auto image = _media->thumbnailInline()) { + return image->original(); + } + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::black); + result.setDevicePixelRatio(ratio); + return result; +} + +QImage Sibling::LoaderPhoto::good() { + if (const auto image = _media->image(Data::PhotoSize::Large)) { + return image->original(); + } else if (!_waitingLoading) { + _photo->session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + if (_media->loaded()) { + _update(); + } + }, _waitingLoading); + } + return QImage(); +} + +Sibling::LoaderVideo::LoaderVideo( + not_null video, + Data::FileOrigin origin, + Fn update) +: _video(video) +, _origin(origin) +, _update(std::move( update)) +, _media(_video->createMediaView()) { + _media->goodThumbnailWanted(); +} + +QImage Sibling::LoaderVideo::blurred() { + if (const auto image = _media->thumbnailInline()) { + return image->original(); + } + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::black); + result.setDevicePixelRatio(ratio); + return result; +} + +QImage Sibling::LoaderVideo::good() { + if (const auto image = _media->goodThumbnail()) { + return image->original(); + } else if (!_video->goodThumbnailChecked()) { + if (!_checkingGoodInCache) { + waitForGoodThumbnail(); + } + } else if (_failed) { + return QImage(); + } else if (!_streamed) { + _streamed = std::make_unique( + _video, + _origin, + [] {}); // waitingCallback + _streamed->lockPlayer(); + _streamed->player().updates( + ) | rpl::start_with_next_error([=](Streaming::Update &&update) { + v::match(update.data, [&](Streaming::Information &update) { + _update(); + }, [](const auto &update) { + }); + }, [=](Streaming::Error &&error) { + streamedFailed(); + }, _streamed->lifetime()); + if (_streamed->ready()) { + _update(); + } else if (!_streamed->valid()) { + streamedFailed(); + } + } else if (_streamed->ready()) { + return _streamed->info().video.cover; + } + return QImage(); +} + +void Sibling::LoaderVideo::streamedFailed() { + _failed = true; + _streamed = nullptr; + _update(); +} + +void Sibling::LoaderVideo::waitForGoodThumbnail() { + _checkingGoodInCache = true; + const auto weak = make_weak(this); + _video->owner().cache().get({}, [=](const auto &) { + crl::on_main([=] { + if (const auto strong = weak.get()) { + if (!strong->updateAfterGoodCheck()) { + strong->_video->session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + strong->updateAfterGoodCheck(); + }, strong->_waitingGoodGeneration); + } + } + }); + }); +} + +bool Sibling::LoaderVideo::updateAfterGoodCheck() { + if (!_video->goodThumbnailChecked()) { + return false; + } + _checkingGoodInCache = false; + _waitingGoodGeneration.destroy(); + _update(); + return true; +} + +Sibling::Sibling( + not_null controller, + const Data::StoriesList &list) +: _controller(controller) +, _id{ list.user, list.items.front().id } { + const auto &item = list.items.front(); + const auto &data = item.media.data; + const auto origin = Data::FileOrigin(); + if (const auto video = std::get_if>(&data)) { + _loader = std::make_unique((*video), origin, [=] { + check(); + }); + } else if (const auto photo = std::get_if>(&data)) { + _loader = std::make_unique((*photo), origin, [=] { + check(); + }); + } else { + Unexpected("Media type in stories list."); + } + _blurred = _loader->blurred(); + check(); + _goodShown.stop(); +} + +Sibling::~Sibling() = default; + +Data::FullStoryId Sibling::shownId() const { + return _id; +} + +bool Sibling::shows(const Data::StoriesList &list) const { + Expects(!list.items.empty()); + + return _id == Data::FullStoryId{ list.user, list.items.front().id }; +} + +QImage Sibling::image() const { + return _good.isNull() ? _blurred : _good; +} + +void Sibling::check() { + Expects(_loader != nullptr); + + auto good = _loader->good(); + if (good.isNull()) { + return; + } + _loader = nullptr; + _good = std::move(good); + _goodShown.start([=] { + _controller->repaintSibling(this); + }, 0., 1., kGoodFadeDuration, anim::linear); +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h new file mode 100644 index 000000000..ad3b961d9 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h @@ -0,0 +1,48 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_stories.h" + +#include "ui/effects/animations.h" + +namespace Media::Stories { + +class Controller; + +class Sibling final { +public: + Sibling( + not_null controller, + const Data::StoriesList &list); + ~Sibling(); + + [[nodiscard]] Data::FullStoryId shownId() const; + [[nodiscard]] bool shows(const Data::StoriesList &list) const; + + [[nodiscard]] QImage image() const; + +private: + class Loader; + class LoaderPhoto; + class LoaderVideo; + + void check(); + + const not_null _controller; + + Data::FullStoryId _id; + QImage _blurred; + QImage _good; + Ui::Animations::Simple _goodShown; + + std::unique_ptr _loader; + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp index 57f16ddd1..f4dc57956 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp @@ -136,7 +136,9 @@ void Slider::paint(QRectF clip) { radius, radius); } else { - p.setOpacity(kOpacityInactive); + p.setOpacity((i < _data.index) + ? kOpacityActive + : kOpacityInactive); p.drawRoundedRect(_rects[i], radius, radius); } } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index cfe82693c..19b4fddfe 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -21,8 +21,11 @@ View::View(not_null delegate) View::~View() = default; -void View::show(const Data::StoriesList &list, int index) { - _controller->show(list, index); +void View::show( + const std::vector &lists, + int index, + int subindex) { + _controller->show(lists, index, subindex); } void View::ready() { @@ -33,12 +36,23 @@ QRect View::contentGeometry() const { return _controller->layout().content; } +rpl::producer View::contentGeometryValue() const { + return _controller->layoutValue( + ) | rpl::map([=](const Layout &layout) { + return layout.content; + }) | rpl::distinct_until_changed(); +} + void View::updatePlayback(const Player::TrackState &state) { _controller->updateVideoPlayback(state); } -bool View::jumpAvailable(int delta) const { - return _controller->jumpAvailable(delta); +bool View::subjumpAvailable(int delta) const { + return _controller->subjumpAvailable(delta); +} + +bool View::subjumpFor(int delta) const { + return _controller->subjumpFor(delta); } bool View::jumpFor(int delta) const { @@ -53,4 +67,16 @@ void View::togglePaused(bool paused) { _controller->togglePaused(paused); } +SiblingView View::siblingLeft() const { + return _controller->siblingLeft(); +} + +SiblingView View::siblingRight() const { + return _controller->siblingRight(); +} + +rpl::lifetime &View::lifetime() { + return _controller->lifetime(); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 96f4e0042..f225d42ad 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -20,24 +20,45 @@ namespace Media::Stories { class Delegate; class Controller; +struct SiblingView { + QImage image; + QRect geometry; + + [[nodiscard]] bool valid() const { + return !image.isNull(); + } + explicit operator bool() const { + return valid(); + } +}; + class View final { public: explicit View(not_null delegate); ~View(); - void show(const Data::StoriesList &list, int index); + void show( + const std::vector &lists, + int index, + int subindex); void ready(); [[nodiscard]] QRect contentGeometry() const; + [[nodiscard]] rpl::producer contentGeometryValue() const; + [[nodiscard]] SiblingView siblingLeft() const; + [[nodiscard]] SiblingView siblingRight() const; void updatePlayback(const Player::TrackState &state); - [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool subjumpAvailable(int delta) const; + [[nodiscard]] bool subjumpFor(int delta) const; [[nodiscard]] bool jumpFor(int delta) const; [[nodiscard]] bool paused() const; void togglePaused(bool paused); + [[nodiscard]] rpl::lifetime &lifetime(); + private: const std::unique_ptr _controller; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 19dd46778..3f3c9f595 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -406,10 +406,14 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); storiesMaxSize: size(405px, 720px); +storiesControlSize: 64px; +storiesLeft: icon {{ "mediaview/stories_next-flip_horizontal", mediaviewControlFg }}; +storiesRight: icon {{ "mediaview/stories_next", mediaviewControlFg }}; storiesSliderWidth: 2px; -storiesSliderMargin: margins(8px, 7px, 8px, 11px); +storiesSliderMargin: margins(8px, 7px, 8px, 6px); storiesSliderSkip: 4px; -storiesHeaderMargin: margins(12px, 3px, 12px, 8px); +storiesSliderOutsideSkip: 4px; +storiesHeaderMargin: margins(12px, 4px, 12px, 8px); storiesHeaderPhoto: UserpicButton(defaultUserpicButton) { size: size(28px, 28px); photoSize: 28px; @@ -422,7 +426,7 @@ storiesHeaderNamePosition: point(50px, 0px); storiesHeaderDate: FlatLabel(defaultFlatLabel) { textFg: mediaviewControlFg; } -storiesHeaderDatePosition: point(50px, 16px); +storiesHeaderDatePosition: point(50px, 17px); storiesControlsMinWidth: 200px; storiesFieldMargin: margins(0px, 14px, 0px, 16px); storiesAttach: IconButton(defaultIconButton) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 1af0ea39b..2e037a46c 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -112,6 +112,11 @@ OverlayWidget::RendererGL::RendererGL(not_null owner) _captionImage.invalidate(); invalidateControls(); }, _lifetime); + + _owner->_storiesChanged.events( + ) | rpl::start_with_next([=] { + invalidateControls(); + }, _lifetime); } void OverlayWidget::RendererGL::init( @@ -568,7 +573,8 @@ void OverlayWidget::RendererGL::paintControl( QRect inner, float64 innerOpacity, const style::icon &icon) { - const auto meta = ControlMeta(control); + const auto stories = (_owner->_stories != nullptr); + const auto meta = ControlMeta(control, stories); Assert(meta.icon == &icon); const auto overAlpha = overOpacity * kOverBackgroundOpacity; @@ -626,11 +632,17 @@ void OverlayWidget::RendererGL::paintControl( FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset); } -auto OverlayWidget::RendererGL::ControlMeta(OverState control) +auto OverlayWidget::RendererGL::ControlMeta(OverState control, bool stories) -> Control { switch (control) { - case OverLeftNav: return { 0, &st::mediaviewLeft }; - case OverRightNav: return { 1, &st::mediaviewRight }; + case OverLeftNav: return { + 0, + stories ? &st::storiesLeft : &st::mediaviewLeft + }; + case OverRightNav: return { + 1, + stories ? &st::storiesRight : &st::mediaviewRight + }; case OverSave: return { 2, &st::mediaviewSave }; case OverRotate: return { 3, &st::mediaviewRotate }; case OverMore: return { 4, &st::mediaviewMore }; @@ -642,12 +654,13 @@ void OverlayWidget::RendererGL::validateControls() { if (!_controlsImage.image().isNull()) { return; } + const auto stories = (_owner->_stories != nullptr); const auto metas = { - ControlMeta(OverLeftNav), - ControlMeta(OverRightNav), - ControlMeta(OverSave), - ControlMeta(OverRotate), - ControlMeta(OverMore), + ControlMeta(OverLeftNav, stories), + ControlMeta(OverRightNav, stories), + ControlMeta(OverSave, stories), + ControlMeta(OverRotate, stories), + ControlMeta(OverMore, stories), }; auto maxWidth = 0; auto fullHeight = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index effab47ad..88d66e2d4 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -134,7 +134,9 @@ private: Ui::GL::Image _controlsImage; static constexpr auto kControlsCount = 5; - [[nodiscard]] static Control ControlMeta(OverState control); + [[nodiscard]] static Control ControlMeta( + OverState control, + bool stories); // Last one is for the over circle image. std::array _controlsTextures; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 13615c184..1514a7527 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -813,16 +813,7 @@ void OverlayWidget::updateGeometryToScreen(bool inMove) { } void OverlayWidget::updateControlsGeometry() { - const auto overRect = QRect( - QPoint(), - QSize(st::mediaviewIconOver, st::mediaviewIconOver)); - const auto navSkip = st::mediaviewHeaderTop; - _leftNav = QRect(0, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); - _leftNavOver = style::centerrect(_leftNav, overRect); - _leftNavIcon = style::centerrect(_leftNav, st::mediaviewLeft); - _rightNav = QRect(width() - st::mediaviewControlSize, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); - _rightNavOver = style::centerrect(_rightNav, overRect); - _rightNavIcon = style::centerrect(_rightNav, st::mediaviewRight); + updateNavigationControlsGeometry(); _saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2); _photoRadialRect = QRect(QPoint((width() - st::radialSize.width()) / 2, (height() - st::radialSize.height()) / 2), st::radialSize); @@ -843,6 +834,32 @@ void OverlayWidget::updateControlsGeometry() { update(); } +void OverlayWidget::updateNavigationControlsGeometry() { + const auto overRect = QRect( + QPoint(), + QSize(st::mediaviewIconOver, st::mediaviewIconOver)); + const auto navSize = _stories + ? st::storiesControlSize + : st::mediaviewControlSize; + const auto navSkip = st::mediaviewHeaderTop; + const auto xLeft = _stories ? (_x - navSize) : 0; + const auto xRight = _stories ? (_x + _w) : (width() - navSize); + _leftNav = QRect(xLeft, navSkip, navSize, height() - 2 * navSkip); + _leftNavOver = _stories + ? QRect() + : style::centerrect(_leftNav, overRect); + _leftNavIcon = style::centerrect( + _leftNav, + _stories ? st::storiesLeft : st::mediaviewLeft); + _rightNav = QRect(xRight, navSkip, navSize, height() - 2 * navSkip); + _rightNavOver = _stories + ? QRect() + : style::centerrect(_rightNav, overRect); + _rightNavIcon = style::centerrect( + _rightNav, + _stories ? st::storiesRight : st::mediaviewRight); +} + bool OverlayWidget::topShadowOnTheRight() const { return _topShadowRight.current(); } @@ -1009,8 +1026,8 @@ void OverlayWidget::updateDocSize() { void OverlayWidget::refreshNavVisibility() { if (_stories) { - _leftNavVisible = _stories->jumpAvailable(-1); - _rightNavVisible = _stories->jumpAvailable(1); + _leftNavVisible = _stories->subjumpAvailable(-1); + _rightNavVisible = _stories->subjumpAvailable(1); } else if (_sharedMediaData) { _leftNavVisible = _index && (*_index > 0); _rightNavVisible = _index && (*_index + 1 < _sharedMediaData->size()); @@ -1432,6 +1449,12 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) { + (_over == OverSave ? _saveNavOver : _saveNavIcon) + (_over == OverRotate ? _rotateNavOver : _rotateNavIcon) + (_over == OverMore ? _moreNavOver : _moreNavIcon) + + ((_stories && _over == OverLeftStories) + ? _stories->siblingLeft().geometry + : QRect()) + + ((_stories && _over == OverRightStories) + ? _stories->siblingRight().geometry + : QRect()) + _headerNav + _nameNav + _dateNav @@ -1547,6 +1570,7 @@ void OverlayWidget::resizeContentByScreenSize() { _y = content.y(); _w = content.width(); _h = content.height(); + updateNavigationControlsGeometry(); return; } recountSkipTop(); @@ -3861,14 +3885,16 @@ std::shared_ptr OverlayWidget::storiesShow() { return _widget->_body; } bool valid() const override { - return _widget->_storiesUser != nullptr; + return _widget->_storiesSession != nullptr; } operator bool() const override { return valid(); } Main::Session &session() const override { - return _widget->_storiesUser->session(); + Expects(_widget->_storiesSession != nullptr); + + return *_widget->_storiesSession; } bool paused(ChatHelpers::PauseReason reason) const override { if (_widget->isHidden() @@ -3976,6 +4002,10 @@ void OverlayWidget::storiesTogglePaused(bool paused) { } } +void OverlayWidget::storiesRepaint() { + update(); +} + void OverlayWidget::playbackToggleFullScreen() { Expects(_streamed != nullptr); @@ -4131,6 +4161,22 @@ void OverlayWidget::paint(not_null renderer) { fillTransparentBackground); } paintRadialLoading(renderer); + if (_stories) { + if (const auto left = _stories->siblingLeft()) { + renderer->paintTransformedStaticContent( + left.image, + { .rect = left.geometry }, + false, // semi-transparent + false); // fill transparent background + } + if (const auto right = _stories->siblingRight()) { + renderer->paintTransformedStaticContent( + right.image, + { .rect = right.geometry }, + false, // semi-transparent + false); // fill transparent background + } + } } else { if (_themePreviewShown) { renderer->paintThemePreview(_themePreviewRect); @@ -4420,14 +4466,14 @@ void OverlayWidget::paintControls( _leftNavVisible, _leftNavOver, _leftNavIcon, - st::mediaviewLeft, + _stories ? st::storiesLeft : st::mediaviewLeft, true }, { OverRightNav, _rightNavVisible, _rightNavOver, _rightNavIcon, - st::mediaviewRight, + _stories ? st::storiesRight : st::mediaviewRight, true }, { OverSave, @@ -4470,6 +4516,10 @@ void OverlayWidget::paintControls( float64 OverlayWidget::controlOpacity( float64 progress, bool nonbright) const { + if (nonbright && _stories) { + return progress * kStoriesNavOverOpacity + + (1. - progress) * kStoriesNavOpacity; + } const auto normal = _windowed ? kNormalIconOpacity : kMaximizedIconOpacity; @@ -4813,15 +4863,13 @@ void OverlayWidget::setContext( _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); - _stories = nullptr; - _storiesUser = nullptr; + setStoriesUser(nullptr); } else if (const auto peer = std::get_if>(&context)) { _peer = *peer; _history = _peer->owner().history(_peer); _message = nullptr; _topicRootId = MsgId(); - _stories = nullptr; - _storiesUser = nullptr; + setStoriesUser(nullptr); } else if (const auto story = std::get_if(&context)) { _message = nullptr; _topicRootId = MsgId(); @@ -4837,18 +4885,14 @@ void OverlayWidget::setContext( i->items, story->id, &Data::StoryItem::id); - _storiesUser = story->user; - if (!_stories) { - _stories = std::make_unique( - static_cast(this)); - } - _stories->show(*i, j - begin(i->items)); + setStoriesUser(story->user); + _stories->show(all, (i - begin(all)), j - begin(i->items)); } else { _message = nullptr; _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; - _stories = nullptr; + setStoriesUser(nullptr); } _migrated = nullptr; if (_history) { @@ -4863,6 +4907,27 @@ void OverlayWidget::setContext( _user = _peer ? _peer->asUser() : nullptr; } +void OverlayWidget::setStoriesUser(UserData *user) { + const auto session = user ? &user->session() : nullptr; + if (!session && !_storiesSession) { + Assert(!_stories); + } else if (!user) { + _stories = nullptr; + _storiesSession = nullptr; + _storiesChanged.fire({}); + } else if (_storiesSession != session) { + _stories = nullptr; + _storiesSession = session; + const auto delegate = static_cast(this); + _stories = std::make_unique(delegate); + _stories->contentGeometryValue( + ) | rpl::skip(1) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _stories->lifetime()); + _storiesChanged.fire({}); + } +} + void OverlayWidget::setSession(not_null session) { if (_session == session) { return; @@ -4908,7 +4973,7 @@ void OverlayWidget::setSession(not_null session) { bool OverlayWidget::moveToNext(int delta) { if (_stories) { - return _stories->jumpFor(delta); + return _stories->subjumpFor(delta); } else if (!_index) { return false; } @@ -4991,9 +5056,14 @@ void OverlayWidget::handleMousePress( if (button == Qt::LeftButton) { _down = OverNone; if (!ClickHandler::getPressed()) { - if (_over == OverLeftNav && moveToNext(-1)) { - _lastAction = position; - } else if (_over == OverRightNav && moveToNext(1)) { + if ((_over == OverLeftNav && moveToNext(-1)) + || (_over == OverRightNav && moveToNext(1)) + || (_stories + && _over == OverLeftStories + && _stories->jumpFor(-1)) + || (_stories + && _over == OverRightStories + && _stories->jumpFor(1))) { _lastAction = position; } else if (_over == OverName || _over == OverDate @@ -5082,8 +5152,18 @@ void OverlayWidget::handleMouseMove(QPoint position) { void OverlayWidget::updateOverRect(OverState state) { switch (state) { - case OverLeftNav: update(_leftNavOver); break; - case OverRightNav: update(_rightNavOver); break; + case OverLeftNav: + update(_stories ? _leftNavIcon : _leftNavOver); + break; + case OverRightNav: + update(_stories ? _rightNavIcon : _rightNavOver); + break; + case OverLeftStories: + update(_stories ? _stories->siblingLeft().geometry : QRect()); + break; + case OverRightStories: + update(_stories ? _stories->siblingRight().geometry : QRect()); + break; case OverName: update(_nameNav); break; case OverDate: update(_dateNav); break; case OverSave: update(_saveNavOver); break; @@ -5170,6 +5250,10 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverVideo); } else if (_leftNavVisible && _leftNav.contains(pos)) { updateOverState(OverLeftNav); + } else if (_stories && _stories->siblingLeft().geometry.contains(pos)) { + updateOverState(OverLeftStories); + } else if (_stories && _stories->siblingRight().geometry.contains(pos)) { + updateOverState(OverRightStories); } else if (_rightNavVisible && _rightNav.contains(pos)) { updateOverState(OverRightNav); } else if (!_stories && _from && _nameNav.contains(pos)) { @@ -5527,6 +5611,7 @@ void OverlayWidget::clearBeforeHide() { _collage = nullptr; _collageData = std::nullopt; clearStreaming(); + setStoriesUser(nullptr); assignMediaPointer(nullptr); _preloadPhotos.clear(); _preloadDocuments.clear(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index ad7506ee1..e0bed4d14 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -137,6 +137,8 @@ private: OverNone, OverLeftNav, OverRightNav, + OverLeftStories, + OverRightStories, OverHeader, OverName, OverDate, @@ -231,6 +233,7 @@ private: void storiesJumpTo(Data::FullStoryId id) override; bool storiesPaused() override; void storiesTogglePaused(bool paused) override; + void storiesRepaint() override; void hideControls(bool force = false); void subscribeToScreenGeometry(); @@ -292,6 +295,7 @@ private: ItemContext, not_null, StoriesContext> context); + void setStoriesUser(UserData *user); void refreshLang(); void showSaveMsgFile(); @@ -332,6 +336,7 @@ private: void updateDocSize(); void updateControls(); void updateControlsGeometry(); + void updateNavigationControlsGeometry(); using MenuCallback = Fn _stories; - UserData *_storiesUser = nullptr; + rpl::event_stream<> _storiesChanged; + Main::Session *_storiesSession = nullptr; rpl::event_stream _storiesStickerOrEmojiChosen; std::unique_ptr _layerBg; diff --git a/Telegram/SourceFiles/platform/platform_overlay_widget.h b/Telegram/SourceFiles/platform/platform_overlay_widget.h index 2bc2eea33..97b6ef7ef 100644 --- a/Telegram/SourceFiles/platform/platform_overlay_widget.h +++ b/Telegram/SourceFiles/platform/platform_overlay_widget.h @@ -20,6 +20,8 @@ namespace Media::View { inline constexpr auto kMaximizedIconOpacity = 0.6; inline constexpr auto kNormalIconOpacity = 0.9; inline constexpr auto kOverBackgroundOpacity = 0.2775; +inline constexpr auto kStoriesNavOpacity = 0.3; +inline constexpr auto kStoriesNavOverOpacity = 0.7; [[nodiscard]] QColor OverBackgroundColor(); } // namespace Media::View From 2bc7f465c22e3c8ea8b5f1a69000a83f3390e5f8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 9 May 2023 19:34:04 +0400 Subject: [PATCH 012/260] Hide "Close friends" in privacy edit by default. --- Telegram/SourceFiles/boxes/edit_privacy_box.cpp | 4 ++++ Telegram/SourceFiles/boxes/edit_privacy_box.h | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index b30ecfb0c..51be4cd11 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -112,6 +112,10 @@ std::unique_ptr PrivacyExceptionsBoxControl } // namespace +bool EditPrivacyController::hasOption(Option option) const { + return (option != Option::CloseFriends); +} + QString EditPrivacyController::optionLabel(Option option) const { switch (option) { case Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now); diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index 6a78c41ae..d9ba7dab9 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -41,9 +41,7 @@ public: [[nodiscard]] virtual Key key() const = 0; [[nodiscard]] virtual rpl::producer title() const = 0; - [[nodiscard]] virtual bool hasOption(Option option) const { - return true; - } + [[nodiscard]] virtual bool hasOption(Option option) const; [[nodiscard]] virtual rpl::producer optionsTitleKey() const = 0; [[nodiscard]] virtual QString optionLabel(Option option) const; [[nodiscard]] virtual rpl::producer warning() const { From 2212b55b1377a5b40214bf5090d829787fa5a97b Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 9 May 2023 19:37:40 +0400 Subject: [PATCH 013/260] Allow downloading my own stories. --- .../SourceFiles/media/stories/media_stories_controller.cpp | 5 +++++ .../SourceFiles/media/stories/media_stories_controller.h | 2 ++ Telegram/SourceFiles/media/stories/media_stories_view.cpp | 4 ++++ Telegram/SourceFiles/media/stories/media_stories_view.h | 1 + .../SourceFiles/media/view/media_view_overlay_widget.cpp | 3 ++- 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index c653ffb7f..29d4f07c0 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/power_save_blocker.h" #include "data/data_stories.h" +#include "data/data_user.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" #include "media/stories/media_stories_sibling.h" @@ -381,6 +382,10 @@ void Controller::togglePaused(bool paused) { } } +bool Controller::canDownload() const { + return _list && _list->user->isSelf(); +} + void Controller::repaintSibling(not_null sibling) { if (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) { _delegate->storiesRepaint(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index dc80c4667..3bfadaa88 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -86,6 +86,8 @@ public: [[nodiscard]] bool paused() const; void togglePaused(bool paused); + [[nodiscard]] bool canDownload() const; + void repaintSibling(not_null sibling); [[nodiscard]] SiblingView siblingLeft() const; [[nodiscard]] SiblingView siblingRight() const; diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 19b4fddfe..37454aa75 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -32,6 +32,10 @@ void View::ready() { _controller->ready(); } +bool View::canDownload() const { + return _controller->canDownload(); +} + QRect View::contentGeometry() const { return _controller->layout().content; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index f225d42ad..e2399c483 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -43,6 +43,7 @@ public: int subindex); void ready(); + [[nodiscard]] bool canDownload() const; [[nodiscard]] QRect contentGeometry() const; [[nodiscard]] rpl::producer contentGeometryValue() const; [[nodiscard]] SiblingView siblingLeft() const; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 1514a7527..ab14487a5 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1044,7 +1044,8 @@ void OverlayWidget::refreshNavVisibility() { } bool OverlayWidget::contentCanBeSaved() const { - if (_stories || hasCopyMediaRestriction()) { + if ((_stories && !_stories->canDownload()) + || hasCopyMediaRestriction()) { return false; } else if (_photo) { return _photo->hasVideo() || _photoMedia->loaded(); From 0ca40e9d34228992a981c957ca971fc0ba64a5a6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 9 May 2023 21:40:06 +0400 Subject: [PATCH 014/260] Fix build with Xcode. --- Telegram/SourceFiles/data/data_stories.cpp | 2 +- Telegram/SourceFiles/media/stories/media_stories_controller.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index e7274fb04..827ec2e18 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -108,7 +108,7 @@ std::optional Stories::parse(const MTPDstoryItem &data) { const auto date = data.vdate().v; return StoryItem{ .id = data.vid().v, - .media = *media, + .media = { *media }, .caption = std::move(caption), .date = date, .privacy = privacy, diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 3bfadaa88..762554a43 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -55,7 +55,6 @@ struct Layout { QRect siblingLeft; QRect siblingRight; - friend inline auto operator<=>(Layout, Layout) = default; friend inline bool operator==(Layout, Layout) = default; }; From bab66c4ff66562c88e90218df1fab127f47602d9 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 10 May 2023 14:33:17 +0400 Subject: [PATCH 015/260] Darken and pause on reply field focus. --- .../history_view_compose_controls.cpp | 14 ++++++++- .../controls/history_view_compose_controls.h | 2 ++ .../stories/media_stories_controller.cpp | 23 ++++++++++++++ .../media/stories/media_stories_controller.h | 7 +++++ .../media/stories/media_stories_reply.cpp | 6 +++- .../media/stories/media_stories_reply.h | 2 ++ .../media/stories/media_stories_view.cpp | 4 +++ .../media/stories/media_stories_view.h | 1 + .../media/view/media_view_overlay_opengl.cpp | 31 ++++++++++++------- .../media/view/media_view_overlay_opengl.h | 4 ++- .../media/view/media_view_overlay_widget.cpp | 5 +-- .../media/view/media_view_overlay_widget.h | 1 + 12 files changed, 83 insertions(+), 17 deletions(-) diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 6c0f635e8..a4edc44cd 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1108,6 +1108,11 @@ bool ComposeControls::focus() { return true; } +rpl::producer ComposeControls::focusedValue() const { + return rpl::single(Ui::InFocusChain(_wrap.get())) + | rpl::then(_focusChanges.events()); +} + rpl::producer<> ComposeControls::cancelRequests() const { return _cancelRequests.events(); } @@ -1596,6 +1601,8 @@ void ComposeControls::initKeyHandler() { }); return Result::Cancel; } + } else if (k->key() == Qt::Key_Escape) { + return Result::Cancel; } return Result::Continue; }); @@ -1608,7 +1615,12 @@ void ComposeControls::initField() { Ui::Connect(_field, &Ui::InputField::cancelled, [=] { escape(); }); Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); }); Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); }); - //Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); }); + Ui::Connect(_field, &Ui::InputField::focused, [=] { + _focusChanges.fire(true); + }); + Ui::Connect(_field, &Ui::InputField::blurred, [=] { + _focusChanges.fire(false); + }); Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); }); InitMessageField(_show, _field, [=](not_null emoji) { if (_history && Data::AllowEmojiWithoutPremium(_history->peer)) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 2f5b12491..16a114ec8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -136,6 +136,7 @@ public: [[nodiscard]] int heightCurrent() const; bool focus(); + [[nodiscard]] rpl::producer focusedValue() const; [[nodiscard]] rpl::producer<> cancelRequests() const; [[nodiscard]] rpl::producer sendRequests() const; [[nodiscard]] rpl::producer sendVoiceRequests() const; @@ -390,6 +391,7 @@ private: std::unique_ptr _preview; Fn _raiseEmojiSuggestions; + rpl::event_stream _focusChanges; rpl::lifetime _historyLifetime; rpl::lifetime _uploaderSubscriptions; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 29d4f07c0..344b41738 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -29,6 +29,7 @@ namespace { constexpr auto kPhotoProgressInterval = crl::time(100); constexpr auto kPhotoDuration = 5 * crl::time(1000); constexpr auto kSiblingMultiplier = 0.448; +constexpr auto kFullContentFade = 0.2; } // namespace @@ -108,6 +109,19 @@ Controller::Controller(not_null delegate) , _slider(std::make_unique(this)) , _replyArea(std::make_unique(this)) { initLayout(); + + _replyArea->focusedValue( + ) | rpl::start_with_next([=](bool focused) { + _contentFaded = focused; + _contentFadeAnimation.start( + [=] { _delegate->storiesRepaint(); }, + focused ? 0. : 1., + focused ? 1. : 0., + st::fadeWrapDuration); + togglePaused(focused); + }, _lifetime); + + _contentFadeAnimation.stop(); } Controller::~Controller() = default; @@ -223,6 +237,11 @@ rpl::producer Controller::layoutValue() const { return _layout.value() | rpl::filter_optional(); } +float64 Controller::contentFade() const { + return _contentFadeAnimation.value(_contentFaded ? 1. : 0.) + * kFullContentFade; +} + std::shared_ptr Controller::uiShow() const { return _delegate->storiesShow(); } @@ -406,6 +425,10 @@ SiblingView Controller::siblingRight() const { return {}; } +void Controller::unfocusReply() { + _wrap->setFocus(); +} + rpl::lifetime &Controller::lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 762554a43..9e3791442 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "data/data_stories.h" +#include "ui/effects/animations.h" namespace base { class PowerSaveBlocker; @@ -66,6 +67,7 @@ public: [[nodiscard]] not_null wrap() const; [[nodiscard]] Layout layout() const; [[nodiscard]] rpl::producer layoutValue() const; + [[nodiscard]] float64 contentFade() const; [[nodiscard]] std::shared_ptr uiShow() const; [[nodiscard]] auto stickerOrEmojiChosen() const @@ -91,6 +93,8 @@ public: [[nodiscard]] SiblingView siblingLeft() const; [[nodiscard]] SiblingView siblingRight() const; + void unfocusReply(); + [[nodiscard]] rpl::lifetime &lifetime(); private: @@ -118,6 +122,9 @@ private: const std::unique_ptr _replyArea; std::unique_ptr _photoPlayback; + Ui::Animations::Simple _contentFadeAnimation; + bool _contentFaded = false; + Data::FullStoryId _shown; std::optional _list; int _index = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index e4a24807c..000635db6 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -68,7 +68,7 @@ void ReplyArea::chooseAttach(std::optional overrideCompress) { void ReplyArea::initActions() { _controls->cancelRequests( ) | rpl::start_with_next([=] { - // #TODO stories + _controller->unfocusReply(); }, _lifetime); _controls->sendRequests( @@ -157,6 +157,10 @@ void ReplyArea::show(ReplyAreaData data) { _controls->clear(); } +rpl::producer ReplyArea::focusedValue() const { + return _controls->focusedValue(); +} + void ReplyArea::showPremiumToast(not_null emoji) { // #TODO stories } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index 571c9c88c..9587e2e07 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -40,6 +40,8 @@ public: void show(ReplyAreaData data); + [[nodiscard]] rpl::producer focusedValue() const; + private: using VoiceToSend = HistoryView::Controls::VoiceToSend; diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 37454aa75..dbcf9cdf4 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -47,6 +47,10 @@ rpl::producer View::contentGeometryValue() const { }) | rpl::distinct_until_changed(); } +float64 View::contentFade() const { + return _controller->contentFade(); +} + void View::updatePlayback(const Player::TrackState &state) { _controller->updateVideoPlayback(state); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index e2399c483..a11f49436 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -46,6 +46,7 @@ public: [[nodiscard]] bool canDownload() const; [[nodiscard]] QRect contentGeometry() const; [[nodiscard]] rpl::producer contentGeometryValue() const; + [[nodiscard]] float64 contentFade() const; [[nodiscard]] SiblingView siblingLeft() const; [[nodiscard]] SiblingView siblingRight() const; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 2e037a46c..4756c6531 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -35,12 +35,13 @@ constexpr auto kControlValues = 4 * 4 + 4 * 4; // over + icon .header = R"( uniform sampler2D f_texture; uniform vec4 shadowTopRect; -uniform vec2 shadowBottomAndOpacity; +uniform vec3 shadowBottomOpacityFullFade; )", .body = R"( float topHeight = shadowTopRect.w; - float bottomHeight = shadowBottomAndOpacity.x; - float opacity = shadowBottomAndOpacity.y; + float bottomHeight = shadowBottomOpacityFullFade.x; + float opacity = shadowBottomOpacityFullFade.y; + float fullFade = shadowBottomOpacityFullFade.z; float viewportHeight = shadowTopRect.y + topHeight; float fullHeight = topHeight + bottomHeight; float topY = min( @@ -50,7 +51,8 @@ uniform vec2 shadowBottomAndOpacity; vec4 fadeTop = texture2D(f_texture, vec2(topX, topY)) * opacity; float bottomY = max(fullHeight - gl_FragCoord.y, topHeight) / fullHeight; vec4 fadeBottom = texture2D(f_texture, vec2(0.5, bottomY)) * opacity; - result.rgb = result.rgb * (1. - fadeTop.a) * (1. - fadeBottom.a); + float fade = min((1. - fadeTop.a) * (1. - fadeBottom.a), fullFade); + result.rgb = result.rgb * fade; )", }; } @@ -113,10 +115,12 @@ OverlayWidget::RendererGL::RendererGL(not_null owner) invalidateControls(); }, _lifetime); - _owner->_storiesChanged.events( - ) | rpl::start_with_next([=] { - invalidateControls(); - }, _lifetime); + crl::on_main(this, [=] { + _owner->_storiesChanged.events( + ) | rpl::start_with_next([=] { + invalidateControls(); + }, _lifetime); + }); } void OverlayWidget::RendererGL::init( @@ -478,14 +482,17 @@ void OverlayWidget::RendererGL::paintTransformedContent( program->setUniformValue("viewport", _uniformViewport); const auto &top = st::mediaviewShadowTop.size(); - const auto point = QPoint(_shadowTopFlip ? 0 : (_viewport.width() - top.width()), 0); + const auto point = QPoint( + _shadowTopFlip ? 0 : (_viewport.width() - top.width()), + 0); program->setUniformValue( "shadowTopRect", Uniform(transformRect(QRect(point, top)))); const auto &bottom = st::mediaviewShadowBottom; - program->setUniformValue( - "shadowBottomAndOpacity", - QVector2D(bottom.height() * _factor, geometry.controlsOpacity)); + program->setUniformValue("shadowBottomOpacityFullFade", QVector3D( + bottom.height() * _factor, + geometry.controlsOpacity, + 1.f - float(geometry.fade))); FillTexturedRectangle(*_f, &*program); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index 88d66e2d4..a171752de 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -15,7 +15,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media::View { -class OverlayWidget::RendererGL final : public OverlayWidget::Renderer { +class OverlayWidget::RendererGL final + : public OverlayWidget::Renderer + , public base::has_weak_ptr { public: explicit RendererGL(not_null owner); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index ab14487a5..8c404e65d 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1492,6 +1492,7 @@ QRect OverlayWidget::finalContentRect() const { } OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { + const auto fade = _stories ? _stories->contentFade() : 0.; const auto controlsOpacity = _controlsOpacity.current(); const auto toRotation = qreal(finalContentRotation()); const auto toRectRotated = QRectF(finalContentRect()); @@ -1504,7 +1505,7 @@ OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { toRectRotated.width()) : toRectRotated; if (!_geometryAnimation.animating()) { - return { toRect, toRotation, controlsOpacity }; + return { toRect, toRotation, controlsOpacity, fade }; } const auto fromRect = _oldGeometry.rect; const auto fromRotation = _oldGeometry.rotation; @@ -1527,7 +1528,7 @@ OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { fromRect.width() + (toRect.width() - fromRect.width()) * progress, fromRect.height() + (toRect.height() - fromRect.height()) * progress ); - return { useRect, useRotation, controlsOpacity }; + return { useRect, useRotation, controlsOpacity, fade }; } void OverlayWidget::updateContentRect() { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index e0bed4d14..61b240659 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -165,6 +165,7 @@ private: QRectF rect; qreal rotation = 0.; qreal controlsOpacity = 0.; + qreal fade = 0.; }; struct StartStreaming { StartStreaming() : continueStreaming(false), startTime(0) { From a0e9e148b0cedbb56eb476685155f2b1eb39b360 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 10 May 2023 23:32:32 +0400 Subject: [PATCH 016/260] Apply rounding to stories. --- .../SourceFiles/media/view/media_view.style | 1 + .../media/view/media_view_overlay_opengl.cpp | 35 +++++++++++++------ .../media/view/media_view_overlay_opengl.h | 3 +- .../media/view/media_view_overlay_widget.cpp | 10 +++--- .../media/view/media_view_overlay_widget.h | 1 + 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 3f3c9f595..5e7a30bb8 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -406,6 +406,7 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); storiesMaxSize: size(405px, 720px); +storiesRadius: 8px; storiesControlSize: 64px; storiesLeft: icon {{ "mediaview/stories_next-flip_horizontal", mediaviewControlFg }}; storiesRight: icon {{ "mediaview/stories_next", mediaviewControlFg }}; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 4756c6531..0ea2f2ee6 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -94,7 +94,7 @@ float roundedCorner() { } )", .body = R"( - result = vec4(roundedCorner()); + result *= roundedCorner(); )", }; } @@ -160,7 +160,8 @@ void OverlayWidget::RendererGL::init( _texturedVertexShader, FragmentShader({ FragmentSampleARGB32Texture(), - FragmentApplyControlsFade() + FragmentApplyControlsFade(), + FragmentRoundedCorners() })); _withTransparencyProgram.emplace(); @@ -179,7 +180,8 @@ void OverlayWidget::RendererGL::init( _texturedVertexShader, FragmentShader({ FragmentSampleYUV420Texture(), - FragmentApplyControlsFade() + FragmentApplyControlsFade(), + FragmentRoundedCorners() })); _nv12Program.emplace(); @@ -188,7 +190,8 @@ void OverlayWidget::RendererGL::init( _texturedVertexShader, FragmentShader({ FragmentSampleNV12Texture(), - FragmentApplyControlsFade() + FragmentApplyControlsFade(), + FragmentRoundedCorners() })); _fillProgram.emplace(); @@ -210,7 +213,10 @@ void OverlayWidget::RendererGL::init( LinkProgram( &*_roundedCornersProgram, VertexShader({ VertexViewportTransform() }), - FragmentShader({ FragmentRoundedCorners() })); + FragmentShader({ + { .body = "result = vec4(1.);" }, + FragmentRoundedCorners(), + })); const auto renderer = reinterpret_cast( f.glGetString(GL_RENDERER)); @@ -369,8 +375,8 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( } program->setUniformValue("f_texture", GLint(nv12 ? 2 : 3)); - toggleBlending(false); - paintTransformedContent(program, geometry); + toggleBlending(geometry.roundRadius > 0.); + paintTransformedContent(program, geometry, false); } void OverlayWidget::RendererGL::paintTransformedStaticContent( @@ -440,13 +446,15 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( program->setUniformValue("s_texture", GLint(0)); program->setUniformValue("f_texture", GLint(1)); - toggleBlending(semiTransparent && !fillTransparentBackground); - paintTransformedContent(&*program, geometry); + toggleBlending((geometry.roundRadius > 0.) + || (semiTransparent && !fillTransparentBackground)); + paintTransformedContent(&*program, geometry, fillTransparentBackground); } void OverlayWidget::RendererGL::paintTransformedContent( not_null program, - ContentGeometry geometry) { + ContentGeometry geometry, + bool fillTransparentBackground) { const auto rect = transformRect(geometry.rect); const auto centerx = rect.x() + rect.width() / 2; const auto centery = rect.y() + rect.height() / 2; @@ -493,7 +501,12 @@ void OverlayWidget::RendererGL::paintTransformedContent( bottom.height() * _factor, geometry.controlsOpacity, 1.f - float(geometry.fade))); - + if (!fillTransparentBackground) { + program->setUniformValue("roundRect", Uniform(rect)); + program->setUniformValue( + "roundRadius", + GLfloat(geometry.roundRadius * _factor)); + } FillTexturedRectangle(*_f, &*program); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index a171752de..9cc233fba 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -51,7 +51,8 @@ private: bool fillTransparentBackground) override; void paintTransformedContent( not_null program, - ContentGeometry geometry); + ContentGeometry geometry, + bool fillTransparentBackground); void paintRadialLoading( QRect inner, bool radial, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 8c404e65d..76111c5e0 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1493,6 +1493,7 @@ QRect OverlayWidget::finalContentRect() const { OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { const auto fade = _stories ? _stories->contentFade() : 0.; + const auto radius = _stories ? float64(st::storiesRadius) : 0.; const auto controlsOpacity = _controlsOpacity.current(); const auto toRotation = qreal(finalContentRotation()); const auto toRectRotated = QRectF(finalContentRect()); @@ -1505,7 +1506,7 @@ OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { toRectRotated.width()) : toRectRotated; if (!_geometryAnimation.animating()) { - return { toRect, toRotation, controlsOpacity, fade }; + return { toRect, toRotation, controlsOpacity, fade, radius }; } const auto fromRect = _oldGeometry.rect; const auto fromRotation = _oldGeometry.rotation; @@ -1528,7 +1529,7 @@ OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { fromRect.width() + (toRect.width() - fromRect.width()) * progress, fromRect.height() + (toRect.height() - fromRect.height()) * progress ); - return { useRect, useRotation, controlsOpacity, fade }; + return { useRect, useRotation, controlsOpacity, fade, radius }; } void OverlayWidget::updateContentRect() { @@ -4164,17 +4165,18 @@ void OverlayWidget::paint(not_null renderer) { } paintRadialLoading(renderer); if (_stories) { + const auto radius = float64(st::storiesRadius); if (const auto left = _stories->siblingLeft()) { renderer->paintTransformedStaticContent( left.image, - { .rect = left.geometry }, + { .rect = left.geometry, .roundRadius = radius }, false, // semi-transparent false); // fill transparent background } if (const auto right = _stories->siblingRight()) { renderer->paintTransformedStaticContent( right.image, - { .rect = right.geometry }, + { .rect = right.geometry, .roundRadius = radius }, false, // semi-transparent false); // fill transparent background } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 61b240659..a95f5735d 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -166,6 +166,7 @@ private: qreal rotation = 0.; qreal controlsOpacity = 0.; qreal fade = 0.; + qreal roundRadius = 0.; }; struct StartStreaming { StartStreaming() : continueStreaming(false), startTime(0) { From 30871ed1167a0ef8c6168650204cfdcce6cee9b8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 May 2023 18:36:59 +0400 Subject: [PATCH 017/260] Show userpic / name on sibling stories. --- .../stories/media_stories_controller.cpp | 92 +++++++++++---- .../media/stories/media_stories_controller.h | 18 ++- .../media/stories/media_stories_delegate.h | 6 + .../media/stories/media_stories_sibling.cpp | 111 +++++++++++++++++- .../media/stories/media_stories_sibling.h | 28 ++++- .../media/stories/media_stories_view.cpp | 16 +-- .../media/stories/media_stories_view.h | 24 +++- .../SourceFiles/media/view/media_view.style | 1 + .../media/view/media_view_overlay_opengl.cpp | 71 +++++++++-- .../media/view/media_view_overlay_opengl.h | 15 ++- .../media/view/media_view_overlay_raster.cpp | 73 +++++++----- .../media/view/media_view_overlay_raster.h | 8 +- .../media/view/media_view_overlay_renderer.h | 13 +- .../media/view/media_view_overlay_widget.cpp | 109 ++++++++++++----- .../media/view/media_view_overlay_widget.h | 4 + 15 files changed, 469 insertions(+), 120 deletions(-) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 344b41738..d54ea6617 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -28,8 +28,11 @@ namespace { constexpr auto kPhotoProgressInterval = crl::time(100); constexpr auto kPhotoDuration = 5 * crl::time(1000); -constexpr auto kSiblingMultiplier = 0.448; constexpr auto kFullContentFade = 0.2; +constexpr auto kSiblingMultiplier = 0.448; +constexpr auto kSiblingOutsidePart = 0.24; +constexpr auto kSiblingUserpicSize = 0.3; +constexpr auto kInnerHeightMultiplier = 1.6; } // namespace @@ -210,14 +213,49 @@ void Controller::initLayout() { layout.controlsBottomPosition.y()); const auto siblingSize = layout.content.size() * kSiblingMultiplier; - const auto siblingTop = layout.content.y() - + (layout.content.height() - siblingSize.height()) / 2; - layout.siblingLeft = QRect( - { -siblingSize.width() / 3, siblingTop }, - siblingSize); - layout.siblingRight = QRect( - { size.width() - (2 * siblingSize.width() / 3), siblingTop }, - siblingSize); + const auto siblingTop = (size.height() - siblingSize.height()) / 2; + const auto outside = int(base::SafeRound( + siblingSize.width() * kSiblingOutsidePart)); + const auto xLeft = -outside; + const auto xRight = size.width() - siblingSize.width() + outside; + const auto userpicSize = int(base::SafeRound( + siblingSize.width() * kSiblingUserpicSize)); + const auto innerHeight = userpicSize * kInnerHeightMultiplier; + const auto userpic = [&](QRect geometry) { + return QRect( + (geometry.width() - userpicSize) / 2, + (geometry.height() - innerHeight) / 2, + userpicSize, + userpicSize + ).translated(geometry.topLeft()); + }; + const auto nameFontSize = st::storiesMaxNameFontSize * contentHeight + / st::storiesMaxSize.height(); + const auto nameBoundingRect = [&](QRect geometry, bool left) { + const auto skipSmall = nameFontSize; + const auto skipBig = skipSmall + outside; + const auto top = userpic(geometry).y() + innerHeight; + return QRect( + left ? skipBig : skipSmall, + (geometry.height() - innerHeight) / 2, + geometry.width() - skipSmall - skipBig, + innerHeight + ).translated(geometry.topLeft()); + }; + const auto left = QRect({ xLeft, siblingTop }, siblingSize); + const auto right = QRect({ xRight, siblingTop }, siblingSize); + layout.siblingLeft = { + .geometry = left, + .userpic = userpic(left), + .nameBoundingRect = nameBoundingRect(left, true), + .nameFontSize = nameFontSize, + }; + layout.siblingRight = { + .geometry = right, + .userpic = userpic(right), + .nameBoundingRect = nameBoundingRect(right, false), + .nameFontSize = nameFontSize, + }; return layout; }); @@ -237,9 +275,13 @@ rpl::producer Controller::layoutValue() const { return _layout.value() | rpl::filter_optional(); } -float64 Controller::contentFade() const { - return _contentFadeAnimation.value(_contentFaded ? 1. : 0.) - * kFullContentFade; +ContentLayout Controller::contentLayout() const { + return { + .geometry = _layout.current()->content, + .fade = (_contentFadeAnimation.value(_contentFaded ? 1. : 0.) + * kFullContentFade), + .radius = float64(st::storiesRadius), + }; } std::shared_ptr Controller::uiShow() const { @@ -349,7 +391,7 @@ bool Controller::subjumpAvailable(int delta) const { bool Controller::subjumpFor(int delta) { const auto index = _index + delta; if (index < 0) { - if (_siblingLeft->shownId().valid()) { + if (_siblingLeft && _siblingLeft->shownId().valid()) { return jumpFor(-1); } else if (!_list || _list->items.empty()) { return false; @@ -360,7 +402,9 @@ bool Controller::subjumpFor(int delta) { }); return true; } else if (index >= _list->total) { - return _siblingRight->shownId().valid() && jumpFor(1); + return _siblingRight + && _siblingRight->shownId().valid() + && jumpFor(1); } else if (index < _list->items.size()) { // #TODO stories load more _delegate->storiesJumpTo({ @@ -411,16 +455,16 @@ void Controller::repaintSibling(not_null sibling) { } } -SiblingView Controller::siblingLeft() const { - if (const auto value = _siblingLeft.get()) { - return { value->image(), _layout.current()->siblingLeft }; - } - return {}; -} - -SiblingView Controller::siblingRight() const { - if (const auto value = _siblingRight.get()) { - return { value->image(), _layout.current()->siblingRight }; +SiblingView Controller::sibling(SiblingType type) const { + const auto &pointer = (type == SiblingType::Left) + ? _siblingLeft + : _siblingRight; + if (const auto value = pointer.get()) { + const auto over = _delegate->storiesSiblingOver(type); + const auto layout = (type == SiblingType::Left) + ? _layout.current()->siblingLeft + : _layout.current()->siblingRight; + return value->view(layout, over); } return {}; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 9e3791442..60c25011d 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -39,12 +39,21 @@ class ReplyArea; class Sibling; class Delegate; struct SiblingView; +enum class SiblingType; +struct ContentLayout; enum class HeaderLayout { Normal, Outside, }; +struct SiblingLayout { + QRect geometry; + QRect userpic; + QRect nameBoundingRect; + int nameFontSize = 0; +}; + struct Layout { QRect content; QRect header; @@ -53,8 +62,8 @@ struct Layout { QPoint controlsBottomPosition; QRect autocompleteRect; HeaderLayout headerLayout = HeaderLayout::Normal; - QRect siblingLeft; - QRect siblingRight; + SiblingLayout siblingLeft; + SiblingLayout siblingRight; friend inline bool operator==(Layout, Layout) = default; }; @@ -67,7 +76,7 @@ public: [[nodiscard]] not_null wrap() const; [[nodiscard]] Layout layout() const; [[nodiscard]] rpl::producer layoutValue() const; - [[nodiscard]] float64 contentFade() const; + [[nodiscard]] ContentLayout contentLayout() const; [[nodiscard]] std::shared_ptr uiShow() const; [[nodiscard]] auto stickerOrEmojiChosen() const @@ -90,8 +99,7 @@ public: [[nodiscard]] bool canDownload() const; void repaintSibling(not_null sibling); - [[nodiscard]] SiblingView siblingLeft() const; - [[nodiscard]] SiblingView siblingRight() const; + [[nodiscard]] SiblingView sibling(SiblingType type) const; void unfocusReply(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index 256aff2f2..8a34c47bd 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -27,6 +27,11 @@ enum class JumpReason { User, }; +enum class SiblingType { + Left, + Right, +}; + class Delegate { public: [[nodiscard]] virtual not_null storiesWrap() = 0; @@ -36,6 +41,7 @@ public: -> rpl::producer = 0; virtual void storiesJumpTo(Data::FullStoryId id) = 0; [[nodiscard]] virtual bool storiesPaused() = 0; + [[nodiscard]] virtual float64 storiesSiblingOver(SiblingType type) = 0; virtual void storiesTogglePaused(bool paused) = 0; virtual void storiesRepaint() = 0; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index 006485088..8558e7980 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -14,15 +14,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" +#include "data/data_user.h" #include "main/main_session.h" #include "media/stories/media_stories_controller.h" +#include "media/stories/media_stories_view.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" +#include "ui/painter.h" +#include "styles/style_media_view.h" namespace Media::Stories { namespace { constexpr auto kGoodFadeDuration = crl::time(200); +constexpr auto kSiblingFade = 0.5; +constexpr auto kSiblingFadeOver = 0.4; +constexpr auto kSiblingNameOpacity = 0.8; +constexpr auto kSiblingNameOpacityOver = 1.; } // namespace @@ -241,8 +249,107 @@ bool Sibling::shows(const Data::StoriesList &list) const { return _id == Data::FullStoryId{ list.user, list.items.front().id }; } -QImage Sibling::image() const { - return _good.isNull() ? _blurred : _good; +SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { + const auto name = nameImage(layout); + return { + .image = _good.isNull() ? _blurred : _good, + .layout = { + .geometry = layout.geometry, + .fade = kSiblingFade * (1 - over) + kSiblingFadeOver * over, + .radius = float64(st::storiesRadius), + }, + .userpic = userpicImage(layout), + .userpicPosition = layout.userpic.topLeft(), + .name = name, + .namePosition = namePosition(layout, name), + .nameOpacity = (kSiblingNameOpacity * (1 - over) + + kSiblingNameOpacityOver * over), + }; +} + +QImage Sibling::userpicImage(const SiblingLayout &layout) { + Expects(_id.user != nullptr); + + const auto ratio = style::DevicePixelRatio(); + const auto size = layout.userpic.width() * ratio; + const auto key = _id.user->userpicUniqueKey(_userpicView); + if (_userpicImage.width() != size || _userpicKey != key) { + _userpicKey = key; + _userpicImage = _id.user->generateUserpicImage(_userpicView, size); + _userpicImage.setDevicePixelRatio(ratio); + } + return _userpicImage; +} + +QImage Sibling::nameImage(const SiblingLayout &layout) { + Expects(_id.user != nullptr); + + if (_nameFontSize != layout.nameFontSize) { + _nameFontSize = layout.nameFontSize; + + const auto family = 0; // Default font family. + const auto font = style::font( + _nameFontSize, + style::internal::FontSemibold, + family); + _name.reset(); + _nameStyle = std::make_unique(style::TextStyle{ + .font = font, + .linkFont = font, + .linkFontOver = font, + }); + }; + const auto text = _id.user->shortName(); + if (_nameText != text) { + _name.reset(); + _nameText = text; + } + if (!_name) { + _nameAvailableWidth = 0; + _name.emplace(*_nameStyle, _nameText); + } + const auto available = layout.nameBoundingRect.width(); + const auto wasCut = (_nameAvailableWidth < _name->maxWidth()); + const auto nowCut = (available < _name->maxWidth()); + if (_nameImage.isNull() + || _nameAvailableWidth != layout.nameBoundingRect.width()) { + _nameAvailableWidth = layout.nameBoundingRect.width(); + if (_nameImage.isNull() || nowCut || wasCut) { + const auto w = std::min(_nameAvailableWidth, _name->maxWidth()); + const auto h = _nameStyle->font->height; + const auto ratio = style::DevicePixelRatio(); + _nameImage = QImage( + QSize(w, h) * ratio, + QImage::Format_ARGB32_Premultiplied); + _nameImage.setDevicePixelRatio(ratio); + _nameImage.fill(Qt::transparent); + auto p = Painter(&_nameImage); + auto hq = PainterHighQualityEnabler(p); + p.setFont(_nameStyle->font); + p.setPen(Qt::white); + _name->drawLeftElided(p, 0, 0, w, w); + } + } + return _nameImage; +} + +QPoint Sibling::namePosition( + const SiblingLayout &layout, + const QImage &image) const { + const auto size = image.size() / image.devicePixelRatio(); + const auto width = size.width(); + const auto left = layout.geometry.x() + + (layout.geometry.width() - width) / 2; + if (left < layout.nameBoundingRect.x()) { + return layout.nameBoundingRect.topLeft(); + } else if (left + width > layout.nameBoundingRect.x() + layout.nameBoundingRect.width()) { + return layout.nameBoundingRect.topLeft() + + QPoint(layout.nameBoundingRect.width() - width, 0); + } + const auto top = layout.nameBoundingRect.y() + + layout.nameBoundingRect.height() + - size.height(); + return QPoint(left, top); } void Sibling::check() { diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h index ad3b961d9..ecbd89de7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.h +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h @@ -10,10 +10,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_stories.h" #include "ui/effects/animations.h" +#include "ui/userpic_view.h" + +namespace style { +struct TextStyle; +} // namespace style namespace Media::Stories { class Controller; +struct SiblingView; +struct SiblingLayout; class Sibling final { public: @@ -25,7 +32,9 @@ public: [[nodiscard]] Data::FullStoryId shownId() const; [[nodiscard]] bool shows(const Data::StoriesList &list) const; - [[nodiscard]] QImage image() const; + [[nodiscard]] SiblingView view( + const SiblingLayout &layout, + float64 over); private: class Loader; @@ -34,6 +43,12 @@ private: void check(); + [[nodiscard]] QImage userpicImage(const SiblingLayout &layout); + [[nodiscard]] QImage nameImage(const SiblingLayout &layout); + [[nodiscard]] QPoint namePosition( + const SiblingLayout &layout, + const QImage &image) const; + const not_null _controller; Data::FullStoryId _id; @@ -41,6 +56,17 @@ private: QImage _good; Ui::Animations::Simple _goodShown; + QImage _userpicImage; + InMemoryKey _userpicKey = {}; + Ui::PeerUserpicView _userpicView; + + QImage _nameImage; + std::unique_ptr _nameStyle; + std::optional _name; + QString _nameText; + int _nameAvailableWidth = 0; + int _nameFontSize = 0; + std::unique_ptr _loader; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index dbcf9cdf4..f5cfd628d 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -36,19 +36,19 @@ bool View::canDownload() const { return _controller->canDownload(); } -QRect View::contentGeometry() const { +QRect View::finalShownGeometry() const { return _controller->layout().content; } -rpl::producer View::contentGeometryValue() const { +rpl::producer View::finalShownGeometryValue() const { return _controller->layoutValue( ) | rpl::map([=](const Layout &layout) { return layout.content; }) | rpl::distinct_until_changed(); } -float64 View::contentFade() const { - return _controller->contentFade(); +ContentLayout View::contentLayout() const { + return _controller->contentLayout(); } void View::updatePlayback(const Player::TrackState &state) { @@ -75,12 +75,8 @@ void View::togglePaused(bool paused) { _controller->togglePaused(paused); } -SiblingView View::siblingLeft() const { - return _controller->siblingLeft(); -} - -SiblingView View::siblingRight() const { - return _controller->siblingRight(); +SiblingView View::sibling(SiblingType type) const { + return _controller->sibling(type); } rpl::lifetime &View::lifetime() { diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index a11f49436..09865c861 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -20,9 +20,22 @@ namespace Media::Stories { class Delegate; class Controller; +struct ContentLayout { + QRect geometry; + float64 fade = 0.; + float64 radius = 0.; +}; + +enum class SiblingType; + struct SiblingView { QImage image; - QRect geometry; + ContentLayout layout; + QImage userpic; + QPoint userpicPosition; + QImage name; + QPoint namePosition; + float64 nameOpacity = 0.; [[nodiscard]] bool valid() const { return !image.isNull(); @@ -44,11 +57,10 @@ public: void ready(); [[nodiscard]] bool canDownload() const; - [[nodiscard]] QRect contentGeometry() const; - [[nodiscard]] rpl::producer contentGeometryValue() const; - [[nodiscard]] float64 contentFade() const; - [[nodiscard]] SiblingView siblingLeft() const; - [[nodiscard]] SiblingView siblingRight() const; + [[nodiscard]] QRect finalShownGeometry() const; + [[nodiscard]] rpl::producer finalShownGeometryValue() const; + [[nodiscard]] ContentLayout contentLayout() const; + [[nodiscard]] SiblingView sibling(SiblingType type) const; void updatePlayback(const Player::TrackState &state); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 5e7a30bb8..c896d253a 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -406,6 +406,7 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); storiesMaxSize: size(405px, 720px); +storiesMaxNameFontSize: 17px; storiesRadius: 8px; storiesControlSize: 64px; storiesLeft: icon {{ "mediaview/stories_next-flip_horizontal", mediaviewControlFg }}; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 0ea2f2ee6..8c1ea6cf9 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/gl/gl_shader.h" #include "ui/painter.h" +#include "media/stories/media_stories_view.h" #include "media/streaming/media_streaming_common.h" #include "platform/platform_overlay_widget.h" #include "base/platform/base_platform_info.h" @@ -133,7 +134,11 @@ void OverlayWidget::RendererGL::init( constexpr auto kRoundingQuads = 4; constexpr auto kRoundingVertices = kRoundingQuads * 6; constexpr auto kRoundingValues = kRoundingVertices * 2; - constexpr auto kValues = kQuadValues + kControlsValues + kRoundingValues; + constexpr auto kStoriesSiblingValues = kStoriesSiblingPartsCount * 16; + constexpr auto kValues = kQuadValues + + kControlsValues + + kRoundingValues + + kStoriesSiblingValues; _contentBuffer.emplace(); _contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw); @@ -315,7 +320,7 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( _streamedIndex = _owner->streamedIndex(); _f->glActiveTexture(GL_TEXTURE0); - _textures.bind(*_f, 1); + _textures.bind(*_f, 3); if (upload) { _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); uploadTexture( @@ -328,7 +333,7 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( _lumaSize = yuv->size; } _f->glActiveTexture(GL_TEXTURE1); - _textures.bind(*_f, 2); + _textures.bind(*_f, 4); if (upload) { uploadTexture( nv12 ? GL_RG : GL_ALPHA, @@ -350,7 +355,7 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( _controlsFadeImage.bind(*_f); } else { _f->glActiveTexture(GL_TEXTURE2); - _textures.bind(*_f, 3); + _textures.bind(*_f, 5); if (upload) { uploadTexture( GL_ALPHA, @@ -383,7 +388,9 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( const QImage &image, ContentGeometry geometry, bool semiTransparent, - bool fillTransparentBackground) { + bool fillTransparentBackground, + int index) { + Expects(index >= 0 && index < 3); Expects(image.isNull() || image.format() == QImage::Format_RGB32 || image.format() == QImage::Format_ARGB32_Premultiplied); @@ -409,11 +416,11 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( } _f->glActiveTexture(GL_TEXTURE0); - _textures.bind(*_f, 0); + _textures.bind(*_f, index); const auto cacheKey = image.isNull() ? qint64(-1) : image.cacheKey(); - const auto upload = (_cacheKey != cacheKey); + const auto upload = (_cacheKeys[index] != cacheKey); if (upload) { - _cacheKey = cacheKey; + _cacheKeys[index] = cacheKey; if (image.isNull()) { // Upload transparent 2x2 texture. const auto stride = 2; @@ -853,6 +860,54 @@ void OverlayWidget::RendererGL::paintRoundedCorners(int radius) { _f->glDisableVertexAttribArray(position); } + +void OverlayWidget::RendererGL::paintStoriesSiblingPart( + int index, + const QImage &image, + QRect rect, + float64 opacity) { + Expects(index >= 0 && index < kStoriesSiblingPartsCount); + + _f->glActiveTexture(GL_TEXTURE0); + + auto &part = _storiesSiblingParts[index]; + part.setImage(image); + part.bind(*_f); + + const auto textured = part.texturedRect( + rect, + QRect(QPoint(), image.size())); + const auto geometry = transformRect(textured.geometry); + const GLfloat coords[] = { + geometry.left(), geometry.top(), + textured.texture.left(), textured.texture.bottom(), + + geometry.right(), geometry.top(), + textured.texture.right(), textured.texture.bottom(), + + geometry.right(), geometry.bottom(), + textured.texture.right(), textured.texture.top(), + + geometry.left(), geometry.bottom(), + textured.texture.left(), textured.texture.top(), + }; + const auto offset = kControlsOffset + + (kControlsCount * kControlValues) / 4 + + (6 * 2 * 4) / 4 // rounding + + (index * 4); + const auto byteOffset = offset * 4 * sizeof(GLfloat); + _contentBuffer->write(byteOffset, coords, sizeof(coords)); + + _controlsProgram->bind(); + _controlsProgram->setUniformValue("viewport", _uniformViewport); + _contentBuffer->write( + offset * 4 * sizeof(GLfloat), + coords, + sizeof(coords)); + _controlsProgram->setUniformValue("g_opacity", GLfloat(opacity)); + FillTexturedRectangle(*_f, &*_controlsProgram, offset); +} + // //void OverlayWidget::RendererGL::invalidate() { // _trackFrameIndex = -1; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index 9cc233fba..b0ba15dc8 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -48,7 +48,8 @@ private: const QImage &image, ContentGeometry geometry, bool semiTransparent, - bool fillTransparentBackground) override; + bool fillTransparentBackground, + int index = 0) override; void paintTransformedContent( not_null program, ContentGeometry geometry, @@ -72,6 +73,11 @@ private: void paintCaption(QRect outer, float64 opacity) override; void paintGroupThumbs(QRect outer, float64 opacity) override; void paintRoundedCorners(int radius) override; + void paintStoriesSiblingPart( + int index, + const QImage &image, + QRect rect, + float64 opacity = 1.) override; //void invalidate(); @@ -117,11 +123,11 @@ private: std::optional _fillProgram; std::optional _controlsProgram; std::optional _roundedCornersProgram; - Ui::GL::Textures<4> _textures; + Ui::GL::Textures<6> _textures; // image, sibling, right sibling, y, u, v QSize _rgbaSize; QSize _lumaSize; QSize _chromaSize; - qint64 _cacheKey = 0; + qint64 _cacheKeys[3] = { 0 }; // image, sibling, right sibling int _trackFrameIndex = 0; int _streamedIndex = 0; bool _chromaNV12 = false; @@ -136,6 +142,9 @@ private: Ui::GL::Image _groupThumbsImage; Ui::GL::Image _controlsImage; + static constexpr auto kStoriesSiblingPartsCount = 4; + Ui::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount]; + static constexpr auto kControlsCount = 5; [[nodiscard]] static Control ControlMeta( OverState control, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp index bd9675380..9a345e3d7 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_overlay_raster.h" #include "ui/painter.h" +#include "media/stories/media_stories_view.h" #include "media/view/media_view_pip.h" #include "platform/platform_overlay_widget.h" #include "styles/style_media_view.h" @@ -15,14 +16,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media::View { OverlayWidget::RendererSW::RendererSW(not_null owner) -: _owner(owner) -, _transparentBrush(style::TransparentPlaceholder()) { + : _owner(owner) + , _transparentBrush(style::TransparentPlaceholder()) { } void OverlayWidget::RendererSW::paintFallback( - Painter &&p, - const QRegion &clip, - Ui::GL::Backend backend) { + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) { _p = &p; _clip = &clip; _clipOuter = clip.boundingRect(); @@ -48,8 +49,8 @@ void OverlayWidget::RendererSW::paintBackground() { } QRect OverlayWidget::RendererSW::TransformRect( - QRectF geometry, - int rotation) { + QRectF geometry, + int rotation) { const auto center = geometry.center(); const auto rect = ((rotation % 180) == 90) ? QRectF( @@ -66,7 +67,7 @@ QRect OverlayWidget::RendererSW::TransformRect( } void OverlayWidget::RendererSW::paintTransformedVideoFrame( - ContentGeometry geometry) { + ContentGeometry geometry) { Expects(_owner->_streamed != nullptr); const auto rotation = int(geometry.rotation); @@ -79,10 +80,11 @@ void OverlayWidget::RendererSW::paintTransformedVideoFrame( } void OverlayWidget::RendererSW::paintTransformedStaticContent( - const QImage &image, - ContentGeometry geometry, - bool semiTransparent, - bool fillTransparentBackground) { + const QImage &image, + ContentGeometry geometry, + bool semiTransparent, + bool fillTransparentBackground, + int index) { const auto rotation = int(geometry.rotation); const auto rect = TransformRect(geometry.rect, rotation); if (!rect.intersects(_clipOuter)) { @@ -99,8 +101,8 @@ void OverlayWidget::RendererSW::paintTransformedStaticContent( } void OverlayWidget::RendererSW::paintControlsFade( - QRect geometry, - float64 opacity) { + QRect geometry, + float64 opacity) { _p->setOpacity(opacity); _p->setClipRect(geometry); const auto width = _owner->width(); @@ -134,9 +136,9 @@ void OverlayWidget::RendererSW::paintControlsFade( } void OverlayWidget::RendererSW::paintTransformedImage( - const QImage &image, - QRect rect, - int rotation) { + const QImage &image, + QRect rect, + int rotation) { PainterHighQualityEnabler hq(*_p); if (UsePainterRotation(rotation)) { if (rotation) { @@ -153,9 +155,9 @@ void OverlayWidget::RendererSW::paintTransformedImage( } void OverlayWidget::RendererSW::paintRadialLoading( - QRect inner, - bool radial, - float64 radialOpacity) { + QRect inner, + bool radial, + float64 radialOpacity) { _owner->paintRadialLoadingContent(*_p, inner, radial, radialOpacity); } @@ -164,8 +166,8 @@ void OverlayWidget::RendererSW::paintThemePreview(QRect outer) { } void OverlayWidget::RendererSW::paintDocumentBubble( - QRect outer, - QRect icon) { + QRect outer, + QRect icon) { if (outer.intersects(_clipOuter)) { _owner->paintDocumentBubbleContent(*_p, outer, icon, _clipOuter); if (icon.intersects(_clipOuter)) { @@ -184,12 +186,12 @@ void OverlayWidget::RendererSW::paintControlsStart() { } void OverlayWidget::RendererSW::paintControl( - OverState control, - QRect over, - float64 overOpacity, - QRect inner, - float64 innerOpacity, - const style::icon &icon) { + OverState control, + QRect over, + float64 overOpacity, + QRect inner, + float64 innerOpacity, + const style::icon &icon) { if (!over.isEmpty() && !over.intersects(_clipOuter)) { return; } @@ -230,6 +232,21 @@ void OverlayWidget::RendererSW::paintRoundedCorners(int radius) { // The RpWindow rounding overlay will do the job. } +void OverlayWidget::RendererSW::paintStoriesSiblingPart( + int index, + const QImage &image, + QRect rect, + float64 opacity) { + const auto changeOpacity = (opacity != 1.); + if (changeOpacity) { + _p->setOpacity(opacity); + } + _p->drawImage(rect, image); + if (changeOpacity) { + _p->setOpacity(1.); + } +} + void OverlayWidget::RendererSW::validateOverControlImage() { const auto size = QSize(st::mediaviewIconOver, st::mediaviewIconOver); const auto alpha = base::SafeRound(kOverBackgroundOpacity * 255); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h index 3df304236..e1b6ca453 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h @@ -27,7 +27,8 @@ private: const QImage &image, ContentGeometry geometry, bool semiTransparent, - bool fillTransparentBackground) override; + bool fillTransparentBackground, + int index = 0) override; void paintTransformedImage( const QImage &image, QRect rect, @@ -52,6 +53,11 @@ private: void paintCaption(QRect outer, float64 opacity) override; void paintGroupThumbs(QRect outer, float64 opacity) override; void paintRoundedCorners(int radius) override; + void paintStoriesSiblingPart( + int index, + const QImage &image, + QRect rect, + float64 opacity = 1.) override; void validateOverControlImage(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h index f4a8cb9bb..cd3ea698e 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_overlay_widget.h" +namespace Media::Stories { +struct SiblingView; +} // namespace Media::Stories + namespace Media::View { class OverlayWidget::Renderer : public Ui::GL::Renderer { @@ -19,7 +23,8 @@ public: const QImage &image, ContentGeometry geometry, bool semiTransparent, - bool fillTransparentBackground) = 0; + bool fillTransparentBackground, + int index = 0) = 0; // image, left sibling, right sibling virtual void paintRadialLoading( QRect inner, bool radial, @@ -39,7 +44,11 @@ public: virtual void paintCaption(QRect outer, float64 opacity) = 0; virtual void paintGroupThumbs(QRect outer, float64 opacity) = 0; virtual void paintRoundedCorners(int radius) = 0; - + virtual void paintStoriesSiblingPart( + int index, + const QImage &image, + QRect rect, + float64 opacity = 1.) = 0; }; } // 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 76111c5e0..f154a93b8 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -125,6 +125,9 @@ constexpr auto kIdsLimit = 48; // Preload next messages if we went further from current than that. constexpr auto kIdsPreloadAfter = 28; +constexpr auto kLeftSiblingTextureIndex = 1; +constexpr auto kRightSiblingTextureIndex = 2; + class PipDelegate final : public Pip::Delegate { public: PipDelegate(QWidget *parent, not_null session); @@ -1444,17 +1447,18 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) { } _helper->setControlsOpacity(_controlsOpacity.current()); const auto content = finalContentRect(); + const auto siblingType = (_over == OverLeftStories) + ? Stories::SiblingType::Left + : Stories::SiblingType::Right; const auto toUpdate = QRegion() + (_over == OverLeftNav ? _leftNavOver : _leftNavIcon) + (_over == OverRightNav ? _rightNavOver : _rightNavIcon) + (_over == OverSave ? _saveNavOver : _saveNavIcon) + (_over == OverRotate ? _rotateNavOver : _rotateNavIcon) + (_over == OverMore ? _moreNavOver : _moreNavIcon) - + ((_stories && _over == OverLeftStories) - ? _stories->siblingLeft().geometry - : QRect()) - + ((_stories && _over == OverRightStories) - ? _stories->siblingRight().geometry + + ((_stories + && (_over == OverLeftStories && _over == OverRightStories)) + ? _stories->sibling(siblingType).layout.geometry : QRect()) + _headerNav + _nameNav @@ -1492,8 +1496,9 @@ QRect OverlayWidget::finalContentRect() const { } OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { - const auto fade = _stories ? _stories->contentFade() : 0.; - const auto radius = _stories ? float64(st::storiesRadius) : 0.; + if (_stories) { + return storiesContentGeometry(_stories->contentLayout()); + } const auto controlsOpacity = _controlsOpacity.current(); const auto toRotation = qreal(finalContentRotation()); const auto toRectRotated = QRectF(finalContentRect()); @@ -1506,7 +1511,7 @@ OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { toRectRotated.width()) : toRectRotated; if (!_geometryAnimation.animating()) { - return { toRect, toRotation, controlsOpacity, fade, radius }; + return { toRect, toRotation, controlsOpacity }; } const auto fromRect = _oldGeometry.rect; const auto fromRotation = _oldGeometry.rotation; @@ -1529,7 +1534,17 @@ OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { fromRect.width() + (toRect.width() - fromRect.width()) * progress, fromRect.height() + (toRect.height() - fromRect.height()) * progress ); - return { useRect, useRotation, controlsOpacity, fade, radius }; + return { useRect, useRotation, controlsOpacity }; +} + +OverlayWidget::ContentGeometry OverlayWidget::storiesContentGeometry( + const Stories::ContentLayout &layout) const { + return { + .rect = QRectF(layout.geometry), + .controlsOpacity = 0., // #TODO stories ?.. + .fade = layout.fade, + .roundRadius = layout.radius, + }; } void OverlayWidget::updateContentRect() { @@ -1568,7 +1583,7 @@ void OverlayWidget::recountSkipTop() { void OverlayWidget::resizeContentByScreenSize() { if (_stories) { - const auto content = _stories->contentGeometry(); + const auto content = _stories->finalShownGeometry(); _x = content.x(); _y = content.y(); _w = content.width(); @@ -4005,6 +4020,11 @@ void OverlayWidget::storiesTogglePaused(bool paused) { } } +float64 OverlayWidget::storiesSiblingOver(Stories::SiblingType type) { + return (type == Stories::SiblingType::Left) + ? overLevel(OverLeftStories) + : overLevel(OverRightStories); +} void OverlayWidget::storiesRepaint() { update(); } @@ -4165,20 +4185,34 @@ void OverlayWidget::paint(not_null renderer) { } paintRadialLoading(renderer); if (_stories) { - const auto radius = float64(st::storiesRadius); - if (const auto left = _stories->siblingLeft()) { + using namespace Stories; + const auto paint = [&](const SiblingView &view, int index) { renderer->paintTransformedStaticContent( - left.image, - { .rect = left.geometry, .roundRadius = radius }, + view.image, + storiesContentGeometry(view.layout), false, // semi-transparent - false); // fill transparent background + false, // fill transparent background + index); + const auto base = (index - 1) * 2; + const auto userpicSize = view.userpic.size() + / view.userpic.devicePixelRatio(); + renderer->paintStoriesSiblingPart( + base, + view.userpic, + QRect(view.userpicPosition, userpicSize)); + const auto nameSize = view.name.size() + / view.name.devicePixelRatio(); + renderer->paintStoriesSiblingPart( + base + 1, + view.name, + QRect(view.namePosition, nameSize), + view.nameOpacity); + }; + if (const auto left = _stories->sibling(SiblingType::Left)) { + paint(left, kLeftSiblingTextureIndex); } - if (const auto right = _stories->siblingRight()) { - renderer->paintTransformedStaticContent( - right.image, - { .rect = right.geometry, .roundRadius = radius }, - false, // semi-transparent - false); // fill transparent background + if (const auto right = _stories->sibling(SiblingType::Right)) { + paint(right, kRightSiblingTextureIndex); } } } else { @@ -4924,7 +4958,7 @@ void OverlayWidget::setStoriesUser(UserData *user) { _storiesSession = session; const auto delegate = static_cast(this); _stories = std::make_unique(delegate); - _stories->contentGeometryValue( + _stories->finalShownGeometryValue( ) | rpl::skip(1) | rpl::start_with_next([=] { updateControlsGeometry(); }, _stories->lifetime()); @@ -5155,6 +5189,7 @@ void OverlayWidget::handleMouseMove(QPoint position) { } void OverlayWidget::updateOverRect(OverState state) { + using Type = Stories::SiblingType; switch (state) { case OverLeftNav: update(_stories ? _leftNavIcon : _leftNavOver); @@ -5163,10 +5198,14 @@ void OverlayWidget::updateOverRect(OverState state) { update(_stories ? _rightNavIcon : _rightNavOver); break; case OverLeftStories: - update(_stories ? _stories->siblingLeft().geometry : QRect()); + update(_stories + ? _stories->sibling(Type::Left).layout.geometry : + QRect()); break; case OverRightStories: - update(_stories ? _stories->siblingRight().geometry : QRect()); + update(_stories + ? _stories->sibling(Type::Right).layout.geometry + : QRect()); break; case OverName: update(_nameNav); break; case OverDate: update(_dateNav); break; @@ -5250,19 +5289,27 @@ void OverlayWidget::updateOver(QPoint pos) { if (_pressed || _dragging) return; + using SiblingType = Stories::SiblingType; if (_fullScreenVideo) { updateOverState(OverVideo); } else if (_leftNavVisible && _leftNav.contains(pos)) { updateOverState(OverLeftNav); - } else if (_stories && _stories->siblingLeft().geometry.contains(pos)) { - updateOverState(OverLeftStories); - } else if (_stories && _stories->siblingRight().geometry.contains(pos)) { - updateOverState(OverRightStories); } else if (_rightNavVisible && _rightNav.contains(pos)) { updateOverState(OverRightNav); + } else if (_stories + && _stories->sibling( + SiblingType::Left).layout.geometry.contains(pos)) { + updateOverState(OverLeftStories); + } else if (_stories + && _stories->sibling( + SiblingType::Right).layout.geometry.contains(pos)) { + updateOverState(OverRightStories); } else if (!_stories && _from && _nameNav.contains(pos)) { updateOverState(OverName); - } else if (!_stories && _message && _message->isRegular() && _dateNav.contains(pos)) { + } else if (!_stories + && _message + && _message->isRegular() + && _dateNav.contains(pos)) { updateOverState(OverDate); } else if (!_stories && _headerHasLink && _headerNav.contains(pos)) { updateOverState(OverHeader); @@ -5270,7 +5317,9 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverSave); } else if (_rotateVisible && _rotateNav.contains(pos)) { updateOverState(OverRotate); - } else if (_document && documentBubbleShown() && _docIconRect.contains(pos)) { + } else if (_document + && documentBubbleShown() + && _docIconRect.contains(pos)) { updateOverState(OverIcon); } else if (_moreNav.contains(pos)) { updateOverState(OverMore); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index a95f5735d..49c4a6df7 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -66,6 +66,7 @@ enum class Error; namespace Media::Stories { class View; +struct ContentLayout; } // namespace Media::Stories namespace Media::View { @@ -235,6 +236,7 @@ private: void storiesJumpTo(Data::FullStoryId id) override; bool storiesPaused() override; void storiesTogglePaused(bool paused) override; + float64 storiesSiblingOver(Stories::SiblingType type) override; void storiesRepaint() override; void hideControls(bool force = false); @@ -390,6 +392,8 @@ private: [[nodiscard]] int finalContentRotation() const; [[nodiscard]] QRect finalContentRect() const; [[nodiscard]] ContentGeometry contentGeometry() const; + [[nodiscard]] ContentGeometry storiesContentGeometry( + const Stories::ContentLayout &layout) const; void updateContentRect(); void contentSizeChanged(); From 0d3df824e37e70ccd7420e1b1e9be3277706b132 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 12 May 2023 10:07:37 +0400 Subject: [PATCH 018/260] Apply stories fade in raster renderer. --- .../media/view/media_view_overlay_raster.cpp | 69 ++++++++++--------- .../media/view/media_view_overlay_raster.h | 5 +- .../media/view/media_view_overlay_widget.cpp | 10 +-- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp index 9a345e3d7..d6d24e1e8 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp @@ -16,14 +16,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media::View { OverlayWidget::RendererSW::RendererSW(not_null owner) - : _owner(owner) - , _transparentBrush(style::TransparentPlaceholder()) { +: _owner(owner) +, _transparentBrush(style::TransparentPlaceholder()) { } void OverlayWidget::RendererSW::paintFallback( - Painter &&p, - const QRegion &clip, - Ui::GL::Backend backend) { + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) { _p = &p; _clip = &clip; _clipOuter = clip.boundingRect(); @@ -49,8 +49,8 @@ void OverlayWidget::RendererSW::paintBackground() { } QRect OverlayWidget::RendererSW::TransformRect( - QRectF geometry, - int rotation) { + QRectF geometry, + int rotation) { const auto center = geometry.center(); const auto rect = ((rotation % 180) == 90) ? QRectF( @@ -67,7 +67,7 @@ QRect OverlayWidget::RendererSW::TransformRect( } void OverlayWidget::RendererSW::paintTransformedVideoFrame( - ContentGeometry geometry) { + ContentGeometry geometry) { Expects(_owner->_streamed != nullptr); const auto rotation = int(geometry.rotation); @@ -76,15 +76,15 @@ void OverlayWidget::RendererSW::paintTransformedVideoFrame( return; } paintTransformedImage(_owner->videoFrame(), rect, rotation); - paintControlsFade(rect, geometry.controlsOpacity); + paintControlsFade(rect, geometry.controlsOpacity, geometry.fade); } void OverlayWidget::RendererSW::paintTransformedStaticContent( - const QImage &image, - ContentGeometry geometry, - bool semiTransparent, - bool fillTransparentBackground, - int index) { + const QImage &image, + ContentGeometry geometry, + bool semiTransparent, + bool fillTransparentBackground, + int index) { const auto rotation = int(geometry.rotation); const auto rect = TransformRect(geometry.rect, rotation); if (!rect.intersects(_clipOuter)) { @@ -97,12 +97,19 @@ void OverlayWidget::RendererSW::paintTransformedStaticContent( if (!image.isNull()) { paintTransformedImage(image, rect, rotation); } - paintControlsFade(rect, geometry.controlsOpacity); + paintControlsFade(rect, geometry.controlsOpacity, geometry.fade); } void OverlayWidget::RendererSW::paintControlsFade( - QRect geometry, - float64 opacity) { + QRect geometry, + float64 opacity, + float64 fullFade) { + if (fullFade > 0.) { + _p->setOpacity(fullFade); + _p->fillRect(geometry, Qt::black); + opacity *= 1. - fullFade; + } + _p->setOpacity(opacity); _p->setClipRect(geometry); const auto width = _owner->width(); @@ -136,9 +143,9 @@ void OverlayWidget::RendererSW::paintControlsFade( } void OverlayWidget::RendererSW::paintTransformedImage( - const QImage &image, - QRect rect, - int rotation) { + const QImage &image, + QRect rect, + int rotation) { PainterHighQualityEnabler hq(*_p); if (UsePainterRotation(rotation)) { if (rotation) { @@ -155,9 +162,9 @@ void OverlayWidget::RendererSW::paintTransformedImage( } void OverlayWidget::RendererSW::paintRadialLoading( - QRect inner, - bool radial, - float64 radialOpacity) { + QRect inner, + bool radial, + float64 radialOpacity) { _owner->paintRadialLoadingContent(*_p, inner, radial, radialOpacity); } @@ -166,8 +173,8 @@ void OverlayWidget::RendererSW::paintThemePreview(QRect outer) { } void OverlayWidget::RendererSW::paintDocumentBubble( - QRect outer, - QRect icon) { + QRect outer, + QRect icon) { if (outer.intersects(_clipOuter)) { _owner->paintDocumentBubbleContent(*_p, outer, icon, _clipOuter); if (icon.intersects(_clipOuter)) { @@ -186,12 +193,12 @@ void OverlayWidget::RendererSW::paintControlsStart() { } void OverlayWidget::RendererSW::paintControl( - OverState control, - QRect over, - float64 overOpacity, - QRect inner, - float64 innerOpacity, - const style::icon &icon) { + OverState control, + QRect over, + float64 overOpacity, + QRect inner, + float64 innerOpacity, + const style::icon &icon) { if (!over.isEmpty() && !over.intersects(_clipOuter)) { return; } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h index e1b6ca453..5c6a7880a 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h @@ -33,7 +33,10 @@ private: const QImage &image, QRect rect, int rotation); - void paintControlsFade(QRect geometry, float64 opacity); + void paintControlsFade( + QRect geometry, + float64 opacity, + float64 fullFade); void paintRadialLoading( QRect inner, bool radial, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index f154a93b8..33526046c 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -4011,10 +4011,12 @@ void OverlayWidget::storiesTogglePaused(bool paused) { || !_streamed->instance.player().active()) { return; } else if (_streamed->instance.player().paused()) { - _streamed->instance.resume(); - updatePlaybackState(); - playbackPauseMusic(); - } else { + if (!paused) { + _streamed->instance.resume(); + updatePlaybackState(); + playbackPauseMusic(); + } + } else if (paused) { _streamed->instance.pause(); updatePlaybackState(); } From 0331955ce7cfaf2731afdb58db43a1e4433c0c77 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 12 May 2023 13:41:25 +0400 Subject: [PATCH 019/260] Show captions with darkening over stories. --- .../stories/media_stories_controller.cpp | 15 +++- .../media/stories/media_stories_controller.h | 2 + .../media/stories/media_stories_header.cpp | 2 +- .../media/stories/media_stories_sibling.cpp | 2 +- .../media/stories/media_stories_view.cpp | 4 + .../media/stories/media_stories_view.h | 4 +- .../SourceFiles/media/view/media_view.style | 3 + .../media/view/media_view_overlay_opengl.cpp | 73 +++++++++------ .../media/view/media_view_overlay_opengl.h | 6 +- .../media/view/media_view_overlay_raster.cpp | 17 ++-- .../media/view/media_view_overlay_widget.cpp | 89 +++++++++++++------ .../media/view/media_view_overlay_widget.h | 6 +- 12 files changed, 151 insertions(+), 72 deletions(-) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index d54ea6617..7bb7409f1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -28,7 +28,7 @@ namespace { constexpr auto kPhotoProgressInterval = crl::time(100); constexpr auto kPhotoDuration = 5 * crl::time(1000); -constexpr auto kFullContentFade = 0.2; +constexpr auto kFullContentFade = 0.35; constexpr auto kSiblingMultiplier = 0.448; constexpr auto kSiblingOutsidePart = 0.24; constexpr auto kSiblingUserpicSize = 0.3; @@ -276,14 +276,22 @@ rpl::producer Controller::layoutValue() const { } ContentLayout Controller::contentLayout() const { + const auto ¤t = _layout.current(); + Assert(current.has_value()); + return { - .geometry = _layout.current()->content, + .geometry = current->content, .fade = (_contentFadeAnimation.value(_contentFaded ? 1. : 0.) * kFullContentFade), - .radius = float64(st::storiesRadius), + .radius = st::storiesRadius, + .headerOutside = (current->headerLayout == HeaderLayout::Outside), }; } +TextWithEntities Controller::captionText() const { + return _captionText; +} + std::shared_ptr Controller::uiShow() const { return _delegate->storiesShow(); } @@ -325,6 +333,7 @@ void Controller::show( return; } _shown = id; + _captionText = item.caption; _header->show({ .user = list.user, .date = item.date }); _slider->show({ .index = _index, .total = list.total }); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 60c25011d..3f9eee581 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -77,6 +77,7 @@ public: [[nodiscard]] Layout layout() const; [[nodiscard]] rpl::producer layoutValue() const; [[nodiscard]] ContentLayout contentLayout() const; + [[nodiscard]] TextWithEntities captionText() const; [[nodiscard]] std::shared_ptr uiShow() const; [[nodiscard]] auto stickerOrEmojiChosen() const @@ -134,6 +135,7 @@ private: bool _contentFaded = false; Data::FullStoryId _shown; + TextWithEntities _captionText; std::optional _list; int _index = 0; bool _started = false; diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index af11254a5..fca65c8ca 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -21,7 +21,7 @@ namespace Media::Stories { namespace { constexpr auto kNameOpacity = 1.; -constexpr auto kDateOpacity = 0.6; +constexpr auto kDateOpacity = 0.8; } // namespace diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index 8558e7980..05e66297b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -256,7 +256,7 @@ SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { .layout = { .geometry = layout.geometry, .fade = kSiblingFade * (1 - over) + kSiblingFadeOver * over, - .radius = float64(st::storiesRadius), + .radius = st::storiesRadius, }, .userpic = userpicImage(layout), .userpicPosition = layout.userpic.topLeft(), diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index f5cfd628d..0412a6d78 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -79,6 +79,10 @@ SiblingView View::sibling(SiblingType type) const { return _controller->sibling(type); } +TextWithEntities View::captionText() const { + return _controller->captionText(); +} + rpl::lifetime &View::lifetime() { return _controller->lifetime(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 09865c861..b5d591e16 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -23,7 +23,8 @@ class Controller; struct ContentLayout { QRect geometry; float64 fade = 0.; - float64 radius = 0.; + int radius = 0; + bool headerOutside = false; }; enum class SiblingType; @@ -61,6 +62,7 @@ public: [[nodiscard]] rpl::producer finalShownGeometryValue() const; [[nodiscard]] ContentLayout contentLayout() const; [[nodiscard]] SiblingView sibling(SiblingType type) const; + [[nodiscard]] TextWithEntities captionText() const; void updatePlayback(const Player::TrackState &state); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c896d253a..a69039406 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -196,6 +196,7 @@ mediaviewSaveMsgStyle: TextStyle(defaultTextStyle) { mediaviewTextPalette: TextPalette(defaultTextPalette) { linkFg: mediaviewTextLinkFg; monoFg: mediaviewCaptionFg; + spoilerFg: mediaviewCaptionFg; } mediaviewCaptionStyle: defaultTextStyle; @@ -429,6 +430,8 @@ storiesHeaderDate: FlatLabel(defaultFlatLabel) { textFg: mediaviewControlFg; } storiesHeaderDatePosition: point(50px, 17px); +storiesShadowTop: icon{{ "mediaview/shadow_bottom-flip_vertical", windowShadowFg }}; +storiesShadowBottom: mediaviewShadowBottom; storiesControlsMinWidth: 200px; storiesFieldMargin: margins(0px, 14px, 0px, 16px); storiesAttach: IconButton(defaultIconButton) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 8c1ea6cf9..e2f2bff04 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -36,13 +36,14 @@ constexpr auto kControlValues = 4 * 4 + 4 * 4; // over + icon .header = R"( uniform sampler2D f_texture; uniform vec4 shadowTopRect; -uniform vec3 shadowBottomOpacityFullFade; +uniform vec4 shadowBottomSkipOpacityFullFade; )", .body = R"( float topHeight = shadowTopRect.w; - float bottomHeight = shadowBottomOpacityFullFade.x; - float opacity = shadowBottomOpacityFullFade.y; - float fullFade = shadowBottomOpacityFullFade.z; + float bottomHeight = shadowBottomSkipOpacityFullFade.x; + float bottomSkip = shadowBottomSkipOpacityFullFade.y; + float opacity = shadowBottomSkipOpacityFullFade.z; + float fullFade = shadowBottomSkipOpacityFullFade.w; float viewportHeight = shadowTopRect.y + topHeight; float fullHeight = topHeight + bottomHeight; float topY = min( @@ -50,7 +51,8 @@ uniform vec3 shadowBottomOpacityFullFade; topHeight / fullHeight); float topX = (gl_FragCoord.x - shadowTopRect.x) / shadowTopRect.z; vec4 fadeTop = texture2D(f_texture, vec2(topX, topY)) * opacity; - float bottomY = max(fullHeight - gl_FragCoord.y, topHeight) / fullHeight; + float bottomY = max(bottomSkip + fullHeight - gl_FragCoord.y, topHeight) + / fullHeight; vec4 fadeBottom = texture2D(f_texture, vec2(0.5, bottomY)) * opacity; float fade = min((1. - fadeTop.a) * (1. - fadeBottom.a), fullFade); result.rgb = result.rgb * fade; @@ -496,16 +498,30 @@ void OverlayWidget::RendererGL::paintTransformedContent( _contentBuffer->write(0, coords, sizeof(coords)); program->setUniformValue("viewport", _uniformViewport); - const auto &top = st::mediaviewShadowTop.size(); - const auto point = QPoint( - _shadowTopFlip ? 0 : (_viewport.width() - top.width()), - 0); - program->setUniformValue( - "shadowTopRect", - Uniform(transformRect(QRect(point, top)))); - const auto &bottom = st::mediaviewShadowBottom; - program->setUniformValue("shadowBottomOpacityFullFade", QVector3D( + if (_owner->_stories) { + const auto &top = st::storiesShadowTop.size(); + const auto shadowTop = geometry.topShadowShown + ? geometry.rect.y() + : geometry.rect.y() - top.height(); + program->setUniformValue( + "shadowTopRect", + Uniform(transformRect( + QRect(QPoint(geometry.rect.x(), shadowTop), top)))); + } else { + const auto &top = st::mediaviewShadowTop.size(); + const auto point = QPoint( + _shadowTopFlip ? 0 : (_viewport.width() - top.width()), + 0); + program->setUniformValue( + "shadowTopRect", + Uniform(transformRect(QRect(point, top)))); + } + const auto &bottom = _owner->_stories + ? st::storiesShadowBottom + : st::mediaviewShadowBottom; + program->setUniformValue("shadowBottomSkipOpacityFullFade", QVector4D( bottom.height() * _factor, + geometry.bottomShadowSkip * _factor, geometry.controlsOpacity, 1.f - float(geometry.fade))); if (!fillTransparentBackground) { @@ -733,14 +749,23 @@ void OverlayWidget::RendererGL::invalidateControls() { void OverlayWidget::RendererGL::validateControlsFade() { const auto flip = !_owner->topShadowOnTheRight(); + const auto forStories = (_owner->_stories != nullptr); if (!_controlsFadeImage.image().isNull() - && _shadowTopFlip == flip) { + && _shadowTopFlip == flip + && _shadowsForStories == forStories) { return; } _shadowTopFlip = flip; - const auto width = st::mediaviewShadowTop.width(); - const auto bottomTop = st::mediaviewShadowTop.height(); - const auto height = bottomTop + st::mediaviewShadowBottom.height(); + _shadowsForStories = forStories; + const auto &top = _shadowsForStories + ? st::storiesShadowTop + : st::mediaviewShadowTop; + const auto &bottom = _shadowsForStories + ? st::storiesShadowBottom + : st::mediaviewShadowBottom; + const auto width = top.width(); + const auto bottomTop = top.height(); + const auto height = bottomTop + bottom.height(); auto image = QImage( QSize(width, height) * _factor, @@ -749,10 +774,10 @@ void OverlayWidget::RendererGL::validateControlsFade() { image.setDevicePixelRatio(_factor); auto p = QPainter(&image); - st::mediaviewShadowTop.paint(p, 0, 0, width); - st::mediaviewShadowBottom.fill( + top.paint(p, 0, 0, width); + bottom.fill( p, - QRect(0, bottomTop, width, st::mediaviewShadowBottom.height())); + QRect(0, bottomTop, width, bottom.height())); p.end(); if (flip) { @@ -760,12 +785,6 @@ void OverlayWidget::RendererGL::validateControlsFade() { } _controlsFadeImage.setImage(std::move(image)); - _shadowTopTexture = QRect( - QPoint(), - QSize(width, st::mediaviewShadowTop.height()) * _factor); - _shadowBottomTexture = QRect( - QPoint(0, bottomTop) * _factor, - QSize(width, st::mediaviewShadowBottom.height()) * _factor); } void OverlayWidget::RendererGL::paintFooter(QRect outer, float64 opacity) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index b0ba15dc8..95d57ec87 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -153,10 +153,8 @@ private: // Last one is for the over circle image. std::array _controlsTextures; - QRect _shadowTopTexture; - QRect _shadowBottomTexture; - - bool _shadowTopFlip; + bool _shadowTopFlip = false; + bool _shadowsForStories = false; bool _blendingEnabled = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp index d6d24e1e8..13109e828 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp @@ -113,11 +113,16 @@ void OverlayWidget::RendererSW::paintControlsFade( _p->setOpacity(opacity); _p->setClipRect(geometry); const auto width = _owner->width(); - const auto &top = st::mediaviewShadowTop; const auto flip = !_owner->topShadowOnTheRight(); - const auto topShadow = QRect( - QPoint(flip ? 0 : (width - top.width()), 0), - top.size()); + const auto stories = (_owner->_stories != nullptr); + const auto &top = stories + ? st::storiesShadowTop + : st::mediaviewShadowTop; + const auto topShadow = stories + ? QRect(geometry.topLeft(), QSize(geometry.width(), top.height())) + : QRect( + QPoint(flip ? 0 : (width - top.width()), 0), + top.size()); if (topShadow.intersected(geometry).intersects(_clipOuter)) { if (flip) { if (_topShadowCache.isNull() @@ -131,7 +136,9 @@ void OverlayWidget::RendererSW::paintControlsFade( top.paint(*_p, topShadow.topLeft(), width); } } - const auto &bottom = st::mediaviewShadowBottom; + const auto &bottom = stories + ? st::storiesShadowBottom + : st::mediaviewShadowBottom; const auto bottomShadow = QRect( QPoint(0, _owner->height() - bottom.height()), QSize(width, bottom.height())); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 33526046c..f5fd2623e 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -127,6 +127,7 @@ constexpr auto kIdsPreloadAfter = 28; constexpr auto kLeftSiblingTextureIndex = 1; constexpr auto kRightSiblingTextureIndex = 2; +constexpr auto kStoriesControlsOpacity = 1.; class PipDelegate final : public Pip::Delegate { public: @@ -1200,27 +1201,35 @@ void OverlayWidget::refreshCaptionGeometry() { _groupThumbs = nullptr; _groupThumbsRect = QRect(); } - const auto captionBottom = (_streamed && _streamed->controls) + const auto captionBottom = _stories + ? (_y + _h) + : (_streamed && _streamed->controls) ? (_streamed->controls->y() - st::mediaviewCaptionMargin.height()) : _groupThumbs ? _groupThumbsTop : height() - st::mediaviewCaptionMargin.height(); - const auto captionWidth = std::min( - _groupThumbsAvailableWidth - - st::mediaviewCaptionPadding.left() - - st::mediaviewCaptionPadding.right(), - _caption.maxWidth()); - const auto captionHeight = std::min( - _caption.countHeight(captionWidth), - height() / 4 + const auto captionWidth = _stories + ? (_w + - st::mediaviewCaptionPadding.left() + - st::mediaviewCaptionPadding.right()) + : std::min( + (_groupThumbsAvailableWidth + - st::mediaviewCaptionPadding.left() + - st::mediaviewCaptionPadding.right()), + _caption.maxWidth()); + const auto maxHeight = (_stories ? (_h / 3) : (height() / 4)) - st::mediaviewCaptionPadding.top() - st::mediaviewCaptionPadding.bottom() - - 2 * st::mediaviewCaptionMargin.height()); + - (_stories ? 0 : (2 * st::mediaviewCaptionMargin.height())); + const auto lineHeight = st::mediaviewCaptionStyle.font->height; + const auto captionHeight = std::min( + _caption.countHeight(captionWidth), + (maxHeight / lineHeight) * lineHeight); _captionRect = QRect( (width() - captionWidth) / 2, - captionBottom - - captionHeight - - st::mediaviewCaptionPadding.bottom(), + (captionBottom + - captionHeight + - st::mediaviewCaptionPadding.bottom()), captionWidth, captionHeight); } @@ -1497,7 +1506,14 @@ QRect OverlayWidget::finalContentRect() const { OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { if (_stories) { - return storiesContentGeometry(_stories->contentLayout()); + auto result = storiesContentGeometry(_stories->contentLayout()); + if (!_caption.isEmpty()) { + result.bottomShadowSkip = _widget->height() + - _captionRect.y() + + st::mediaviewCaptionStyle.font->height + - st::storiesShadowBottom.height(); + } + return result; } const auto controlsOpacity = _controlsOpacity.current(); const auto toRotation = qreal(finalContentRotation()); @@ -1541,9 +1557,10 @@ OverlayWidget::ContentGeometry OverlayWidget::storiesContentGeometry( const Stories::ContentLayout &layout) const { return { .rect = QRectF(layout.geometry), - .controlsOpacity = 0., // #TODO stories ?.. + .controlsOpacity = kStoriesControlsOpacity, .fade = layout.fade, .roundRadius = layout.radius, + .topShadowShown = !layout.headerOutside, }; } @@ -2708,21 +2725,26 @@ void OverlayWidget::refreshFromLabel() { void OverlayWidget::refreshCaption() { _caption = Ui::Text::String(); - if (!_message) { - return; - } else if (const auto media = _message->media()) { - if (media->webpage()) { - return; + const auto caption = [&] { + if (_message) { + if (const auto media = _message->media()) { + if (media->webpage()) { + return TextWithEntities(); + } + } + return _message->translatedText(); + } else if (_stories) { + return _stories->captionText(); } - } - const auto caption = _message->translatedText(); + return TextWithEntities(); + }(); if (caption.text.isEmpty()) { return; } using namespace HistoryView; _caption = Ui::Text::String(st::msgMinWidth); - const auto duration = (_streamed && _document) + const auto duration = (_streamed && _document && _message) ? DurationForTimestampLinks(_document) : 0; const auto base = duration @@ -2735,7 +2757,9 @@ void OverlayWidget::refreshCaption() { update(captionGeometry()); }; const auto context = Core::MarkedTextContext{ - .session = &_message->history()->session(), + .session = (_stories + ? _storiesSession + : &_message->history()->session()), .customEmojiRepaint = captionRepaint, }; _caption.setMarkedText( @@ -2743,7 +2767,9 @@ void OverlayWidget::refreshCaption() { (base.isEmpty() ? caption : AddTimestampLinks(caption, duration, base)), - Ui::ItemTextOptions(_message), + (_message + ? Ui::ItemTextOptions(_message) + : Ui::ItemTextDefaultOptions()), context); if (_caption.hasSpoilers()) { const auto weak = Ui::MakeWeak(widget()); @@ -4627,10 +4653,15 @@ void OverlayWidget::paintCaptionContent( QRect clip, float64 opacity) { const auto inner = outer.marginsRemoved(st::mediaviewCaptionPadding); - p.setOpacity(opacity); - p.setBrush(st::mediaviewCaptionBg); - p.setPen(Qt::NoPen); - p.drawRoundedRect(outer, st::mediaviewCaptionRadius, st::mediaviewCaptionRadius); + if (!_stories) { + p.setOpacity(opacity); + p.setBrush(st::mediaviewCaptionBg); + p.setPen(Qt::NoPen); + p.drawRoundedRect( + outer, + st::mediaviewCaptionRadius, + st::mediaviewCaptionRadius); + } if (inner.intersects(clip)) { p.setPen(st::mediaviewCaptionFg); _caption.draw(p, { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 49c4a6df7..fdb8997c6 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -166,8 +166,12 @@ private: QRectF rect; qreal rotation = 0.; qreal controlsOpacity = 0.; + + // Stories. qreal fade = 0.; - qreal roundRadius = 0.; + int bottomShadowSkip = 0; + int roundRadius = 0; + bool topShadowShown = false; }; struct StartStreaming { StartStreaming() : continueStreaming(false), startTime(0) { From a745c9ff75b00257b7044751624ad5c475ad67a5 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 12 May 2023 21:10:00 +0400 Subject: [PATCH 020/260] Display full caption if it doesn't fit. --- Telegram/CMakeLists.txt | 2 + .../media_stories_caption_full_view.cpp | 81 +++++++++++++++++++ .../stories/media_stories_caption_full_view.h | 46 +++++++++++ .../stories/media_stories_controller.cpp | 25 ++++++ .../media/stories/media_stories_controller.h | 3 + .../media/stories/media_stories_view.cpp | 4 + .../media/stories/media_stories_view.h | 1 + .../SourceFiles/media/view/media_view.style | 5 ++ .../media/view/media_view_overlay_widget.cpp | 77 +++++++++++++++--- .../media/view/media_view_overlay_widget.h | 17 +++- 10 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_caption_full_view.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5a095f698..5c559e8a5 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -962,6 +962,8 @@ PRIVATE media/player/media_player_volume_controller.h media/player/media_player_widget.cpp media/player/media_player_widget.h + media/stories/media_stories_caption_full_view.cpp + media/stories/media_stories_caption_full_view.h media/stories/media_stories_controller.cpp media/stories/media_stories_controller.h media/stories/media_stories_delegate.cpp diff --git a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp new file mode 100644 index 000000000..b301ced92 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp @@ -0,0 +1,81 @@ +/* +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/stories/media_stories_caption_full_view.h" + +#include "core/ui_integration.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/labels.h" +#include "styles/style_media_view.h" + +namespace Media::Stories { + +CaptionFullView::CaptionFullView( + not_null parent, + not_null session, + const TextWithEntities &text, + Fn close) +: RpWidget(parent) +, _scroll(std::make_unique((RpWidget*)this)) +, _text(_scroll->setOwnedWidget( + object_ptr>( + _scroll.get(), + object_ptr(_scroll.get(), st::storiesCaptionFull), + st::mediaviewCaptionPadding))->entity()) +, _close(std::move(close)) +, _background(st::storiesRadius, st::mediaviewCaptionBg) { + _text->setMarkedText(text, Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [=] { _text->update(); }, + }); + + parent->sizeValue() | rpl::start_with_next([=](QSize size) { + setGeometry(QRect(QPoint(), size)); + }, lifetime()); + + show(); + setFocus(); +} + +CaptionFullView::~CaptionFullView() = default; + +void CaptionFullView::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + _background.paint(p, _scroll->geometry()); + _background.paint(p, _scroll->geometry()); +} + +void CaptionFullView::resizeEvent(QResizeEvent *e) { + const auto wanted = _text->naturalWidth(); + const auto padding = st::mediaviewCaptionPadding; + const auto margin = st::mediaviewCaptionMargin * 2; + const auto available = (rect() - padding).width() + - (margin.width() * 2); + const auto use = std::min(wanted, available); + _text->resizeToWidth(use); + const auto fullw = use + padding.left() + padding.right(); + const auto fullh = std::min( + _text->height() + padding.top() + padding.bottom(), + height() - (margin.height() * 2)); + const auto left = (width() - fullw) / 2; + const auto top = (height() - fullh) / 2; + _scroll->setGeometry(left, top, fullw, fullh); +} + +void CaptionFullView::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Escape) { + _close(); + } +} + +void CaptionFullView::mousePressEvent(QMouseEvent *e) { + if (e->button() == Qt::LeftButton) { + _close(); + } +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.h b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.h new file mode 100644 index 000000000..94b6e2cff --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.h @@ -0,0 +1,46 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/rp_widget.h" +#include "ui/round_rect.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class FlatLabel; +class ScrollArea; +} // namespace Ui + +namespace Media::Stories { + +class CaptionFullView final : private Ui::RpWidget { +public: + CaptionFullView( + not_null parent, + not_null session, + const TextWithEntities &text, + Fn close); + ~CaptionFullView(); + +private: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + + std::unique_ptr _scroll; + const not_null _text; + Fn _close; + Ui::RoundRect _background; + +}; + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 7bb7409f1..a0ff10a86 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -9,8 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/power_save_blocker.h" +#include "chat_helpers/compose/compose_show.h" #include "data/data_stories.h" #include "data/data_user.h" +#include "media/stories/media_stories_caption_full_view.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" #include "media/stories/media_stories_sibling.h" @@ -115,6 +117,9 @@ Controller::Controller(not_null delegate) _replyArea->focusedValue( ) | rpl::start_with_next([=](bool focused) { + if (focused) { + _captionFullView = nullptr; + } _contentFaded = focused; _contentFadeAnimation.start( [=] { _delegate->storiesRepaint(); }, @@ -292,6 +297,18 @@ TextWithEntities Controller::captionText() const { return _captionText; } +void Controller::showFullCaption() { + if (_captionText.empty()) { + return; + } + togglePaused(true); + _captionFullView = std::make_unique( + wrap(), + &_delegate->storiesShow()->session(), + _captionText, + [=] { togglePaused(false); }); +} + std::shared_ptr Controller::uiShow() const { return _delegate->storiesShow(); } @@ -334,10 +351,15 @@ void Controller::show( } _shown = id; _captionText = item.caption; + _captionFullView = nullptr; _header->show({ .user = list.user, .date = item.date }); _slider->show({ .index = _index, .total = list.total }); _replyArea->show({ .user = list.user }); + + if (_contentFaded) { + togglePaused(true); + } } void Controller::showSiblings( @@ -447,6 +469,9 @@ bool Controller::paused() const { } void Controller::togglePaused(bool paused) { + if (!paused) { + _captionFullView = nullptr; + } if (_photoPlayback) { _photoPlayback->togglePaused(paused); } else { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 3f9eee581..88a307dc0 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -41,6 +41,7 @@ class Delegate; struct SiblingView; enum class SiblingType; struct ContentLayout; +class CaptionFullView; enum class HeaderLayout { Normal, @@ -78,6 +79,7 @@ public: [[nodiscard]] rpl::producer layoutValue() const; [[nodiscard]] ContentLayout contentLayout() const; [[nodiscard]] TextWithEntities captionText() const; + void showFullCaption(); [[nodiscard]] std::shared_ptr uiShow() const; [[nodiscard]] auto stickerOrEmojiChosen() const @@ -130,6 +132,7 @@ private: const std::unique_ptr _slider; const std::unique_ptr _replyArea; std::unique_ptr _photoPlayback; + std::unique_ptr _captionFullView; Ui::Animations::Simple _contentFadeAnimation; bool _contentFaded = false; diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 0412a6d78..82c4388d2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -83,6 +83,10 @@ TextWithEntities View::captionText() const { return _controller->captionText(); } +void View::showFullCaption() { + _controller->showFullCaption(); +} + rpl::lifetime &View::lifetime() { return _controller->lifetime(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index b5d591e16..f36b65cf1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -63,6 +63,7 @@ public: [[nodiscard]] ContentLayout contentLayout() const; [[nodiscard]] SiblingView sibling(SiblingType type) const; [[nodiscard]] TextWithEntities captionText() const; + void showFullCaption(); void updatePlayback(const Player::TrackState &state); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index a69039406..3cd675b90 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -448,3 +448,8 @@ storiesAttach: IconButton(defaultIconButton) { } } storiesSideSkip: 145px; +storiesCaptionFull: FlatLabel(defaultFlatLabel) { + style: mediaviewCaptionStyle; + textFg: mediaviewCaptionFg; + minWidth: 360px; +} diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index f5fd2623e..2d7da4d8b 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1217,14 +1217,32 @@ void OverlayWidget::refreshCaptionGeometry() { - st::mediaviewCaptionPadding.left() - st::mediaviewCaptionPadding.right()), _caption.maxWidth()); - const auto maxHeight = (_stories ? (_h / 3) : (height() / 4)) + const auto maxExpandedOuterHeight = (_stories + ? (_h - st::storiesShadowTop.height()) + : height()); + const auto maxCollapsedOuterHeight = !_stories + ? (height() / 4) + : (_h / 3); + const auto maxExpandedHeight = maxExpandedOuterHeight - st::mediaviewCaptionPadding.top() - - st::mediaviewCaptionPadding.bottom() - - (_stories ? 0 : (2 * st::mediaviewCaptionMargin.height())); + - st::mediaviewCaptionPadding.bottom(); + const auto maxCollapsedHeight = maxCollapsedOuterHeight + - st::mediaviewCaptionPadding.top() + - st::mediaviewCaptionPadding.bottom(); const auto lineHeight = st::mediaviewCaptionStyle.font->height; + const auto wantedHeight = _caption.countHeight(captionWidth); + const auto maxHeight = _captionExpanded + ? maxExpandedHeight + : maxCollapsedHeight; const auto captionHeight = std::min( - _caption.countHeight(captionWidth), + wantedHeight, (maxHeight / lineHeight) * lineHeight); + _captionFitsIfExpanded = _stories + && (wantedHeight <= maxExpandedHeight); + _captionShownFull = (wantedHeight <= maxCollapsedHeight); + if (_captionShownFull) { + _captionExpanded = false; + } _captionRect = QRect( (width() - captionWidth) / 2, (captionBottom @@ -3000,6 +3018,7 @@ void OverlayWidget::show(OpenRequest request) { _streamingStartPaused = false; displayDocument( document, + anim::activation::normal, request.cloudTheme() ? *request.cloudTheme() : Data::CloudTheme(), @@ -3014,9 +3033,11 @@ void OverlayWidget::show(OpenRequest request) { } } -void OverlayWidget::displayPhoto(not_null photo) { +void OverlayWidget::displayPhoto( + not_null photo, + anim::activation activation) { if (photo->isNull()) { - displayDocument(nullptr); + displayDocument(nullptr, activation); return; } _touchbarDisplay.fire(TouchBarItemType::Photo); @@ -3055,7 +3076,7 @@ void OverlayWidget::displayPhoto(not_null photo) { } contentSizeChanged(); refreshFromLabel(); - displayFinished(); + displayFinished(activation); } void OverlayWidget::destroyThemePreview() { @@ -3071,15 +3092,16 @@ void OverlayWidget::redisplayContent() { if (isHidden() || !_session) { return; } else if (_photo) { - displayPhoto(_photo); + displayPhoto(_photo, anim::activation::background); } else { - displayDocument(_document); + displayDocument(_document, anim::activation::background); } } // Empty messages shown as docs: doc can be nullptr. void OverlayWidget::displayDocument( DocumentData *doc, + anim::activation activation, const Data::CloudTheme &cloud, const StartStreaming &startStreaming) { _fullScreenVideo = false; @@ -3209,7 +3231,7 @@ void OverlayWidget::displayDocument( if (_showAsPip && _streamed && _streamed->controls) { switchToPip(); } else { - displayFinished(); + displayFinished(activation); } } @@ -3236,7 +3258,8 @@ void OverlayWidget::updateThemePreviewGeometry() { } } -void OverlayWidget::displayFinished() { +void OverlayWidget::displayFinished(anim::activation activation) { + _captionExpanded = _captionFitsIfExpanded = _captionShownFull = false; updateControls(); if (isHidden()) { _helper->beforeShow(_fullscreen); @@ -3247,6 +3270,8 @@ void OverlayWidget::displayFinished() { //setAttribute(Qt::WA_DontShowOnScreen, false); //Ui::Platform::UpdateOverlayed(_window); showAndActivate(); + } else if (activation == anim::activation::background) { + return; } else if (isMinimized()) { _helper->beforeShow(_fullscreen); showAndActivate(); @@ -4015,10 +4040,11 @@ void OverlayWidget::storiesJumpTo(Data::FullStoryId id) { clearStreaming(); _streamingStartPaused = false; const auto &data = j->media.data; + const auto activation = anim::activation::background; if (const auto photo = std::get_if>(&data)) { - displayPhoto(*photo); + displayPhoto(*photo, activation); } else { - displayDocument(v::get>(data)); + displayDocument(v::get>(data), activation); } } @@ -5302,6 +5328,9 @@ void OverlayWidget::updateOver(QPoint pos) { } else if (_captionRect.contains(pos)) { auto textState = _caption.getState(pos - _captionRect.topLeft(), _captionRect.width()); lnk = textState.link; + if (_stories && !_captionShownFull && !lnk) { + lnk = ensureCaptionExpandLink(); + } lnkhost = this; } else if (_groupThumbs && _groupThumbsRect.contains(pos)) { const auto point = pos - QPoint(_groupThumbsLeft, _groupThumbsTop); @@ -5373,6 +5402,28 @@ void OverlayWidget::updateOver(QPoint pos) { } } +ClickHandlerPtr OverlayWidget::ensureCaptionExpandLink() { + if (!_captionExpandLink) { + const auto toggle = crl::guard(_widget, [=] { + if (!_stories) { + return; + } else if (_captionExpanded) { + _captionExpanded = false; + refreshCaptionGeometry(); + update(); + } else if (_captionFitsIfExpanded) { + _captionExpanded = true; + refreshCaptionGeometry(); + update(); + } else { + _stories->showFullCaption(); + } + }); + _captionExpandLink = std::make_shared(toggle); + } + return _captionExpandLink; +} + void OverlayWidget::handleMouseRelease( QPoint position, Qt::MouseButton button) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index fdb8997c6..6007e9fab 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -23,6 +23,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace anim { +enum class activation : uchar; +} // namespace anim + namespace Data { class PhotoMedia; class DocumentMedia; @@ -148,6 +152,7 @@ private: OverMore, OverIcon, OverVideo, + OverCaption, }; struct Entity { std::variant< @@ -356,12 +361,15 @@ private: void resizeContentByScreenSize(); void recountSkipTop(); - void displayPhoto(not_null photo); + void displayPhoto( + not_null photo, + anim::activation activation = anim::activation::normal); void displayDocument( DocumentData *document, + anim::activation activation = anim::activation::normal, const Data::CloudTheme &cloud = Data::CloudTheme(), const StartStreaming &startStreaming = StartStreaming()); - void displayFinished(); + void displayFinished(anim::activation activation); void redisplayContent(); void findCurrent(); @@ -496,6 +504,7 @@ private: [[nodiscard]] bool topShadowOnTheRight() const; void applyHideWindowWorkaround(); + [[nodiscard]] ClickHandlerPtr ensureCaptionExpandLink(); Window::SessionController *findWindow(bool switchTo = true) const; @@ -558,6 +567,10 @@ private: int _groupThumbsTop = 0; Ui::Text::String _caption; QRect _captionRect; + ClickHandlerPtr _captionExpandLink; + bool _captionShownFull = false; + bool _captionFitsIfExpanded = false; + bool _captionExpanded = false; int _width = 0; int _height = 0; From 75d2b5994f6cdf8dd46955fd07bc1441d375c1dd Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 16 May 2023 19:48:49 +0400 Subject: [PATCH 021/260] Apply dark theme to reply controls in stories. --- .../chat_helpers/chat_helpers.style | 505 ++++++++++++++++-- .../chat_helpers/emoji_list_widget.cpp | 17 +- .../chat_helpers/field_autocomplete.cpp | 19 +- .../chat_helpers/field_autocomplete.h | 8 +- .../chat_helpers/gifs_list_widget.cpp | 4 +- .../chat_helpers/message_field.cpp | 1 + .../chat_helpers/stickers_list_footer.cpp | 2 +- .../chat_helpers/stickers_list_widget.cpp | 15 +- .../chat_helpers/stickers_list_widget.h | 3 + .../chat_helpers/tabbed_selector.cpp | 53 +- .../chat_helpers/tabbed_selector.h | 26 +- .../SourceFiles/history/history_widget.cpp | 2 +- .../history_view_compose_controls.cpp | 83 ++- .../controls/history_view_compose_controls.h | 23 +- .../media/stories/media_stories_reply.cpp | 9 + .../SourceFiles/media/view/media_view.style | 111 +++- .../media/view/media_view_overlay_widget.cpp | 3 + .../SourceFiles/ui/boxes/time_picker_box.cpp | 2 +- .../attach_abstract_single_file_preview.cpp | 1 + .../ui/chat/attach/attach_album_thumbnail.cpp | 1 + Telegram/SourceFiles/ui/chat/chat.style | 359 +------------ Telegram/SourceFiles/ui/chat/message_bar.cpp | 1 + Telegram/SourceFiles/ui/chat/pinned_bar.cpp | 2 +- Telegram/SourceFiles/ui/chat/requests_bar.cpp | 4 +- .../SourceFiles/ui/controls/emoji_button.cpp | 28 +- .../SourceFiles/ui/controls/emoji_button.h | 8 +- .../ui/controls/jump_down_button.cpp | 2 +- .../SourceFiles/ui/controls/send_button.cpp | 62 +-- .../SourceFiles/ui/controls/send_button.h | 8 +- .../SourceFiles/ui/controls/tabbed_search.cpp | 2 +- .../window/themes/window_theme_preview.cpp | 22 +- Telegram/SourceFiles/window/window.style | 2 +- 32 files changed, 836 insertions(+), 552 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 97f94cb19..ceec7e3ec 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -11,9 +11,17 @@ using "boxes/boxes.style"; using "ui/layers/layers.style"; using "ui/widgets/widgets.style"; +GroupCallUserpics { + size: pixels; + shift: pixels; + stroke: pixels; + align: align; +} + TabbedSearch { outer: color; bg: color; + bgActive: color; fg: color; fgActive: color; fadeLeft: icon; @@ -44,14 +52,53 @@ EmojiPan { iconArea: pixels; bg: color; overBg: color; + expandBg: color; + pathBg: color; + pathFg: color; + textFg: color; categoriesBg: color; categoriesBgOver: color; fadeLeft: icon; fadeRight: icon; + tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; } +MessageBar { + title: TextStyle; + titleFg: color; + text: TextStyle; + textFg: color; + textPalette: TextPalette; + duration: int; +} + +EmojiButton { + inner: IconButton; + bg: color; + lineFg: color; + lineFgOver: color; +} + +SendButton { + inner: IconButton; + record: icon; + recordOver: icon; + sendDisabledFg: color; +} + +ComposeControls { + bg: color; + radius: pixels; + + field: InputField; + send: SendButton; + attach: IconButton; + emoji: EmojiButton; + tabbed: EmojiPan; +} + switchPmButton: RoundButton(defaultBoxButton) { width: 320px; height: 34px; @@ -196,50 +243,55 @@ emojiPanSlideDuration: 200; emojiPanArea: size(34px, 32px); emojiPanRadius: 8px; +defaultTabbedSearchCancel: CrossButton { + width: 33px; + height: 33px; + + cross: CrossAnimation { + size: 27px; + skip: 8px; + stroke: 1.; + minScale: 0.3; + } + crossFg: menuIconFg; + crossFgOver: menuIconFg; + crossPosition: point(1px, 3px); + + duration: 150; + loadingPeriod: 1000; + ripple: emptyRippleAnimation; +} +defaultTabbedSearchField: InputField(defaultMultiSelectSearchField) { + textMargins: margins(2px, 7px, 2px, 0px); +} +defaultTabbedSearchButton: IconButton(defaultIconButton) { + width: 33px; + height: 33px; + icon: icon{{ "emoji/emoji_search_input", emojiIconFg }}; + iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }}; + iconPosition: point(7px, -1px); + ripple: emptyRippleAnimation; +} +defaultTabbedSearchBack: IconButton(defaultIconButton) { + width: 33px; + height: 33px; + icon: icon{{ "emoji/emoji_back", menuIconFg }}; + iconOver: icon{{ "emoji/emoji_back", menuIconFg }}; + iconPosition: point(7px, -1px); + ripple: emptyRippleAnimation; +} defaultTabbedSearch: TabbedSearch { outer: emojiPanBg; bg: emojiPanHover; + bgActive: windowBgRipple; fg: emojiIconFg; fgActive: emojiSubIconFgActive; fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanHover }}; fadeRight: icon {{ "fade_horizontal", emojiPanHover }}; - field: InputField(defaultMultiSelectSearchField) { - textMargins: margins(2px, 7px, 2px, 0px); - } - search: IconButton(defaultIconButton) { - width: 33px; - height: 33px; - icon: icon{{ "emoji/emoji_search_input", emojiIconFg }}; - iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }}; - iconPosition: point(7px, -1px); - ripple: emptyRippleAnimation; - } - back: IconButton(defaultIconButton) { - width: 33px; - height: 33px; - icon: icon{{ "emoji/emoji_back", menuIconFg }}; - iconOver: icon{{ "emoji/emoji_back", menuIconFg }}; - iconPosition: point(7px, -1px); - ripple: emptyRippleAnimation; - } - cancel: CrossButton { - width: 33px; - height: 33px; - - cross: CrossAnimation { - size: 27px; - skip: 8px; - stroke: 1.; - minScale: 0.3; - } - crossFg: menuIconFg; - crossFgOver: menuIconFg; - crossPosition: point(1px, 3px); - - duration: 150; - loadingPeriod: 1000; - ripple: emptyRippleAnimation; - } + field: defaultTabbedSearchField; + search: defaultTabbedSearchButton; + back: defaultTabbedSearchBack; + cancel: defaultTabbedSearchCancel; defaultFieldWidth: 103px; groupWidth: 30px; groupSkip: 2px; @@ -261,10 +313,15 @@ defaultEmojiPan: EmojiPan { iconArea: 28px; bg: emojiPanBg; overBg: emojiPanHover; + expandBg: emojiPanHeaderFg; + pathBg: windowBgRipple; + pathFg: windowBgOver; + textFg: windowFg; categoriesBg: emojiPanCategories; categoriesBgOver: windowBgRipple; fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; + tabs: emojiTabs; search: defaultTabbedSearch; searchMargin: margins(1px, 11px, 2px, 5px); } @@ -428,3 +485,377 @@ emojiSuggestionsFadeRight: icon {{ "fade_horizontal", boxBg }}; choosePeerGroupIcon: icon {{ "info/edit/create_group", lightButtonFg }}; choosePeerChannelIcon: icon {{ "info/edit/create_channel", lightButtonFg }}; choosePeerCreateIconLeft: 25px; + +historyRequestsUserpics: GroupCallUserpics { + size: 22px; + shift: 8px; + stroke: 4px; + align: align(left); +} +historyRequestsHeight: 33px; + +historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px); + +historyComposeAreaPalette: TextPalette(defaultTextPalette) { + linkFg: historyComposeAreaFgService; +} + +defaultMessageBar: MessageBar { + title: semiboldTextStyle; + titleFg: windowActiveTextFg; + text: defaultTextStyle; + textFg: historyComposeAreaFg; + textPalette: historyComposeAreaPalette; + duration: 160; +} + +historyComposeButton: FlatButton { + color: windowActiveTextFg; + overColor: windowActiveTextFg; + + bgColor: historyComposeButtonBg; + overBgColor: historyComposeButtonBgOver; + + width: -32px; + height: 46px; + + textTop: 14px; + + font: semiboldFont; + overFont: semiboldFont; + + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyComposeButtonBgRipple; + } +} +historyUnblock: FlatButton(historyComposeButton) { + color: attentionButtonFg; + overColor: attentionButtonFgOver; +} +historyContactStatusButton: FlatButton(historyComposeButton) { + height: 49px; + textTop: 16px; + overBgColor: historyComposeButtonBg; + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyComposeButtonBgOver; + } +} +historyContactStatusBlock: FlatButton(historyContactStatusButton) { + color: attentionButtonFg; + overColor: attentionButtonFg; +} +historyContactStatusLabel: FlatLabel(defaultFlatLabel) { + minWidth: 240px; +} +historyEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) { + align: align(top); + textFg: windowSubTextFg; +} +historyContactStatusMinSkip: 16px; + +historyReplySkip: 51px; +historyReplyNameFg: windowActiveTextFg; +historyReplyHeight: 49px; +historyReplyIconPosition: point(5px, 5px); +historyReplyIcon: icon {{ "chat/input_reply", historyReplyIconFg }}; +historyForwardIcon: icon {{ "chat/input_forward", historyReplyIconFg }}; +historyEditIcon: icon {{ "chat/input_edit", historyReplyIconFg }}; +historyReplyCancel: IconButton { + width: 49px; + height: 49px; + + icon: historyReplyCancelIcon; + iconOver: historyReplyCancelIconOver; + iconPosition: point(-1px, -1px); + + rippleAreaPosition: point(4px, 4px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +historyPinnedShowAll: IconButton(historyReplyCancel) { + icon: icon {{ "pinned_show_all", historyReplyCancelFg }}; + iconOver: icon {{ "pinned_show_all", historyReplyCancelFgOver }}; +} +historyPinnedBotButton: RoundButton(defaultActiveButton) { + width: -34px; + height: 30px; + textTop: 6px; + padding: margins(2px, 10px, 10px, 9px); +} +historyPinnedBotButtonMaxWidth: 150px; + +historyToDownPosition: point(12px, 10px); +historyToDownAbove: icon {{ "history_down_arrow", historyToDownFg }}; +historyToDownAboveOver: icon {{ "history_down_arrow", historyToDownFgOver }}; +historyToDownPaddingTop: 10px; +historyToDownBelow: icon { + { "history_down_shadow", historyToDownShadow }, + { "history_down_circle", historyToDownBg }, +}; +historyToDownBelowOver: icon { + { "history_down_shadow", historyToDownShadow }, + { "history_down_circle", historyToDownBgOver }, +}; +historyToDown: TwoIconButton { + width: 52px; + height: 62px; + + iconBelow: historyToDownBelow; + iconBelowOver: historyToDownBelowOver; + iconAbove: historyToDownAbove; + iconAboveOver: historyToDownAboveOver; + iconPosition: point(0px, historyToDownPaddingTop); + + rippleAreaPosition: point(5px, 15px); + rippleAreaSize: 42px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyToDownBgRipple; + } +} +historyToDownBadgeFont: semiboldFont; +historyToDownBadgeSize: 22px; + +historyToDownShownAfter: 480px; +historyToDownDuration: 150; + +dialogsToUpAbove: icon {{ "history_down_arrow-flip_vertical", historyToDownFg, point(0px, 1px) }}; +dialogsToUpAboveOver: icon {{ "history_down_arrow-flip_vertical", historyToDownFgOver, point(0px, 1px) }}; + +dialogsToUp: TwoIconButton(historyToDown) { + iconAbove: dialogsToUpAbove; + iconAboveOver: dialogsToUpAboveOver; +} + +historyUnreadMentions: TwoIconButton(historyToDown) { + iconAbove: icon {{ "history_unread_mention", historyToDownFg }}; + iconAboveOver: icon {{ "history_unread_mention", historyToDownFgOver }}; +} +historyUnreadReactions: TwoIconButton(historyToDown) { + iconAbove: icon {{ "history_unread_reaction", historyToDownFg }}; + iconAboveOver: icon {{ "history_unread_reaction", historyToDownFgOver }}; +} +historyUnreadThingsSkip: 4px; + +historyComposeField: InputField(defaultInputField) { + font: normalFont; + textMargins: margins(0px, 0px, 0px, 0px); + textAlign: align(left); + textFg: historyComposeAreaFg; + textBg: historyComposeAreaBg; + heightMin: 36px; + heightMax: 72px; + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(7px, 5px, 7px, 5px); + placeholderAlign: align(topleft); + placeholderScale: 0.; + placeholderFont: normalFont; + placeholderShift: -50px; + border: 0px; + borderActive: 0px; + duration: 100; +} +historyComposeFieldMaxHeight: 224px; +// historyMinHeight: 56px; + +historyAttach: IconButton(defaultIconButton) { + width: 44px; + height: 46px; + + icon: icon {{ "chat/input_attach", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_attach", historyComposeIconFgOver }}; + + rippleAreaPosition: point(2px, 3px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} + +historyMessagesTTL: IconButtonWithText { + iconButton: IconButton(historyAttach) { + icon: icon {{ "chat/input_autodelete", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_autodelete", historyComposeIconFgOver }}; + } + textFg: historyComposeIconFg; + textFgOver: historyComposeIconFgOver; + textPadding: margins(21px, 20px, 3px, 7px); + textAlign: align(left); + + font: font(10px semibold); +} +historyReplaceMedia: IconButton(historyAttach) { + icon: icon {{ "chat/input_replace", windowBgActive }}; + iconOver: icon {{ "chat/input_replace", windowBgActive }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgOver; + } +} + +historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }}; +historyEmojiCircle: size(20px, 20px); +historyEmojiCircleLine: 1.5; +historyEmojiCircleFg: historyComposeIconFg; +historyEmojiCircleFgOver: historyComposeIconFgOver; +historyBotKeyboardShow: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_keyboard", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_keyboard", historyComposeIconFgOver }}; +} +historyBotKeyboardHide: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFgOver }}; +} +historyBotCommandStart: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_command", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_command", historyComposeIconFgOver }}; +} +historyScheduledToggle: IconButton(historyAttach) { + icon: icon { + { "chat/input_scheduled", historyComposeIconFg }, + { "chat/input_scheduled_dot", attentionButtonFg } + }; + iconOver: icon { + { "chat/input_scheduled", historyComposeIconFgOver }, + { "chat/input_scheduled_dot", attentionButtonFg } + }; +} + +historyAttachEmojiInner: IconButton(historyAttach) { + icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_smile_face", historyComposeIconFgOver }}; +} +historyAttachEmoji: EmojiButton { + inner: historyAttachEmojiInner; + bg: historyComposeAreaBg; + lineFg: historyEmojiCircleFg; + lineFgOver: historyEmojiCircleFgOver; +} +boxAttachEmoji: EmojiButton(historyAttachEmoji) { + inner: IconButton(historyAttachEmojiInner) { + width: 30px; + height: 30px; + rippleAreaSize: 0px; + } +} +boxAttachEmojiTop: 20px; + +historySendIcon: icon {{ "chat/input_send", historySendIconFg }}; +historySendIconOver: icon {{ "chat/input_send", historySendIconFgOver }}; +historySendIconPosition: point(10px, 11px); +historySendSize: size(44px, 46px); +historyScheduleIcon: icon {{ "chat/input_schedule", historyComposeAreaBg }}; +historyScheduleIconPosition: point(7px, 8px); +historyEditSaveIcon: icon {{ "chat/input_save", historySendIconFg }}; +historyEditSaveIconOver: icon {{ "chat/input_save", historySendIconFgOver }}; + +historyEditMediaBg: videoPlayIconBg; +historyEditMedia: icon{{ "chat/input_draw", videoPlayIconFg }}; +historyMessagesTTLPickerHeight: 200px; +historyMessagesTTLPickerItemHeight: 40px; +historyMessagesTTLLabel: FlatLabel(defaultFlatLabel) { + minWidth: 200px; + align: align(topleft); + textFg: windowSubTextFg; +} + +historyRecordVoiceFg: historyComposeIconFg; +historyRecordVoiceFgOver: historyComposeIconFgOver; +historyRecordVoiceFgInactive: attentionButtonFg; +historyRecordVoiceFgActive: windowBgActive; +historyRecordVoiceFgActiveIcon: windowFgActive; +historyRecordVoiceShowDuration: 120; +historyRecordVoiceDuration: 120; +historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; +historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; +historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; +historyRecordSendIconPosition: point(2px, 0px); +historyRecordVoiceRippleBgActive: lightButtonBgOver; +historyRecordSignalRadius: 5px; +historyRecordCancel: windowSubTextFg; +historyRecordCancelActive: windowActiveTextFg; +historyRecordFont: font(13px); +historyRecordDurationSkip: 12px; +historyRecordDurationFg: historyComposeAreaFg; + +historyRecordMainBlobMinRadius: 23px; +historyRecordMainBlobMaxRadius: 37px; +historyRecordMinorBlobMinRadius: 40px; +historyRecordMinorBlobMaxRadius: 47px; +historyRecordMajorBlobMinRadius: 43px; +historyRecordMajorBlobMaxRadius: 50px; + +historyRecordTextStyle: TextStyle(defaultTextStyle) { + font: historyRecordFont; +} + +historyRecordTextWidthForWrap: 210px; +historyRecordTextLeft: 15px; +historyRecordTextRight: 25px; + +historyRecordLockShowDuration: historyToDownDuration; +historyRecordLockSize: size(75px, 133px); + +historyRecordLockIconFg: historyToDownFg; +historyRecordLockIconSize: size(14px, 17px); +historyRecordLockIconBottomHeight: 9px; +historyRecordLockIconLineHeight: 2px; +historyRecordLockIconLineSkip: 3px; +historyRecordLockIconLineWidth: 2px; +historyRecordLockIconArcHeight: 4px; +historyRecordStopIconWidth: 12px; + +historyRecordLockTopShadow: icon {{ "voice_lock/record_lock_top_shadow", historyToDownShadow }}; +historyRecordLockTop: icon {{ "voice_lock/record_lock_top", historyToDownBg }}; +historyRecordLockBottomShadow: icon {{ "voice_lock/record_lock_bottom_shadow", historyToDownShadow }}; +historyRecordLockBottom: icon {{ "voice_lock/record_lock_bottom", historyToDownBg }}; +historyRecordLockBodyShadow: icon {{ "voice_lock/record_lock_body_shadow", historyToDownShadow }}; +historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }}; +historyRecordLockMargin: margins(4px, 4px, 4px, 4px); +historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }}; +historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px); + +historyRecordDelete: IconButton(historyAttach) { + icon: icon {{ "info/info_media_delete", historyComposeIconFg }}; + iconOver: icon {{ "info/info_media_delete", historyComposeIconFgOver }}; + iconPosition: point(10px, 11px); +} +historyRecordWaveformRightSkip: 10px; +historyRecordWaveformBgMargins: margins(5px, 7px, 5px, 7px); + +historyRecordWaveformBar: 3px; + +historyRecordLockPosition: point(1px, 35px); + +historyRecordCancelButtonWidth: 100px; +historyRecordCancelButtonFg: lightButtonFg; + +historySilentToggle: IconButton(historyBotKeyboardShow) { + icon: icon {{ "chat/input_silent", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_silent", historyComposeIconFgOver }}; +} +historySilentToggleOn: icon {{ "chat/input_silent_on", historyComposeIconFg }}; +historySilentToggleOnOver: icon {{ "chat/input_silent_on", historyComposeIconFgOver }}; + +historySend: SendButton { + inner: IconButton(historyAttach) { + icon: historySendIcon; + iconOver: historySendIconOver; + } + record: historyRecordVoice; + recordOver: historyRecordVoiceOver; + sendDisabledFg: historyComposeIconFg; +} + +defaultComposeControls: ComposeControls { + bg: historyComposeAreaBg; + radius: 0px; + + field: historyComposeField; + send: historySend; + attach: historyAttach; + emoji: historyAttachEmoji; + tabbed: defaultEmojiPan; +} diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 19dd9c827..a6ff9af75 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -56,7 +56,7 @@ using Core::RecentEmojiDocument; class EmojiColorPicker final : public Ui::RpWidget { public: - EmojiColorPicker(QWidget *parent); + EmojiColorPicker(QWidget *parent, const style::EmojiPan &st); void showEmoji(EmojiPtr emoji); @@ -87,6 +87,8 @@ private: void updateSelected(); void setSelected(int newSelected); + const style::EmojiPan &_st; + bool _ignoreShow = false; QVector _variants; @@ -118,9 +120,12 @@ struct EmojiListWidget::RecentOne { RecentEmojiId id; }; -EmojiColorPicker::EmojiColorPicker(QWidget *parent) +EmojiColorPicker::EmojiColorPicker( + QWidget *parent, + const style::EmojiPan &st) : RpWidget(parent) -, _overBg(st::emojiPanRadius, st::emojiPanHover) { +, _st(st) +, _overBg(st::emojiPanRadius, _st.overBg) { setMouseTracking(true); } @@ -398,7 +403,7 @@ EmojiListWidget::EmojiListWidget( , _customRecentFactory(std::move(descriptor.customRecentFactory)) , _overBg(st::emojiPanRadius, st().overBg) , _collapsedBg(st::emojiPanExpand.height / 2, st::emojiPanHeaderFg) -, _picker(this) +, _picker(this, st()) , _showPickerTimer([=] { showPicker(); }) { setMouseTracking(true); if (st().bg->c.alpha() > 0) { @@ -1006,7 +1011,7 @@ void EmojiListWidget::validateEmojiPaintContext( st::stickerPanPremium1, st::stickerPanPremium2, 0.5) - : st::windowFg->c), + : st().textFg->c), .size = QSize(_customSingleSize, _customSingleSize), .now = crl::now(), .scale = context.progress, @@ -1178,7 +1183,7 @@ void EmojiListWidget::drawCollapsedBadge( const auto buttonx = position.x() + (_singleSize.width() - buttonw) / 2; const auto buttony = position.y() + (_singleSize.height() - buttonh) / 2; _collapsedBg.paint(p, QRect(buttonx, buttony, buttonw, buttonh)); - p.setPen(st::emojiPanBg); + p.setPen(this->st().bg); p.setFont(st.font); p.drawText( buttonx + (buttonw - textWidth) / 2, diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 8b24df18a..b78615721 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -69,6 +69,7 @@ public: Inner( std::shared_ptr show, + const style::EmojiPan &st, not_null parent, not_null mrows, not_null hrows, @@ -126,11 +127,13 @@ private: const std::shared_ptr _show; const not_null _session; + const style::EmojiPan &_st; const not_null _parent; const not_null _mrows; const not_null _hrows; const not_null _brows; const not_null _srows; + Ui::RoundRect _overBg; rpl::lifetime _stickersLifetime; std::weak_ptr _lottieRenderer; base::unique_qptr _menu; @@ -192,10 +195,12 @@ FieldAutocomplete::FieldAutocomplete( FieldAutocomplete::FieldAutocomplete( QWidget *parent, - std::shared_ptr show) + std::shared_ptr show, + const style::EmojiPan *stOverride) : RpWidget(parent) , _show(std::move(show)) , _session(&_show->session()) +, _st(stOverride ? *stOverride : st::defaultEmojiPan) , _scroll(this) { hide(); @@ -204,6 +209,7 @@ FieldAutocomplete::FieldAutocomplete( _inner = _scroll->setOwnedWidget( object_ptr( _show, + _st, this, &_mrows, &_hrows, @@ -285,7 +291,7 @@ void FieldAutocomplete::paintEvent(QPaintEvent *e) { return; } - p.fillRect(rect(), st::mentionBg); + p.fillRect(rect(), _st.bg); } void FieldAutocomplete::showFiltered( @@ -817,6 +823,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { FieldAutocomplete::Inner::Inner( std::shared_ptr show, + const style::EmojiPan &st, not_null parent, not_null mrows, not_null hrows, @@ -824,14 +831,16 @@ FieldAutocomplete::Inner::Inner( not_null srows) : _show(std::move(show)) , _session(&_show->session()) +, _st(st) , _parent(parent) , _mrows(mrows) , _hrows(hrows) , _brows(brows) , _srows(srows) +, _overBg(st::roundRadiusSmall, _st.overBg) , _pathGradient(std::make_unique( - st::windowBgRipple, - st::windowBgOver, + _st.pathBg, + _st.pathFg, [=] { update(); })) , _premiumMark(_session) , _previewTimer([=] { showPreview(); }) { @@ -900,7 +909,7 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { if (_sel == index) { QPoint tl(pos); if (rtl()) tl.setX(width() - tl.x() - st::stickerPanSize.width()); - Ui::FillRoundRect(p, QRect(tl, st::stickerPanSize), st::emojiPanHover, Ui::StickerHoverCorners); + _overBg.paint(p, QRect(tl, st::stickerPanSize)); } media->checkStickerSmall(); diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 1bb75f122..2606dc1f2 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/object_ptr.h" +namespace style { +struct EmojiPan; +} // namespace style + namespace Ui { class PopupMenu; class ScrollArea; @@ -53,7 +57,8 @@ public: not_null controller); FieldAutocomplete( QWidget *parent, - std::shared_ptr show); + std::shared_ptr show, + const style::EmojiPan *stOverride = nullptr); ~FieldAutocomplete(); [[nodiscard]] std::shared_ptr uiShow() const; @@ -153,6 +158,7 @@ private: const std::shared_ptr _show; const not_null _session; + const style::EmojiPan &_st; QPixmap _cache; MentionRows _mrows; HashtagRows _hrows; diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index d8ba04cf5..9ff10845d 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -98,7 +98,7 @@ GifsListWidget::GifsListWidget( GifsListDescriptor &&descriptor) : Inner( parent, - st::defaultEmojiPan, + descriptor.st ? *descriptor.st : st::defaultEmojiPan, descriptor.show, descriptor.paused) , _show(std::move(descriptor.show)) @@ -332,7 +332,7 @@ void GifsListWidget::inlineResultsDone(const MTPmessages_BotResults &result) { void GifsListWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - p.fillRect(clip, st::emojiPanBg); + p.fillRect(clip, st().bg); paintInlineItems(p, clip); } diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 36696ccab..03430e3ab 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "base/qt/qt_common_adapters.h" #include diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index a846b5362..a9aa598d6 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -1351,7 +1351,7 @@ void StickersListFooter::paintSetIconToCache( const auto y = (st().footer - icon.pixh) / 2; if (icon.custom) { icon.custom->paint(p, Ui::Text::CustomEmoji::Context{ - .textColor = st::windowFg->c, + .textColor = st().textFg->c, .size = QSize(icon.pixw, icon.pixh), .now = now, .scale = context.progress, diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 349980b6c..40aae70eb 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -179,15 +179,17 @@ StickersListWidget::StickersListWidget( StickersListDescriptor &&descriptor) : Inner( parent, - st::defaultEmojiPan, + descriptor.st ? *descriptor.st : st::defaultEmojiPan, descriptor.show, descriptor.paused) , _mode(descriptor.mode) , _show(std::move(descriptor.show)) +, _overBg(st::roundRadiusSmall, st().overBg) , _api(&session().mtp()) , _localSetsManager(std::make_unique(&session())) , _section(Section::Stickers) , _isMasks(_mode == Mode::Masks) +, _settingsHidden(descriptor.settingsHidden) , _updateItemsTimer([=] { updateItems(); }) , _updateSetsTimer([=] { updateSets(); }) , _trendingAddBgOver( @@ -201,8 +203,8 @@ StickersListWidget::StickersListWidget( ImageRoundRadius::Small, st::stickerGroupCategoryAdd.textBg) , _pathGradient(std::make_unique( - st::windowBgRipple, - st::windowBgOver, + st().pathBg, + st().pathFg, [=] { update(); })) , _megagroupSetAbout(st::columnMinimalWidthThird - st::emojiScroll.width - st().headerLeft) , _addText(tr::lng_stickers_featured_add(tr::now).toUpper()) @@ -284,7 +286,8 @@ object_ptr StickersListWidget::createFooter() { .session = &session(), .paused = footerPaused, .parent = this, - .settingsButtonVisible = true, + .settingsButtonVisible = !_settingsHidden, + .st = &st(), }); _footer = result; @@ -844,7 +847,7 @@ QRect StickersListWidget::stickerRect(int section, int sel) { void StickersListWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - p.fillRect(clip, st::emojiPanBg); + p.fillRect(clip, st().bg); paintStickers(p, clip); } @@ -1342,7 +1345,7 @@ void StickersListWidget::paintSticker( if (selected) { auto tl = pos; if (rtl()) tl.setX(width() - tl.x() - _singleSize.width()); - Ui::FillRoundRect(p, QRect(tl, _singleSize), st::emojiPanHover, Ui::StickerHoverCorners); + _overBg.paint(p, QRect(tl, _singleSize)); } media->checkStickerSmall(); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index afde28b81..dc4e7dc44 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -70,6 +70,7 @@ struct StickersListDescriptor { StickersListMode mode = StickersListMode::Full; Fn paused; const style::EmojiPan *st = nullptr; + bool settingsHidden = false; }; class StickersListWidget final : public TabbedSelector::Inner { @@ -351,6 +352,7 @@ private: const Mode _mode; const std::shared_ptr _show; + Ui::RoundRect _overBg; std::unique_ptr _search; MTP::Sender _api; std::unique_ptr _localSetsManager; @@ -373,6 +375,7 @@ private: Section _section = Section::Stickers; const bool _isMasks; + bool _settingsHidden = false; base::Timer _updateItemsTimer; base::Timer _updateSetsTimer; diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 463b35f16..5131c9f9b 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -50,7 +50,7 @@ public: void setFinalImages(Direction direction, QImage &&left, QImage &&right, QRect inner, bool wasSectionIcons); void start(); - void paintFrame(QPainter &p, float64 dt, float64 opacity); + void paintFrame(QPainter &p, const style::EmojiPan &st, float64 dt, float64 opacity); private: Direction _direction = Direction::LeftToRight; @@ -131,7 +131,11 @@ void TabbedSelector::SlideAnimation::start() { _frameIntsPerLineAdd = (_width - _innerWidth) + _frameIntsPerLineAdded; } -void TabbedSelector::SlideAnimation::paintFrame(QPainter &p, float64 dt, float64 opacity) { +void TabbedSelector::SlideAnimation::paintFrame( + QPainter &p, + const style::EmojiPan &st, + float64 dt, + float64 opacity) { Expects(started()); Expects(dt >= 0.); @@ -168,8 +172,8 @@ void TabbedSelector::SlideAnimation::paintFrame(QPainter &p, float64 dt, float64 { auto p = QPainter(&_frame); p.setOpacity(opacity); - p.fillRect(_painterInnerLeft, _painterInnerTop, _painterInnerWidth, _painterCategoriesTop - _painterInnerTop, st::emojiPanBg); - p.fillRect(_painterInnerLeft, _painterCategoriesTop, _painterInnerWidth, _painterInnerBottom - _painterCategoriesTop, _wasSectionIcons ? st::emojiPanCategories : st::emojiPanBg); + p.fillRect(_painterInnerLeft, _painterInnerTop, _painterInnerWidth, _painterCategoriesTop - _painterInnerTop, st.bg); + p.fillRect(_painterInnerLeft, _painterCategoriesTop, _painterInnerWidth, _painterInnerBottom - _painterCategoriesTop, _wasSectionIcons ? st.categoriesBg : st.bg); p.setCompositionMode(QPainter::CompositionMode_SourceOver); if (leftTo > _innerLeft) { p.setOpacity(opacity * leftAlpha); @@ -325,11 +329,24 @@ TabbedSelector::TabbedSelector( std::shared_ptr show, PauseReason level, Mode mode) +: TabbedSelector(parent, { + .show = std::move(show), + .st = (mode == Mode::EmojiStatus + ? st::statusEmojiPan + : st::defaultEmojiPan), + .level = level, + .mode = mode, +}) { +} + +TabbedSelector::TabbedSelector( + QWidget *parent, + TabbedSelectorDescriptor &&descriptor) : RpWidget(parent) -, _st((mode == Mode::EmojiStatus) ? st::statusEmojiPan : st::defaultEmojiPan) -, _show(std::move(show)) -, _level(level) -, _mode(mode) +, _st(descriptor.st) +, _show(std::move(descriptor.show)) +, _level(descriptor.level) +, _mode(descriptor.mode) , _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg)) , _categoriesRounding( Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.categoriesBg)) @@ -360,6 +377,7 @@ TabbedSelector::TabbedSelector( : SelectorTab::Emoji) , _hasEmojiTab(ranges::contains(_tabs, SelectorTab::Emoji, &Tab::type)) , _hasStickersTab(ranges::contains(_tabs, SelectorTab::Stickers, &Tab::type)) +, _stickersSettingsHidden(descriptor.stickersSettingsHidden) , _hasGifsTab(ranges::contains(_tabs, SelectorTab::Gifs, &Tab::type)) , _hasMasksTab(ranges::contains(_tabs, SelectorTab::Masks, &Tab::type)) , _tabbed(_tabs.size() > 1) { @@ -445,10 +463,10 @@ TabbedSelector::TabbedSelector( ) | rpl::start_with_next([=] { _panelRounding = Ui::PrepareCornerPixmaps( st::emojiPanRadius, - st::emojiPanBg); + _st.bg); _categoriesRounding = Ui::PrepareCornerPixmaps( st::emojiPanRadius, - st::emojiPanCategories); + _st.categoriesBg); }, lifetime()); if (hasEmojiTab()) { @@ -503,6 +521,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { .mode = StickersMode::Full, .paused = paused, .st = &_st, + .settingsHidden = _stickersSettingsHidden, }); } case SelectorTab::Gifs: { @@ -704,10 +723,10 @@ void TabbedSelector::paintSlideFrame(QPainter &p) { if (_roundRadius > 0) { paintBgRoundedPart(p); } else if (_tabsSlider) { - p.fillRect(0, 0, width(), _tabsSlider->height(), st::emojiPanBg); + p.fillRect(0, 0, width(), _tabsSlider->height(), _st.bg); } auto slideDt = _a_slide.value(1.); - _slideAnimation->paintFrame(p, slideDt, 1.); + _slideAnimation->paintFrame(p, _st, slideDt, 1.); } void TabbedSelector::paintBgRoundedPart(QPainter &p) { @@ -716,7 +735,7 @@ void TabbedSelector::paintBgRoundedPart(QPainter &p) { : _tabsSlider ? QRect(0, 0, width(), _tabsSlider->height()) : QRect(0, 0, width(), _roundRadius); - Ui::FillRoundRect(p, fill, st::emojiPanBg, { + Ui::FillRoundRect(p, fill, _st.bg, { .p = { _dropDown ? QPixmap() : _panelRounding.p[0], _dropDown ? QPixmap() : _panelRounding.p[1], @@ -765,10 +784,10 @@ void TabbedSelector::paintContent(QPainter &p) { sidesTop, st::emojiScroll.width, sidesHeight), - st::emojiPanBg); + _st.bg); p.fillRect( myrtlrect(0, sidesTop, st::emojiPanRadius, sidesHeight), - st::emojiPanBg); + _st.bg); } } @@ -1030,7 +1049,7 @@ void TabbedSelector::setAllowEmojiWithoutPremium(bool allow) { } void TabbedSelector::createTabsSlider() { - _tabsSlider.create(this, st::emojiTabs); + _tabsSlider.create(this, _st.tabs); fillTabsSliderSections(); @@ -1324,7 +1343,7 @@ void TabbedSelector::Inner::paintEmptySearchResults( iconTop + icon.height() - st::normalFont->height, height() - 2 * st::normalFont->height); p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); + p.setPen(_st.tabs.labelFg); p.drawTextLeft( (width() - textWidth) / 2, textTop, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index f7f65811a..bcaa4fdee 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -75,6 +75,21 @@ struct EmojiChosen { using InlineChosen = InlineBots::ResultSelected; +enum class TabbedSelectorMode { + Full, + EmojiOnly, + MediaEditor, + EmojiStatus, +}; + +struct TabbedSelectorDescriptor { + std::shared_ptr show; + const style::EmojiPan &st; + PauseReason level = {}; + TabbedSelectorMode mode = TabbedSelectorMode::Full; + bool stickersSettingsHidden = false; +}; + [[nodiscard]] std::unique_ptr MakeSearch( not_null parent, const style::EmojiPan &st, @@ -86,12 +101,7 @@ using InlineChosen = InlineBots::ResultSelected; class TabbedSelector : public Ui::RpWidget { public: static constexpr auto kPickCustomTimeId = -1; - enum class Mode { - Full, - EmojiOnly, - MediaEditor, - EmojiStatus, - }; + using Mode = TabbedSelectorMode; enum class Action { Update, Cancel, @@ -102,6 +112,9 @@ public: std::shared_ptr show, PauseReason level, Mode mode = Mode::Full); + TabbedSelector( + QWidget *parent, + TabbedSelectorDescriptor &&descriptor); ~TabbedSelector(); [[nodiscard]] Main::Session &session() const; @@ -278,6 +291,7 @@ private: const bool _hasEmojiTab; const bool _hasStickersTab; + const bool _stickersSettingsHidden; const bool _hasGifsTab; const bool _hasMasksTab; const bool _tabbed; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 86e37617c..87da53703 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -229,7 +229,7 @@ HistoryWidget::HistoryWidget( , _supportAutocomplete(session().supportMode() ? object_ptr(this, &session()) : nullptr) -, _send(std::make_shared(this)) +, _send(std::make_shared(this, st::historySend)) , _unblock(this, tr::lng_unblock_button(tr::now).toUpper(), st::historyUnblock) , _botStart(this, tr::lng_bot_start(tr::now).toUpper(), st::historyComposeButton) , _joinChannel( diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index a4edc44cd..a3fd8b3c8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -58,7 +58,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/session/send_as_peers.h" #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio.h" -#include "styles/style_chat.h" #include "ui/text/text_options.h" #include "ui/ui_utility.h" #include "ui/widgets/input_fields.h" @@ -72,6 +71,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "mainwindow.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { @@ -938,7 +939,11 @@ ComposeControls::ComposeControls( ComposeControls::ComposeControls( not_null parent, ComposeControlsDescriptor descriptor) -: _parent(parent) +: _st(descriptor.stOverride + ? *descriptor.stOverride + : st::defaultComposeControls) +, _features(descriptor.features) +, _parent(parent) , _show(std::move(descriptor.show)) , _session(&_show->session()) , _regularWindow(descriptor.regularWindow) @@ -946,30 +951,37 @@ ComposeControls::ComposeControls( ? nullptr : std::make_unique( _parent, - _show, - Window::GifPauseReason::TabbedPanel)) + ChatHelpers::TabbedSelectorDescriptor{ + .show = _show, + .st = _st.tabbed, + .level = Window::GifPauseReason::TabbedPanel, + .mode = ChatHelpers::TabbedSelector::Mode::Full, + .stickersSettingsHidden = !_features.stickersSettings, + })) , _selector(_regularWindow ? _regularWindow->tabbedSelector() : not_null(_ownedSelector.get())) , _mode(descriptor.mode) , _wrap(std::make_unique(parent)) , _writeRestricted(std::make_unique(parent)) -, _send(std::make_shared(_wrap.get())) +, _send(std::make_shared(_wrap.get(), _st.send)) , _attachToggle(Ui::CreateChild( _wrap.get(), - st::historyAttach)) + _st.attach)) , _tabbedSelectorToggle(Ui::CreateChild( _wrap.get(), - st::historyAttachEmoji)) + _st.emoji)) , _field( Ui::CreateChild( _wrap.get(), - st::historyComposeField, + _st.field, Ui::InputField::Mode::MultiLine, tr::lng_message_ph())) -, _botCommandStart(Ui::CreateChild( - _wrap.get(), - st::historyBotCommandStart)) +, _botCommandStart(_features.botCommandSend + ? Ui::CreateChild( + _wrap.get(), + st::historyBotCommandStart) + : nullptr) , _autocomplete(std::make_unique(parent, _show)) , _header(std::make_unique(_wrap.get(), _show)) , _voiceRecordBar(std::make_unique( @@ -982,6 +994,9 @@ ComposeControls::ComposeControls( , _unavailableEmojiPasted(std::move(descriptor.unavailableEmojiPasted)) , _saveDraftTimer([=] { saveDraft(); }) , _saveCloudDraftTimer([=] { saveCloudDraft(); }) { + if (_st.radius > 0) { + _backgroundRect.emplace(_st.radius, _st.bg); + } if (descriptor.stickerOrEmojiChosen) { std::move( descriptor.stickerOrEmojiChosen @@ -1421,7 +1436,9 @@ void ComposeControls::init() { updateWrappingVisibility(); }, _wrap->lifetime()); - _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); + if (_botCommandStart) { + _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); + } _wrap->sizeValue( ) | rpl::start_with_next([=](QSize size) { @@ -2382,9 +2399,11 @@ void ComposeControls::updateControlsGeometry(QSize size) { right += _send->width(); _tabbedSelectorToggle->moveToRight(right, buttonsTop); right += _tabbedSelectorToggle->width(); - _botCommandStart->moveToRight(right, buttonsTop); - if (_botCommandShown) { - right += _botCommandStart->width(); + if (_botCommandStart) { + _botCommandStart->moveToRight(right, buttonsTop); + if (_botCommandShown) { + right += _botCommandStart->width(); + } } if (_silent) { _silent->moveToRight(right, buttonsTop); @@ -2401,7 +2420,9 @@ void ComposeControls::updateControlsGeometry(QSize size) { } void ComposeControls::updateControlsVisibility() { - _botCommandStart->setVisible(_botCommandShown); + if (_botCommandStart) { + _botCommandStart->setVisible(_botCommandShown); + } if (_ttlInfo) { _ttlInfo->show(); } @@ -2419,7 +2440,8 @@ void ComposeControls::updateControlsVisibility() { bool ComposeControls::updateBotCommandShown() { auto shown = false; const auto peer = _history ? _history->peer.get() : nullptr; - if (peer + if (_botCommandStart + && peer && ((peer->isChat() && peer->asChat()->botStatus > 0) || (peer->isMegagroup() && peer->asChannel()->mgInfo->botStatus > 0) || (peer->isUser() && peer->asUser()->isBot()))) { @@ -2449,7 +2471,9 @@ void ComposeControls::updateOuterGeometry(QRect rect) { void ComposeControls::updateMessagesTTLShown() { const auto peer = _history ? _history->peer.get() : nullptr; - const auto shown = peer && (peer->messagesTTL() > 0); + const auto shown = _features.ttlInfo + && peer + && (peer->messagesTTL() > 0); if (!shown && _ttlInfo) { _ttlInfo = nullptr; updateControlsVisibility(); @@ -2467,7 +2491,8 @@ void ComposeControls::updateMessagesTTLShown() { bool ComposeControls::updateSendAsButton() { const auto peer = _history ? _history->peer.get() : nullptr; - if (!peer + if (!_features.sendAs + || !peer || !_regularWindow || isEditingMessage() || !session().sendAsPeers().shouldChoose(peer)) { @@ -2491,7 +2516,10 @@ bool ComposeControls::updateSendAsButton() { void ComposeControls::updateAttachBotsMenu() { _attachBotsMenu = nullptr; - if (!_history || !_sendActionFactory || !_regularWindow) { + if (!_features.attachBotsMenu + || !_history + || !_sendActionFactory + || !_regularWindow) { return; } _attachBotsMenu = InlineBots::MakeAttachBotsMenu( @@ -2515,7 +2543,18 @@ void ComposeControls::updateAttachBotsMenu() { void ComposeControls::paintBackground(QRect clip) { Painter p(_wrap.get()); - p.fillRect(clip, st::historyComposeAreaBg); + if (_backgroundRect) { + //p.setCompositionMode(QPainter::CompositionMode_Source); + //p.fillRect(clip, Qt::transparent); + //p.setCompositionMode(QPainter::CompositionMode_SourceOver); + //_backgroundRect->paint(p, _wrap->rect()); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(_st.bg); + p.setPen(Qt::NoPen); + p.drawRoundedRect(_wrap->rect(), _st.radius, _st.radius); + } else { + p.fillRect(clip, _st.bg); + } } void ComposeControls::escape() { @@ -2913,7 +2952,7 @@ bool ComposeControls::preventsClose(Fn &&continueCallback) const { } bool ComposeControls::hasSilentBroadcastToggle() const { - if (!_history) { + if (!_features.silentBroadcastToggle || !_history) { return false; } const auto &peer = _history->peer; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 16a114ec8..d5749bfcb 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "dialogs/dialogs_key.h" #include "history/view/controls/compose_controls_common.h" +#include "ui/round_rect.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/widgets/input_fields.h" @@ -21,6 +22,10 @@ class History; class DocumentData; class FieldAutocomplete; +namespace style { +struct ComposeControls; +} // namespace style + namespace SendMenu { enum class Type; } // namespace SendMenu @@ -88,13 +93,25 @@ enum class ComposeControlsMode { Scheduled, }; +struct ComposeControlsFeatures { + bool sendAs = true; + bool ttlInfo = true; + bool botCommandSend = true; + bool silentBroadcastToggle = true; + bool attachBotsMenu = true; + bool inlineBots = true; + bool stickersSettings = true; +}; + struct ComposeControlsDescriptor { + const style::ComposeControls *stOverride = nullptr; std::shared_ptr show; Fn)> unavailableEmojiPasted; ComposeControlsMode mode = ComposeControlsMode::Normal; SendMenu::Type sendMenuType = {}; Window::SessionController *regularWindow = nullptr; rpl::producer stickerOrEmojiChosen; + ComposeControlsFeatures features; }; class ComposeControls final { @@ -311,6 +328,8 @@ private: void registerDraftSource(); void changeFocusedControl(); + const style::ComposeControls &_st; + const ComposeControlsFeatures _features; const not_null _parent; const std::shared_ptr _show; const not_null _session; @@ -332,12 +351,14 @@ private: const std::unique_ptr _wrap; const std::unique_ptr _writeRestricted; + std::optional _backgroundRect; + const std::shared_ptr _send; const not_null _attachToggle; std::unique_ptr _replaceMedia; const not_null _tabbedSelectorToggle; const not_null _field; - const not_null _botCommandStart; + Ui::IconButton * const _botCommandStart = nullptr; std::unique_ptr _sendAs; std::unique_ptr _silent; std::unique_ptr _ttlInfo; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 000635db6..f800f192c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -26,6 +26,7 @@ ReplyArea::ReplyArea(not_null controller) , _controls(std::make_unique( _controller->wrap(), HistoryView::ComposeControlsDescriptor{ + .stOverride = &st::storiesComposeControls, .show = _controller->uiShow(), .unavailableEmojiPasted = [=](not_null emoji) { showPremiumToast(emoji); @@ -33,6 +34,14 @@ ReplyArea::ReplyArea(not_null controller) .mode = HistoryView::ComposeControlsMode::Normal, .sendMenuType = SendMenu::Type::SilentOnly, .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), + .features = { + .sendAs = false, + .ttlInfo = false, + .botCommandSend = false, + .silentBroadcastToggle = false, + .attachBotsMenu = false, + .inlineBots = false, + }, } )) { initGeometry(); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 3cd675b90..7e01278eb 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -11,6 +11,7 @@ using "ui/widgets/widgets.style"; using "ui/menu_icons.style"; using "media/player/media_player.style"; using "boxes/boxes.style"; +using "chat_helpers/chat_helpers.style"; mediaviewOverDuration: 150; @@ -434,22 +435,106 @@ storiesShadowTop: icon{{ "mediaview/shadow_bottom-flip_vertical", windowShadowFg storiesShadowBottom: mediaviewShadowBottom; storiesControlsMinWidth: 200px; storiesFieldMargin: margins(0px, 14px, 0px, 16px); -storiesAttach: IconButton(defaultIconButton) { - width: 44px; - height: 46px; - - icon: icon {{ "chat/input_attach", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_attach", historyComposeIconFgOver }}; - - rippleAreaPosition: point(2px, 3px); - rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } -} storiesSideSkip: 145px; storiesCaptionFull: FlatLabel(defaultFlatLabel) { style: mediaviewCaptionStyle; textFg: mediaviewCaptionFg; minWidth: 360px; } +storiesComposeBg: groupCallMembersBg; +storiesComposeBgOver: groupCallMembersBgOver; +storiesComposeBgRipple: groupCallMembersBgRipple; +storiesComposeWhiteText: groupCallMembersFg; +storiesComposeGrayText: groupCallMemberNotJoinedStatus; +storiesComposeGrayIcon: groupCallMemberInactiveIcon; +storiesComposeBlue: groupCallActiveFg; +storiesComposeRipple: RippleAnimation(defaultRippleAnimation) { + color: groupCallMembersBgRipple; +} +storiesAttach: IconButton(historyAttach) { + icon: icon {{ "chat/input_attach", storiesComposeGrayIcon }}; + iconOver: icon {{ "chat/input_attach", storiesComposeGrayIcon }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: storiesComposeBgOver; + } +} +storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; +storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; +storiesComposeControls: ComposeControls(defaultComposeControls) { + bg: storiesComposeBg; + radius: storiesRadius; + field: InputField(historyComposeField) { + textFg: storiesComposeWhiteText; + textBg: storiesComposeBg; + placeholderFg: storiesComposeGrayText; + placeholderFgActive: storiesComposeGrayText; + placeholderFgError: storiesComposeGrayText; + } + send: SendButton(historySend) { + inner: IconButton(storiesAttach) { + icon: icon {{ "chat/input_send", storiesComposeBlue }}; + iconOver: icon {{ "chat/input_send", storiesComposeBlue }}; + } + record: storiesRecordVoice; + recordOver: storiesRecordVoiceOver; + sendDisabledFg: storiesComposeGrayText; + } + attach: storiesAttach; + emoji: EmojiButton(historyAttachEmoji) { + inner: IconButton(storiesAttach) { + icon: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }}; + iconOver: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }}; + } + bg: storiesComposeBg; + lineFg: storiesComposeGrayIcon; + lineFgOver: storiesComposeGrayIcon; + } + tabbed: EmojiPan(defaultEmojiPan) { + bg: storiesComposeBg; + overBg: storiesComposeBgOver; + expandBg: storiesComposeGrayText; + pathBg: storiesComposeBgRipple; + pathFg: storiesComposeBgOver; + textFg: storiesComposeWhiteText; + categoriesBg: storiesComposeBg; + categoriesBgOver: storiesComposeBgOver; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", storiesComposeBg }}; + fadeRight: icon {{ "fade_horizontal", storiesComposeBg }}; + tabs: SettingsSlider(emojiTabs) { + barFgActive: storiesComposeBlue; + labelFg: storiesComposeGrayText; + labelFgActive: storiesComposeBlue; + rippleBg: storiesComposeBgOver; + rippleBgActive: storiesComposeBgOver; + } + search: TabbedSearch(defaultTabbedSearch) { + outer: storiesComposeBg; + bg: storiesComposeBgOver; + bgActive: storiesComposeBgRipple; + fg: storiesComposeGrayIcon; + fgActive: storiesComposeWhiteText; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", storiesComposeBgOver }}; + fadeRight: icon {{ "fade_horizontal", storiesComposeBgOver }}; + field: InputField(defaultTabbedSearchField) { + placeholderFg: storiesComposeGrayText; + placeholderFgActive: storiesComposeGrayText; + placeholderFgError: storiesComposeGrayText; + } + search: IconButton(defaultTabbedSearchButton) { + icon: icon{{ "emoji/emoji_search_input", storiesComposeGrayIcon }}; + iconOver: icon{{ "emoji/emoji_search_input", storiesComposeGrayIcon }}; + ripple: storiesComposeRipple; + } + back: IconButton(defaultTabbedSearchBack) { + icon: icon{{ "emoji/emoji_back", storiesComposeGrayIcon }}; + iconOver: icon{{ "emoji/emoji_back", storiesComposeGrayIcon }}; + ripple: storiesComposeRipple; + } + cancel: CrossButton(defaultTabbedSearchCancel) { + crossFg: storiesComposeGrayIcon; + crossFgOver: storiesComposeGrayIcon; + ripple: emptyRippleAnimation; + } + } + } +} diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 2d7da4d8b..8ff30f817 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -89,6 +89,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "calls/calls_instance.h" #include "styles/style_media_view.h" +#include "styles/style_calls.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" @@ -336,6 +337,8 @@ OverlayWidget::OverlayWidget() , _lastAction(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction) , _stateAnimation([=](crl::time now) { return stateAnimationCallback(now); }) , _dropdown(_body, st::mediaviewDropdownMenu) { + _layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox); + CrashReports::SetAnnotation("OpenGL Renderer", "[not-initialized]"); Lang::Updated( diff --git a/Telegram/SourceFiles/ui/boxes/time_picker_box.cpp b/Telegram/SourceFiles/ui/boxes/time_picker_box.cpp index 746fbd4e2..0cf0d5121 100644 --- a/Telegram/SourceFiles/ui/boxes/time_picker_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/time_picker_box.cpp @@ -13,7 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animation_value.h" #include "ui/ui_utility.h" #include "ui/widgets/vertical_drum_picker.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" namespace Ui { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp index f6e8ee28e..60cc5ae68 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "base/timer_rpl.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_boxes.h" namespace Ui { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp index 56e4bdcb7..2e71553ab 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/power_saving.h" #include "base/call_delayed.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_boxes.h" #include diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 7e6b3dfcd..d4b66ab07 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -9,22 +9,7 @@ using "ui/basic.style"; using "dialogs/dialogs.style"; using "ui/widgets/widgets.style"; using "ui/menu_icons.style"; - -MessageBar { - title: TextStyle; - titleFg: color; - text: TextStyle; - textFg: color; - textPalette: TextPalette; - duration: int; -} - -GroupCallUserpics { - size: pixels; - shift: pixels; - stroke: pixels; - align: align; -} +using "chat_helpers/chat_helpers.style"; // GroupCallUserpics msgMaxWidth: 430px; msgFont: font(fsize); @@ -145,9 +130,6 @@ outSemiboldPalette: TextPalette(outTextPalette) { selectFg: msgOutServiceFgSelected; selectLinkFg: msgOutServiceFgSelected; } -historyComposeAreaPalette: TextPalette(defaultTextPalette) { - linkFg: historyComposeAreaFgService; -} mediaCaptionSkip: 5px; mediaInBubbleSkip: 5px; @@ -162,15 +144,6 @@ mediaInPaletteSelected: TextPalette(defaultTextPalette) { linkFg: mediaInFgSelected; } -defaultMessageBar: MessageBar { - title: semiboldTextStyle; - titleFg: windowActiveTextFg; - text: messageTextStyle; - textFg: historyComposeAreaFg; - textPalette: historyComposeAreaPalette; - duration: 160; -} - minPhotoSize: 100px; maxMediaSize: 430px; maxStickerSize: 224px; @@ -229,58 +202,6 @@ historyPremiumViewSet: RoundButton(defaultActiveButton) { ripple: emptyRippleAnimation; } -historyToDownPosition: point(12px, 10px); -historyToDownAbove: icon {{ "history_down_arrow", historyToDownFg }}; -historyToDownAboveOver: icon {{ "history_down_arrow", historyToDownFgOver }}; -historyToDownPaddingTop: 10px; -historyToDownBelow: icon { - { "history_down_shadow", historyToDownShadow }, - { "history_down_circle", historyToDownBg }, -}; -historyToDownBelowOver: icon { - { "history_down_shadow", historyToDownShadow }, - { "history_down_circle", historyToDownBgOver }, -}; -historyToDown: TwoIconButton { - width: 52px; - height: 62px; - - iconBelow: historyToDownBelow; - iconBelowOver: historyToDownBelowOver; - iconAbove: historyToDownAbove; - iconAboveOver: historyToDownAboveOver; - iconPosition: point(0px, historyToDownPaddingTop); - - rippleAreaPosition: point(5px, 15px); - rippleAreaSize: 42px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: historyToDownBgRipple; - } -} -historyToDownBadgeFont: semiboldFont; -historyToDownBadgeSize: 22px; - -historyToDownShownAfter: 480px; -historyToDownDuration: 150; - -dialogsToUpAbove: icon {{ "history_down_arrow-flip_vertical", historyToDownFg, point(0px, 1px) }}; -dialogsToUpAboveOver: icon {{ "history_down_arrow-flip_vertical", historyToDownFgOver, point(0px, 1px) }}; - -dialogsToUp: TwoIconButton(historyToDown) { - iconAbove: dialogsToUpAbove; - iconAboveOver: dialogsToUpAboveOver; -} - -historyUnreadMentions: TwoIconButton(historyToDown) { - iconAbove: icon {{ "history_unread_mention", historyToDownFg }}; - iconAboveOver: icon {{ "history_unread_mention", historyToDownFgOver }}; -} -historyUnreadReactions: TwoIconButton(historyToDown) { - iconAbove: icon {{ "history_unread_reaction", historyToDownFg }}; - iconAboveOver: icon {{ "history_unread_reaction", historyToDownFgOver }}; -} -historyUnreadThingsSkip: 4px; - membersInnerWidth: 310px; membersInnerHeightMax: 360px; membersInnerDropdown: InnerDropdown(defaultInnerDropdown) { @@ -366,29 +287,6 @@ historyPinOutIcon: icon {{ "history_pin", historyOutIconFg }}; historyPinOutSelectedIcon: icon {{ "history_pin", historyOutIconFgSelected }}; historyPinInvertedIcon: icon {{ "history_pin", historySendingInvertedIconFg }}; -historyComposeField: InputField(defaultInputField) { - font: msgFont; - textMargins: margins(0px, 0px, 0px, 0px); - textAlign: align(left); - textFg: historyComposeAreaFg; - textBg: historyComposeAreaBg; - heightMin: 36px; - heightMax: 72px; - placeholderFg: placeholderFg; - placeholderFgActive: placeholderFgActive; - placeholderFgError: placeholderFgActive; - placeholderMargins: margins(7px, 5px, 7px, 5px); - placeholderAlign: align(topleft); - placeholderScale: 0.; - placeholderFont: normalFont; - placeholderShift: -50px; - border: 0px; - borderActive: 0px; - duration: 100; -} -historyComposeFieldMaxHeight: 224px; -// historyMinHeight: 56px; - historySendPadding: 9px; historySendRight: 2px; historyBotMenuSkip: 8px; @@ -399,245 +297,6 @@ historyBotMenuButton: RoundButton(defaultActiveButton) { textTop: 6px; } -historyComposeButton: FlatButton { - color: windowActiveTextFg; - overColor: windowActiveTextFg; - - bgColor: historyComposeButtonBg; - overBgColor: historyComposeButtonBgOver; - - width: -32px; - height: 46px; - - textTop: 14px; - - font: semiboldFont; - overFont: semiboldFont; - - ripple: RippleAnimation(defaultRippleAnimation) { - color: historyComposeButtonBgRipple; - } -} -historyUnblock: FlatButton(historyComposeButton) { - color: attentionButtonFg; - overColor: attentionButtonFgOver; -} -historyContactStatusButton: FlatButton(historyComposeButton) { - height: 49px; - textTop: 16px; - overBgColor: historyComposeButtonBg; - ripple: RippleAnimation(defaultRippleAnimation) { - color: historyComposeButtonBgOver; - } -} -historyContactStatusBlock: FlatButton(historyContactStatusButton) { - color: attentionButtonFg; - overColor: attentionButtonFg; -} -historyContactStatusLabel: FlatLabel(defaultFlatLabel) { - minWidth: 240px; -} -historyEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) { - align: align(top); - textFg: windowSubTextFg; -} -historyContactStatusMinSkip: 16px; - -historySendIcon: icon {{ "chat/input_send", historySendIconFg }}; -historySendIconOver: icon {{ "chat/input_send", historySendIconFgOver }}; -historySendIconPosition: point(10px, 11px); -historySendSize: size(44px, 46px); -historyScheduleIcon: icon {{ "chat/input_schedule", historyComposeAreaBg }}; -historyScheduleIconPosition: point(7px, 8px); -historyEditSaveIcon: icon {{ "chat/input_save", historySendIconFg }}; -historyEditSaveIconOver: icon {{ "chat/input_save", historySendIconFgOver }}; - -historyAttach: IconButton(defaultIconButton) { - width: 44px; - height: 46px; - - icon: icon {{ "chat/input_attach", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_attach", historyComposeIconFgOver }}; - - rippleAreaPosition: point(2px, 3px); - rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } -} - -historyAttachEmoji: IconButton(historyAttach) { - icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_smile_face", historyComposeIconFgOver }}; -} -historyMessagesTTL: IconButtonWithText { - iconButton: IconButton(historyAttach) { - icon: icon {{ "chat/input_autodelete", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_autodelete", historyComposeIconFgOver }}; - } - textFg: historyComposeIconFg; - textFgOver: historyComposeIconFgOver; - textPadding: margins(21px, 20px, 3px, 7px); - textAlign: align(left); - - font: font(10px semibold); -} -historyReplaceMedia: IconButton(historyAttach) { - icon: icon {{ "chat/input_replace", windowBgActive }}; - iconOver: icon {{ "chat/input_replace", windowBgActive }}; - ripple: RippleAnimation(defaultRippleAnimation) { - color: lightButtonBgOver; - } -} -historyEditMediaBg: videoPlayIconBg; -historyEditMedia: icon{{ "chat/input_draw", videoPlayIconFg }}; -historyMessagesTTLPickerHeight: 200px; -historyMessagesTTLPickerItemHeight: 40px; -historyMessagesTTLLabel: FlatLabel(defaultFlatLabel) { - minWidth: 200px; - align: align(topleft); - textFg: windowSubTextFg; -} - -historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }}; -historyEmojiCircle: size(20px, 20px); -historyEmojiCircleLine: 1.5; -historyEmojiCircleFg: historyComposeIconFg; -historyEmojiCircleFgOver: historyComposeIconFgOver; -historyBotKeyboardShow: IconButton(historyAttach) { - icon: icon {{ "chat/input_bot_keyboard", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_bot_keyboard", historyComposeIconFgOver }}; -} -historyBotKeyboardHide: IconButton(historyAttach) { - icon: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFgOver }}; -} -historyBotCommandStart: IconButton(historyAttach) { - icon: icon {{ "chat/input_bot_command", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_bot_command", historyComposeIconFgOver }}; -} -historyScheduledToggle: IconButton(historyAttach) { - icon: icon { - { "chat/input_scheduled", historyComposeIconFg }, - { "chat/input_scheduled_dot", attentionButtonFg } - }; - iconOver: icon { - { "chat/input_scheduled", historyComposeIconFgOver }, - { "chat/input_scheduled_dot", attentionButtonFg } - }; -} - -historyRecordVoiceFg: historyComposeIconFg; -historyRecordVoiceFgOver: historyComposeIconFgOver; -historyRecordVoiceFgInactive: attentionButtonFg; -historyRecordVoiceFgActive: windowBgActive; -historyRecordVoiceFgActiveIcon: windowFgActive; -historyRecordVoiceShowDuration: 120; -historyRecordVoiceDuration: 120; -historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; -historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; -historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; -historyRecordSendIconPosition: point(2px, 0px); -historyRecordVoiceRippleBgActive: lightButtonBgOver; -historyRecordSignalRadius: 5px; -historyRecordCancel: windowSubTextFg; -historyRecordCancelActive: windowActiveTextFg; -historyRecordFont: font(13px); -historyRecordDurationSkip: 12px; -historyRecordDurationFg: historyComposeAreaFg; - -historyRecordMainBlobMinRadius: 23px; -historyRecordMainBlobMaxRadius: 37px; -historyRecordMinorBlobMinRadius: 40px; -historyRecordMinorBlobMaxRadius: 47px; -historyRecordMajorBlobMinRadius: 43px; -historyRecordMajorBlobMaxRadius: 50px; - -historyRecordTextStyle: TextStyle(defaultTextStyle) { - font: historyRecordFont; -} - -historyRecordTextWidthForWrap: 210px; -historyRecordTextLeft: 15px; -historyRecordTextRight: 25px; - -historyRecordLockShowDuration: historyToDownDuration; -historyRecordLockSize: size(75px, 133px); - -historyRecordLockIconFg: historyToDownFg; -historyRecordLockIconSize: size(14px, 17px); -historyRecordLockIconBottomHeight: 9px; -historyRecordLockIconLineHeight: 2px; -historyRecordLockIconLineSkip: 3px; -historyRecordLockIconLineWidth: 2px; -historyRecordLockIconArcHeight: 4px; -historyRecordStopIconWidth: 12px; - -historyRecordLockTopShadow: icon {{ "voice_lock/record_lock_top_shadow", historyToDownShadow }}; -historyRecordLockTop: icon {{ "voice_lock/record_lock_top", historyToDownBg }}; -historyRecordLockBottomShadow: icon {{ "voice_lock/record_lock_bottom_shadow", historyToDownShadow }}; -historyRecordLockBottom: icon {{ "voice_lock/record_lock_bottom", historyToDownBg }}; -historyRecordLockBodyShadow: icon {{ "voice_lock/record_lock_body_shadow", historyToDownShadow }}; -historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }}; -historyRecordLockMargin: margins(4px, 4px, 4px, 4px); -historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }}; -historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px); - -historyRecordDelete: IconButton(historyAttach) { - icon: icon {{ "info/info_media_delete", historyComposeIconFg }}; - iconOver: icon {{ "info/info_media_delete", historyComposeIconFgOver }}; - iconPosition: point(10px, 11px); -} -historyRecordWaveformRightSkip: 10px; -historyRecordWaveformBgMargins: margins(5px, 7px, 5px, 7px); - -historyRecordWaveformBar: 3px; - -historyRecordLockPosition: point(1px, 35px); - -historyRecordCancelButtonWidth: 100px; -historyRecordCancelButtonFg: lightButtonFg; - -historySilentToggle: IconButton(historyBotKeyboardShow) { - icon: icon {{ "chat/input_silent", historyComposeIconFg }}; - iconOver: icon {{ "chat/input_silent", historyComposeIconFgOver }}; -} -historySilentToggleOn: icon {{ "chat/input_silent_on", historyComposeIconFg }}; -historySilentToggleOnOver: icon {{ "chat/input_silent_on", historyComposeIconFgOver }}; - -historyReplySkip: 51px; -historyReplyNameFg: windowActiveTextFg; -historyReplyHeight: 49px; -historyReplyIconPosition: point(5px, 5px); -historyReplyIcon: icon {{ "chat/input_reply", historyReplyIconFg }}; -historyForwardIcon: icon {{ "chat/input_forward", historyReplyIconFg }}; -historyEditIcon: icon {{ "chat/input_edit", historyReplyIconFg }}; -historyReplyCancel: IconButton { - width: 49px; - height: 49px; - - icon: historyReplyCancelIcon; - iconOver: historyReplyCancelIconOver; - iconPosition: point(-1px, -1px); - - rippleAreaPosition: point(4px, 4px); - rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } -} -historyPinnedShowAll: IconButton(historyReplyCancel) { - icon: icon {{ "pinned_show_all", historyReplyCancelFg }}; - iconOver: icon {{ "pinned_show_all", historyReplyCancelFgOver }}; -} -historyPinnedBotButton: RoundButton(defaultActiveButton) { - width: -34px; - height: 30px; - textTop: 6px; - padding: margins(2px, 10px, 10px, 9px); -} -historyPinnedBotButtonMaxWidth: 150px; - topicButtonSkip: 3px; topicButtonPadding: margins(6px, 3px, 8px, 3px); topicButtonArrowSkip: 8px; @@ -960,13 +619,6 @@ historyCommentsUserpics: GroupCallUserpics { align: align(left); } -boxAttachEmoji: IconButton(historyAttachEmoji) { - width: 30px; - height: 30px; - rippleAreaSize: 0px; -} -boxAttachEmojiTop: 20px; - historyGroupAboutMargin: 16px; historyGroupAboutPadding: margins(24px, 16px, 24px, 16px); historyGroupAboutBulletSkip: 16px; @@ -1012,8 +664,6 @@ historyCommentsOpenInSelected: icon {{ "history_comments_open", msgFileThumbLink historyCommentsOpenOut: icon {{ "history_comments_open", msgFileThumbLinkOutFg }}; historyCommentsOpenOutSelected: icon {{ "history_comments_open", msgFileThumbLinkOutFgSelected }}; -historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px); - historyGroupCallUserpics: GroupCallUserpics { size: 32px; shift: 12px; @@ -1137,13 +787,6 @@ reactionsTabs: MultiSelect(defaultMultiSelect) { padding: margins(12px, 10px, 12px, 10px); } reactionsTabIconSkip: 3px; -historyRequestsUserpics: GroupCallUserpics { - size: 22px; - shift: 8px; - stroke: 4px; - align: align(left); -} -historyRequestsHeight: 33px; SendAsButton { width: pixels; diff --git a/Telegram/SourceFiles/ui/chat/message_bar.cpp b/Telegram/SourceFiles/ui/chat/message_bar.cpp index 868d7a615..722e8322a 100644 --- a/Telegram/SourceFiles/ui/chat/message_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/message_bar.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/palette.h" namespace Ui { diff --git a/Telegram/SourceFiles/ui/chat/pinned_bar.cpp b/Telegram/SourceFiles/ui/chat/pinned_bar.cpp index fddd61fe4..dcd4133c0 100644 --- a/Telegram/SourceFiles/ui/chat/pinned_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/pinned_bar.cpp @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/shadow.h" #include "ui/widgets/buttons.h" #include "ui/wrap/fade_wrap.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/palette.h" #include diff --git a/Telegram/SourceFiles/ui/chat/requests_bar.cpp b/Telegram/SourceFiles/ui/chat/requests_bar.cpp index 9636e02ce..c8841f579 100644 --- a/Telegram/SourceFiles/ui/chat/requests_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/requests_bar.cpp @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_options.h" #include "ui/painter.h" #include "lang/lang_keys.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_calls.h" #include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget. #include "styles/style_window.h" // st::columnMinimalWidthLeft @@ -145,7 +145,7 @@ void RequestsBar::paint(Painter &p) { const auto userpicsLeft = userpicsTop * 2; const auto textTop = st::lineWidth + (st::historyRequestsHeight - st::lineWidth - - st::msgServiceNameFont->height) / 2; + - st::semiboldFont->height) / 2; const auto width = _inner->width(); const auto &font = st::defaultMessageBar.title.font; p.setPen(st::defaultMessageBar.titleFg); diff --git a/Telegram/SourceFiles/ui/controls/emoji_button.cpp b/Telegram/SourceFiles/ui/controls/emoji_button.cpp index 06775e2a0..b2656f584 100644 --- a/Telegram/SourceFiles/ui/controls/emoji_button.cpp +++ b/Telegram/SourceFiles/ui/controls/emoji_button.cpp @@ -10,29 +10,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/radial_animation.h" #include "ui/effects/ripple_animation.h" #include "ui/painter.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Ui { -EmojiButton::EmojiButton(QWidget *parent, const style::IconButton &st) -: RippleButton(parent, st.ripple) +EmojiButton::EmojiButton(QWidget *parent, const style::EmojiButton &st) +: RippleButton(parent, st.inner.ripple) , _st(st) { - resize(_st.width, _st.height); + resize(_st.inner.width, _st.inner.height); setCursor(style::cur_pointer); } void EmojiButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); - p.fillRect(e->rect(), st::historyComposeAreaBg); - paintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y(), _rippleOverride ? &(*_rippleOverride)->c : nullptr); + p.fillRect(e->rect(), _st.bg); + const auto &st = _st.inner; + paintRipple(p, st.rippleAreaPosition.x(), st.rippleAreaPosition.y(), _rippleOverride ? &(*_rippleOverride)->c : nullptr); const auto over = isOver(); const auto loadingState = _loading ? _loading->computeState() : RadialState{ 0., 0, RadialState::kFull }; - const auto icon = _iconOverride ? _iconOverride : &(over ? _st.iconOver : _st.icon); - auto position = _st.iconPosition; + const auto icon = _iconOverride ? _iconOverride : &(over ? st.iconOver : st.icon); + auto position = st.iconPosition; if (position.x() < 0) { position.setX((width() - icon->width()) / 2); } @@ -53,9 +54,7 @@ void EmojiButton::paintEvent(QPaintEvent *e) { const auto color = (_colorOverride ? *_colorOverride - : (over - ? st::historyEmojiCircleFgOver - : st::historyEmojiCircleFg)); + : (over ? _st.lineFgOver : _st.lineFg)); const auto line = style::ConvertScaleExact(st::historyEmojiCircleLine); if (anim::Disabled() && _loading && _loading->animating()) { anim::DrawStaticLoading(p, inner, line, color); @@ -112,14 +111,15 @@ void EmojiButton::onStateChanged(State was, StateChangeSource source) { } QPoint EmojiButton::prepareRippleStartPosition() const { - if (!_st.rippleAreaSize) { + if (!_st.inner.rippleAreaSize) { return DisabledRippleStartPosition(); } - return mapFromGlobal(QCursor::pos()) - _st.rippleAreaPosition; + return mapFromGlobal(QCursor::pos()) - _st.inner.rippleAreaPosition; } QImage EmojiButton::prepareRippleMask() const { - return RippleAnimation::EllipseMask(QSize(_st.rippleAreaSize, _st.rippleAreaSize)); + const auto size = _st.inner.rippleAreaSize; + return RippleAnimation::EllipseMask(QSize(size, size)); } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/emoji_button.h b/Telegram/SourceFiles/ui/controls/emoji_button.h index a39d40c9c..bee3b68f6 100644 --- a/Telegram/SourceFiles/ui/controls/emoji_button.h +++ b/Telegram/SourceFiles/ui/controls/emoji_button.h @@ -9,13 +9,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" +namespace style { +struct EmojiButton; +} // namespace style + namespace Ui { class InfiniteRadialAnimation; class EmojiButton final : public RippleButton { public: - EmojiButton(QWidget *parent, const style::IconButton &st); + EmojiButton(QWidget *parent, const style::EmojiButton &st); void setLoading(bool loading); void setColorOverrides( @@ -33,7 +37,7 @@ protected: private: void loadingAnimationCallback(); - const style::IconButton &_st; + const style::EmojiButton &_st; std::unique_ptr _loading; diff --git a/Telegram/SourceFiles/ui/controls/jump_down_button.cpp b/Telegram/SourceFiles/ui/controls/jump_down_button.cpp index 24c54d8d6..aded3fc78 100644 --- a/Telegram/SourceFiles/ui/controls/jump_down_button.cpp +++ b/Telegram/SourceFiles/ui/controls/jump_down_button.cpp @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/ripple_animation.h" #include "ui/unread_badge_paint.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Ui { diff --git a/Telegram/SourceFiles/ui/controls/send_button.cpp b/Telegram/SourceFiles/ui/controls/send_button.cpp index fe9f6b115..2e83de2fc 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.cpp +++ b/Telegram/SourceFiles/ui/controls/send_button.cpp @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/ripple_animation.h" #include "ui/painter.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Ui { namespace { @@ -18,9 +18,10 @@ constexpr int kWideScale = 5; } // namespace -SendButton::SendButton(QWidget *parent) -: RippleButton(parent, st::historyReplyCancel.ripple) { - resize(st::historySendSize); +SendButton::SendButton(QWidget *parent, const style::SendButton &st) +: RippleButton(parent, st.inner.ripple) +, _st(st) { + resize(_st.inner.width, _st.inner.height); } void SendButton::setType(Type type) { @@ -93,35 +94,17 @@ void SendButton::paintEvent(QPaintEvent *e) { } void SendButton::paintRecord(QPainter &p, bool over) { - const auto recordActive = 0.; if (!isDisabled()) { - auto rippleColor = anim::color( - st::historyAttachEmoji.ripple.color, - st::historyRecordVoiceRippleBgActive, - recordActive); paintRipple( p, - (width() - st::historyAttachEmoji.rippleAreaSize) / 2, - st::historyAttachEmoji.rippleAreaPosition.y(), - &rippleColor); + (width() - _st.inner.rippleAreaSize) / 2, + _st.inner.rippleAreaPosition.y()); } - auto fastIcon = [&] { - if (isDisabled()) { - return &st::historyRecordVoice; - } else if (recordActive == 1.) { - return &st::historyRecordVoiceActive; - } else if (over) { - return &st::historyRecordVoiceOver; - } - return &st::historyRecordVoice; - }; - fastIcon()->paintInCenter(p, rect()); - if (!isDisabled() && recordActive > 0. && recordActive < 1.) { - p.setOpacity(recordActive); - st::historyRecordVoiceActive.paintInCenter(p, rect()); - p.setOpacity(1.); - } + const auto &icon = (isDisabled() || !over) + ? _st.record + : _st.recordOver; + icon.paintInCenter(p, rect()); } void SendButton::paintSave(QPainter &p, bool over) { @@ -132,7 +115,10 @@ void SendButton::paintSave(QPainter &p, bool over) { } void SendButton::paintCancel(QPainter &p, bool over) { - paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y()); + paintRipple( + p, + (width() - _st.inner.rippleAreaSize) / 2, + _st.inner.rippleAreaPosition.y()); const auto &cancelIcon = over ? st::historyReplyCancelIconOver @@ -141,9 +127,7 @@ void SendButton::paintCancel(QPainter &p, bool over) { } void SendButton::paintSend(QPainter &p, bool over) { - const auto &sendIcon = over - ? st::historySendIconOver - : st::historySendIcon; + const auto &sendIcon = over ? _st.inner.iconOver : _st.inner.icon; if (isDisabled()) { const auto color = st::historyRecordVoiceFg->c; sendIcon.paint(p, st::historySendIconPosition, width(), color); @@ -199,20 +183,14 @@ QPixmap SendButton::grabContent() { } QImage SendButton::prepareRippleMask() const { - auto size = (_type == Type::Record) - ? st::historyAttachEmoji.rippleAreaSize - : st::historyReplyCancel.rippleAreaSize; + const auto size = _st.inner.rippleAreaSize; return RippleAnimation::EllipseMask(QSize(size, size)); } QPoint SendButton::prepareRippleStartPosition() const { - auto real = mapFromGlobal(QCursor::pos()); - auto size = (_type == Type::Record) - ? st::historyAttachEmoji.rippleAreaSize - : st::historyReplyCancel.rippleAreaSize; - auto y = (_type == Type::Record) - ? st::historyAttachEmoji.rippleAreaPosition.y() - : (height() - st::historyReplyCancel.rippleAreaSize) / 2; + const auto real = mapFromGlobal(QCursor::pos()); + const auto size = _st.inner.rippleAreaSize; + const auto y = (height() - _st.inner.rippleAreaSize) / 2; return real - QPoint((width() - size) / 2, y); } diff --git a/Telegram/SourceFiles/ui/controls/send_button.h b/Telegram/SourceFiles/ui/controls/send_button.h index 02ac3c093..5b3cc104d 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.h +++ b/Telegram/SourceFiles/ui/controls/send_button.h @@ -9,11 +9,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" +namespace style { +struct SendButton; +} // namespace style + namespace Ui { class SendButton final : public RippleButton { public: - explicit SendButton(QWidget *parent); + SendButton(QWidget *parent, const style::SendButton &st); static constexpr auto kSlowmodeDelayLimit = 100 * 60; @@ -49,6 +53,8 @@ private: void paintSchedule(QPainter &p, bool over); void paintSlowmode(QPainter &p); + const style::SendButton &_st; + Type _type = Type::Send; Type _afterSlowmodeType = Type::Send; QPixmap _contentFrom, _contentTo; diff --git a/Telegram/SourceFiles/ui/controls/tabbed_search.cpp b/Telegram/SourceFiles/ui/controls/tabbed_search.cpp index cb28ea643..a4bf604a5 100644 --- a/Telegram/SourceFiles/ui/controls/tabbed_search.cpp +++ b/Telegram/SourceFiles/ui/controls/tabbed_search.cpp @@ -181,7 +181,7 @@ void GroupsStrip::paintEvent(QPaintEvent *e) { const auto size = SearchWithGroups::IconSizeOverride(); if (_chosen == index) { p.setPen(Qt::NoPen); - p.setBrush(st::windowBgRipple); + p.setBrush(_st.bgActive); p.drawEllipse( left + skip, top + (height - single) / 2 + skip, diff --git a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp index f30c88d8f..8db74f0f2 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_window.h" #include "styles/style_media_view.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_info.h" @@ -561,16 +562,17 @@ void Generator::paintComposeArea() { auto right = st::historySendRight + st::historySendSize.width(); st::historyRecordVoice[_palette].paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height())); - const auto emojiIconLeft = (st::historyAttachEmoji.iconPosition.x() < 0) - ? ((st::historyAttachEmoji.width - st::historyAttachEmoji.icon.width()) / 2) - : st::historyAttachEmoji.iconPosition.x(); - const auto emojiIconTop = (st::historyAttachEmoji.iconPosition.y() < 0) - ? ((st::historyAttachEmoji.height - st::historyAttachEmoji.icon.height()) / 2) - : st::historyAttachEmoji.iconPosition.y(); - const auto &emojiIcon = st::historyAttachEmoji.icon[_palette]; - right += st::historyAttachEmoji.width; + const auto &emojiButton = st::historyAttachEmoji.inner; + const auto emojiIconLeft = (emojiButton.iconPosition.x() < 0) + ? ((emojiButton.width - emojiButton.icon.width()) / 2) + : emojiButton.iconPosition.x(); + const auto emojiIconTop = (emojiButton.iconPosition.y() < 0) + ? ((emojiButton.height - emojiButton.icon.height()) / 2) + : emojiButton.iconPosition.y(); + const auto &emojiIcon = emojiButton.icon[_palette]; + right += emojiButton.width; auto attachEmojiLeft = _composeArea.x() + _composeArea.width() - right; - _p->fillRect(attachEmojiLeft, controlsTop, st::historyAttachEmoji.width, st::historyAttachEmoji.height, st::historyComposeAreaBg[_palette]); + _p->fillRect(attachEmojiLeft, controlsTop, emojiButton.width, emojiButton.height, st::historyComposeAreaBg[_palette]); emojiIcon.paint(*_p, attachEmojiLeft + emojiIconLeft, controlsTop + emojiIconTop, _rect.width()); auto pen = st::historyEmojiCircleFg[_palette]->p; @@ -591,7 +593,7 @@ void Generator::paintComposeArea() { auto fieldLeft = _composeArea.x() + st::historyAttach.width; auto fieldTop = _composeArea.y() + _composeArea.height() - st::historyAttach.height + st::historySendPadding; - auto fieldWidth = _composeArea.width() - st::historyAttach.width - st::historySendSize.width() - st::historySendRight - st::historyAttachEmoji.width; + auto fieldWidth = _composeArea.width() - st::historyAttach.width - st::historySendSize.width() - st::historySendRight - emojiButton.width; auto fieldHeight = st::historySendSize.height() - 2 * st::historySendPadding; auto field = QRect(fieldLeft, fieldTop, fieldWidth, fieldHeight); _p->fillRect(field, st::historyComposeField.textBg[_palette]); diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 3f079cacb..b2acd7367 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ using "ui/basic.style"; using "ui/widgets/widgets.style"; -using "ui/chat/chat.style"; +using "chat_helpers/chat_helpers.style"; using "boxes/boxes.style"; // UserpicButton windowMinWidth: 380px; From a02876562a0d96f4d36f2960a4e7cb87f5071ef1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 17 May 2023 15:51:04 +0400 Subject: [PATCH 022/260] Finish improved stories reply area theming. --- Telegram/CMakeLists.txt | 1 + .../Resources/icons/emoji/stickers_add.png | Bin 470 -> 0 bytes .../Resources/icons/emoji/stickers_add@2x.png | Bin 899 -> 0 bytes .../Resources/icons/emoji/stickers_add@3x.png | Bin 1345 -> 0 bytes .../icons/emoji/stickers_add_dot.png | Bin 255 -> 0 bytes .../icons/emoji/stickers_add_dot@2x.png | Bin 312 -> 0 bytes .../icons/emoji/stickers_add_dot@3x.png | Bin 494 -> 0 bytes .../icons/emoji/stickers_add_unread.png | Bin 527 -> 0 bytes .../icons/emoji/stickers_add_unread@2x.png | Bin 1007 -> 0 bytes .../icons/emoji/stickers_add_unread@3x.png | Bin 1430 -> 0 bytes .../SourceFiles/boxes/sticker_set_box.cpp | 1 + Telegram/SourceFiles/boxes/translate_box.cpp | 4 +- .../chat_helpers/chat_helpers.style | 198 ++++++++++++------ .../chat_helpers/compose/compose_features.h | 27 +++ .../chat_helpers/emoji_list_widget.cpp | 47 +++-- .../chat_helpers/emoji_list_widget.h | 3 + .../chat_helpers/emoji_suggestions_widget.cpp | 149 +++++++++++-- .../chat_helpers/emoji_suggestions_widget.h | 119 +---------- .../chat_helpers/field_autocomplete.cpp | 7 +- .../chat_helpers/gifs_list_widget.cpp | 18 +- .../chat_helpers/gifs_list_widget.h | 7 +- .../chat_helpers/message_field.cpp | 12 +- .../SourceFiles/chat_helpers/message_field.h | 4 +- .../chat_helpers/stickers_list_footer.cpp | 63 +++--- .../chat_helpers/stickers_list_footer.h | 8 +- .../chat_helpers/stickers_list_widget.cpp | 87 ++++---- .../chat_helpers/stickers_list_widget.h | 7 +- .../SourceFiles/chat_helpers/tabbed_panel.cpp | 8 +- .../chat_helpers/tabbed_selector.cpp | 10 +- .../chat_helpers/tabbed_selector.h | 6 +- .../SourceFiles/history/history_widget.cpp | 2 +- .../history_view_compose_controls.cpp | 15 +- .../controls/history_view_compose_controls.h | 17 +- .../info_userpic_emoji_builder_widget.cpp | 2 +- .../media/stories/media_stories_reply.cpp | 35 +++- .../SourceFiles/media/view/media_view.style | 110 +++++++++- Telegram/SourceFiles/menu/menu_send.cpp | 13 +- Telegram/SourceFiles/menu/menu_send.h | 7 +- 38 files changed, 638 insertions(+), 349 deletions(-) delete mode 100644 Telegram/Resources/icons/emoji/stickers_add.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add@2x.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add@3x.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add_dot.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add_dot@2x.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add_dot@3x.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add_unread.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add_unread@2x.png delete mode 100644 Telegram/Resources/icons/emoji/stickers_add_unread@3x.png create mode 100644 Telegram/SourceFiles/chat_helpers/compose/compose_features.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5c559e8a5..3ed0b31c8 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -348,6 +348,7 @@ PRIVATE calls/calls_video_bubble.h calls/calls_video_incoming.cpp calls/calls_video_incoming.h + chat_helpers/compose/compose_features.h chat_helpers/compose/compose_show.cpp chat_helpers/compose/compose_show.h chat_helpers/bot_command.cpp diff --git a/Telegram/Resources/icons/emoji/stickers_add.png b/Telegram/Resources/icons/emoji/stickers_add.png deleted file mode 100644 index d4c77a202c4fe31fcf48f38f030c8f0df0d2be25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 470 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf%)!&eF~maf z?i9nURtJH$_@uss<-O_w0;?Mm-m>m#*}h@>3E8IQ7ra>39$~(*V(QkZM}I$R3$1bU zFmT;;)adj1?VC4mUbp@Bep!EYbz|SlpKW5VC+=EzdefS9uZ909Jo#OB+2pg0xa-G# zJUlTjPVC%rs;+^LEaolK>{1F1-Oiw=q{y%#_r(D=skVdL-ZtM*5@cfd&GqAg=4@X7 zO&v!W64KL`u!`)z{i;;bT9Btgy>8dE)Qjh0<2&~1CU0lsnUOv9*XAudUaneotZ>Sy zPDZvfLMQilcwgSKEqCeZ#wLXWrLogzZkjP!<>coWg9DF*h2KR@{7`SRUi53Nzo?O* zb@y3U-9(8~6AX9`Yn%@1%6841W!3ljMowoQuR(&`*|3bP0l+XkK$!@e| diff --git a/Telegram/Resources/icons/emoji/stickers_add@2x.png b/Telegram/Resources/icons/emoji/stickers_add@2x.png deleted file mode 100644 index f188f440d5c88257b522c9abcd0c829c02f5d77b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 899 zcmV-}1AP36P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NE?MXyIR9Fe^n7@i5K@i5(UDiaw zR1-B=A3+QS1<^pC!w2vg6!s1DDOB)JU}R=ys4T1pi{@%%a`(F{wpX@$#%X80n*%41 zw&ts^zNzZ2?wL^Ne%&*0&%kXn5cW?Wi^Wo@6g=DQ_INz{C$we=hr@+Jq1|pTm&^Tr z|MTqPFGg+w0A@{X>+tya*laf6-`~$PI1YgU#>Fa%#Y5EV^_Q2ITrMX84u`{dJYFmo zRBE@|VN52I)GnXTSF6={JT9>HdfjTZ27`f>tP9{sB+}`0R4V54IU%2(o}Apc1pt_G z!5tDOyq%pnI~)#$`t|jdJbCvAjEnd}0+n}PGm^(rYNpd^B9SoToUORvs5JS_ld4p= z+kJR=aE|MC2ghEoCz>F1gSy`%;ljcrUadw#;I8syRrF7VEZ5a)g&-!u6_xrV`WQhX z^9fqpUhtcP79#2K@<7pO)Me3qND8TxL#~XT1~ggeek|RK4oLa&=klUSdI98ieVoQG z>7kum6hx~Oui={o+J#ZWR;Db6>#4yy3{ z{Cr*=N;a8HKvpUh%A{PJN~UO4AmzfqAc(rMGO4kgl+V#JMia*_-HrmI{R>iP31OT- z=90a=y}`oS?+e$r{1sXWp%a<~jYcD&?#Du%%Zr6+F5O>3=q_eK{eB-PRV$TBx{n`A zxm=c>A|bp|1#WXixYhB4LU3Jt-0P1YPzUaSh9>nufjGiS{)2VMM^oTlgU}SL7ey%? zc~Z4wqrT~@^#A#>LGr{ey@397)6X#~mc@WRe!CA=7A13%%&h;%pY)uR-R<`b+%sUx Zz%Qq1$)w8hFSq~z002ovPDHLkV1nQ&jgbHV diff --git a/Telegram/Resources/icons/emoji/stickers_add@3x.png b/Telegram/Resources/icons/emoji/stickers_add@3x.png deleted file mode 100644 index 48de8115e323951613e07414864f0ccc3f964d3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1345 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|Wxo#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz1|Sip>6gA`6MbbHUhz_QxY#W5s< z_3bQMeUm_uept zOZ0rd^l?LL>+kR0%#A(&#P8ru>*s&Y|DR)79KSq(Bm9H|GTdtvaOOnd$`C18Sy@R* zNjW(=8JV2?{QT_f!`DB5{(SlJ<)=@d9zA;W=g*%vZ{ECpJK9aA=8&1W`T29_-o1T$ z_x}C;)-fv<_Hee@RaP8%G;__`wYGM4RxQGdn-rflR4fV64hasv{O8Y~w{LTK?ypzV znsDo}(~qp~pZpvF4A)MrEc?B3rDoiWqQ2|buS-iyKYsjpPx7<3Lh*l=OpMl85SGWS zuwhYr{=x_yJzZVb3|kwUHEY&f$lbBoQBs)4RK%9+!H$CC9~a-ebm@@y`jvb3?OV5g zeR)~gvf>7y?HN8X4>V*Gq*mX%7q=_(Y(jng{`Kqid9V3PJ2YH6VR7iL|Jt0oiB7Vj zrki){cyV2{;ec1gj?;g327cMa6dxD2Z29uPZmAau4?sF{%oiWvjPDP0&b^S;FLN#nZogFMZ6Abf4tQ@k4(1Oo0w&E4{bY z*^%ETU-F{Zf6HZmbwPd6s7BtH}2jQ?VmNtBust7q3K%< zyn9~tO#OE8;zUhXma8#yHL_(i9CdEqD4K2R>h9j&XklUDaqQHubpj2ON>fw#eO>_5 zM%NtyN8uHw2Ge#4F*EN!!7FIO=&LiW=V>P|4^PL(u#9I*#@)ubn*E`SR?I1g}fK z_oy%4a7JjAP|UHs3<;kO)z2HJByIYbzb{+URdfHmd2-njSA?vldTm_2x_ftddHM3? z%bx};6FGfbS=Y(A!&M}v{@t559+s}#*kcv#zV6|3a9D*S1N5n|H6~ z%t+cOr`!0_?(nU{$E-fwSOF5y(`hWcq?{%!EUaYia)HHUqe)7UHD_A>iyuF3ENM~Q zRoXU9tuoJS$246|P*9x<>XHCv749Qv&h(rp72A^St7+T&za;yB=7XBNX98Bn=W6oy z3r5?S`*7-PkG^x{*vtkyr4uI?vNRSx{VdUWps4zTd(KarRev>(J1Nd@f30RVZ?;`e z+TZEz=M{Z&_T+z2z2rY@t8Ti|>C>k}&rS_|#w=!e!O-FW)4aEhyEqqLia*96ws}*M zc%v=P=aUyNTnI?E6Pby>nF!|clyQ1RvI>gTe~DWM4fuGm(L diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot.png b/Telegram/Resources/icons/emoji/stickers_add_dot.png deleted file mode 100644 index 1bb4078f4ef84939c06077e904c5366a51d5576d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlfUWqP_ehFAzD zCrE5-m>DwvgcUiWqsHizux`05aBSM%e8JkPIRzXXJqZ2I*1v*I#_li%Op zSI$m%dM1!!U-Ls?9otM^ag}KYPn_WRB6vYpTrbAJ#6%_R_KIB>1tfSIFA6M3=U`xv WbkX?w=dQy|kma7PelF{r5}E)nyGHQ< diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png b/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png deleted file mode 100644 index 1723a7f362b4ee19c6fa27df5984115e19a03e46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@HgnjK|#PZP}w$E#P%i|8GSGb1PPOgx&J_{FpB;g6kqY)l*q0@@RTPFZfh zc1*(RRgC8wA?~YN%GQ3J**@uh)uiKh&Xg~$NsMj`{@QbV@gfF)*I1soU{S6gA`6MbbAj}7UAjQ7?Q#I z_L^Z)feo{VZjw2Y+mvc=N+{ zsSk#q_szF@{Pa*O0~3cr1CW@b`l0Na)A_Z@kzpdPjsg~a#yrlk*B@KhnYT7xO_bP{ z`+MWgX{jeSo!OSly!-05Da%$KxzSR^Gke)b8-MGGfu*0;d<&|5uHAKLMO*0E6XjnY z^VZm`Is2k&@2X|S%QgBQ`~KhUeAC0kc7Jx)xs?-b12#|CbY?-qy4P7vCm9!L8f!${ z|30hlX~g9zk|I8K^Gaj+XRllJYQwP|OqQ+X=Dq8sFPr?_mmhP#o%iJa);WjjQ@kfV zDG+kzesX`2&guI%pG590SY^F!&Y@2y@8)fCb;u?;og^V}p1P>_=jV5Av*b Yj0WeW+@f@u!$4u}>FVdQ&MBb@0G5=r5C8xG diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread.png b/Telegram/Resources/icons/emoji/stickers_add_unread.png deleted file mode 100644 index d8172e22b3195555a73c7c591972c1e2013eb6b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 527 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgftkToPF~maf zZpg;2W(R?@vG0V}m~ez?^0KUM?%0;8`a}3%>)xY(oj+)AndIQa#ko?nC(Kh?vE$Be zoi)c|g&*$n*?4A8t+BPS_3p(NZ=X6o`BX@#sc-P4CA-#b)`?z!efsI8{r^wijnJFk zemHUe{q#s%F7~=8OQXM3lag1U$mgFm96P5scTG55 zzgWTMS5ueO^P2lhnGd->R#!!Pc6 zIplijuCK19XS^q=Tyb&SmU}oxM=WBZSlIpVi#3$yJpO1g)2E|v>&Xr$=Bb->3|CCK zlQQMG<)#JF7Bgl1iV9`g^8{T3r@SrmKB%@>fy2Z%dA9G$Tvn3_hYfO?d+q-GGE`MC zpC_C9z~Rv~!|e*?cjV7Z_h$+G(B@EGkld@@FIMa5dV)33A-UF)#%i#b3 diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png b/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png deleted file mode 100644 index c48562529e6a2a6b2ec9a4de5c4217d7da2cb14c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1007 zcmVPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NFSxH1eR9Fe^m_17~K@i6^hNLl~ z=?wUiLb0;2kTilMg0JZ3$OrHCdput79Kq0r&sp>jpBSlry) zj6@ zeVs5V6R(gdShbNdVW1(@Xf#N8j!`0!aH^^?oNjM#)l>~&pp`2H83-&}u+g6-ruvZG z-`}&gyu9o-u$Ks|Y~e=J6S};-M70-fA;(;*zieSj=PbYykA!%w)m-HHb(l-|$8Spvk`NbZlM}iH@8b+yfh+ z+U;b%=J!>ajHCKVhO0;!`bN_tc@gFBw)xnA6}Kk$6gLBGr1~M`cH7_IKR-YBg0+&9 zt|p&j#4A)klmlkCFPaotuMbvyzv3$v-?C8fU5lF}e1y!4G)GlB3i=yuN8x2SkPkdC d@IYTY@E<*jYW#(_g^K_H002ovPDHLkV1fgd%1HnK diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png b/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png deleted file mode 100644 index 46a03b994c2f7388e781560a87777fe89a82ce12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1430 zcmV;H1!?+;P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS=07*naRA>e5n!7JOK@`W`OGF|f zo&||#LP?@j2o;405nBHNy(q+^Qz(CbfPzFsB^nZiLLwm~qVZ}Z;!UB!?-LW}PWIlr zb9c?|`u(!E;Oy+oIp1^U%$YN1HZL#!#TJMy5L+O&Kx~270%5qg`T6(Z*MPaEFF4nZ7r|H#>Q%DYV6{% zz%7wTOixdXCW?4?c$l4?C2Uv_5DAOo;$k9%uGQPy+rq*^d3kvxc0%d__S}ie%E~`q zKZx8%1>2L8h~(^KO#X5Ulv1XF@C~=KvvX-_X?%Q~UB@?*)IiQxGF>w>GpQF|sLjny z#RS63xKv;4%OO=U85$Z&y~si(v$Ut0M@L8Hf!$f?kO{)FoD%x``;F2KigCj4?(SAx zIM0(z894^jfr0|ZImNuEr^hJ8pcn^`va&M8Wqp0!$S%+h+uPfUe_dT&b91x1EVE)V zeq&=p=tzjItu2w;)zu|*M#7*N=U*8{<{@+>#NOVX$TjYz2E`g18-*A3385n)ZfYBpC?E_8Qyx4*xiZV}~*>Ftgdfg!*g9ddnKSy>T}l8ryt!CJqc zr&KTlfPev*#y$gSa78`Di?uSa38hH~paaV&+@P2qGxX4(kyp@-IshG5M!5#XxaN^z zeSLjGM?&ZTbYOnK5QXGIi1qD<#n%`sYYYHtjR8r*V>U>J8KW8{sV7-XghhEoOBw|v zfMrujwHp+p)AI7Nuoxa57CK}?7p+6`592qK2B=3+l~8_2odQVy_0dO0Ud*9_Bs&;j zbZkGOqbmIo-;1A=;)U4ql;M{4rO$R@5`?RDT`%=#L>>)v~U{ z1DP?3nF!S}1VsmTxlUx)W1!LsaY1R4&e!_q5abr@!m?kfrG*EEG*`NYI6Vtm_ZiTT z_;7LIDOaX5-@M3$j#3G>Fb{lP>H0Wm<0nTgEiI0k0*^HE$r0Zq$w@@%qyDmA1sK^K z3b;J7Y3BHm7{^}y3o%_8MUyeXDv({+MFIC$q;UQ|>r*P>iVU}$2f8En^p*pKrw=hX z3lkw2V(ldI4L9*kSpLt^ov6T%YYmDpOodu$xFQ#m=jZ3RB^wk!;)G?{F}v6Tu?1oa k#1@Dx5L+O&K*$#O3zAGWa{bB&;{X5v07*qoM6N<$f?oQH@c;k- diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 0f661181a..82e4e3deb 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -606,6 +606,7 @@ void StickerSetBox::updateButtons() { const auto session = &_show->session(); auto box = ChatHelpers::MakeConfirmRemoveSetBox( session, + st::boxLabel, _inner->setId()); if (box) { _show->showBox(std::move(box)); diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index 662cf51e5..066ba855b 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -63,7 +63,7 @@ ShowButton::ShowButton(not_null parent) _button.sizeValue( ) | rpl::start_with_next([=](const QSize &s) { resize( - s.width() + st::emojiSuggestionsFadeRight.width(), + s.width() + st::defaultEmojiSuggestions.fadeRight.width(), s.height()); _button.moveToRight(0, 0); }, lifetime()); @@ -74,7 +74,7 @@ void ShowButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto clip = e->rect(); - const auto &icon = st::emojiSuggestionsFadeRight; + const auto &icon = st::defaultEmojiSuggestions.fadeRight; const auto fade = QRect(0, 0, icon.width(), height()); if (fade.intersects(clip)) { icon.fill(p, fade); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index ceec7e3ec..ba786876f 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -10,6 +10,7 @@ using "ui/basic.style"; using "boxes/boxes.style"; using "ui/layers/layers.style"; using "ui/widgets/widgets.style"; +using "ui/menu_icons.style"; GroupCallUserpics { size: pixels; @@ -36,9 +37,50 @@ TabbedSearch { height: pixels; } +ComposeIcons { + settings: icon; + + recent: icon; + recentActive: icon; + people: icon; + peopleActive: icon; + nature: icon; + natureActive: icon; + food: icon; + foodActive: icon; + activity: icon; + activityActive: icon; + travel: icon; + travelActive: icon; + objects: icon; + objectsActive: icon; + symbols: icon; + symbolsActive: icon; + + menuFave: icon; + menuUnfave: icon; + menuStickerSet: icon; + menuRecentRemove: icon; + menuGifAdd: icon; + menuGifRemove: icon; + menuMute: icon; + menuSchedule: icon; + menuWhenOnline: icon; +} + +EmojiSuggestions { + dropdown: InnerDropdown; + bg: color; + overBg: color; + textFg: color; + fadeLeft: icon; + fadeRight: icon; +} + EmojiPan { margin: margins; padding: margins; + showAnimation: PanelAnimation; desiredSize: pixels; verticalSizeSub: pixels; header: pixels; @@ -51,8 +93,12 @@ EmojiPan { iconWidth: pixels; iconArea: pixels; bg: color; + headerFg: color; + trendingHeaderFg: color; + trendingSubheaderFg: color; + trendingUnreadFg: color; + trendingInstalled: icon; overBg: color; - expandBg: color; pathBg: color; pathFg: color; textFg: color; @@ -60,9 +106,14 @@ EmojiPan { categoriesBgOver: color; fadeLeft: icon; fadeRight: icon; + menu: PopupMenu; tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; + removeSet: IconButton; + boxLabel: FlatLabel; + icons: ComposeIcons; + autocompleteBottomSkip: pixels; } MessageBar { @@ -96,6 +147,7 @@ ComposeControls { send: SendButton; attach: IconButton; emoji: EmojiButton; + suggestions: EmojiSuggestions; tabbed: EmojiPan; } @@ -191,12 +243,6 @@ stickersScroll: ScrollArea(boxScroll) { stickersRowDisabledOpacity: 0.4; stickersRowDuration: 200; -stickersSettings: icon {{ "emoji/emoji_settings", emojiIconFg }}; -stickersTrending: icon {{ "emoji/stickers_add", emojiIconFg }}; -stickersTrendingUnread: icon { - { "emoji/stickers_add_unread", emojiIconFg }, - { "emoji/stickers_add_dot", dialogsUnreadBg } -}; emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }}; filtersRemove: IconButton(stickersRemove) { @@ -210,22 +256,6 @@ emojiTabs: SettingsSlider(defaultTabsSlider) { barTop: 40px; labelTop: 12px; } -emojiRecent: icon {{ "emoji/emoji_recent", emojiIconFg }}; -emojiRecentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }}; -emojiPeople: icon {{ "emoji/emoji_smile", emojiIconFg }}; -emojiPeopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }}; -emojiNature: icon {{ "emoji/emoji_nature", emojiIconFg }}; -emojiNatureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }}; -emojiFood: icon {{ "emoji/emoji_food", emojiIconFg }}; -emojiFoodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }}; -emojiActivity: icon {{ "emoji/emoji_activities", emojiIconFg }}; -emojiActivityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }}; -emojiTravel: icon {{ "emoji/emoji_travel", emojiIconFg }}; -emojiTravelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }}; -emojiObjects: icon {{ "emoji/emoji_objects", emojiIconFg }}; -emojiObjectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }}; -emojiSymbols: icon {{ "emoji/emoji_love", emojiIconFg }}; -emojiSymbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }}; emojiCategoryIconTop: 6px; emojiPanAnimation: PanelAnimation(defaultPanelAnimation) { @@ -297,40 +327,6 @@ defaultTabbedSearch: TabbedSearch { groupSkip: 2px; height: 33px; } -defaultEmojiPan: EmojiPan { - margin: margins(7px, 0px, 7px, 0px); - padding: margins(7px, 0px, 4px, 7px); - desiredSize: 37px; - verticalSizeSub: 1px; - header: 33px; - headerLeft: 14px; - headerLockLeft: 7px; - headerLockedLeft: 26px; - headerTop: 10px; - footer: 36px; - iconSkip: 3px; - iconWidth: 30px; - iconArea: 28px; - bg: emojiPanBg; - overBg: emojiPanHover; - expandBg: emojiPanHeaderFg; - pathBg: windowBgRipple; - pathFg: windowBgOver; - textFg: windowFg; - categoriesBg: emojiPanCategories; - categoriesBgOver: windowBgRipple; - fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; - fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; - tabs: emojiTabs; - search: defaultTabbedSearch; - searchMargin: margins(1px, 11px, 2px, 5px); -} -statusEmojiPan: EmojiPan(defaultEmojiPan) { - categoriesBg: windowBg; - categoriesBgOver: windowBgOver; - fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }}; - fadeRight: icon {{ "fade_horizontal", windowBg }}; -} inlineResultsMinHeight: 278px; inlineResultsMaxHeight: 640px; @@ -394,6 +390,81 @@ stickersToast: Toast(defaultToast) { stickersEmpty: icon {{ "stickers_empty", windowSubTextFg }}; emojiEmpty: icon {{ "emoji_empty", windowSubTextFg }}; +defaultComposeIcons: ComposeIcons { + settings: icon {{ "emoji/emoji_settings", emojiIconFg }}; + + recent: icon {{ "emoji/emoji_recent", emojiIconFg }}; + recentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }}; + people: icon {{ "emoji/emoji_smile", emojiIconFg }}; + peopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }}; + nature: icon {{ "emoji/emoji_nature", emojiIconFg }}; + natureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }}; + food: icon {{ "emoji/emoji_food", emojiIconFg }}; + foodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }}; + activity: icon {{ "emoji/emoji_activities", emojiIconFg }}; + activityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }}; + travel: icon {{ "emoji/emoji_travel", emojiIconFg }}; + travelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }}; + objects: icon {{ "emoji/emoji_objects", emojiIconFg }}; + objectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }}; + symbols: icon {{ "emoji/emoji_love", emojiIconFg }}; + symbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }}; + + menuFave: menuIconFave; + menuUnfave: menuIconUnfave; + menuStickerSet: menuIconStickers; + menuRecentRemove: menuIconDelete; + menuGifAdd: menuIconGif; + menuGifRemove: menuIconDelete; + menuMute: menuIconMute; + menuSchedule: menuIconSchedule; + menuWhenOnline: menuIconWhenOnline; +} +defaultEmojiPan: EmojiPan { + margin: margins(7px, 0px, 7px, 0px); + padding: margins(7px, 0px, 4px, 7px); + showAnimation: emojiPanAnimation; + desiredSize: 37px; + verticalSizeSub: 1px; + header: 33px; + headerLeft: 14px; + headerLockLeft: 7px; + headerLockedLeft: 26px; + headerTop: 10px; + footer: 36px; + iconSkip: 3px; + iconWidth: 30px; + iconArea: 28px; + bg: emojiPanBg; + headerFg: emojiPanHeaderFg; + trendingHeaderFg: stickersTrendingHeaderFg; + trendingSubheaderFg: stickersTrendingSubheaderFg; + trendingUnreadFg: stickersFeaturedUnreadBg; + trendingInstalled: stickersFeaturedInstalled; + overBg: emojiPanHover; + pathBg: windowBgRipple; + pathFg: windowBgOver; + textFg: windowFg; + categoriesBg: emojiPanCategories; + categoriesBgOver: windowBgRipple; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; + fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; + menu: popupMenuWithIcons; + tabs: emojiTabs; + search: defaultTabbedSearch; + searchMargin: margins(1px, 11px, 2px, 5px); + removeSet: stickerPanRemoveSet; + boxLabel: boxLabel; + icons: defaultComposeIcons; + autocompleteBottomSkip: 0px; +} +statusEmojiPan: EmojiPan(defaultEmojiPan) { + categoriesBg: windowBg; + categoriesBgOver: windowBgOver; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }}; + fadeRight: icon {{ "fade_horizontal", windowBg }}; +} + inlineBotsScroll: ScrollArea(defaultSolidScroll) { deltat: stickerPanPadding; deltab: stickerPanPadding; @@ -410,6 +481,15 @@ emojiSuggestionsScrolledWidth: 240px; emojiSuggestionsPadding: margins(emojiColorsPadding, 0px, emojiColorsPadding, 0px); emojiSuggestionsFadeAfter: 20px; +defaultEmojiSuggestions: EmojiSuggestions { + dropdown: emojiSuggestionsDropdown; + bg: menuBg; + overBg: emojiPanHover; + textFg: windowFg; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", boxBg }}; + fadeRight: icon {{ "fade_horizontal", boxBg }}; +} + mentionHeight: 40px; mentionPadding: margins(8px, 5px, 8px, 5px); mentionTop: 11px; @@ -479,9 +559,6 @@ reactPanelScroll: ScrollArea(emojiScroll) { deltab: 7px; } -emojiSuggestionsFadeLeft: icon {{ "fade_horizontal-flip_horizontal", boxBg }}; -emojiSuggestionsFadeRight: icon {{ "fade_horizontal", boxBg }}; - choosePeerGroupIcon: icon {{ "info/edit/create_group", lightButtonFg }}; choosePeerChannelIcon: icon {{ "info/edit/create_channel", lightButtonFg }}; choosePeerCreateIconLeft: 25px; @@ -857,5 +934,6 @@ defaultComposeControls: ComposeControls { send: historySend; attach: historyAttach; emoji: historyAttachEmoji; + suggestions: defaultEmojiSuggestions; tabbed: defaultEmojiPan; } diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h new file mode 100644 index 000000000..2f9915679 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h @@ -0,0 +1,27 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace ChatHelpers { + +struct ComposeFeatures { + bool sendAs = true; + bool ttlInfo = true; + bool botCommandSend = true; + bool silentBroadcastToggle = true; + bool attachBotsMenu = true; + bool inlineBots = true; + bool megagroupSet = true; + bool stickersSettings = true; + bool openStickerSets = true; + bool autocompleteHashtags = true; + bool autocompleteMentions = true; + bool autocompleteCommands = true; +}; + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index a6ff9af75..3d5f35c57 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -99,6 +99,7 @@ private: QSize _singleSize; QPoint _areaPosition; QPoint _innerPosition; + Ui::RoundRect _backgroundRect; Ui::RoundRect _overBg; bool _hiding = false; @@ -125,6 +126,7 @@ EmojiColorPicker::EmojiColorPicker( const style::EmojiPan &st) : RpWidget(parent) , _st(st) +, _backgroundRect(st::emojiPanRadius, _st.bg) , _overBg(st::emojiPanRadius, _st.overBg) { setMouseTracking(true); } @@ -181,8 +183,8 @@ void EmojiColorPicker::paintEvent(QPaintEvent *e) { p.drawPixmap(0, 0, _cache); return; } - Ui::Shadow::paint(p, inner, width(), st::emojiPanAnimation.shadow); - Ui::FillRoundRect(p, inner, st::boxBg, Ui::BoxCorners); + Ui::Shadow::paint(p, inner, width(), _st.showAnimation.shadow); + _backgroundRect.paint(p, inner); auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width(); if (rtl()) x = width() - x - st::emojiColorsSep; @@ -393,6 +395,7 @@ EmojiListWidget::EmojiListWidget( descriptor.show, std::move(descriptor.paused)) , _show(std::move(descriptor.show)) +, _features(descriptor.features) , _mode(descriptor.mode) , _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1) , _premiumIcon(_mode == Mode::EmojiStatus @@ -402,7 +405,7 @@ EmojiListWidget::EmojiListWidget( std::make_unique(&session())) , _customRecentFactory(std::move(descriptor.customRecentFactory)) , _overBg(st::emojiPanRadius, st().overBg) -, _collapsedBg(st::emojiPanExpand.height / 2, st::emojiPanHeaderFg) +, _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg) , _picker(this, st()) , _showPickerTimer([=] { showPicker(); }) { setMouseTracking(true); @@ -1072,7 +1075,7 @@ void EmojiListWidget::paint( - paintButtonGetWidth(p, info, buttonSelected, clip); if (info.section > 0 && clip.top() < info.rowsTop) { p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); auto titleText = (info.section < _staticCount) ? ChatHelpers::EmojiCategoryTitle(info.section)(tr::now) : _custom[info.section - _staticCount].title; @@ -1091,7 +1094,7 @@ void EmojiListWidget::paint( } const auto textBaseline = top + st::emojiPanHeaderFont->ascent; p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); p.drawText(titleLeft, textBaseline, titleText); } if (clip.top() + clip.height() > info.rowsTop) { @@ -1459,7 +1462,8 @@ void EmojiListWidget::displaySet(uint64 setId) { } void EmojiListWidget::removeSet(uint64 setId) { - if (auto box = MakeConfirmRemoveSetBox(&session(), setId)) { + const auto &labelSt = st().boxLabel; + if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) { checkHideWithBox(std::move(box)); } } @@ -1532,9 +1536,10 @@ QRect EmojiListWidget::removeButtonRect(const SectionInfo &info) const { if (_mode != Mode::Full) { return QRect(); } - const auto buttonw = st::stickerPanRemoveSet.rippleAreaPosition.x() - + st::stickerPanRemoveSet.rippleAreaSize; - const auto buttonh = st::stickerPanRemoveSet.height; + const auto &removeSt = st().removeSet; + const auto buttonw = removeSt.rippleAreaPosition.x() + + removeSt.rippleAreaSize; + const auto buttonh = removeSt.height; const auto buttonx = emojiRight() - st::emojiPanRemoveSkip - buttonw; const auto buttony = info.top + st::emojiPanRemoveTop; return QRect(buttonx, buttony, buttonw, buttonh); @@ -1966,19 +1971,18 @@ int EmojiListWidget::paintButtonGetWidth( if (remove.isEmpty()) { return 0; } else if (remove.intersects(clip)) { + const auto &removeSt = st().removeSet; if (custom.ripple) { custom.ripple->paint( p, - remove.x() + st::stickerPanRemoveSet.rippleAreaPosition.x(), - remove.y() + st::stickerPanRemoveSet.rippleAreaPosition.y(), + remove.x() + removeSt.rippleAreaPosition.x(), + remove.y() + removeSt.rippleAreaPosition.y(), width()); if (custom.ripple->empty()) { custom.ripple.reset(); } } - const auto &icon = selected - ? st::stickerPanRemoveSet.iconOver - : st::stickerPanRemoveSet.icon; + const auto &icon = selected ? removeSt.iconOver : removeSt.icon; icon.paint( p, (remove.topLeft() @@ -2045,7 +2049,9 @@ void EmojiListWidget::updateSelected() { if (hasButton(section) && myrtlrect(buttonRect(section)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; - } else if (section >= _staticCount && _mode == Mode::Full) { + } else if (_features.openStickerSets + && section >= _staticCount + && _mode == Mode::Full) { newSelected = OverSet{ section }; } } else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom) { @@ -2159,13 +2165,12 @@ std::unique_ptr EmojiListWidget::createButtonRipple( && section < _staticCount + _custom.size()); const auto remove = hasRemoveButton(section); - const auto &st = remove - ? st::stickerPanRemoveSet.ripple - : st::emojiPanButton.ripple; + const auto &removeSt = st().removeSet; + const auto &st = remove ? removeSt.ripple : st::emojiPanButton.ripple; auto mask = remove ? Ui::RippleAnimation::EllipseMask(QSize( - st::stickerPanRemoveSet.rippleAreaSize, - st::stickerPanRemoveSet.rippleAreaSize)) + removeSt.rippleAreaSize, + removeSt.rippleAreaSize)) : rightButton(section).rippleMask; return std::make_unique( st, @@ -2179,7 +2184,7 @@ QPoint EmojiListWidget::buttonRippleTopLeft(int section) const { return myrtlrect(buttonRect(section)).topLeft() + (hasRemoveButton(section) - ? st::stickerPanRemoveSet.rippleAreaPosition + ? st().removeSet.rippleAreaPosition : QPoint()); } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index fd307eda8..cc97d64db 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" #include "ui/widgets/tooltip.h" #include "ui/round_rect.h" @@ -84,6 +85,7 @@ struct EmojiListDescriptor { DocumentId, Fn)> customRecentFactory; const style::EmojiPan *st = nullptr; + ComposeFeatures features; }; class EmojiListWidget final @@ -345,6 +347,7 @@ private: void applyNextSearchQuery(); const std::shared_ptr _show; + const ComposeFeatures _features; Mode _mode = Mode::Full; std::unique_ptr _search; const int _staticCount = 0; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index b74e37708..ccd235355 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/emoji_config.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" +#include "ui/round_rect.h" #include "platform/platform_specific.h" #include "core/application.h" #include "base/event_filter.h" @@ -41,15 +42,133 @@ constexpr auto kAnimationDuration = crl::time(120); } // namespace +class SuggestionsWidget final : public Ui::RpWidget { +public: + SuggestionsWidget( + QWidget *parent, + const style::EmojiSuggestions &st, + not_null session, + bool suggestCustomEmoji, + Fn)> allowCustomWithoutPremium); + ~SuggestionsWidget(); + + void showWithQuery(SuggestionsQuery query, bool force = false); + void selectFirstResult(); + bool handleKeyEvent(int key); + + [[nodiscard]] rpl::producer toggleAnimated() const; + + struct Chosen { + QString emoji; + QString customData; + }; + [[nodiscard]] rpl::producer triggered() const; + +private: + struct Row { + Row(not_null emoji, const QString &replacement); + + Ui::Text::CustomEmoji *custom = nullptr; + DocumentData *document = nullptr; + not_null emoji; + QString replacement; + }; + struct Custom { + not_null document; + not_null emoji; + QString replacement; + }; + + bool eventHook(QEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void enterEventHook(QEnterEvent *e) override; + void leaveEventHook(QEvent *e) override; + + void scrollByWheelEvent(not_null e); + void paintFadings(QPainter &p) const; + + [[nodiscard]] std::vector getRowsByQuery(const QString &text) const; + [[nodiscard]] base::flat_multi_map lookupCustom( + const std::vector &rows) const; + [[nodiscard]] std::vector appendCustom( + std::vector rows); + [[nodiscard]] std::vector appendCustom( + std::vector rows, + const base::flat_multi_map &custom); + void resizeToRows(); + void setSelected( + int selected, + anim::type animated = anim::type::instant); + void setPressed(int pressed); + void clearMouseSelection(); + void clearSelection(); + void updateSelectedItem(); + void updateItem(int index); + [[nodiscard]] QRect inner() const; + [[nodiscard]] QPoint innerShift() const; + [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const; + void selectByMouse(QPoint globalPosition); + bool triggerSelectedRow() const; + void triggerRow(const Row &row) const; + + [[nodiscard]] int scrollCurrent() const; + void scrollTo(int value, anim::type animated = anim::type::instant); + void stopAnimations(); + + [[nodiscard]] not_null resolveCustomEmoji( + not_null document); + void customEmojiRepaint(); + + const style::EmojiSuggestions &_st; + const not_null _session; + SuggestionsQuery _query; + std::vector _rows; + bool _suggestCustomEmoji = false; + Fn)> _allowCustomWithoutPremium; + + Ui::RoundRect _overRect; + + base::flat_map< + not_null, + std::unique_ptr> _customEmoji; + bool _repaintScheduled = false; + + std::optional _lastMousePosition; + bool _mouseSelection = false; + int _selected = -1; + int _pressed = -1; + + int _scrollValue = 0; + Ui::Animations::Simple _scrollAnimation; + Ui::Animations::Simple _selectedAnimation; + int _scrollMax = 0; + int _oneWidth = 0; + QMargins _padding; + + QPoint _mousePressPosition; + int _dragScrollStart = -1; + + rpl::event_stream _toggleAnimated; + rpl::event_stream _triggered; + +}; + SuggestionsWidget::SuggestionsWidget( QWidget *parent, + const style::EmojiSuggestions &st, not_null session, bool suggestCustomEmoji, Fn)> allowCustomWithoutPremium) : RpWidget(parent) +, _st(st) , _session(session) , _suggestCustomEmoji(suggestCustomEmoji) , _allowCustomWithoutPremium(std::move(allowCustomWithoutPremium)) +, _overRect(st::roundRadiusSmall, _st.overBg) , _oneWidth(st::emojiSuggestionSize) , _padding(st::emojiSuggestionsPadding) { resize( @@ -284,7 +403,7 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) { _repaintScheduled = false; const auto clip = e->rect(); - p.fillRect(clip, st::boxBg); + p.fillRect(clip, _st.bg); const auto shift = innerShift(); p.translate(-shift); @@ -298,15 +417,13 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) { ? _pressed : _selectedAnimation.value(_selected); if (selected > -1.) { - Ui::FillRoundRect( + _overRect.paint( p, - QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth), - st::emojiPanHover, - Ui::StickerHoverCorners); + QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth)); } auto context = Ui::CustomEmoji::Context{ - .textColor = st::windowFg->c, + .textColor = _st.textFg->c, .now = crl::now(), }; for (auto i = from; i != till; ++i) { @@ -338,9 +455,9 @@ void SuggestionsWidget::paintFadings(QPainter &p) const { const auto rect = myrtlrect( shift.x(), 0, - st::emojiSuggestionsFadeLeft.width(), + _st.fadeLeft.width(), height()); - st::emojiSuggestionsFadeLeft.fill(p, rect); + _st.fadeLeft.fill(p, rect); p.setOpacity(1.); } const auto o_right = std::clamp( @@ -350,11 +467,11 @@ void SuggestionsWidget::paintFadings(QPainter &p) const { if (o_right > 0.) { p.setOpacity(o_right); const auto rect = myrtlrect( - shift.x() + width() - st::emojiSuggestionsFadeRight.width(), + shift.x() + width() - _st.fadeRight.width(), 0, - st::emojiSuggestionsFadeRight.width(), + _st.fadeRight.width(), height()); - st::emojiSuggestionsFadeRight.fill(p, rect); + _st.fadeRight.fill(p, rect); p.setOpacity(1.); } } @@ -601,17 +718,17 @@ SuggestionsController::SuggestionsController( not_null field, not_null session, const Options &options) -: _field(field) +: _st(options.st ? *options.st : st::defaultEmojiSuggestions) +, _field(field) , _session(session) , _showExactTimer([=] { showWithQuery(getEmojiQuery()); }) , _options(options) { - _container = base::make_unique_q( - outer, - st::emojiSuggestionsDropdown); + _container = base::make_unique_q(outer, _st.dropdown); _container->setAutoHiding(false); _suggestions = _container->setOwnedWidget( object_ptr( _container, + _st, session, _options.suggestCustomEmoji, _options.allowCustomWithoutPremium)); @@ -910,7 +1027,7 @@ void SuggestionsController::updateGeometry() { auto boundingRect = _container->parentWidget()->rect(); auto origin = rtl() ? PanelAnimation::Origin::BottomRight : PanelAnimation::Origin::BottomLeft; auto point = rtl() ? (aroundRect.topLeft() + QPoint(aroundRect.width(), 0)) : aroundRect.topLeft(); - const auto padding = st::emojiSuggestionsDropdown.padding; + const auto padding = _st.dropdown.padding; const auto shift = std::min(_container->width() - padding.left() - padding.right(), st::emojiSuggestionSize) / 2; point -= rtl() ? QPoint(_container->width() - padding.right() - shift, _container->height()) : QPoint(padding.left() + shift, _container->height()); if (rtl()) { diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h index 513fb518d..b89de0ad9 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace style { +struct EmojiSuggestions; +} // namespace style + namespace Main { class Session; } // namespace Main @@ -29,125 +33,17 @@ class CustomEmoji; namespace Ui::Emoji { +class SuggestionsWidget; + using SuggestionsQuery = std::variant; -class SuggestionsWidget final : public Ui::RpWidget { -public: - SuggestionsWidget( - QWidget *parent, - not_null session, - bool suggestCustomEmoji, - Fn)> allowCustomWithoutPremium); - ~SuggestionsWidget(); - - void showWithQuery(SuggestionsQuery query, bool force = false); - void selectFirstResult(); - bool handleKeyEvent(int key); - - [[nodiscard]] rpl::producer toggleAnimated() const; - - struct Chosen { - QString emoji; - QString customData; - }; - [[nodiscard]] rpl::producer triggered() const; - -private: - struct Row { - Row(not_null emoji, const QString &replacement); - - Ui::Text::CustomEmoji *custom = nullptr; - DocumentData *document = nullptr; - not_null emoji; - QString replacement; - }; - struct Custom { - not_null document; - not_null emoji; - QString replacement; - }; - - bool eventHook(QEvent *e) override; - void paintEvent(QPaintEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void enterEventHook(QEnterEvent *e) override; - void leaveEventHook(QEvent *e) override; - - void scrollByWheelEvent(not_null e); - void paintFadings(QPainter &p) const; - - [[nodiscard]] std::vector getRowsByQuery(const QString &text) const; - [[nodiscard]] base::flat_multi_map lookupCustom( - const std::vector &rows) const; - [[nodiscard]] std::vector appendCustom( - std::vector rows); - [[nodiscard]] std::vector appendCustom( - std::vector rows, - const base::flat_multi_map &custom); - void resizeToRows(); - void setSelected( - int selected, - anim::type animated = anim::type::instant); - void setPressed(int pressed); - void clearMouseSelection(); - void clearSelection(); - void updateSelectedItem(); - void updateItem(int index); - [[nodiscard]] QRect inner() const; - [[nodiscard]] QPoint innerShift() const; - [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const; - void selectByMouse(QPoint globalPosition); - bool triggerSelectedRow() const; - void triggerRow(const Row &row) const; - - [[nodiscard]] int scrollCurrent() const; - void scrollTo(int value, anim::type animated = anim::type::instant); - void stopAnimations(); - - [[nodiscard]] not_null resolveCustomEmoji( - not_null document); - void customEmojiRepaint(); - - const not_null _session; - SuggestionsQuery _query; - std::vector _rows; - bool _suggestCustomEmoji = false; - Fn)> _allowCustomWithoutPremium; - - base::flat_map< - not_null, - std::unique_ptr> _customEmoji; - bool _repaintScheduled = false; - - std::optional _lastMousePosition; - bool _mouseSelection = false; - int _selected = -1; - int _pressed = -1; - - int _scrollValue = 0; - Ui::Animations::Simple _scrollAnimation; - Ui::Animations::Simple _selectedAnimation; - int _scrollMax = 0; - int _oneWidth = 0; - QMargins _padding; - - QPoint _mousePressPosition; - int _dragScrollStart = -1; - - rpl::event_stream _toggleAnimated; - rpl::event_stream _triggered; - -}; - class SuggestionsController { public: struct Options { bool suggestExactFirstWord = true; bool suggestCustomEmoji = false; Fn)> allowCustomWithoutPremium; + const style::EmojiSuggestions *st = nullptr; }; SuggestionsController( @@ -189,6 +85,7 @@ private: bool fieldFilter(not_null event); bool outerFilter(not_null event); + const style::EmojiSuggestions &_st; bool _shown = false; bool _forceHidden = false; int _queryStartPosition = 0; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index b78615721..6f312926f 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -685,6 +685,7 @@ void FieldAutocomplete::recount(bool resetScroll) { } else if (!_brows.empty()) { h = _brows.size() * st::mentionHeight; } + h += _st.autocompleteBottomSkip; if (_inner->width() != _boundings.width() || _inner->height() != h) { _inner->resize(_boundings.width(), h); @@ -1375,8 +1376,10 @@ void FieldAutocomplete::Inner::setSel(int sel, bool scroll) { int32 row = _sel / _stickersPerRow; const auto padding = st::stickerPanPadding; _scrollToRequested.fire({ - padding + row * st::stickerPanSize.height(), - padding + (row + 1) * st::stickerPanSize.height() }); + (row ? padding : 0) + row * st::stickerPanSize.height(), + (padding + + (row + 1) * st::stickerPanSize.height() + + _st.autocompleteBottomSkip) }); } } } diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 9ff10845d..48a0e9800 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -57,7 +57,8 @@ constexpr auto kMinAfterScrollDelay = crl::time(33); void AddGifAction( Fn &&, const style::icon*)> callback, std::shared_ptr show, - not_null document) { + not_null document, + const style::ComposeIcons *iconsOverride) { if (!document->isGifv()) { return; } @@ -67,6 +68,9 @@ void AddGifAction( const auto text = (saved ? tr::lng_context_delete_gif : tr::lng_context_save_gif)(tr::now); + const auto &icons = iconsOverride + ? *iconsOverride + : st::defaultComposeIcons; callback(text, [=] { Api::ToggleSavedGif( show, @@ -80,7 +84,7 @@ void AddGifAction( document->session().local().writeSavedGifs(); } data.stickers().notifySavedGifsUpdated(); - }, saved ? &st::menuIconDelete : &st::menuIconGif); + }, saved ? &icons.menuGifRemove : &icons.menuGifAdd); } GifsListWidget::GifsListWidget( @@ -380,18 +384,18 @@ base::unique_qptr GifsListWidget::fillContextMenu( return nullptr; } - auto menu = base::make_unique_q( - this, - st::popupMenuWithIcons); + auto menu = base::make_unique_q(this, st().menu); const auto send = [=, selected = _selected](Api::SendOptions options) { selectInlineResult(selected, options, true); }; + const auto icons = &st().icons; SendMenu::FillSendMenu( menu, type, SendMenu::DefaultSilentCallback(send), SendMenu::DefaultScheduleCallback(this, type, send), - SendMenu::DefaultWhenOnlineCallback(send)); + SendMenu::DefaultWhenOnlineCallback(send), + icons); if (const auto item = _mosaic.maybeItemAt(_selected)) { const auto document = item->getDocument() @@ -404,7 +408,7 @@ base::unique_qptr GifsListWidget::fillContextMenu( const style::icon *icon) { menu->addAction(text, std::move(done), icon); }; - AddGifAction(std::move(callback), _show, document); + AddGifAction(std::move(callback), _show, document, icons); } } return menu; diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h index d21c8e531..84e20c864 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace style { +struct ComposeIcons; +} // namespace style + namespace Api { struct SendOptions; } // namespace Api @@ -48,7 +52,8 @@ namespace ChatHelpers { void AddGifAction( Fn &&, const style::icon*)> callback, std::shared_ptr show, - not_null document); + not_null document, + const style::ComposeIcons *iconsOverride = nullptr); class StickersListFooter; struct StickerIcon; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 03430e3ab..b0e263c4a 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -498,7 +498,8 @@ InlineBotQuery ParseInlineBotQuery( } AutocompleteQuery ParseMentionHashtagBotCommandQuery( - not_null field) { + not_null field, + ChatHelpers::ComposeFeatures features) { auto result = AutocompleteQuery(); const auto cursor = field->textCursor(); @@ -530,6 +531,9 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery( const auto text = fragment.text(); for (auto i = position - fragmentPosition; i != 0; --i) { if (text[i - 1] == '@') { + if (!features.autocompleteMentions) { + return {}; + } if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); @@ -540,12 +544,18 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery( } return result; } else if (text[i - 1] == '#') { + if (!features.autocompleteHashtags) { + return {}; + } if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); } return result; } else if (text[i - 1] == '/') { + if (!features.autocompleteCommands) { + return {}; + } if (i < 2 && !fragmentPosition) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 727e51a5b..13012557b 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "base/timer.h" #include "base/qt_connection.h" +#include "chat_helpers/compose/compose_features.h" #ifndef TDESKTOP_DISABLE_SPELLCHECK #include "boxes/dictionaries_manager.h" @@ -90,7 +91,8 @@ struct AutocompleteQuery { bool fromStart = false; }; AutocompleteQuery ParseMentionHashtagBotCommandQuery( - not_null field); + not_null field, + ChatHelpers::ComposeFeatures features); class MessageLinksParser : private QObject { public: diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index a9aa598d6..15e2e8c34 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -292,7 +292,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) descriptor.st ? *descriptor.st : st::defaultEmojiPan) , _session(descriptor.session) , _paused(descriptor.paused) -, _settingsButtonVisible(descriptor.settingsButtonVisible) +, _features(descriptor.features) , _iconState([=] { update(); }) , _subiconState([=] { update(); }) , _selectionBg(st::emojiPanRadius, st().categoriesBgOver) @@ -300,7 +300,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) setMouseTracking(true); _iconsLeft = st().iconSkip - + (_settingsButtonVisible ? st().iconWidth : 0); + + (_features.stickersSettings ? st().iconWidth : 0); _iconsRight = st().iconSkip; _session->downloaderTaskFinished( @@ -618,7 +618,7 @@ void StickersListFooter::paint( return; } - if (_settingsButtonVisible && !hasOnlyFeaturedSets()) { + if (_features.stickersSettings) { paintStickerSettingsIcon(p); } @@ -1012,12 +1012,12 @@ void StickersListFooter::updateSelected() { if (rtl()) x = width() - x; const auto settingsLeft = _iconsLeft - _singleWidth; auto newOver = OverState(SpecialOver::None); - if (_settingsButtonVisible + if (_features.stickersSettings && x >= settingsLeft && x < settingsLeft + _singleWidth && y >= _iconsTop && y < _iconsTop + st().footer) { - if (!_icons.empty() && !hasOnlyFeaturedSets()) { + if (!_icons.empty()) { newOver = SpecialOver::Settings; } } else if (!_icons.empty()) { @@ -1161,17 +1161,11 @@ void StickersListFooter::refreshSubiconsGeometry() { updateEmojiWidthCallback(); } -bool StickersListFooter::hasOnlyFeaturedSets() const { - return (_icons.size() == 1) - && (_icons[0].setId == Data::Stickers::FeaturedSetId); -} - void StickersListFooter::paintStickerSettingsIcon(QPainter &p) const { const auto settingsLeft = _iconsLeft - _singleWidth; - st::stickersSettings.paint( + st().icons.settings.paint( p, - settingsLeft - + (_singleWidth - st::stickersSettings.width()) / 2, + (settingsLeft + (_singleWidth - st().icons.settings.width()) / 2), _iconsTop + st::emojiCategoryIconTop, width()); } @@ -1411,22 +1405,22 @@ void StickersListFooter::paintSetIconToCache( using Section = Ui::Emoji::Section; const auto sectionIcon = [&](Section section, bool active) { const auto icons = std::array{ - &st::emojiRecent, - &st::emojiRecentActive, - &st::emojiPeople, - &st::emojiPeopleActive, - &st::emojiNature, - &st::emojiNatureActive, - &st::emojiFood, - &st::emojiFoodActive, - &st::emojiActivity, - &st::emojiActivityActive, - &st::emojiTravel, - &st::emojiTravelActive, - &st::emojiObjects, - &st::emojiObjectsActive, - &st::emojiSymbols, - &st::emojiSymbolsActive, + &st().icons.recent, + &st().icons.recentActive, + &st().icons.people, + &st().icons.peopleActive, + &st().icons.nature, + &st().icons.natureActive, + &st().icons.food, + &st().icons.foodActive, + &st().icons.activity, + &st().icons.activityActive, + &st().icons.travel, + &st().icons.travelActive, + &st().icons.objects, + &st().icons.objectsActive, + &st().icons.symbols, + &st().icons.symbolsActive, }; const auto index = int(section) * 2 + (active ? 1 : 0); @@ -1464,15 +1458,8 @@ void StickersListFooter::paintSetIconToCache( } else { paintOne(0, [&] { const auto selected = (info.index == _iconState.selected); - if (icon.setId == Data::Stickers::FeaturedSetId) { - const auto &stickers = _session->data().stickers(); - return stickers.featuredSetsUnreadCount() - ? &st::stickersTrendingUnread - : &st::stickersTrending; - //} else if (setId == Stickers::FavedSetId) { - // return &st::stickersFaved; - } else if (icon.setId == AllEmojiSectionSetId()) { - return &st::emojiPeople; + if (icon.setId == AllEmojiSectionSetId()) { + return &st().icons.people; } else if (const auto section = SetIdEmojiSection(icon.setId)) { return sectionIcon(*section, selected); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h index 1a08f6853..f7a0afc1d 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h @@ -7,8 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "media/clip/media_clip_reader.h" +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" +#include "media/clip/media_clip_reader.h" #include "mtproto/sender.h" #include "ui/dpr/dpr_image.h" #include "ui/round_rect.h" @@ -116,8 +117,8 @@ public: not_null session; Fn paused; not_null parent; - bool settingsButtonVisible = false; const style::EmojiPan *st = nullptr; + ComposeFeatures features; }; explicit StickersListFooter(Descriptor &&descriptor); @@ -130,7 +131,6 @@ public: uint64 activeSetId, Fn()> renderer, ValidateIconAnimations animations); - [[nodiscard]] bool hasOnlyFeaturedSets() const; void leaveToChildEvent(QEvent *e, QWidget *child) override; @@ -270,7 +270,7 @@ private: const not_null _session; const Fn _paused; - const bool _settingsButtonVisible = false; + const ComposeFeatures _features; static constexpr auto kVisibleIconsCount = 8; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 40aae70eb..cf0dda5eb 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -184,12 +184,12 @@ StickersListWidget::StickersListWidget( descriptor.paused) , _mode(descriptor.mode) , _show(std::move(descriptor.show)) +, _features(descriptor.features) , _overBg(st::roundRadiusSmall, st().overBg) , _api(&session().mtp()) , _localSetsManager(std::make_unique(&session())) , _section(Section::Stickers) , _isMasks(_mode == Mode::Masks) -, _settingsHidden(descriptor.settingsHidden) , _updateItemsTimer([=] { updateItems(); }) , _updateSetsTimer([=] { updateSets(); }) , _trendingAddBgOver( @@ -286,8 +286,8 @@ object_ptr StickersListWidget::createFooter() { .session = &session(), .paused = footerPaused, .parent = this, - .settingsButtonVisible = !_settingsHidden, .st = &st(), + .features = _features, }); _footer = result; @@ -298,7 +298,7 @@ object_ptr StickersListWidget::createFooter() { _footer->openSettingsRequests( ) | rpl::start_with_next([=] { - const auto onlyFeatured = _footer->hasOnlyFeaturedSets(); + const auto onlyFeatured = !_isMasks && _mySets.empty(); _show->showBox(Box( _show, (onlyFeatured @@ -908,7 +908,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { auto add = featuredAddRect(info); int checkx = add.left() + (add.width() - st::stickersFeaturedInstalled.width()) / 2; int checky = add.top() + (add.height() - st::stickersFeaturedInstalled.height()) / 2; - st::stickersFeaturedInstalled.paint(p, QPoint(checkx, checky), width()); + st().trendingInstalled.paint(p, QPoint(checkx, checky), width()); } if (set.flags & SetFlag::Unread) { widthForTitle -= st::stickersFeaturedUnreadSize + st::stickersFeaturedUnreadSkip; @@ -921,12 +921,12 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { titleWidth = st::stickersTrendingHeaderFont->width(titleText); } p.setFont(st::stickersTrendingHeaderFont); - p.setPen(st::stickersTrendingHeaderFg); + p.setPen(st().trendingHeaderFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st::stickersTrendingHeaderTop, width(), titleText, titleWidth); if (set.flags & SetFlag::Unread) { p.setPen(Qt::NoPen); - p.setBrush(st::stickersFeaturedUnreadBg); + p.setBrush(st().trendingUnreadFg); { PainterHighQualityEnabler hq(p); @@ -936,7 +936,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { auto statusText = (count > 0) ? tr::lng_stickers_count(tr::now, lt_count, count) : tr::lng_contacts_loading(tr::now); p.setFont(st::stickersTrendingSubheaderFont); - p.setPen(st::stickersTrendingSubheaderFg); + p.setPen(st().trendingSubheaderFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st::stickersTrendingSubheaderTop, width(), statusText); if (info.rowsTop >= clip.y() + clip.height()) { @@ -963,13 +963,14 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { if (hasRemoveButton(info.section)) { auto remove = removeButtonRect(info); auto selected = selectedButton ? (selectedButton->section == info.section) : false; + const auto &removeSt = st().removeSet; if (set.ripple) { - set.ripple->paint(p, remove.x() + st::stickerPanRemoveSet.rippleAreaPosition.x(), remove.y() + st::stickerPanRemoveSet.rippleAreaPosition.y(), width()); + set.ripple->paint(p, remove.x() + removeSt.rippleAreaPosition.x(), remove.y() + removeSt.rippleAreaPosition.y(), width()); if (set.ripple->empty()) { set.ripple.reset(); } } - const auto &icon = selected ? st::stickerPanRemoveSet.iconOver : st::stickerPanRemoveSet.icon; + const auto &icon = selected ? removeSt.iconOver : removeSt.icon; icon.paint( p, remove.x() + (remove.width() - icon.width()) / 2, @@ -983,7 +984,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { titleWidth = st::stickersTrendingHeaderFont->width(titleText); } p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st().headerTop, width(), titleText, titleWidth); } if (clip.top() + clip.height() <= info.rowsTop) { @@ -1108,7 +1109,7 @@ int StickersListWidget::megagroupSetInfoLeft() const { } void StickersListWidget::paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected) { - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); auto infoLeft = megagroupSetInfoLeft(); _megagroupSetAbout.drawLeft(p, infoLeft, y, width() - infoLeft, width()); @@ -1483,8 +1484,9 @@ QRect StickersListWidget::removeButtonRect(int index) const { } QRect StickersListWidget::removeButtonRect(const SectionInfo &info) const { - auto buttonw = st::stickerPanRemoveSet.width; - auto buttonh = st::stickerPanRemoveSet.height; + const auto &removeSt = st().removeSet; + auto buttonw = removeSt.width; + auto buttonh = removeSt.height; auto buttonx = stickersRight() - buttonw; auto buttony = info.top + (st().header - buttonh) / 2; return QRect(buttonx, buttony, buttonw, buttonh); @@ -1561,10 +1563,11 @@ std::unique_ptr StickersListWidget::createButtonRipple(int std::move(mask), [this, section] { rtlupdate(featuredAddRect(section)); }); } - auto maskSize = QSize(st::stickerPanRemoveSet.rippleAreaSize, st::stickerPanRemoveSet.rippleAreaSize); + const auto &removeSt = st().removeSet; + auto maskSize = QSize(removeSt.rippleAreaSize, removeSt.rippleAreaSize); auto mask = Ui::RippleAnimation::EllipseMask(maskSize); return std::make_unique( - st::stickerPanRemoveSet.ripple, + removeSt.ripple, std::move(mask), [this, section] { rtlupdate(removeButtonRect(section)); }); } @@ -1575,7 +1578,8 @@ QPoint StickersListWidget::buttonRippleTopLeft(int section) const { if (shownSets()[section].externalLayout) { return myrtlrect(featuredAddRect(section)).topLeft(); } - return myrtlrect(removeButtonRect(section)).topLeft() + st::stickerPanRemoveSet.rippleAreaPosition; + return myrtlrect(removeButtonRect(section)).topLeft() + + st().removeSet.rippleAreaPosition; } void StickersListWidget::showStickerSetBox(not_null document) { @@ -1604,9 +1608,7 @@ base::unique_qptr StickersListWidget::fillContextMenu( auto &set = sets[section]; Assert(index >= 0 && index < set.stickers.size()); - auto menu = base::make_unique_q( - this, - st::popupMenuWithIcons); + auto menu = base::make_unique_q(this, st().menu); const auto document = set.stickers[sticker->index].document; const auto send = [=](Api::SendOptions options) { @@ -1618,12 +1620,14 @@ base::unique_qptr StickersListWidget::fillContextMenu( : messageSentAnimationInfo(section, index, document), }); }; + const auto icons = &st().icons; SendMenu::FillSendMenu( menu, type, SendMenu::DefaultSilentCallback(send), SendMenu::DefaultScheduleCallback(this, type, send), - SendMenu::DefaultWhenOnlineCallback(send)); + SendMenu::DefaultWhenOnlineCallback(send), + icons); const auto show = _show; const auto toggleFavedSticker = [=] { @@ -1638,11 +1642,13 @@ base::unique_qptr StickersListWidget::fillContextMenu( ? tr::lng_faved_stickers_remove : tr::lng_faved_stickers_add)(tr::now), toggleFavedSticker, - isFaved ? &st::menuIconUnfave : &st::menuIconFave); + isFaved ? &icons->menuUnfave : &icons->menuFave); - menu->addAction(tr::lng_context_pack_info(tr::now), [=] { - showStickerSetBox(document); - }, &st::menuIconStickers); + if (_features.openStickerSets) { + menu->addAction(tr::lng_context_pack_info(tr::now), [=] { + showStickerSetBox(document); + }, &icons->menuStickerSet); + } if (const auto id = set.id; id == Data::Stickers::RecentSetId) { menu->addAction(tr::lng_recent_stickers_remove(tr::now), [=] { @@ -1650,7 +1656,7 @@ base::unique_qptr StickersListWidget::fillContextMenu( document, Data::FileOriginStickerSet(id, 0), false); - }, &st::menuIconDelete); + }, &icons->menuRecentRemove); } return menu; } @@ -1708,7 +1714,8 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { return; } const auto document = set.stickers[sticker->index].document; - if (e->modifiers() & Qt::ControlModifier) { + if (_features.openStickerSets + && (e->modifiers() & Qt::ControlModifier)) { showStickerSetBox(document); } else { _chosen.fire({ @@ -1859,12 +1866,6 @@ void StickersListWidget::processPanelHideFinished() { if (_footer) { _footer->clearHeavyData(); } - // Preserve panel state through visibility toggles. - //// Reset to the recent stickers section. - //if (_section == Section::Featured && (!_footer || !_footer->hasOnlyFeaturedSets())) { - // setSection(Section::Stickers); - // validateSelectedIcon(ValidateIconAnimations::None); - //} } void StickersListWidget::setSection(Section section) { @@ -1994,9 +1995,6 @@ void StickersListWidget::refreshSettingsVisibility() { void StickersListWidget::refreshFooterIcons() { refreshIcons(ValidateIconAnimations::None); - if (_footer->hasOnlyFeaturedSets() && _section != Section::Featured) { - showStickerSet(Data::Stickers::FeaturedSetId); - } } void StickersListWidget::preloadImages() { @@ -2064,9 +2062,6 @@ void StickersListWidget::refreshRecent() { if (_section == Section::Stickers) { refreshRecentStickers(); } - if (_footer && _footer->hasOnlyFeaturedSets() && _section != Section::Featured) { - showStickerSet(Data::Stickers::FeaturedSetId); - } } auto StickersListWidget::collectRecentStickers() -> std::vector { @@ -2201,7 +2196,7 @@ void StickersListWidget::refreshFavedStickers() { } void StickersListWidget::refreshMegagroupStickers(GroupStickersPlace place) { - if (!_megagroupSet || _isMasks) { + if (!_features.megagroupSet || !_megagroupSet || _isMasks) { return; } auto canEdit = _megagroupSet->canEditStickers(); @@ -2354,10 +2349,12 @@ void StickersListWidget::updateSelected() { newSelected = OverButton{ section }; } else if (featuredHasAddButton(section) && myrtlrect(featuredAddRect(info)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; - } else if (!(sets[section].flags & SetFlag::Special)) { + } else if (_features.openStickerSets + && !(sets[section].flags & SetFlag::Special)) { newSelected = OverSet{ section }; - } else if (sets[section].id == Data::Stickers::MegagroupSetId - && (_megagroupSet->canEditStickers() || !sets[section].stickers.empty())) { + } else if ((sets[section].id == Data::Stickers::MegagroupSetId) + && (_megagroupSet->canEditStickers() + || !sets[section].stickers.empty())) { newSelected = OverSet{ section }; } } else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom && sx >= 0) { @@ -2624,10 +2621,12 @@ void StickersListWidget::removeMegagroupSet(bool locally) { close(); }), .cancelled = [](Fn &&close) { close(); }, + .labelStyle = &st().boxLabel, })); } void StickersListWidget::removeSet(uint64 setId) { + const auto &st = this->st().boxLabel; if (setId == Data::Stickers::MegagroupSetId) { const auto &sets = shownSets(); const auto i = ranges::find(sets, setId, &Set::id); @@ -2635,7 +2634,7 @@ void StickersListWidget::removeSet(uint64 setId) { const auto removeLocally = i->stickers.empty() || !_megagroupSet->canEditStickers(); removeMegagroupSet(removeLocally); - } else if (auto box = MakeConfirmRemoveSetBox(&session(), setId)) { + } else if (auto box = MakeConfirmRemoveSetBox(&session(), st, setId)) { checkHideWithBox(std::move(box)); } } @@ -2660,6 +2659,7 @@ StickersListWidget::~StickersListWidget() = default; object_ptr MakeConfirmRemoveSetBox( not_null session, + const style::FlatLabel &st, uint64 setId) { const auto &sets = session->data().stickers().sets(); const auto it = sets.find(setId); @@ -2726,6 +2726,7 @@ object_ptr MakeConfirmRemoveSetBox( } }, .confirmText = tr::lng_stickers_remove_pack_confirm(), + .labelStyle = &st, }); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index dc4e7dc44..6e2c2fa04 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" #include "data/stickers/data_stickers.h" #include "ui/round_rect.h" @@ -50,6 +51,7 @@ enum class Notification; namespace style { struct EmojiPan; +struct FlatLabel; } // namespace style namespace ChatHelpers { @@ -70,7 +72,7 @@ struct StickersListDescriptor { StickersListMode mode = StickersListMode::Full; Fn paused; const style::EmojiPan *st = nullptr; - bool settingsHidden = false; + ComposeFeatures features; }; class StickersListWidget final : public TabbedSelector::Inner { @@ -352,6 +354,7 @@ private: const Mode _mode; const std::shared_ptr _show; + const ComposeFeatures _features; Ui::RoundRect _overBg; std::unique_ptr _search; MTP::Sender _api; @@ -375,7 +378,6 @@ private: Section _section = Section::Stickers; const bool _isMasks; - bool _settingsHidden = false; base::Timer _updateItemsTimer; base::Timer _updateSetsTimer; @@ -426,6 +428,7 @@ private: [[nodiscard]] object_ptr MakeConfirmRemoveSetBox( not_null session, + const style::FlatLabel &st, uint64 setId); } // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp index 5321e649e..e8ec060fb 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp @@ -240,7 +240,7 @@ void TabbedPanel::paintEvent(QPaintEvent *e) { hideFinished(); } else { if (!_cache.isNull()) _cache = QPixmap(); - Ui::Shadow::paint(p, innerRect(), width(), st::emojiPanAnimation.shadow); + Ui::Shadow::paint(p, innerRect(), width(), _selector->st().showAnimation.shadow); } } @@ -362,7 +362,11 @@ void TabbedPanel::startShowAnimation() { if (!_a_show.animating()) { auto image = grabForAnimation(); - _showAnimation = std::make_unique(st::emojiPanAnimation, _dropDown ? Ui::PanelAnimation::Origin::TopRight : Ui::PanelAnimation::Origin::BottomRight); + _showAnimation = std::make_unique( + _selector->st().showAnimation, + (_dropDown + ? Ui::PanelAnimation::Origin::TopRight + : Ui::PanelAnimation::Origin::BottomRight)); auto inner = rect().marginsRemoved(st::emojiPanMargins); _showAnimation->setFinalImage(std::move(image), QRect(inner.topLeft() * cIntRetinaFactor(), inner.size() * cIntRetinaFactor())); _showAnimation->setCornerMasks(Images::CornersMask(st::emojiPanRadius)); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 5131c9f9b..43b08c4a4 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -344,6 +344,7 @@ TabbedSelector::TabbedSelector( TabbedSelectorDescriptor &&descriptor) : RpWidget(parent) , _st(descriptor.st) +, _features(descriptor.features) , _show(std::move(descriptor.show)) , _level(descriptor.level) , _mode(descriptor.mode) @@ -377,7 +378,6 @@ TabbedSelector::TabbedSelector( : SelectorTab::Emoji) , _hasEmojiTab(ranges::contains(_tabs, SelectorTab::Emoji, &Tab::type)) , _hasStickersTab(ranges::contains(_tabs, SelectorTab::Stickers, &Tab::type)) -, _stickersSettingsHidden(descriptor.stickersSettingsHidden) , _hasGifsTab(ranges::contains(_tabs, SelectorTab::Gifs, &Tab::type)) , _hasMasksTab(ranges::contains(_tabs, SelectorTab::Masks, &Tab::type)) , _tabbed(_tabs.size() > 1) { @@ -487,6 +487,10 @@ TabbedSelector::TabbedSelector( TabbedSelector::~TabbedSelector() = default; +const style::EmojiPan &TabbedSelector::st() const { + return _st; +} + Main::Session &TabbedSelector::session() const { return _show->session(); } @@ -511,6 +515,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { : EmojiMode::Full), .paused = paused, .st = &_st, + .features = _features, }); } case SelectorTab::Stickers: { @@ -521,7 +526,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { .mode = StickersMode::Full, .paused = paused, .st = &_st, - .settingsHidden = _stickersSettingsHidden, + .features = _features, }); } case SelectorTab::Gifs: { @@ -540,6 +545,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { .mode = StickersMode::Masks, .paused = paused, .st = &_st, + .features = _features, }); } } diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index bcaa4fdee..d41aebb80 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "api/api_common.h" +#include "chat_helpers/compose/compose_features.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/effects/message_sending_animation_common.h" @@ -87,7 +88,7 @@ struct TabbedSelectorDescriptor { const style::EmojiPan &st; PauseReason level = {}; TabbedSelectorMode mode = TabbedSelectorMode::Full; - bool stickersSettingsHidden = false; + ComposeFeatures features; }; [[nodiscard]] std::unique_ptr MakeSearch( @@ -117,6 +118,7 @@ public: TabbedSelectorDescriptor &&descriptor); ~TabbedSelector(); + [[nodiscard]] const style::EmojiPan &st() const; [[nodiscard]] Main::Session &session() const; [[nodiscard]] PauseReason level() const; @@ -267,6 +269,7 @@ private: not_null masks() const; const style::EmojiPan &_st; + const ComposeFeatures _features; const std::shared_ptr _show; const PauseReason _level = {}; @@ -291,7 +294,6 @@ private: const bool _hasEmojiTab; const bool _hasStickersTab; - const bool _stickersSettingsHidden; const bool _hasGifsTab; const bool _hasMasksTab; const bool _tabbed; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 87da53703..ec4aa4d07 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1388,7 +1388,7 @@ AutocompleteQuery HistoryWidget::parseMentionHashtagBotCommandQuery() const { const auto result = (isChoosingTheme() || (_inlineBot && !_inlineLookingUpBot)) ? AutocompleteQuery() - : ParseMentionHashtagBotCommandQuery(_field); + : ParseMentionHashtagBotCommandQuery(_field, {}); if (result.query.isEmpty()) { return result; } else if (result.query[0] == '#' diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index a3fd8b3c8..1f4fb79b0 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -956,7 +956,7 @@ ComposeControls::ComposeControls( .st = _st.tabbed, .level = Window::GifPauseReason::TabbedPanel, .mode = ChatHelpers::TabbedSelector::Mode::Full, - .stickersSettingsHidden = !_features.stickersSettings, + .features = _features, })) , _selector(_regularWindow ? _regularWindow->tabbedSelector() @@ -982,7 +982,10 @@ ComposeControls::ComposeControls( _wrap.get(), st::historyBotCommandStart) : nullptr) -, _autocomplete(std::make_unique(parent, _show)) +, _autocomplete(std::make_unique( + parent, + _show, + &_st.tabbed)) , _header(std::make_unique(_wrap.get(), _show)) , _voiceRecordBar(std::make_unique( _wrap.get(), @@ -1389,7 +1392,7 @@ void ComposeControls::checkAutocomplete() { const auto peer = _history->peer; const auto autocomplete = _isInlineBot ? AutocompleteQuery() - : ParseMentionHashtagBotCommandQuery(_field); + : ParseMentionHashtagBotCommandQuery(_field, _features); if (!autocomplete.query.isEmpty()) { if (autocomplete.query[0] == '#' && cRecentWriteHashtags().isEmpty() @@ -1656,7 +1659,11 @@ void ComposeControls::initField() { _parent, _field, _session, - { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); + { + .suggestCustomEmoji = true, + .allowCustomWithoutPremium = allow, + .st = &_st.suggestions, + }); _raiseEmojiSuggestions = [=] { suggestions->raise(); }; const auto rawTextEdit = _field->rawTextEdit().get(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index d5749bfcb..223940671 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -7,10 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "base/required.h" #include "api/api_common.h" +#include "base/required.h" #include "base/unique_qptr.h" #include "base/timer.h" +#include "chat_helpers/compose/compose_features.h" #include "dialogs/dialogs_key.h" #include "history/view/controls/compose_controls_common.h" #include "ui/round_rect.h" @@ -93,16 +94,6 @@ enum class ComposeControlsMode { Scheduled, }; -struct ComposeControlsFeatures { - bool sendAs = true; - bool ttlInfo = true; - bool botCommandSend = true; - bool silentBroadcastToggle = true; - bool attachBotsMenu = true; - bool inlineBots = true; - bool stickersSettings = true; -}; - struct ComposeControlsDescriptor { const style::ComposeControls *stOverride = nullptr; std::shared_ptr show; @@ -111,7 +102,7 @@ struct ComposeControlsDescriptor { SendMenu::Type sendMenuType = {}; Window::SessionController *regularWindow = nullptr; rpl::producer stickerOrEmojiChosen; - ComposeControlsFeatures features; + ChatHelpers::ComposeFeatures features; }; class ComposeControls final { @@ -329,7 +320,7 @@ private: void changeFocusedControl(); const style::ComposeControls &_st; - const ComposeControlsFeatures _features; + const ChatHelpers::ComposeFeatures _features; const not_null _parent; const std::shared_ptr _show; const not_null _session; diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp index 6f9186882..3ce74ef55 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp @@ -321,7 +321,7 @@ void EmojiSelector::createSelector(Type type) { if (isEmoji) { st::userpicBuilderEmojiToggleStickersIcon.paintInCenter(p, r); } else { - st::emojiPeople.paintInCenter(p, r); + st::defaultEmojiPan.icons.people.paintInCenter(p, r); } }, toggleButton->lifetime()); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index f800f192c..1b44b8fa2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/inline_bot_result.h" #include "media/stories/media_stories_controller.h" #include "menu/menu_send.h" +#include "styles/style_chat_helpers.h" #include "styles/style_media_view.h" namespace Media::Stories { @@ -41,6 +42,12 @@ ReplyArea::ReplyArea(not_null controller) .silentBroadcastToggle = false, .attachBotsMenu = false, .inlineBots = false, + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + .autocompleteHashtags = false, + .autocompleteMentions = false, + .autocompleteCommands = false, }, } )) { @@ -52,13 +59,27 @@ ReplyArea::~ReplyArea() { } void ReplyArea::initGeometry() { - _controller->layoutValue( - ) | rpl::start_with_next([=](const Layout &layout) { - _controls->resizeToWidth(layout.content.width()); - const auto position = layout.controlsBottomPosition - - QPoint(0, _controls->heightCurrent()); - _controls->move(position.x(), position.y()); - _controls->setAutocompleteBoundingRect(layout.autocompleteRect); + rpl::combine( + _controller->layoutValue(), + _controls->height() + ) | rpl::start_with_next([=](const Layout &layout, int height) { + const auto content = layout.content; + _controls->resizeToWidth(content.width()); + if (_controls->heightCurrent() == height) { + const auto position = layout.controlsBottomPosition + - QPoint(0, height); + _controls->move(position.x(), position.y()); + const auto &tabbed = st::storiesComposeControls.tabbed; + const auto upper = QRect( + content.x(), + content.y(), + content.width(), + (position.y() + + tabbed.autocompleteBottomSkip + - content.y())); + _controls->setAutocompleteBoundingRect( + layout.autocompleteRect.intersected(upper)); + } }, _lifetime); } diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 7e01278eb..aa9377f09 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -407,7 +407,7 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); -storiesMaxSize: size(405px, 720px); +storiesMaxSize: size(540px, 960px); storiesMaxNameFontSize: 17px; storiesRadius: 8px; storiesControlSize: 64px; @@ -460,6 +460,53 @@ storiesAttach: IconButton(historyAttach) { } storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; +storiesRemoveSet: IconButton(stickerPanRemoveSet) { + icon: icon {{ "simple_close", storiesComposeGrayIcon }}; + iconOver: icon {{ "simple_close", storiesComposeGrayIcon }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: storiesComposeBgOver; + } +} +storiesMenu: Menu(defaultMenu) { + itemBg: groupCallMenuBg; + itemBgOver: groupCallMenuBgOver; + itemFg: groupCallMembersFg; + itemFgOver: groupCallMembersFg; + itemFgDisabled: groupCallMemberNotJoinedStatus; + itemFgShortcut: groupCallMemberNotJoinedStatus; + itemFgShortcutOver: groupCallMemberNotJoinedStatus; + itemFgShortcutDisabled: groupCallMemberNotJoinedStatus; + + separator: MenuSeparator(defaultMenuSeparator) { + fg: groupCallMenuBgOver; + } + arrow: icon {{ "menu/submenu_arrow", groupCallMemberNotJoinedStatus }}; + + ripple: RippleAnimation(defaultRippleAnimation) { + color: groupCallMenuBgRipple; + } +} +storiesMenuShadow: Shadow(defaultEmptyShadow) { + fallback: groupCallMenuBg; +} +storiesMenuAnimation: PanelAnimation(defaultPanelAnimation) { + fadeBg: groupCallMenuBg; + shadow: storiesMenuShadow; +} +storiesPopupMenu: PopupMenu(defaultPopupMenu) { + shadow: storiesMenuShadow; + menu: storiesMenu; + animation: storiesMenuAnimation; +} +storiesMenuWithIcons: Menu(storiesMenu) { + itemIconPosition: point(15px, 5px); + itemPadding: margins(54px, 8px, 17px, 8px); +} +storiesPopupMenuWithIcons: PopupMenu(storiesPopupMenu) { + scrollPadding: margins(0px, 5px, 0px, 5px); + menu: storiesMenuWithIcons; +} + storiesComposeControls: ComposeControls(defaultComposeControls) { bg: storiesComposeBg; radius: storiesRadius; @@ -469,6 +516,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { placeholderFg: storiesComposeGrayText; placeholderFgActive: storiesComposeGrayText; placeholderFgError: storiesComposeGrayText; + menu: storiesPopupMenu; } send: SendButton(historySend) { inner: IconButton(storiesAttach) { @@ -489,10 +537,30 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { lineFg: storiesComposeGrayIcon; lineFgOver: storiesComposeGrayIcon; } - tabbed: EmojiPan(defaultEmojiPan) { + suggestions: EmojiSuggestions(defaultEmojiSuggestions) { + dropdown: InnerDropdown(emojiSuggestionsDropdown) { + animation: PanelAnimation(defaultPanelAnimation) { + fadeBg: storiesComposeBg; + } + bg: storiesComposeBg; + } bg: storiesComposeBg; overBg: storiesComposeBgOver; - expandBg: storiesComposeGrayText; + textFg: storiesComposeWhiteText; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", storiesComposeBg }}; + fadeRight: icon {{ "fade_horizontal", storiesComposeBg }}; + } + tabbed: EmojiPan(defaultEmojiPan) { + showAnimation: PanelAnimation(emojiPanAnimation) { + fadeBg: storiesComposeBg; + } + bg: storiesComposeBg; + headerFg: storiesComposeGrayText; + trendingHeaderFg: storiesComposeWhiteText; + trendingSubheaderFg: storiesComposeGrayText; + trendingUnreadFg: storiesComposeBlue; + trendingInstalled: icon {{ "chat/input_save", storiesComposeBlue }}; + overBg: storiesComposeBgOver; pathBg: storiesComposeBgRipple; pathFg: storiesComposeBgOver; textFg: storiesComposeWhiteText; @@ -500,6 +568,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { categoriesBgOver: storiesComposeBgOver; fadeLeft: icon {{ "fade_horizontal-flip_horizontal", storiesComposeBg }}; fadeRight: icon {{ "fade_horizontal", storiesComposeBg }}; + menu: storiesPopupMenuWithIcons; tabs: SettingsSlider(emojiTabs) { barFgActive: storiesComposeBlue; labelFg: storiesComposeGrayText; @@ -536,5 +605,40 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { ripple: emptyRippleAnimation; } } + removeSet: storiesRemoveSet; + boxLabel: FlatLabel(boxLabel) { + textFg: groupCallMembersFg; + } + icons: ComposeIcons { + settings: icon {{ "emoji/emoji_settings", storiesComposeGrayIcon }}; + + recent: icon {{ "emoji/emoji_recent", storiesComposeGrayIcon }}; + recentActive: icon {{ "emoji/emoji_recent", storiesComposeWhiteText }}; + people: icon {{ "emoji/emoji_smile", storiesComposeGrayIcon }}; + peopleActive: icon {{ "emoji/emoji_smile", storiesComposeWhiteText }}; + nature: icon {{ "emoji/emoji_nature", storiesComposeGrayIcon }}; + natureActive: icon {{ "emoji/emoji_nature", storiesComposeWhiteText }}; + food: icon {{ "emoji/emoji_food", storiesComposeGrayIcon }}; + foodActive: icon {{ "emoji/emoji_food", storiesComposeWhiteText }}; + activity: icon {{ "emoji/emoji_activities", storiesComposeGrayIcon }}; + activityActive: icon {{ "emoji/emoji_activities", storiesComposeWhiteText }}; + travel: icon {{ "emoji/emoji_travel", storiesComposeGrayIcon }}; + travelActive: icon {{ "emoji/emoji_travel", storiesComposeWhiteText }}; + objects: icon {{ "emoji/emoji_objects", storiesComposeGrayIcon }}; + objectsActive: icon {{ "emoji/emoji_objects", storiesComposeWhiteText }}; + symbols: icon {{ "emoji/emoji_love", storiesComposeGrayIcon }}; + symbolsActive: icon {{ "emoji/emoji_love", storiesComposeWhiteText }}; + + menuFave: icon {{ "menu/favorite", storiesComposeWhiteText }}; + menuUnfave: icon {{ "menu/unfavorite", storiesComposeWhiteText }}; + menuStickerSet: icon {{ "menu/stickers", storiesComposeWhiteText }}; + menuRecentRemove: icon {{ "menu/delete", storiesComposeWhiteText }}; + menuGifAdd: icon {{ "menu/gif", storiesComposeWhiteText }}; + menuGifRemove: icon {{ "menu/delete", storiesComposeWhiteText }}; + menuMute: icon {{ "menu/mute", storiesComposeWhiteText }}; + menuSchedule: icon {{ "menu/calendar", storiesComposeWhiteText }}; + menuWhenOnline: icon {{ "menu/send_when_online", storiesComposeWhiteText }}; + } + autocompleteBottomSkip: 10px; } } diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 2d5ca3194..02ab8ed4b 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_unread_things.h" #include "apiwrap.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" #include @@ -56,10 +57,14 @@ FillMenuResult FillSendMenu( Type type, Fn silent, Fn schedule, - Fn whenOnline) { + Fn whenOnline, + const style::ComposeIcons *iconsOverride) { if (!silent && !schedule) { return FillMenuResult::None; } + const auto &icons = iconsOverride + ? *iconsOverride + : st::defaultComposeIcons; const auto now = type; if (now == Type::Disabled || (!silent && now == Type::SilentOnly)) { @@ -70,7 +75,7 @@ FillMenuResult FillSendMenu( menu->addAction( tr::lng_send_silent_message(tr::now), silent, - &st::menuIconMute); + &icons.menuMute); } if (schedule && now != Type::SilentOnly) { menu->addAction( @@ -78,13 +83,13 @@ FillMenuResult FillSendMenu( ? tr::lng_reminder_message(tr::now) : tr::lng_schedule_message(tr::now)), schedule, - &st::menuIconSchedule); + &icons.menuSchedule); } if (whenOnline && now == Type::ScheduledToUser) { menu->addAction( tr::lng_scheduled_send_until_online(tr::now), whenOnline, - &st::menuIconWhenOnline); + &icons.menuWhenOnline); } return FillMenuResult::Success; } diff --git a/Telegram/SourceFiles/menu/menu_send.h b/Telegram/SourceFiles/menu/menu_send.h index 0676de49e..79edc3840 100644 --- a/Telegram/SourceFiles/menu/menu_send.h +++ b/Telegram/SourceFiles/menu/menu_send.h @@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace style { +struct ComposeIcons; +} // namespace style + namespace Api { struct SendOptions; } // namespace Api @@ -47,7 +51,8 @@ FillMenuResult FillSendMenu( Type type, Fn silent, Fn schedule, - Fn whenOnline); + Fn whenOnline, + const style::ComposeIcons *iconsOverride = nullptr); void SetupMenuAndShortcuts( not_null button, From 00b4f7738427c52ed0e85170cdee92178fa87130 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 18 May 2023 12:56:15 +0400 Subject: [PATCH 023/260] Finish theming for voice recording in stories. --- .../chat_helpers/chat_helpers.style | 49 ++++- .../SourceFiles/history/history_widget.cpp | 12 -- .../history_view_compose_controls.cpp | 32 +-- .../controls/history_view_compose_controls.h | 1 + .../history_view_voice_record_bar.cpp | 192 ++++++++++++------ .../controls/history_view_voice_record_bar.h | 28 ++- .../history_view_voice_record_button.cpp | 5 +- .../history_view_voice_record_button.h | 15 +- .../media/stories/media_stories_reply.cpp | 1 + .../SourceFiles/media/view/media_view.style | 32 ++- 10 files changed, 244 insertions(+), 123 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index ba786876f..3d40ef02d 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -139,6 +139,29 @@ SendButton { sendDisabledFg: color; } +RecordBarLock { + ripple: RippleAnimation; + originTop: icon; + originBottom: icon; + originBody: icon; + shadowTop: icon; + shadowBottom: icon; + shadowBody: icon; + arrow: icon; + fg: color; +} + +RecordBar { + radius: pixels; + bg: color; + durationFg: color; + cancel: color; + cancelActive: color; + cancelRipple: RippleAnimation; + lock: RecordBarLock; + remove: IconButton; +} + ComposeControls { bg: color; radius: pixels; @@ -149,6 +172,7 @@ ComposeControls { emoji: EmojiButton; suggestions: EmojiSuggestions; tabbed: EmojiPan; + record: RecordBar; } switchPmButton: RoundButton(defaultBoxButton) { @@ -875,7 +899,6 @@ historyRecordTextRight: 25px; historyRecordLockShowDuration: historyToDownDuration; historyRecordLockSize: size(75px, 133px); -historyRecordLockIconFg: historyToDownFg; historyRecordLockIconSize: size(14px, 17px); historyRecordLockIconBottomHeight: 9px; historyRecordLockIconLineHeight: 2px; @@ -916,6 +939,29 @@ historySilentToggle: IconButton(historyBotKeyboardShow) { historySilentToggleOn: icon {{ "chat/input_silent_on", historyComposeIconFg }}; historySilentToggleOnOver: icon {{ "chat/input_silent_on", historyComposeIconFgOver }}; +defaultRecordBarLock: RecordBarLock { + ripple: defaultRippleAnimation; + originTop: historyRecordLockTop; + originBottom: historyRecordLockBottom; + originBody: historyRecordLockBody; + shadowTop: historyRecordLockTopShadow; + shadowBottom: historyRecordLockBottomShadow; + shadowBody: historyRecordLockBodyShadow; + arrow: historyRecordLockArrow; + fg: historyToDownFg; +} +defaultRecordBar: RecordBar { + bg: historyComposeAreaBg; + durationFg: historyRecordDurationFg; + cancel: historyRecordCancel; + cancelActive: historyRecordCancelActive; + cancelRipple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgRipple; + } + lock: defaultRecordBarLock; + remove: historyRecordDelete; +} + historySend: SendButton { inner: IconButton(historyAttach) { icon: historySendIcon; @@ -936,4 +982,5 @@ defaultComposeControls: ComposeControls { emoji: historyAttachEmoji; suggestions: defaultEmojiSuggestions; tabbed: defaultEmojiPan; + record: defaultRecordBar; } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index ec4aa4d07..c6e81a086 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -943,18 +943,6 @@ void HistoryWidget::refreshTabbedPanel() { } void HistoryWidget::initVoiceRecordBar() { - { - auto scrollHeight = rpl::combine( - _scroll->topValue(), - _scroll->heightValue() - ) | rpl::map([](int top, int height) { - return top + height - st::historyRecordLockPosition.y(); - }); - _voiceRecordBar->setLockBottom(std::move(scrollHeight)); - } - - _voiceRecordBar->setSendButtonGeometryValue(_send->geometryValue()); - _voiceRecordBar->setStartRecordingFilter([=] { const auto error = [&]() -> std::optional { if (_peer) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 1f4fb79b0..50fe6c136 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -989,10 +989,14 @@ ComposeControls::ComposeControls( , _header(std::make_unique(_wrap.get(), _show)) , _voiceRecordBar(std::make_unique( _wrap.get(), - parent, - _show, - _send, - st::historySendSize.height())) + Controls::VoiceRecordBarDescriptor{ + .outerContainer = parent, + .show = _show, + .send = _send, + .stOverride = &_st.record, + .recorderHeight = st::historySendSize.height(), + .lockFromBottom = descriptor.voiceLockFromBottom, + })) , _sendMenuType(descriptor.sendMenuType) , _unavailableEmojiPasted(std::move(descriptor.unavailableEmojiPasted)) , _saveDraftTimer([=] { saveDraft(); }) @@ -2278,26 +2282,6 @@ void ComposeControls::initVoiceRecordBar() { return false; }); - { - auto geometry = rpl::merge( - _wrap->geometryValue(), - _send->geometryValue() - ) | rpl::map([=](QRect geometry) { - auto r = _send->geometry(); - r.setY(r.y() + _wrap->y()); - return r; - }); - _voiceRecordBar->setSendButtonGeometryValue(std::move(geometry)); - } - - { - auto bottom = _wrap->geometryValue( - ) | rpl::map([=](QRect geometry) { - return geometry.y() - st::historyRecordLockPosition.y(); - }); - _voiceRecordBar->setLockBottom(std::move(bottom)); - } - _voiceRecordBar->updateSendButtonTypeRequests( ) | rpl::start_with_next([=] { updateSendButtonType(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 223940671..d0696bef6 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -102,6 +102,7 @@ struct ComposeControlsDescriptor { SendMenu::Type sendMenuType = {}; Window::SessionController *regularWindow = nullptr; rpl::producer stickerOrEmojiChosen; + bool voiceLockFromBottom = false; ChatHelpers::ComposeFeatures features; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index b80310d3b..63e4f6ee8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -27,17 +27,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio_capture.h" #include "media/player/media_player_button.h" #include "media/player/media_player_instance.h" -#include "styles/style_chat.h" -#include "styles/style_layers.h" -#include "styles/style_media_player.h" #include "ui/controls/send_button.h" #include "ui/effects/animation_value.h" #include "ui/effects/ripple_animation.h" #include "ui/text/format_values.h" #include "ui/painter.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" +#include "styles/style_media_player.h" namespace HistoryView::Controls { - namespace { using SendActionUpdate = VoiceRecordBar::SendActionUpdate; @@ -206,6 +206,7 @@ class ListenWrap final { public: ListenWrap( not_null parent, + const style::RecordBar &st, not_null session, ::Media::Capture::Result &&data, const style::font &font); @@ -231,12 +232,12 @@ private: not_null _parent; + const style::RecordBar &_st; const not_null _session; const not_null _document; const std::unique_ptr _voiceData; const std::shared_ptr _mediaView; const std::unique_ptr<::Media::Capture::Result> _data; - const style::IconButton &_stDelete; const base::unique_qptr _delete; const style::font &_durationFont; const QString _duration; @@ -264,17 +265,18 @@ private: ListenWrap::ListenWrap( not_null parent, + const style::RecordBar &st, not_null session, ::Media::Capture::Result &&data, const style::font &font) : _parent(parent) +, _st(st) , _session(session) , _document(DummyDocument(&session->data())) , _voiceData(ProcessCaptureResult(data)) , _mediaView(_document->createMediaView()) , _data(std::make_unique<::Media::Capture::Result>(std::move(data))) -, _stDelete(st::historyRecordDelete) -, _delete(base::make_unique_q(parent, _stDelete)) +, _delete(base::make_unique_q(parent, _st.remove)) , _durationFont(font) , _duration(Ui::FormatDurationText( float64(_data->samples) / ::Media::Player::kDefaultFrequency)) @@ -299,7 +301,7 @@ void ListenWrap::init() { _waveformBgRect = QRect({ 0, 0 }, size) .marginsRemoved(st::historyRecordWaveformBgMargins); { - const auto m = _stDelete.width + _waveformBgRect.height() / 2; + const auto m = _st.remove.width + _waveformBgRect.height() / 2; _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved( style::margins(m, 0, m, 0)); } @@ -319,22 +321,23 @@ void ListenWrap::init() { PainterHighQualityEnabler hq(p); const auto progress = _showProgress.current(); p.setOpacity(progress); + const auto &remove = _st.remove; if (progress > 0. && progress < 1.) { - _stDelete.icon.paint(p, _stDelete.iconPosition, _parent->width()); + remove.icon.paint(p, remove.iconPosition, _parent->width()); } { const auto hideOffset = _isShowAnimation ? 0 : anim::interpolate(kHideWaveformBgOffset, 0, progress); - const auto deleteIconLeft = _stDelete.iconPosition.x(); + const auto deleteIconLeft = remove.iconPosition.x(); const auto bgRectRight = anim::interpolate( deleteIconLeft, - _stDelete.width, + remove.width, _isShowAnimation ? progress : 1.); const auto bgRectLeft = anim::interpolate( _parent->width() - deleteIconLeft - _waveformBgRect.height(), - _stDelete.width, + remove.width, _isShowAnimation ? progress : 1.); const auto bgRectMargins = style::margins( bgRectLeft - hideOffset, @@ -357,7 +360,7 @@ void ListenWrap::init() { p.setOpacity(progress); } p.setPen(Qt::NoPen); - p.setBrush(st::historyRecordCancelActive); + p.setBrush(_st.cancelActive); QPainterPath path; path.setFillRule(Qt::WindingFill); path.addEllipse(bgLeftCircleRect); @@ -605,10 +608,13 @@ rpl::lifetime &ListenWrap::lifetime() { class RecordLock final : public Ui::RippleButton { public: - RecordLock(not_null parent); + RecordLock( + not_null parent, + const style::RecordBarLock &st); void requestPaintProgress(float64 progress); void requestPaintLockToStopProgress(float64 progress); + void setVisibleTopPart(int part); [[nodiscard]] rpl::producer<> locks() const; [[nodiscard]] bool isLocked() const; @@ -627,6 +633,7 @@ private: void setProgress(float64 progress); void startLockingAnimation(float64 to); + const style::RecordBarLock &_st; const QRect _rippleRect; const QPen _arcPen; @@ -634,10 +641,15 @@ private: float64 _lockToStopProgress = 0.; rpl::variable _progress = 0.; + int _visibleTopPart = -1; + }; -RecordLock::RecordLock(not_null parent) -: RippleButton(parent, st::defaultRippleAnimation) +RecordLock::RecordLock( + not_null parent, + const style::RecordBarLock &st) +: RippleButton(parent, st.ripple) +, _st(st) , _rippleRect(QRect( 0, 0, @@ -653,6 +665,10 @@ RecordLock::RecordLock(not_null parent) init(); } +void RecordLock::setVisibleTopPart(int part) { + _visibleTopPart = part; +} + void RecordLock::init() { shownValue( ) | rpl::start_with_next([=](bool shown) { @@ -670,7 +686,13 @@ void RecordLock::init() { paintRequest( ) | rpl::start_with_next([=](const QRect &clip) { + if (!_visibleTopPart) { + return; + } Painter p(this); + if (_visibleTopPart > 0 && _visibleTopPart < height()) { + p.setClipRect(0, 0, width(), _visibleTopPart); + } if (isLocked()) { const auto top = anim::interpolate( 0, @@ -687,12 +709,12 @@ void RecordLock::init() { void RecordLock::drawProgress(Painter &p) { const auto progress = _progress.current(); - const auto &originTop = st::historyRecordLockTop; - const auto &originBottom = st::historyRecordLockBottom; - const auto &originBody = st::historyRecordLockBody; - const auto &shadowTop = st::historyRecordLockTopShadow; - const auto &shadowBottom = st::historyRecordLockBottomShadow; - const auto &shadowBody = st::historyRecordLockBodyShadow; + const auto &originTop = _st.originTop; + const auto &originBottom = _st.originBottom; + const auto &originBody = _st.originBody; + const auto &shadowTop = _st.shadowTop; + const auto &shadowBottom = _st.shadowBottom; + const auto &shadowBody = _st.shadowBody; const auto &shadowMargins = st::historyRecordLockMargin; const auto bottomMargin = anim::interpolate( @@ -739,7 +761,7 @@ void RecordLock::drawProgress(Painter &p) { originBody.fill(p, content); } { - const auto &arrow = st::historyRecordLockArrow; + const auto &arrow = _st.arrow; const auto arrowRect = QRect( inner.x(), content.y() + content.height() - arrow.height() / 2, @@ -791,7 +813,7 @@ void RecordLock::drawProgress(Painter &p) { PainterHighQualityEnabler hq(p); p.translate(inner.topLeft() + lockTranslation); p.setPen(Qt::NoPen); - p.setBrush(st::historyRecordLockIconFg); + p.setBrush(_st.fg); p.drawRoundedRect(blockRect, xRadius, 3); } else { // Paint an animation frame. @@ -844,7 +866,7 @@ void RecordLock::drawProgress(Painter &p) { p.drawImage( inner.topLeft(), - style::colorizeImage(frame, st::historyRecordLockIconFg)); + style::colorizeImage(frame, _st.fg)); } } } @@ -914,7 +936,10 @@ QPoint RecordLock::prepareRippleStartPosition() const { class CancelButton final : public Ui::RippleButton { public: - CancelButton(not_null parent, int height); + CancelButton( + not_null parent, + const style::RecordBar &st, + int height); void requestPaintProgress(float64 progress); @@ -925,6 +950,7 @@ protected: private: void init(); + const style::RecordBar &_st; const int _width; const QRect _rippleRect; @@ -934,8 +960,12 @@ private: }; -CancelButton::CancelButton(not_null parent, int height) -: Ui::RippleButton(parent, st::defaultLightButton.ripple) +CancelButton::CancelButton( + not_null parent, + const style::RecordBar &st, + int height) +: Ui::RippleButton(parent, st.cancelRipple) +, _st(st) , _width(st::historyRecordCancelButtonWidth) , _rippleRect(QRect(0, (height - _width) / 2, _width, _width)) , _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now)) { @@ -958,7 +988,7 @@ void CancelButton::init() { paintRipple(p, _rippleRect.x(), _rippleRect.y()); - p.setPen(st::historyRecordCancelButtonFg); + p.setPen(_st.cancelActive); _text.draw( p, 0, @@ -983,24 +1013,23 @@ void CancelButton::requestPaintProgress(float64 progress) { VoiceRecordBar::VoiceRecordBar( not_null parent, - not_null sectionWidget, - std::shared_ptr show, - std::shared_ptr send, - int recorderHeight) + VoiceRecordBarDescriptor &&descriptor) : RpWidget(parent) -, _sectionWidget(sectionWidget) -, _show(std::move(show)) -, _send(send) -, _lock(std::make_unique(sectionWidget)) -, _level(std::make_unique(sectionWidget)) -, _cancel(std::make_unique(this, recorderHeight)) +, _st(descriptor.stOverride ? *descriptor.stOverride : st::defaultRecordBar) +, _outerContainer(descriptor.outerContainer) +, _show(std::move(descriptor.show)) +, _send(std::move(descriptor.send)) +, _lock(std::make_unique(_outerContainer, _st.lock)) +, _level(std::make_unique(_outerContainer, _st)) +, _cancel(std::make_unique(this, _st, descriptor.recorderHeight)) , _startTimer([=] { startRecording(); }) , _message( st::historyRecordTextStyle, tr::lng_record_cancel(tr::now), TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto }) +, _lockFromBottom(descriptor.lockFromBottom) , _cancelFont(st::historyRecordFont) { - resize(QSize(parent->width(), recorderHeight)); + resize(QSize(parent->width(), descriptor.recorderHeight)); init(); hideFast(); } @@ -1010,7 +1039,12 @@ VoiceRecordBar::VoiceRecordBar( std::shared_ptr show, std::shared_ptr send, int recorderHeight) -: VoiceRecordBar(parent, parent, std::move(show), send, recorderHeight) { +: VoiceRecordBar(parent, { + .outerContainer = parent, + .show = std::move(show), + .send = std::move(send), + .recorderHeight = recorderHeight, +}) { } VoiceRecordBar::~VoiceRecordBar() { @@ -1040,14 +1074,32 @@ void VoiceRecordBar::updateMessageGeometry() { } void VoiceRecordBar::updateLockGeometry() { - const auto right = anim::interpolate( - -_lock->width(), - st::historyRecordLockPosition.x(), - _showLockAnimation.value(_lockShowing.current() ? 1. : 0.)); - _lock->moveToRight(right, _lock->y()); + const auto parent = parentWidget(); + const auto me = Ui::MapFrom(_outerContainer, parent, geometry()); + const auto finalTop = me.y() + - st::historyRecordLockPosition.y() + - _lock->height(); + const auto finalRight = _outerContainer->width() + - (me.x() + me.width()) + + st::historyRecordLockPosition.x(); + const auto progress = _showLockAnimation.value( + _lockShowing.current() ? 1. : 0.); + if (_lockFromBottom) { + const auto top = anim::interpolate(me.y(), finalTop, progress); + _lock->moveToRight(finalRight, top); + _lock->setVisibleTopPart(me.y() - top); + } else { + const auto from = -_lock->width(); + const auto right = anim::interpolate(from, finalRight, progress); + _lock->moveToRight(right, finalTop); + } } void VoiceRecordBar::init() { + if (_st.radius > 0) { + _backgroundRect.emplace(_st.radius, _st.bg); + } + // Keep VoiceRecordBar behind SendButton. rpl::single( ) | rpl::then( @@ -1087,7 +1139,6 @@ void VoiceRecordBar::init() { } _cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0); updateMessageGeometry(); - updateLockGeometry(); }, lifetime()); paintRequest( @@ -1096,7 +1147,11 @@ void VoiceRecordBar::init() { if (_showAnimation.animating()) { p.setOpacity(showAnimationRatio()); } - p.fillRect(clip, st::historyComposeAreaBg); + if (_backgroundRect) { + _backgroundRect->paint(p, rect()); + } else { + p.fillRect(clip, _st.bg); + } p.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio())); const auto opacity = p.opacity(); @@ -1237,6 +1292,9 @@ void VoiceRecordBar::init() { _cancel->setClickedCallback([=] { hideAnimated(); }); + + initLockGeometry(); + initLevelGeometry(); } void VoiceRecordBar::activeAnimate(bool active) { @@ -1278,22 +1336,25 @@ void VoiceRecordBar::setStartRecordingFilter(Fn &&callback) { _startRecordingFilter = std::move(callback); } -void VoiceRecordBar::setLockBottom(rpl::producer &&bottom) { +void VoiceRecordBar::initLockGeometry() { rpl::combine( - std::move(bottom), - _lock->sizeValue() | rpl::map_to(true) // Dummy value. - ) | rpl::start_with_next([=](int value, bool dummy) { - _lock->moveToLeft(_lock->x(), value - _lock->height()); + _lock->heightValue(), + geometryValue(), + static_cast(parentWidget())->geometryValue() + ) | rpl::start_with_next([=] { + updateLockGeometry(); }, lifetime()); } -void VoiceRecordBar::setSendButtonGeometryValue( - rpl::producer &&geometry) { - std::move( - geometry - ) | rpl::start_with_next([=](QRect r) { - const auto center = (r.width() - _level->width()) / 2; - _level->moveToLeft(r.x() + center, r.y() + center); +void VoiceRecordBar::initLevelGeometry() { + rpl::combine( + _send->geometryValue(), + geometryValue(), + static_cast(parentWidget())->geometryValue() + ) | rpl::start_with_next([=](QRect send, QRect me, QRect parent) { + const auto mapped = Ui::MapFrom(_outerContainer, this, send); + const auto center = (send.width() - _level->width()) / 2; + _level->moveToLeft(mapped.x() + center, mapped.y() + center); }, lifetime()); } @@ -1431,6 +1492,7 @@ void VoiceRecordBar::stopRecording(StopType type) { } else if (type == StopType::Listen) { _listen = std::make_unique( this, + _st, &_show->session(), std::move(data), _cancelFont); @@ -1444,7 +1506,7 @@ void VoiceRecordBar::stopRecording(StopType type) { void VoiceRecordBar::drawDuration(Painter &p) { const auto duration = FormatVoiceDuration(_recordingSamples); p.setFont(_cancelFont); - p.setPen(st::historyRecordDurationFg); + p.setPen(_st.durationFg); p.drawText(_durationRect, style::al_left, duration); } @@ -1478,11 +1540,7 @@ void VoiceRecordBar::drawRedCircle(Painter &p) { } void VoiceRecordBar::drawMessage(Painter &p, float64 recordActive) { - p.setPen( - anim::pen( - st::historyRecordCancel, - st::historyRecordCancelActive, - 1. - recordActive)); + p.setPen(anim::pen(_st.cancel, _st.cancelActive, 1. - recordActive)); const auto opacity = p.opacity(); p.setOpacity(opacity * (1. - _lock->lockToStopProgress())); @@ -1621,8 +1679,8 @@ void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) { void VoiceRecordBar::orderControls() { stackUnder(_send.get()); - _level->raise(); _lock->raise(); + _level->raise(); } void VoiceRecordBar::installListenStateFilter() { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index ea48e887a..97fed00d9 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -11,10 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "history/view/controls/compose_controls_common.h" #include "ui/effects/animations.h" +#include "ui/round_rect.h" #include "ui/rp_widget.h" struct VoiceData; +namespace style { +struct RecordBar; +} // namespace style + namespace Ui { class SendButton; } // namespace Ui @@ -34,6 +39,15 @@ class ListenWrap; class RecordLock; class CancelButton; +struct VoiceRecordBarDescriptor { + not_null outerContainer; + std::shared_ptr show; + std::shared_ptr send; + const style::RecordBar *stOverride = nullptr; + int recorderHeight = 0; + bool lockFromBottom = false; +}; + class VoiceRecordBar final : public Ui::RpWidget { public: using SendActionUpdate = Controls::SendActionUpdate; @@ -41,10 +55,7 @@ public: VoiceRecordBar( not_null parent, - not_null sectionWidget, - std::shared_ptr show, - std::shared_ptr send, - int recorderHeight); + VoiceRecordBarDescriptor &&descriptor); VoiceRecordBar( not_null parent, std::shared_ptr show, @@ -75,8 +86,6 @@ public: void requestToSendWithOptions(Api::SendOptions options); - void setLockBottom(rpl::producer &&bottom); - void setSendButtonGeometryValue(rpl::producer &&geometry); void setStartRecordingFilter(Fn &&callback); [[nodiscard]] bool isRecording() const; @@ -93,6 +102,8 @@ private: }; void init(); + void initLockGeometry(); + void initLevelGeometry(); void updateMessageGeometry(); void updateLockGeometry(); @@ -125,7 +136,8 @@ private: void computeAndSetLockProgress(QPoint globalPos); - const not_null _sectionWidget; + const style::RecordBar &_st; + const not_null _outerContainer; const std::shared_ptr _show; const std::shared_ptr _send; const std::unique_ptr _lock; @@ -159,11 +171,13 @@ private: rpl::event_stream<> _recordingTipRequests; bool _recordingTipRequired = false; + bool _lockFromBottom = false; const style::font &_cancelFont; rpl::lifetime _recordingLifetime; + std::optional _backgroundRect; Ui::Animations::Simple _showLockAnimation; Ui::Animations::Simple _lockToStopAnimation; Ui::Animations::Simple _showListenAnimation; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index f05b3ed44..71a652f4d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -47,8 +47,11 @@ auto Blobs() { } // namespace -VoiceRecordButton::VoiceRecordButton(not_null parent) +VoiceRecordButton::VoiceRecordButton( + not_null parent, + const style::RecordBar &st) : AbstractButton(parent) +, _st(st) , _blobs(std::make_unique( Blobs(), kLevelDuration, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index 750c8abfb..fc477b485 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -11,17 +11,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/rp_widget.h" -namespace Ui { -namespace Paint { +namespace style { +struct RecordBar; +} // namespace style + +namespace Ui::Paint { class Blobs; -} // namespace Paint -} // namespace Ui +} // namespace Ui::Paint namespace HistoryView::Controls { class VoiceRecordButton final : public Ui::AbstractButton { public: - explicit VoiceRecordButton(not_null parent); + VoiceRecordButton( + not_null parent, + const style::RecordBar &st); ~VoiceRecordButton(); enum class Type { @@ -43,6 +47,7 @@ public: private: void init(); + const style::RecordBar &_st; std::unique_ptr _blobs; crl::time _lastUpdateTime = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 1b44b8fa2..1f6aca80a 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -35,6 +35,7 @@ ReplyArea::ReplyArea(not_null controller) .mode = HistoryView::ComposeControlsMode::Normal, .sendMenuType = SendMenu::Type::SilentOnly, .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), + .voiceLockFromBottom = true, .features = { .sendAs = false, .ttlInfo = false, diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index aa9377f09..98ff6dd4f 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -448,24 +448,23 @@ storiesComposeWhiteText: groupCallMembersFg; storiesComposeGrayText: groupCallMemberNotJoinedStatus; storiesComposeGrayIcon: groupCallMemberInactiveIcon; storiesComposeBlue: groupCallActiveFg; +storiesComposeRippleLight: RippleAnimation(defaultRippleAnimation) { + color: storiesComposeBgOver; +} storiesComposeRipple: RippleAnimation(defaultRippleAnimation) { color: groupCallMembersBgRipple; } storiesAttach: IconButton(historyAttach) { icon: icon {{ "chat/input_attach", storiesComposeGrayIcon }}; iconOver: icon {{ "chat/input_attach", storiesComposeGrayIcon }}; - ripple: RippleAnimation(defaultRippleAnimation) { - color: storiesComposeBgOver; - } + ripple: storiesComposeRippleLight; } storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRemoveSet: IconButton(stickerPanRemoveSet) { icon: icon {{ "simple_close", storiesComposeGrayIcon }}; iconOver: icon {{ "simple_close", storiesComposeGrayIcon }}; - ripple: RippleAnimation(defaultRippleAnimation) { - color: storiesComposeBgOver; - } + ripple: storiesComposeRippleLight; } storiesMenu: Menu(defaultMenu) { itemBg: groupCallMenuBg; @@ -641,4 +640,25 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } autocompleteBottomSkip: 10px; } + record: RecordBar(defaultRecordBar) { + bg: storiesComposeBg; + durationFg: storiesComposeWhiteText; + radius: storiesRadius; + cancel: storiesComposeGrayText; + cancelActive: storiesComposeBlue; + cancelRipple: storiesComposeRippleLight; + lock: RecordBarLock(defaultRecordBarLock) { + ripple: storiesComposeRipple; + originTop: icon {{ "voice_lock/record_lock_top", storiesComposeBg }}; + originBottom: icon {{ "voice_lock/record_lock_bottom", storiesComposeBg }}; + originBody: icon {{ "voice_lock/record_lock_body", storiesComposeBg }}; + arrow: icon {{ "voice_lock/voice_arrow", storiesComposeGrayIcon }}; + fg: storiesComposeGrayIcon; + } + remove: IconButton(storiesAttach) { + icon: icon {{ "info/info_media_delete", storiesComposeGrayIcon }}; + iconOver: icon {{ "info/info_media_delete", storiesComposeGrayIcon }}; + iconPosition: point(10px, 11px); + } + } } From ae4d660c381cc5b3d6a0fb140f0fbdefdeaed112 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 18 May 2023 14:04:14 +0400 Subject: [PATCH 024/260] Improve stories controls geometry constraints. --- .../chat_helpers/chat_helpers.style | 4 ++ .../chat_helpers/emoji_suggestions_widget.cpp | 1 + .../chat_helpers/field_autocomplete.cpp | 9 ++++- .../history_view_compose_controls.cpp | 4 ++ .../stories/media_stories_controller.cpp | 37 +++++++++++++++---- .../media/stories/media_stories_reply.cpp | 6 +-- .../media/stories/media_stories_sibling.cpp | 16 ++++---- .../SourceFiles/media/view/media_view.style | 5 ++- .../media/view/media_view_overlay_widget.cpp | 1 + 9 files changed, 60 insertions(+), 23 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 3d40ef02d..4e03be93a 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -172,6 +172,8 @@ ComposeControls { emoji: EmojiButton; suggestions: EmojiSuggestions; tabbed: EmojiPan; + tabbedHeightMin: pixels; + tabbedHeightMax: pixels; record: RecordBar; } @@ -982,5 +984,7 @@ defaultComposeControls: ComposeControls { emoji: historyAttachEmoji; suggestions: defaultEmojiSuggestions; tabbed: defaultEmojiPan; + tabbedHeightMin: emojiPanMinHeight; + tabbedHeightMax: emojiPanMaxHeight; record: defaultRecordBar; } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index ccd235355..7233c5a21 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -866,6 +866,7 @@ void SuggestionsController::showWithQuery(SuggestionsQuery query) { const auto force = base::take(_keywordsRefreshed); _lastShownQuery = query; _suggestions->showWithQuery(_lastShownQuery, force); + _container->resizeToContent(); } SuggestionsQuery SuggestionsController::getEmojiQuery() { diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 6f312926f..7f5c7a52e 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -693,9 +693,14 @@ void FieldAutocomplete::recount(bool resetScroll) { if (h > _boundings.height()) h = _boundings.height(); if (h > maxh) h = maxh; if (width() != _boundings.width() || height() != h) { - setGeometry(_boundings.x(), _boundings.y() + _boundings.height() - h, _boundings.width(), h); + setGeometry( + _boundings.x(), + _boundings.y() + _boundings.height() - h, + _boundings.width(), + h); _scroll->resize(_boundings.width(), h); - } else if (y() != _boundings.y() + _boundings.height() - h) { + } else if (x() != _boundings.x() + || y() != _boundings.y() + _boundings.height() - h) { move(_boundings.x(), _boundings.y() + _boundings.height() - h); } if (resetScroll) st = 0; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 50fe6c136..29f1736ae 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -2596,6 +2596,10 @@ void ComposeControls::createTabbedPanel() { setTabbedPanel(std::make_unique( _parent, std::move(descriptor))); + _tabbedPanel->setDesiredHeightValues( + st::emojiPanHeightRatio, + _st.tabbedHeightMin, + _st.tabbedHeightMax); } void ComposeControls::setTabbedPanel( diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index a0ff10a86..f46f37304 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -31,7 +31,8 @@ namespace { constexpr auto kPhotoProgressInterval = crl::time(100); constexpr auto kPhotoDuration = 5 * crl::time(1000); constexpr auto kFullContentFade = 0.35; -constexpr auto kSiblingMultiplier = 0.448; +constexpr auto kSiblingMultiplierDefault = 0.448; +constexpr auto kSiblingMultiplierMax = 0.72; constexpr auto kSiblingOutsidePart = 0.24; constexpr auto kSiblingUserpicSize = 0.3; constexpr auto kInnerHeightMultiplier = 1.6; @@ -217,12 +218,30 @@ void Controller::initLayout() { layout.controlsWidth, layout.controlsBottomPosition.y()); - const auto siblingSize = layout.content.size() * kSiblingMultiplier; + const auto sidesAvailable = size.width() - layout.content.width(); + const auto widthForSiblings = sidesAvailable + - 2 * st::storiesFieldMargin.bottom(); + const auto siblingWidthMax = widthForSiblings + / (2 * (1. - kSiblingOutsidePart)); + const auto siblingMultiplierMax = std::max( + kSiblingMultiplierDefault, + st::storiesSiblingWidthMin / float64(layout.content.width())); + const auto siblingMultiplier = std::min({ + siblingMultiplierMax, + kSiblingMultiplierMax, + siblingWidthMax / layout.content.width(), + }); + const auto siblingSize = layout.content.size() * siblingMultiplier; const auto siblingTop = (size.height() - siblingSize.height()) / 2; - const auto outside = int(base::SafeRound( + const auto outsideMax = int(base::SafeRound( siblingSize.width() * kSiblingOutsidePart)); - const auto xLeft = -outside; - const auto xRight = size.width() - siblingSize.width() + outside; + const auto leftAvailable = layout.content.x() - siblingSize.width(); + const auto xDesired = leftAvailable / 3; + const auto xPossible = std::min( + xDesired, + (leftAvailable - st::storiesControlSize)); + const auto xLeft = std::max(xPossible, -outsideMax); + const auto xRight = size.width() - siblingSize.width() - xLeft; const auto userpicSize = int(base::SafeRound( siblingSize.width() * kSiblingUserpicSize)); const auto innerHeight = userpicSize * kInnerHeightMultiplier; @@ -234,11 +253,13 @@ void Controller::initLayout() { userpicSize ).translated(geometry.topLeft()); }; - const auto nameFontSize = st::storiesMaxNameFontSize * contentHeight - / st::storiesMaxSize.height(); + const auto nameFontSize = std::max( + (st::storiesMaxNameFontSize * contentHeight + / st::storiesMaxSize.height()), + st::fsize); const auto nameBoundingRect = [&](QRect geometry, bool left) { const auto skipSmall = nameFontSize; - const auto skipBig = skipSmall + outside; + const auto skipBig = skipSmall - std::min(xLeft, 0); const auto top = userpic(geometry).y() + innerHeight; return QRect( left ? skipBig : skipSmall, diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 1f6aca80a..49b2e70a3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -65,16 +65,16 @@ void ReplyArea::initGeometry() { _controls->height() ) | rpl::start_with_next([=](const Layout &layout, int height) { const auto content = layout.content; - _controls->resizeToWidth(content.width()); + _controls->resizeToWidth(layout.controlsWidth); if (_controls->heightCurrent() == height) { const auto position = layout.controlsBottomPosition - QPoint(0, height); _controls->move(position.x(), position.y()); const auto &tabbed = st::storiesComposeControls.tabbed; const auto upper = QRect( - content.x(), + position.x(), content.y(), - content.width(), + layout.controlsWidth, (position.y() + tabbed.autocompleteBottomSkip - content.y())); diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index 05e66297b..a37b4a472 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -338,18 +338,16 @@ QPoint Sibling::namePosition( const QImage &image) const { const auto size = image.size() / image.devicePixelRatio(); const auto width = size.width(); + const auto bounding = layout.nameBoundingRect; const auto left = layout.geometry.x() + (layout.geometry.width() - width) / 2; - if (left < layout.nameBoundingRect.x()) { - return layout.nameBoundingRect.topLeft(); - } else if (left + width > layout.nameBoundingRect.x() + layout.nameBoundingRect.width()) { - return layout.nameBoundingRect.topLeft() - + QPoint(layout.nameBoundingRect.width() - width, 0); + const auto top = bounding.y() + bounding.height() - size.height(); + if (left < bounding.x()) { + return { bounding.x(), top }; + } else if (left + width > bounding.x() + bounding.width()) { + return { bounding.x() + bounding.width() - width, top }; } - const auto top = layout.nameBoundingRect.y() - + layout.nameBoundingRect.height() - - size.height(); - return QPoint(left, top); + return { left, top }; } void Sibling::check() { diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 98ff6dd4f..c97645659 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -408,6 +408,7 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); storiesMaxSize: size(540px, 960px); +storiesSiblingWidthMin: 200px; // Try making sibling not less than this. storiesMaxNameFontSize: 17px; storiesRadius: 8px; storiesControlSize: 64px; @@ -433,7 +434,7 @@ storiesHeaderDate: FlatLabel(defaultFlatLabel) { storiesHeaderDatePosition: point(50px, 17px); storiesShadowTop: icon{{ "mediaview/shadow_bottom-flip_vertical", windowShadowFg }}; storiesShadowBottom: mediaviewShadowBottom; -storiesControlsMinWidth: 200px; +storiesControlsMinWidth: 280px; storiesFieldMargin: margins(0px, 14px, 0px, 16px); storiesSideSkip: 145px; storiesCaptionFull: FlatLabel(defaultFlatLabel) { @@ -640,6 +641,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } autocompleteBottomSkip: 10px; } + tabbedHeightMin: 220px; + tabbedHeightMax: 480px; record: RecordBar(defaultRecordBar) { bg: storiesComposeBg; durationFg: storiesComposeWhiteText; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 8ff30f817..cd34868a0 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -5025,6 +5025,7 @@ void OverlayWidget::setStoriesUser(UserData *user) { updateControlsGeometry(); }, _stories->lifetime()); _storiesChanged.fire({}); + _dropdown->raise(); } } From 2c5d990e1c1900357b7a03c4c892d0fcc40fc5fe Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 18 May 2023 17:46:24 +0400 Subject: [PATCH 025/260] Implement full theming of attachments in stories. --- Telegram/SourceFiles/boxes/boxes.style | 45 --- .../SourceFiles/boxes/edit_caption_box.cpp | 15 +- .../SourceFiles/boxes/premium_limits_box.cpp | 29 +- .../SourceFiles/boxes/premium_limits_box.h | 13 +- .../SourceFiles/boxes/premium_preview_box.cpp | 130 ++++---- .../SourceFiles/boxes/premium_preview_box.h | 9 + Telegram/SourceFiles/boxes/send_files_box.cpp | 156 ++++++---- Telegram/SourceFiles/boxes/send_files_box.h | 50 ++- .../calls/group/calls_group_panel.cpp | 2 +- .../chat_helpers/chat_helpers.style | 103 +++++++ .../chat_helpers/compose/compose_show.h | 2 + .../chat_helpers/emoji_list_widget.cpp | 1 + .../chat_helpers/gifs_list_widget.cpp | 1 + .../chat_helpers/stickers_list_widget.cpp | 13 +- .../SourceFiles/chat_helpers/tabbed_panel.h | 2 +- .../controllers/stickers_panel_controller.cpp | 24 +- .../controllers/stickers_panel_controller.h | 7 +- Telegram/SourceFiles/editor/photo_editor.cpp | 24 +- Telegram/SourceFiles/editor/photo_editor.h | 12 + .../editor/photo_editor_layer_widget.cpp | 7 +- .../editor/photo_editor_layer_widget.h | 10 +- .../SourceFiles/history/history_widget.cpp | 3 +- .../view/history_view_replies_section.cpp | 3 +- .../view/history_view_scheduled_section.cpp | 3 +- .../media/stories/media_stories_reply.cpp | 290 +++++++++++++++++- .../media/stories/media_stories_reply.h | 48 +++ .../SourceFiles/media/view/media_view.style | 87 +++++- .../media/view/media_view_overlay_widget.cpp | 161 +++++----- .../media/view/media_view_overlay_widget.h | 2 + .../SourceFiles/settings/settings_premium.cpp | 18 +- .../SourceFiles/settings/settings_premium.h | 7 +- .../SourceFiles/storage/localimageloader.cpp | 2 +- .../attach_abstract_single_file_preview.cpp | 25 +- .../attach_abstract_single_file_preview.h | 10 +- .../attach_abstract_single_media_preview.cpp | 14 +- .../attach_abstract_single_media_preview.h | 10 +- .../ui/chat/attach/attach_album_preview.cpp | 3 + .../ui/chat/attach/attach_album_preview.h | 6 + .../ui/chat/attach/attach_album_thumbnail.cpp | 26 +- .../ui/chat/attach/attach_album_thumbnail.h | 6 + .../ui/chat/attach/attach_controls.cpp | 2 +- .../attach_item_single_file_preview.cpp | 3 +- .../attach/attach_item_single_file_preview.h | 1 + .../attach_item_single_media_preview.cpp | 3 +- .../attach/attach_item_single_media_preview.h | 1 + .../attach/attach_single_file_preview.cpp | 3 +- .../chat/attach/attach_single_file_preview.h | 1 + .../attach/attach_single_media_preview.cpp | 5 +- .../chat/attach/attach_single_media_preview.h | 2 + Telegram/SourceFiles/ui/effects/premium.style | 27 +- .../ui/effects/premium_graphics.cpp | 19 +- .../SourceFiles/ui/effects/premium_graphics.h | 7 + .../window/window_session_controller.cpp | 13 +- 53 files changed, 1113 insertions(+), 353 deletions(-) diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 54f915a45..fb0a19ef5 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -344,11 +344,6 @@ autoDownloadLimitSlider: MediaSlider(defaultContinuousSlider) { } autoDownloadLimitPadding: margins(22px, 8px, 22px, 8px); -confirmCaptionArea: InputField(defaultInputField) { - textMargins: margins(1px, 26px, 31px, 4px); - heightMax: 158px; -} -confirmBg: windowBgOver; confirmMaxHeight: 245px; supportInfoField: InputField(defaultInputField) { @@ -391,51 +386,11 @@ sendMediaPreviewSize: 308px; sendMediaPreviewHeightMax: 1280; sendMediaRowSkip: 10px; -editMediaButtonSize: 32px; - -editMediaButtonIconFile: icon {{ "send_media/send_media_replace", menuIconFg }}; -editMediaButton: IconButton(defaultIconButton) { - width: editMediaButtonSize; - height: editMediaButtonSize; - - icon: editMediaButtonIconFile; - - rippleAreaSize: editMediaButtonSize; - ripple: defaultRippleAnimation; -} - editMediaHintLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; minWidth: sendMediaPreviewSize; } -// SendFilesBox - -sendBoxAlbumGroupEditInternalSkip: 8px; -sendBoxAlbumGroupSkipRight: 5px; -sendBoxAlbumGroupSkipTop: 5px; -sendBoxAlbumGroupRadius: 4px; -sendBoxAlbumGroupSize: size(62px, 25px); -sendBoxAlbumSmallGroupSize: size(30px, 25px); - -sendBoxFileGroupSkipTop: 2px; -sendBoxFileGroupSkipRight: 5px; -sendBoxFileGroupEditInternalSkip: -1px; - -sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgRipple; - } -} -sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; -sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_delete", menuIconFg }}; - -sendBoxAlbumButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg }}; -sendBoxAlbumGroupButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg, point(4px, 1px) }}; -sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_delete", roundedFg }}; - -// End of SendFilesBox - calendarTitleHeight: boxTitleHeight; calendarPrevious: IconButton { width: calendarTitleHeight; diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 41f19a0b8..c9779b1f1 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -246,12 +246,12 @@ EditCaptionBox::EditCaptionBox( , _scroll(base::make_unique_q(this, st::boxScroll)) , _field(base::make_unique_q( this, - st::confirmCaptionArea, + st::defaultComposeFiles.caption, Ui::InputField::Mode::MultiLine, tr::lng_photo_caption())) , _emojiToggle(base::make_unique_q( this, - st::boxAttachEmoji)) + st::defaultComposeFiles.emoji)) , _initialText(std::move(text)) , _initialList(std::move(list)) , _saved(std::move(saved)) { @@ -402,6 +402,7 @@ void EditCaptionBox::rebuildPreview() { if (photo || document->isVideoFile() || document->isAnimation()) { const auto media = Ui::CreateChild( this, + st::defaultComposeControls, gifPaused, _historyItem, Ui::AttachControls::Type::EditOnly); @@ -410,6 +411,7 @@ void EditCaptionBox::rebuildPreview() { } else { _content.reset(Ui::CreateChild( this, + st::defaultComposeControls, _historyItem, Ui::AttachControls::Type::EditOnly)); } @@ -418,6 +420,7 @@ void EditCaptionBox::rebuildPreview() { const auto media = Ui::SingleMediaPreview::Create( this, + st::defaultComposeControls, gifPaused, file, Ui::AttachControls::Type::EditOnly); @@ -429,6 +432,7 @@ void EditCaptionBox::rebuildPreview() { } else { _content.reset(Ui::CreateChild( this, + st::defaultComposeControls, file, Ui::AttachControls::Type::EditOnly)); } @@ -482,7 +486,7 @@ void EditCaptionBox::setupField() { _field->setSubmitSettings( Core::App().settings().sendSubmitWay()); - _field->setMaxHeight(st::confirmCaptionArea.heightMax); + _field->setMaxHeight(st::defaultComposeFiles.caption.heightMax); connect(_field, &Ui::InputField::submitted, [=] { save(); }); connect(_field, &Ui::InputField::cancelled, [=] { closeBox(); }); @@ -596,7 +600,7 @@ void EditCaptionBox::setupPhotoEditorEventHandler() { if (!_preparedList.files.empty()) { Editor::OpenWithPreparedFile( this, - controller, + controller->uiShow(), &_preparedList.files.front(), st::sendMediaPreviewSize, [=] { rebuildPreview(); }); @@ -845,7 +849,8 @@ bool EditCaptionBox::validateLength(const QString &text) const { if (remove <= 0) { return true; } - _controller->show(Box(CaptionLimitReachedBox, session, remove)); + _controller->show( + Box(CaptionLimitReachedBox, session, remove, nullptr)); return false; } diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 55fbe0a35..3fbdc34c1 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -404,6 +404,7 @@ std::unique_ptr PublicsController::createRow( void SimpleLimitBox( not_null box, + const style::PremiumLimits *stOverride, not_null session, bool premiumPossible, rpl::producer title, @@ -411,6 +412,8 @@ void SimpleLimitBox( const QString &refAddition, const InfographicDescriptor &descriptor, bool fixed = false) { + const auto &st = stOverride ? *stOverride : st::defaultPremiumLimits; + box->setWidth(st::boxWideWidth); const auto top = fixed @@ -431,6 +434,7 @@ void SimpleLimitBox( if (premiumPossible) { Ui::Premium::AddLimitRow( top, + st, descriptor.premiumLimit, descriptor.phrase, 0, @@ -473,6 +477,7 @@ void SimpleLimitBox( void SimpleLimitBox( not_null box, + const style::PremiumLimits *stOverride, not_null session, rpl::producer title, rpl::producer text, @@ -481,6 +486,7 @@ void SimpleLimitBox( bool fixed = false) { SimpleLimitBox( box, + stOverride, session, session->premiumPossible(), std::move(title), @@ -524,6 +530,7 @@ void SimplePinsLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filter_pin_limit_title(), std::move(text), @@ -561,6 +568,7 @@ void ChannelsLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_channels_limit_title(), std::move(text), @@ -650,6 +658,7 @@ void PublicLinksLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_links_limit_title(), std::move(text), @@ -716,6 +725,7 @@ void FilterChatsLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_filter_chats_limit_title(), std::move(text), @@ -753,6 +763,7 @@ void FilterLinksLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_filter_links_limit_title(), std::move(text), @@ -798,6 +809,7 @@ void FiltersLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filters_limit_title(), std::move(text), @@ -836,6 +848,7 @@ void ShareableFiltersLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filter_shared_limit_title(), std::move(text), @@ -900,6 +913,7 @@ void ForumPinsLimitBox( Ui::Text::RichLangValue); SimpleLimitBox( box, + nullptr, &forum->session(), false, tr::lng_filter_pin_limit_title(), @@ -911,7 +925,8 @@ void ForumPinsLimitBox( void CaptionLimitBox( not_null box, not_null session, - int remove) { + int remove, + const style::PremiumLimits *stOverride) { const auto premium = session->premium(); const auto premiumPossible = session->premiumPossible(); @@ -943,6 +958,7 @@ void CaptionLimitBox( SimpleLimitBox( box, + stOverride, session, tr::lng_caption_limit_title(), std::move(text), @@ -953,15 +969,17 @@ void CaptionLimitBox( void CaptionLimitReachedBox( not_null box, not_null session, - int remove) { + int remove, + const style::PremiumLimits *stOverride) { Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{ .text = tr::lng_caption_limit_reached(tr::now, lt_count, remove), + .labelStyle = stOverride ? &stOverride->boxLabel : nullptr, .inform = true, }); if (!session->premium()) { box->addLeftButton(tr::lng_limits_increase(), [=] { box->getDelegate()->showBox( - Box(CaptionLimitBox, session, remove), + Box(CaptionLimitBox, session, remove, stOverride), Ui::LayerOption::KeepOther, anim::type::normal); box->closeBox(); @@ -972,7 +990,8 @@ void CaptionLimitReachedBox( void FileSizeLimitBox( not_null box, not_null session, - uint64 fileSizeBytes) { + uint64 fileSizeBytes, + const style::PremiumLimits *stOverride) { const auto limits = Data::PremiumLimits(session); const auto defaultLimit = float64(limits.uploadMaxDefault()); const auto premiumLimit = float64(limits.uploadMaxPremium()); @@ -1011,6 +1030,7 @@ void FileSizeLimitBox( SimpleLimitBox( box, + stOverride, session, premiumPossible, tr::lng_file_size_limit_title(), @@ -1084,6 +1104,7 @@ void AccountsLimitBox( if (premiumPossible) { Ui::Premium::AddLimitRow( top, + st::defaultPremiumLimits, (QString::number(std::max(current, defaultLimit) + 1) + ((current + 1 == premiumLimit) ? "" : "+")), QString::number(defaultLimit)); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.h b/Telegram/SourceFiles/boxes/premium_limits_box.h index 55be7e40f..bb9115a3e 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.h +++ b/Telegram/SourceFiles/boxes/premium_limits_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" +namespace style { +struct PremiumLimits; +} // namespace style + namespace Data { class Forum; } // namespace Data @@ -57,15 +61,18 @@ void ForumPinsLimitBox( void CaptionLimitBox( not_null box, not_null session, - int remove); + int remove, + const style::PremiumLimits *stOverride = nullptr); void CaptionLimitReachedBox( not_null box, not_null session, - int remove); + int remove, + const style::PremiumLimits *stOverride = nullptr); void FileSizeLimitBox( not_null box, not_null session, - uint64 fileSizeBytes); + uint64 fileSizeBytes, + const style::PremiumLimits *stOverride = nullptr); void AccountsLimitBox( not_null box, not_null session); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index ffc5bc709..d63288a01 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -76,7 +76,7 @@ bool operator==(const Descriptor &a, const Descriptor &b) { struct Preload { Descriptor descriptor; std::shared_ptr media; - base::weak_ptr controller; + std::weak_ptr show; }; [[nodiscard]] std::vector &Preloads() { @@ -168,7 +168,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] not_null StickerPreview( not_null parent, - not_null controller, + std::shared_ptr show, const std::shared_ptr &media, Fn readyCallback = nullptr) { using namespace HistoryView; @@ -194,6 +194,8 @@ void PreloadSticker(const std::shared_ptr &media) { struct State { std::unique_ptr lottie; std::unique_ptr effect; + style::owned_color pathFg = style::owned_color( + QColor(255, 255, 255, 64)); std::unique_ptr pathGradient; bool readyInvoked = false; }; @@ -239,15 +241,17 @@ void PreloadSticker(const std::shared_ptr &media) { }; createLottieIfReady(); if (!state->lottie || !state->effect) { - controller->session().downloaderTaskFinished( + show->session().downloaderTaskFinished( ) | rpl::take_while([=] { createLottieIfReady(); return !state->lottie || !state->effect; }) | rpl::start(result->lifetime()); } - state->pathGradient = MakePathShiftGradient( - controller->chatStyle(), - [=] { result->update(); }); + state->pathGradient = std::make_unique( + st::shadowFg, + state->pathFg.color(), + [=] { result->update(); }, + rpl::never<>()); result->paintRequest( ) | rpl::start_with_next([=] { @@ -262,7 +266,7 @@ void PreloadSticker(const std::shared_ptr &media) { if (!state->lottie || !state->lottie->ready() || !state->effect->ready()) { - p.setBrush(controller->chatStyle()->msgServiceBg()); + p.setBrush(st::shadowFg); ChatHelpers::PaintStickerThumbnailPath( p, media.get(), @@ -302,7 +306,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] not_null StickersPreview( not_null parent, - not_null controller, + std::shared_ptr show, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); result->show(); @@ -327,7 +331,7 @@ void PreloadSticker(const std::shared_ptr &media) { bool nextReady = false; int index = 0; }; - const auto premium = &controller->session().api().premium(); + const auto premium = &show->session().api().premium(); const auto state = lifetime.make_state(); const auto create = [=](std::shared_ptr media) { const auto outer = Ui::CreateChild(result); @@ -340,7 +344,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[maybe_unused]] const auto sticker = StickerPreview( outer, - controller, + show, media, state->singleReadyCallback); @@ -520,7 +524,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null VideoPreview( not_null parent, - not_null controller, + std::shared_ptr show, not_null document, bool alignToBottom, Fn readyCallback) { @@ -683,7 +687,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenericPreview( not_null parent, - not_null controller, + std::shared_ptr show, PremiumPreview section, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); @@ -699,7 +703,7 @@ struct VideoPreviewDocument { std::vector> medias; Ui::RpWidget *single = nullptr; }; - const auto session = &controller->session(); + const auto session = &show->session(); const auto state = lifetime.make_state(); const auto create = [=] { const auto document = LookupVideo(session, section); @@ -708,7 +712,7 @@ struct VideoPreviewDocument { } state->single = VideoPreview( result, - controller, + show, document, !VideoAlignToTop(section), readyCallback); @@ -724,14 +728,18 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenerateDefaultPreview( not_null parent, - not_null controller, + std::shared_ptr show, PremiumPreview section, Fn readyCallback) { switch (section) { case PremiumPreview::Stickers: - return StickersPreview(parent, controller, readyCallback); + return StickersPreview(parent, std::move(show), readyCallback); default: - return GenericPreview(parent, controller, section, readyCallback); + return GenericPreview( + parent, + std::move(show), + section, + readyCallback); } } @@ -792,7 +800,7 @@ struct VideoPreviewDocument { void PreviewBox( not_null box, - not_null controller, + std::shared_ptr show, const Descriptor &descriptor, const std::shared_ptr &media, const QImage &back) { @@ -825,7 +833,7 @@ void PreviewBox( }; const auto state = outer->lifetime().make_state(); state->selected = descriptor.section; - state->order = Settings::PremiumPreviewOrder(&controller->session()); + state->order = Settings::PremiumPreviewOrder(&show->session()); const auto index = [=](PremiumPreview section) { const auto it = ranges::find(state->order, section); @@ -880,7 +888,7 @@ void PreviewBox( }; state->stickersPreload = GenerateDefaultPreview( outer, - controller, + show, PremiumPreview::Stickers, ready); state->stickersPreload->hide(); @@ -890,13 +898,13 @@ void PreviewBox( switch (descriptor.section) { case PremiumPreview::Stickers: state->content = media - ? StickerPreview(outer, controller, media, state->preload) - : StickersPreview(outer, controller, state->preload); + ? StickerPreview(outer, show, media, state->preload) + : StickersPreview(outer, show, state->preload); break; default: state->content = GenericPreview( outer, - controller, + show, descriptor.section, state->preload); break; @@ -955,7 +963,7 @@ void PreviewBox( } else { state->content = GenerateDefaultPreview( outer, - controller, + show, now, state->preload); } @@ -1003,7 +1011,7 @@ void PreviewBox( state->preload(); } }; - if (descriptor.fromSettings && controller->session().premium()) { + if (descriptor.fromSettings && show->session().premium()) { box->setShowFinishedCallback(showFinished); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); } else { @@ -1030,16 +1038,21 @@ void PreviewBox( auto button = descriptor.fromSettings ? object_ptr::fromRaw( Settings::CreateSubscribeButton({ - controller, - box, - computeRef, + .parent = box, + .computeRef = computeRef, + .show = show, })) : CreateUnlockButton(box, std::move(unlock)); button->resizeToWidth(width); if (!descriptor.fromSettings) { button->setClickedCallback([=] { + const auto window = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (!window) { + return; + } Settings::ShowPremium( - controller, + window, Settings::LookupPremiumRef(state->selected.current())); }); } @@ -1052,7 +1065,7 @@ void PreviewBox( if (descriptor.fromSettings) { Data::AmPremiumValue( - &controller->session() + &show->session() ) | rpl::skip(1) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); @@ -1076,25 +1089,26 @@ void PreviewBox( } void Show( - not_null controller, + std::shared_ptr show, const Descriptor &descriptor, const std::shared_ptr &media, QImage back) { - const auto box = controller->show( - Box(PreviewBox, controller, descriptor, media, back)); + auto box = Box(PreviewBox, show, descriptor, media, back); + const auto raw = box.data(); + show->showBox(std::move(box)); if (descriptor.shownCallback) { - descriptor.shownCallback(box); + descriptor.shownCallback(raw); } } -void Show(not_null controller, QImage back) { +void Show(std::shared_ptr show, QImage back) { auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { - const auto already = i->controller.get(); + const auto already = i->show.lock(); if (!already) { i = list.erase(i); - } else if (already == controller) { - Show(controller, i->descriptor, i->media, back); + } else if (already == show) { + Show(std::move(show), i->descriptor, i->media, back); i = list.erase(i); return; } else { @@ -1104,21 +1118,23 @@ void Show(not_null controller, QImage back) { } void Show( - not_null controller, + std::shared_ptr show, Descriptor &&descriptor) { - if (!controller->session().premiumPossible()) { - const auto box = controller->show(Box(PremiumUnavailableBox)); + if (!show->session().premiumPossible()) { + auto box = Box(PremiumUnavailableBox); + const auto raw = box.data(); + show->showBox(std::move(box)); if (descriptor.shownCallback) { - descriptor.shownCallback(box); + descriptor.shownCallback(raw); } return; } auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { - const auto already = i->controller.get(); + const auto already = i->show.lock(); if (!already) { i = list.erase(i); - } else if (already == controller) { + } else if (already == show) { if (i->descriptor == descriptor) { return; } @@ -1135,13 +1151,13 @@ void Show( } } - const auto weak = base::make_weak(controller); + const auto weak = std::weak_ptr(show); list.push_back({ .descriptor = descriptor, .media = (descriptor.requestedSticker ? descriptor.requestedSticker->createMediaView() : nullptr), - .controller = weak, + .show = weak, }); if (const auto &media = list.back().media) { PreloadSticker(media); @@ -1166,8 +1182,8 @@ void Show( Images::CornersMask(st::boxRadius), RectPart::TopLeft | RectPart::TopRight); crl::on_main([=] { - if (const auto strong = weak.get()) { - Show(strong, result); + if (auto strong = weak.lock()) { + Show(std::move(strong), result); } }); }); @@ -1178,7 +1194,7 @@ void Show( void ShowStickerPreviewBox( not_null controller, not_null document) { - Show(controller, Descriptor{ + Show(controller->uiShow(), Descriptor{ .section = PremiumPreview::Stickers, .requestedSticker = document, }); @@ -1188,7 +1204,14 @@ void ShowPremiumPreviewBox( not_null controller, PremiumPreview section, Fn)> shown) { - Show(controller, Descriptor{ + ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown)); +} + +void ShowPremiumPreviewBox( + std::shared_ptr show, + PremiumPreview section, + Fn)> shown) { + Show(std::move(show), Descriptor{ .section = section, .shownCallback = std::move(shown), }); @@ -1198,7 +1221,7 @@ void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, Fn hiddenCallback) { - Show(controller, Descriptor{ + Show(controller->uiShow(), Descriptor{ .section = section, .fromSettings = true, .hiddenCallback = std::move(hiddenCallback), @@ -1337,7 +1360,10 @@ void DoubledLimitsPreviewBox( Main::Domain::kPremiumMaxAccounts, till, }); - Ui::Premium::ShowListBox(box, std::move(entries)); + Ui::Premium::ShowListBox( + box, + st::defaultPremiumLimits, + std::move(entries)); } object_ptr CreateUnlockButton( diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 0bf439d29..30fb8f866 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class DocumentData; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { struct ReactionId; } // namespace Data @@ -59,6 +63,11 @@ void ShowPremiumPreviewBox( PremiumPreview section, Fn)> shown = nullptr); +void ShowPremiumPreviewBox( + std::shared_ptr show, + PremiumPreview section, + Fn)> shown = nullptr); + void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index baceecfe5..335582ceb 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -137,13 +137,19 @@ SendFilesLimits DefaultLimitsForPeer(not_null peer) { SendFilesCheck DefaultCheckForPeer( not_null controller, not_null peer) { + return DefaultCheckForPeer(controller->uiShow(), peer); +} + +SendFilesCheck DefaultCheckForPeer( + std::shared_ptr show, + not_null peer) { return [=]( const Ui::PreparedFile &file, bool compress, bool silent) { const auto error = Data::FileRestrictionError(peer, file, compress); if (error && !silent) { - controller->showToast(*error); + show->showToast(*error); } return !error.has_value(); }; @@ -151,6 +157,7 @@ SendFilesCheck DefaultCheckForPeer( SendFilesBox::Block::Block( not_null parent, + const style::ComposeControls &st, not_null*> items, int from, int till, @@ -170,20 +177,24 @@ SendFilesBox::Block::Block( if (_isAlbum) { const auto preview = Ui::CreateChild( parent.get(), + st, my, way); _preview.reset(preview); } else { const auto media = Ui::SingleMediaPreview::Create( parent, + st, gifPaused, first); if (media) { _isSingleMedia = true; _preview.reset(media); } else { - _preview.reset( - Ui::CreateChild(parent.get(), first)); + _preview.reset(Ui::CreateChild( + parent.get(), + st, + first)); } } _preview->show(); @@ -328,15 +339,32 @@ SendFilesBox::SendFilesBox( SendFilesCheck check, Api::SendType sendType, SendMenu::Type sendMenuType) -: _controller(controller) -, _sendType(sendType) +: SendFilesBox(nullptr, { + .show = controller->uiShow(), + .list = std::move(list), + .caption = caption, + .limits = limits, + .check = check, + .sendType = sendType, + .sendMenuType = sendMenuType, +}) { +} + +SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor) +: _show(std::move(descriptor.show)) +, _st(descriptor.stOverride + ? *descriptor.stOverride + : st::defaultComposeControls) +, _sendType(descriptor.sendType) , _titleHeight(st::boxTitleHeight) -, _list(std::move(list)) -, _limits(limits) -, _sendMenuType(sendMenuType) -, _check(std::move(check)) -, _caption(this, st::confirmCaptionArea, Ui::InputField::Mode::MultiLine) -, _prefilledCaptionText(std::move(caption)) +, _list(std::move(descriptor.list)) +, _limits(descriptor.limits) +, _sendMenuType(descriptor.sendMenuType) +, _check(std::move(descriptor.check)) +, _confirmedCallback(std::move(descriptor.confirmed)) +, _cancelledCallback(std::move(descriptor.cancelled)) +, _caption(this, _st.files.caption, Ui::InputField::Mode::MultiLine) +, _prefilledCaptionText(std::move(descriptor.caption)) , _scroll(this, st::boxScroll) , _inner( _scroll->setOwnedWidget( @@ -431,7 +459,7 @@ void SendFilesBox::setupDragArea() { const auto droppedCallback = [=](bool compress) { return [=](const QMimeData *data) { addFiles(data); - Window::ActivateWindow(_controller); + _show->activate(); }; }; areas.document->setDroppedCallback(droppedCallback(false)); @@ -479,7 +507,7 @@ void SendFilesBox::openDialogToAddFileToAlbum() { return true; }; const auto callback = [=](FileDialog::OpenResult &&result) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); FileDialogCallback( std::move(result), checkResult, @@ -563,11 +591,11 @@ void SendFilesBox::addMenuButton() { return; } - const auto top = addTopButton(st::infoTopBarMenu); + const auto top = addTopButton(_st.files.menu); top->setClickedCallback([=] { - _menu = base::make_unique_q( - top, - st::popupMenuExpandedSeparator); + const auto &tabbed = _st.tabbed; + const auto &icons = tabbed.icons; + _menu = base::make_unique_q(top, tabbed.menu); if (hasSpoilerMenu()) { const auto spoilered = allWithSpoilers(); _menu->addAction( @@ -575,9 +603,9 @@ void SendFilesBox::addMenuButton() { ? tr::lng_context_disable_spoiler(tr::now) : tr::lng_context_spoiler_effect(tr::now)), [=] { toggleSpoilers(!spoilered); }, - spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler); + spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler); if (hasSendMenu()) { - _menu->addSeparator(); + _menu->addSeparator(&tabbed.expandedSeparator); } } if (hasSendMenu()) { @@ -586,7 +614,8 @@ void SendFilesBox::addMenuButton() { _sendMenuType, [=] { sendSilent(); }, [=] { sendScheduled(); }, - [=] { sendWhenOnline(); }); + [=] { sendWhenOnline(); }, + &_st.tabbed.icons); } _menu->popup(QCursor::pos()); return true; @@ -711,12 +740,12 @@ void SendFilesBox::generatePreviewFrom(int fromBlock) { } void SendFilesBox::pushBlock(int from, int till) { - const auto gifPaused = [controller = _controller] { - return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); + const auto gifPaused = [show = _show] { + return show->paused(Window::GifPauseReason::Layer); }; _blocks.emplace_back( _inner.data(), + _st, &_list.files, from, till, @@ -807,7 +836,7 @@ void SendFilesBox::pushBlock(int from, int till) { return checkSlowmode(list) && checkRights(list); }; const auto callback = [=](FileDialog::OpenResult &&result) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); FileDialogCallback( std::move(result), checkResult, @@ -825,15 +854,15 @@ void SendFilesBox::pushBlock(int from, int till) { const auto openedOnce = widget->lifetime().make_state(false); block.itemModifyRequest( - ) | rpl::start_with_next([=, controller = _controller](int index) { + ) | rpl::start_with_next([=, show = _show](int index) { if (!(*openedOnce)) { - controller->session().settings().incrementPhotoEditorHintShown(); - controller->session().saveSettings(); + show->session().settings().incrementPhotoEditorHintShown(); + show->session().saveSettings(); } *openedOnce = true; Editor::OpenWithPreparedFile( this, - controller, + show, &_list.files[index], st::sendMediaPreviewSize, [=] { refreshAllAfterChanges(from); }); @@ -856,12 +885,14 @@ void SendFilesBox::setupSendWayControls() { this, tr::lng_send_grouped(tr::now), groupFilesFirst, - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _sendImagesAsPhotos.create( this, tr::lng_send_compressed(tr::now), _sendWay.current().sendImagesAsPhotos(), - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _sendWay.changes( ) | rpl::start_with_next([=](SendFilesWay value) { @@ -905,7 +936,8 @@ void SendFilesBox::setupSendWayControls() { this, tr::lng_remember(tr::now), false, - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _wayRemember->hide(); rpl::combine( _groupFiles->checkedValue(), @@ -953,25 +985,32 @@ void SendFilesBox::updateSendWayControls() { : tr::lng_send_compressed_one(tr::now)); _hintLabel->setVisible( - _controller->session().settings().photoEditorHintShown() + _show->session().settings().photoEditorHintShown() ? _list.canHaveEditorHintLabel() : false); } void SendFilesBox::setupCaption() { - const auto allow = [=](const auto&) { + const auto allow = [=](const auto &) { return (_limits & SendFilesAllow::EmojiWithoutPremium); }; + const auto show = _show; InitMessageFieldHandlers( - _controller, + &show->session(), + show, _caption.data(), - Window::GifPauseReason::Layer, - allow); + [=] { return show->paused(Window::GifPauseReason::Layer); }, + allow, + &_st.files.caption); Ui::Emoji::SuggestionsController::Init( getDelegate()->outerContainer(), _caption, - &_controller->session(), - { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); + &_show->session(), + { + .suggestCustomEmoji = true, + .allowCustomWithoutPremium = allow, + .st = &_st.suggestions, + }); if (!_prefilledCaptionText.text.isEmpty()) { _caption->setTextWithTags( @@ -1019,12 +1058,21 @@ void SendFilesBox::setupEmojiPanel() { using Selector = ChatHelpers::TabbedSelector; _emojiPanel = base::make_unique_q( container, - _controller, - object_ptr( - nullptr, - _controller->uiShow(), - Window::GifPauseReason::Layer, - Selector::Mode::EmojiOnly)); + ChatHelpers::TabbedPanelDescriptor{ + .ownedSelector = object_ptr( + nullptr, + ChatHelpers::TabbedSelectorDescriptor{ + .show = _show, + .st = _st.tabbed, + .level = Window::GifPauseReason::Layer, + .mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly, + .features = { + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + }, + }), + }); _emojiPanel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, @@ -1041,11 +1089,9 @@ void SendFilesBox::setupEmojiPanel() { const auto info = data.document->sticker(); if (info && info->setType == Data::StickersType::Emoji - && !_controller->session().premium() + && !_show->session().premium() && !(_limits & SendFilesAllow::EmojiWithoutPremium)) { - ShowPremiumPreviewBox( - _controller, - PremiumPreview::AnimatedEmoji); + ShowPremiumPreviewBox(_show, PremiumPreview::AnimatedEmoji); } else { Data::InsertCustomEmoji(_caption.data(), data.document); } @@ -1057,7 +1103,7 @@ void SendFilesBox::setupEmojiPanel() { }; _emojiFilter.reset(base::install_event_filter(container, filterCallback)); - _emojiToggle.create(this, st::boxAttachEmoji); + _emojiToggle.create(this, _st.files.emoji); _emojiToggle->setVisible(!_caption->isHidden()); _emojiToggle->installEventFilter(_emojiPanel); _emojiToggle->addClickHandler([=] { @@ -1095,7 +1141,7 @@ bool SendFilesBox::canAddFiles(not_null data) const { } bool SendFilesBox::addFiles(not_null data) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); auto list = [&] { const auto urls = Core::ReadMimeUrls(data); auto result = CanAddUrls(urls) @@ -1242,7 +1288,7 @@ void SendFilesBox::paintEvent(QPaintEvent *e) { Painter p(this); p.setFont(st::boxTitleFont); - p.setPen(st::boxTitleFg); + p.setPen(getDelegate()->style().title.textFg); p.drawTextLeft( st::boxPhotoTitlePosition.x(), st::boxTitlePosition.y() - st::boxTopMargin, @@ -1318,7 +1364,7 @@ void SendFilesBox::saveSendWaySettings() { } bool SendFilesBox::validateLength(const QString &text) const { - const auto session = &_controller->session(); + const auto session = &_show->session(); const auto limit = Data::PremiumLimits(session).captionLengthCurrent(); const auto remove = int(text.size()) - limit; const auto way = _sendWay.current(); @@ -1328,7 +1374,8 @@ bool SendFilesBox::validateLength(const QString &text) const { way.sendImagesAsPhotos())) { return true; } - _controller->show(Box(CaptionLimitReachedBox, session, remove)); + _show->showBox( + Box(CaptionLimitReachedBox, session, remove, &_st.premium)); return false; } @@ -1385,8 +1432,7 @@ void SendFilesBox::sendScheduled() { ? SendMenu::Type::ScheduledToUser : _sendMenuType; const auto callback = [=](Api::SendOptions options) { send(options); }; - _controller->show( - HistoryView::PrepareScheduleBox(this, type, callback)); + _show->showBox(HistoryView::PrepareScheduleBox(this, type, callback)); } void SendFilesBox::sendWhenOnline() { diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 8e93673f0..6e6c6fa82 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -15,6 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localimageloader.h" #include "storage/storage_media_prepare.h" +namespace style { +struct ComposeControls; +} // namespace style + namespace Window { class SessionController; } // namespace Window @@ -26,6 +30,7 @@ enum class SendType; namespace ChatHelpers { class TabbedPanel; +class Show; } // namespace ChatHelpers namespace Ui { @@ -71,6 +76,29 @@ using SendFilesCheck = Fn controller, not_null peer); +[[nodiscard]] SendFilesCheck DefaultCheckForPeer( + std::shared_ptr show, + not_null peer); + +using SendFilesConfirmed = Fn; + +struct SendFilesBoxDescriptor { + std::shared_ptr show; + Ui::PreparedList list; + TextWithTags caption; + SendFilesLimits limits = {}; + SendFilesCheck check; + Api::SendType sendType = {}; + SendMenu::Type sendMenuType = {}; + const style::ComposeControls *stOverride = nullptr; + SendFilesConfirmed confirmed; + Fn cancelled; +}; class SendFilesBox : public Ui::BoxContent { public: @@ -87,14 +115,9 @@ public: SendFilesCheck check, Api::SendType sendType, SendMenu::Type sendMenuType); + SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor); - void setConfirmedCallback( - Fn callback) { + void setConfirmedCallback(SendFilesConfirmed callback) { _confirmedCallback = std::move(callback); } void setCancelledCallback(Fn callback) { @@ -116,6 +139,7 @@ private: public: Block( not_null parent, + const style::ComposeControls &st, not_null*> items, int from, int till, @@ -201,7 +225,8 @@ private: void enqueueNextPrepare(); void addPreparedAsyncFile(Ui::PreparedFile &&file); - const not_null _controller; + const std::shared_ptr _show; + const style::ComposeControls &_st; const Api::SendType _sendType = Api::SendType(); QString _titleText; @@ -211,15 +236,10 @@ private: std::optional _removingIndex; SendFilesLimits _limits = {}; - SendMenu::Type _sendMenuType = SendMenu::Type(); + SendMenu::Type _sendMenuType = {}; SendFilesCheck _check; - Fn _confirmedCallback; + SendFilesConfirmed _confirmedCallback; Fn _cancelledCallback; bool _confirmed = false; diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 454d658ed..8af7d3f0d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -124,7 +124,7 @@ void Show::showOrHideBoxOrLayer( } else if (const auto panel = _panel.get()) { panel->hideLayer(animated); } - } +} not_null Show::toastParent() const { const auto panel = _panel.get(); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 4e03be93a..8ad939619 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -11,6 +11,7 @@ using "boxes/boxes.style"; using "ui/layers/layers.style"; using "ui/widgets/widgets.style"; using "ui/menu_icons.style"; +using "ui/effects/premium.style"; GroupCallUserpics { size: pixels; @@ -66,6 +67,8 @@ ComposeIcons { menuMute: icon; menuSchedule: icon; menuWhenOnline: icon; + menuSpoiler: icon; + menuSpoilerOff: icon; } EmojiSuggestions { @@ -107,6 +110,7 @@ EmojiPan { fadeLeft: icon; fadeRight: icon; menu: PopupMenu; + expandedSeparator: MenuSeparator; tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; @@ -162,6 +166,24 @@ RecordBar { remove: IconButton; } +ComposeFiles { + check: Check; + checkbox: Checkbox; + menu: IconButton; + caption: InputField; + emoji: EmojiButton; + confirmBg: color; + buttonFile: IconButton; + buttonFileEdit: icon; + buttonFileDelete: icon; + iconBg: color; + iconPlay: icon; + iconImage: icon; + iconDocument: icon; + nameFg: color; + statusFg: color; +} + ComposeControls { bg: color; radius: pixels; @@ -175,6 +197,8 @@ ComposeControls { tabbedHeightMin: pixels; tabbedHeightMax: pixels; record: RecordBar; + files: ComposeFiles; + premium: PremiumLimits; } switchPmButton: RoundButton(defaultBoxButton) { @@ -416,6 +440,42 @@ stickersToast: Toast(defaultToast) { stickersEmpty: icon {{ "stickers_empty", windowSubTextFg }}; emojiEmpty: icon {{ "emoji_empty", windowSubTextFg }}; +editMediaButtonSize: 32px; + +editMediaButtonIconFile: icon {{ "send_media/send_media_replace", menuIconFg }}; +editMediaButton: IconButton(defaultIconButton) { + width: editMediaButtonSize; + height: editMediaButtonSize; + + icon: editMediaButtonIconFile; + + rippleAreaSize: editMediaButtonSize; + ripple: defaultRippleAnimation; +} + +sendBoxAlbumGroupEditInternalSkip: 8px; +sendBoxAlbumGroupSkipRight: 5px; +sendBoxAlbumGroupSkipTop: 5px; +sendBoxAlbumGroupRadius: 4px; +sendBoxAlbumGroupSize: size(62px, 25px); +sendBoxAlbumSmallGroupSize: size(30px, 25px); + +sendBoxFileGroupSkipTop: 2px; +sendBoxFileGroupSkipRight: 5px; +sendBoxFileGroupEditInternalSkip: -1px; + +sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgRipple; + } +} +sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; +sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_delete", menuIconFg }}; + +sendBoxAlbumButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg }}; +sendBoxAlbumGroupButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg, point(4px, 1px) }}; +sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_delete", roundedFg }}; + defaultComposeIcons: ComposeIcons { settings: icon {{ "emoji/emoji_settings", emojiIconFg }}; @@ -445,6 +505,8 @@ defaultComposeIcons: ComposeIcons { menuMute: menuIconMute; menuSchedule: menuIconSchedule; menuWhenOnline: menuIconWhenOnline; + menuSpoiler: menuIconSpoiler; + menuSpoilerOff: menuIconSpoilerOff; } defaultEmojiPan: EmojiPan { margin: margins(7px, 0px, 7px, 0px); @@ -476,6 +538,10 @@ defaultEmojiPan: EmojiPan { fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; menu: popupMenuWithIcons; + expandedSeparator: MenuSeparator(defaultMenuSeparator) { + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } tabs: emojiTabs; search: defaultTabbedSearch; searchMargin: margins(1px, 11px, 2px, 5px); @@ -974,6 +1040,41 @@ historySend: SendButton { sendDisabledFg: historyComposeIconFg; } +defaultComposeFilesMenu: IconButton(defaultIconButton) { + width: 48px; + height: 54px; + + icon: icon {{ "title_menu_dots", boxTitleCloseFg }}; + iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }}; + iconPosition: point(18px, -1px); + + rippleAreaPosition: point(1px, 6px); + rippleAreaSize: 42px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +defaultComposeFilesField: InputField(defaultInputField) { + textMargins: margins(1px, 26px, 31px, 4px); + heightMax: 158px; +} +defaultComposeFiles: ComposeFiles { + check: defaultCheck; + checkbox: defaultBoxCheckbox; + menu: defaultComposeFilesMenu; + caption: defaultComposeFilesField; + emoji: boxAttachEmoji; + confirmBg: windowBgOver; + buttonFile: sendBoxAlbumGroupButtonFile; + buttonFileEdit: sendBoxAlbumGroupEditButtonIconFile; + buttonFileDelete: sendBoxAlbumGroupDeleteButtonIconFile; + iconBg: msgFileInBg; + iconPlay: icon {{ "history_file_play", historyFileInIconFg }}; + iconImage: icon {{ "history_file_image", historyFileInIconFg }}; + iconDocument: icon {{ "history_file_document", historyFileInIconFg }}; + nameFg: historyFileNameInFg; + statusFg: mediaInFg; +} defaultComposeControls: ComposeControls { bg: historyComposeAreaBg; radius: 0px; @@ -987,4 +1088,6 @@ defaultComposeControls: ComposeControls { tabbedHeightMin: emojiPanMinHeight; tabbedHeightMax: emojiPanMaxHeight; record: defaultRecordBar; + files: defaultComposeFiles; + premium: defaultPremiumLimits; } diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h index a1ad76e6c..4a418c042 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h @@ -46,6 +46,8 @@ enum class WindowUsage { class Show : public Main::SessionShow { public: + virtual void activate() = 0; + [[nodiscard]] virtual bool paused(PauseReason reason) const = 0; [[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 3d5f35c57..9f29463ce 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -726,6 +726,7 @@ object_ptr EmojiListWidget::createFooter() { .paused = footerPaused, .parent = this, .st = &st(), + .features = { .stickersSettings = false }, }); _footer = result; diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 48a0e9800..0ae43c3dd 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -172,6 +172,7 @@ object_ptr GifsListWidget::createFooter() { .paused = pausedMethod(), .parent = this, .st = &st(), + .features = { .stickersSettings = false }, }); _footer = result; _chosenSetId = Data::Stickers::RecentSetId; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index cf0dda5eb..afee47efd 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/stickers_list_widget.h" +#include "core/application.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" @@ -221,9 +222,15 @@ StickersListWidget::StickersListWidget( } _settings->addClickHandler([=] { - using Section = StickersBox::Section; - _show->showBox( - Box(_show, Section::Installed, _isMasks)); + if (const auto window = _show->resolveWindow( + WindowUsage::PremiumPromo)) { + // While media viewer can't show StickersBox. + using Section = StickersBox::Section; + window->show( + Box(_show, Section::Installed, _isMasks)); + Core::App().hideMediaView(); + Window::ActivateWindow(window); + } }); session().downloaderTaskFinished( diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h index 153c19581..fcedb5efc 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h @@ -30,7 +30,7 @@ struct TabbedPanelDescriptor { Window::SessionController *regularWindow = nullptr; object_ptr ownedSelector = { nullptr }; TabbedSelector *nonOwnedSelector = nullptr; -};; +}; class TabbedPanel : public Ui::RpWidget { public: diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp index 66fc0f997..2c0482c33 100644 --- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp +++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp @@ -12,21 +12,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" // Window::GifPauseReason #include "styles/style_chat_helpers.h" +#include "styles/style_media_view.h" namespace Editor { StickersPanelController::StickersPanelController( not_null panelContainer, - not_null controller) + std::shared_ptr show) : _stickersPanel( base::make_unique_q( panelContainer, - controller, - object_ptr( - nullptr, - controller->uiShow(), - Window::GifPauseReason::Layer, - ChatHelpers::TabbedSelector::Mode::MediaEditor))) { + ChatHelpers::TabbedPanelDescriptor{ + .ownedSelector = object_ptr( + nullptr, + ChatHelpers::TabbedSelectorDescriptor{ + .show = show, + .st = st::storiesComposeControls.tabbed, + .level = Window::GifPauseReason::Layer, + .mode = ChatHelpers::TabbedSelector::Mode::MediaEditor, + .features = { + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + }, + }), + })) { _stickersPanel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h index 5404a8adb..9514ea23b 100644 --- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h +++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h @@ -11,16 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace ChatHelpers { class TabbedPanel; +class Show; } // namespace ChatHelpers namespace Ui { class RpWidget; } // namespace Ui -namespace Window { -class SessionController; -} // namespace Window - namespace Editor { class StickersPanelController final { @@ -34,7 +31,7 @@ public: StickersPanelController( not_null panelContainer, - not_null controller); + std::shared_ptr show); [[nodiscard]] auto stickerChosen() const -> rpl::producer>; diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index a11fe9760..801e12258 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -52,16 +52,34 @@ PhotoEditor::PhotoEditor( std::shared_ptr photo, PhotoModifications modifications, EditorData data) +: PhotoEditor( + parent, + controller->uiShow(), + (controller->sessionController() + ? controller->sessionController()->uiShow() + : nullptr), + std::move(photo), + std::move(modifications), + std::move(data)) { +} + +PhotoEditor::PhotoEditor( + not_null parent, + std::shared_ptr show, + std::shared_ptr sessionShow, + std::shared_ptr photo, + PhotoModifications modifications, + EditorData data) : RpWidget(parent) , _modifications(std::move(modifications)) , _controllers(std::make_shared( - controller->sessionController() + sessionShow ? std::make_unique( this, - controller->sessionController()) + std::move(sessionShow)) : nullptr, std::make_unique(), - controller->uiShow())) + std::move(show))) , _content(base::make_unique_q( this, photo, diff --git a/Telegram/SourceFiles/editor/photo_editor.h b/Telegram/SourceFiles/editor/photo_editor.h index e226b1ddb..4169339a6 100644 --- a/Telegram/SourceFiles/editor/photo_editor.h +++ b/Telegram/SourceFiles/editor/photo_editor.h @@ -15,8 +15,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class LayerWidget; +class Show; } // namespace Ui +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Window { class Controller; } // namespace Window @@ -36,6 +41,13 @@ public: std::shared_ptr photo, PhotoModifications modifications, EditorData data = EditorData()); + PhotoEditor( + not_null parent, + std::shared_ptr show, + std::shared_ptr sessionShow, + std::shared_ptr photo, + PhotoModifications modifications, + EditorData data = EditorData()); void save(); [[nodiscard]] rpl::producer doneRequests() const; diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp index c58fb1cc0..daf93c08d 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp @@ -22,7 +22,7 @@ namespace Editor { void OpenWithPreparedFile( not_null parent, - not_null controller, + std::shared_ptr show, not_null file, int previewWidth, Fn &&doneCallback) { @@ -56,13 +56,14 @@ void OpenWithPreparedFile( const auto fileImage = std::make_shared(std::move(copy)); auto editor = base::make_unique_q( parent, - &controller->window(), + show, + show, fileImage, image->modifications); const auto raw = editor.get(); auto layer = std::make_unique(parent, std::move(editor)); InitEditorLayer(layer.get(), raw, std::move(callback)); - controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther); + show->showLayer(std::move(layer), Ui::LayerOption::KeepOther); } void PrepareProfilePhoto( diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h index d7fd19a75..f1c2fd445 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h @@ -6,13 +6,13 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -// -//#include "ui/image/image.h" -//#include "editor/photo_editor_common.h" -//#include "base/unique_qptr.h" enum class ImageRoundRadius; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Ui { class RpWidget; struct PreparedFile; @@ -31,7 +31,7 @@ struct EditorData; void OpenWithPreparedFile( not_null parent, - not_null controller, + std::shared_ptr show, not_null file, int previewWidth, Fn &&doneCallback); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index c6e81a086..99db1769a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5142,7 +5142,8 @@ bool HistoryWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } controller()->showToast(text); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 22145b376..f8ddd36e2 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -1123,7 +1123,8 @@ bool RepliesWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 85ce03471..66255b466 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -559,7 +559,8 @@ bool ScheduledWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 49b2e70a3..bbc28a75d 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -7,16 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_reply.h" +#include "api/api_common.h" +#include "apiwrap.h" #include "base/call_delayed.h" +#include "boxes/premium_limits_box.h" +#include "boxes/send_files_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/tabbed_selector.h" +#include "core/file_utilities.h" +#include "core/mime_type.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/view/controls/compose_controls_common.h" #include "history/view/controls/history_view_compose_controls.h" +#include "history/history_item_helpers.h" +#include "history/history.h" #include "inline_bots/inline_bot_result.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" #include "media/stories/media_stories_controller.h" #include "menu/menu_send.h" +#include "storage/localimageloader.h" +#include "storage/storage_media_prepare.h" +#include "ui/chat/attach/attach_prepare.h" +#include "styles/style_boxes.h" // sendMediaPreviewSize. #include "styles/style_chat_helpers.h" #include "styles/style_media_view.h" @@ -85,15 +99,276 @@ void ReplyArea::initGeometry() { } void ReplyArea::send(Api::SendOptions options) { - // #TODO stories + const auto webPageId = _controls->webPageId(); + + auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + message.textWithTags = _controls->getTextWithAppliedMarkdown(); + message.webPageId = webPageId; + + const auto error = GetErrorTextForSending( + _data.user, + { + .topicRootId = MsgId(0), + .text = &message.textWithTags, + .ignoreSlowmodeCountdown = (options.scheduled != 0), + }); + if (!error.isEmpty()) { + _controller->uiShow()->showToast(error); + } + + session().api().sendMessage(std::move(message)); + + _controls->clear(); + finishSending(); } void ReplyArea::sendVoice(VoiceToSend &&data) { - // #TODO stories + auto action = prepareSendAction(data.options); + session().api().sendVoiceMessage( + data.bytes, + data.waveform, + data.duration, + std::move(action)); + + _controls->clearListenState(); + finishSending(); } -void ReplyArea::chooseAttach(std::optional overrideCompress) { - // #TODO stories +void ReplyArea::finishSending() { + _controls->hidePanelsAnimated(); + _controller->wrap()->setFocus(); +} + +void ReplyArea::uploadFile( + const QByteArray &fileContent, + SendMediaType type) { + session().api().sendFile(fileContent, type, prepareSendAction({})); +} + +bool ReplyArea::showSendingFilesError( + const Ui::PreparedList &list) const { + return showSendingFilesError(list, std::nullopt); +} + +bool ReplyArea::showSendingFilesError( + const Ui::PreparedList &list, + std::optional compress) const { + const auto text = [&] { + const auto peer = _data.user; + const auto error = Data::FileRestrictionError(peer, list, compress); + if (error) { + return *error; + } + using Error = Ui::PreparedList::Error; + switch (list.error) { + case Error::None: return QString(); + case Error::EmptyFile: + case Error::Directory: + case Error::NonLocalUrl: return tr::lng_send_image_empty( + tr::now, + lt_name, + list.errorData); + case Error::TooLargeFile: return u"(toolarge)"_q; + } + return tr::lng_forward_send_files_cant(tr::now); + }(); + if (text.isEmpty()) { + return false; + } else if (text == u"(toolarge)"_q) { + const auto fileSize = list.files.back().size; + _controller->uiShow()->showBox(Box( + FileSizeLimitBox, + &session(), + fileSize, + &st::storiesComposePremium)); + return true; + } + + _controller->uiShow()->showToast(text); + return true; +} + +Api::SendAction ReplyArea::prepareSendAction( + Api::SendOptions options) const { + Expects(_data.user != nullptr); + + const auto history = _data.user->owner().history(_data.user); + auto result = Api::SendAction(history, options); + result.options.sendAs = _controls->sendAsPeer(); + return result; +} + +void ReplyArea::chooseAttach( + std::optional overrideSendImagesAsPhotos) { + if (!_data.user) { + return; + } + const auto user = not_null(_data.user); + _choosingAttach = false; + if (const auto error = Data::AnyFileRestrictionError(user)) { + _controller->uiShow()->showToast(*error); + return; + } + + const auto filter = (overrideSendImagesAsPhotos == true) + ? FileDialog::ImagesOrAllFilter() + : FileDialog::AllOrImagesFilter(); + const auto callback = [=](FileDialog::OpenResult &&result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + if (!result.remoteContent.isEmpty()) { + auto read = Images::Read({ + .content = result.remoteContent, + }); + if (!read.image.isNull() && !read.animated) { + confirmSendingFiles( + std::move(read.image), + std::move(result.remoteContent), + overrideSendImagesAsPhotos); + } else { + uploadFile(result.remoteContent, SendMediaType::File); + } + } else { + const auto premium = session().premium(); + auto list = Storage::PrepareMediaList( + result.paths, + st::sendMediaPreviewSize, + premium); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list)); + } + }; + FileDialog::GetOpenPaths( + _controller->wrap().get(), + tr::lng_choose_files(tr::now), + filter, + crl::guard(&_shownUserGuard, callback), + nullptr); +} + +bool ReplyArea::confirmSendingFiles( + not_null data, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + const auto hasImage = data->hasImage(); + const auto premium = session().user()->isPremium(); + + if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { + auto list = Storage::PrepareMediaList( + urls, + st::sendMediaPreviewSize, + premium); + if (list.error != Ui::PreparedList::Error::NonLocalUrl) { + if (list.error == Ui::PreparedList::Error::None + || !hasImage) { + const auto emptyTextOnCancel = QString(); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list), emptyTextOnCancel); + return true; + } + } + } + + if (auto read = Core::ReadMimeImage(data)) { + confirmSendingFiles( + std::move(read.image), + std::move(read.content), + overrideSendImagesAsPhotos, + insertTextOnCancel); + return true; + } + return false; +} + +bool ReplyArea::confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel) { + if (_controls->confirmMediaEdit(list)) { + return true; + } else if (showSendingFilesError(list)) { + return false; + } + + const auto show = _controller->uiShow(); + auto confirmed = [=](auto &&...args) { + sendingFilesConfirmed(std::forward(args)...); + }; + auto box = Box(SendFilesBoxDescriptor{ + .show = show, + .list = std::move(list), + .caption = _controls->getTextWithAppliedMarkdown(), + .limits = DefaultLimitsForPeer(_data.user), + .check = DefaultCheckForPeer(show, _data.user), + .sendType = Api::SendType::Normal, + .sendMenuType = SendMenu::Type::SilentOnly, + .stOverride = &st::storiesComposeControls, + .confirmed = crl::guard(this, confirmed), + .cancelled = _controls->restoreTextCallback(insertTextOnCancel), + }); + if (const auto shown = show->show(std::move(box))) { + shown->setCloseByOutsideClick(false); + } + + return true; +} + +void ReplyArea::sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + Expects(list.filesToProcess.empty()); + + if (showSendingFilesError(list, way.sendImagesAsPhotos())) { + return; + } + auto groups = DivideByGroups( + std::move(list), + way, + _data.user->slowmodeApplied()); + const auto type = way.sendImagesAsPhotos() + ? SendMediaType::Photo + : SendMediaType::File; + auto action = prepareSendAction(options); + action.clearDraft = false; + if ((groups.size() != 1 || !groups.front().sentWithCaption()) + && !caption.text.isEmpty()) { + auto message = Api::MessageToSend(action); + message.textWithTags = base::take(caption); + session().api().sendMessage(std::move(message)); + } + for (auto &group : groups) { + const auto album = (group.type != Ui::AlbumType::None) + ? std::make_shared() + : nullptr; + session().api().sendFiles( + std::move(group.list), + type, + base::take(caption), + album, + action); + } + finishSending(); +} + +bool ReplyArea::confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + if (image.isNull()) { + return false; + } + + auto list = Storage::PrepareMediaFromImage( + std::move(image), + std::move(content), + st::sendMediaPreviewSize); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + return confirmSendingFiles(std::move(list), insertTextOnCancel); } void ReplyArea::initActions() { @@ -180,6 +455,7 @@ void ReplyArea::show(ReplyAreaData data) { } return; } + invalidate_weak_ptrs(&_shownUserGuard); const auto user = data.user; const auto history = user ? user->owner().history(user).get() : nullptr; _controls->setHistory({ @@ -188,6 +464,12 @@ void ReplyArea::show(ReplyAreaData data) { _controls->clear(); } +Main::Session &ReplyArea::session() const { + Expects(_data.user != nullptr); + + return _data.user->session(); +} + rpl::producer ReplyArea::focusedValue() const { return _controls->focusedValue(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index 9587e2e07..c7ea6f88c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -9,7 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" +enum class SendMediaType; + namespace Api { +struct SendAction; struct SendOptions; } // namespace Api @@ -21,6 +24,15 @@ namespace HistoryView::Controls { struct VoiceToSend; } // namespace HistoryView::Controls +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +struct PreparedList; +class SendFilesWay; +} // namespace Ui + namespace Media::Stories { class Controller; @@ -45,9 +57,44 @@ public: private: using VoiceToSend = HistoryView::Controls::VoiceToSend; + [[nodiscard]] Main::Session &session() const; + + bool confirmSendingFiles(const QStringList &files); + bool confirmSendingFiles(not_null data); + + void uploadFile(const QByteArray &fileContent, SendMediaType type); + bool confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional overrideSendImagesAsPhotos = std::nullopt, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + const QStringList &files, + const QString &insertTextOnCancel); + bool confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + not_null data, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel = QString()); + bool showSendingFilesError(const Ui::PreparedList &list) const; + bool showSendingFilesError( + const Ui::PreparedList &list, + std::optional compress) const; + void sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter); + void finishSending(); + void initGeometry(); void initActions(); + [[nodiscard]] Api::SendAction prepareSendAction( + Api::SendOptions options) const; void send(Api::SendOptions options); void sendVoice(VoiceToSend &&data); void chooseAttach(std::optional overrideSendImagesAsPhotos); @@ -58,6 +105,7 @@ private: const std::unique_ptr _controls; ReplyAreaData _data; + base::has_weak_ptr _shownUserGuard; bool _choosingAttach = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c97645659..c822bcf0d 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -467,6 +467,9 @@ storiesRemoveSet: IconButton(stickerPanRemoveSet) { iconOver: icon {{ "simple_close", storiesComposeGrayIcon }}; ripple: storiesComposeRippleLight; } +storiesMenuSeparator: MenuSeparator(defaultMenuSeparator) { + fg: groupCallMenuBgOver; +} storiesMenu: Menu(defaultMenu) { itemBg: groupCallMenuBg; itemBgOver: groupCallMenuBgOver; @@ -477,9 +480,7 @@ storiesMenu: Menu(defaultMenu) { itemFgShortcutOver: groupCallMemberNotJoinedStatus; itemFgShortcutDisabled: groupCallMemberNotJoinedStatus; - separator: MenuSeparator(defaultMenuSeparator) { - fg: groupCallMenuBgOver; - } + separator: storiesMenuSeparator; arrow: icon {{ "menu/submenu_arrow", groupCallMemberNotJoinedStatus }}; ripple: RippleAnimation(defaultRippleAnimation) { @@ -507,6 +508,23 @@ storiesPopupMenuWithIcons: PopupMenu(storiesPopupMenu) { menu: storiesMenuWithIcons; } +storiesAttachEmojiInner: IconButton(storiesAttach) { + icon: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }}; + iconOver: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }}; +} +storiesAttachEmoji: EmojiButton(historyAttachEmoji) { + inner: storiesAttachEmojiInner; + bg: storiesComposeBg; + lineFg: storiesComposeGrayIcon; + lineFgOver: storiesComposeGrayIcon; +} +storiesComposePremium: PremiumLimits(defaultPremiumLimits) { + boxLabel: FlatLabel(boxLabel) { + textFg: groupCallMembersFg; + } + nonPremiumBg: storiesComposeBgOver; + nonPremiumFg: storiesComposeWhiteText; +} storiesComposeControls: ComposeControls(defaultComposeControls) { bg: storiesComposeBg; radius: storiesRadius; @@ -528,15 +546,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { sendDisabledFg: storiesComposeGrayText; } attach: storiesAttach; - emoji: EmojiButton(historyAttachEmoji) { - inner: IconButton(storiesAttach) { - icon: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }}; - iconOver: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }}; - } - bg: storiesComposeBg; - lineFg: storiesComposeGrayIcon; - lineFgOver: storiesComposeGrayIcon; - } + emoji: storiesAttachEmoji; suggestions: EmojiSuggestions(defaultEmojiSuggestions) { dropdown: InnerDropdown(emojiSuggestionsDropdown) { animation: PanelAnimation(defaultPanelAnimation) { @@ -569,6 +579,10 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { fadeLeft: icon {{ "fade_horizontal-flip_horizontal", storiesComposeBg }}; fadeRight: icon {{ "fade_horizontal", storiesComposeBg }}; menu: storiesPopupMenuWithIcons; + expandedSeparator: MenuSeparator(storiesMenuSeparator) { + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } tabs: SettingsSlider(emojiTabs) { barFgActive: storiesComposeBlue; labelFg: storiesComposeGrayText; @@ -638,6 +652,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { menuMute: icon {{ "menu/mute", storiesComposeWhiteText }}; menuSchedule: icon {{ "menu/calendar", storiesComposeWhiteText }}; menuWhenOnline: icon {{ "menu/send_when_online", storiesComposeWhiteText }}; + menuSpoiler: icon {{ "menu/spoiler_on", storiesComposeWhiteText }}; + menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }}; } autocompleteBottomSkip: 10px; } @@ -664,4 +680,51 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { iconPosition: point(10px, 11px); } } + files: ComposeFiles(defaultComposeFiles) { + check: Check(defaultCheck) { + untoggledFg: groupCallMemberNotJoinedStatus; + toggledFg: groupCallActiveFg; + icon: icon {{ "default_checkbox_check", groupCallMembersFg, point(4px, 7px) }}; + } + checkbox: Checkbox(defaultBoxCheckbox) { + textFg: groupCallMembersFg; + textFgActive: groupCallMembersFg; + rippleBg: groupCallMembersBgRipple; + rippleBgActive: groupCallMembersBgRipple; + } + menu: IconButton(defaultComposeFilesMenu) { + icon: icon {{ "title_menu_dots", storiesComposeGrayIcon }}; + iconOver: icon {{ "title_menu_dots", storiesComposeGrayIcon }}; + ripple: storiesComposeRippleLight; + } + caption: InputField(defaultComposeFilesField) { + textFg: storiesComposeWhiteText; + textBg: storiesComposeBg; + placeholderFg: storiesComposeGrayText; + placeholderFgActive: storiesComposeBlue; + borderFg: storiesComposeGrayText; + borderFgActive: storiesComposeBlue; + menu: storiesPopupMenu; + } + emoji: EmojiButton(storiesAttachEmoji) { + inner: IconButton(storiesAttachEmojiInner) { + width: 30px; + height: 30px; + rippleAreaSize: 0px; + } + } + confirmBg: storiesComposeBgOver; + buttonFile: IconButton(sendBoxAlbumGroupButtonFile) { + ripple: storiesComposeRipple; + } + buttonFileEdit: icon {{ "send_media/send_media_replace", storiesComposeGrayIcon }}; + buttonFileDelete: icon {{ "send_media/send_media_delete", storiesComposeGrayIcon }}; + iconBg: storiesComposeBlue; + iconPlay: icon {{ "history_file_play", storiesComposeWhiteText }}; + iconImage: icon {{ "history_file_image", storiesComposeWhiteText }}; + iconDocument: icon {{ "history_file_document", storiesComposeWhiteText }}; + nameFg: storiesComposeWhiteText; + statusFg: storiesComposeGrayText; + } + premium: storiesComposePremium; } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index cd34868a0..01326c842 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -287,6 +287,87 @@ struct OverlayWidget::PipWrap { rpl::lifetime lifetime; }; +class OverlayWidget::Show final : public ChatHelpers::Show { +public: + explicit Show(not_null widget) : _widget(widget) { + } + + void activate() override { + if (!_widget->isHidden()) { + _widget->activate(); + } + } + + void showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const override { + _widget->_layerBg->uiShow()->showOrHideBoxOrLayer( + std::move(layer), + options, + anim::type::normal); + } + not_null toastParent() const override { + return _widget->_body; + } + bool valid() const override { + return _widget->_storiesSession != nullptr; + } + operator bool() const override { + return valid(); + } + + Main::Session &session() const override { + Expects(_widget->_storiesSession != nullptr); + + return *_widget->_storiesSession; + } + bool paused(ChatHelpers::PauseReason reason) const override { + if (_widget->isHidden() + || (!_widget->_fullscreen + && !_widget->_window->isActiveWindow())) { + return true; + } else if (reason < ChatHelpers::PauseReason::Layer + && _widget->_layerBg->topShownLayer() != nullptr) { + return true; + } + return false; + } + rpl::producer<> pauseChanged() const override { + return rpl::never<>(); + } + + rpl::producer adjustShadowLeft() const override { + return rpl::single(false); + } + SendMenu::Type sendMenuType() const override { + return SendMenu::Type::SilentOnly; + } + + bool showMediaPreview( + Data::FileOrigin origin, + not_null document) const override { + return false; // #TODO stories + } + bool showMediaPreview( + Data::FileOrigin origin, + not_null photo) const override { + return false; // #TODO stories + } + + void processChosenSticker( + ChatHelpers::FileChosen &&chosen) const override { + _widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen)); + } + +private: + not_null _widget; + +}; + OverlayWidget::Streamed::Streamed( not_null document, Data::FileOrigin origin, @@ -3936,81 +4017,10 @@ not_null OverlayWidget::storiesWrap() { } std::shared_ptr OverlayWidget::storiesShow() { - class Show final : public ChatHelpers::Show { - public: - explicit Show(not_null widget) : _widget(widget) { - } - - void showBox( - object_ptr content, - Ui::LayerOptions options - = Ui::LayerOption::KeepOther) const override { - _widget->_layerBg->showBox( - std::move(content), - options, - anim::type::normal); - } - void hideLayer() const override { - _widget->_layerBg->hideAll(anim::type::normal); - } - not_null toastParent() const override { - return _widget->_body; - } - bool valid() const override { - return _widget->_storiesSession != nullptr; - } - operator bool() const override { - return valid(); - } - - Main::Session &session() const override { - Expects(_widget->_storiesSession != nullptr); - - return *_widget->_storiesSession; - } - bool paused(ChatHelpers::PauseReason reason) const override { - if (_widget->isHidden() - || (!_widget->_fullscreen - && !_widget->_window->isActiveWindow())) { - return true; - } else if (reason < ChatHelpers::PauseReason::Layer - && _widget->_layerBg->topShownLayer() != nullptr) { - return true; - } - return false; - } - rpl::producer<> pauseChanged() const override { - return rpl::never<>(); - } - - rpl::producer adjustShadowLeft() const override { - return rpl::single(false); - } - SendMenu::Type sendMenuType() const override { - return SendMenu::Type::SilentOnly; - } - - bool showMediaPreview( - Data::FileOrigin origin, - not_null document) const override { - return false; // #TODO stories - } - bool showMediaPreview( - Data::FileOrigin origin, - not_null photo) const override { - return false; // #TODO stories - } - - void processChosenSticker( - ChatHelpers::FileChosen &&chosen) const override { - _widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen)); - } - - private: - not_null _widget; - - }; - return std::make_shared(this); + if (!_cachedShow) { + _cachedShow = std::make_shared(this); + } + return _cachedShow; } auto OverlayWidget::storiesStickerOrEmojiChosen() @@ -5753,6 +5763,7 @@ void OverlayWidget::clearBeforeHide() { _collageData = std::nullopt; clearStreaming(); setStoriesUser(nullptr); + _layerBg->hideAll(anim::type::instant); assignMediaPointer(nullptr); _preloadPhotos.clear(); _preloadDocuments.clear(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 6007e9fab..43551bf41 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -131,6 +131,7 @@ public: rpl::lifetime &lifetime(); private: + class Show; struct Streamed; struct PipWrap; class Renderer; @@ -600,6 +601,7 @@ private: bool _showAsPip = false; std::unique_ptr _stories; + std::shared_ptr _cachedShow; rpl::event_stream<> _storiesChanged; Main::Session *_storiesSession = nullptr; rpl::event_stream _storiesStickerOrEmojiChosen; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 2c3f2eecf..055b1296e 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1782,6 +1782,11 @@ QString LookupPremiumRef(PremiumPreview section) { not_null CreateSubscribeButton( SubscribeButtonArgs &&args) { + Expects(args.show || args.controller); + + if (!args.show && args.controller) { + args.show = args.controller->uiShow(); + } const auto result = Ui::CreateChild( args.parent.get(), args.gradientStops @@ -1789,9 +1794,14 @@ not_null CreateSubscribeButton( : Ui::Premium::ButtonGradientStops()); result->setClickedCallback([ - controller = args.controller, + show = args.show, computeRef = args.computeRef, computeBotUrl = args.computeBotUrl] { + const auto window = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (!window) { + return; + } const auto url = computeBotUrl ? computeBotUrl() : QString(); if (!url.isEmpty()) { const auto local = Core::TryConvertUrlToLocal(url); @@ -1801,12 +1811,12 @@ not_null CreateSubscribeButton( UrlClickHandler::Open( local, QVariant::fromValue(ClickHandlerContext{ - .sessionWindow = base::make_weak(controller), + .sessionWindow = base::make_weak(window), .botStartAutoSubmit = true, })); } else { - SendScreenAccept(controller); - StartPremiumPayment(controller, computeRef()); + SendScreenAccept(window); + StartPremiumPayment(window, computeRef()); } }); diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 2ce4aba67..0ad7b036c 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL enum class PremiumPreview; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Ui { class RpWidget; class GradientButton; @@ -48,12 +52,13 @@ void StartPremiumPayment( [[nodiscard]] QString LookupPremiumRef(PremiumPreview section); struct SubscribeButtonArgs final { - not_null controller; + Window::SessionController *controller = nullptr; not_null parent; Fn computeRef; std::optional> text; std::optional gradientStops; Fn computeBotUrl; // nullable + std::shared_ptr show; }; [[nodiscard]] not_null CreateSubscribeButton( diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 1e9d03bdb..1386b2a74 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -1101,7 +1101,7 @@ void FileLoadTask::finish() { } else if (_result->filesize > kFileSizePremiumLimit || (_result->filesize > kFileSizeLimit && !premium)) { Ui::show( - Box(FileSizeLimitBox, session, _result->filesize), + Box(FileSizeLimitBox, session, _result->filesize, nullptr), Ui::LayerOption::KeepOther); removeFromAlbum(); } else { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp index 60cc5ae68..1b7a81beb 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp @@ -20,14 +20,16 @@ namespace Ui { AbstractSingleFilePreview::AbstractSingleFilePreview( QWidget *parent, + const style::ComposeControls &st, AttachControls::Type type) : AbstractSinglePreview(parent) +, _st(st) , _type(type) -, _editMedia(this, st::sendBoxAlbumGroupButtonFile) -, _deleteMedia(this, st::sendBoxAlbumGroupButtonFile) { +, _editMedia(this, _st.files.buttonFile) +, _deleteMedia(this, _st.files.buttonFile) { - _editMedia->setIconOverride(&st::sendBoxAlbumGroupEditButtonIconFile); - _deleteMedia->setIconOverride(&st::sendBoxAlbumGroupDeleteButtonIconFile); + _editMedia->setIconOverride(&_st.files.buttonFileEdit); + _deleteMedia->setIconOverride(&_st.files.buttonFileDelete); if (type == AttachControls::Type::Full) { _deleteMedia->show(); @@ -100,18 +102,17 @@ void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) { if (_data.fileIsAudio && !_data.fileThumb.isNull()) { p.drawPixmap(inner.topLeft(), _data.fileThumb); } else { - p.setBrush(st::msgFileInBg); + p.setBrush(_st.files.iconBg); PainterHighQualityEnabler hq(p); p.drawEllipse(inner); } - auto &icon = _data.fileIsAudio ? (_data.fileThumb.isNull() - ? st::historyFileInPlay + ? _st.files.iconPlay : st::historyFileThumbPlay) : _data.fileIsImage - ? st::historyFileInImage - : st::historyFileInDocument; + ? _st.files.iconImage + : _st.files.iconDocument; icon.paintInCenter(p, inner); } else { QRect rthumb( @@ -119,7 +120,7 @@ void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) { p.drawPixmap(rthumb.topLeft(), _data.fileThumb); } p.setFont(st::semiboldFont); - p.setPen(st::historyFileNameInFg); + p.setPen(_st.files.nameFg); p.drawTextLeft( x + nameleft, y + nametop, width(), @@ -127,7 +128,7 @@ void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) { _data.nameWidth); p.setFont(st::normalFont); - p.setPen(st::mediaInFg); + p.setPen(_st.files.statusFg); p.drawTextLeft( x + nameleft, y + statustop, @@ -167,7 +168,7 @@ void AbstractSingleFilePreview::updateTextWidthFor(Data &data) { - st.thumbSize - st.thumbSkip // Right buttons. - - st::sendBoxAlbumGroupButtonFile.width * buttonsCount + - _st.files.buttonFile.width * buttonsCount - st::sendBoxAlbumGroupEditInternalSkip * buttonsCount - st::sendBoxAlbumGroupSkipRight; data.nameWidth = st::semiboldFont->width(data.name); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h index cbb0ae30a..f14992142 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h @@ -11,13 +11,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_controls.h" #include "base/object_ptr.h" +namespace style { +struct ComposeControls; +} // namespace style + namespace Ui { class IconButton; class AbstractSingleFilePreview : public AbstractSinglePreview { public: - AbstractSingleFilePreview(QWidget *parent, AttachControls::Type type); + AbstractSingleFilePreview( + QWidget *parent, + const style::ComposeControls &st, + AttachControls::Type type); ~AbstractSingleFilePreview(); [[nodiscard]] rpl::producer<> deleteRequests() const override; @@ -46,6 +53,7 @@ private: void updateTextWidthFor(Data &data); + const style::ComposeControls &_st; const AttachControls::Type _type; Data _data; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp index 59790b5d6..29f38cdde 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" @@ -30,8 +31,10 @@ constexpr auto kMinPreviewWidth = 20; AbstractSingleMediaPreview::AbstractSingleMediaPreview( QWidget *parent, + const style::ComposeControls &st, AttachControls::Type type) : AbstractSinglePreview(parent) +, _st(st) , _minThumbH(st::sendBoxAlbumGroupSize.height() + st::sendBoxAlbumGroupSkipTop * 2) , _controls(base::make_unique_q(this, type)) { @@ -173,7 +176,7 @@ void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) { _previewTop, _previewLeft - padding.left(), _previewHeight, - st::confirmBg); + _st.files.confirmBg); } if ((_previewLeft + _previewWidth) < (width() - padding.right())) { p.fillRect( @@ -181,7 +184,7 @@ void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) { _previewTop, width() - padding.right() - _previewLeft - _previewWidth, _previewHeight, - st::confirmBg); + _st.files.confirmBg); } if (_previewTop > 0) { p.fillRect( @@ -189,7 +192,7 @@ void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) { 0, width() - padding.right() - padding.left(), height(), - st::confirmBg); + _st.files.confirmBg); } } @@ -264,14 +267,15 @@ void AbstractSingleMediaPreview::showContextMenu(QPoint position) { } _menu = base::make_unique_q( this, - st::popupMenuWithIcons); + _st.tabbed.menu); + const auto &icons = _st.tabbed.icons; const auto spoilered = hasSpoiler(); _menu->addAction(spoilered ? tr::lng_context_disable_spoiler(tr::now) : tr::lng_context_spoiler_effect(tr::now), [=] { setSpoiler(!spoilered); - }, spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler); + }, spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler); if (_menu->empty()) { _menu = nullptr; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h index ef85bf0d2..3bacc6e6d 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h @@ -12,13 +12,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_send_files_way.h" #include "ui/abstract_button.h" +namespace style { +struct ComposeControls; +} // namespace style + namespace Ui { class PopupMenu; class AbstractSingleMediaPreview : public AbstractSinglePreview { public: - AbstractSingleMediaPreview(QWidget *parent, AttachControls::Type type); + AbstractSingleMediaPreview( + QWidget *parent, + const style::ComposeControls &st, + AttachControls::Type type); ~AbstractSingleMediaPreview(); void setSendWay(SendFilesWay way); @@ -60,6 +67,7 @@ private: void applyCursor(style::cursor cursor); void showContextMenu(QPoint position); + const style::ComposeControls &_st; SendFilesWay _sendWay; bool _animated = false; QPixmap _preview; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp index 24e46d3d8..d33681b9f 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp @@ -28,9 +28,11 @@ constexpr auto kDragDuration = crl::time(200); AlbumPreview::AlbumPreview( QWidget *parent, + const style::ComposeControls &st, gsl::span items, SendFilesWay way) : RpWidget(parent) +, _st(st) , _sendWay(way) , _dragTimer([=] { switchToDrag(); }) { setMouseTracking(true); @@ -135,6 +137,7 @@ void AlbumPreview::prepareThumbs(gsl::span items) { _thumbs.reserve(count); for (auto i = 0; i != count; ++i) { _thumbs.push_back(std::make_unique( + _st, items[i], layout[i], this, diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h index ccb64970e..7811910da 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_send_files_way.h" #include "base/timer.h" +namespace style { +struct ComposeControls; +} // namespace style + namespace Ui { struct PreparedFile; @@ -22,6 +26,7 @@ class AlbumPreview final : public RpWidget { public: AlbumPreview( QWidget *parent, + const style::ComposeControls &st, gsl::span items, SendFilesWay way); ~AlbumPreview(); @@ -86,6 +91,7 @@ private: void showContextMenu(not_null thumb, QPoint position); + const style::ComposeControls &_st; SendFilesWay _sendWay; style::cursor _cursor = style::cur_default; std::vector _order; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp index 2e71553ab..2f847c7c0 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp @@ -26,13 +26,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { AlbumThumbnail::AlbumThumbnail( + const style::ComposeControls &st, const PreparedFile &file, const GroupMediaLayout &layout, QWidget *parent, Fn repaint, Fn editCallback, Fn deleteCallback) -: _layout(layout) +: _st(st) +, _layout(layout) , _fullPreview(file.preview) , _shrinkSize(int(std::ceil(st::roundRadiusLarge / 1.4))) , _isPhoto(file.type == PreparedFile::Type::Photo) @@ -60,8 +62,8 @@ AlbumThumbnail::AlbumThumbnail( .outer = { imageWidth, imageHeight }, })); - const auto &st = st::attachPreviewThumbLayout; - const auto idealSize = st.thumbSize * style::DevicePixelRatio(); + const auto &layoutSt = st::attachPreviewThumbLayout; + const auto idealSize = layoutSt.thumbSize * style::DevicePixelRatio(); const auto fileThumbSize = (previewWidth > previewHeight) ? QSize(previewWidth * idealSize / previewHeight, idealSize) : QSize(idealSize, previewHeight * idealSize / previewWidth); @@ -70,12 +72,12 @@ AlbumThumbnail::AlbumThumbnail( fileThumbSize, { .options = Option::RoundSmall, - .outer = { st.thumbSize, st.thumbSize }, + .outer = { layoutSt.thumbSize, layoutSt.thumbSize }, })); const auto availableFileWidth = st::sendMediaPreviewSize - - st.thumbSize - - st.thumbSkip + - layoutSt.thumbSize + - layoutSt.thumbSkip // Right buttons. - st::sendBoxAlbumGroupButtonFile.width * 2 - st::sendBoxAlbumGroupEditInternalSkip * 2 @@ -99,8 +101,8 @@ AlbumThumbnail::AlbumThumbnail( } _statusWidth = st::normalFont->width(_status); - _editMedia.create(parent, st::sendBoxAlbumGroupButtonFile); - _deleteMedia.create(parent, st::sendBoxAlbumGroupButtonFile); + _editMedia.create(parent, _st.files.buttonFile); + _deleteMedia.create(parent, _st.files.buttonFile); const auto duration = st::historyAttach.ripple.hideDuration; _editMedia->setClickedCallback([=] { @@ -108,8 +110,8 @@ AlbumThumbnail::AlbumThumbnail( }); _deleteMedia->setClickedCallback(deleteCallback); - _editMedia->setIconOverride(&st::sendBoxAlbumGroupEditButtonIconFile); - _deleteMedia->setIconOverride(&st::sendBoxAlbumGroupDeleteButtonIconFile); + _editMedia->setIconOverride(&_st.files.buttonFileEdit); + _deleteMedia->setIconOverride(&_st.files.buttonFileDelete); setSpoiler(file.spoiler); setButtonVisible(false); @@ -482,7 +484,7 @@ void AlbumThumbnail::paintFile( p.drawPixmap(left, top, _fileThumb); p.setFont(st::semiboldFont); - p.setPen(st::historyFileNameInFg); + p.setPen(_st.files.nameFg); p.drawTextLeft( textLeft, top + st.nameTop, @@ -490,7 +492,7 @@ void AlbumThumbnail::paintFile( _name, _nameWidth); p.setFont(st::normalFont); - p.setPen(st::mediaInFg); + p.setPen(_st.files.statusFg); p.drawTextLeft( textLeft, top + st.statusTop, diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h index d85bd9c32..c3ffac584 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/round_rect.h" #include "base/object_ptr.h" +namespace style { +struct ComposeControls; +} // namespace style + namespace Ui { struct PreparedFile; @@ -23,6 +27,7 @@ class SpoilerAnimation; class AlbumThumbnail final { public: AlbumThumbnail( + const style::ComposeControls &st, const PreparedFile &file, const GroupMediaLayout &layout, QWidget *parent, @@ -78,6 +83,7 @@ private: float64 shrinkProgress); void paintPlayVideo(QPainter &p, QRect geometry); + const style::ComposeControls &_st; GroupMediaLayout _layout; std::optional _animateFromGeometry; const QImage _fullPreview; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp index addfc3a9a..7ad686350 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/chat/attach/attach_controls.h" -#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" namespace Ui { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp index 7973f0589..1c235c96b 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.cpp @@ -37,9 +37,10 @@ AttachControls::Type CheckControlsType( ItemSingleFilePreview::ItemSingleFilePreview( QWidget *parent, + const style::ComposeControls &st, not_null item, AttachControls::Type type) -: AbstractSingleFilePreview(parent, CheckControlsType(item, type)) { +: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type)) { const auto media = item->media(); Assert(media != nullptr); const auto document = media->document(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.h index 8a25457fd..3e8baafcd 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_file_preview.h @@ -25,6 +25,7 @@ class ItemSingleFilePreview final : public AbstractSingleFilePreview { public: ItemSingleFilePreview( QWidget *parent, + const style::ComposeControls &st, not_null item, AttachControls::Type type); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp index a04d75e4c..eb06d363b 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp @@ -32,10 +32,11 @@ using namespace ::Media::Streaming; ItemSingleMediaPreview::ItemSingleMediaPreview( QWidget *parent, + const style::ComposeControls &st, Fn gifPaused, not_null item, AttachControls::Type type) -: AbstractSingleMediaPreview(parent, type) +: AbstractSingleMediaPreview(parent, st, type) , _gifPaused(std::move(gifPaused)) , _fullId(item->fullId()) { const auto media = item->media(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.h index 07b93b73b..69c6604a4 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.h @@ -32,6 +32,7 @@ class ItemSingleMediaPreview final : public AbstractSingleMediaPreview { public: ItemSingleMediaPreview( QWidget *parent, + const style::ComposeControls &st, Fn gifPaused, not_null item, AttachControls::Type type); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp index afce50d72..4c50ee2dd 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp @@ -19,9 +19,10 @@ namespace Ui { SingleFilePreview::SingleFilePreview( QWidget *parent, + const style::ComposeControls &st, const PreparedFile &file, AttachControls::Type type) -: AbstractSingleFilePreview(parent, type) { +: AbstractSingleFilePreview(parent, st, type) { preparePreview(file); } diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h index f1b3b94ec..24647c917 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.h @@ -17,6 +17,7 @@ class SingleFilePreview final : public AbstractSingleFilePreview { public: SingleFilePreview( QWidget *parent, + const style::ComposeControls &st, const PreparedFile &file, AttachControls::Type type = AttachControls::Type::Full); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp index 41ee35c87..ed250540c 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp @@ -16,6 +16,7 @@ namespace Ui { SingleMediaPreview *SingleMediaPreview::Create( QWidget *parent, + const style::ComposeControls &st, Fn gifPaused, const PreparedFile &file, AttachControls::Type type) { @@ -43,6 +44,7 @@ SingleMediaPreview *SingleMediaPreview::Create( } return CreateChild( parent, + st, std::move(gifPaused), preview, animated, @@ -54,6 +56,7 @@ SingleMediaPreview *SingleMediaPreview::Create( SingleMediaPreview::SingleMediaPreview( QWidget *parent, + const style::ComposeControls &st, Fn gifPaused, QImage preview, bool animated, @@ -61,7 +64,7 @@ SingleMediaPreview::SingleMediaPreview( bool spoiler, const QString &animatedPreviewPath, AttachControls::Type type) -: AbstractSingleMediaPreview(parent, type) +: AbstractSingleMediaPreview(parent, st, type) , _gifPaused(std::move(gifPaused)) , _sticker(sticker) { Expects(!preview.isNull()); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h index 546de452d..ac5d4f84f 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h @@ -22,12 +22,14 @@ class SingleMediaPreview final : public AbstractSingleMediaPreview { public: static SingleMediaPreview *Create( QWidget *parent, + const style::ComposeControls &st, Fn gifPaused, const PreparedFile &file, AttachControls::Type type = AttachControls::Type::Full); SingleMediaPreview( QWidget *parent, + const style::ComposeControls &st, Fn gifPaused, QImage preview, bool animated, diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 557f5ec81..2deb51c8d 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -7,7 +7,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ using "ui/basic.style"; using "ui/widgets/widgets.style"; -using "settings/settings.style"; +using "ui/layers/layers.style"; + +PremiumLimits { + boxLabel: FlatLabel; + nonPremiumBg: color; + nonPremiumFg: color; +} + +defaultPremiumBoxLabel: FlatLabel(defaultFlatLabel) { + minWidth: 220px; + align: align(topleft); + style: TextStyle(boxTextStyle) { + lineHeight: 22px; + } +} +defaultPremiumLimits: PremiumLimits { + boxLabel: defaultPremiumBoxLabel; + nonPremiumBg: windowBgOver; + nonPremiumFg: windowFg; +} // Preview. premiumPreviewBox: Box(defaultBox) { @@ -144,8 +163,10 @@ premiumGiftUserpicPadding: margins(10px, 27px, 18px, 13px); premiumGiftTitlePadding: margins(18px, 0px, 18px, 0px); premiumGiftAboutPadding: margins(18px, 5px, 18px, 23px); premiumGiftTermsPadding: margins(18px, 27px, 18px, 0px); - -premiumGiftTerms: FlatLabel(settingLocalPasscodeDescription) { +premiumGiftTerms: FlatLabel(defaultFlatLabel) { + minWidth: 256px; + align: align(top); + textFg: windowSubTextFg; style: TextStyle(defaultTextStyle) { font: font(11px); linkFont: font(11px); diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index 29facc90e..2e9972257 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -596,12 +596,14 @@ class Line final : public Ui::RpWidget { public: Line( not_null parent, + const style::PremiumLimits &st, int max, TextFactory textFactory, int min, float64 ratio); Line( not_null parent, + const style::PremiumLimits &st, QString max, QString min, float64 ratio); @@ -614,6 +616,8 @@ protected: private: void recache(const QSize &s); + const style::PremiumLimits &_st; + int _leftWidth = 0; int _rightWidth = 0; @@ -631,12 +635,14 @@ private: Line::Line( not_null parent, + const style::PremiumLimits &st, int max, TextFactory textFactory, int min, float64 ratio) : Line( parent, + st, max ? textFactory(max) : QString(), min ? textFactory(min) : QString(), ratio) { @@ -644,10 +650,12 @@ Line::Line( Line::Line( not_null parent, + const style::PremiumLimits &st, QString max, QString min, float64 ratio) : Ui::RpWidget(parent) +, _st(st) , _leftText(st::semiboldTextStyle, tr::lng_premium_free(tr::now)) , _rightText(st::semiboldTextStyle, tr::lng_premium(tr::now)) , _rightLabel(st::semiboldTextStyle, max) @@ -689,7 +697,7 @@ void Line::paintEvent(QPaintEvent *event) { + _leftText.maxWidth() + 3 * textPadding; if (_leftWidth >= leftMinWidth) { - p.setPen(st::windowFg); + p.setPen(_st.nonPremiumFg); _leftLabel.drawRight( p, textPadding, @@ -751,7 +759,7 @@ void Line::recache(const QSize &s) { halfRect.setLeft(halfRect.center().x()); pathRect.addRect(halfRect); - p.fillPath(pathRound(_leftWidth) + pathRect, st::windowBgOver); + p.fillPath(pathRound(_leftWidth) + pathRect, _st.nonPremiumBg); _leftPixmap = std::move(leftPixmap); } @@ -811,16 +819,18 @@ void AddBubbleRow( void AddLimitRow( not_null parent, + const style::PremiumLimits &st, QString max, QString min, float64 ratio) { parent->add( - object_ptr(parent, max, min, ratio), + object_ptr(parent, st, max, min, ratio), st::boxRowPadding); } void AddLimitRow( not_null parent, + const style::PremiumLimits &st, int max, std::optional> phrase, int min, @@ -828,6 +838,7 @@ void AddLimitRow( const auto factory = ProcessTextFactory(phrase); AddLimitRow( parent, + st, max ? factory(max) : QString(), min ? factory(min) : QString(), ratio); @@ -1009,6 +1020,7 @@ QGradientStops GiftGradientStops() { void ShowListBox( not_null box, + const style::PremiumLimits &st, std::vector entries) { const auto &stLabel = st::defaultFlatLabel; @@ -1036,6 +1048,7 @@ void ShowListBox( const auto limitRow = content->add( object_ptr( content, + st, entry.rightNumber, TextFactory([=, text = ProcessTextFactory(std::nullopt)]( int n) { diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.h b/Telegram/SourceFiles/ui/effects/premium_graphics.h index de8fb6cb1..089758d93 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.h +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/round_checkbox.h" +namespace style { +struct PremiumLimits; +} // namespace style + namespace tr { template struct phrase; @@ -48,12 +52,14 @@ void AddBubbleRow( void AddLimitRow( not_null parent, + const style::PremiumLimits &st, QString max, QString min = {}, float64 ratio = kLimitRowRatio); void AddLimitRow( not_null parent, + const style::PremiumLimits &st, int max, std::optional> phrase, int min = 0, @@ -90,6 +96,7 @@ struct ListEntry final { }; void ShowListBox( not_null box, + const style::PremiumLimits &st, std::vector entries); void AddGiftOptions( diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 8ec809049..4e2563f79 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -113,6 +113,8 @@ class MainWindowShow final : public ChatHelpers::Show { public: explicit MainWindowShow(not_null controller); + void activate() override; + void showOrHideBoxOrLayer( std::variant< v::null_t, @@ -151,6 +153,12 @@ MainWindowShow::MainWindowShow(not_null controller) : _window(base::make_weak(controller)) { } +void MainWindowShow::activate() { + if (const auto window = _window.get()) { + Window::ActivateWindow(window); + } +} + void MainWindowShow::showOrHideBoxOrLayer( std::variant< v::null_t, @@ -244,10 +252,7 @@ void MainWindowShow::processChosenSticker( } // namespace void ActivateWindow(not_null controller) { - const auto window = controller->widget(); - window->raise(); - window->activateWindow(); - Ui::ActivateWindowDelayed(window); + Ui::ActivateWindow(controller->widget()); } bool IsPaused( From 1d27c8c940795af5a46aa883b9593626a8fca68c Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 22 May 2023 19:59:16 +0400 Subject: [PATCH 026/260] Paint nice stories userpics in chats list. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/data/data_stories.cpp | 8 + Telegram/SourceFiles/data/data_stories.h | 10 +- Telegram/SourceFiles/dialogs/dialogs.style | 41 +++ .../dialogs/dialogs_inner_widget.cpp | 42 ++- .../dialogs/dialogs_inner_widget.h | 9 + .../dialogs/ui/dialogs_stories_content.cpp | 187 ++++++++++++ .../dialogs/ui/dialogs_stories_content.h | 21 ++ .../dialogs/ui/dialogs_stories_list.cpp | 283 ++++++++++++++++++ .../dialogs/ui/dialogs_stories_list.h | 76 +++++ Telegram/cmake/td_ui.cmake | 3 + 11 files changed, 675 insertions(+), 7 deletions(-) create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp create mode 100644 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 3ed0b31c8..a32950631 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -591,6 +591,8 @@ PRIVATE dialogs/ui/dialogs_layout.h dialogs/ui/dialogs_message_view.cpp dialogs/ui/dialogs_message_view.h + dialogs/ui/dialogs_stories_content.cpp + dialogs/ui/dialogs_stories_content.h dialogs/ui/dialogs_topics_view.cpp dialogs/ui/dialogs_topics_view.h dialogs/ui/dialogs_video_userpic.cpp diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 827ec2e18..2fb2f8c28 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -149,6 +149,10 @@ bool Stories::allLoaded() const { return _allLoaded; } +rpl::producer<> Stories::allChanged() const { + return _allChanged.events(); +} + // #TODO stories testing StoryId Stories::generate( not_null item, @@ -244,10 +248,14 @@ StoryId Stories::generate( void Stories::pushToBack(StoriesList &&list) { const auto i = ranges::find(_all, list.user, &StoriesList::user); if (i != end(_all)) { + if (*i == list) { + return; + } *i = std::move(list); } else { _all.push_back(std::move(list)); } + _allChanged.fire({}); } void Stories::pushToFront(StoriesList &&list) { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 373f90b95..ce8ca4d60 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -30,6 +30,7 @@ struct StoryItem { TextWithEntities caption; TimeId date = 0; StoryPrivacy privacy; + bool pinned = false; friend inline bool operator==(StoryItem, StoryItem) = default; }; @@ -37,8 +38,11 @@ struct StoryItem { struct StoriesList { not_null user; std::vector items; + StoryId readTill = 0; int total = 0; + [[nodiscard]] bool unread() const; + friend inline bool operator==(StoriesList, StoriesList) = default; }; @@ -61,11 +65,14 @@ public: explicit Stories(not_null owner); ~Stories(); + [[nodiscard]] Session &owner() const; + void loadMore(); void apply(const MTPDupdateStories &data); [[nodiscard]] const std::vector &all(); [[nodiscard]] bool allLoaded() const; + [[nodiscard]] rpl::producer<> allChanged() const; // #TODO stories testing [[nodiscard]] StoryId generate( @@ -76,7 +83,7 @@ public: not_null> media); private: - [[nodiscard]] StoriesList parse(const MTPUserStories &data); + [[nodiscard]] StoriesList parse(const MTPUserStories &stories); [[nodiscard]] std::optional parse(const MTPDstoryItem &data); void pushToBack(StoriesList &&list); @@ -85,6 +92,7 @@ private: const not_null _owner; std::vector _all; + rpl::event_stream<> _allChanged; QString _state; bool _allLoaded = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index f1ae4dbaa..c520e9de3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -485,3 +485,44 @@ chooseTopicListItem: PeerListItem(defaultPeerListItem) { chooseTopicList: PeerList(defaultPeerList) { item: chooseTopicListItem; } + +DialogsStories { + left: pixels; + height: pixels; + photo: pixels; + photoLeft: pixels; + photoTop: pixels; + shift: pixels; + lineTwice: pixels; + lineReadTwice: pixels; + nameTop: pixels; + nameStyle: TextStyle; +} + +dialogsStories: DialogsStories { + left: 4px; + height: 35px; + photo: 24px; + photoLeft: 10px; + shift: 16px; + lineTwice: 3px; + lineReadTwice: 0px; + nameTop: 9px; + nameStyle: semiboldTextStyle; +} + +dialogsStoriesFull: DialogsStories { + left: 4px; + height: 77px; + photo: 42px; + photoLeft: 10px; + photoTop: 9px; + lineTwice: 4px; + lineReadTwice: 2px; + nameTop: 58px; + nameStyle: TextStyle(defaultTextStyle) { + font: font(12px); + linkFont: font(12px); + linkFontOver: font(12px); + } +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index b763ca574..5c29a8a36 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_inner_widget.h" -#include "dialogs/dialogs_indexed_list.h" #include "dialogs/ui/dialogs_layout.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" #include "dialogs/ui/dialogs_video_userpic.h" +#include "dialogs/dialogs_indexed_list.h" #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "history/history.h" @@ -137,6 +139,10 @@ InnerWidget::InnerWidget( rpl::producer childListShown) : RpWidget(parent) , _controller(controller) +, _stories(std::make_unique( + this, + Stories::ContentForSession(&controller->session()), + [=] { return st::dialogsStoriesFull.height - _visibleTop; })) , _shownList(controller->session().data().chatsList()->indexed()) , _st(&st::defaultDialogRow) , _pinnedShiftAnimation([=](crl::time now) { @@ -406,8 +412,19 @@ int InnerWidget::skipTopHeight() const { : 0; } +bool InnerWidget::storiesShown() const { + return (_state == WidgetState::Default) + && !_openedFolder + && !_openedForum; +} + +int InnerWidget::collapsedRowsOffset() const { + return storiesShown() ? _stories->height() : 0; +} + int InnerWidget::dialogsOffset() const { - return _collapsedRows.size() * st::dialogsImportantBarHeight + return collapsedRowsOffset() + + (_collapsedRows.size() * st::dialogsImportantBarHeight) - skipTopHeight(); } @@ -493,6 +510,7 @@ void InnerWidget::changeOpenedFolder(Data::Folder *folder) { stopReorderPinned(); clearSelection(); _openedFolder = folder; + _stories->setVisible(storiesShown()); refreshShownList(); refreshWithCollapsedRows(true); if (_loadMoreCallback) { @@ -519,6 +537,7 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) { } _openedForum = forum; _st = forum ? &st::forumTopicRow : &st::defaultDialogRow; + _stories->setVisible(storiesShown()); refreshShownList(); _openedForumLifetime.destroy(); @@ -596,7 +615,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), context); }; if (_state == WidgetState::Default) { - paintCollapsedRows(p, r); + const auto collapsedSkip = collapsedRowsOffset(); + p.translate(0, collapsedSkip); + paintCollapsedRows(p, r.translated(0, -collapsedSkip)); const auto &list = _shownList->all(); const auto shownBottom = _shownList->height() - skipTopHeight(); @@ -1748,6 +1769,7 @@ void InnerWidget::setSearchedPressed(int pressed) { } void InnerWidget::resizeEvent(QResizeEvent *e) { + _stories->resizeToWidth(width()); resizeEmptyLabel(); moveCancelSearchButtons(); } @@ -2255,7 +2277,7 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { if (_filter.isEmpty() && !_searchFromPeer) { clearFilter(); } else { - _state = WidgetState::Filtered; + setState(WidgetState::Filtered); _waitingForSearch = true; _filterResults.clear(); _filterResultsGlobal.clear(); @@ -2506,6 +2528,7 @@ void InnerWidget::visibleTopBottomUpdated( int visibleBottom) { _visibleTop = visibleTop; _visibleBottom = visibleBottom; + _stories->update(); preloadRowsData(); const auto loadTill = _visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop); @@ -2915,10 +2938,10 @@ void InnerWidget::repaintSearchResult(int index) { void InnerWidget::clearFilter() { if (_state == WidgetState::Filtered || _searchInChat) { if (_searchInChat) { - _state = WidgetState::Filtered; + setState(WidgetState::Filtered); _waitingForSearch = true; } else { - _state = WidgetState::Default; + setState(WidgetState::Default); } _hashtagResults.clear(); _filterResults.clear(); @@ -2930,6 +2953,13 @@ void InnerWidget::clearFilter() { } } +void InnerWidget::setState(WidgetState state) { + if (_state != state) { + _state = state; + _stories->setVisible(storiesShown()); + } +} + void InnerWidget::selectSkip(int32 direction) { clearMouseSelection(); if (_state == WidgetState::Default) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 6b01e940f..56e71ec33 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -52,6 +52,10 @@ struct PaintContext; struct TopicJumpCache; } // namespace Dialogs::Ui +namespace Dialogs::Stories { +class List; +} // namespace Dialogs::Stories + namespace Dialogs { class Row; @@ -219,6 +223,7 @@ private: void dialogRowReplaced(Row *oldRow, Row *newRow); + void setState(WidgetState state); void editOpenedFilter(); void repaintCollapsedFolderRow(not_null folder); void refreshWithCollapsedRows(bool toTop = false); @@ -309,7 +314,9 @@ private: void fillArchiveSearchMenu(not_null menu); void refreshShownList(); + [[nodiscard]] bool storiesShown() const; [[nodiscard]] int skipTopHeight() const; + [[nodiscard]] int collapsedRowsOffset() const; [[nodiscard]] int dialogsOffset() const; [[nodiscard]] int shownHeight(int till = -1) const; [[nodiscard]] int fixedOnTopCount() const; @@ -394,6 +401,8 @@ private: const not_null _controller; + const std::unique_ptr _stories; + not_null _shownList; FilterId _filterId = 0; bool _mouseSelection = false; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp new file mode 100644 index 000000000..d4844a582 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -0,0 +1,187 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "dialogs/ui/dialogs_stories_content.h" + +#include "data/data_changes.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_user.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "main/main_session.h" +#include "ui/painter.h" + +#include "history/history.h" // #TODO stories testing + +namespace Dialogs::Stories { +namespace { + +class PeerUserpic final : public Userpic { +public: + explicit PeerUserpic(not_null peer); + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + struct Subscribed { + explicit Subscribed(Fn callback) + : callback(std::move(callback)) { + } + + Ui::PeerUserpicView view; + Fn callback; + InMemoryKey key; + rpl::lifetime photoLifetime; + rpl::lifetime downloadLifetime; + }; + + [[nodiscard]] bool waitingUserpicLoad() const; + void processNewPhoto(); + + const not_null _peer; + QImage _frame; + std::unique_ptr _subscribed; + +}; + +class State final { +public: + explicit State(not_null data); + + [[nodiscard]] Content next(); + +private: + const not_null _data; + base::flat_map, std::shared_ptr> _userpics; + +}; + +PeerUserpic::PeerUserpic(not_null peer) +: _peer(peer) { +} + +QImage PeerUserpic::image(int size) { + Expects(_subscribed != nullptr); + + const auto good = (_frame.width() == size * _frame.devicePixelRatio()); + const auto key = _peer->userpicUniqueKey(_subscribed->view); + if (!good || (_subscribed->key != key && !waitingUserpicLoad())) { + _subscribed->key = key; + _frame = QImage( + QSize(size, size) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(style::DevicePixelRatio()); + _frame.fill(Qt::transparent); + + auto p = Painter(&_frame); + _peer->paintUserpic(p, _subscribed->view, 0, 0, size); + } + return _frame; +} + +bool PeerUserpic::waitingUserpicLoad() const { + return _peer->hasUserpic() && _peer->useEmptyUserpic(_subscribed->view); +} + +void PeerUserpic::subscribeToUpdates(Fn callback) { + if (!callback) { + _subscribed = nullptr; + return; + } + _subscribed = std::make_unique(std::move(callback)); + + _peer->session().changes().peerUpdates( + _peer, + Data::PeerUpdate::Flag::Photo + ) | rpl::start_with_next([=] { + _subscribed->callback(); + processNewPhoto(); + }, _subscribed->photoLifetime); + + processNewPhoto(); +} + +void PeerUserpic::processNewPhoto() { + Expects(_subscribed != nullptr); + + if (!waitingUserpicLoad()) { + _subscribed->downloadLifetime.destroy(); + return; + } + _peer->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return !waitingUserpicLoad(); + }) | rpl::start_with_next([=] { + _subscribed->callback(); + _subscribed->downloadLifetime.destroy(); + }, _subscribed->downloadLifetime); +} + +State::State(not_null data) +: _data(data) { +} + +Content State::next() { + auto result = Content(); +#if 0 // #TODO stories testing + const auto &all = _data->all(); + result.users.reserve(all.size()); + for (const auto &list : all) { + auto userpic = std::shared_ptr(); + const auto user = list.user; +#endif + const auto list = _data->owner().chatsList(); + const auto &all = list->indexed()->all(); + result.users.reserve(all.size()); + for (const auto &entry : all) { + if (const auto history = entry->history()) { + if (const auto user = history->peer->asUser(); user && !user->isBot()) { + auto userpic = std::shared_ptr(); + if (const auto i = _userpics.find(user); i != end(_userpics)) { + userpic = i->second; + } else { + userpic = std::make_shared(user); + _userpics.emplace(user, userpic); + } + result.users.push_back({ + .id = uint64(user->id.value), + .name = user->shortName(), + .userpic = std::move(userpic), + .unread = history->chatListBadgesState().unread// list.unread(), + }); + } + } + } + return result; +} + +} // namespace + +rpl::producer ContentForSession(not_null session) { + return [=](auto consumer) { + auto result = rpl::lifetime(); + const auto stories = &session->data().stories(); + const auto state = result.make_state(stories); + rpl::single( + rpl::empty + ) | rpl::then( +#if 0 // #TODO stories testing + stories->allChanged() +#endif + session->data().chatsListChanges( + ) | rpl::filter( + rpl::mappers::_1 == nullptr + ) | rpl::to_empty + ) | rpl::start_with_next([=] { + consumer.put_next(state->next()); + }, result); + return result; + }; +} + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h new file mode 100644 index 000000000..dc81f2528 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h @@ -0,0 +1,21 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Main { +class Session; +} // namespace Main + +namespace Dialogs::Stories { + +struct Content; + +[[nodiscard]] rpl::producer ContentForSession( + not_null session); + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp new file mode 100644 index 000000000..b87d4b5de --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -0,0 +1,283 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "dialogs/ui/dialogs_stories_list.h" + +#include "ui/painter.h" +#include "styles/style_dialogs.h" + +namespace Dialogs::Stories { +namespace { + +constexpr auto kSmallUserpicsShown = 3; +constexpr auto kSmallReadOpacity = 0.6; + +} // namespace + +List::List( + not_null parent, + rpl::producer content, + Fn shownHeight) +: RpWidget(parent) +, _shownHeight(shownHeight) { + resize(0, st::dialogsStoriesFull.height); + + std::move(content) | rpl::start_with_next([=](Content &&content) { + showContent(std::move(content)); + }, lifetime()); +} + +void List::showContent(Content &&content) { + if (_content == content) { + return; + } + _content = std::move(content); + auto items = base::take(_items); + _items.reserve(_content.users.size()); + for (const auto &user : _content.users) { + const auto i = ranges::find(items, user.id, [](const Item &item) { + return item.user.id; + }); + if (i != end(items)) { + _items.push_back(std::move(*i)); + auto &item = _items.back(); + if (item.user.userpic != user.userpic) { + item.user.userpic = user.userpic; + item.subscribed = false; + } + if (item.user.name != user.name) { + item.user.name = user.name; + item.nameCache = QImage(); + } + } else { + _items.emplace_back(Item{ .user = user }); + } + } + update(); +} + +rpl::producer List::clicks() const { + return _clicks.events(); +} + +rpl::producer<> List::expandRequests() const { + return _expandRequests.events(); +} + +void List::paintEvent(QPaintEvent *e) { + const auto &st = st::dialogsStories; + const auto &full = st::dialogsStoriesFull; + const auto shownHeight = std::max(_shownHeight(), st.height); + const auto ratio = float64(shownHeight - st.height) + / (full.height - st.height); + const auto lerp = [=](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + const auto photo = lerp(st.photo, full.photo); + const auto photoTopSmall = (st.height - st.photo) / 2.; + const auto photoTop = lerp(photoTopSmall, full.photoTop); + const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; + const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; + const auto nameTop = (photoTop + photo) + * (full.nameTop / float64(full.photoTop + full.photo)); + const auto infoTop = st.nameTop + - (st.photoTop + (st.photo / 2.)) + + (photoTop + (photo / 2.)); + const auto singleSmall = st.shift; + const auto singleFull = full.photoLeft * 2 + full.photo; + const auto single = lerp(singleSmall, singleFull); + const auto itemsCount = int(_items.size()); + const auto leftSmall = st.left; + const auto leftFull = full.left - _scrollLeft; + const auto startIndexFull = std::max(-leftFull, 0) / singleFull; + const auto cellLeftFull = leftFull + (startIndexFull * singleFull); + const auto endIndexFull = std::min( + (width() - cellLeftFull + singleFull - 1) / singleFull, + itemsCount); + const auto startIndexSmall = 0; + const auto endIndexSmall = std::min(kSmallUserpicsShown, itemsCount); + const auto cellLeftSmall = leftSmall; + const auto userpicLeftFull = cellLeftFull + full.photoLeft; + const auto userpicLeftSmall = cellLeftSmall + st.photoLeft; + const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull); + const auto photoLeft = lerp(st.photoLeft, full.photoLeft); + const auto left = userpicLeft - photoLeft; + const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.); + const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.); + + auto p = QPainter(this); + p.fillRect(e->rect(), st::dialogsBg); + p.translate(0, height() - shownHeight); + + const auto drawSmall = (ratio < 1.); + const auto drawFull = (ratio > 0.); + auto hq = PainterHighQualityEnabler(p); + + const auto subscribe = [&](not_null item) { + if (!item->subscribed) { + item->subscribed = true; + //const auto id = item.user.id; + item->user.userpic->subscribeToUpdates([=] { + update(); + }); + } + }; + const auto count = std::max( + endIndexFull - startIndexFull, + endIndexSmall - startIndexSmall); + + struct Single { + float64 x = 0.; + int indexSmall = 0; + Item *itemSmall = nullptr; + int indexFull = 0; + Item *itemFull = nullptr; + + explicit operator bool() const { + return itemSmall || itemFull; + } + }; + const auto lookup = [&](int index) { + const auto indexSmall = startIndexSmall + index; + const auto indexFull = startIndexFull + index; + const auto small = (drawSmall && indexSmall < endIndexSmall) + ? &_items[indexSmall] + : nullptr; + const auto full = (drawFull && indexFull < endIndexFull) + ? &_items[indexFull] + : nullptr; + const auto x = left + single * index; + return Single{ x, indexSmall, small, indexFull, full }; + }; + const auto hasUnread = [&](const Single &single) { + return (single.itemSmall && single.itemSmall->user.unread) + || (single.itemFull && single.itemFull->user.unread); + }; + const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) { + auto nextGradientPainted = false; + for (auto i = count; i != 0;) { + --i; + const auto gradientPainted = nextGradientPainted; + nextGradientPainted = false; + if (const auto current = lookup(i)) { + if (!gradientPainted) { + paintGradient(current); + } + if (i > 0 && hasUnread(current)) { + if (const auto next = lookup(i - 1)) { + if (current.itemSmall || !next.itemSmall) { + nextGradientPainted = true; + paintGradient(next); + } + } + } + paintOther(current); + } + } + }; + enumerate([&](Single single) { + // Unread gradient. + const auto x = single.x; + const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo); + const auto small = single.itemSmall; + const auto itemFull = single.itemFull; + const auto smallUnread = small && small->user.unread; + const auto fullUnread = itemFull && itemFull->user.unread; + const auto unreadOpacity = (smallUnread && fullUnread) + ? 1. + : smallUnread + ? (1. - ratio) + : fullUnread + ? ratio + : 0.; + if (unreadOpacity > 0.) { + p.setOpacity(unreadOpacity); + const auto outerAdd = 2 * line; + const auto outer = userpic.marginsAdded( + { outerAdd, outerAdd, outerAdd, outerAdd }); + p.setPen(Qt::NoPen); + auto gradient = QLinearGradient( + userpic.topRight(), + userpic.bottomLeft()); + gradient.setStops({ + { 0., st::groupCallLive1->c }, + { 1., st::groupCallMuted1->c }, + }); + p.setBrush(gradient); + p.drawEllipse(outer); + p.setOpacity(1.); + } + }, [&](Single single) { + Expects(single.itemSmall || single.itemFull); + + const auto x = single.x; + const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo); + const auto small = single.itemSmall; + const auto itemFull = single.itemFull; + const auto smallUnread = small && small->user.unread; + const auto fullUnread = itemFull && itemFull->user.unread; + + // White circle with possible read gray line. + if (itemFull && !fullUnread) { + auto color = st::dialogsUnreadBgMuted->c; + color.setAlphaF(color.alphaF() * ratio); + auto pen = QPen(color); + pen.setWidthF(lineRead); + p.setPen(pen); + } else { + p.setPen(Qt::NoPen); + } + const auto add = line + (itemFull ? (lineRead / 2.) : 0.); + const auto rect = userpic.marginsAdded({ add, add, add, add }); + p.setBrush(st::dialogsBg); + p.drawEllipse(rect); + + // Userpic. + if (itemFull == small) { + p.setOpacity(smallUnread ? 1. : readUserpicOpacity); + subscribe(itemFull); + const auto size = full.photo; + p.drawImage(userpic, itemFull->user.userpic->image(size)); + } else { + if (small) { + p.setOpacity(smallUnread + ? (itemFull ? 1. : (1. - ratio)) + : (itemFull + ? kSmallReadOpacity + : readUserpicAppearingOpacity)); + subscribe(small); + const auto size = (ratio > 0.) ? full.photo : st.photo; + p.drawImage(userpic, small->user.userpic->image(size)); + } + if (itemFull) { + p.setOpacity(ratio); + subscribe(itemFull); + const auto size = full.photo; + p.drawImage(userpic, itemFull->user.userpic->image(size)); + } + } + p.setOpacity(1.); + }); +} + +void List::wheelEvent(QWheelEvent *e) { + +} + +void List::mouseMoveEvent(QMouseEvent *e) { + +} + +void List::mousePressEvent(QMouseEvent *e) { + +} + +void List::mouseReleaseEvent(QMouseEvent *e) { + +} + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h new file mode 100644 index 000000000..c0b80501e --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -0,0 +1,76 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/qt/qt_compare.h" +#include "ui/rp_widget.h" + +class QPainter; + +namespace Dialogs::Stories { + +class Userpic { +public: + [[nodiscard]] virtual QImage image(int size) = 0; + virtual void subscribeToUpdates(Fn callback) = 0; +}; + +struct User { + uint64 id = 0; + QString name; + std::shared_ptr userpic; + bool unread = false; + + friend inline bool operator==(const User &a, const User &b) = default; +}; + +struct Content { + std::vector users; + + friend inline bool operator==( + const Content &a, + const Content &b) = default; +}; + +class List final : public Ui::RpWidget { +public: + List( + not_null parent, + rpl::producer content, + Fn shownHeight); + + [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer<> expandRequests() const; + +private: + struct Item { + User user; + QImage frameSmall; + QImage frameFull; + QImage nameCache; + QColor nameCacheColor; + bool subscribed = false; + }; + + void showContent(Content &&content); + void paintEvent(QPaintEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + + Content _content; + std::vector _items; + Fn _shownHeight = 0; + rpl::event_stream _clicks; + rpl::event_stream<> _expandRequests; + int _scrollLeft = 0; + +}; + +} // namespace Dialogs::Stories diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 404f72080..08b9990c4 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -70,6 +70,9 @@ PRIVATE data/data_subscription_option.h + dialogs/ui/dialogs_stories_list.cpp + dialogs/ui/dialogs_stories_list.h + editor/controllers/undo_controller.cpp editor/controllers/undo_controller.h editor/editor_crop.cpp From 16128d61c0fd59f19b3fb00585d7421e40290f03 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 23 May 2023 20:13:02 +0400 Subject: [PATCH 027/260] Implement nice stories list scrolling. --- .../dialogs/dialogs_inner_widget.cpp | 45 +++++- .../dialogs/dialogs_inner_widget.h | 6 + .../SourceFiles/dialogs/dialogs_widget.cpp | 43 ++++-- Telegram/SourceFiles/dialogs/dialogs_widget.h | 3 +- .../dialogs/ui/dialogs_stories_content.cpp | 10 +- .../dialogs/ui/dialogs_stories_list.cpp | 142 +++++++++++++++++- .../dialogs/ui/dialogs_stories_list.h | 24 ++- .../window/window_session_controller.cpp | 4 +- 8 files changed, 248 insertions(+), 29 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 5c29a8a36..a4c0bb267 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -142,7 +142,7 @@ InnerWidget::InnerWidget( , _stories(std::make_unique( this, Stories::ContentForSession(&controller->session()), - [=] { return st::dialogsStoriesFull.height - _visibleTop; })) + [=] { return _stories->height() - _visibleTop; })) , _shownList(controller->session().data().chatsList()->indexed()) , _st(&st::defaultDialogRow) , _pinnedShiftAnimation([=](crl::time now) { @@ -323,6 +323,18 @@ InnerWidget::InnerWidget( switchToFilter(filterId); }, lifetime()); + _stories->heightValue( + ) | rpl::filter([=] { + return (_viewportHeight > 0) && (defaultScrollTop() > _visibleTop); + }) | rpl::start_with_next([=] { + jumpToTop(); + }, lifetime()); + + _stories->entered( + ) | rpl::start_with_next([=] { + clearSelection(); + }, lifetime()); + handleChatListEntryRefreshes(); refreshWithCollapsedRows(true); @@ -428,6 +440,16 @@ int InnerWidget::dialogsOffset() const { - skipTopHeight(); } +rpl::producer<> InnerWidget::scrollToVeryTopRequests() const { + return _stories->expandRequests(); +} + +int InnerWidget::defaultScrollTop() const { + return storiesShown() + ? std::max(_stories->height() - st::dialogsStories.height, 0) + : 0; +} + int InnerWidget::fixedOnTopCount() const { auto result = 0; for (const auto &row : *_shownList) { @@ -1699,6 +1721,15 @@ void InnerWidget::mousePressReleased( } } +void InnerWidget::setViewportHeight(int viewportHeight) { + if (_viewportHeight != viewportHeight) { + _viewportHeight = viewportHeight; + if (height() < defaultScrollTop() + viewportHeight) { + refresh(); + } + } +} + void InnerWidget::setCollapsedPressed(int pressed) { if (_collapsedPressed != pressed) { if (_collapsedPressed >= 0) { @@ -2745,10 +2776,13 @@ void InnerWidget::refresh(bool toTop) { h = searchedOffset() + (_searchResults.size() * _st->height); } } + if (const auto storiesSkip = defaultScrollTop()) { + accumulate_max(h, storiesSkip + _viewportHeight); + } resize(width(), h); if (toTop) { stopReorderPinned(); - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); preloadRowsData(); } _controller->setDialogsListDisplayForced( @@ -3226,7 +3260,7 @@ void InnerWidget::switchToFilter(FilterId filterId) { filterId = 0; } if (_filterId == filterId) { - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); return; } saveChatsFilterScrollState(_filterId); @@ -3251,6 +3285,11 @@ void InnerWidget::switchToFilter(FilterId filterId) { } } +void InnerWidget::jumpToTop() { + const auto to = defaultScrollTop(); + _mustScrollTo.fire({ to, -1 }); +} + void InnerWidget::saveChatsFilterScrollState(FilterId filterId) { _chatsFilterScrollStates[filterId] = -y(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 56e71ec33..c7b2d899f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -104,6 +104,10 @@ public: const QVector &my, const QVector &result); + [[nodiscard]] rpl::producer<> scrollToVeryTopRequests() const; + [[nodiscard]] int defaultScrollTop() const; + void setViewportHeight(int viewportHeight); + [[nodiscard]] FilterId filterId() const; void clearSelection(); @@ -279,6 +283,7 @@ private: int defaultRowTop(not_null row) const; void setupOnlineStatusCheck(); + void jumpToTop(); void updateRowCornerStatusShown(not_null history); void repaintDialogRowCornerStatus(not_null history); @@ -402,6 +407,7 @@ private: const not_null _controller; const std::unique_ptr _stories; + int _viewportHeight = 0; not_null _shownList; FilterId _filterId = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index dc8f3243e..24ff876e9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -260,6 +260,11 @@ Widget::Widget( } }, lifetime()); + _inner->scrollToVeryTopRequests( + ) | rpl::start_with_next([=] { + scrollToDefaultChecked(true); + }, lifetime()); + _inner->mustScrollTo( ) | rpl::start_with_next([=](const Ui::ScrollToRequest &data) { if (_scroll) { @@ -527,13 +532,15 @@ void Widget::setGeometryWithTopMoved( _topDelta = 0; } +void Widget::scrollToDefaultChecked(bool verytop) { + if (_scrollToAnimation.animating()) { + return; + } + scrollToDefault(verytop); +} + void Widget::setupScrollUpButton() { - _scrollToTop->setClickedCallback([=] { - if (_scrollToAnimation.animating()) { - return; - } - scrollToTop(); - }); + _scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); }); base::install_event_filter(_scrollToTop, [=](not_null event) { if (event->type() != QEvent::Wheel) { return base::EventFilterResult::Continue; @@ -1111,10 +1118,13 @@ void Widget::jumpToTop(bool belowPinned) { } } -void Widget::scrollToTop() { +void Widget::scrollToDefault(bool verytop) { _scrollToAnimation.stop(); auto scrollTop = _scroll->scrollTop(); - const auto scrollTo = 0; + const auto scrollTo = verytop ? 0 : _inner->defaultScrollTop(); + if (scrollTop <= scrollTo) { + return; + } const auto maxAnimatedDelta = _scroll->height(); if (scrollTo + maxAnimatedDelta < scrollTop) { scrollTop = scrollTo + maxAnimatedDelta; @@ -2494,7 +2504,11 @@ void Widget::updateControlsGeometry() { } auto scrollTop = forumReportTop + (_forumReportBar ? _forumReportBar->bar().height() : 0); - auto newScrollTop = _scroll->scrollTop() + _topDelta; + const auto wasScrollTop = _scroll->scrollTop(); + const auto newScrollTop = (_topDelta < 0 + && wasScrollTop <= _inner->defaultScrollTop()) + ? wasScrollTop + : (wasScrollTop + _topDelta); auto scrollHeight = height() - scrollTop; const auto putBottomButton = [&](auto &button) { if (button && !button->isHidden()) { @@ -2518,13 +2532,22 @@ void Widget::updateControlsGeometry() { const auto scrollw = _childList ? _narrowWidth : barw; const auto wasScrollHeight = _scroll->height(); + if (scrollHeight >= wasScrollHeight) { + _inner->setViewportHeight(scrollHeight); + } _scroll->setGeometry(0, scrollTop, scrollw, scrollHeight); + if (scrollHeight < wasScrollHeight) { + _inner->setViewportHeight(scrollHeight); + } _inner->resize(scrollw, _inner->height()); _inner->setNarrowRatio(narrowRatio); if (scrollHeight != wasScrollHeight) { controller()->floatPlayerAreaUpdated(); } - if (_topDelta) { + const auto startWithTop = _inner->defaultScrollTop(); + if (wasScrollHeight < startWithTop && scrollHeight >= startWithTop) { + _scroll->scrollToY(startWithTop); + } else if (newScrollTop != wasScrollTop) { _scroll->scrollToY(newScrollTop); } else { listScrollUpdated(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 009f6ca3a..891afef3a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -209,7 +209,8 @@ private: mtpRequestId requestId); void peopleFailed(const MTP::Error &error, mtpRequestId requestId); - void scrollToTop(); + void scrollToDefault(bool verytop = false); + void scrollToDefaultChecked(bool verytop = false); void setupScrollUpButton(); void updateScrollUpVisibility(); void startScrollUpButtonAnimation(bool shown); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index d4844a582..021d07328 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -173,10 +173,12 @@ rpl::producer ContentForSession(not_null session) { #if 0 // #TODO stories testing stories->allChanged() #endif - session->data().chatsListChanges( - ) | rpl::filter( - rpl::mappers::_1 == nullptr - ) | rpl::to_empty + rpl::merge( + session->data().chatsListChanges( + ) | rpl::filter( + rpl::mappers::_1 == nullptr + ) | rpl::to_empty, + session->data().unreadBadgeChanges()) ) | rpl::start_with_next([=] { consumer.put_next(state->next()); }, result); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index b87d4b5de..c4dfbbbd2 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "styles/style_dialogs.h" +#include + namespace Dialogs::Stories { namespace { @@ -24,19 +26,31 @@ List::List( Fn shownHeight) : RpWidget(parent) , _shownHeight(shownHeight) { - resize(0, st::dialogsStoriesFull.height); + setCursor(style::cur_default); std::move(content) | rpl::start_with_next([=](Content &&content) { showContent(std::move(content)); }, lifetime()); + + _shownAnimation.stop(); + resize(0, _items.empty() ? 0 : st::dialogsStoriesFull.height); } void List::showContent(Content &&content) { if (_content == content) { return; } + if (content.users.empty()) { + _hidingItems = base::take(_items); + if (!_hidingItems.empty()) { + toggleAnimated(false); + } + return; + } + const auto hidden = _content.users.empty(); _content = std::move(content); - auto items = base::take(_items); + auto items = base::take(_items.empty() ? _hidingItems : _items); + _hidingItems.clear(); _items.reserve(_content.users.size()); for (const auto &user : _content.users) { const auto i = ranges::find(items, user.id, [](const Item &item) { @@ -53,10 +67,39 @@ void List::showContent(Content &&content) { item.user.name = user.name; item.nameCache = QImage(); } + item.user.unread = user.unread; } else { _items.emplace_back(Item{ .user = user }); } } + updateScrollMax(); + update(); + if (hidden) { + toggleAnimated(true); + } +} + +void List::toggleAnimated(bool shown) { + _shownAnimation.start( + [=] { updateHeight(); }, + shown ? 0. : 1., + shown ? 1. : 0., + st::slideWrapDuration); +} + +void List::updateHeight() { + const auto shown = _shownAnimation.value(_items.empty() ? 0. : 1.); + resize( + width(), + anim::interpolate(0, st::dialogsStoriesFull.height, shown)); +} + +void List::updateScrollMax() { + const auto &full = st::dialogsStoriesFull; + const auto singleFull = full.photoLeft * 2 + full.photo; + const auto widthFull = full.left + int(_items.size()) * singleFull; + _scrollLeftMax = std::max(widthFull - width(), 0); + _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); update(); } @@ -68,6 +111,18 @@ rpl::producer<> List::expandRequests() const { return _expandRequests.events(); } +rpl::producer<> List::entered() const { + return _entered.events(); +} + +void List::enterEventHook(QEnterEvent *e) { + _entered.fire({}); +} + +void List::resizeEvent(QResizeEvent *e) { + updateScrollMax(); +} + void List::paintEvent(QPaintEvent *e) { const auto &st = st::dialogsStories; const auto &full = st::dialogsStoriesFull; @@ -96,7 +151,7 @@ void List::paintEvent(QPaintEvent *e) { const auto startIndexFull = std::max(-leftFull, 0) / singleFull; const auto cellLeftFull = leftFull + (startIndexFull * singleFull); const auto endIndexFull = std::min( - (width() - cellLeftFull + singleFull - 1) / singleFull, + (width() - leftFull + singleFull - 1) / singleFull, itemsCount); const auto startIndexSmall = 0; const auto endIndexSmall = std::min(kSmallUserpicsShown, itemsCount); @@ -265,19 +320,92 @@ void List::paintEvent(QPaintEvent *e) { } void List::wheelEvent(QWheelEvent *e) { + const auto horizontal = (e->angleDelta().x() != 0); + if (!horizontal) { + e->ignore(); + return; + } + auto delta = horizontal + ? ((style::RightToLeft() ? -1 : 1) * (e->pixelDelta().x() + ? e->pixelDelta().x() + : e->angleDelta().x())) + : (e->pixelDelta().y() + ? e->pixelDelta().y() + : e->angleDelta().y()); -} - -void List::mouseMoveEvent(QMouseEvent *e) { - + const auto now = _scrollLeft; + const auto used = now - delta; + const auto next = std::clamp(used, 0, _scrollLeftMax); + if (next != now) { + _expandRequests.fire({}); + _scrollLeft = next; + //updateSelected(); + update(); + } + e->accept(); } void List::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + _mouseDownPosition = _lastMousePosition = e->globalPos(); + //updateSelected(); +} +void List::mouseMoveEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + //updateSelected(); + + if (!_dragging && _mouseDownPosition) { + if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() + >= QApplication::startDragDistance()) { + if (_shownHeight() < st::dialogsStoriesFull.height) { + _expandRequests.fire({}); + } + _dragging = true; + _startDraggingLeft = _scrollLeft; + } + } + checkDragging(); +} + +void List::checkDragging() { + if (_dragging) { + const auto sign = (style::RightToLeft() ? -1 : 1); + const auto newLeft = std::clamp( + (sign * (_mouseDownPosition->x() - _lastMousePosition.x()) + + _startDraggingLeft), + 0, + _scrollLeftMax); + if (newLeft != _scrollLeft) { + _scrollLeft = newLeft; + update(); + } + } } void List::mouseReleaseEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + const auto guard = gsl::finally([&] { + _mouseDownPosition = std::nullopt; + }); + //const auto wasDown = std::exchange(_pressed, SpecialOver::None); + if (finishDragging()) { + return; + } + //updateSelected(); +} + +bool List::finishDragging() { + if (!_dragging) { + return false; + } + checkDragging(); + _dragging = false; + //updateSelected(); + return true; } } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index c0b80501e..d8b1bdbae 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -46,30 +46,48 @@ public: [[nodiscard]] rpl::producer clicks() const; [[nodiscard]] rpl::producer<> expandRequests() const; + [[nodiscard]] rpl::producer<> entered() const; private: struct Item { User user; - QImage frameSmall; - QImage frameFull; QImage nameCache; QColor nameCacheColor; bool subscribed = false; }; void showContent(Content &&content); + void enterEventHook(QEnterEvent *e) override; + void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; void wheelEvent(QWheelEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; + void updateScrollMax(); + void checkDragging(); + bool finishDragging(); + + void updateHeight(); + void toggleAnimated(bool shown); + Content _content; std::vector _items; + std::vector _hidingItems; Fn _shownHeight = 0; rpl::event_stream _clicks; rpl::event_stream<> _expandRequests; + rpl::event_stream<> _entered; + + Ui::Animations::Simple _shownAnimation; + + QPoint _lastMousePosition; + std::optional _mouseDownPosition; + int _startDraggingLeft = 0; int _scrollLeft = 0; + int _scrollLeftMax = 0; + bool _dragging = false; }; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 4e2563f79..34d9ca1be 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1141,7 +1141,9 @@ void SessionController::openFolder(not_null folder) { if (_openedFolder.current() != folder) { resetFakeUnreadWhileOpened(); } - setActiveChatsFilter(0); + if (activeChatsFilterCurrent() != 0) { + setActiveChatsFilter(0); + } closeForum(); _openedFolder = folder.get(); } From 1fc37178b7f0c9d6e768654a0d9ec8e777280849 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 23 May 2023 21:11:58 +0400 Subject: [PATCH 028/260] Show names for chats list stories. --- Telegram/SourceFiles/dialogs/dialogs.style | 8 +- .../dialogs/ui/dialogs_stories_list.cpp | 74 +++++++++++++++---- .../dialogs/ui/dialogs_stories_list.h | 2 + 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index c520e9de3..52d80079a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -519,10 +519,10 @@ dialogsStoriesFull: DialogsStories { photoTop: 9px; lineTwice: 4px; lineReadTwice: 2px; - nameTop: 58px; + nameTop: 56px; nameStyle: TextStyle(defaultTextStyle) { - font: font(12px); - linkFont: font(12px); - linkFontOver: font(12px); + font: font(11px); + linkFont: font(11px); + linkFontOver: font(11px); } } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index c4dfbbbd2..c16fe7015 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -18,6 +18,13 @@ namespace { constexpr auto kSmallUserpicsShown = 3; constexpr auto kSmallReadOpacity = 0.6; +[[nodiscard]] int AvailableNameWidth() { + const auto &full = st::dialogsStoriesFull; + const auto &font = full.nameStyle.font; + const auto skip = font->spacew; + return full.photoLeft * 2 + full.photo - 2 * skip; +} + } // namespace List::List( @@ -137,8 +144,6 @@ void List::paintEvent(QPaintEvent *e) { const auto photoTop = lerp(photoTopSmall, full.photoTop); const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; - const auto nameTop = (photoTop + photo) - * (full.nameTop / float64(full.photoTop + full.photo)); const auto infoTop = st.nameTop - (st.photoTop + (st.photo / 2.)) + (photoTop + (photo / 2.)); @@ -161,6 +166,11 @@ void List::paintEvent(QPaintEvent *e) { const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull); const auto photoLeft = lerp(st.photoLeft, full.photoLeft); const auto left = userpicLeft - photoLeft; + const auto nameScale = shownHeight / float64(full.height); + const auto nameTop = nameScale * full.nameTop; + const auto nameWidth = nameScale * AvailableNameWidth(); + const auto nameHeight = nameScale * full.nameStyle.font->height; + const auto nameLeft = photoLeft + (photo - nameWidth) / 2.; const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.); const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.); @@ -172,15 +182,6 @@ void List::paintEvent(QPaintEvent *e) { const auto drawFull = (ratio > 0.); auto hq = PainterHighQualityEnabler(p); - const auto subscribe = [&](not_null item) { - if (!item->subscribed) { - item->subscribed = true; - //const auto id = item.user.id; - item->user.userpic->subscribeToUpdates([=] { - update(); - }); - } - }; const auto count = std::max( endIndexFull - startIndexFull, endIndexSmall - startIndexSmall); @@ -235,6 +236,15 @@ void List::paintEvent(QPaintEvent *e) { } }; enumerate([&](Single single) { + // Name. + if (const auto full = single.itemFull) { + p.setOpacity(ratio); + validateName(full); + p.drawImage( + QRectF(single.x + nameLeft, nameTop, nameWidth, nameHeight), + full->nameCache); + } + // Unread gradient. const auto x = single.x; const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo); @@ -277,7 +287,8 @@ void List::paintEvent(QPaintEvent *e) { const auto fullUnread = itemFull && itemFull->user.unread; // White circle with possible read gray line. - if (itemFull && !fullUnread) { + const auto hasReadLine = (itemFull && !fullUnread); + if (hasReadLine) { auto color = st::dialogsUnreadBgMuted->c; color.setAlphaF(color.alphaF() * ratio); auto pen = QPen(color); @@ -286,7 +297,7 @@ void List::paintEvent(QPaintEvent *e) { } else { p.setPen(Qt::NoPen); } - const auto add = line + (itemFull ? (lineRead / 2.) : 0.); + const auto add = line + (hasReadLine ? (lineRead / 2.) : 0.); const auto rect = userpic.marginsAdded({ add, add, add, add }); p.setBrush(st::dialogsBg); p.drawEllipse(rect); @@ -294,7 +305,7 @@ void List::paintEvent(QPaintEvent *e) { // Userpic. if (itemFull == small) { p.setOpacity(smallUnread ? 1. : readUserpicOpacity); - subscribe(itemFull); + validateUserpic(itemFull); const auto size = full.photo; p.drawImage(userpic, itemFull->user.userpic->image(size)); } else { @@ -304,13 +315,13 @@ void List::paintEvent(QPaintEvent *e) { : (itemFull ? kSmallReadOpacity : readUserpicAppearingOpacity)); - subscribe(small); + validateUserpic(small); const auto size = (ratio > 0.) ? full.photo : st.photo; p.drawImage(userpic, small->user.userpic->image(size)); } if (itemFull) { p.setOpacity(ratio); - subscribe(itemFull); + validateUserpic(itemFull); const auto size = full.photo; p.drawImage(userpic, itemFull->user.userpic->image(size)); } @@ -319,6 +330,37 @@ void List::paintEvent(QPaintEvent *e) { }); } +void List::validateUserpic(not_null item) { + if (!item->subscribed) { + item->subscribed = true; + //const auto id = item.user.id; + item->user.userpic->subscribeToUpdates([=] { + update(); + }); + } +} + +void List::validateName(not_null item) { + const auto &color = st::dialogsNameFg; + if (!item->nameCache.isNull() && item->nameCacheColor == color->c) { + return; + } + const auto &full = st::dialogsStoriesFull; + const auto &font = full.nameStyle.font; + const auto available = AvailableNameWidth(); + const auto text = Ui::Text::String(full.nameStyle, item->user.name); + const auto ratio = style::DevicePixelRatio(); + item->nameCacheColor = color->c; + item->nameCache = QImage( + QSize(available, font->height) * ratio, + QImage::Format_ARGB32_Premultiplied); + item->nameCache.setDevicePixelRatio(ratio); + item->nameCache.fill(Qt::transparent); + auto p = Painter(&item->nameCache); + p.setPen(color); + text.drawElided(p, 0, 0, available, 1, style::al_top); +} + void List::wheelEvent(QWheelEvent *e) { const auto horizontal = (e->angleDelta().x() != 0); if (!horizontal) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index d8b1bdbae..cbc635482 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -65,6 +65,8 @@ private: void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; + void validateUserpic(not_null item); + void validateName(not_null item); void updateScrollMax(); void checkDragging(); bool finishDragging(); From d57ada8a6418d435bfd0bccffdd2d2cd762319b7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 23 May 2023 22:54:13 +0400 Subject: [PATCH 029/260] Show stories summary status in chats list. --- Telegram/Resources/langs/lang.strings | 5 + Telegram/SourceFiles/dialogs/dialogs.style | 8 +- .../dialogs/ui/dialogs_stories_list.cpp | 198 ++++++++++++++++-- .../dialogs/ui/dialogs_stories_list.h | 47 ++++- 4 files changed, 240 insertions(+), 18 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8bfb90dff..54b768de8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3778,6 +3778,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_userpic_builder_color_subtitle" = "Choose background"; "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji"; +"lng_stories_row_count#one" = "{count} Story"; +"lng_stories_row_count#other" = "{count} Stories"; +"lng_stories_row_unread_and_one" = "{accumulated}, {user}"; +"lng_stories_row_unread_and_last" = "{accumulated} and {user}"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 52d80079a..47a025611 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -495,6 +495,8 @@ DialogsStories { shift: pixels; lineTwice: pixels; lineReadTwice: pixels; + nameLeft: pixels; + nameRight: pixels; nameTop: pixels; nameStyle: TextStyle; } @@ -507,7 +509,9 @@ dialogsStories: DialogsStories { shift: 16px; lineTwice: 3px; lineReadTwice: 0px; - nameTop: 9px; + nameLeft: 11px; + nameRight: 10px; + nameTop: 3px; nameStyle: semiboldTextStyle; } @@ -519,6 +523,8 @@ dialogsStoriesFull: DialogsStories { photoTop: 9px; lineTwice: 4px; lineReadTwice: 2px; + nameLeft: 0px; + nameRight: 0px; nameTop: 56px; nameStyle: TextStyle(defaultTextStyle) { font: font(11px); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index c16fe7015..d5d1f3d3c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/dialogs_stories_list.h" +#include "lang/lang_keys.h" #include "ui/painter.h" #include "styles/style_dialogs.h" @@ -17,6 +18,7 @@ namespace { constexpr auto kSmallUserpicsShown = 3; constexpr auto kSmallReadOpacity = 0.6; +constexpr auto kSummaryExpandLeft = 1.5; [[nodiscard]] int AvailableNameWidth() { const auto &full = st::dialogsStoriesFull; @@ -40,7 +42,7 @@ List::List( }, lifetime()); _shownAnimation.stop(); - resize(0, _items.empty() ? 0 : st::dialogsStoriesFull.height); + resize(0, _data.empty() ? 0 : st::dialogsStoriesFull.height); } void List::showContent(Content &&content) { @@ -48,24 +50,25 @@ void List::showContent(Content &&content) { return; } if (content.users.empty()) { - _hidingItems = base::take(_items); - if (!_hidingItems.empty()) { + _hidingData = base::take(_data); + if (!_hidingData.empty()) { toggleAnimated(false); } return; } const auto hidden = _content.users.empty(); _content = std::move(content); - auto items = base::take(_items.empty() ? _hidingItems : _items); - _hidingItems.clear(); - _items.reserve(_content.users.size()); + auto items = base::take( + _data.items.empty() ? _hidingData.items : _data.items); + _hidingData = {}; + _data.items.reserve(_content.users.size()); for (const auto &user : _content.users) { const auto i = ranges::find(items, user.id, [](const Item &item) { return item.user.id; }); if (i != end(items)) { - _items.push_back(std::move(*i)); - auto &item = _items.back(); + _data.items.push_back(std::move(*i)); + auto &item = _data.items.back(); if (item.user.userpic != user.userpic) { item.user.userpic = user.userpic; item.subscribed = false; @@ -76,16 +79,94 @@ void List::showContent(Content &&content) { } item.user.unread = user.unread; } else { - _items.emplace_back(Item{ .user = user }); + _data.items.emplace_back(Item{ .user = user }); } } updateScrollMax(); + updateSummary(_data); update(); if (hidden) { toggleAnimated(true); } } +List::Summaries List::ComposeSummaries(Data &data) { + const auto total = int(data.items.size()); + auto unreadInFirst = 0; + auto unreadTotal = 0; + for (auto i = 0; i != total; ++i) { + if (data.items[i].user.unread) { + ++unreadTotal; + if (i < kSmallUserpicsShown) { + ++unreadInFirst; + } + } + } + auto result = Summaries(); + result.total.string + = tr::lng_stories_row_count(tr::now, lt_count, total); + const auto append = [&](QString &to, int index, bool last) { + if (to.isEmpty()) { + to = data.items[index].user.name; + } else { + to = (last + ? tr::lng_stories_row_unread_and_last + : tr::lng_stories_row_unread_and_one)( + tr::now, + lt_accumulated, + to, + lt_user, + data.items[index].user.name); + } + }; + if (!total) { + return result; + } else if (total <= kSmallUserpicsShown) { + for (auto i = 0; i != total; ++i) { + append(result.allNames.string, i, i == total - 1); + } + } + if (unreadInFirst > 0 && unreadInFirst == unreadTotal) { + for (auto i = 0; i != total; ++i) { + if (data.items[i].user.unread) { + append(result.unreadNames.string, i, !--unreadTotal); + } + } + } + return result; +} + +bool List::StringsEqual(const Summaries &a, const Summaries &b) { + return (a.total.string == b.total.string) + && (a.allNames.string == b.allNames.string) + && (a.unreadNames.string == b.unreadNames.string); +} + +void List::Populate(Summary &summary) { + if (summary.empty()) { + return; + } + summary.cache = QImage(); + summary.text = Ui::Text::String( + st::dialogsStories.nameStyle, + summary.string); +} + +void List::Populate(Summaries &summaries) { + Populate(summaries.total); + Populate(summaries.allNames); + Populate(summaries.unreadNames); +} + +void List::updateSummary(Data &data) { + auto summaries = ComposeSummaries(data); + if (StringsEqual(summaries, data.summaries)) { + return; + } + data.summaries = std::move(summaries); + Populate(data.summaries); +} + void List::toggleAnimated(bool shown) { _shownAnimation.start( [=] { updateHeight(); }, @@ -95,16 +176,19 @@ void List::toggleAnimated(bool shown) { } void List::updateHeight() { - const auto shown = _shownAnimation.value(_items.empty() ? 0. : 1.); + const auto shown = _shownAnimation.value(_data.empty() ? 0. : 1.); resize( width(), anim::interpolate(0, st::dialogsStoriesFull.height, shown)); + if (_data.empty() && shown == 0.) { + _hidingData = {}; + } } void List::updateScrollMax() { const auto &full = st::dialogsStoriesFull; const auto singleFull = full.photoLeft * 2 + full.photo; - const auto widthFull = full.left + int(_items.size()) * singleFull; + const auto widthFull = full.left + int(_data.items.size()) * singleFull; _scrollLeftMax = std::max(widthFull - width(), 0); _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); update(); @@ -139,18 +223,19 @@ void List::paintEvent(QPaintEvent *e) { const auto lerp = [=](float64 a, float64 b) { return a + (b - a) * ratio; }; + auto &rendering = _data.empty() ? _hidingData : _data; const auto photo = lerp(st.photo, full.photo); const auto photoTopSmall = (st.height - st.photo) / 2.; const auto photoTop = lerp(photoTopSmall, full.photoTop); const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; - const auto infoTop = st.nameTop + const auto summaryTop = st.nameTop - (st.photoTop + (st.photo / 2.)) + (photoTop + (photo / 2.)); const auto singleSmall = st.shift; const auto singleFull = full.photoLeft * 2 + full.photo; const auto single = lerp(singleSmall, singleFull); - const auto itemsCount = int(_items.size()); + const auto itemsCount = int(rendering.items.size()); const auto leftSmall = st.left; const auto leftFull = full.left - _scrollLeft; const auto startIndexFull = std::max(-leftFull, 0) / singleFull; @@ -182,6 +267,8 @@ void List::paintEvent(QPaintEvent *e) { const auto drawFull = (ratio > 0.); auto hq = PainterHighQualityEnabler(p); + paintSummary(p, rendering, summaryTop, ratio); + const auto count = std::max( endIndexFull - startIndexFull, endIndexSmall - startIndexSmall); @@ -201,10 +288,10 @@ void List::paintEvent(QPaintEvent *e) { const auto indexSmall = startIndexSmall + index; const auto indexFull = startIndexFull + index; const auto small = (drawSmall && indexSmall < endIndexSmall) - ? &_items[indexSmall] + ? &rendering.items[indexSmall] : nullptr; const auto full = (drawFull && indexFull < endIndexFull) - ? &_items[indexFull] + ? &rendering.items[indexFull] : nullptr; const auto x = left + single * index; return Single{ x, indexSmall, small, indexFull, full }; @@ -361,6 +448,87 @@ void List::validateName(not_null item) { text.drawElided(p, 0, 0, available, 1, style::al_top); } +List::Summary &List::ChooseSummary( + Summaries &summaries, + int totalItems, + int fullWidth) { + const auto &st = st::dialogsStories; + const auto used = std::min(totalItems, kSmallUserpicsShown); + const auto taken = st.left + + st.photoLeft + + st.photo + + (used - 1) * st.shift + + st.nameLeft + + st.nameRight; + const auto available = fullWidth - taken; + const auto prepare = [&](Summary &summary) { + if (!summary.empty() && (summary.text.maxWidth() <= available)) { + summary.available = available; + return true; + } + return false; + }; + if (prepare(summaries.unreadNames)) { + return summaries.unreadNames; + } else if (prepare(summaries.allNames)) { + return summaries.allNames; + } + prepare(summaries.total); + return summaries.total; +} + +void List::PrerenderSummary(Summary &summary) { + if (!summary.cache.isNull() + && summary.cacheForWidth == summary.available + && summary.cacheColor == st::dialogsNameFg->c) { + return; + } + const auto &st = st::dialogsStories; + const auto use = std::min(summary.text.maxWidth(), summary.available); + const auto ratio = style::DevicePixelRatio(); + summary.cache = QImage( + QSize(use, st.nameStyle.font->height) * ratio, + QImage::Format_ARGB32_Premultiplied); + summary.cache.setDevicePixelRatio(ratio); + summary.cache.fill(Qt::transparent); + auto p = Painter(&summary.cache); + p.setPen(st::dialogsNameFg); + summary.text.drawElided(p, 0, 0, summary.available); +} + +void List::paintSummary( + QPainter &p, + Data &data, + float64 summaryTop, + float64 hidden) { + const auto total = int(data.items.size()); + auto &summary = ChooseSummary(data.summaries, total, width()); + PrerenderSummary(summary); + const auto lerp = [&](float64 from, float64 to) { + return from + (to - from) * hidden; + }; + const auto &st = st::dialogsStories; + const auto &full = st::dialogsStoriesFull; + const auto used = std::min(total, kSmallUserpicsShown); + const auto fullLeft = st.left + + st.photoLeft + + st.photo + + (used - 1) * st.shift + + st.nameLeft; + const auto leftFinal = std::min( + full.left + (full.photoLeft * 2 + full.photo) * total, + width()) * kSummaryExpandLeft; + const auto left = lerp(fullLeft, leftFinal); + const auto ratio = summary.cache.devicePixelRatio(); + const auto summaryWidth = lerp(summary.cache.width() / ratio, 0.); + const auto summaryHeight = lerp(summary.cache.height() / ratio, 0.); + summaryTop += ((summary.cache.height() / ratio) - summaryHeight) / 2.; + p.setOpacity(1. - hidden); + p.drawImage( + QRectF(left, summaryTop, summaryWidth, summaryHeight), + summary.cache); +} + void List::wheelEvent(QWheelEvent *e) { const auto horizontal = (e->angleDelta().x() != 0); if (!horizontal) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index cbc635482..636f855f7 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -55,6 +55,43 @@ private: QColor nameCacheColor; bool subscribed = false; }; + struct Summary { + QString string; + Ui::Text::String text; + int available = 0; + QImage cache; + QColor cacheColor; + int cacheForWidth = 0; + + [[nodiscard]] bool empty() const { + return string.isEmpty(); + } + }; + struct Summaries { + Summary total; + Summary allNames; + Summary unreadNames; + }; + struct Data { + std::vector items; + Summaries summaries; + + [[nodiscard]] bool empty() const { + return items.empty(); + } + }; + + [[nodiscard]] static Summaries ComposeSummaries(Data &data); + [[nodiscard]] static bool StringsEqual( + const Summaries &a, + const Summaries &b); + static void Populate(Summary &summary); + static void Populate(Summaries &summaries); + [[nodiscard]] static Summary &ChooseSummary( + Summaries &summaries, + int totalItems, + int fullWidth); + static void PrerenderSummary(Summary &summary); void showContent(Content &&content); void enterEventHook(QEnterEvent *e) override; @@ -68,15 +105,21 @@ private: void validateUserpic(not_null item); void validateName(not_null item); void updateScrollMax(); + void updateSummary(Data &data); void checkDragging(); bool finishDragging(); void updateHeight(); void toggleAnimated(bool shown); + void paintSummary( + QPainter &p, + Data &data, + float64 summaryTop, + float64 hidden); Content _content; - std::vector _items; - std::vector _hidingItems; + Data _data; + Data _hidingData; Fn _shownHeight = 0; rpl::event_stream _clicks; rpl::event_stream<> _expandRequests; From 455cb0d21bd3b72eb8d1c1735bc6e795970dd20a Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 19 May 2023 21:36:54 +0400 Subject: [PATCH 030/260] Fix build with Xcode. --- .../history/view/controls/history_view_voice_record_button.cpp | 1 - .../history/view/controls/history_view_voice_record_button.h | 1 - Telegram/SourceFiles/media/stories/media_stories_controller.h | 2 ++ Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index 71a652f4d..9bed92be9 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -51,7 +51,6 @@ VoiceRecordButton::VoiceRecordButton( not_null parent, const style::RecordBar &st) : AbstractButton(parent) -, _st(st) , _blobs(std::make_unique( Blobs(), kLevelDuration, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index fc477b485..bd3f0150c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -47,7 +47,6 @@ public: private: void init(); - const style::RecordBar &_st; std::unique_ptr _blobs; crl::time _lastUpdateTime = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 88a307dc0..2715ce8f5 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -53,6 +53,8 @@ struct SiblingLayout { QRect userpic; QRect nameBoundingRect; int nameFontSize = 0; + + friend inline bool operator==(SiblingLayout, SiblingLayout) = default; }; struct Layout { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 01326c842..733c61851 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1568,7 +1568,7 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) { + (_over == OverRotate ? _rotateNavOver : _rotateNavIcon) + (_over == OverMore ? _moreNavOver : _moreNavIcon) + ((_stories - && (_over == OverLeftStories && _over == OverRightStories)) + && (_over == OverLeftStories || _over == OverRightStories)) ? _stories->sibling(siblingType).layout.geometry : QRect()) + _headerNav From 04e7ce44084727c869bd652c3c99931d64c61397 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 25 May 2023 13:32:13 +0400 Subject: [PATCH 031/260] Update API scheme on layer 160: Replies. --- Telegram/Resources/tl/api.tl | 37 ++++---- Telegram/SourceFiles/api/api_bot.cpp | 6 +- Telegram/SourceFiles/api/api_common.cpp | 10 ++- Telegram/SourceFiles/api/api_common.h | 5 +- Telegram/SourceFiles/api/api_peer_photo.cpp | 3 +- Telegram/SourceFiles/api/api_polls.cpp | 11 +-- Telegram/SourceFiles/api/api_ringtones.cpp | 3 +- Telegram/SourceFiles/api/api_sending.cpp | 23 ++--- Telegram/SourceFiles/apiwrap.cpp | 84 ++++++++----------- .../boxes/background_preview_box.cpp | 2 +- .../boxes/reactions_settings_box.cpp | 6 +- .../chat_helpers/stickers_dice_pack.cpp | 2 +- .../data/data_download_manager.cpp | 2 +- Telegram/SourceFiles/data/data_histories.cpp | 64 ++++++++++---- Telegram/SourceFiles/data/data_histories.h | 35 ++++---- Telegram/SourceFiles/data/data_msg_id.h | 31 +++++++ .../data/data_scheduled_messages.cpp | 4 +- Telegram/SourceFiles/data/data_stories.h | 14 ---- Telegram/SourceFiles/data/data_types.h | 1 - .../export/data/export_data_types.cpp | 4 + .../admin_log/history_admin_log_item.cpp | 2 +- Telegram/SourceFiles/history/history.cpp | 10 ++- Telegram/SourceFiles/history/history.h | 8 +- .../history/history_inner_widget.cpp | 2 +- Telegram/SourceFiles/history/history_item.cpp | 48 ++++++++--- Telegram/SourceFiles/history/history_item.h | 12 +-- .../history/history_item_components.h | 1 + .../history/history_item_helpers.cpp | 13 ++- .../SourceFiles/history/history_widget.cpp | 7 +- .../view/history_view_replies_section.cpp | 5 +- .../inline_bots/bot_attach_web_view.cpp | 19 ++--- .../inline_bots/inline_bot_result.cpp | 4 +- .../inline_bots/inline_bot_result.h | 2 +- .../inline_bots/inline_bot_send_data.cpp | 18 ++-- .../inline_bots/inline_bot_send_data.h | 10 +-- .../stories/media_stories_controller.cpp | 28 ++++--- .../media/stories/media_stories_controller.h | 2 +- .../media/stories/media_stories_delegate.h | 11 ++- .../media/stories/media_stories_sibling.cpp | 21 ++--- .../media/stories/media_stories_sibling.h | 6 +- .../media/view/media_view_overlay_widget.cpp | 23 ++--- .../media/view/media_view_overlay_widget.h | 6 +- .../passport/passport_form_controller.cpp | 2 +- .../SourceFiles/storage/localimageloader.cpp | 56 +------------ .../SourceFiles/storage/localimageloader.h | 46 +--------- .../support/support_autocomplete.cpp | 4 +- .../window/notifications_manager.cpp | 7 +- .../window/themes/window_theme.cpp | 3 +- .../window/themes/window_theme_editor_box.cpp | 3 +- .../SourceFiles/window/window_peer_menu.cpp | 12 +-- 50 files changed, 364 insertions(+), 374 deletions(-) diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 4d682710b..d5b1f3652 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -412,6 +412,7 @@ updateAutoSaveSettings#ec05b097 = Update; updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; updateStories#66fad7b5 stories:UserStories = Update; updateReadStories#feb5345a user_id:long max_id:int = Update; +updateStoryID#1bf335b9 id:int random_id:long = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1280,6 +1281,7 @@ messages.messageViews#b6c4f543 views:Vector chats:Vector use messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; messageReplyHeader#a6d57763 flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; +messageReplyStoryHeader#9c98bfc1 user_id:long story_id:int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; @@ -1551,25 +1553,30 @@ messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = Mess sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#518b47d8 recent_viewers:Vector views_count:int = StoryViews; +storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#a1d8cf8f id:int date:int = StoryItem; -storyItem#8fcd96ac flags:# pinned:flags.5?true expired:flags.6?true id:int date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; +storyItem#8fcd96ac flags:# pinned:flags.5?true expired:flags.6?true public:flags.7?true id:int date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; stories.allStoriesNotModified#47e0a07e state:string = stories.AllStories; -stories.allStories#5b1aa68c flags:# has_more:flags.0?true state:string user_stories:Vector users:Vector = stories.AllStories; +stories.allStories#839e0428 flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector = stories.AllStories; stories.stories#4fe57df1 count:int stories:Vector users:Vector = stories.Stories; +stories.userStories#37a6ff5f stories:UserStories users:Vector = stories.UserStories; + storyView#a71aacc2 user_id:long date:int = StoryView; stories.storyViewsList#fb3f77ac count:int views:Vector users:Vector = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; +inputReplyToMessage#9c5386e4 flags:# reply_to_msg_id:int top_msg_id:flags.0?int = InputReplyTo; +inputReplyToStory#15b0f283 user_id:InputUser story_id:int = InputReplyTo; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1730,8 +1737,8 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#1cc20387 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.9?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.sendMedia#7547c966 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.9?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMessage#280d096f flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMedia#72ccc23d flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.forwardMessages#c661bbc4 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; @@ -1775,7 +1782,7 @@ messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; -messages.sendInlineBotResult#d3fbdccb flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.9?int random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendInlineBotResult#f7bc68ba flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; @@ -1804,13 +1811,13 @@ messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs; messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector = Bool; messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool; messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia; -messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int random_id:long = Updates; +messages.sendScreenshotNotification#a1405817 peer:InputPeer reply_to:InputReplyTo random_id:long = Updates; messages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#b6f11a1c flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.9?int multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMultiMedia#456e8987 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -1882,8 +1889,8 @@ messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = mes messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; messages.toggleBotInAttachMenu#69f59d69 flags:# write_allowed:flags.0?true bot:InputUser enabled:Bool = Bool; -messages.requestWebView#178b480b flags:# from_bot_menu:flags.4?true silent:flags.5?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to_msg_id:flags.0?int top_msg_id:flags.9?int send_as:flags.13?InputPeer = WebViewResult; -messages.prolongWebView#7ff34309 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to_msg_id:flags.0?int top_msg_id:flags.9?int send_as:flags.13?InputPeer = Bool; +messages.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = WebViewResult; +messages.prolongWebView#b0d81a83 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = Bool; messages.requestSimpleWebView#299bec8e flags:# from_switch_webview:flags.1?true bot:InputUser url:string theme_params:flags.0?DataJSON platform:string = SimpleWebViewResult; messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent; messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates; @@ -2104,15 +2111,17 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.sendStory#8e826f5d flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector = Updates; +stories.sendStory#8b5c6986 flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long = Updates; +stories.editStory#2aae7a41 flags:# id:int media:flags.0?InputMedia caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#b5d501d7 id:Vector = Vector; -stories.editStoryPrivacy#78981875 id:int privacy_rules:Vector = Updates; stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true state:flags.0?string = stories.AllStories; -stories.getUserStories#c946f3c0 flags:# pinned:flags.0?true user_id:InputUser offset_id:int limit:int = stories.Stories; -stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector = stories.Stories; +stories.getUserStories#96d528e0 user_id:InputUser = stories.UserStories; +stories.getPinnedStories#b471137 user_id:InputUser offset_id:int limit:int = stories.Stories; stories.getExpiredStories#8f792f2 offset_id:int limit:int = stories.Stories; +stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector = stories.Stories; stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; +stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList; stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 165402d7e..65826aa2f 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -375,8 +375,10 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { ShowAtTheEndMsgId); auto action = Api::SendAction(history); action.clearDraft = false; - action.replyTo = itemId; - action.topicRootId = topicRootId; + action.replyTo = { + .msgId = itemId, + .topicRootId = topicRootId, + }; history->session().api().shareContact( history->session().user(), action); diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index f4afa97f6..0a44a916f 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -8,7 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "base/qt/qt_key_modifiers.h" +#include "data/data_histories.h" #include "data/data_thread.h" +#include "history/history.h" namespace Api { @@ -17,8 +19,8 @@ SendAction::SendAction( SendOptions options) : history(thread->owningHistory()) , options(options) -, replyTo(thread->topicRootId()) -, topicRootId(replyTo) { +, replyTo({ .msgId = thread->topicRootId() }) { + replyTo.topicRootId = replyTo.msgId; } SendOptions DefaultSendWhenOnlineOptions() { @@ -28,4 +30,8 @@ SendOptions DefaultSendWhenOnlineOptions() { }; } +MTPInputReplyTo SendAction::mtpReplyTo() const { + return Data::ReplyToForMTP(&history->owner(), replyTo); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 1bdb97ee5..08ee59ca1 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -40,11 +40,12 @@ struct SendAction { not_null history; SendOptions options; - MsgId replyTo = 0; - MsgId topicRootId = 0; + FullReplyTo replyTo; bool clearDraft = true; bool generateLocal = true; MsgId replaceMediaOf = 0; + + [[nodiscard]] MTPInputReplyTo mtpReplyTo() const; }; struct MessageToSend { diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index a449b2200..71512172b 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -97,8 +97,7 @@ constexpr auto kSharedMediaLimit = 100; photo, photoThumbs, MTP_documentEmpty(MTP_long(0)), - jpeg, - 0); + jpeg); } [[nodiscard]] std::optional PrepareMtpMarkup( diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index d18c947c1..227b6e9db 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -43,13 +43,12 @@ void Polls::create( const auto history = action.history; const auto peer = history->peer; - const auto topicRootId = action.replyTo ? action.topicRootId : 0; + const auto topicRootId = action.replyTo.msgId + ? action.replyTo.topicRootId + : 0; auto sendFlags = MTPmessages_SendMedia::Flags(0); if (action.replyTo) { - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto clearCloudDraft = action.clearDraft; if (clearCloudDraft) { @@ -74,13 +73,11 @@ void Polls::create( histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), PollDataToInputMedia(&data), MTP_string(), MTP_long(randomId), diff --git a/Telegram/SourceFiles/api/api_ringtones.cpp b/Telegram/SourceFiles/api/api_ringtones.cpp index 40b07e3f3..d9ad40176 100644 --- a/Telegram/SourceFiles/api/api_ringtones.cpp +++ b/Telegram/SourceFiles/api/api_ringtones.cpp @@ -60,8 +60,7 @@ SendMediaReady PrepareRingtoneDocument( MTP_photoEmpty(MTP_long(0)), PreparedPhotoThumbs(), document, - QByteArray(), - 0); + QByteArray()); } } // namespace diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index e8d18c03c..9cfe272f8 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -86,10 +86,7 @@ void SendExistingMedia( auto sendFlags = MTPmessages_SendMedia::Flags(0); if (message.action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (message.action.topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); const auto silentPost = ShouldSendSilent(peer, message.action.options); @@ -150,13 +147,11 @@ void SendExistingMedia( histories.sendPreparedMessage( history, message.action.replyTo, - message.action.topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), inputMedia(), MTP_string(captionText), MTP_long(randomId), @@ -273,10 +268,7 @@ bool SendDice(MessageToSend &message) { auto sendFlags = MTPmessages_SendMedia::Flags(0); if (message.action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (message.action.topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto replyHeader = NewMessageReplyHeader(message.action); const auto anonymousPost = peer->amAnonymous(); @@ -320,13 +312,11 @@ bool SendDice(MessageToSend &message) { histories.sendPreparedMessage( history, message.action.replyTo, - message.action.topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_inputMediaDice(MTP_string(emoji)), MTP_string(), MTP_long(randomId), @@ -378,12 +368,12 @@ void SendConfirmedFile( if (!isEditing) { const auto histories = &session->data().histories(); - file->to.replyTo = histories->convertTopicReplyTo( + file->to.replyTo.msgId = histories->convertTopicReplyToId( history, - file->to.replyTo); - file->to.topicRootId = histories->convertTopicReplyTo( + file->to.replyTo.msgId); + file->to.replyTo.topicRootId = histories->convertTopicReplyToId( history, - file->to.topicRootId); + file->to.replyTo.topicRootId); } session->uploader().upload(newId, file); @@ -391,7 +381,6 @@ void SendConfirmedFile( auto action = SendAction(history, file->to.options); action.clearDraft = false; action.replyTo = file->to.replyTo; - action.topicRootId = file->to.topicRootId; action.generateLocal = true; action.replaceMediaOf = file->to.replaceMediaOf; session->api().sendAction(action); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index c60f96862..304c609d4 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3091,8 +3091,9 @@ void ApiWrap::sharedMediaDone( void ApiWrap::sendAction(const SendAction &action) { if (!action.options.scheduled && !action.replaceMediaOf) { - const auto topic = action.topicRootId - ? action.history->peer->forumTopicFor(action.topicRootId) + const auto topicRootId = action.replyTo.topicRootId; + const auto topic = topicRootId + ? action.history->peer->forumTopicFor(topicRootId) : nullptr; if (topic) { topic->readTillEnd(); @@ -3106,12 +3107,13 @@ void ApiWrap::sendAction(const SendAction &action) { void ApiWrap::finishForwarding(const SendAction &action) { const auto history = action.history; - auto toForward = history->resolveForwardDraft(action.topicRootId); + const auto topicRootId = action.replyTo.topicRootId; + auto toForward = history->resolveForwardDraft(topicRootId); if (!toForward.items.empty()) { const auto error = GetErrorTextForSending( history->peer, { - .topicRootId = action.topicRootId, + .topicRootId = topicRootId, .forward = &toForward.items, }); if (!error.isEmpty()) { @@ -3119,7 +3121,7 @@ void ApiWrap::finishForwarding(const SendAction &action) { } forwardMessages(std::move(toForward), action); - history->setForwardDraft(action.topicRootId, {}); + history->setForwardDraft(topicRootId, {}); } _session->data().sendHistoryChangeNotifications(); @@ -3163,31 +3165,33 @@ void ApiWrap::forwardMessages( const auto silentPost = ShouldSendSilent(peer, action.options); const auto sendAs = action.options.sendAs; + using SendFlag = MTPmessages_ForwardMessages::Flag; auto flags = MessageFlags(); - auto sendFlags = MTPmessages_ForwardMessages::Flags(0); + auto sendFlags = SendFlag() | SendFlag(); FillMessagePostFlags(action, peer, flags); if (silentPost) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_silent; + sendFlags |= SendFlag::f_silent; } if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - sendFlags |= MTPmessages_ForwardMessages::Flag::f_schedule_date; + sendFlags |= SendFlag::f_schedule_date; } if (draft.options != Data::ForwardOptions::PreserveInfo) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_drop_author; + sendFlags |= SendFlag::f_drop_author; } if (draft.options == Data::ForwardOptions::NoNamesAndCaptions) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_drop_media_captions; + sendFlags |= SendFlag::f_drop_media_captions; } if (sendAs) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_send_as; + sendFlags |= SendFlag::f_send_as; } const auto kGeneralId = Data::ForumTopic::kGeneralId; - const auto topMsgId = (action.topicRootId == kGeneralId) + const auto topicRootId = action.replyTo.topicRootId; + const auto topMsgId = (topicRootId == kGeneralId) ? MsgId(0) - : action.topicRootId; + : topicRootId; if (topMsgId) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_top_msg_id; + sendFlags |= SendFlag::f_top_msg_id; } auto forwardFrom = draft.items.front()->history()->peer; @@ -3529,14 +3533,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) { action.generateLocal = true; sendAction(action); - const auto replyToId = action.replyTo; + const auto replyToId = action.replyTo.msgId; const auto replyTo = replyToId ? peer->owner().message(peer, replyToId) : nullptr; const auto topicRootId = replyTo ? replyTo->topicRootId() - : action.topicRootId - ? action.topicRootId + : action.replyTo.topicRootId + ? action.replyTo.topicRootId : Data::ForumTopic::kGeneralId; const auto topic = peer->forumTopicFor(topicRootId); if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) @@ -3575,10 +3579,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { auto sendFlags = MTPmessages_SendMessage::Flags(0); if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to_msg_id; - if (action.topicRootId) { - sendFlags |= MTPmessages_SendMessage::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to; } const auto replyHeader = NewMessageReplyHeader(action); MTPMessageMedia media = MTP_messageMediaEmpty(); @@ -3605,7 +3606,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_entities; } const auto clearCloudDraft = action.clearDraft; - const auto topicRootId = action.topicRootId; + const auto topicRootId = action.replyTo.topicRootId; if (clearCloudDraft) { sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft; history->clearCloudDraft(topicRootId); @@ -3642,13 +3643,11 @@ void ApiWrap::sendMessage(MessageToSend &&message) { histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), msgText, MTP_long(randomId), MTPReplyMarkup(), @@ -3737,29 +3736,29 @@ void ApiWrap::sendInlineResult( ? (*localMessageId) : _session->data().nextLocalMessageId()); const auto randomId = base::RandomValue(); - const auto topicRootId = action.replyTo ? action.topicRootId : 0; + const auto topicRootId = action.replyTo.msgId + ? action.replyTo.topicRootId + : 0; + using SendFlag = MTPmessages_SendInlineBotResult::Flag; auto flags = NewMessageFlags(peer); - auto sendFlags = MTPmessages_SendInlineBotResult::Flag::f_clear_draft | 0; + auto sendFlags = SendFlag::f_clear_draft | SendFlag(); if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_reply_to_msg_id; - if (topicRootId) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_top_msg_id; - } + sendFlags |= SendFlag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); const auto silentPost = ShouldSendSilent(peer, action.options); FillMessagePostFlags(action, peer, flags); if (silentPost) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_silent; + sendFlags |= SendFlag::f_silent; } if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_schedule_date; + sendFlags |= SendFlag::f_schedule_date; } if (action.options.hideViaBot) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_hide_via; + sendFlags |= SendFlag::f_hide_via; } const auto sendAs = action.options.sendAs; @@ -3793,13 +3792,11 @@ void ApiWrap::sendInlineResult( histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_long(randomId), MTP_long(data->getQueryId()), MTP_string(data->getId()), @@ -3912,8 +3909,7 @@ void ApiWrap::sendMediaWithRandomId( Api::SendOptions options, uint64 randomId) { const auto history = item->history(); - const auto replyTo = item->replyToId(); - const auto topicRootId = item->topicRootId(); + const auto replyTo = item->replyTo(); auto caption = item->originalText(); TextUtilities::Trim(caption); @@ -3926,8 +3922,7 @@ void ApiWrap::sendMediaWithRandomId( using Flag = MTPmessages_SendMedia::Flag; const auto flags = Flag(0) - | (replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (replyTo ? Flag::f_reply_to : Flag(0)) | (ShouldSendSilent(history->peer, options) ? Flag::f_silent : Flag(0)) @@ -3941,13 +3936,11 @@ void ApiWrap::sendMediaWithRandomId( histories.sendPreparedMessage( history, replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(flags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), media, MTP_string(caption.text), MTP_long(randomId), @@ -4028,13 +4021,11 @@ void ApiWrap::sendAlbumIfReady(not_null album) { return; } const auto history = sample->history(); - const auto replyTo = sample->replyToId(); - const auto topicRootId = sample->topicRootId(); + const auto replyTo = sample->replyTo(); const auto sendAs = album->options.sendAs; using Flag = MTPmessages_SendMultiMedia::Flag; const auto flags = Flag(0) - | (replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (replyTo ? Flag::f_reply_to : Flag(0)) | (ShouldSendSilent(history->peer, album->options) ? Flag::f_silent : Flag(0)) @@ -4045,13 +4036,11 @@ void ApiWrap::sendAlbumIfReady(not_null album) { histories.sendPreparedMessage( history, replyTo, - topicRootId, uint64(0), // randomId Data::Histories::PrepareMessage( MTP_flags(flags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_vector(medias), MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()) @@ -4074,7 +4063,6 @@ FileLoadTo ApiWrap::fileLoadTaskOptions(const SendAction &action) const { peer->id, action.options, action.replyTo, - action.topicRootId, action.replaceMediaOf); } diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 738a88165..c83af953d 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -95,7 +95,7 @@ constexpr auto kDefaultDimming = 50; const auto flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | (out ? MessageFlag::Outgoing : MessageFlag(0)); - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto groupedId = uint64(); const auto item = history->makeMessage( diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 8c3845efe..7ddcd57ab 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -76,11 +76,11 @@ AdminLog::OwnedItem GenerateItem( const auto item = history->addNewLocalMessage( history->nextNonHistoryEntryId(), - MessageFlag::FakeHistoryItem + (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId - | MessageFlag::HasReplyInfo, + | MessageFlag::HasReplyInfo), UserId(), // via - replyTo, + FullReplyTo{ .msgId = replyTo }, base::unixtime::now(), // date from, QString(), // postAuthor diff --git a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp index fed572b75..dcebbfdc4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp @@ -128,7 +128,7 @@ void DicePack::generateLocal(int index, const QString &name) { QByteArray(), nullptr, SendMediaType::File, - FileLoadTo(0, {}, 0, 0, 0), + FileLoadTo(0, {}, {}, 0), {}, false); task.process({ .generateGoodThumbnail = false }); diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 05080dfcb..088dc0b92 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -865,7 +865,7 @@ not_null DownloadManager::generateItem( ? previousItem->history() : session->data().history(session->user()); const auto flags = MessageFlag::FakeHistoryItem; - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto date = base::unixtime::now(); const auto postAuthor = QString(); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 7c80d75ac..d69b11ce4 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_scheduled_messages.h" +#include "data/data_user.h" #include "base/unixtime.h" #include "base/random.h" #include "main/main_session.h" @@ -31,6 +32,29 @@ constexpr auto kReadRequestTimeout = 3 * crl::time(1000); } // namespace +MTPInputReplyTo ReplyToForMTP( + not_null owner, + FullReplyTo replyTo) { + if (replyTo.msgId || replyTo.topicRootId) { + using Flag = MTPDinputReplyToMessage::Flag; + return MTP_inputReplyToMessage( + (replyTo.topicRootId + ? MTP_flags(Flag::f_top_msg_id) + : MTP_flags(0)), + MTP_int(replyTo.msgId ? replyTo.msgId : replyTo.topicRootId), + MTP_int(replyTo.topicRootId)); + } else if (replyTo.storyId) { + if (const auto peer = owner->peerLoaded(replyTo.storyId.peer)) { + if (const auto user = peer->asUser()) { + return MTP_inputReplyToStory( + user->inputUser, + MTP_int(replyTo.storyId.story)); + } + } + } + return MTPInputReplyTo(); +} + Histories::Histories(not_null owner) : _owner(owner) , _readRequestsTimer([=] { sendReadRequests(); }) { @@ -885,23 +909,24 @@ bool Histories::isCreatingTopic( int Histories::sendPreparedMessage( not_null history, - MsgId replyTo, - MsgId topicRootId, + FullReplyTo replyTo, uint64 randomId, - Fn message, + Fn, FullReplyTo)> message, Fn done, Fn fail) { - if (isCreatingTopic(history, topicRootId)) { + if (isCreatingTopic(history, replyTo.topicRootId)) { const auto id = ++_requestAutoincrement; - const auto creatingId = FullMsgId(history->peer->id, topicRootId); + const auto creatingId = FullMsgId( + history->peer->id, + replyTo.topicRootId); auto i = _creatingTopics.find(creatingId); if (i == end(_creatingTopics)) { - sendCreateTopicRequest(history, topicRootId); + sendCreateTopicRequest(history, replyTo.topicRootId); i = _creatingTopics.emplace(creatingId).first; } i->second.push_back({ .randomId = randomId, - .replyTo = replyTo, + .replyTo = replyTo.msgId, .message = std::move(message), .done = std::move(done), .fail = std::move(fail), @@ -910,9 +935,12 @@ int Histories::sendPreparedMessage( _creatingTopicRequests.emplace(id); return id; } - const auto realReply = convertTopicReplyTo(history, replyTo); - const auto realRoot = convertTopicReplyTo(history, topicRootId); - return v::match(message(realReply, realRoot), [&](const auto &request) { + const auto realReplyTo = FullReplyTo{ + .msgId = convertTopicReplyToId(history, replyTo.msgId), + .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), + .storyId = replyTo.storyId, + }; + return v::match(message(_owner, realReplyTo), [&](const auto &request) { const auto type = RequestType::Send; return sendRequest(history, type, [=](Fn finish) { const auto session = &_owner->session(); @@ -955,8 +983,10 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { _creatingTopicRequests.erase(entry.requestId); sendPreparedMessage( history, - entry.replyTo, - realRoot, + FullReplyTo{ + .msgId = entry.replyTo, + .topicRootId = realRoot, + }, entry.randomId, std::move(entry.message), std::move(entry.done), @@ -976,14 +1006,14 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { } } -MsgId Histories::convertTopicReplyTo( +MsgId Histories::convertTopicReplyToId( not_null history, - MsgId replyTo) const { - if (!replyTo) { + MsgId replyToId) const { + if (!replyToId) { return {}; } - const auto i = _createdTopicIds.find({ history->peer->id, replyTo }); - return (i != end(_createdTopicIds)) ? i->second : replyTo; + const auto i = _createdTopicIds.find({ history->peer->id, replyToId }); + return (i != end(_createdTopicIds)) ? i->second : replyToId; } void Histories::checkPostponed(not_null history, int id) { diff --git a/Telegram/SourceFiles/data/data_histories.h b/Telegram/SourceFiles/data/data_histories.h index 05f7534f4..c98dc352c 100644 --- a/Telegram/SourceFiles/data/data_histories.h +++ b/Telegram/SourceFiles/data/data_histories.h @@ -26,6 +26,10 @@ namespace Data { class Session; class Folder; +[[nodiscard]] MTPInputReplyTo ReplyToForMTP( + not_null owner, + FullReplyTo replyTo); + class Histories final { public: enum class RequestType : uchar { @@ -102,29 +106,27 @@ public: MTPmessages_SendMultiMedia>; int sendPreparedMessage( not_null history, - MsgId replyTo, - MsgId topicRootId, + FullReplyTo replyTo, uint64 randomId, - Fn message, + Fn, FullReplyTo)> message, Fn done, Fn fail); struct ReplyToPlaceholder { }; - struct TopicRootPlaceholder { - }; template - static Fn PrepareMessage( - const Args &...args) { - return [=](MsgId replyTo, MsgId topicRootId) -> RequestType { - return { ReplaceReplyIds(args, replyTo, topicRootId)... }; + static auto PrepareMessage(const Args &...args) + -> Fn, FullReplyTo)> { + return [=](not_null owner, FullReplyTo replyTo) + -> RequestType { + return { ReplaceReplyIds(owner, args, replyTo)... }; }; } void checkTopicCreated(FullMsgId rootId, MsgId realRoot); - [[nodiscard]] MsgId convertTopicReplyTo( + [[nodiscard]] MsgId convertTopicReplyToId( not_null history, - MsgId replyTo) const; + MsgId replyToId) const; private: struct PostponedHistoryRequest { @@ -151,7 +153,7 @@ private: struct DelayedByTopicMessage { uint64 randomId = 0; MsgId replyTo = 0; - Fn message; + Fn, FullReplyTo)> message; Fn done; Fn fail; int requestId = 0; @@ -166,11 +168,12 @@ private: }; template - static auto ReplaceReplyIds(Arg arg, MsgId replyTo, MsgId topicRootId) { + static auto ReplaceReplyIds( + not_null owner, + Arg arg, + FullReplyTo replyTo) { if constexpr (std::is_same_v) { - return MTP_int(replyTo); - } else if constexpr (std::is_same_v) { - return MTP_int(topicRootId); + return ReplyToForMTP(owner, replyTo); } else { return arg; } diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index ac39eddfb..af6b73fcc 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -136,6 +136,37 @@ struct GlobalMsgId { } }; +using StoryId = int32; + +struct FullStoryId { + PeerId peer = 0; + StoryId story = 0; + + [[nodiscard]] bool valid() const { + return peer != 0 && story != 0; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullStoryId, FullStoryId) = default; + friend inline bool operator==(FullStoryId, FullStoryId) = default; +}; + +struct FullReplyTo { + MsgId msgId = 0; + MsgId topicRootId = 0; + FullStoryId storyId; + + [[nodiscard]] bool valid() const { + return msgId || storyId; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; + friend inline bool operator==(FullReplyTo, FullReplyTo) = default; +}; + namespace std { template <> diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 21e0c48b3..820b7c0d2 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -192,13 +192,13 @@ void ScheduledMessages::sendNowSimpleMessage( const auto history = local->history(); auto action = Api::SendAction(history); - action.replyTo = local->replyToId(); + action.replyTo = local->replyTo(); const auto replyHeader = NewMessageReplyHeader(action); const auto localFlags = NewMessageFlags(history->peer) & ~MessageFlag::BeingSent; const auto flags = MTPDmessage::Flag::f_entities | MTPDmessage::Flag::f_from_id - | (local->replyToId() + | (action.replyTo ? MTPDmessage::Flag::f_reply_to : MTPDmessage::Flag(0)) | (update.vttl_period() diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index ce8ca4d60..6c3aaa56d 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -46,20 +46,6 @@ struct StoriesList { friend inline bool operator==(StoriesList, StoriesList) = default; }; -struct FullStoryId { - UserData *user = nullptr; - StoryId id = 0; - - [[nodiscard]] bool valid() const { - return user != nullptr && id != 0; - } - explicit operator bool() const { - return valid(); - } - friend inline auto operator<=>(FullStoryId, FullStoryId) = default; - friend inline bool operator==(FullStoryId, FullStoryId) = default; -}; - class Stories final { public: explicit Stories(not_null owner); diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 8f6ee551b..710cc4e1f 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -135,7 +135,6 @@ using PollId = uint64; using WallPaperId = uint64; using CallId = uint64; using BotAppId = uint64; -using StoryId = int32; constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index b0bd24268..1b66a550f 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1260,6 +1260,8 @@ Message ParseMessage( if (result.replyToPeerId == result.peerId) { result.replyToPeerId = 0; } + }, [&](const MTPDmessageReplyStoryHeader &data) { + // #TODO stories export }); } } @@ -1307,6 +1309,8 @@ Message ParseMessage( result.replyToPeerId = data.vreply_to_peer_id() ? ParsePeerId(*data.vreply_to_peer_id()) : PeerId(0); + }, [&](const MTPDmessageReplyStoryHeader &data) { + // #TODO stories export }); } if (const auto viaBotId = data.vvia_bot_id()) { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index ba2442308..4e2e3cd41 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -820,7 +820,7 @@ void GenerateItems( const auto makeSimpleTextMessage = [&](TextWithEntities &&text) { const auto bodyFlags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry; - const auto bodyReplyTo = MsgId(); + const auto bodyReplyTo = FullReplyTo(); const auto bodyViaBotId = UserId(); const auto bodyGroupedId = uint64(); return history->makeMessage( diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 262af2bec..5617a0991 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -631,7 +631,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -679,7 +679,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -705,7 +705,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -731,7 +731,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -1144,6 +1144,8 @@ void History::applyServiceChanges( topic->setHasPinnedMessages(true); } } + }, [&](const MTPDmessageReplyStoryHeader &data) { + LOG(("API Error: story reply in messageActionPinMessage.")); }); } }, [&](const MTPDmessageActionGroupCall &data) { diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index d2b80be10..933832d79 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -148,7 +148,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -168,7 +168,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -179,7 +179,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -190,7 +190,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index e719d9716..8d0d8b980 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -390,7 +390,7 @@ bool HistoryInner::BotAbout::refresh() { | MessageFlag::Local; const auto postAuthor = QString(); const auto date = TimeId(0); - const auto replyTo = MsgId(0); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(0); const auto groupedId = uint64(0); const auto textWithEntities = TextUtilities::ParseEntities( diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index b0c2e55f0..cab562735 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -131,6 +131,7 @@ struct HistoryItem::CreateConfig { PeerId replyToPeer = 0; MsgId replyTo = 0; MsgId replyToTop = 0; + StoryId replyToStory = 0; bool replyIsTopicPost = false; UserId viaBotId = 0; int viewsCount = -1; @@ -506,7 +507,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -541,7 +542,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -576,7 +577,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -606,7 +607,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -648,7 +649,7 @@ HistoryItem::HistoryItem( /*from.peer ? from.peer->id : */PeerId(0)) { createComponentsHelper( _flags, - MsgId(0), // replyTo + FullReplyTo(), UserId(0), // viaBotId QString(), // postAuthor HistoryMessageMarkupData()); @@ -1542,6 +1543,7 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) { data.vreply_to_top_id().value_or( data.vreply_to_msg_id().v), data.is_forum_topic()); + }, [](const MTPDmessageReplyStoryHeader &data) { }); } setPostAuthor(data.vpost_author().value_or_empty()); @@ -2742,6 +2744,26 @@ MsgId HistoryItem::topicRootId() const { return Data::ForumTopic::kGeneralId; } +FullStoryId HistoryItem::replyToStory() const { + if (const auto reply = Get()) { + if (reply->replyToStoryId) { + const auto peerId = reply->replyToPeerId + ? reply->replyToPeerId + : _history->peer->id; + return { .peer = peerId, .story = reply->replyToStoryId }; + } + } + return {}; +} + +FullReplyTo HistoryItem::replyTo() const { + return { + .msgId = replyToId(), + .topicRootId = topicRootId(), + .storyId = replyToStory(), + }; +} + void HistoryItem::setText(const TextWithEntities &textWithEntities) { for (const auto &entity : textWithEntities.entities) { auto type = entity.type(); @@ -2903,7 +2925,7 @@ const std::vector &HistoryItem::customTextLinks() const { void HistoryItem::createComponents(CreateConfig &&config) { uint64 mask = 0; - if (config.replyTo) { + if (config.replyTo || config.replyToStory) { mask |= HistoryMessageReply::Bit(); } if (config.viaBotId) { @@ -3125,17 +3147,19 @@ void HistoryItem::setSponsoredFrom(const Data::SponsoredFrom &from) { void HistoryItem::createComponentsHelper( MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, const QString &postAuthor, HistoryMessageMarkupData &&markup) { auto config = CreateConfig(); config.viaBotId = viaBotId; if (flags & MessageFlag::HasReplyInfo) { - config.replyTo = replyTo; - const auto to = LookupReplyTo(_history, replyTo); + config.replyTo = replyTo.msgId; + config.replyToStory = replyTo.storyId.story; + config.replyToPeer = replyTo.storyId ? replyTo.storyId.peer : 0; + const auto to = LookupReplyTo(_history, replyTo.msgId); const auto replyToTop = LookupReplyToTop(to); - config.replyToTop = replyToTop ? replyToTop : replyTo; + config.replyToTop = replyToTop ? replyToTop : replyTo.msgId; const auto forum = _history->asForum(); config.replyIsTopicPost = LookupReplyIsTopicPost(to) || (to && to->Has()) @@ -3245,6 +3269,9 @@ void HistoryItem::createComponents(const MTPDmessage &data) { : id; config.replyToTop = data.vreply_to_top_id().value_or(id); config.replyIsTopicPost = data.is_forum_topic(); + }, [&](const MTPDmessageReplyStoryHeader &data) { + config.replyToPeer = peerFromUser(data.vuser_id()); + config.replyToStory = data.vstory_id().v; }); } config.viaBotId = data.vvia_bot_id().value_or_empty(); @@ -3499,6 +3526,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { dependent->msgId); } } + }, [](const MTPDmessageReplyStoryHeader &data) { }); } setServiceMessageByAction(action); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b26793963..7ef66d7d6 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -117,7 +117,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -147,7 +147,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -159,7 +159,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -171,7 +171,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -458,6 +458,8 @@ public: [[nodiscard]] MsgId replyToId() const; [[nodiscard]] MsgId replyToTop() const; [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] FullStoryId replyToStory() const; + [[nodiscard]] FullReplyTo replyTo() const; [[nodiscard]] bool inThread(MsgId rootId) const; [[nodiscard]] not_null author() const; @@ -518,7 +520,7 @@ private: void createComponentsHelper( MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, const QString &postAuthor, HistoryMessageMarkupData &&markup); diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 5b92c6cfc..0545a9760 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -243,6 +243,7 @@ struct HistoryMessageReply PeerId replyToPeerId = 0; MsgId replyToMsgId = 0; MsgId replyToMsgTop = 0; + StoryId replyToStoryId = 0; using ColorKey = PeerId; ColorKey replyToColorKey = 0; DocumentId replyToDocumentId = 0; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 7042cb557..a7def067b 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -309,8 +309,13 @@ MessageFlags FlagsFromMTP( } MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { - if (const auto id = action.replyTo) { - const auto to = LookupReplyTo(action.history, id); + if (const auto replyTo = action.replyTo) { + if (replyTo.storyId) { + return MTP_messageReplyStoryHeader( + MTP_long(peerToUser(replyTo.storyId.peer).bare), + MTP_int(replyTo.storyId.story)); + } + const auto to = LookupReplyTo(action.history, replyTo.msgId); if (const auto replyToTop = LookupReplyToTop(to)) { using Flag = MTPDmessageReplyHeader::Flag; return MTP_messageReplyHeader( @@ -318,13 +323,13 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { | (LookupReplyIsTopicPost(to) ? Flag::f_forum_topic : Flag(0))), - MTP_int(id), + MTP_int(replyTo.msgId), MTPPeer(), MTP_int(replyToTop)); } return MTP_messageReplyHeader( MTP_flags(0), - MTP_int(id), + MTP_int(replyTo.msgId), MTPPeer(), MTPint()); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 99db1769a..106dce71e 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -853,7 +853,7 @@ HistoryWidget::HistoryWidget( }) | rpl::start_with_next([=](const Api::SendAction &action) { const auto lastKeyboardUsed = lastForceReplyReplied(FullMsgId( action.history->peer->id, - action.replyTo)); + action.replyTo.msgId)); if (action.replaceMediaOf) { } else if (action.options.scheduled) { cancelReply(lastKeyboardUsed); @@ -3841,8 +3841,7 @@ void HistoryWidget::hideSelectorControlsAnimated() { Api::SendAction HistoryWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = replyToId(); - result.topicRootId = 0; + result.replyTo = { .msgId = replyToId() }; result.options.sendAs = _sendAs ? _history->session().sendAsPeers().resolveChosen( _history->peer).get() @@ -4348,7 +4347,7 @@ void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { auto message = Api::MessageToSend(prepareSendAction({})); message.textWithTags = { toSend, TextWithTags::Tags() }; - message.action.replyTo = request.replyTo + message.action.replyTo.msgId = request.replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? request.replyTo : replyToId()) diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index f8ddd36e2..aeacfd251 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -1012,7 +1012,7 @@ void RepliesWidget::sendingFilesConfirmed( album, action); } - if (_composeControls->replyingToMessage().msg == action.replyTo) { + if (_composeControls->replyingToMessage().msg == action.replyTo.msgId) { _composeControls->cancelReplyMessage(); refreshTopBarActiveChat(); } @@ -1135,8 +1135,7 @@ bool RepliesWidget::showSendingFilesError( Api::SendAction RepliesWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = replyToId(); - result.topicRootId = _rootId; + result.replyTo = { .msgId = replyToId(), .topicRootId = _rootId }; result.options.sendAs = _composeControls->sendAsPeer(); return result; } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 5b4398c9f..e00e50d43 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -481,7 +481,6 @@ bool AttachWebView::IsSame( && (a->fromSwitch == b.fromSwitch) && (a->action.history == b.action.history) && (a->action.replyTo == b.action.replyTo) - && (a->action.topicRootId == b.action.topicRootId) && (a->action.options.sendAs == b.action.options.sendAs) && (a->action.options.silent == b.action.options.silent); } @@ -533,8 +532,7 @@ void AttachWebView::request(const WebViewButton &button) { const auto flags = Flag::f_theme_params | (button.url.isEmpty() ? Flag(0) : Flag::f_url) | (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param) - | (action.replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (action.topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (action.replyTo ? Flag::f_reply_to : Flag(0)) | (action.options.sendAs ? Flag::f_send_as : Flag(0)) | (action.options.silent ? Flag::f_silent : Flag(0)); _requestId = _session->api().request(MTPmessages_RequestWebView( @@ -545,8 +543,7 @@ void AttachWebView::request(const WebViewButton &button) { MTP_string(_startCommand), MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop"), - MTP_int(action.replyTo.bare), - MTP_int(action.topicRootId.bare), + action.mtpReplyTo(), (action.options.sendAs ? action.options.sendAs->input : MTP_inputPeerEmpty()) @@ -810,8 +807,7 @@ void AttachWebView::requestMenu( MTP_flags(Flag::f_theme_params | Flag::f_url | Flag::f_from_bot_menu - | (action.replyTo? Flag::f_reply_to_msg_id : Flag(0)) - | (action.topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (action.replyTo? Flag::f_reply_to : Flag(0)) | (action.options.sendAs ? Flag::f_send_as : Flag(0)) | (action.options.silent ? Flag::f_silent : Flag(0))), action.history->peer->input, @@ -820,8 +816,7 @@ void AttachWebView::requestMenu( MTPstring(), // start_param MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop"), - MTP_int(action.replyTo.bare), - MTP_int(action.topicRootId.bare), + action.mtpReplyTo(), (action.options.sendAs ? action.options.sendAs->input : MTP_inputPeerEmpty()) @@ -1189,15 +1184,13 @@ void AttachWebView::started(uint64 queryId) { _session->api().request(base::take(_prolongId)).cancel(); _prolongId = _session->api().request(MTPmessages_ProlongWebView( MTP_flags(Flag(0) - | (action.replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (action.topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (action.replyTo ? Flag::f_reply_to : Flag(0)) | (action.options.sendAs ? Flag::f_send_as : Flag(0)) | (action.options.silent ? Flag::f_silent : Flag(0))), action.history->peer->input, _bot->inputUser, MTP_long(queryId), - MTP_int(action.replyTo.bare), - MTP_int(action.topicRootId.bare), + action.mtpReplyTo(), (action.options.sendAs ? action.options.sendAs->input : MTP_inputPeerEmpty()) diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index 02de341e6..2c10d887c 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -374,7 +374,7 @@ void Result::addToHistory( PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor) const { flags |= MessageFlag::FromInlineBot; @@ -390,7 +390,7 @@ void Result::addToHistory( fromId, date, viaBotId, - replyToId, + replyTo, postAuthor, std::move(markup)); } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.h b/Telegram/SourceFiles/inline_bots/inline_bot_result.h index 45c8f6b39..296deb613 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.h @@ -69,7 +69,7 @@ public: PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor) const; QString getErrorOnSend(not_null history) const; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp index c1ec877e8..a2f342d28 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp @@ -36,18 +36,18 @@ void SendDataCommon::addToHistory( PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const { auto fields = getSentMessageFields(); - if (replyToId) { + if (replyTo) { flags |= MessageFlag::HasReplyInfo; } history->addNewLocalMessage( msgId, flags, viaBotId, - replyToId, + replyTo, date, fromId, postAuthor, @@ -119,14 +119,14 @@ void SendPhoto::addToHistory( PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const { history->addNewLocalMessage( msgId, flags, viaBotId, - replyToId, + replyTo, date, fromId, postAuthor, @@ -150,14 +150,14 @@ void SendFile::addToHistory( PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const { history->addNewLocalMessage( msgId, flags, viaBotId, - replyToId, + replyTo, date, fromId, postAuthor, @@ -181,14 +181,14 @@ void SendGame::addToHistory( PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const { history->addNewLocalMessage( msgId, flags, viaBotId, - replyToId, + replyTo, date, fromId, postAuthor, diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h index bab720f11..84c25624f 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h @@ -48,7 +48,7 @@ public: PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const = 0; virtual QString getErrorOnSend( @@ -90,7 +90,7 @@ public: PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const override; @@ -258,7 +258,7 @@ public: PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const override; @@ -299,7 +299,7 @@ public: PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const override; @@ -334,7 +334,7 @@ public: PeerId fromId, TimeId date, UserId viaBotId, - MsgId replyToId, + FullReplyTo replyTo, const QString &postAuthor, HistoryMessageMarkupData &&markup) const override; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index f46f37304..3f1a2871c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -363,9 +363,9 @@ void Controller::show( } _index = subindex; - const auto id = Data::FullStoryId{ - .user = list.user, - .id = item.id, + const auto id = FullStoryId{ + .peer = list.user->id, + .story = item.id, }; if (_shown == id) { return; @@ -425,7 +425,7 @@ void Controller::updatePlayback(const Player::TrackState &state) { updatePowerSaveBlocker(state); if (Player::IsStoppedAtEnd(state.state)) { if (!subjumpFor(1)) { - _delegate->storiesJumpTo({}); + _delegate->storiesClose(); } } } @@ -448,9 +448,9 @@ bool Controller::subjumpFor(int delta) { } else if (!_list || _list->items.empty()) { return false; } - _delegate->storiesJumpTo({ - .user = _list->user, - .id = _list->items.front().id + _delegate->storiesJumpTo(&_list->user->session(), { + .peer = _list->user->id, + .story = _list->items.front().id }); return true; } else if (index >= _list->total) { @@ -459,9 +459,9 @@ bool Controller::subjumpFor(int delta) { && jumpFor(1); } else if (index < _list->items.size()) { // #TODO stories load more - _delegate->storiesJumpTo({ - .user = _list->user, - .id = _list->items[index].id + _delegate->storiesJumpTo(&_list->user->session(), { + .peer = _list->user->id, + .story = _list->items[index].id }); } return true; @@ -471,12 +471,16 @@ bool Controller::subjumpFor(int delta) { bool Controller::jumpFor(int delta) { if (delta == -1) { if (const auto left = _siblingLeft.get()) { - _delegate->storiesJumpTo(left->shownId()); + _delegate->storiesJumpTo( + &left->peer()->session(), + left->shownId()); return true; } } else if (delta == 1) { if (const auto right = _siblingRight.get()) { - _delegate->storiesJumpTo(right->shownId()); + _delegate->storiesJumpTo( + &right->peer()->session(), + right->shownId()); return true; } } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 2715ce8f5..f553f895e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -139,7 +139,7 @@ private: Ui::Animations::Simple _contentFadeAnimation; bool _contentFaded = false; - Data::FullStoryId _shown; + FullStoryId _shown; TextWithEntities _captionText; std::optional _list; int _index = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index 8a34c47bd..a9c3b0a35 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -12,9 +12,9 @@ class Show; struct FileChosen; } // namespace ChatHelpers -namespace Data { -struct FullStoryId; -} // namespace Data +namespace Main { +class Session; +} // namespace Main namespace Ui { class RpWidget; @@ -39,7 +39,10 @@ public: -> std::shared_ptr = 0; [[nodiscard]] virtual auto storiesStickerOrEmojiChosen() -> rpl::producer = 0; - virtual void storiesJumpTo(Data::FullStoryId id) = 0; + virtual void storiesJumpTo( + not_null session, + FullStoryId id) = 0; + virtual void storiesClose() = 0; [[nodiscard]] virtual bool storiesPaused() = 0; [[nodiscard]] virtual float64 storiesSiblingOver(SiblingType type) = 0; virtual void storiesTogglePaused(bool paused) = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index a37b4a472..4b5e09bde 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -217,7 +217,8 @@ Sibling::Sibling( not_null controller, const Data::StoriesList &list) : _controller(controller) -, _id{ list.user, list.items.front().id } { +, _id{ list.user->id, list.items.front().id } +, _peer(list.user) { const auto &item = list.items.front(); const auto &data = item.media.data; const auto origin = Data::FileOrigin(); @@ -239,14 +240,18 @@ Sibling::Sibling( Sibling::~Sibling() = default; -Data::FullStoryId Sibling::shownId() const { +FullStoryId Sibling::shownId() const { return _id; } +not_null Sibling::peer() const { + return _peer; +} + bool Sibling::shows(const Data::StoriesList &list) const { Expects(!list.items.empty()); - return _id == Data::FullStoryId{ list.user, list.items.front().id }; + return _id == FullStoryId{ list.user->id, list.items.front().id }; } SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { @@ -268,22 +273,18 @@ SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { } QImage Sibling::userpicImage(const SiblingLayout &layout) { - Expects(_id.user != nullptr); - const auto ratio = style::DevicePixelRatio(); const auto size = layout.userpic.width() * ratio; - const auto key = _id.user->userpicUniqueKey(_userpicView); + const auto key = _peer->userpicUniqueKey(_userpicView); if (_userpicImage.width() != size || _userpicKey != key) { _userpicKey = key; - _userpicImage = _id.user->generateUserpicImage(_userpicView, size); + _userpicImage = _peer->generateUserpicImage(_userpicView, size); _userpicImage.setDevicePixelRatio(ratio); } return _userpicImage; } QImage Sibling::nameImage(const SiblingLayout &layout) { - Expects(_id.user != nullptr); - if (_nameFontSize != layout.nameFontSize) { _nameFontSize = layout.nameFontSize; @@ -299,7 +300,7 @@ QImage Sibling::nameImage(const SiblingLayout &layout) { .linkFontOver = font, }); }; - const auto text = _id.user->shortName(); + const auto text = _peer->shortName(); if (_nameText != text) { _name.reset(); _nameText = text; diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h index ecbd89de7..dc355fec6 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.h +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h @@ -29,7 +29,8 @@ public: const Data::StoriesList &list); ~Sibling(); - [[nodiscard]] Data::FullStoryId shownId() const; + [[nodiscard]] FullStoryId shownId() const; + [[nodiscard]] not_null peer() const; [[nodiscard]] bool shows(const Data::StoriesList &list) const; [[nodiscard]] SiblingView view( @@ -51,7 +52,8 @@ private: const not_null _controller; - Data::FullStoryId _id; + FullStoryId _id; + not_null _peer; QImage _blurred; QImage _good; Ui::Animations::Simple _goodShown; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 733c61851..1bd915558 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -4028,28 +4028,27 @@ auto OverlayWidget::storiesStickerOrEmojiChosen() return _storiesStickerOrEmojiChosen.events(); } -void OverlayWidget::storiesJumpTo(Data::FullStoryId id) { +void OverlayWidget::storiesJumpTo( + not_null session, + FullStoryId id) { Expects(_stories != nullptr); + Expects(id.valid()); - if (!id) { - close(); - return; - } - const auto &all = id.user->owner().stories().all(); + const auto &all = session->data().stories().all(); const auto i = ranges::find( all, - not_null(id.user), - &Data::StoriesList::user); + id.peer, + [](const Data::StoriesList &list) { return list.user->id; }); if (i == end(all)) { close(); return; } - const auto j = ranges::find(i->items, id.id, &Data::StoryItem::id); + const auto j = ranges::find(i->items, id.story, &Data::StoryItem::id); if (j == end(i->items)) { close(); return; } - setContext(StoriesContext{ i->user, id.id }); + setContext(StoriesContext{ i->user, id.story }); clearStreaming(); _streamingStartPaused = false; const auto &data = j->media.data; @@ -4061,6 +4060,10 @@ void OverlayWidget::storiesJumpTo(Data::FullStoryId id) { } } +void OverlayWidget::storiesClose() { + close(); +} + bool OverlayWidget::storiesPaused() { return _streamed && !_streamed->instance.player().failed() diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 43551bf41..a82aa1f3c 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -30,7 +30,6 @@ enum class activation : uchar; namespace Data { class PhotoMedia; class DocumentMedia; -struct FullStoryId; } // namespace Data namespace Ui { @@ -243,7 +242,10 @@ private: std::shared_ptr storiesShow() override; auto storiesStickerOrEmojiChosen() -> rpl::producer override; - void storiesJumpTo(Data::FullStoryId id) override; + void storiesJumpTo( + not_null session, + FullStoryId id) override; + void storiesClose() override; bool storiesPaused() override; void storiesTogglePaused(bool paused) override; float64 storiesSiblingOver(Stories::SiblingType type) override; diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index f1961f042..cf9f4c775 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -1584,7 +1584,7 @@ void FormController::uploadEncryptedFile( auto prepared = std::make_shared( TaskId(), file.uploadData->fileId, - FileLoadTo(PeerId(), Api::SendOptions(), MsgId(), MsgId(), MsgId()), + FileLoadTo(PeerId(), Api::SendOptions(), FullReplyTo(), MsgId()), TextWithTags(), false, std::shared_ptr(nullptr)); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 1386b2a74..1516594a9 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -221,56 +221,6 @@ int PhotoSideLimit() { return PhotoSideLimit(SendLargePhotos.value()); } -SendMediaPrepare::SendMediaPrepare( - const QString &file, - const PeerId &peer, - SendMediaType type, - MsgId replyTo) -: id(base::RandomValue()) -, file(file) -, peer(peer) -, type(type) -, replyTo(replyTo) { -} - -SendMediaPrepare::SendMediaPrepare( - const QImage &img, - const PeerId &peer, - SendMediaType type, - MsgId replyTo) -: id(base::RandomValue()) -, img(img) -, peer(peer) -, type(type) -, replyTo(replyTo) { -} - -SendMediaPrepare::SendMediaPrepare( - const QByteArray &data, - const PeerId &peer, - SendMediaType type, - MsgId replyTo) -: id(base::RandomValue()) -, data(data) -, peer(peer) -, type(type) -, replyTo(replyTo) { -} - -SendMediaPrepare::SendMediaPrepare( - const QByteArray &data, - int duration, - const PeerId &peer, - SendMediaType type, - MsgId replyTo) -: id(base::RandomValue()) -, data(data) -, peer(peer) -, type(type) -, duration(duration) -, replyTo(replyTo) { -} - SendMediaReady::SendMediaReady( SendMediaType type, const QString &file, @@ -284,10 +234,8 @@ SendMediaReady::SendMediaReady( const MTPPhoto &photo, const PreparedPhotoThumbs &photoThumbs, const MTPDocument &document, - const QByteArray &jpeg, - MsgId replyTo) -: replyTo(replyTo) -, type(type) + const QByteArray &jpeg) +: type(type) , file(file) , filename(filename) , filesize(filesize) diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h index 59e3c1325..3d4354623 100644 --- a/Telegram/SourceFiles/storage/localimageloader.h +++ b/Telegram/SourceFiles/storage/localimageloader.h @@ -36,41 +36,6 @@ enum class SendMediaType { Secure, }; -struct SendMediaPrepare { - SendMediaPrepare( - const QString &file, - const PeerId &peer, - SendMediaType type, - MsgId replyTo); - SendMediaPrepare( - const QImage &img, - const PeerId &peer, - SendMediaType type, - MsgId replyTo); - SendMediaPrepare( - const QByteArray &data, - const PeerId &peer, - SendMediaType type, - MsgId replyTo); - SendMediaPrepare( - const QByteArray &data, - int duration, - const PeerId &peer, - SendMediaType type, - MsgId replyTo); - - PhotoId id; - QString file; - QImage img; - QByteArray data; - PeerId peer; - SendMediaType type; - int duration = 0; - MsgId replyTo; - -}; -using SendMediaPrepareList = QList; - using UploadFileParts = QMap; struct SendMediaReady { SendMediaReady() = default; // temp @@ -87,10 +52,8 @@ struct SendMediaReady { const MTPPhoto &photo, const PreparedPhotoThumbs &photoThumbs, const MTPDocument &document, - const QByteArray &jpeg, - MsgId replyTo); + const QByteArray &jpeg); - MsgId replyTo; SendMediaType type; QString file, filename; int64 filesize = 0; @@ -206,19 +169,16 @@ struct FileLoadTo { FileLoadTo( PeerId peer, Api::SendOptions options, - MsgId replyTo, - MsgId topicRootId, + FullReplyTo replyTo, MsgId replaceMediaOf) : peer(peer) , options(options) , replyTo(replyTo) - , topicRootId(topicRootId) , replaceMediaOf(replaceMediaOf) { } PeerId peer; Api::SendOptions options; - MsgId replyTo; - MsgId topicRootId; + FullReplyTo replyTo; MsgId replaceMediaOf; }; diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp index 3ebec4074..aba4ee89a 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.cpp +++ b/Telegram/SourceFiles/support/support_autocomplete.cpp @@ -276,7 +276,7 @@ AdminLog::OwnedItem GenerateCommentItem( const auto flags = MessageFlag::HasFromId | MessageFlag::Outgoing | MessageFlag::FakeHistoryItem; - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto groupedId = uint64(); const auto item = history->makeMessage( @@ -298,7 +298,7 @@ AdminLog::OwnedItem GenerateContactItem( not_null delegate, not_null history, const Contact &data) { - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto postAuthor = QString(); const auto groupedId = uint64(); diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index da8bc9a1f..2049d28d1 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1113,13 +1113,16 @@ void Manager::notificationReplied( auto message = Api::MessageToSend(Api::SendAction(history)); message.textWithTags = reply; - message.action.replyTo = (id.msgId > 0 && !history->peer->isUser() + const auto replyToId = (id.msgId > 0 && !history->peer->isUser() && id.msgId != topicRootId) ? id.msgId : history->peer->isForum() ? topicRootId : MsgId(0); - message.action.topicRootId = topic ? topic->rootId() : 0; + message.action.replyTo = { + .msgId = replyToId, + .topicRootId = topic ? topic->rootId() : 0, + }; message.action.clearDraft = false; history->session().api().sendMessage(std::move(message)); diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index 64850eaef..c43996e6a 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -1581,8 +1581,7 @@ SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) { MTP_photoEmpty(MTP_long(0)), thumbnails, document, - QByteArray(), - 0); + QByteArray()); } } // namespace Theme diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp index b045fb2de..46b5cf5fb 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp @@ -451,8 +451,7 @@ SendMediaReady PrepareThemeMedia( MTP_photoEmpty(MTP_long(0)), thumbnails, document, - thumbnailBytes, - 0); + thumbnailBytes); } Fn SavePreparedTheme( diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 3921a21cb..7b908dc47 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -108,21 +108,16 @@ void ShareBotGame( const auto topicRootId = replyTo; auto flags = MTPmessages_SendMedia::Flags(0); if (replyTo) { - flags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (topicRootId) { - flags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + flags |= MTPmessages_SendMedia::Flag::f_reply_to; } histories.sendPreparedMessage( history, - replyTo, - topicRootId, + FullReplyTo{ .msgId = replyTo, .topicRootId = topicRootId }, randomId, Data::Histories::PrepareMessage( MTP_flags(flags), history->peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_inputMediaGame( MTP_inputGameShortName( bot->inputUser, @@ -1447,8 +1442,7 @@ void PeerMenuCreatePoll( peer->owner().history(peer), result.options); action.clearDraft = false; - action.replyTo = replyToId; - action.topicRootId = topicRootId; + action.replyTo = { .msgId = replyToId, .topicRootId = topicRootId }; if (const auto local = action.history->localDraft(topicRootId)) { action.clearDraft = local->textWithTags.text.isEmpty(); } From ff902f2a1f30fb3aa938395211b709acddf4a25c Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 25 May 2023 14:56:32 +0400 Subject: [PATCH 032/260] Send StoryReply info correctly. --- Telegram/SourceFiles/media/stories/media_stories_controller.cpp | 2 +- Telegram/SourceFiles/media/stories/media_stories_reply.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 3f1a2871c..82d1696e1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -376,7 +376,7 @@ void Controller::show( _header->show({ .user = list.user, .date = item.date }); _slider->show({ .index = _index, .total = list.total }); - _replyArea->show({ .user = list.user }); + _replyArea->show({ .user = list.user, .id = id.story }); if (_contentFaded) { togglePaused(true); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index bbc28a75d..b11228f46 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -195,6 +195,7 @@ Api::SendAction ReplyArea::prepareSendAction( const auto history = _data.user->owner().history(_data.user); auto result = Api::SendAction(history, options); result.options.sendAs = _controls->sendAsPeer(); + result.replyTo.storyId = { .peer = _data.user->id, .story = _data.id }; return result; } From 7a042c23e960d882a2942029b2b9ec5c1f826dfb Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 May 2023 11:21:19 +0400 Subject: [PATCH 033/260] Use real stories data, open from chats list. --- Telegram/SourceFiles/data/data_stories.cpp | 228 +++++++++--------- Telegram/SourceFiles/data/data_stories.h | 76 ++++-- .../dialogs/dialogs_inner_widget.cpp | 11 + .../dialogs/ui/dialogs_stories_content.cpp | 18 +- .../dialogs/ui/dialogs_stories_list.cpp | 179 +++++++++++--- .../dialogs/ui/dialogs_stories_list.h | 7 + .../stories/media_stories_controller.cpp | 45 ++-- .../media/stories/media_stories_sibling.cpp | 56 +++-- .../media/stories/media_stories_sibling.h | 5 +- .../media/view/media_view_open_common.h | 20 ++ .../media/view/media_view_overlay_widget.cpp | 92 +++---- .../media/view/media_view_overlay_widget.h | 4 +- .../window/window_session_controller.cpp | 17 ++ .../window/window_session_controller.h | 2 + 14 files changed, 482 insertions(+), 278 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 2fb2f8c28..dbddd0da0 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -26,7 +26,78 @@ namespace { } // namespace bool StoriesList::unread() const { - return !items.empty() && readTill < items.front().id; + return !ids.empty() && readTill < ids.front(); +} + +Story::Story( + StoryId id, + not_null peer, + StoryMedia media, + TimeId date) +: _id(id) +, _peer(peer) +, _media(std::move(media)) +, _date(date) { +} + +Session &Story::owner() const { + return _peer->owner(); +} + +Main::Session &Story::session() const { + return _peer->session(); +} + +not_null Story::peer() const { + return _peer; +} + +StoryId Story::id() const { + return _id; +} + +TimeId Story::date() const { + return _date; +} + +const StoryMedia &Story::media() const { + return _media; +} + +PhotoData *Story::photo() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +DocumentData *Story::document() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +void Story::setPinned(bool pinned) { + _pinned = pinned; +} + +bool Story::pinned() const { + return _pinned; +} + +void Story::setCaption(TextWithEntities &&caption) { + _caption = std::move(caption); +} + +const TextWithEntities &Story::caption() const { + return _caption; +} + +void Story::apply(const MTPDstoryItem &data) { + _pinned = data.is_pinned(); + _caption = TextWithEntities{ + data.vcaption().value_or_empty(), + Api::EntitiesFromMTP( + &owner().session(), + data.ventities().value_or_empty()), + }; } Stories::Stories(not_null owner) : _owner(owner) { @@ -41,6 +112,7 @@ Session &Stories::owner() const { void Stories::apply(const MTPDupdateStories &data) { pushToFront(parse(data.vstories())); + _allChanged.fire({}); } StoriesList Stories::parse(const MTPUserStories &stories) { @@ -54,25 +126,36 @@ StoriesList Stories::parse(const MTPUserStories &stories) { .total = count, }; const auto &list = data.vstories().v; - result.items.reserve(list.size()); + result.ids.reserve(list.size()); for (const auto &story : list) { story.match([&](const MTPDstoryItem &data) { - if (auto entry = parse(data)) { - result.items.push_back(std::move(*entry)); + if (const auto story = parse(result.user, data)) { + result.ids.push_back(story->id()); } else { --result.total; } - }, [&](const MTPDstoryItemSkipped &) { - }, [&](const MTPDstoryItemDeleted &) { + }, [&](const MTPDstoryItemSkipped &data) { + result.ids.push_back(data.vid().v); + }, [&](const MTPDstoryItemDeleted &data) { + _deleted.emplace(FullStoryId{ + .peer = peerFromUser(userId), + .story = data.vid().v, + }); --result.total; }); } - result.total = std::min(result.total, int(result.items.size())); + result.total = std::max(result.total, int(result.ids.size())); return result; } -std::optional Stories::parse(const MTPDstoryItem &data) { +Story *Stories::parse(not_null peer, const MTPDstoryItem &data) { const auto id = data.vid().v; + auto &stories = _stories[peer->id]; + const auto i = stories.find(id); + if (i != end(stories)) { + i->second->apply(data); + return i->second.get(); + } using MaybeMedia = std::optional< std::variant, not_null>>; const auto media = data.vmedia().match([&]( @@ -95,24 +178,15 @@ std::optional Stories::parse(const MTPDstoryItem &data) { return {}; }, [](const auto &) { return MaybeMedia(); }); if (!media) { - return {}; + return nullptr; } - auto caption = TextWithEntities{ - data.vcaption().value_or_empty(), - Api::EntitiesFromMTP( - &_owner->session(), - data.ventities().value_or_empty()), - }; - auto privacy = StoryPrivacy(); - - const auto date = data.vdate().v; - return StoryItem{ - .id = data.vid().v, - .media = { *media }, - .caption = std::move(caption), - .date = date, - .privacy = privacy, - }; + const auto result = stories.emplace(id, std::make_unique( + id, + peer, + StoryMedia{ *media }, + data.vdate().v)).first->second.get(); + result->apply(data); + return result; } void Stories::loadMore() { @@ -134,6 +208,7 @@ void Stories::loadMore() { for (const auto &single : data.vuser_stories().v) { pushToBack(parse(single)); } + _allChanged.fire({}); }, [](const MTPDstories_allStoriesNotModified &) { }); }).fail([=] { @@ -153,96 +228,20 @@ rpl::producer<> Stories::allChanged() const { return _allChanged.events(); } -// #TODO stories testing -StoryId Stories::generate( - not_null item, - std::variant< - v::null_t, - not_null, - not_null> media) { - if (v::is_null(media) - || !item->from()->isUser() - || !item->isRegular()) { - return {}; +base::expected, NoStory> Stories::lookup( + FullStoryId id) const { + const auto i = _stories.find(id.peer); + if (i != end(_stories)) { + const auto j = i->second.find(id.story); + if (j != end(i->second)) { + return j->second.get(); + } } - const auto document = v::is>(media) - ? v::get>(media).get() - : nullptr; - if (document && !document->isVideoFile()) { - return {}; - } - using namespace Storage; - auto resultId = StoryId(); - const auto listType = SharedMediaType::PhotoVideo; - const auto itemId = item->id; - const auto peer = item->history()->peer; - const auto session = &peer->session(); - auto full = std::vector(); - const auto lifetime = session->storage().query(SharedMediaQuery( - SharedMediaKey(peer->id, MsgId(0), listType, itemId), - 32, - 32 - )) | rpl::start_with_next([&](SharedMediaResult &&result) { - if (!result.messageIds.contains(itemId)) { - result.messageIds.emplace(itemId); - } - auto index = StoryId(); - const auto owner = &peer->owner(); - for (const auto id : result.messageIds) { - if (const auto item = owner->message(peer, id)) { - const auto user = item->from()->asUser(); - if (!user) { - continue; - } - const auto i = ranges::find( - full, - not_null(user), - &StoriesList::user); - auto &stories = (i == end(full)) - ? full.emplace_back(StoriesList{ .user = user }) - : *i; - if (id == itemId) { - resultId = ++index; - stories.items.push_back({ - .id = resultId, - .media = (document - ? StoryMedia{ not_null(document) } - : StoryMedia{ - v::get>(media) }), - .caption = item->originalText(), - .date = item->date(), - }); - ++stories.total; - } else if (const auto media = item->media()) { - const auto photo = media->photo(); - const auto document = media->document(); - if (photo || (document && document->isVideoFile())) { - stories.items.push_back({ - .id = ++index, - .media = (document - ? StoryMedia{ not_null(document) } - : StoryMedia{ not_null(photo) }), - .caption = item->originalText(), - .date = item->date(), - }); - ++stories.total; - } - } - } - } - for (auto &stories : full) { - const auto i = ranges::find( - _all, - stories.user, - &StoriesList::user); - if (i != end(_all)) { - *i = std::move(stories); - } else { - _all.push_back(std::move(stories)); - } - } - }); - return resultId; + return base::make_unexpected( + _deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown); +} + +void Stories::resolve(FullStoryId id, Fn done) { } void Stories::pushToBack(StoriesList &&list) { @@ -255,7 +254,6 @@ void Stories::pushToBack(StoriesList &&list) { } else { _all.push_back(std::move(list)); } - _allChanged.fire({}); } void Stories::pushToFront(StoriesList &&list) { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 6c3aaa56d..e5f3b49db 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -7,37 +7,64 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/expected.h" + class PhotoData; class DocumentData; +namespace Main { +class Session; +} // namespace Main + namespace Data { class Session; -struct StoryPrivacy { - friend inline bool operator==(StoryPrivacy, StoryPrivacy) = default; -}; - struct StoryMedia { std::variant, not_null> data; friend inline bool operator==(StoryMedia, StoryMedia) = default; }; -struct StoryItem { - StoryId id = 0; - StoryMedia media; - TextWithEntities caption; - TimeId date = 0; - StoryPrivacy privacy; - bool pinned = false; +class Story { +public: + Story( + StoryId id, + not_null peer, + StoryMedia media, + TimeId date); + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + [[nodiscard]] not_null peer() const; + + [[nodiscard]] StoryId id() const; + [[nodiscard]] TimeId date() const; + [[nodiscard]] const StoryMedia &media() const; + [[nodiscard]] PhotoData *photo() const; + [[nodiscard]] DocumentData *document() const; + + void setPinned(bool pinned); + [[nodiscard]] bool pinned() const; + + void setCaption(TextWithEntities &&caption); + [[nodiscard]] const TextWithEntities &caption() const; + + void apply(const MTPDstoryItem &data); + +private: + const StoryId _id = 0; + const not_null _peer; + const StoryMedia _media; + TextWithEntities _caption; + const TimeId _date = 0; + bool _pinned = false; - friend inline bool operator==(StoryItem, StoryItem) = default; }; struct StoriesList { not_null user; - std::vector items; + std::vector ids; StoryId readTill = 0; int total = 0; @@ -46,6 +73,11 @@ struct StoriesList { friend inline bool operator==(StoriesList, StoriesList) = default; }; +enum class NoStory : uchar { + Unknown, + Deleted, +}; + class Stories final { public: explicit Stories(not_null owner); @@ -60,22 +92,24 @@ public: [[nodiscard]] bool allLoaded() const; [[nodiscard]] rpl::producer<> allChanged() const; - // #TODO stories testing - [[nodiscard]] StoryId generate( - not_null item, - std::variant< - v::null_t, - not_null, - not_null> media); + [[nodiscard]] base::expected, NoStory> lookup( + FullStoryId id) const; + void resolve(FullStoryId id, Fn done); private: [[nodiscard]] StoriesList parse(const MTPUserStories &stories); - [[nodiscard]] std::optional parse(const MTPDstoryItem &data); + [[nodiscard]] Story *parse( + not_null peer, + const MTPDstoryItem &data); void pushToBack(StoriesList &&list); void pushToFront(StoriesList &&list); const not_null _owner; + base::flat_map< + PeerId, + base::flat_map>> _stories; + base::flat_set _deleted; std::vector _all; rpl::event_stream<> _allChanged; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index a4c0bb267..33a2a93cd 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_stories.h" #include "data/stickers/data_stickers.h" #include "data/data_send_action.h" #include "base/unixtime.h" @@ -335,6 +336,11 @@ InnerWidget::InnerWidget( clearSelection(); }, lifetime()); + _stories->clicks( + ) | rpl::start_with_next([=](uint64 id) { + _controller->openPeerStories(PeerId(int64(id))); + }, lifetime()); + handleChatListEntryRefreshes(); refreshWithCollapsedRows(true); @@ -590,6 +596,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { if (_controller->contentOverlapped(this, e)) { return; } + const auto fillGuard = gsl::finally([&] { + // We translate painter down, but it'll be cropped below rect. + p.fillRect(rect(), st::dialogsBg); + }); + const auto activeEntry = _controller->activeChatEntryCurrent(); const auto videoPaused = _controller->isGifPausedAtLeastFor( Window::GifPauseReason::Any); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 021d07328..b1262388f 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -128,13 +128,13 @@ State::State(not_null data) Content State::next() { auto result = Content(); -#if 0 // #TODO stories testing +#if 1 // #TODO stories testing const auto &all = _data->all(); result.users.reserve(all.size()); for (const auto &list : all) { auto userpic = std::shared_ptr(); const auto user = list.user; -#endif +#else const auto list = _data->owner().chatsList(); const auto &all = list->indexed()->all(); result.users.reserve(all.size()); @@ -142,6 +142,7 @@ Content State::next() { if (const auto history = entry->history()) { if (const auto user = history->peer->asUser(); user && !user->isBot()) { auto userpic = std::shared_ptr(); +#endif if (const auto i = _userpics.find(user); i != end(_userpics)) { userpic = i->second; } else { @@ -152,10 +153,14 @@ Content State::next() { .id = uint64(user->id.value), .name = user->shortName(), .userpic = std::move(userpic), - .unread = history->chatListBadgesState().unread// list.unread(), +#if 1 // #TODO stories testing + .unread = list.unread(), +#else + .unread = history->chatListBadgesState().unread }); } - } +#endif + }); } return result; } @@ -170,15 +175,16 @@ rpl::producer ContentForSession(not_null session) { rpl::single( rpl::empty ) | rpl::then( -#if 0 // #TODO stories testing +#if 1 // #TODO stories testing stories->allChanged() -#endif +#else rpl::merge( session->data().chatsListChanges( ) | rpl::filter( rpl::mappers::_1 == nullptr ) | rpl::to_empty, session->data().unreadBadgeChanges()) +#endif ) | rpl::start_with_next([=] { consumer.put_next(state->next()); }, result); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index d5d1f3d3c..3a8cdfd0a 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -29,6 +29,20 @@ constexpr auto kSummaryExpandLeft = 1.5; } // namespace +struct List::Layout { + int itemsCount = 0; + int shownHeight = 0; + float64 ratio = 0.; + float64 userpicLeft = 0.; + float64 photoLeft = 0.; + float64 left = 0.; + int startIndexSmall = 0; + int endIndexSmall = 0; + int startIndexFull = 0; + int endIndexFull = 0; + int singleFull = 0; +}; + List::List( not_null parent, rpl::producer content, @@ -42,6 +56,7 @@ List::List( }, lifetime()); _shownAnimation.stop(); + setMouseTracking(true); resize(0, _data.empty() ? 0 : st::dialogsStoriesFull.height); } @@ -214,30 +229,29 @@ void List::resizeEvent(QResizeEvent *e) { updateScrollMax(); } -void List::paintEvent(QPaintEvent *e) { +List::Layout List::computeLayout() const { const auto &st = st::dialogsStories; const auto &full = st::dialogsStoriesFull; const auto shownHeight = std::max(_shownHeight(), st.height); const auto ratio = float64(shownHeight - st.height) / (full.height - st.height); - const auto lerp = [=](float64 a, float64 b) { + const auto lerp = [&](float64 a, float64 b) { return a + (b - a) * ratio; }; auto &rendering = _data.empty() ? _hidingData : _data; - const auto photo = lerp(st.photo, full.photo); - const auto photoTopSmall = (st.height - st.photo) / 2.; - const auto photoTop = lerp(photoTopSmall, full.photoTop); - const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; - const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; - const auto summaryTop = st.nameTop - - (st.photoTop + (st.photo / 2.)) - + (photoTop + (photo / 2.)); - const auto singleSmall = st.shift; const auto singleFull = full.photoLeft * 2 + full.photo; - const auto single = lerp(singleSmall, singleFull); const auto itemsCount = int(rendering.items.size()); - const auto leftSmall = st.left; - const auto leftFull = full.left - _scrollLeft; + const auto narrowWidth = st::defaultDialogRow.padding.left() + + st::defaultDialogRow.photoSize + + st::defaultDialogRow.padding.left(); + const auto narrow = (width() <= narrowWidth); + const auto smallWidth = st.photo + (itemsCount - 1) * st.shift; + const auto leftSmall = narrow + ? ((narrowWidth - smallWidth) / 2 - st.photoLeft) + : st.left; + const auto leftFull = (narrow + ? ((narrowWidth - full.photo) / 2 - full.photoLeft) + : full.left) - _scrollLeft; const auto startIndexFull = std::max(-leftFull, 0) / singleFull; const auto cellLeftFull = leftFull + (startIndexFull * singleFull); const auto endIndexFull = std::min( @@ -250,18 +264,51 @@ void List::paintEvent(QPaintEvent *e) { const auto userpicLeftSmall = cellLeftSmall + st.photoLeft; const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull); const auto photoLeft = lerp(st.photoLeft, full.photoLeft); - const auto left = userpicLeft - photoLeft; - const auto nameScale = shownHeight / float64(full.height); + return Layout{ + .itemsCount = itemsCount, + .shownHeight = shownHeight, + .ratio = ratio, + .userpicLeft = userpicLeft, + .photoLeft = photoLeft, + .left = userpicLeft - photoLeft, + .startIndexSmall = startIndexSmall, + .endIndexSmall = endIndexSmall, + .startIndexFull = startIndexFull, + .endIndexFull = endIndexFull, + .singleFull = singleFull, + }; +} + +void List::paintEvent(QPaintEvent *e) { + const auto &st = st::dialogsStories; + const auto &full = st::dialogsStoriesFull; + const auto layout = computeLayout(); + const auto ratio = layout.ratio; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + auto &rendering = _data.empty() ? _hidingData : _data; + const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; + const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; + const auto singleSmall = st.shift; + const auto single = lerp(singleSmall, layout.singleFull); + const auto photoTopSmall = (st.height - st.photo) / 2.; + const auto photoTop = lerp(photoTopSmall, full.photoTop); + const auto photo = lerp(st.photo, full.photo); + const auto summaryTop = st.nameTop + - (st.photoTop + (st.photo / 2.)) + + (photoTop + (photo / 2.)); + const auto nameScale = layout.shownHeight / float64(full.height); const auto nameTop = nameScale * full.nameTop; const auto nameWidth = nameScale * AvailableNameWidth(); const auto nameHeight = nameScale * full.nameStyle.font->height; - const auto nameLeft = photoLeft + (photo - nameWidth) / 2.; + const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.); const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.); auto p = QPainter(this); p.fillRect(e->rect(), st::dialogsBg); - p.translate(0, height() - shownHeight); + p.translate(0, height() - layout.shownHeight); const auto drawSmall = (ratio < 1.); const auto drawFull = (ratio > 0.); @@ -270,8 +317,8 @@ void List::paintEvent(QPaintEvent *e) { paintSummary(p, rendering, summaryTop, ratio); const auto count = std::max( - endIndexFull - startIndexFull, - endIndexSmall - startIndexSmall); + layout.endIndexFull - layout.startIndexFull, + layout.endIndexSmall - layout.startIndexSmall); struct Single { float64 x = 0.; @@ -285,15 +332,15 @@ void List::paintEvent(QPaintEvent *e) { } }; const auto lookup = [&](int index) { - const auto indexSmall = startIndexSmall + index; - const auto indexFull = startIndexFull + index; - const auto small = (drawSmall && indexSmall < endIndexSmall) + const auto indexSmall = layout.startIndexSmall + index; + const auto indexFull = layout.startIndexFull + index; + const auto small = (drawSmall && indexSmall < layout.endIndexSmall) ? &rendering.items[indexSmall] : nullptr; - const auto full = (drawFull && indexFull < endIndexFull) + const auto full = (drawFull && indexFull < layout.endIndexFull) ? &rendering.items[indexFull] : nullptr; - const auto x = left + single * index; + const auto x = layout.left + single * index; return Single{ x, indexSmall, small, indexFull, full }; }; const auto hasUnread = [&](const Single &single) { @@ -334,7 +381,7 @@ void List::paintEvent(QPaintEvent *e) { // Unread gradient. const auto x = single.x; - const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo); + const auto userpic = QRectF(x + layout.photoLeft, photoTop, photo, photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; const auto smallUnread = small && small->user.unread; @@ -367,7 +414,7 @@ void List::paintEvent(QPaintEvent *e) { Expects(single.itemSmall || single.itemFull); const auto x = single.x; - const auto userpic = QRectF(x + photoLeft, photoTop, photo, photo); + const auto userpic = QRectF(x + layout.photoLeft, photoTop, photo, photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; const auto smallUnread = small && small->user.unread; @@ -549,7 +596,7 @@ void List::wheelEvent(QWheelEvent *e) { if (next != now) { _expandRequests.fire({}); _scrollLeft = next; - //updateSelected(); + updateSelected(); update(); } e->accept(); @@ -559,13 +606,16 @@ void List::mousePressEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) { return; } - _mouseDownPosition = _lastMousePosition = e->globalPos(); - //updateSelected(); + _lastMousePosition = e->globalPos(); + updateSelected(); + + _mouseDownPosition = _lastMousePosition; + _pressed = _selected; } void List::mouseMoveEvent(QMouseEvent *e) { _lastMousePosition = e->globalPos(); - //updateSelected(); + updateSelected(); if (!_dragging && _mouseDownPosition) { if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() @@ -601,11 +651,18 @@ void List::mouseReleaseEvent(QMouseEvent *e) { _mouseDownPosition = std::nullopt; }); - //const auto wasDown = std::exchange(_pressed, SpecialOver::None); + const auto pressed = std::exchange(_pressed, -1); if (finishDragging()) { return; } - //updateSelected(); + updateSelected(); + if (_selected == pressed) { + if (_selected < 0) { + _expandRequests.fire({}); + } else if (_selected < _data.items.size()) { + _clicks.fire_copy(_data.items[_selected].user.id); + } + } } bool List::finishDragging() { @@ -614,8 +671,62 @@ bool List::finishDragging() { } checkDragging(); _dragging = false; - //updateSelected(); + updateSelected(); return true; } +void List::updateSelected() { + if (_pressed >= 0) { + return; + } + const auto &st = st::dialogsStories; + const auto &full = st::dialogsStoriesFull; + const auto p = mapFromGlobal(_lastMousePosition); + const auto layout = computeLayout(); + const auto firstRightFull = full.left + layout.singleFull; + const auto firstRightSmall = st.left + + st.photoLeft + + st.photo; + const auto stepFull = layout.singleFull; + const auto stepSmall = st.shift; + const auto lastRightAddFull = 0; + const auto lastRightAddSmall = st.photoLeft; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * layout.ratio; + }; + const auto firstRight = lerp(firstRightSmall, firstRightFull); + const auto step = lerp(stepSmall, stepFull); + const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull); + const auto activateFull = (layout.ratio >= 0.5); + const auto startIndex = activateFull + ? layout.startIndexFull + : layout.startIndexSmall; + const auto endIndex = activateFull + ? layout.endIndexFull + : layout.endIndexSmall; + const auto x = p.x(); + const auto infiniteIndex = (x < firstRight) + ? 0 + : int(std::floor(((x - firstRight) / step) + 1)); + const auto index = (endIndex == startIndex) + ? -1 + : (infiniteIndex == endIndex - startIndex + && x < firstRight + + (endIndex - startIndex - 1) * step + + lastRightAdd) + ? (infiniteIndex - 1) // Last small part should still be clickable. + : infiniteIndex; + const auto selected = (index < 0 + || startIndex + index >= layout.itemsCount) + ? -1 + : (startIndex + index); + if (_selected != selected) { + const auto over = (selected >= 0); + if (over != (_selected >= 0)) { + setCursor(over ? style::cur_pointer : style::cur_default); + } + _selected = selected; + } +} + } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 636f855f7..1286db93e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -49,6 +49,7 @@ public: [[nodiscard]] rpl::producer<> entered() const; private: + struct Layout; struct Item { User user; QImage nameCache; @@ -106,6 +107,7 @@ private: void validateName(not_null item); void updateScrollMax(); void updateSummary(Data &data); + void updateSelected(); void checkDragging(); bool finishDragging(); @@ -117,6 +119,8 @@ private: float64 summaryTop, float64 hidden); + [[nodiscard]] Layout computeLayout() const; + Content _content; Data _data; Data _hidingData; @@ -134,6 +138,9 @@ private: int _scrollLeftMax = 0; bool _dragging = false; + int _selected = -1; + int _pressed = -1; + }; } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 82d1696e1..f68680c43 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/power_save_blocker.h" #include "chat_helpers/compose/compose_show.h" +#include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" #include "media/stories/media_stories_caption_full_view.h" @@ -127,7 +128,9 @@ Controller::Controller(not_null delegate) focused ? 0. : 1., focused ? 1. : 0., st::fadeWrapDuration); - togglePaused(focused); + if (_started) { + togglePaused(focused); + } }, _lifetime); _contentFadeAnimation.stop(); @@ -344,15 +347,23 @@ void Controller::show( int index, int subindex) { Expects(index >= 0 && index < lists.size()); - Expects(subindex >= 0 && subindex < lists[index].items.size()); + Expects(subindex >= 0 && subindex < lists[index].ids.size()); showSiblings(lists, index); const auto &list = lists[index]; - const auto &item = list.items[subindex]; + const auto id = list.ids[subindex]; + const auto maybeStory = list.user->owner().stories().lookup({ + .peer = list.user->id, + .story = id, + }); + if (!maybeStory) { + return; + } + const auto story = *maybeStory; const auto guard = gsl::finally([&] { _started = false; - if (v::is>(item.media.data)) { + if (story->photo()) { _photoPlayback = std::make_unique(this); } else { _photoPlayback = nullptr; @@ -363,20 +374,20 @@ void Controller::show( } _index = subindex; - const auto id = FullStoryId{ + const auto storyId = FullStoryId{ .peer = list.user->id, - .story = item.id, + .story = id, }; - if (_shown == id) { + if (_shown == storyId) { return; } - _shown = id; - _captionText = item.caption; + _shown = storyId; + _captionText = story->caption(); _captionFullView = nullptr; - _header->show({ .user = list.user, .date = item.date }); + _header->show({ .user = list.user, .date = story->date() }); _slider->show({ .index = _index, .total = list.total }); - _replyArea->show({ .user = list.user, .id = id.story }); + _replyArea->show({ .user = list.user, .id = id }); if (_contentFaded) { togglePaused(true); @@ -395,7 +406,7 @@ void Controller::showSiblings( void Controller::showSibling( std::unique_ptr &sibling, const Data::StoriesList *list) { - if (!list || list->items.empty()) { + if (!list || list->ids.empty()) { sibling = nullptr; } else if (!sibling || !sibling->shows(*list)) { sibling = std::make_unique(this, *list); @@ -407,7 +418,7 @@ void Controller::ready() { return; } _started = true; - if (_photoPlayback) { + if (!_contentFaded && _photoPlayback) { _photoPlayback->togglePaused(false); } } @@ -445,23 +456,23 @@ bool Controller::subjumpFor(int delta) { if (index < 0) { if (_siblingLeft && _siblingLeft->shownId().valid()) { return jumpFor(-1); - } else if (!_list || _list->items.empty()) { + } else if (!_list || _list->ids.empty()) { return false; } _delegate->storiesJumpTo(&_list->user->session(), { .peer = _list->user->id, - .story = _list->items.front().id + .story = _list->ids.front() }); return true; } else if (index >= _list->total) { return _siblingRight && _siblingRight->shownId().valid() && jumpFor(1); - } else if (index < _list->items.size()) { + } else if (index < _list->ids.size()) { // #TODO stories load more _delegate->storiesJumpTo(&_list->user->session(), { .peer = _list->user->id, - .story = _list->items[index].id + .story = _list->ids[index] }); } return true; diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index 4b5e09bde..ac3c43258 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -217,29 +217,47 @@ Sibling::Sibling( not_null controller, const Data::StoriesList &list) : _controller(controller) -, _id{ list.user->id, list.items.front().id } +, _id{ list.user->id, list.ids.front() } , _peer(list.user) { - const auto &item = list.items.front(); - const auto &data = item.media.data; - const auto origin = Data::FileOrigin(); - if (const auto video = std::get_if>(&data)) { - _loader = std::make_unique((*video), origin, [=] { - check(); - }); - } else if (const auto photo = std::get_if>(&data)) { - _loader = std::make_unique((*photo), origin, [=] { - check(); - }); - } else { - Unexpected("Media type in stories list."); - } - _blurred = _loader->blurred(); - check(); + checkStory(); _goodShown.stop(); } Sibling::~Sibling() = default; +void Sibling::checkStory() { + const auto maybeStory = _peer->owner().stories().lookup(_id); + if (!maybeStory) { + if (_blurred.isNull()) { + _blurred = QImage( + st::storiesMaxSize, + QImage::Format_ARGB32_Premultiplied); + _blurred.fill(Qt::black); + + if (maybeStory.error() == Data::NoStory::Unknown) { + _peer->owner().stories().resolve(_id, crl::guard(this, [=] { + checkStory(); + })); + } + } + return; + } + const auto story = *maybeStory; + const auto &data = story->media().data; + const auto origin = Data::FileOrigin(); + v::match(story->media().data, [&](not_null photo) { + _loader = std::make_unique(photo, origin, [=] { + check(); + }); + }, [&](not_null document) { + _loader = std::make_unique(document, origin, [=] { + check(); + }); + }); + _blurred = _loader->blurred(); + check(); +} + FullStoryId Sibling::shownId() const { return _id; } @@ -249,9 +267,9 @@ not_null Sibling::peer() const { } bool Sibling::shows(const Data::StoriesList &list) const { - Expects(!list.items.empty()); + Expects(!list.ids.empty()); - return _id == FullStoryId{ list.user->id, list.items.front().id }; + return _id == FullStoryId{ list.user->id, list.ids.front() }; } SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h index dc355fec6..f6eb4edc7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.h +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h @@ -7,8 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/weak_ptr.h" #include "data/data_stories.h" - #include "ui/effects/animations.h" #include "ui/userpic_view.h" @@ -22,7 +22,7 @@ class Controller; struct SiblingView; struct SiblingLayout; -class Sibling final { +class Sibling final : public base::has_weak_ptr { public: Sibling( not_null controller, @@ -42,6 +42,7 @@ private: class LoaderPhoto; class LoaderVideo; + void checkStory(); void check(); [[nodiscard]] QImage userpicImage(const SiblingLayout &layout); diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index ad9101976..79b835461 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -14,6 +14,10 @@ class PeerData; class PhotoData; class HistoryItem; +namespace Data { +class Story; +} // namespace Data + namespace Window { class SessionController; } // namespace Window @@ -67,6 +71,17 @@ public: , _cloudTheme(cloudTheme) { } + OpenRequest( + Window::SessionController *controller, + not_null story, + bool continueStreaming = false, + crl::time startTime = 0) + : _controller(controller) + , _story(story) + , _continueStreaming(continueStreaming) + , _startTime(startTime) { + } + [[nodiscard]] PeerData *peer() const { return _peer; } @@ -87,6 +102,10 @@ public: return _document; } + [[nodiscard]] Data::Story *story() const { + return _story; + } + [[nodiscard]] std::optional cloudTheme() const { return _cloudTheme; } @@ -107,6 +126,7 @@ private: Window::SessionController *_controller = nullptr; DocumentData *_document = nullptr; PhotoData *_photo = nullptr; + Data::Story *_story = nullptr; PeerData *_peer = nullptr; HistoryItem *_item = nullptr; MsgId _topicRootId = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 1bd915558..c80d42841 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -3036,8 +3036,9 @@ void OverlayWidget::activate() { } void OverlayWidget::show(OpenRequest request) { - const auto document = request.document(); - const auto photo = request.photo(); + const auto story = request.story(); + const auto document = story ? story->document() : request.document(); + const auto photo = story ? story->photo() : request.photo(); const auto contextItem = request.item(); const auto contextPeer = request.peer(); const auto contextTopicRootId = request.topicRootId(); @@ -3057,15 +3058,9 @@ void OverlayWidget::show(OpenRequest request) { } setSession(&photo->session()); - // #TODO stories testing - if (const auto storyId = (!contextPeer && contextItem) - ? contextItem->history()->owner().stories().generate( - contextItem, - photo) - : StoryId()) { - setContext(StoriesContext{ contextItem->from()->asUser(), storyId }); - } else - if (contextPeer) { + if (story) { + setContext(StoriesContext{ story->peer(), story->id() }); + } else if (contextPeer) { setContext(contextPeer); } else if (contextItem) { setContext(ItemContext{ contextItem, contextTopicRootId }); @@ -3083,15 +3078,9 @@ void OverlayWidget::show(OpenRequest request) { } else if (document) { setSession(&document->session()); - // #TODO stories testing - if (const auto storyId = contextItem - ? contextItem->history()->owner().stories().generate( - contextItem, - document) - : StoryId()) { - setContext(StoriesContext{ contextItem->from()->asUser(), storyId }); - } else - if (contextItem) { + if (story) { + setContext(StoriesContext{ story->peer(), story->id() }); + } else if (contextItem) { setContext(ItemContext{ contextItem, contextTopicRootId }); } else { setContext(v::null); @@ -4034,30 +4023,20 @@ void OverlayWidget::storiesJumpTo( Expects(_stories != nullptr); Expects(id.valid()); - const auto &all = session->data().stories().all(); - const auto i = ranges::find( - all, - id.peer, - [](const Data::StoriesList &list) { return list.user->id; }); - if (i == end(all)) { + const auto maybeStory = session->data().stories().lookup(id); + if (!maybeStory) { close(); return; } - const auto j = ranges::find(i->items, id.story, &Data::StoryItem::id); - if (j == end(i->items)) { - close(); - return; - } - setContext(StoriesContext{ i->user, id.story }); + const auto story = *maybeStory; + setContext(StoriesContext{ story->peer(), story->id() }); clearStreaming(); _streamingStartPaused = false; - const auto &data = j->media.data; - const auto activation = anim::activation::background; - if (const auto photo = std::get_if>(&data)) { - displayPhoto(*photo, activation); - } else { - displayDocument(v::get>(data), activation); - } + v::match(story->media().data, [&](not_null photo) { + displayPhoto(photo, anim::activation::background); + }, [&](not_null document) { + displayDocument(document, anim::activation::background); + }); } void OverlayWidget::storiesClose() { @@ -4976,36 +4955,33 @@ void OverlayWidget::setContext( _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); - setStoriesUser(nullptr); + setStoriesPeer(nullptr); } else if (const auto peer = std::get_if>(&context)) { _peer = *peer; _history = _peer->owner().history(_peer); _message = nullptr; _topicRootId = MsgId(); - setStoriesUser(nullptr); + setStoriesPeer(nullptr); } else if (const auto story = std::get_if(&context)) { _message = nullptr; _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; - const auto &all = story->user->owner().stories().all(); + const auto &all = story->peer->owner().stories().all(); const auto i = ranges::find( all, - story->user, + story->peer, &Data::StoriesList::user); Assert(i != end(all)); - const auto j = ranges::find( - i->items, - story->id, - &Data::StoryItem::id); - setStoriesUser(story->user); - _stories->show(all, (i - begin(all)), j - begin(i->items)); + const auto j = ranges::find(i->ids, story->id); + setStoriesPeer(story->peer); + _stories->show(all, (i - begin(all)), j - begin(i->ids)); } else { _message = nullptr; _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; - setStoriesUser(nullptr); + setStoriesPeer(nullptr); } _migrated = nullptr; if (_history) { @@ -5020,11 +4996,11 @@ void OverlayWidget::setContext( _user = _peer ? _peer->asUser() : nullptr; } -void OverlayWidget::setStoriesUser(UserData *user) { - const auto session = user ? &user->session() : nullptr; +void OverlayWidget::setStoriesPeer(PeerData *peer) { + const auto session = peer ? &peer->session() : nullptr; if (!session && !_storiesSession) { Assert(!_stories); - } else if (!user) { + } else if (!peer) { _stories = nullptr; _storiesSession = nullptr; _storiesChanged.fire({}); @@ -5099,14 +5075,6 @@ bool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) { if (v::is_null(entity.data) && !entity.item) { return false; } - // #TODO stories testing - if (const auto storyId = entity.item - ? entity.item->history()->owner().stories().generate( - entity.item, - entity.data) - : StoryId()) { - setContext(StoriesContext{ entity.item->from()->asUser(), storyId }); - } else if (const auto item = entity.item) { setContext(ItemContext{ item, entity.topicRootId }); } else if (_peer) { @@ -5765,7 +5733,7 @@ void OverlayWidget::clearBeforeHide() { _collage = nullptr; _collageData = std::nullopt; clearStreaming(); - setStoriesUser(nullptr); + setStoriesPeer(nullptr); _layerBg->hideAll(anim::type::instant); assignMediaPointer(nullptr); _preloadPhotos.clear(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index a82aa1f3c..2bc572a91 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -303,7 +303,7 @@ private: MsgId topicRootId = 0; }; struct StoriesContext { - not_null user; + not_null peer; StoryId id = 0; }; void setContext(std::variant< @@ -311,7 +311,7 @@ private: ItemContext, not_null, StoriesContext> context); - void setStoriesUser(UserData *user); + void setStoriesPeer(PeerData *peer); void refreshLang(); void showSaveMsgFile(); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 34d9ca1be..7ca01634e 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_replies_list.h" #include "data/data_peer_values.h" +#include "data/data_stories.h" #include "passport/passport_form_controller.h" #include "chat_helpers/tabbed_selector.h" #include "chat_helpers/emoji_interactions.h" @@ -2463,6 +2464,22 @@ Ui::ChatThemeBackgroundData SessionController::backgroundData( }; } +void SessionController::openPeerStories(PeerId peerId) { + using namespace Media::View; + using namespace Data; + + auto &stories = session().data().stories(); + const auto &all = stories.all(); + const auto i = ranges::find(all, peerId, [](const StoriesList &list) { + return list.user->id; + }); + if (i != end(all) && !i->ids.empty()) { + if (const auto from = stories.lookup({ peerId, i->ids.front() })) { + window().openInMediaView(OpenRequest(this, *from)); + } + } +} + HistoryView::PaintContext SessionController::preparePaintContext( PaintContextArgs &&args) { const auto visibleAreaTopLocal = content()->mapFromGlobal( diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 5258e7b8e..8f6ed90ec 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -564,6 +564,8 @@ public: return _peerThemeOverride.value(); } + void openPeerStories(PeerId peerId); + struct PaintContextArgs { not_null theme; int visibleAreaTop = 0; From b195ec4fd5832a6bf57aef33915c78ff0da52d51 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 May 2023 12:45:50 +0400 Subject: [PATCH 034/260] Support stories file reference refreshing. --- Telegram/SourceFiles/apiwrap.cpp | 9 +++++ .../SourceFiles/data/data_file_origin.cpp | 36 ++++++++++--------- Telegram/SourceFiles/data/data_file_origin.h | 20 ++++++++++- .../stories/media_stories_controller.cpp | 5 +++ .../media/stories/media_stories_controller.h | 2 ++ .../media/stories/media_stories_view.cpp | 5 +++ .../media/stories/media_stories_view.h | 2 ++ .../media/view/media_view_overlay_widget.cpp | 10 +++--- 8 files changed, 68 insertions(+), 21 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 304c609d4..7840bdcfa 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2509,6 +2509,15 @@ void ApiWrap::refreshFileReference( request(MTPaccount_GetSavedRingtones(MTP_long(0))); }, [&](Data::FileOriginPremiumPreviews data) { request(MTPhelp_GetPremiumPromo()); + }, [&](Data::FileOriginStory data) { + const auto user = _session->data().peer(data.peerId)->asUser(); + if (user) { + request(MTPstories_GetStoriesByID( + user->inputUser, + MTP_vector(1, MTP_int(data.storyId)))); + } else { + fail(); + } }, [&](v::null_t) { fail(); }); diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index 92750b49f..ead9b1729 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -40,10 +40,8 @@ struct FileReferenceAccumulator { }); } void push(const MTPPage &data) { - data.match([&](const auto &data) { - push(data.vphotos()); - push(data.vdocuments()); - }); + push(data.data().vphotos()); + push(data.data().vdocuments()); } void push(const MTPWallPaper &data) { data.match([&](const MTPDwallPaper &data) { @@ -52,14 +50,10 @@ struct FileReferenceAccumulator { }); } void push(const MTPTheme &data) { - data.match([&](const MTPDtheme &data) { - push(data.vdocument()); - }); + push(data.data().vdocument()); } void push(const MTPWebPageAttribute &data) { - data.match([&](const MTPDwebPageAttributeTheme &data) { - push(data.vdocuments()); - }); + push(data.data().vdocuments()); } void push(const MTPWebPage &data) { data.match([&](const MTPDwebPage &data) { @@ -104,6 +98,13 @@ struct FileReferenceAccumulator { }, [](const MTPDmessageEmpty &data) { }); } + void push(const MTPStoryItem &data) { + data.match([&](const MTPDstoryItem &data) { + push(data.vmedia()); + }, [](const MTPDstoryItemDeleted &) { + }, [](const MTPDstoryItemSkipped &) { + }); + } void push(const MTPmessages_Messages &data) { data.match([](const MTPDmessages_messagesNotModified &) { }, [&](const auto &data) { @@ -116,9 +117,7 @@ struct FileReferenceAccumulator { }); } void push(const MTPusers_UserFull &data) { - data.match([&](const auto &data) { - push(data.vfull_user().data().vpersonal_photo()); - }); + push(data.data().vfull_user().data().vpersonal_photo()); } void push(const MTPmessages_RecentStickers &data) { data.match([&](const MTPDmessages_recentStickers &data) { @@ -151,9 +150,10 @@ struct FileReferenceAccumulator { }); } void push(const MTPhelp_PremiumPromo &data) { - data.match([&](const MTPDhelp_premiumPromo &data) { - push(data.vvideos()); - }); + push(data.data().vvideos()); + } + void push(const MTPstories_Stories &data) { + push(data.data().vstories()); } UpdatedFileReferences result; @@ -216,6 +216,10 @@ UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data) { return GetFileReferencesHelper(data); } +UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data) { + return GetFileReferencesHelper(data); +} + UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data) { return GetFileReferencesHelper(data); } diff --git a/Telegram/SourceFiles/data/data_file_origin.h b/Telegram/SourceFiles/data/data_file_origin.h index 195ae9188..b3185d2df 100644 --- a/Telegram/SourceFiles/data/data_file_origin.h +++ b/Telegram/SourceFiles/data/data_file_origin.h @@ -120,6 +120,20 @@ struct FileOriginPremiumPreviews { } }; +struct FileOriginStory { + FileOriginStory(PeerId peerId, StoryId storyId) + : peerId(peerId) + , storyId(storyId) { + } + + PeerId peerId = 0; + StoryId storyId = 0; + + friend inline auto operator<=>( + FileOriginStory, + FileOriginStory) = default; +}; + struct FileOrigin { using Variant = std::variant< v::null_t, @@ -132,7 +146,8 @@ struct FileOrigin { FileOriginWallpaper, FileOriginTheme, FileOriginRingtones, - FileOriginPremiumPreviews>; + FileOriginPremiumPreviews, + FileOriginStory>; FileOrigin() = default; FileOrigin(FileOriginMessage data) : data(data) { @@ -155,6 +170,8 @@ struct FileOrigin { } FileOrigin(FileOriginPremiumPreviews data) : data(data) { } + FileOrigin(FileOriginStory data) : data(data) { + } explicit operator bool() const { return !v::is_null(data); @@ -204,6 +221,7 @@ UpdatedFileReferences GetFileReferences(const MTPTheme &data); UpdatedFileReferences GetFileReferences( const MTPaccount_SavedRingtones &data); UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data); +UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data); // Admin Log Event. UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index f68680c43..02b8282d4 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/power_save_blocker.h" #include "chat_helpers/compose/compose_show.h" +#include "data/data_file_origin.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -317,6 +318,10 @@ ContentLayout Controller::contentLayout() const { }; } +Data::FileOrigin Controller::fileOrigin() const { + return Data::FileOriginStory(_shown.peer, _shown.story); +} + TextWithEntities Controller::captionText() const { return _captionText; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index f553f895e..ebc4a0d47 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -21,6 +21,7 @@ struct FileChosen; namespace Data { struct StoriesList; +struct FileOrigin; } // namespace Data namespace Ui { @@ -80,6 +81,7 @@ public: [[nodiscard]] Layout layout() const; [[nodiscard]] rpl::producer layoutValue() const; [[nodiscard]] ContentLayout contentLayout() const; + [[nodiscard]] Data::FileOrigin fileOrigin() const; [[nodiscard]] TextWithEntities captionText() const; void showFullCaption(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 82c4388d2..18a4bb499 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_view.h" +#include "data/data_file_origin.h" #include "media/stories/media_stories_controller.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" @@ -79,6 +80,10 @@ SiblingView View::sibling(SiblingType type) const { return _controller->sibling(type); } +Data::FileOrigin View::fileOrigin() const { + return _controller->fileOrigin(); +} + TextWithEntities View::captionText() const { return _controller->captionText(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index f36b65cf1..d01bd3d61 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { struct StoriesList; +struct FileOrigin; } // namespace Data namespace Media::Player { @@ -62,6 +63,7 @@ public: [[nodiscard]] rpl::producer finalShownGeometryValue() const; [[nodiscard]] ContentLayout contentLayout() const; [[nodiscard]] SiblingView sibling(SiblingType type) const; + [[nodiscard]] Data::FileOrigin fileOrigin() const; [[nodiscard]] TextWithEntities captionText() const; void showFullCaption(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index c80d42841..f2efd4578 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -2584,7 +2584,9 @@ auto OverlayWidget::sharedMediaKey() const -> std::optional { } Data::FileOrigin OverlayWidget::fileOrigin() const { - if (_message) { + if (_stories) { + return _stories->fileOrigin(); + } else if (_message) { return _message->fullId(); } else if (_photo && _user) { return Data::FileOriginUserPhoto(peerToUser(_user->id), _photo->id); @@ -2828,15 +2830,15 @@ void OverlayWidget::refreshFromLabel() { void OverlayWidget::refreshCaption() { _caption = Ui::Text::String(); const auto caption = [&] { - if (_message) { + if (_stories) { + return _stories->captionText(); + } else if (_message) { if (const auto media = _message->media()) { if (media->webpage()) { return TextWithEntities(); } } return _message->translatedText(); - } else if (_stories) { - return _stories->captionText(); } return TextWithEntities(); }(); From 2e6790c45c44ed0372946cd866fbdea790a06bcd Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 May 2023 13:27:34 +0400 Subject: [PATCH 035/260] Support replies to stories layout in messages. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/data/data_changes.cpp | 33 +++ Telegram/SourceFiles/data/data_changes.h | 34 +++ Telegram/SourceFiles/data/data_stories.cpp | 219 +++++++++++++++++- Telegram/SourceFiles/data/data_stories.h | 32 ++- Telegram/SourceFiles/history/history_item.cpp | 21 +- .../history/history_item_components.cpp | 88 +++++-- .../history/history_item_components.h | 42 ++++ .../history/history_item_helpers.cpp | 38 ++- .../history/history_item_helpers.h | 12 +- .../history/view/history_view_message.cpp | 3 +- .../stories/media_stories_controller.cpp | 14 ++ .../media/stories/media_stories_controller.h | 7 + .../window/window_session_controller.cpp | 16 +- .../window/window_session_controller.h | 1 + 15 files changed, 522 insertions(+), 39 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 54b768de8..1b1d41d18 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -285,6 +285,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_message_text" = "New message text..."; "lng_deleted" = "Deleted Account"; "lng_deleted_message" = "Deleted message"; +"lng_deleted_story" = "Deleted story"; "lng_pinned_message" = "Pinned message"; "lng_pinned_previous" = "Previous message"; "lng_pinned_unpin_sure" = "Would you like to unpin this message?"; diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index 9f28a7af0..f20041565 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -272,6 +272,38 @@ void Changes::entryRemoved(not_null entry) { _entryChanges.drop(entry); } +void Changes::storyUpdated( + not_null story, + StoryUpdate::Flags flags) { + const auto drop = (flags & StoryUpdate::Flag::Destroyed); + _storyChanges.updated(story, flags, drop); + if (!drop) { + scheduleNotifications(); + } +} + +rpl::producer Changes::storyUpdates( + StoryUpdate::Flags flags) const { + return _storyChanges.updates(flags); +} + +rpl::producer Changes::storyUpdates( + not_null story, + StoryUpdate::Flags flags) const { + return _storyChanges.updates(story, flags); +} + +rpl::producer Changes::storyFlagsValue( + not_null story, + StoryUpdate::Flags flags) const { + return _storyChanges.flagsValue(story, flags); +} + +rpl::producer Changes::realtimeStoryUpdates( + StoryUpdate::Flag flag) const { + return _storyChanges.realtimeUpdates(flag); +} + void Changes::scheduleNotifications() { if (!_notify) { _notify = true; @@ -291,6 +323,7 @@ void Changes::sendNotifications() { _messageChanges.sendNotifications(); _entryChanges.sendNotifications(); _topicChanges.sendNotifications(); + _storyChanges.sendNotifications(); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index e5bbc48e4..ee8ff6075 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -38,6 +38,7 @@ inline constexpr int CountBit(Flag Last = Flag::LastUsedBit) { namespace Data { class ForumTopic; +class Story; struct NameUpdate { NameUpdate( @@ -215,6 +216,24 @@ struct EntryUpdate { }; +struct StoryUpdate { + enum class Flag : uint32 { + None = 0, + + Edited = (1U << 0), + Destroyed = (1U << 1), + NewAdded = (1U << 2), + + LastUsedBit = (1U << 2), + }; + using Flags = base::flags; + friend inline constexpr auto is_flag_type(Flag) { return true; } + + not_null story; + Flags flags = 0; + +}; + class Changes final { public: explicit Changes(not_null session); @@ -298,6 +317,20 @@ public: EntryUpdate::Flag flag) const; void entryRemoved(not_null entry); + void storyUpdated( + not_null story, + StoryUpdate::Flags flags); + [[nodiscard]] rpl::producer storyUpdates( + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer storyUpdates( + not_null story, + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer storyFlagsValue( + not_null story, + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer realtimeStoryUpdates( + StoryUpdate::Flag flag) const; + void sendNotifications(); private: @@ -348,6 +381,7 @@ private: Manager _topicChanges; Manager _messageChanges; Manager _entryChanges; + Manager _storyChanges; bool _notify = false; diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index dbddd0da0..2ee3be031 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -8,11 +8,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_stories.h" #include "api/api_text_entities.h" +#include "apiwrap.h" +#include "data/data_changes.h" #include "data/data_document.h" +#include "data/data_file_origin.h" #include "data/data_photo.h" #include "data/data_session.h" +#include "lang/lang_keys.h" #include "main/main_session.h" -#include "apiwrap.h" +#include "ui/text/text_utilities.h" // #TODO stories testing #include "data/data_user.h" @@ -23,6 +27,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { namespace { +constexpr auto kMaxResolveTogether = 100; + +using UpdateFlag = StoryUpdate::Flag; + } // namespace bool StoriesList::unread() const { @@ -56,6 +64,10 @@ StoryId Story::id() const { return _id; } +FullStoryId Story::fullId() const { + return { _peer->id, _id }; +} + TimeId Story::date() const { return _date; } @@ -74,6 +86,45 @@ DocumentData *Story::document() const { return result ? result->get() : nullptr; } +bool Story::hasReplyPreview() const { + return v::match(_media.data, [](not_null photo) { + return !photo->isNull(); + }, [](not_null document) { + return document->hasThumbnail(); + }); +} + +Image *Story::replyPreview() const { + return v::match(_media.data, [&](not_null photo) { + return photo->getReplyPreview( + Data::FileOriginStory(_peer->id, _id), + _peer, + false); + }, [&](not_null document) { + return document->getReplyPreview( + Data::FileOriginStory(_peer->id, _id), + _peer, + false); + }); +} + +TextWithEntities Story::inReplyText() const { + const auto type = u"Story"_q; + return _caption.text.isEmpty() + ? Ui::Text::PlainLink(type) + : tr::lng_dialogs_text_media( + tr::now, + lt_media_part, + tr::lng_dialogs_text_media_wrapped( + tr::now, + lt_media, + Ui::Text::PlainLink(type), + Ui::Text::WithEntities), + lt_caption, + _caption, + Ui::Text::WithEntities); +} + void Story::setPinned(bool pinned) { _pinned = pinned; } @@ -110,6 +161,10 @@ Session &Stories::owner() const { return *_owner; } +Main::Session &Stories::session() const { + return _owner->session(); +} + void Stories::apply(const MTPDupdateStories &data) { pushToFront(parse(data.vstories())); _allChanged.fire({}); @@ -137,10 +192,7 @@ StoriesList Stories::parse(const MTPUserStories &stories) { }, [&](const MTPDstoryItemSkipped &data) { result.ids.push_back(data.vid().v); }, [&](const MTPDstoryItemDeleted &data) { - _deleted.emplace(FullStoryId{ - .peer = peerFromUser(userId), - .story = data.vid().v, - }); + applyDeleted({ peerFromUser(userId), data.vid().v }); --result.total; }); } @@ -189,6 +241,35 @@ Story *Stories::parse(not_null peer, const MTPDstoryItem &data) { return result; } +void Stories::updateDependentMessages(not_null story) { + const auto i = _dependentMessages.find(story); + if (i != end(_dependentMessages)) { + for (const auto &dependent : i->second) { + dependent->updateDependencyItem(); + } + } + session().changes().storyUpdated( + story, + Data::StoryUpdate::Flag::Edited); +} + +void Stories::registerDependentMessage( + not_null dependent, + not_null dependency) { + _dependentMessages[dependency].emplace(dependent); +} + +void Stories::unregisterDependentMessage( + not_null dependent, + not_null dependency) { + const auto i = _dependentMessages.find(dependency); + if (i != end(_dependentMessages)) { + if (i->second.remove(dependent) && i->second.empty()) { + _dependentMessages.erase(i); + } + } +} + void Stories::loadMore() { if (_loadMoreRequestId || _allLoaded) { return; @@ -216,6 +297,119 @@ void Stories::loadMore() { }).send(); } +void Stories::sendResolveRequests() { + if (!_resolveRequests.empty()) { + return; + } + struct Prepared { + QVector ids; + std::vector> callbacks; + }; + auto leftToSend = kMaxResolveTogether; + auto byPeer = base::flat_map(); + for (auto i = begin(_resolves); i != end(_resolves);) { + auto &[peerId, ids] = *i; + auto &prepared = byPeer[peerId]; + for (auto &[storyId, callbacks] : ids) { + prepared.ids.push_back(MTP_int(storyId)); + prepared.callbacks.insert( + end(prepared.callbacks), + std::make_move_iterator(begin(callbacks)), + std::make_move_iterator(end(callbacks))); + if (!--leftToSend) { + break; + } + } + const auto sending = int(prepared.ids.size()); + if (sending == ids.size()) { + i = _resolves.erase(i); + if (!leftToSend) { + break; + } + } else { + ids.erase(begin(ids), begin(ids) + sending); + break; + } + } + const auto api = &_owner->session().api(); + for (auto &entry : byPeer) { + const auto peerId = entry.first; + auto &prepared = entry.second; + const auto finish = [=, ids = prepared.ids](mtpRequestId id) { + for (const auto &id : ids) { + finalizeResolve({ peerId, id.v }); + } + if (auto callbacks = _resolveRequests.take(id)) { + for (const auto &callback : *callbacks) { + callback(); + } + } + if (_resolveRequests.empty() && !_resolves.empty()) { + crl::on_main(&session(), [=] { sendResolveRequests(); }); + } + }; + const auto user = _owner->session().data().peer(peerId)->asUser(); + if (!user) { + _resolveRequests[0] = std::move(prepared.callbacks); + finish(0); + continue; + } + const auto requestId = api->request(MTPstories_GetStoriesByID( + user->inputUser, + MTP_vector(std::move(prepared.ids)) + )).done([=](const MTPstories_Stories &result, mtpRequestId id) { + owner().processUsers(result.data().vusers()); + processResolvedStories(user, result.data().vstories().v); + finish(id); + }).fail([=](const MTP::Error &error, mtpRequestId id) { + finish(id); + }).send(); + _resolveRequests.emplace(requestId, std::move(prepared.callbacks)); + } +} + +void Stories::processResolvedStories( + not_null peer, + const QVector &list) { + for (const auto &item : list) { + item.match([&](const MTPDstoryItem &data) { + [[maybe_unused]] const auto story = parse(peer, data); + }, [&](const MTPDstoryItemSkipped &data) { + LOG(("API Error: Unexpected storyItemSkipped in resolve.")); + }, [&](const MTPDstoryItemDeleted &data) { + applyDeleted({ peer->id, data.vid().v }); + }); + } +} + +void Stories::finalizeResolve(FullStoryId id) { + const auto already = lookup(id); + if (!already.has_value() && already.error() == NoStory::Unknown) { + LOG(("API Error: Could not resolve story %1_%2" + ).arg(id.peer.value + ).arg(id.story)); + applyDeleted(id); + } +} + +void Stories::applyDeleted(FullStoryId id) { + const auto i = _stories.find(id.peer); + if (i != end(_stories)) { + const auto j = i->second.find(id.story); + if (j != end(i->second)) { + auto story = std::move(j->second); + i->second.erase(j); + session().changes().storyUpdated( + story.get(), + UpdateFlag::Destroyed); + if (i->second.empty()) { + _stories.erase(i); + } + } + } + _deleted.emplace(id); +} + const std::vector &Stories::all() { return _all; } @@ -242,6 +436,21 @@ base::expected, NoStory> Stories::lookup( } void Stories::resolve(FullStoryId id, Fn done) { + const auto already = lookup(id); + if (already.has_value() || already.error() != NoStory::Unknown) { + done(); + return; + } + auto &ids = _resolves[id.peer]; + if (ids.empty()) { + crl::on_main(&session(), [=] { + sendResolveRequests(); + }); + } + auto &callbacks = ids[id.story]; + if (done) { + callbacks.push_back(std::move(done)); + } } void Stories::pushToBack(StoriesList &&list) { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index e5f3b49db..641e26edf 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/expected.h" +class Image; class PhotoData; class DocumentData; @@ -39,11 +40,16 @@ public: [[nodiscard]] not_null peer() const; [[nodiscard]] StoryId id() const; + [[nodiscard]] FullStoryId fullId() const; [[nodiscard]] TimeId date() const; [[nodiscard]] const StoryMedia &media() const; [[nodiscard]] PhotoData *photo() const; [[nodiscard]] DocumentData *document() const; + [[nodiscard]] bool hasReplyPreview() const; + [[nodiscard]] Image *replyPreview() const; + [[nodiscard]] TextWithEntities inReplyText() const; + void setPinned(bool pinned); [[nodiscard]] bool pinned() const; @@ -55,7 +61,7 @@ public: private: const StoryId _id = 0; const not_null _peer; - const StoryMedia _media; + StoryMedia _media; TextWithEntities _caption; const TimeId _date = 0; bool _pinned = false; @@ -84,6 +90,15 @@ public: ~Stories(); [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + + void updateDependentMessages(not_null story); + void registerDependentMessage( + not_null dependent, + not_null dependency); + void unregisterDependentMessage( + not_null dependent, + not_null dependency); void loadMore(); void apply(const MTPDupdateStories &data); @@ -101,9 +116,15 @@ private: [[nodiscard]] Story *parse( not_null peer, const MTPDstoryItem &data); + void processResolvedStories( + not_null peer, + const QVector &list); + void sendResolveRequests(); + void finalizeResolve(FullStoryId id); void pushToBack(StoriesList &&list); void pushToFront(StoriesList &&list); + void applyDeleted(FullStoryId id); const not_null _owner; base::flat_map< @@ -111,6 +132,15 @@ private: base::flat_map>> _stories; base::flat_set _deleted; + base::flat_map< + PeerId, + base::flat_map>>> _resolves; + base::flat_map>> _resolveRequests; + + std::map< + not_null, + base::flat_set>> _dependentMessages; + std::vector _all; rpl::event_stream<> _allChanged; QString _state; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index cab562735..c0e60d38b 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2544,7 +2544,7 @@ void HistoryItem::setReplyFields( && !IsServerMsgId(reply->replyToMsgId)) { reply->replyToMsgId = replyTo; if (!reply->updateData(this)) { - RequestDependentMessageData( + RequestDependentMessageItem( this, reply->replyToPeerId, reply->replyToMsgId); @@ -2966,12 +2966,21 @@ void HistoryItem::createComponents(CreateConfig &&config) { reply->replyToPeerId = config.replyToPeer; reply->replyToMsgId = config.replyTo; reply->replyToMsgTop = isScheduled() ? 0 : config.replyToTop; + reply->replyToStoryId = config.replyToStory; + reply->storyReply = (config.replyToStory != 0); reply->topicPost = config.replyIsTopicPost; if (!reply->updateData(this)) { - RequestDependentMessageData( - this, - reply->replyToPeerId, - reply->replyToMsgId); + if (reply->replyToMsgId) { + RequestDependentMessageItem( + this, + reply->replyToPeerId, + reply->replyToMsgId); + } else if (reply->replyToStoryId) { + RequestDependentMessageStory( + this, + reply->replyToPeerId, + reply->replyToStoryId); + } } } if (const auto via = Get()) { @@ -3518,7 +3527,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { dependent->topicPost = data.is_forum_topic() || Has(); if (!updateServiceDependent()) { - RequestDependentMessageData( + RequestDependentMessageItem( this, (dependent->peerId ? dependent->peerId diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 0cecbe2fe..4b5284555 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_web_page.h" #include "data/data_file_click_handler.h" +#include "data/data_stories.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" @@ -257,15 +258,17 @@ bool HistoryMessageReply::updateData( bool force) { const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { - if (replyToMsg || !replyToMsgId) { + if ((replyToMsg || !replyToMsgId) + && (replyToStory || !replyToStoryId)) { return true; } } - if (!replyToMsg) { + const auto peerId = replyToPeerId + ? replyToPeerId + : holder->history()->peer->id; + if (!replyToMsg && replyToMsgId) { replyToMsg = holder->history()->owner().message( - (replyToPeerId - ? replyToPeerId - : holder->history()->peer->id), + peerId, replyToMsgId); if (replyToMsg) { if (replyToMsg->isEmpty()) { @@ -279,8 +282,22 @@ bool HistoryMessageReply::updateData( } } } + if (!replyToStory && replyToStoryId) { + const auto maybe = holder->history()->owner().stories().lookup({ + peerId, + replyToStoryId, + }); + if (maybe) { + replyToStory = *maybe; + holder->history()->owner().stories().registerDependentMessage( + holder, + replyToStory.get()); + } else if (maybe.error() == Data::NoStory::Deleted) { + force = true; + } + } - if (replyToMsg) { + if (replyToMsg || replyToStory) { const auto repaint = [=] { holder->customEmojiRepaint(); }; const auto context = Core::MarkedTextContext{ .session = &holder->history()->session(), @@ -288,14 +305,16 @@ bool HistoryMessageReply::updateData( }; replyToText.setMarkedText( st::messageTextStyle, - replyToMsg->inReplyText(), + (replyToMsg + ? replyToMsg->inReplyText() + : replyToStory->inReplyText()), Ui::DialogTextOptions(), context); updateName(holder); setReplyToLinkFrom(holder); - if (!replyToMsg->Has()) { + if (replyToMsg && !replyToMsg->Has()) { if (auto bot = replyToMsg->viaBot()) { replyToVia = std::make_unique(); replyToVia->create( @@ -304,15 +323,17 @@ bool HistoryMessageReply::updateData( } } - { + if (replyToMsg) { const auto peer = replyToMsg->history()->peer; replyToColorKey = (!holder->out() && (peer->isMegagroup() || peer->isChat())) ? replyToMsg->from()->id : PeerId(0); + } else { + replyToColorKey = PeerId(0); } - const auto media = replyToMsg->media(); + const auto media = replyToMsg ? replyToMsg->media() : nullptr; if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { spoiler = nullptr; } else if (!spoiler) { @@ -320,19 +341,23 @@ bool HistoryMessageReply::updateData( } } else if (force) { replyToMsgId = 0; + replyToStoryId = 0; replyToColorKey = PeerId(0); spoiler = nullptr; } if (force) { holder->history()->owner().requestItemResize(holder); } - return (replyToMsg || !replyToMsgId); + return (replyToMsg || !replyToMsgId) + && (replyToStory || !replyToStoryId); } void HistoryMessageReply::setReplyToLinkFrom( not_null holder) { replyToLnk = replyToMsg ? JumpToMessageClickHandler(replyToMsg.get(), holder->fullId()) + : replyToStory + ? JumpToStoryClickHandler(replyToStory.get()) : nullptr; } @@ -365,7 +390,9 @@ PeerData *HistoryMessageReply::replyToFrom( QString HistoryMessageReply::replyToFromName( not_null holder) const { - if (!replyToMsg) { + if (replyToStory) { + return replyToFromName(replyToStory->peer()); + } else if (!replyToMsg) { return QString(); } else if (holder->Has()) { if (const auto fwd = replyToMsg->Get()) { @@ -405,10 +432,15 @@ void HistoryMessageReply::updateName( replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); if (const auto from = replyToFrom(holder)) { replyToVersion = from->nameVersion(); - } else { + } else if (replyToMsg) { replyToVersion = replyToMsg->author()->nameVersion(); + } else { + replyToVersion = replyToStory->peer()->nameVersion(); } - bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; + bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) + || (replyToMsg + && replyToMsg->media() + && replyToMsg->media()->hasReplyPreview()); int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; int32 w = replyToName.maxWidth(); if (replyToVia) { @@ -417,14 +449,17 @@ void HistoryMessageReply::updateName( maxReplyWidth = previewSkip + qMax(w, qMin(replyToText.maxWidth(), int32(st::maxSignatureSize))); } else { - maxReplyWidth = st::msgDateFont->width(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now)); + maxReplyWidth = st::msgDateFont->width(statePhrase()); } maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + maxReplyWidth + st::msgReplyPadding.right(); } void HistoryMessageReply::resize(int width) const { if (replyToVia) { - bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; + bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) + || (replyToMsg + && replyToMsg->media() + && replyToMsg->media()->hasReplyPreview()); int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; replyToVia->resize(width - st::msgReplyBarSkip - previewSkip - replyToName.maxWidth() - st::msgServiceFont->spacew); } @@ -471,16 +506,19 @@ void HistoryMessageReply::paint( const auto pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); if (w > st::msgReplyBarSkip) { - if (replyToMsg) { - const auto media = replyToMsg->media(); - auto hasPreview = media && media->hasReplyPreview(); + if (replyToMsg || replyToStory) { + const auto media = replyToMsg ? replyToMsg->media() : nullptr; + auto hasPreview = (replyToStory && replyToStory->hasReplyPreview()) || (media && media->hasReplyPreview()); if (hasPreview && w < st::msgReplyBarSkip + st::msgReplyBarSize.height()) { hasPreview = false; } auto previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; if (hasPreview) { - if (const auto image = media->replyPreview()) { + const auto image = media + ? media->replyPreview() + : replyToStory->replyPreview(); + if (image) { auto to = style::rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), w + 2 * x); const auto preview = image->pixSingle( image->size() / style::DevicePixelRatio(), @@ -542,11 +580,19 @@ void HistoryMessageReply::paint( p.setPen(inBubble ? stm->msgDateFg : st->msgDateImgFg()); - p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now), w - st::msgReplyBarSkip)); + p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(statePhrase(), w - st::msgReplyBarSkip)); } } } +QString HistoryMessageReply::statePhrase() const { + return (replyToMsgId || replyToStoryId) + ? tr::lng_profile_loading(tr::now) + : storyReply + ? tr::lng_deleted_story(tr::now) + : tr::lng_deleted_message(tr::now); +} + void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; replyToWebPageId = 0; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 0545a9760..32d1d43f9 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -25,6 +25,7 @@ struct PeerUserpicView; namespace Data { class Session; +class Story; } // namespace Data namespace Media::Player { @@ -182,6 +183,44 @@ private: }; +class ReplyToStoryPointer final { +public: + ReplyToStoryPointer(Data::Story *story = nullptr) : _data(story) { + } + ReplyToStoryPointer(ReplyToStoryPointer &&other) + : _data(base::take(other._data)) { + } + ReplyToStoryPointer &operator=(ReplyToStoryPointer &&other) { + _data = base::take(other._data); + return *this; + } + ReplyToStoryPointer &operator=(Data::Story *item) { + _data = item; + return *this; + } + + [[nodiscard]] bool empty() const { + return !_data; + } + [[nodiscard]] Data::Story *get() const { + return _data; + } + explicit operator bool() const { + return !empty(); + } + + [[nodiscard]] Data::Story *operator->() const { + return _data; + } + [[nodiscard]] Data::Story &operator*() const { + return *_data; + } + +private: + Data::Story *_data = nullptr; + +}; + struct HistoryMessageReply : public RuntimeComponent { HistoryMessageReply() = default; @@ -236,6 +275,7 @@ struct HistoryMessageReply [[nodiscard]] ClickHandlerPtr replyToLink() const { return replyToLnk; } + [[nodiscard]] QString statePhrase() const; void setReplyToLinkFrom(not_null holder); void refreshReplyToMedia(); @@ -249,6 +289,7 @@ struct HistoryMessageReply DocumentId replyToDocumentId = 0; WebPageId replyToWebPageId = 0; ReplyToMessagePointer replyToMsg; + ReplyToStoryPointer replyToStory; std::unique_ptr replyToVia; std::unique_ptr spoiler; ClickHandlerPtr replyToLnk; @@ -257,6 +298,7 @@ struct HistoryMessageReply mutable int maxReplyWidth = 0; int toWidth = 0; bool topicPost = false; + bool storyReply = false; }; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index a7def067b..68eaa0d1e 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_media_types.h" #include "data/data_message_reactions.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" @@ -132,7 +133,7 @@ QString GetErrorTextForSending( return GetErrorTextForSending(thread->peer(), std::move(request)); } -void RequestDependentMessageData( +void RequestDependentMessageItem( not_null item, PeerId peerId, MsgId msgId) { @@ -153,6 +154,23 @@ void RequestDependentMessageData( done); } +void RequestDependentMessageStory( + not_null item, + PeerId peerId, + StoryId storyId) { + const auto fullId = item->fullId(); + const auto history = item->history(); + const auto session = &history->session(); + const auto done = [=] { + if (const auto item = session->data().message(fullId)) { + item->updateDependencyItem(); + } + }; + history->owner().stories().resolve( + { peerId ? peerId : history->peer->id, storyId }, + done); +} + MessageFlags NewMessageFlags(not_null peer) { return MessageFlag::BeingSent | (peer->isSelf() ? MessageFlag() : MessageFlag::Outgoing); @@ -266,6 +284,24 @@ ClickHandlerPtr JumpToMessageClickHandler( }); } +ClickHandlerPtr JumpToStoryClickHandler(not_null story) { + return JumpToStoryClickHandler(story->peer(), story->id()); +} + +ClickHandlerPtr JumpToStoryClickHandler( + not_null peer, + StoryId storyId) { + return std::make_shared([=] { + const auto separate = Core::App().separateWindowForPeer(peer); + const auto controller = separate + ? separate->sessionController() + : peer->session().tryResolveWindow(); + if (controller) { + controller->openPeerStory(peer, storyId); + } + }); +} + MessageFlags FlagsFromMTP( MsgId id, MTPDmessage::Flags flags, diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 46b772517..f9f45d029 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -15,6 +15,7 @@ struct SendAction; } // namespace Api namespace Data { +class Story; class Thread; } // namespace Data @@ -69,10 +70,14 @@ void CheckReactionNotificationSchedule( const TextWithEntities &text = TextWithEntities()); [[nodiscard]] TextWithEntities UnsupportedMessageText(); -void RequestDependentMessageData( +void RequestDependentMessageItem( not_null item, PeerId peerId, MsgId msgId); +void RequestDependentMessageStory( + not_null item, + PeerId peerId, + StoryId storyId); [[nodiscard]] MessageFlags NewMessageFlags(not_null peer); [[nodiscard]] bool ShouldSendSilent( not_null peer, @@ -115,6 +120,11 @@ struct SendingErrorRequest { [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( not_null item, FullMsgId returnToId = FullMsgId()); +[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( + not_null story); +ClickHandlerPtr JumpToStoryClickHandler( + not_null peer, + StoryId storyId); [[nodiscard]] not_null GenerateJoinedMessage( not_null history, diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 5e0f36ddf..18f699b21 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2268,7 +2268,8 @@ bool Message::getStateReplyInfo( if (auto reply = displayedReply()) { int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); if (point.y() >= trect.top() && point.y() < trect.top() + h) { - if (reply->replyToMsg && QRect(trect.x(), trect.y() + st::msgReplyPadding.top(), trect.width(), st::msgReplyBarSize.height()).contains(point)) { + if ((reply->replyToMsg || reply->replyToStory) + && QRect(trect.x(), trect.y() + st::msgReplyPadding.top(), trect.width(), st::msgReplyBarSize.height()).contains(point)) { outResult->link = reply->replyToLink(); } return true; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 02b8282d4..bbd787d91 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/power_save_blocker.h" #include "chat_helpers/compose/compose_show.h" +#include "data/data_changes.h" #include "data/data_file_origin.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" +#include "main/main_session.h" #include "media/stories/media_stories_caption_full_view.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" @@ -394,6 +396,18 @@ void Controller::show( _slider->show({ .index = _index, .total = list.total }); _replyArea->show({ .user = list.user, .id = id }); + const auto session = &list.user->session(); + if (_session != session) { + _session = session; + _sessionLifetime = session->changes().storyUpdates( + Data::StoryUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](Data::StoryUpdate update) { + if (update.story->fullId() == _shown) { + _delegate->storiesClose(); + } + }); + } + if (_contentFaded) { togglePaused(true); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index ebc4a0d47..6619e64a6 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -28,6 +28,10 @@ namespace Ui { class RpWidget; } // namespace Ui +namespace Main { +class Session; +} // namespace Main + namespace Media::Player { struct TrackState; } // namespace Media::Player @@ -152,6 +156,9 @@ private: std::unique_ptr _powerSaveBlocker; + Main::Session *_session = nullptr; + rpl::lifetime _sessionLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 7ca01634e..81cb18923 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2464,6 +2464,18 @@ Ui::ChatThemeBackgroundData SessionController::backgroundData( }; } +void SessionController::openPeerStory( + not_null peer, + StoryId storyId) { + using namespace Media::View; + using namespace Data; + + auto &stories = session().data().stories(); + if (const auto from = stories.lookup({ peer->id, storyId })) { + window().openInMediaView(OpenRequest(this, *from)); + } +} + void SessionController::openPeerStories(PeerId peerId) { using namespace Media::View; using namespace Data; @@ -2474,9 +2486,7 @@ void SessionController::openPeerStories(PeerId peerId) { return list.user->id; }); if (i != end(all) && !i->ids.empty()) { - if (const auto from = stories.lookup({ peerId, i->ids.front() })) { - window().openInMediaView(OpenRequest(this, *from)); - } + openPeerStory(i->user, i->ids.front()); } } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 8f6ed90ec..07f763ca8 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -564,6 +564,7 @@ public: return _peerThemeOverride.value(); } + void openPeerStory(not_null peer, StoryId storyId); void openPeerStories(PeerId peerId); struct PaintContextArgs { From 9f548b523e6cb4e52b1bc937bb0fa305a58d048c Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 May 2023 18:48:33 +0400 Subject: [PATCH 036/260] Apply updates correctly. --- Telegram/SourceFiles/data/data_stories.cpp | 47 +++++++++++++++++-- Telegram/SourceFiles/data/data_stories.h | 3 +- Telegram/SourceFiles/history/history_item.cpp | 12 +++++ Telegram/SourceFiles/history/history_item.h | 2 + .../history/history_item_components.cpp | 20 +++++++- .../history/history_item_components.h | 7 ++- 6 files changed, 82 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 2ee3be031..3f12fd6fe 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -166,7 +166,7 @@ Main::Session &Stories::session() const { } void Stories::apply(const MTPDupdateStories &data) { - pushToFront(parse(data.vstories())); + applyChanges(parse(data.vstories())); _allChanged.fire({}); } @@ -402,14 +402,42 @@ void Stories::applyDeleted(FullStoryId id) { session().changes().storyUpdated( story.get(), UpdateFlag::Destroyed); + removeDependencyStory(story.get()); if (i->second.empty()) { _stories.erase(i); } } } + const auto j = ranges::find(_all, id.peer, [](const StoriesList &list) { + return list.user->id; + }); + if (j != end(_all)) { + const auto till = ranges::remove(j->ids, id.story); + const auto removed = int(std::distance(till, end(j->ids))); + if (till != end(j->ids)) { + j->ids.erase(till, end(j->ids)); + j->total = std::max(j->total - removed, 0); + if (j->ids.empty()) { + _all.erase(j); + } + _allChanged.fire({}); + } + } _deleted.emplace(id); } +void Stories::removeDependencyStory(not_null story) { + const auto i = _dependentMessages.find(story); + if (i != end(_dependentMessages)) { + const auto items = std::move(i->second); + _dependentMessages.erase(i); + + for (const auto &dependent : items) { + dependent->dependencyStoryRemoved(story); + } + } +} + const std::vector &Stories::all() { return _all; } @@ -465,12 +493,21 @@ void Stories::pushToBack(StoriesList &&list) { } } -void Stories::pushToFront(StoriesList &&list) { +void Stories::applyChanges(StoriesList &&list) { const auto i = ranges::find(_all, list.user, &StoriesList::user); if (i != end(_all)) { - *i = std::move(list); - ranges::rotate(begin(_all), i, i + 1); - } else { + auto added = false; + for (const auto id : list.ids) { + if (!ranges::contains(i->ids, id)) { + i->ids.insert(begin(i->ids), id); + ++i->total; + added = true; + } + } + if (added) { + ranges::rotate(begin(_all), i, i + 1); + } + } else if (!list.ids.empty()) { _all.insert(begin(_all), std::move(list)); } } diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 641e26edf..979cba19b 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -123,8 +123,9 @@ private: void finalizeResolve(FullStoryId id); void pushToBack(StoriesList &&list); - void pushToFront(StoriesList &&list); + void applyChanges(StoriesList &&list); void applyDeleted(FullStoryId id); + void removeDependencyStory(not_null story); const not_null _owner; base::flat_map< diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index c0e60d38b..1a99b7024 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -725,6 +725,18 @@ void HistoryItem::dependencyItemRemoved(not_null dependency) { } } +void HistoryItem::dependencyStoryRemoved( + not_null dependency) { + if (const auto reply = Get()) { + const auto documentId = reply->replyToDocumentId; + reply->storyRemoved(this, dependency); + if (documentId != reply->replyToDocumentId + && generateLocalEntitiesByReply()) { + _history->owner().requestItemTextRefresh(this); + } + } +} + void HistoryItem::updateDependencyItem() { if (const auto reply = Get()) { const auto documentId = reply->replyToDocumentId; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 7ef66d7d6..bbc074879 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -57,6 +57,7 @@ class MessageReactions; class ForumTopic; class Thread; struct SponsoredFrom; +class Story; } // namespace Data namespace Main { @@ -185,6 +186,7 @@ public: }; void dependencyItemRemoved(not_null dependency); + void dependencyStoryRemoved(not_null dependency); void updateDependencyItem(); [[nodiscard]] MsgId dependencyMsgId() const; [[nodiscard]] bool notificationReady() const; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 4b5284555..ce9a8edf2 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -369,7 +369,14 @@ void HistoryMessageReply::clearData(not_null holder) { replyToMsg.get()); replyToMsg = nullptr; } + if (replyToStory) { + holder->history()->owner().stories().unregisterDependentMessage( + holder, + replyToStory.get()); + replyToStory = nullptr; + } replyToMsgId = 0; + replyToStoryId = 0; refreshReplyToMedia(); } @@ -466,14 +473,23 @@ void HistoryMessageReply::resize(int width) const { } void HistoryMessageReply::itemRemoved( - HistoryItem *holder, - HistoryItem *removed) { + not_null holder, + not_null removed) { if (replyToMsg.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } } +void HistoryMessageReply::storyRemoved( + not_null holder, + not_null removed) { + if (replyToStory.get() == removed) { + clearData(holder); + holder->history()->owner().requestItemResize(holder); + } +} + void HistoryMessageReply::paint( Painter &p, not_null holder, diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 32d1d43f9..99100f6b0 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -249,7 +249,12 @@ struct HistoryMessageReply [[nodiscard]] bool isNameUpdated(not_null holder) const; void updateName(not_null holder) const; void resize(int width) const; - void itemRemoved(HistoryItem *holder, HistoryItem *removed); + void itemRemoved( + not_null holder, + not_null removed); + void storyRemoved( + not_null holder, + not_null removed); void paint( Painter &p, From 0edbb91b72b056dca43517677fa5f4d46b1998a7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 May 2023 19:18:10 +0400 Subject: [PATCH 037/260] Process media edition updates from API. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/data/data_stories.cpp | 90 ++++++++++++++-------- Telegram/SourceFiles/data/data_stories.h | 4 +- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1b1d41d18..7fa025d38 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1962,6 +1962,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_sticker" = "Sticker"; "lng_in_dlg_sticker_emoji" = "{emoji} Sticker"; "lng_in_dlg_poll" = "Poll"; +"lng_in_dlg_story" = "Story"; "lng_in_dlg_media_count#one" = "{count} media"; "lng_in_dlg_media_count#other" = "{count} media"; "lng_in_dlg_photo_count#one" = "{count} photo"; diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 3f12fd6fe..149125377 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -31,6 +31,31 @@ constexpr auto kMaxResolveTogether = 100; using UpdateFlag = StoryUpdate::Flag; +std::optional ParseMedia( + not_null owner, + const MTPMessageMedia &media) { + return media.match([&](const MTPDmessageMediaPhoto &data) + -> std::optional { + if (const auto photo = data.vphoto()) { + const auto result = owner->processPhoto(*photo); + if (!result->isNull()) { + return StoryMedia{ result }; + } + } + return {}; + }, [&](const MTPDmessageMediaDocument &data) + -> std::optional { + if (const auto document = data.vdocument()) { + const auto result = owner->processDocument(*document); + if (!result->isNull() + && (result->isGifv() || result->isVideoFile())) { + return StoryMedia{ result }; + } + } + return {}; + }, [](const auto &) { return std::optional(); }); +} + } // namespace bool StoriesList::unread() const { @@ -109,7 +134,7 @@ Image *Story::replyPreview() const { } TextWithEntities Story::inReplyText() const { - const auto type = u"Story"_q; + const auto type = tr::lng_in_dlg_story(tr::now); return _caption.text.isEmpty() ? Ui::Text::PlainLink(type) : tr::lng_dialogs_text_media( @@ -141,14 +166,24 @@ const TextWithEntities &Story::caption() const { return _caption; } -void Story::apply(const MTPDstoryItem &data) { - _pinned = data.is_pinned(); - _caption = TextWithEntities{ +bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { + const auto pinned = data.is_pinned(); + auto caption = TextWithEntities{ data.vcaption().value_or_empty(), Api::EntitiesFromMTP( &owner().session(), data.ventities().value_or_empty()), }; + const auto changed = (_media != media) + || (_pinned != pinned) + || (_caption != caption); + if (!changed) { + return false; + } + _media = std::move(media); + _pinned = pinned; + _caption = std::move(caption); + return true; } Stories::Stories(not_null owner) : _owner(owner) { @@ -184,9 +219,10 @@ StoriesList Stories::parse(const MTPUserStories &stories) { result.ids.reserve(list.size()); for (const auto &story : list) { story.match([&](const MTPDstoryItem &data) { - if (const auto story = parse(result.user, data)) { + if (const auto story = parseAndApply(result.user, data)) { result.ids.push_back(story->id()); } else { + applyDeleted({ peerFromUser(userId), data.vid().v }); --result.total; } }, [&](const MTPDstoryItemSkipped &data) { @@ -200,44 +236,30 @@ StoriesList Stories::parse(const MTPUserStories &stories) { return result; } -Story *Stories::parse(not_null peer, const MTPDstoryItem &data) { +Story *Stories::parseAndApply( + not_null peer, + const MTPDstoryItem &data) { + const auto media = ParseMedia(_owner, data.vmedia()); + if (!media) { + return nullptr; + } const auto id = data.vid().v; auto &stories = _stories[peer->id]; const auto i = stories.find(id); if (i != end(stories)) { - i->second->apply(data); + if (i->second->applyChanges(*media, data)) { + session().changes().storyUpdated( + i->second.get(), + UpdateFlag::Edited); + } return i->second.get(); } - using MaybeMedia = std::optional< - std::variant, not_null>>; - const auto media = data.vmedia().match([&]( - const MTPDmessageMediaPhoto &data) -> MaybeMedia { - if (const auto photo = data.vphoto()) { - const auto result = _owner->processPhoto(*photo); - if (!result->isNull()) { - return result; - } - } - return {}; - }, [&](const MTPDmessageMediaDocument &data) -> MaybeMedia { - if (const auto document = data.vdocument()) { - const auto result = _owner->processDocument(*document); - if (!result->isNull() - && (result->isGifv() || result->isVideoFile())) { - return result; - } - } - return {}; - }, [](const auto &) { return MaybeMedia(); }); - if (!media) { - return nullptr; - } const auto result = stories.emplace(id, std::make_unique( id, peer, StoryMedia{ *media }, data.vdate().v)).first->second.get(); - result->apply(data); + result->applyChanges(*media, data); return result; } @@ -373,7 +395,9 @@ void Stories::processResolvedStories( const QVector &list) { for (const auto &item : list) { item.match([&](const MTPDstoryItem &data) { - [[maybe_unused]] const auto story = parse(peer, data); + if (!parseAndApply(peer, data)) { + applyDeleted({ peer->id, data.vid().v }); + } }, [&](const MTPDstoryItemSkipped &data) { LOG(("API Error: Unexpected storyItemSkipped in resolve.")); }, [&](const MTPDstoryItemDeleted &data) { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 979cba19b..c669fe47c 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -56,7 +56,7 @@ public: void setCaption(TextWithEntities &&caption); [[nodiscard]] const TextWithEntities &caption() const; - void apply(const MTPDstoryItem &data); + bool applyChanges(StoryMedia media, const MTPDstoryItem &data); private: const StoryId _id = 0; @@ -113,7 +113,7 @@ public: private: [[nodiscard]] StoriesList parse(const MTPUserStories &stories); - [[nodiscard]] Story *parse( + [[nodiscard]] Story *parseAndApply( not_null peer, const MTPDstoryItem &data); void processResolvedStories( From cdd4774bb8761b1d35c9c628f64760e73ba0ba37 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 May 2023 19:51:56 +0400 Subject: [PATCH 038/260] Fix initial stories collapsing in chats list. --- Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 11 ++++++++--- Telegram/SourceFiles/dialogs/dialogs_inner_widget.h | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 33a2a93cd..484b840bc 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -328,6 +328,7 @@ InnerWidget::InnerWidget( ) | rpl::filter([=] { return (_viewportHeight > 0) && (defaultScrollTop() > _visibleTop); }) | rpl::start_with_next([=] { + refreshForDefaultScroll(); jumpToTop(); }, lifetime()); @@ -1735,9 +1736,7 @@ void InnerWidget::mousePressReleased( void InnerWidget::setViewportHeight(int viewportHeight) { if (_viewportHeight != viewportHeight) { _viewportHeight = viewportHeight; - if (height() < defaultScrollTop() + viewportHeight) { - refresh(); - } + refreshForDefaultScroll(); } } @@ -2766,6 +2765,12 @@ void InnerWidget::editOpenedFilter() { } } +void InnerWidget::refreshForDefaultScroll() { + if (height() < defaultScrollTop() + _viewportHeight) { + refresh(); + } +} + void InnerWidget::refresh(bool toTop) { if (!_geometryInited) { return; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index c7b2d899f..1dcaf68fe 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -122,6 +122,7 @@ public: void clearFilter(); void refresh(bool toTop = false); + void refreshForDefaultScroll(); void refreshEmptyLabel(); void resizeEmptyLabel(); From d82381881a2619c497034ed6d7c5278fd39a0bf7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 May 2023 20:30:02 +0400 Subject: [PATCH 039/260] Allow sending stickers / GIFs in story replies. --- .../SourceFiles/boxes/premium_preview_box.cpp | 4 +- .../SourceFiles/boxes/premium_preview_box.h | 2 +- .../stories/media_stories_controller.cpp | 2 +- .../media/stories/media_stories_reply.cpp | 116 ++++++++++++++++-- .../media/stories/media_stories_reply.h | 24 ++++ .../SourceFiles/window/section_widget.cpp | 8 +- Telegram/SourceFiles/window/section_widget.h | 7 ++ 7 files changed, 147 insertions(+), 16 deletions(-) diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index d63288a01..20581a726 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -1192,9 +1192,9 @@ void Show( } // namespace void ShowStickerPreviewBox( - not_null controller, + std::shared_ptr show, not_null document) { - Show(controller->uiShow(), Descriptor{ + Show(std::move(show), Descriptor{ .section = PremiumPreview::Stickers, .requestedSticker = document, }); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 30fb8f866..51f9bcfbd 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -34,7 +34,7 @@ class Session; } // namespace Main void ShowStickerPreviewBox( - not_null controller, + std::shared_ptr show, not_null document); void DoubledLimitsPreviewBox( diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index bbd787d91..0edb1baec 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -345,7 +345,7 @@ std::shared_ptr Controller::uiShow() const { } auto Controller::stickerOrEmojiChosen() const -->rpl::producer { +-> rpl::producer { return _delegate->storiesStickerOrEmojiChosen(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index b11228f46..105f61def 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_reply.h" #include "api/api_common.h" +#include "api/api_sending.h" #include "apiwrap.h" #include "base/call_delayed.h" #include "boxes/premium_limits_box.h" @@ -28,8 +29,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_controller.h" #include "menu/menu_send.h" #include "storage/localimageloader.h" +#include "storage/storage_account.h" #include "storage/storage_media_prepare.h" #include "ui/chat/attach/attach_prepare.h" +#include "window/section_widget.h" #include "styles/style_boxes.h" // sendMediaPreviewSize. #include "styles/style_chat_helpers.h" #include "styles/style_media_view.h" @@ -134,6 +137,96 @@ void ReplyArea::sendVoice(VoiceToSend &&data) { finishSending(); } +bool ReplyArea::sendExistingDocument( + not_null document, + Api::SendOptions options, + std::optional localId) { + Expects(_data.user != nullptr); + + const auto show = _controller->uiShow(); + const auto error = Data::RestrictionError( + _data.user, + ChatRestriction::SendStickers); + if (error) { + show->showToast(*error); + return false; + } else if (Window::ShowSendPremiumError(show, document)) { + return false; + } + + Api::SendExistingDocument( + Api::MessageToSend(prepareSendAction(options)), + document, + localId); + + _controls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ReplyArea::sendExistingPhoto(not_null photo) { + sendExistingPhoto(photo, {}); +} + +bool ReplyArea::sendExistingPhoto( + not_null photo, + Api::SendOptions options) { + Expects(_data.user != nullptr); + + const auto show = _controller->uiShow(); + const auto error = Data::RestrictionError( + _data.user, + ChatRestriction::SendPhotos); + if (error) { + show->showToast(*error); + return false; + } + + Api::SendExistingPhoto( + Api::MessageToSend(prepareSendAction(options)), + photo); + + _controls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ReplyArea::sendInlineResult( + not_null result, + not_null bot) { + const auto errorText = result->getErrorOnSend(history()); + if (!errorText.isEmpty()) { + _controller->uiShow()->showToast(errorText); + return; + } + sendInlineResult(result, bot, {}, std::nullopt); +} + +void ReplyArea::sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options, + std::optional localMessageId) { + auto action = prepareSendAction(options); + action.generateLocal = true; + session().api().sendInlineResult(bot, result, action, localMessageId); + + _controls->clear(); + + auto &bots = cRefRecentInlineBots(); + const auto index = bots.indexOf(bot); + if (index) { + if (index > 0) { + bots.removeAt(index); + } else if (bots.size() >= RecentInlineBotsLimit) { + bots.resize(RecentInlineBotsLimit - 1); + } + bots.push_front(bot); + bot->session().local().writeRecentHashtagsAndBots(); + } + finishSending(); +} + void ReplyArea::finishSending() { _controls->hidePanelsAnimated(); _controller->wrap()->setFocus(); @@ -188,12 +281,17 @@ bool ReplyArea::showSendingFilesError( return true; } +not_null ReplyArea::history() const { + Expects(_data.user != nullptr); + + return _data.user->owner().history(_data.user); +} + Api::SendAction ReplyArea::prepareSendAction( Api::SendOptions options) const { Expects(_data.user != nullptr); - const auto history = _data.user->owner().history(_data.user); - auto result = Api::SendAction(history, options); + auto result = Api::SendAction(history(), options); result.options.sendAs = _controls->sendAsPeer(); result.replyTo.storyId = { .peer = _data.user->id, .story = _data.id }; return result; @@ -402,23 +500,19 @@ void ReplyArea::initActions() { _controls->fileChosen( ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { _controller->uiShow()->hideLayer(); - //controller()->sendingAnimation().appendSending( - // data.messageSendingFrom); - //const auto localId = data.messageSendingFrom.localId; - //sendExistingDocument(data.document, data.options, localId); + const auto localId = data.messageSendingFrom.localId; + sendExistingDocument(data.document, data.options, localId); }, _lifetime); _controls->photoChosen( ) | rpl::start_with_next([=](ChatHelpers::PhotoChosen chosen) { - //sendExistingPhoto(chosen.photo, chosen.options); + sendExistingPhoto(chosen.photo, chosen.options); }, _lifetime); _controls->inlineResultChosen( ) | rpl::start_with_next([=](ChatHelpers::InlineChosen chosen) { - //controller()->sendingAnimation().appendSending( - // chosen.messageSendingFrom); - //const auto localId = chosen.messageSendingFrom.localId; - //sendInlineResult(chosen.result, chosen.bot, chosen.options, localId); + const auto localId = chosen.messageSendingFrom.localId; + sendInlineResult(chosen.result, chosen.bot, chosen.options, localId); }, _lifetime); _controls->setMimeDataHook([=]( diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index c7ea6f88c..d9683521e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" +class History; enum class SendMediaType; namespace Api { @@ -24,6 +25,10 @@ namespace HistoryView::Controls { struct VoiceToSend; } // namespace HistoryView::Controls +namespace InlineBots { +class Result; +} // namespace InlineBots + namespace Main { class Session; } // namespace Main @@ -58,6 +63,7 @@ private: using VoiceToSend = HistoryView::Controls::VoiceToSend; [[nodiscard]] Main::Session &session() const; + [[nodiscard]] not_null history() const; bool confirmSendingFiles(const QStringList &files); bool confirmSendingFiles(not_null data); @@ -90,6 +96,24 @@ private: bool ctrlShiftEnter); void finishSending(); + void sendExistingDocument(not_null document); + bool sendExistingDocument( + not_null document, + Api::SendOptions options, + std::optional localId); + void sendExistingPhoto(not_null photo); + bool sendExistingPhoto( + not_null photo, + Api::SendOptions options); + void sendInlineResult( + not_null result, + not_null bot); + void sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options, + std::optional localMessageId); + void initGeometry(); void initActions(); diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index bdc5fc849..d08a28c59 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -457,11 +457,17 @@ auto ChatThemeValueFromPeer( bool ShowSendPremiumError( not_null controller, not_null document) { + return ShowSendPremiumError(controller->uiShow(), document); +} + +bool ShowSendPremiumError( + std::shared_ptr show, + not_null document) { if (!document->isPremiumSticker() || document->session().premium()) { return false; } - ShowStickerPreviewBox(controller, document); + ShowStickerPreviewBox(std::move(show), document); return true; } diff --git a/Telegram/SourceFiles/window/section_widget.h b/Telegram/SourceFiles/window/section_widget.h index 89be8819c..d8cfda814 100644 --- a/Telegram/SourceFiles/window/section_widget.h +++ b/Telegram/SourceFiles/window/section_widget.h @@ -16,6 +16,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PeerData; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { struct ReactionId; class ForumTopic; @@ -238,6 +242,9 @@ private: [[nodiscard]] bool ShowSendPremiumError( not_null controller, not_null document); +[[nodiscard]] bool ShowSendPremiumError( + std::shared_ptr show, + not_null document); [[nodiscard]] bool ShowReactPremiumError( not_null controller, From 4e165a2107d133e6346b27ff3bba72e91b814e8c Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 May 2023 11:45:04 +0400 Subject: [PATCH 040/260] Sort chats list stories by unread state. --- Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index b1262388f..7b127cd3c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -162,6 +162,9 @@ Content State::next() { #endif }); } + ranges::stable_partition(result.users, [](const User &user) { + return user.unread; + }); return result; } From 4a676414605bb9861908e65034b97f8d90397d9a Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 May 2023 12:06:21 +0400 Subject: [PATCH 041/260] Load more story users on demand. --- Telegram/SourceFiles/data/data_stories.cpp | 4 +++- .../SourceFiles/dialogs/dialogs_inner_widget.cpp | 5 +++++ .../dialogs/ui/dialogs_stories_list.cpp | 14 ++++++++++++++ .../SourceFiles/dialogs/ui/dialogs_stories_list.h | 3 +++ .../media/stories/media_stories_controller.cpp | 5 +++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 149125377..e86c395f3 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -299,7 +299,9 @@ void Stories::loadMore() { const auto api = &_owner->session().api(); using Flag = MTPstories_GetAllStories::Flag; _loadMoreRequestId = api->request(MTPstories_GetAllStories( - MTP_flags(_state.isEmpty() ? Flag(0) : Flag::f_next), + MTP_flags(_state.isEmpty() + ? Flag(0) + : (Flag::f_next | Flag::f_state)), MTP_string(_state) )).done([=](const MTPstories_AllStories &result) { _loadMoreRequestId = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 484b840bc..1b354b377 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -342,6 +342,11 @@ InnerWidget::InnerWidget( _controller->openPeerStories(PeerId(int64(id))); }, lifetime()); + _stories->loadMoreRequests( + ) | rpl::start_with_next([=] { + session().data().stories().loadMore(); + }, lifetime()); + handleChatListEntryRefreshes(); refreshWithCollapsedRows(true); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 3a8cdfd0a..9e7370466 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -19,6 +19,7 @@ namespace { constexpr auto kSmallUserpicsShown = 3; constexpr auto kSmallReadOpacity = 0.6; constexpr auto kSummaryExpandLeft = 1.5; +constexpr auto kPreloadPages = 2; [[nodiscard]] int AvailableNameWidth() { const auto &full = st::dialogsStoriesFull; @@ -206,6 +207,7 @@ void List::updateScrollMax() { const auto widthFull = full.left + int(_data.items.size()) * singleFull; _scrollLeftMax = std::max(widthFull - width(), 0); _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); + checkLoadMore(); update(); } @@ -221,6 +223,10 @@ rpl::producer<> List::entered() const { return _entered.events(); } +rpl::producer<> List::loadMoreRequests() const { + return _loadMoreRequests.events(); +} + void List::enterEventHook(QEnterEvent *e) { _entered.fire({}); } @@ -597,6 +603,7 @@ void List::wheelEvent(QWheelEvent *e) { _expandRequests.fire({}); _scrollLeft = next; updateSelected(); + checkLoadMore(); update(); } e->accept(); @@ -640,11 +647,18 @@ void List::checkDragging() { _scrollLeftMax); if (newLeft != _scrollLeft) { _scrollLeft = newLeft; + checkLoadMore(); update(); } } } +void List::checkLoadMore() { + if (_scrollLeftMax - _scrollLeft < width() * kPreloadPages) { + _loadMoreRequests.fire({}); + } +} + void List::mouseReleaseEvent(QMouseEvent *e) { _lastMousePosition = e->globalPos(); const auto guard = gsl::finally([&] { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 1286db93e..06a0fc286 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -47,6 +47,7 @@ public: [[nodiscard]] rpl::producer clicks() const; [[nodiscard]] rpl::producer<> expandRequests() const; [[nodiscard]] rpl::producer<> entered() const; + [[nodiscard]] rpl::producer<> loadMoreRequests() const; private: struct Layout; @@ -110,6 +111,7 @@ private: void updateSelected(); void checkDragging(); bool finishDragging(); + void checkLoadMore(); void updateHeight(); void toggleAnimated(bool shown); @@ -128,6 +130,7 @@ private: rpl::event_stream _clicks; rpl::event_stream<> _expandRequests; rpl::event_stream<> _entered; + rpl::event_stream<> _loadMoreRequests; Ui::Animations::Simple _shownAnimation; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 0edb1baec..c0eb8153a 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -40,6 +40,7 @@ constexpr auto kSiblingMultiplierMax = 0.72; constexpr auto kSiblingOutsidePart = 0.24; constexpr auto kSiblingUserpicSize = 0.3; constexpr auto kInnerHeightMultiplier = 1.6; +constexpr auto kPreloadUsersCount = 3; } // namespace @@ -381,6 +382,10 @@ void Controller::show( } _index = subindex; + if (int(lists.size()) - index < kPreloadUsersCount) { + story->peer()->owner().stories().loadMore(); + } + const auto storyId = FullStoryId{ .peer = list.user->id, .story = id, From f3233707525f387ba82cc2a664b6e6ee8d8ac608 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 May 2023 16:03:23 +0400 Subject: [PATCH 042/260] Preload stories in both directions. --- Telegram/SourceFiles/data/data_stories.cpp | 132 ++++++++++++------ Telegram/SourceFiles/data/data_stories.h | 9 +- .../stories/media_stories_controller.cpp | 80 ++++++++--- .../media/stories/media_stories_controller.h | 4 + 4 files changed, 161 insertions(+), 64 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index e86c395f3..6ff76e0f9 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -28,6 +28,8 @@ namespace Data { namespace { constexpr auto kMaxResolveTogether = 100; +constexpr auto kIgnorePreloadAroundIfLoaded = 15; +constexpr auto kPreloadAroundCount = 30; using UpdateFlag = StoryUpdate::Flag; @@ -322,36 +324,31 @@ void Stories::loadMore() { } void Stories::sendResolveRequests() { - if (!_resolveRequests.empty()) { + if (!_resolveSent.empty()) { return; } - struct Prepared { - QVector ids; - std::vector> callbacks; - }; auto leftToSend = kMaxResolveTogether; - auto byPeer = base::flat_map(); - for (auto i = begin(_resolves); i != end(_resolves);) { + auto byPeer = base::flat_map>(); + for (auto i = begin(_resolvePending); i != end(_resolvePending);) { auto &[peerId, ids] = *i; - auto &prepared = byPeer[peerId]; - for (auto &[storyId, callbacks] : ids) { - prepared.ids.push_back(MTP_int(storyId)); - prepared.callbacks.insert( - end(prepared.callbacks), - std::make_move_iterator(begin(callbacks)), - std::make_move_iterator(end(callbacks))); - if (!--leftToSend) { - break; - } - } - const auto sending = int(prepared.ids.size()); - if (sending == ids.size()) { - i = _resolves.erase(i); - if (!leftToSend) { - break; - } + auto &sent = _resolveSent[peerId]; + if (ids.size() <= leftToSend) { + sent = base::take(ids); + i = _resolvePending.erase(i); + leftToSend -= int(sent.size()); } else { - ids.erase(begin(ids), begin(ids) + sending); + sent = { + std::make_move_iterator(begin(ids)), + std::make_move_iterator(begin(ids) + leftToSend) + }; + ids.erase(begin(ids), begin(ids) + leftToSend); + leftToSend = 0; + } + auto &prepared = byPeer[peerId]; + for (auto &[storyId, callbacks] : sent) { + prepared.push_back(MTP_int(storyId)); + } + if (!leftToSend) { break; } } @@ -359,36 +356,35 @@ void Stories::sendResolveRequests() { for (auto &entry : byPeer) { const auto peerId = entry.first; auto &prepared = entry.second; - const auto finish = [=, ids = prepared.ids](mtpRequestId id) { - for (const auto &id : ids) { - finalizeResolve({ peerId, id.v }); - } - if (auto callbacks = _resolveRequests.take(id)) { - for (const auto &callback : *callbacks) { + const auto finish = [=](PeerId peerId) { + const auto sent = _resolveSent.take(peerId); + Assert(sent.has_value()); + for (const auto &[storyId, list] : *sent) { + finalizeResolve({ peerId, storyId }); + for (const auto &callback : list) { callback(); } } - if (_resolveRequests.empty() && !_resolves.empty()) { + _itemsChanged.fire_copy(peerId); + if (_resolveSent.empty() && !_resolvePending.empty()) { crl::on_main(&session(), [=] { sendResolveRequests(); }); } }; const auto user = _owner->session().data().peer(peerId)->asUser(); if (!user) { - _resolveRequests[0] = std::move(prepared.callbacks); - finish(0); + finish(peerId); continue; } const auto requestId = api->request(MTPstories_GetStoriesByID( user->inputUser, - MTP_vector(std::move(prepared.ids)) - )).done([=](const MTPstories_Stories &result, mtpRequestId id) { + MTP_vector(prepared) + )).done([=](const MTPstories_Stories &result) { owner().processUsers(result.data().vusers()); processResolvedStories(user, result.data().vstories().v); - finish(id); - }).fail([=](const MTP::Error &error, mtpRequestId id) { - finish(id); + finish(user->id); + }).fail([=] { + finish(peerId); }).send(); - _resolveRequests.emplace(requestId, std::move(prepared.callbacks)); } } @@ -476,6 +472,10 @@ rpl::producer<> Stories::allChanged() const { return _allChanged.events(); } +rpl::producer Stories::itemsChanged() const { + return _itemsChanged.events(); +} + base::expected, NoStory> Stories::lookup( FullStoryId id) const { const auto i = _stories.find(id.peer); @@ -492,10 +492,20 @@ base::expected, NoStory> Stories::lookup( void Stories::resolve(FullStoryId id, Fn done) { const auto already = lookup(id); if (already.has_value() || already.error() != NoStory::Unknown) { - done(); + if (done) { + done(); + } return; } - auto &ids = _resolves[id.peer]; + if (const auto i = _resolveSent.find(id.peer); i != end(_resolveSent)) { + if (const auto j = i->second.find(id.story); j != end(i->second)) { + if (done) { + j->second.push_back(std::move(done)); + } + return; + } + } + auto &ids = _resolvePending[id.peer]; if (ids.empty()) { crl::on_main(&session(), [=] { sendResolveRequests(); @@ -525,7 +535,7 @@ void Stories::applyChanges(StoriesList &&list) { auto added = false; for (const auto id : list.ids) { if (!ranges::contains(i->ids, id)) { - i->ids.insert(begin(i->ids), id); + i->ids.push_back(id); ++i->total; added = true; } @@ -538,4 +548,40 @@ void Stories::applyChanges(StoriesList &&list) { } } +void Stories::loadAround(FullStoryId id) { + const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) { + return list.user->id; + }); + if (i == end(_all)) { + return; + } + const auto j = ranges::find(i->ids, id.story); + if (j == end(i->ids)) { + return; + } + const auto ignore = [&] { + const auto side = kIgnorePreloadAroundIfLoaded; + const auto left = ranges::min(j - begin(i->ids), side); + const auto right = ranges::min(end(i->ids) - j, side); + for (auto k = j - left; k != j + right; ++k) { + const auto maybeStory = lookup({ id.peer, *k }); + if (!maybeStory && maybeStory.error() == NoStory::Unknown) { + return false; + } + } + return true; + }(); + if (ignore) { + return; + } + const auto side = kPreloadAroundCount; + const auto left = ranges::min(j - begin(i->ids), side); + const auto right = ranges::min(end(i->ids) - j, side); + const auto from = j - left; + const auto till = j + right; + for (auto k = from; k != till; ++k) { + resolve({ id.peer, *k }, nullptr); + } +} + } // namespace Data \ No newline at end of file diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index c669fe47c..92db99acd 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -102,10 +102,12 @@ public: void loadMore(); void apply(const MTPDupdateStories &data); + void loadAround(FullStoryId id); [[nodiscard]] const std::vector &all(); [[nodiscard]] bool allLoaded() const; [[nodiscard]] rpl::producer<> allChanged() const; + [[nodiscard]] rpl::producer itemsChanged() const; [[nodiscard]] base::expected, NoStory> lookup( FullStoryId id) const; @@ -135,8 +137,10 @@ private: base::flat_map< PeerId, - base::flat_map>>> _resolves; - base::flat_map>> _resolveRequests; + base::flat_map>>> _resolvePending; + base::flat_map< + PeerId, + base::flat_map>>> _resolveSent; std::map< not_null, @@ -144,6 +148,7 @@ private: std::vector _all; rpl::event_stream<> _allChanged; + rpl::event_stream _itemsChanged; QString _state; bool _allLoaded = false; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index c0eb8153a..83a62b624 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -361,10 +361,11 @@ void Controller::show( const auto &list = lists[index]; const auto id = list.ids[subindex]; - const auto maybeStory = list.user->owner().stories().lookup({ + const auto storyId = FullStoryId{ .peer = list.user->id, .story = id, - }); + }; + const auto maybeStory = list.user->owner().stories().lookup(storyId); if (!maybeStory) { return; } @@ -381,15 +382,8 @@ void Controller::show( _list = list; } _index = subindex; + _waitingForId = {}; - if (int(lists.size()) - index < kPreloadUsersCount) { - story->peer()->owner().stories().loadMore(); - } - - const auto storyId = FullStoryId{ - .peer = list.user->id, - .story = id, - }; if (_shown == storyId) { return; } @@ -411,8 +405,20 @@ void Controller::show( _delegate->storiesClose(); } }); + session->data().stories().itemsChanged( + ) | rpl::start_with_next([=](PeerId peerId) { + if (_waitingForId.peer == peerId) { + checkWaitingFor(); + } + }, _sessionLifetime); } + auto &stories = session->data().stories(); + if (int(lists.size()) - index < kPreloadUsersCount) { + stories.loadMore(); + } + stories.loadAround(storyId); + if (_contentFaded) { togglePaused(true); } @@ -483,25 +489,61 @@ bool Controller::subjumpFor(int delta) { } else if (!_list || _list->ids.empty()) { return false; } - _delegate->storiesJumpTo(&_list->user->session(), { - .peer = _list->user->id, - .story = _list->ids.front() - }); + subjumpTo(0); return true; } else if (index >= _list->total) { return _siblingRight && _siblingRight->shownId().valid() && jumpFor(1); } else if (index < _list->ids.size()) { - // #TODO stories load more - _delegate->storiesJumpTo(&_list->user->session(), { - .peer = _list->user->id, - .story = _list->ids[index] - }); + subjumpTo(index); } return true; } +void Controller::subjumpTo(int index) { + Expects(_list.has_value()); + Expects(index >= 0 && index < _list->ids.size()); + + const auto id = FullStoryId{ + .peer = _list->user->id, + .story = _list->ids[index] + }; + auto &stories = _list->user->owner().stories(); + if (stories.lookup(id)) { + _delegate->storiesJumpTo(&_list->user->session(), id); + } else if (_waitingForId != id) { + _waitingForId = id; + stories.loadAround(id); + } +} + +void Controller::checkWaitingFor() { + Expects(_waitingForId.valid()); + Expects(_list.has_value()); + + auto &stories = _list->user->owner().stories(); + const auto &all = stories.all(); + const auto i = ranges::find_if(all, [&](const Data::StoriesList &data) { + return data.user->id == _waitingForId.peer; + }); + if (i == end(all)) { + _waitingForId = {}; + return; + } + const auto j = ranges::find(i->ids, _waitingForId.story); + if (j == end(i->ids)) { + _waitingForId = {}; + return; + } + const auto maybe = stories.lookup(_waitingForId); + if (!maybe) { + return; + } + _delegate->storiesJumpTo( + &_list->user->session(), + base::take(_waitingForId)); +} bool Controller::jumpFor(int delta) { if (delta == -1) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 6619e64a6..79bca280e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -131,6 +131,9 @@ private: std::unique_ptr &sibling, const Data::StoriesList *list); + void subjumpTo(int index); + void checkWaitingFor(); + const not_null _delegate; rpl::variable> _layout; @@ -148,6 +151,7 @@ private: FullStoryId _shown; TextWithEntities _captionText; std::optional _list; + FullStoryId _waitingForId; int _index = 0; bool _started = false; From f814e401b9a7c5d3311bed76cd8d091f0e0ec24e Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 May 2023 19:09:36 +0400 Subject: [PATCH 043/260] Mark stories as read. --- Telegram/SourceFiles/core/application.cpp | 4 + Telegram/SourceFiles/data/data_stories.cpp | 100 ++++++++++++++++-- Telegram/SourceFiles/data/data_stories.h | 13 ++- .../dialogs/ui/dialogs_stories_list.cpp | 30 +++--- .../stories/media_stories_controller.cpp | 33 +++++- .../media/stories/media_stories_controller.h | 2 + .../window/window_session_controller.cpp | 3 +- 7 files changed, 158 insertions(+), 27 deletions(-) diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 4fb01d12b..6ac6a20b8 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_download_manager.h" @@ -1685,6 +1686,9 @@ bool Application::readyToQuit() { if (session->api().isQuitPrevent()) { prevented = true; } + if (session->data().stories().isQuitPrevent()) { + prevented = true; + } } } } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 6ff76e0f9..82b6c91b3 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "apiwrap.h" +#include "core/application.h" #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_file_origin.h" @@ -30,6 +31,7 @@ namespace { constexpr auto kMaxResolveTogether = 100; constexpr auto kIgnorePreloadAroundIfLoaded = 15; constexpr auto kPreloadAroundCount = 30; +constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); using UpdateFlag = StoryUpdate::Flag; @@ -61,7 +63,7 @@ std::optional ParseMedia( } // namespace bool StoriesList::unread() const { - return !ids.empty() && readTill < ids.front(); + return !ids.empty() && readTill < ids.back(); } Story::Story( @@ -188,7 +190,9 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { return true; } -Stories::Stories(not_null owner) : _owner(owner) { +Stories::Stories(not_null owner) +: _owner(owner) +, _markReadTimer([=] { sendMarkAsReadRequests(); }) { } Stories::~Stories() { @@ -222,13 +226,13 @@ StoriesList Stories::parse(const MTPUserStories &stories) { for (const auto &story : list) { story.match([&](const MTPDstoryItem &data) { if (const auto story = parseAndApply(result.user, data)) { - result.ids.push_back(story->id()); + result.ids.emplace(story->id()); } else { applyDeleted({ peerFromUser(userId), data.vid().v }); --result.total; } }, [&](const MTPDstoryItemSkipped &data) { - result.ids.push_back(data.vid().v); + result.ids.emplace(data.vid().v); }, [&](const MTPDstoryItemDeleted &data) { applyDeleted({ peerFromUser(userId), data.vid().v }); --result.total; @@ -434,13 +438,13 @@ void Stories::applyDeleted(FullStoryId id) { return list.user->id; }); if (j != end(_all)) { - const auto till = ranges::remove(j->ids, id.story); - const auto removed = int(std::distance(till, end(j->ids))); - if (till != end(j->ids)) { - j->ids.erase(till, end(j->ids)); - j->total = std::max(j->total - removed, 0); + const auto removed = j->ids.remove(id.story); + if (removed) { if (j->ids.empty()) { _all.erase(j); + } else { + Assert(j->total > 0); + --j->total; } _allChanged.fire({}); } @@ -534,8 +538,8 @@ void Stories::applyChanges(StoriesList &&list) { if (i != end(_all)) { auto added = false; for (const auto id : list.ids) { - if (!ranges::contains(i->ids, id)) { - i->ids.push_back(id); + if (!i->ids.contains(id)) { + i->ids.emplace(id); ++i->total; added = true; } @@ -584,4 +588,78 @@ void Stories::loadAround(FullStoryId id) { } } +void Stories::markAsRead(FullStoryId id) { + const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) { + return list.user->id; + }); + Assert(i != end(_all)); + if (i->readTill >= id.story) { + return; + } else if (!_markReadPending.contains(id.peer)) { + sendMarkAsReadRequests(); + } + _markReadPending.emplace(id.peer); + i->readTill = id.story; + _markReadTimer.callOnce(kMarkAsReadDelay); + _allChanged.fire({}); +} + +void Stories::sendMarkAsReadRequest( + not_null peer, + StoryId tillId) { + Expects(peer->isUser()); + + const auto peerId = peer->id; + _markReadRequests.emplace(peerId); + const auto finish = [=] { + _markReadRequests.remove(peerId); + if (!_markReadTimer.isActive() + && _markReadPending.contains(peerId)) { + sendMarkAsReadRequests(); + } + if (_markReadRequests.empty()) { + if (Core::Quitting()) { + LOG(("Stories doesn't prevent quit any more.")); + } + Core::App().quitPreventFinished(); + } + }; + + const auto api = &_owner->session().api(); + api->request(MTPstories_ReadStories( + peer->asUser()->inputUser, + MTP_int(tillId) + )).done(finish).fail(finish).send(); +} + +void Stories::sendMarkAsReadRequests() { + _markReadTimer.cancel(); + for (auto i = begin(_markReadPending); i != end(_markReadPending);) { + const auto peerId = *i; + if (_markReadRequests.contains(peerId)) { + ++i; + continue; + } + const auto j = ranges::find(_all, peerId, []( + const StoriesList &list) { + return list.user->id; + }); + if (j != end(_all)) { + sendMarkAsReadRequest(j->user, j->readTill); + } + i = _markReadPending.erase(i); + } +} + +bool Stories::isQuitPrevent() { + if (!_markReadPending.empty()) { + sendMarkAsReadRequests(); + } + if (_markReadRequests.empty()) { + return false; + } + LOG(("Stories prevents quit, marking as read...")); + return true; +} + } // namespace Data \ No newline at end of file diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 92db99acd..44da5e7e3 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/expected.h" +#include "base/timer.h" class Image; class PhotoData; @@ -70,7 +71,7 @@ private: struct StoriesList { not_null user; - std::vector ids; + base::flat_set ids; StoryId readTill = 0; int total = 0; @@ -113,6 +114,9 @@ public: FullStoryId id) const; void resolve(FullStoryId id, Fn done); + [[nodiscard]] bool isQuitPrevent(); + void markAsRead(FullStoryId id); + private: [[nodiscard]] StoriesList parse(const MTPUserStories &stories); [[nodiscard]] Story *parseAndApply( @@ -129,6 +133,9 @@ private: void applyDeleted(FullStoryId id); void removeDependencyStory(not_null story); + void sendMarkAsReadRequests(); + void sendMarkAsReadRequest(not_null peer, StoryId tillId); + const not_null _owner; base::flat_map< PeerId, @@ -154,6 +161,10 @@ private: mtpRequestId _loadMoreRequestId = 0; + base::flat_set _markReadPending; + base::Timer _markReadTimer; + base::flat_set _markReadRequests; + }; } // namespace Data diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 9e7370466..58625ddef 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -37,11 +37,15 @@ struct List::Layout { float64 userpicLeft = 0.; float64 photoLeft = 0.; float64 left = 0.; + float64 single = 0.; + int leftFull = 0; + int leftSmall = 0; + int singleFull = 0; + int singleSmall = 0; int startIndexSmall = 0; int endIndexSmall = 0; int startIndexFull = 0; int endIndexFull = 0; - int singleFull = 0; }; List::List( @@ -277,11 +281,15 @@ List::Layout List::computeLayout() const { .userpicLeft = userpicLeft, .photoLeft = photoLeft, .left = userpicLeft - photoLeft, + .single = lerp(st.shift, singleFull), + .leftFull = leftFull, + .leftSmall = leftSmall, + .singleFull = singleFull, + .singleSmall = st.shift, .startIndexSmall = startIndexSmall, .endIndexSmall = endIndexSmall, .startIndexFull = startIndexFull, .endIndexFull = endIndexFull, - .singleFull = singleFull, }; } @@ -296,8 +304,6 @@ void List::paintEvent(QPaintEvent *e) { auto &rendering = _data.empty() ? _hidingData : _data; const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; - const auto singleSmall = st.shift; - const auto single = lerp(singleSmall, layout.singleFull); const auto photoTopSmall = (st.height - st.photo) / 2.; const auto photoTop = lerp(photoTopSmall, full.photoTop); const auto photo = lerp(st.photo, full.photo); @@ -346,7 +352,7 @@ void List::paintEvent(QPaintEvent *e) { const auto full = (drawFull && indexFull < layout.endIndexFull) ? &rendering.items[indexFull] : nullptr; - const auto x = layout.left + single * index; + const auto x = layout.left + layout.single * index; return Single{ x, indexSmall, small, indexFull, full }; }; const auto hasUnread = [&](const Single &single) { @@ -697,19 +703,17 @@ void List::updateSelected() { const auto &full = st::dialogsStoriesFull; const auto p = mapFromGlobal(_lastMousePosition); const auto layout = computeLayout(); - const auto firstRightFull = full.left + layout.singleFull; - const auto firstRightSmall = st.left + const auto firstRightFull = layout.leftFull + + (layout.startIndexFull + 1) * layout.singleFull; + const auto firstRightSmall = layout.leftSmall + st.photoLeft + st.photo; - const auto stepFull = layout.singleFull; - const auto stepSmall = st.shift; const auto lastRightAddFull = 0; const auto lastRightAddSmall = st.photoLeft; const auto lerp = [&](float64 a, float64 b) { return a + (b - a) * layout.ratio; }; const auto firstRight = lerp(firstRightSmall, firstRightFull); - const auto step = lerp(stepSmall, stepFull); const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull); const auto activateFull = (layout.ratio >= 0.5); const auto startIndex = activateFull @@ -721,14 +725,16 @@ void List::updateSelected() { const auto x = p.x(); const auto infiniteIndex = (x < firstRight) ? 0 - : int(std::floor(((x - firstRight) / step) + 1)); + : int(std::floor(((x - firstRight) / layout.single) + 1)); const auto index = (endIndex == startIndex) ? -1 : (infiniteIndex == endIndex - startIndex && x < firstRight - + (endIndex - startIndex - 1) * step + + (endIndex - startIndex - 1) * layout.single + lastRightAdd) ? (infiniteIndex - 1) // Last small part should still be clickable. + : (startIndex + infiniteIndex >= endIndex) + ? -1 : infiniteIndex; const auto selected = (index < 0 || startIndex + index >= layout.itemsCount) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 83a62b624..650ef7af2 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -41,6 +41,8 @@ constexpr auto kSiblingOutsidePart = 0.24; constexpr auto kSiblingUserpicSize = 0.3; constexpr auto kInnerHeightMultiplier = 1.6; constexpr auto kPreloadUsersCount = 3; +constexpr auto kMarkAsReadAfterSeconds = 1; +constexpr auto kMarkAsReadAfterProgress = 0.2; } // namespace @@ -360,7 +362,7 @@ void Controller::show( showSiblings(lists, index); const auto &list = lists[index]; - const auto id = list.ids[subindex]; + const auto id = *(begin(list.ids) + subindex); const auto storyId = FullStoryId{ .peer = list.user->id, .story = id, @@ -464,6 +466,7 @@ void Controller::updatePhotoPlayback(const Player::TrackState &state) { void Controller::updatePlayback(const Player::TrackState &state) { _slider->updatePlayback(state); updatePowerSaveBlocker(state); + maybeMarkAsRead(state); if (Player::IsStoppedAtEnd(state.state)) { if (!subjumpFor(1)) { _delegate->storiesClose(); @@ -471,6 +474,26 @@ void Controller::updatePlayback(const Player::TrackState &state) { } } +void Controller::maybeMarkAsRead(const Player::TrackState &state) { + const auto length = state.length; + const auto position = Player::IsStoppedAtEnd(state.state) + ? state.length + : Player::IsStoppedOrStopping(state.state) + ? 0 + : state.position; + if (position > state.frequency * kMarkAsReadAfterSeconds) { + if (position > kMarkAsReadAfterProgress * length) { + markAsRead(); + } + } +} + +void Controller::markAsRead() { + Expects(_list.has_value()); + + _list->user->owner().stories().markAsRead(_shown); +} + bool Controller::subjumpAvailable(int delta) const { const auto index = _index + delta; if (index < 0) { @@ -482,6 +505,9 @@ bool Controller::subjumpAvailable(int delta) const { } bool Controller::subjumpFor(int delta) { + if (delta > 0) { + markAsRead(); + } const auto index = _index + delta; if (index < 0) { if (_siblingLeft && _siblingLeft->shownId().valid()) { @@ -507,7 +533,7 @@ void Controller::subjumpTo(int index) { const auto id = FullStoryId{ .peer = _list->user->id, - .story = _list->ids[index] + .story = *(begin(_list->ids) + index) }; auto &stories = _list->user->owner().stories(); if (stories.lookup(id)) { @@ -554,6 +580,9 @@ bool Controller::jumpFor(int delta) { return true; } } else if (delta == 1) { + if (_list && _index + 1 >= _list->total) { + markAsRead(); + } if (const auto right = _siblingRight.get()) { _delegate->storiesJumpTo( &right->peer()->session(), diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 79bca280e..f381e51c1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -123,6 +123,8 @@ private: void updatePhotoPlayback(const Player::TrackState &state); void updatePlayback(const Player::TrackState &state); void updatePowerSaveBlocker(const Player::TrackState &state); + void maybeMarkAsRead(const Player::TrackState &state); + void markAsRead(); void showSiblings( const std::vector &lists, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 81cb18923..b30e9dd13 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2486,7 +2486,8 @@ void SessionController::openPeerStories(PeerId peerId) { return list.user->id; }); if (i != end(all) && !i->ids.empty()) { - openPeerStory(i->user, i->ids.front()); + const auto j = i->ids.lower_bound(i->readTill + 1); + openPeerStory(i->user, j != i->ids.end() ? *j : i->ids.front()); } } From b8cf00a0b2d4086312b7a929b2f2cda57b161697 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 May 2023 09:44:31 +0400 Subject: [PATCH 044/260] Fix replying to stories with voice messages. --- Telegram/SourceFiles/data/data_histories.cpp | 18 +++++++++--------- .../controls/history_view_compose_controls.cpp | 2 +- .../media/stories/media_stories_controller.cpp | 2 ++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index d69b11ce4..4ca3cb635 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -35,15 +35,7 @@ constexpr auto kReadRequestTimeout = 3 * crl::time(1000); MTPInputReplyTo ReplyToForMTP( not_null owner, FullReplyTo replyTo) { - if (replyTo.msgId || replyTo.topicRootId) { - using Flag = MTPDinputReplyToMessage::Flag; - return MTP_inputReplyToMessage( - (replyTo.topicRootId - ? MTP_flags(Flag::f_top_msg_id) - : MTP_flags(0)), - MTP_int(replyTo.msgId ? replyTo.msgId : replyTo.topicRootId), - MTP_int(replyTo.topicRootId)); - } else if (replyTo.storyId) { + if (replyTo.storyId) { if (const auto peer = owner->peerLoaded(replyTo.storyId.peer)) { if (const auto user = peer->asUser()) { return MTP_inputReplyToStory( @@ -51,6 +43,14 @@ MTPInputReplyTo ReplyToForMTP( MTP_int(replyTo.storyId.story)); } } + } else if (replyTo.msgId || replyTo.topicRootId) { + using Flag = MTPDinputReplyToMessage::Flag; + return MTP_inputReplyToMessage( + (replyTo.topicRootId + ? MTP_flags(Flag::f_top_msg_id) + : MTP_flags(0)), + MTP_int(replyTo.msgId ? replyTo.msgId : replyTo.topicRootId), + MTP_int(replyTo.topicRootId)); } return MTPInputReplyTo(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 29f1736ae..23a57dc06 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -2274,7 +2274,7 @@ void ComposeControls::initVoiceRecordBar() { return std::nullopt; }(); if (error) { - _show->showBox(Ui::MakeInformBox(*error)); + _show->showToast(*error); return true; } else if (_showSlowmodeError && _showSlowmodeError()) { return true; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 650ef7af2..35d2f3709 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -421,6 +421,8 @@ void Controller::show( } stories.loadAround(storyId); + list.user->updateFull(); + if (_contentFaded) { togglePaused(true); } From 8b22f9dcacd5ab5101a1d869ea69cd0e15f56a04 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 May 2023 17:57:11 +0400 Subject: [PATCH 045/260] Better track paused story state. --- Telegram/SourceFiles/data/data_stories.cpp | 2 +- Telegram/SourceFiles/data/data_stories.h | 2 +- .../history_view_compose_controls.cpp | 19 ++- .../controls/history_view_compose_controls.h | 3 + .../stories/media_stories_controller.cpp | 118 +++++++++++------- .../media/stories/media_stories_controller.h | 10 ++ .../media/stories/media_stories_delegate.h | 1 + .../media/stories/media_stories_reply.cpp | 36 ++++-- .../media/stories/media_stories_reply.h | 4 +- .../SourceFiles/media/view/media_view.style | 1 + .../media/view/media_view_overlay_opengl.cpp | 7 +- .../media/view/media_view_overlay_opengl.h | 2 +- .../media/view/media_view_overlay_widget.cpp | 4 + .../media/view/media_view_overlay_widget.h | 1 + 14 files changed, 149 insertions(+), 61 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 82b6c91b3..dd6d80fbb 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -588,7 +588,7 @@ void Stories::loadAround(FullStoryId id) { } } -void Stories::markAsRead(FullStoryId id) { +void Stories::markAsRead(FullStoryId id, bool viewed) { const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) { return list.user->id; }); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 44da5e7e3..7a4546a0b 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -115,7 +115,7 @@ public: void resolve(FullStoryId id, Fn done); [[nodiscard]] bool isQuitPrevent(); - void markAsRead(FullStoryId id); + void markAsRead(FullStoryId id, bool viewed); private: [[nodiscard]] StoriesList parse(const MTPUserStories &stories); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 23a57dc06..45c07c9ee 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1135,6 +1135,10 @@ rpl::producer ComposeControls::focusedValue() const { | rpl::then(_focusChanges.events()); } +rpl::producer ComposeControls::tabbedPanelShownValue() const { + return _tabbedPanel ? _tabbedPanel->shownValue() : rpl::single(false); +} + rpl::producer<> ComposeControls::cancelRequests() const { return _cancelRequests.events(); } @@ -2013,9 +2017,12 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { updateControlsGeometry(_wrap->size()); }); + const auto hadFocus = Ui::InFocusChain(_field); if (!draft) { clearFieldText(0, fieldHistoryAction); - _field->setFocus(); + if (hadFocus) { + _field->setFocus(); + } _header->editMessage({}); _header->replyToMessage({}); _canReplaceMedia = false; @@ -2025,7 +2032,9 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { _textUpdateEvents = 0; setFieldText(draft->textWithTags, 0, fieldHistoryAction); - _field->setFocus(); + if (hadFocus) { + _field->setFocus(); + } draft->cursor.applyTo(_field); _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; if (_preview) { @@ -2253,11 +2262,13 @@ void ComposeControls::initVoiceRecordBar() { _voiceRecordBar->recordingStateChanges( ) | rpl::start_with_next([=](bool active) { if (active) { + _recording = true; changeFocusedControl(); } _field->setVisible(!active); if (!active) { changeFocusedControl(); + _recording = false; } }, _wrap->lifetime()); @@ -2938,6 +2949,10 @@ bool ComposeControls::isRecording() const { return _voiceRecordBar->isRecording(); } +rpl::producer ComposeControls::recordingValue() const { + return _recording.value(); +} + bool ComposeControls::preventsClose(Fn &&continueCallback) const { if (_voiceRecordBar->isActive()) { _voiceRecordBar->showDiscardBox(std::move(continueCallback)); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index d0696bef6..11b2efe22 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -146,6 +146,7 @@ public: bool focus(); [[nodiscard]] rpl::producer focusedValue() const; + [[nodiscard]] rpl::producer tabbedPanelShownValue() const; [[nodiscard]] rpl::producer<> cancelRequests() const; [[nodiscard]] rpl::producer sendRequests() const; [[nodiscard]] rpl::producer sendVoiceRequests() const; @@ -213,6 +214,7 @@ public: [[nodiscard]] rpl::producer lockShowStarts() const; [[nodiscard]] bool isLockPresent() const; [[nodiscard]] bool isRecording() const; + [[nodiscard]] rpl::producer recordingValue() const; void applyCloudDraft(); void applyDraft( @@ -379,6 +381,7 @@ private: rpl::event_stream> _attachRequests; rpl::event_stream _replyNextRequests; rpl::event_stream<> _focusRequests; + rpl::variable _recording; TextUpdateEvents _textUpdateEvents = TextUpdateEvents() | TextUpdateEvent::SaveDraft diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 35d2f3709..6921f7b80 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/power_save_blocker.h" +#include "base/qt_signal_producer.h" #include "chat_helpers/compose/compose_show.h" #include "data/data_changes.h" #include "data/data_file_origin.h" @@ -29,6 +30,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_widgets.h" #include "styles/style_boxes.h" // UserpicButton +#include + namespace Media::Stories { namespace { @@ -123,27 +126,52 @@ Controller::Controller(not_null delegate) , _replyArea(std::make_unique(this)) { initLayout(); - _replyArea->focusedValue( - ) | rpl::start_with_next([=](bool focused) { - if (focused) { + _replyArea->activeValue( + ) | rpl::start_with_next([=](bool active) { + if (active) { _captionFullView = nullptr; } - _contentFaded = focused; - _contentFadeAnimation.start( - [=] { _delegate->storiesRepaint(); }, - focused ? 0. : 1., - focused ? 1. : 0., - st::fadeWrapDuration); - if (_started) { - togglePaused(focused); - } + _replyActive = active; + updateContentFaded(); }, _lifetime); + _replyArea->focusedValue( + ) | rpl::start_with_next([=](bool focused) { + _replyFocused = focused; + }, _lifetime); + + _delegate->storiesLayerShown( + ) | rpl::start_with_next([=](bool shown) { + _layerShown = shown; + updatePlayingAllowed(); + }, _lifetime); + + const auto window = _wrap->window()->windowHandle(); + Assert(window != nullptr); + base::qt_signal_producer( + window, + &QWindow::activeChanged + ) | rpl::start_with_next([=] { + _windowActive = window->isActive(); + updatePlayingAllowed(); + }, _lifetime); + _windowActive = window->isActive(); + _contentFadeAnimation.stop(); } Controller::~Controller() = default; +void Controller::updateContentFaded() { + _contentFaded = _replyActive; + _contentFadeAnimation.start( + [=] { _delegate->storiesRepaint(); }, + _contentFaded ? 0. : 1., + _contentFaded ? 1. : 0., + st::fadeWrapDuration); + updatePlayingAllowed(); +} + void Controller::initLayout() { const auto headerHeight = st::storiesHeaderMargin.top() + st::storiesHeaderPhoto.photoSize @@ -373,6 +401,7 @@ void Controller::show( } const auto story = *maybeStory; const auto guard = gsl::finally([&] { + _paused = false; _started = false; if (story->photo()) { _photoPlayback = std::make_unique(this); @@ -421,10 +450,33 @@ void Controller::show( } stories.loadAround(storyId); - list.user->updateFull(); + if (_replyFocused) { + unfocusReply(); + } + updatePlayingAllowed(); - if (_contentFaded) { - togglePaused(true); + list.user->updateFull(); +} + +void Controller::updatePlayingAllowed() { + if (!_shown) { + return; + } + setPlayingAllowed(_started + && _windowActive + && !_paused + && !_replyActive + && !_layerShown); +} + +void Controller::setPlayingAllowed(bool allowed) { + if (allowed) { + _captionFullView = nullptr; + } + if (_photoPlayback) { + _photoPlayback->togglePaused(!allowed); + } else { + _delegate->storiesTogglePaused(!allowed); } } @@ -452,9 +504,7 @@ void Controller::ready() { return; } _started = true; - if (!_contentFaded && _photoPlayback) { - _photoPlayback->togglePaused(false); - } + updatePlayingAllowed(); } void Controller::updateVideoPlayback(const Player::TrackState &state) { @@ -493,7 +543,7 @@ void Controller::maybeMarkAsRead(const Player::TrackState &state) { void Controller::markAsRead() { Expects(_list.has_value()); - _list->user->owner().stories().markAsRead(_shown); + _list->user->owner().stories().markAsRead(_shown, _started); } bool Controller::subjumpAvailable(int delta) const { @@ -551,21 +601,11 @@ void Controller::checkWaitingFor() { Expects(_list.has_value()); auto &stories = _list->user->owner().stories(); - const auto &all = stories.all(); - const auto i = ranges::find_if(all, [&](const Data::StoriesList &data) { - return data.user->id == _waitingForId.peer; - }); - if (i == end(all)) { - _waitingForId = {}; - return; - } - const auto j = ranges::find(i->ids, _waitingForId.story); - if (j == end(i->ids)) { - _waitingForId = {}; - return; - } const auto maybe = stories.lookup(_waitingForId); if (!maybe) { + if (maybe.error() == Data::NoStory::Deleted) { + _waitingForId = {}; + } return; } _delegate->storiesJumpTo( @@ -596,19 +636,13 @@ bool Controller::jumpFor(int delta) { } bool Controller::paused() const { - return _photoPlayback - ? _photoPlayback->paused() - : _delegate->storiesPaused(); + return _paused; } void Controller::togglePaused(bool paused) { - if (!paused) { - _captionFullView = nullptr; - } - if (_photoPlayback) { - _photoPlayback->togglePaused(paused); - } else { - _delegate->storiesTogglePaused(paused); + if (_paused != paused) { + _paused = paused; + updatePlayingAllowed(); } } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index f381e51c1..47adde27e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -126,6 +126,10 @@ private: void maybeMarkAsRead(const Player::TrackState &state); void markAsRead(); + void updateContentFaded(); + void updatePlayingAllowed(); + void setPlayingAllowed(bool allowed); + void showSiblings( const std::vector &lists, int index); @@ -150,6 +154,12 @@ private: Ui::Animations::Simple _contentFadeAnimation; bool _contentFaded = false; + bool _windowActive = false; + bool _replyFocused = false; + bool _replyActive = false; + bool _layerShown = false; + bool _paused = false; + FullStoryId _shown; TextWithEntities _captionText; std::optional _list; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index a9c3b0a35..549788c84 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -44,6 +44,7 @@ public: FullStoryId id) = 0; virtual void storiesClose() = 0; [[nodiscard]] virtual bool storiesPaused() = 0; + [[nodiscard]] virtual rpl::producer storiesLayerShown() = 0; [[nodiscard]] virtual float64 storiesSiblingOver(SiblingType type) = 0; virtual void storiesTogglePaused(bool paused) = 0; virtual void storiesRepaint() = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 105f61def..92abe1133 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -299,11 +299,11 @@ Api::SendAction ReplyArea::prepareSendAction( void ReplyArea::chooseAttach( std::optional overrideSendImagesAsPhotos) { + _chooseAttachRequest = false; if (!_data.user) { return; } const auto user = not_null(_data.user); - _choosingAttach = false; if (const auto error = Data::AnyFileRestrictionError(user)) { _controller->uiShow()->showToast(*error); return; @@ -312,15 +312,18 @@ void ReplyArea::chooseAttach( const auto filter = (overrideSendImagesAsPhotos == true) ? FileDialog::ImagesOrAllFilter() : FileDialog::AllOrImagesFilter(); + const auto weak = make_weak(&_shownUserGuard); const auto callback = [=](FileDialog::OpenResult &&result) { - if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + const auto guard = gsl::finally([&] { + _choosingAttach = false; + }); + if (!weak + || (result.paths.isEmpty() && result.remoteContent.isEmpty())) { return; - } - - if (!result.remoteContent.isEmpty()) { + } else if (!result.remoteContent.isEmpty()) { auto read = Images::Read({ .content = result.remoteContent, - }); + }); if (!read.image.isNull() && !read.animated) { confirmSendingFiles( std::move(read.image), @@ -339,12 +342,14 @@ void ReplyArea::chooseAttach( confirmSendingFiles(std::move(list)); } }; + + _choosingAttach = true; FileDialog::GetOpenPaths( _controller->wrap().get(), tr::lng_choose_files(tr::now), filter, - crl::guard(&_shownUserGuard, callback), - nullptr); + crl::guard(this, callback), + crl::guard(this, [=] { _choosingAttach = false; })); } bool ReplyArea::confirmSendingFiles( @@ -488,9 +493,9 @@ void ReplyArea::initActions() { _controls->attachRequests( ) | rpl::filter([=] { - return !_choosingAttach; + return !_chooseAttachRequest; }) | rpl::start_with_next([=](std::optional overrideCompress) { - _choosingAttach = true; + _chooseAttachRequest = true; base::call_delayed( st::storiesAttach.ripple.hideDuration, this, @@ -569,6 +574,17 @@ rpl::producer ReplyArea::focusedValue() const { return _controls->focusedValue(); } +rpl::producer ReplyArea::activeValue() const { + using namespace rpl::mappers; + return rpl::combine( + _controls->focusedValue(), + _controls->recordingValue(), + _controls->tabbedPanelShownValue(), + _choosingAttach.value(), + _1 || _2 || _3 || _4 + ) | rpl::distinct_until_changed(); +} + void ReplyArea::showPremiumToast(not_null emoji) { // #TODO stories } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index d9683521e..d0cef32c3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -58,6 +58,7 @@ public: void show(ReplyAreaData data); [[nodiscard]] rpl::producer focusedValue() const; + [[nodiscard]] rpl::producer activeValue() const; private: using VoiceToSend = HistoryView::Controls::VoiceToSend; @@ -130,7 +131,8 @@ private: ReplyAreaData _data; base::has_weak_ptr _shownUserGuard; - bool _choosingAttach = false; + bool _chooseAttachRequest = false; + rpl::variable _choosingAttach; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index c822bcf0d..bded9e12e 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -599,6 +599,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { fadeLeft: icon {{ "fade_horizontal-flip_horizontal", storiesComposeBgOver }}; fadeRight: icon {{ "fade_horizontal", storiesComposeBgOver }}; field: InputField(defaultTabbedSearchField) { + textFg: storiesComposeWhiteText; placeholderFg: storiesComposeGrayText; placeholderFgActive: storiesComposeGrayText; placeholderFgError: storiesComposeGrayText; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index e2f2bff04..a1bdce930 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -431,9 +431,10 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( Ui::GL::kFormatRGBA, Ui::GL::kFormatRGBA, QSize(2, 2), - _rgbaSize, + _rgbaSize[index], stride, data); + _rgbaSize[index] = QSize(2, 2); } else { const auto stride = image.bytesPerLine() / 4; const auto data = image.constBits(); @@ -441,10 +442,10 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( Ui::GL::kFormatRGBA, Ui::GL::kFormatRGBA, image.size(), - _rgbaSize, + _rgbaSize[index], stride, data); - _rgbaSize = image.size(); + _rgbaSize[index] = image.size(); } } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index 95d57ec87..df6f2aa71 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -124,7 +124,7 @@ private: std::optional _controlsProgram; std::optional _roundedCornersProgram; Ui::GL::Textures<6> _textures; // image, sibling, right sibling, y, u, v - QSize _rgbaSize; + QSize _rgbaSize[3]; QSize _lumaSize; QSize _chromaSize; qint64 _cacheKeys[3] = { 0 }; // image, sibling, right sibling diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index f2efd4578..df290b574 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -4053,6 +4053,10 @@ bool OverlayWidget::storiesPaused() { && _streamed->instance.player().paused(); } +rpl::producer OverlayWidget::storiesLayerShown() { + return _layerBg->layerShownValue(); +} + void OverlayWidget::storiesTogglePaused(bool paused) { if (!_streamed || _streamed->instance.player().failed() diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 2bc572a91..a32c0ef59 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -247,6 +247,7 @@ private: FullStoryId id) override; void storiesClose() override; bool storiesPaused() override; + rpl::producer storiesLayerShown() override; void storiesTogglePaused(bool paused) override; float64 storiesSiblingOver(Stories::SiblingType type) override; void storiesRepaint() override; From e90642f3a0785f14f3e60a73531ccf8636e124ba Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 May 2023 18:41:02 +0400 Subject: [PATCH 046/260] Add bio privacy section. --- Telegram/Resources/langs/lang.strings | 9 +++++ .../settings/settings_privacy_controllers.cpp | 35 +++++++++++++++++++ .../settings/settings_privacy_controllers.h | 17 +++++++++ .../settings/settings_privacy_security.cpp | 5 +++ 4 files changed, 66 insertions(+) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7fa025d38..460d20418 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -584,6 +584,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_forwards_privacy" = "Forwarded messages"; "lng_settings_profile_photo_privacy" = "Profile photo"; "lng_settings_voices_privacy" = "Voice messages"; +"lng_settings_bio_privacy" = "Bio"; "lng_settings_privacy_premium" = "Only subscribers of {link} can restrict receiving voice messages."; "lng_settings_privacy_premium_link" = "Telegram Premium"; "lng_settings_passcode_disable" = "Disable Passcode"; @@ -999,6 +1000,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_groups_always_title" = "Always allow"; "lng_edit_privacy_groups_never_title" = "Never allow"; +"lng_edit_privacy_about_title" = "Bio privacy settings"; +"lng_edit_privacy_about_header" = "Who can see my bio"; +"lng_edit_privacy_about_always_empty" = "Always allow"; +"lng_edit_privacy_about_never_empty" = "Never allow"; +"lng_edit_privacy_about_exceptions" = "These users will or will not be able to see your profile bio regardless of the settings above."; +"lng_edit_privacy_about_always_title" = "Always allow"; +"lng_edit_privacy_about_never_title" = "Never allow"; + "lng_edit_privacy_calls_title" = "Voice calls privacy"; "lng_edit_privacy_calls_header" = "Who can call you"; "lng_edit_privacy_calls_always_empty" = "Always allow"; diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 9468c13f2..8f6b00900 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -1284,4 +1284,39 @@ auto VoicesPrivacyController::exceptionsDescription() const return tr::lng_edit_privacy_voices_exceptions(); } +UserPrivacy::Key AboutPrivacyController::key() const { + return Key::About; +} + +rpl::producer AboutPrivacyController::title() const { + return tr::lng_edit_privacy_about_title(); +} + +rpl::producer AboutPrivacyController::optionsTitleKey() const { + return tr::lng_edit_privacy_about_header(); +} + +rpl::producer AboutPrivacyController::exceptionButtonTextKey( + Exception exception) const { + switch (exception) { + case Exception::Always: return tr::lng_edit_privacy_about_always_empty(); + case Exception::Never: return tr::lng_edit_privacy_about_never_empty(); + } + Unexpected("Invalid exception value."); +} + +rpl::producer AboutPrivacyController::exceptionBoxTitle( + Exception exception) const { + switch (exception) { + case Exception::Always: return tr::lng_edit_privacy_about_always_title(); + case Exception::Never: return tr::lng_edit_privacy_about_never_title(); + } + Unexpected("Invalid exception value."); +} + +auto AboutPrivacyController::exceptionsDescription() const +-> rpl::producer { + return tr::lng_edit_privacy_about_exceptions(); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h index 3aa1e36d2..d6f57b9db 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h @@ -270,4 +270,21 @@ private: }; +class AboutPrivacyController final : public EditPrivacyController { +public: + using Option = EditPrivacyBox::Option; + using Exception = EditPrivacyBox::Exception; + + Key key() const override; + + rpl::producer title() const override; + rpl::producer optionsTitleKey() const override; + rpl::producer exceptionButtonTextKey( + Exception exception) const override; + rpl::producer exceptionBoxTitle( + Exception exception) const override; + rpl::producer exceptionsDescription() const override; + +}; + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index 164bb4443..7b8b3e73c 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -310,6 +310,11 @@ void SetupPrivacy( { &st::settingsPremiumIconVoice, kIconRed }, Key::Voices, [=] { return std::make_unique(session); }); + add( + tr::lng_settings_bio_privacy(), + { &st::settingsIconAccount, kIconDarkOrange }, + Key::About, + [] { return std::make_unique(); }); session->api().userPrivacy().reload(Api::UserPrivacy::Key::AddedByPhone); From d76c80bf0e3099b0d00c2725beeceb228b01f969 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 May 2023 21:12:15 +0400 Subject: [PATCH 047/260] Show recent viewers in self stories. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 3 + Telegram/SourceFiles/core/mime_type.cpp | 11 + Telegram/SourceFiles/core/mime_type.h | 1 + Telegram/SourceFiles/data/data_stories.cpp | 50 ++++- Telegram/SourceFiles/data/data_stories.h | 7 + .../view/history_view_replies_section.cpp | 13 +- .../view/history_view_scheduled_section.cpp | 16 +- .../stories/media_stories_controller.cpp | 20 +- .../media/stories/media_stories_controller.h | 3 + .../stories/media_stories_recent_views.cpp | 188 ++++++++++++++++++ .../stories/media_stories_recent_views.h | 53 +++++ .../media/stories/media_stories_reply.cpp | 11 +- .../media/stories/media_stories_reply.h | 6 - .../SourceFiles/media/view/media_view.style | 8 + 15 files changed, 342 insertions(+), 50 deletions(-) create mode 100644 Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_recent_views.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a32950631..c02353676 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -973,6 +973,8 @@ PRIVATE media/stories/media_stories_delegate.h media/stories/media_stories_header.cpp media/stories/media_stories_header.h + media/stories/media_stories_recent_views.cpp + media/stories/media_stories_recent_views.h media/stories/media_stories_reply.cpp media/stories/media_stories_reply.h media/stories/media_stories_sibling.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 460d20418..892983de5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3793,6 +3793,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_row_count#other" = "{count} Stories"; "lng_stories_row_unread_and_one" = "{accumulated}, {user}"; "lng_stories_row_unread_and_last" = "{accumulated} and {user}"; +"lng_stories_views#one" = "{count} view"; +"lng_stories_views#other" = "{count} views"; +"lng_stories_no_views" = "No views"; // Wnd specific diff --git a/Telegram/SourceFiles/core/mime_type.cpp b/Telegram/SourceFiles/core/mime_type.cpp index e75a5fc94..8ba1b6494 100644 --- a/Telegram/SourceFiles/core/mime_type.cpp +++ b/Telegram/SourceFiles/core/mime_type.cpp @@ -229,4 +229,15 @@ QList ReadMimeUrls(not_null data) { : QList(); } +bool CanSendFiles(not_null data) { + if (data->hasImage()) { + return true; + } else if (const auto urls = ReadMimeUrls(data); !urls.empty()) { + if (ranges::all_of(urls, &QUrl::isLocalFile)) { + return true; + } + } + return false; +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/mime_type.h b/Telegram/SourceFiles/core/mime_type.h index d5aaa9eb0..3271adafe 100644 --- a/Telegram/SourceFiles/core/mime_type.h +++ b/Telegram/SourceFiles/core/mime_type.h @@ -67,5 +67,6 @@ struct MimeImageData { [[nodiscard]] MimeImageData ReadMimeImage(not_null data); [[nodiscard]] QString ReadMimeText(not_null data); [[nodiscard]] QList ReadMimeUrls(not_null data); +[[nodiscard]] bool CanSendFiles(not_null data); } // namespace Core diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index dd6d80fbb..39c5ae437 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -36,10 +36,10 @@ constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); using UpdateFlag = StoryUpdate::Flag; std::optional ParseMedia( - not_null owner, - const MTPMessageMedia &media) { + not_null owner, + const MTPMessageMedia &media) { return media.match([&](const MTPDmessageMediaPhoto &data) - -> std::optional { + -> std::optional { if (const auto photo = data.vphoto()) { const auto result = owner->processPhoto(*photo); if (!result->isNull()) { @@ -48,7 +48,7 @@ std::optional ParseMedia( } return {}; }, [&](const MTPDmessageMediaDocument &data) - -> std::optional { + -> std::optional { if (const auto document = data.vdocument()) { const auto result = owner->processDocument(*document); if (!result->isNull() @@ -71,10 +71,10 @@ Story::Story( not_null peer, StoryMedia media, TimeId date) -: _id(id) -, _peer(peer) -, _media(std::move(media)) -, _date(date) { + : _id(id) + , _peer(peer) + , _media(std::move(media)) + , _date(date) { } Session &Story::owner() const { @@ -170,6 +170,21 @@ const TextWithEntities &Story::caption() const { return _caption; } +void Story::setViewsData( + std::vector> recent, + int total) { + _recentViewers = std::move(recent); + _views = total; +} + +const std::vector> &Story::recentViewers() const { + return _recentViewers; +} + +int Story::views() const { + return _views; +} + bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { const auto pinned = data.is_pinned(); auto caption = TextWithEntities{ @@ -178,15 +193,32 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { &owner().session(), data.ventities().value_or_empty()), }; + auto views = 0; + auto recent = std::vector>(); + if (const auto info = data.vviews()) { + views = info->data().vviews_count().v; + if (const auto list = info->data().vrecent_viewers()) { + recent.reserve(list->v.size()); + auto &owner = _peer->owner(); + for (const auto &id : list->v) { + recent.push_back(owner.peer(peerFromUser(id))); + } + } + } + const auto changed = (_media != media) || (_pinned != pinned) - || (_caption != caption); + || (_caption != caption) + || (_views != views) + || (_recentViewers != recent); if (!changed) { return false; } _media = std::move(media); _pinned = pinned; _caption = std::move(caption); + _views = views; + _recentViewers = std::move(recent); return true; } diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 7a4546a0b..d8d7dcee3 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -57,6 +57,11 @@ public: void setCaption(TextWithEntities &&caption); [[nodiscard]] const TextWithEntities &caption() const; + void setViewsData(std::vector> recent, int total); + [[nodiscard]] auto recentViewers() const + -> const std::vector> &; + [[nodiscard]] int views() const; + bool applyChanges(StoryMedia media, const MTPDstoryItem &data); private: @@ -64,6 +69,8 @@ private: const not_null _peer; StoryMedia _media; TextWithEntities _caption; + std::vector> _recentViewers; + int _views = 0; const TimeId _date = 0; bool _pinned = false; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index aeacfd251..0c1b268af 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -97,17 +97,6 @@ namespace { constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200); -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - rpl::producer RootViewContent( not_null history, MsgId rootId, @@ -819,7 +808,7 @@ void RepliesWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 66255b466..77922c6c1 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -65,20 +65,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace HistoryView { -namespace { - -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - -} // namespace object_ptr ScheduledMemento::createWidget( QWidget *parent, @@ -308,7 +294,7 @@ void ScheduledWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 6921f7b80..6d11fd247 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_header.h" #include "media/stories/media_stories_sibling.h" #include "media/stories/media_stories_slider.h" +#include "media/stories/media_stories_recent_views.h" #include "media/stories/media_stories_reply.h" #include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" @@ -123,7 +124,8 @@ Controller::Controller(not_null delegate) , _wrap(_delegate->storiesWrap()) , _header(std::make_unique
(this)) , _slider(std::make_unique(this)) -, _replyArea(std::make_unique(this)) { +, _replyArea(std::make_unique(this)) +, _recentViews(std::make_unique(this)) { initLayout(); _replyArea->activeValue( @@ -249,6 +251,9 @@ void Controller::initLayout() { + layout.content.height() + fieldMinHeight - st::storiesFieldMargin.bottom())); + layout.views = QRect( + layout.controlsBottomPosition - QPoint(0, fieldMinHeight), + QSize(layout.controlsWidth, fieldMinHeight)); layout.autocompleteRect = QRect( layout.controlsBottomPosition.x(), 0, @@ -422,9 +427,18 @@ void Controller::show( _captionText = story->caption(); _captionFullView = nullptr; + if (_replyFocused) { + unfocusReply(); + } + _header->show({ .user = list.user, .date = story->date() }); _slider->show({ .index = _index, .total = list.total }); _replyArea->show({ .user = list.user, .id = id }); + _recentViews->show({ + .list = story->recentViewers(), + .total = story->views(), + .valid = list.user->isSelf(), + }); const auto session = &list.user->session(); if (_session != session) { @@ -450,11 +464,7 @@ void Controller::show( } stories.loadAround(storyId); - if (_replyFocused) { - unfocusReply(); - } updatePlayingAllowed(); - list.user->updateFull(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 47adde27e..eef7cc1a3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -41,6 +41,7 @@ namespace Media::Stories { class Header; class Slider; class ReplyArea; +class RecentViews; class Sibling; class Delegate; struct SiblingView; @@ -68,6 +69,7 @@ struct Layout { QRect slider; int controlsWidth = 0; QPoint controlsBottomPosition; + QRect views; QRect autocompleteRect; HeaderLayout headerLayout = HeaderLayout::Normal; SiblingLayout siblingLeft; @@ -148,6 +150,7 @@ private: const std::unique_ptr
_header; const std::unique_ptr _slider; const std::unique_ptr _replyArea; + const std::unique_ptr _recentViews; std::unique_ptr _photoPlayback; std::unique_ptr _captionFullView; diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp new file mode 100644 index 000000000..9148e6b6f --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -0,0 +1,188 @@ +/* +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/stories/media_stories_recent_views.h" + +#include "data/data_peer.h" +#include "main/main_session.h" +#include "media/stories/media_stories_controller.h" +#include "lang/lang_keys.h" +#include "ui/chat/group_call_userpics.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "ui/userpic_view.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_media_view.h" + +namespace Media::Stories { +namespace { + +[[nodiscard]] rpl::producer> ContentByUsers( + const std::vector> &list) { + struct Userpic { + not_null peer; + mutable Ui::PeerUserpicView view; + mutable InMemoryKey uniqueKey; + }; + + struct State { + std::vector userpics; + std::vector current; + base::has_weak_ptr guard; + bool someUserpicsNotLoaded = false; + bool scheduled = false; + }; + + static const auto size = st::storiesRecentViewsUserpics.size; + + static const auto GenerateUserpic = [](Userpic &userpic) { + auto result = userpic.peer->generateUserpicImage( + userpic.view, + size * style::DevicePixelRatio()); + result.setDevicePixelRatio(style::DevicePixelRatio()); + return result; + }; + + static const auto RegenerateUserpics = [](not_null state) { + Expects(state->userpics.size() == state->current.size()); + + state->someUserpicsNotLoaded = false; + const auto count = int(state->userpics.size()); + for (auto i = 0; i != count; ++i) { + auto &userpic = state->userpics[i]; + auto &participant = state->current[i]; + const auto peer = userpic.peer; + const auto key = peer->userpicUniqueKey(userpic.view); + if (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) { + state->someUserpicsNotLoaded = true; + } + if (userpic.uniqueKey == key) { + continue; + } + participant.userpicKey = userpic.uniqueKey = key; + participant.userpic = GenerateUserpic(userpic); + } + }; + + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + const auto state = lifetime.make_state(); + const auto pushNext = [=] { + RegenerateUserpics(state); + consumer.put_next_copy(state->current); + }; + + for (const auto &peer : list) { + state->userpics.push_back(Userpic{ + .peer = peer, + }); + state->current.push_back(Ui::GroupCallUser{ + .id = uint64(peer->id.value), + }); + peer->loadUserpic(); + } + pushNext(); + + if (!list.empty()) { + list.front()->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return state->someUserpicsNotLoaded && !state->scheduled; + }) | rpl::start_with_next([=] { + for (const auto &userpic : state->userpics) { + if (userpic.peer->userpicUniqueKey(userpic.view) + != userpic.uniqueKey) { + state->scheduled = true; + crl::on_main(&state->guard, [=] { + state->scheduled = false; + pushNext(); + }); + return; + } + } + }, lifetime); + } + return lifetime; + }; +} + +} // namespace + +RecentViews::RecentViews(not_null controller) +: _controller(controller) { +} + +RecentViews::~RecentViews() = default; + +void RecentViews::show(RecentViewsData data) { + if (_data == data) { + return; + } + const auto totalChanged = _text.isEmpty() || (_data.total != data.total); + const auto usersChanged = !_userpics || (_data.list != data.list); + _data = data; + if (!_data.valid) { + _text = {}; + _userpics = nullptr; + _widget = nullptr; + return; + } + if (!_widget) { + const auto parent = _controller->wrap(); + auto widget = std::make_unique(parent); + const auto raw = widget.get(); + raw->show(); + + _controller->layoutValue( + ) | rpl::start_with_next([=](const Layout &layout) { + raw->setGeometry(layout.views); + }, raw->lifetime()); + + raw->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = Painter(raw); + const auto skip = st::storiesRecentViewsSkip; + const auto full = _userpicsWidth + skip + _text.maxWidth(); + const auto use = std::min(full, raw->width()); + const auto ux = (raw->width() - use) / 2; + const auto height = st::storiesRecentViewsUserpics.size; + const auto uy = (raw->height() - height) / 2; + const auto tx = ux + _userpicsWidth + skip; + const auto ty = (raw->height() - st::normalFont->height) / 2; + _userpics->paint(p, ux, uy, height); + p.setPen(st::storiesComposeWhiteText); + _text.drawElided(p, tx, ty, use - _userpicsWidth - skip); + }, raw->lifetime()); + + _widget = std::move(widget); + } + if (totalChanged) { + _text.setText(st::defaultTextStyle, data.total + ? tr::lng_stories_views(tr::now, lt_count, data.total) + : tr::lng_stories_no_views(tr::now)); + } + if (!_userpics) { + _userpics = std::make_unique( + st::storiesRecentViewsUserpics, + rpl::single(true), + [=] { _widget->update(); }); + + _userpics->widthValue() | rpl::start_with_next([=](int width) { + _userpicsWidth = width; + }, _widget->lifetime()); + } + if (usersChanged) { + _userpicsLifetime = ContentByUsers( + data.list + ) | rpl::start_with_next([=]( + const std::vector &list) { + _userpics->update(list, true); + }); + } +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h new file mode 100644 index 000000000..64a6d4e82 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h @@ -0,0 +1,53 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/text/text.h" + +namespace Ui { +class RpWidget; +class GroupCallUserpics; +} // namespace Ui + +namespace Media::Stories { + +class Controller; + +struct RecentViewsData { + std::vector> list; + int total = 0; + bool valid = false; + + friend inline auto operator<=>( + const RecentViewsData &, + const RecentViewsData &) = default; + friend inline bool operator==( + const RecentViewsData &, + const RecentViewsData &) = default; +}; + +class RecentViews final { +public: + explicit RecentViews(not_null controller); + ~RecentViews(); + + void show(RecentViewsData data); + +private: + const not_null _controller; + + std::unique_ptr _widget; + std::unique_ptr _userpics; + Ui::Text::String _text; + RecentViewsData _data; + rpl::lifetime _userpicsLifetime; + int _userpicsWidth = 0; + +}; + +} // namespace Media::Stories \ No newline at end of file diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 92abe1133..cbff23d73 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -524,12 +524,12 @@ void ReplyArea::initActions() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return false;// checkSendingFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { - return false;/* confirmSendingFiles( + return confirmSendingFiles( data, std::nullopt, - Core::ReadMimeText(data));*/ + Core::ReadMimeText(data)); } Unexpected("action in MimeData hook."); }); @@ -562,6 +562,11 @@ void ReplyArea::show(ReplyAreaData data) { .history = history, }); _controls->clear(); + if (!user || user->isSelf()) { + _controls->hide(); + } else { + _controls->show(); + } } Main::Session &ReplyArea::session() const { diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index d0cef32c3..d50496d5d 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -66,18 +66,12 @@ private: [[nodiscard]] Main::Session &session() const; [[nodiscard]] not_null history() const; - bool confirmSendingFiles(const QStringList &files); - bool confirmSendingFiles(not_null data); - void uploadFile(const QByteArray &fileContent, SendMediaType type); bool confirmSendingFiles( QImage &&image, QByteArray &&content, std::optional overrideSendImagesAsPhotos = std::nullopt, const QString &insertTextOnCancel = QString()); - bool confirmSendingFiles( - const QStringList &files, - const QString &insertTextOnCancel); bool confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel = QString()); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index bded9e12e..2a3c3f7b2 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -729,3 +729,11 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } premium: storiesComposePremium; } +storiesRecentViewsUserpics: GroupCallUserpics { + size: 24px; + shift: 9px; + stroke: 4px; + align: align(left); +} +storiesRecentViewsSkip: 8px; + From 1f1e543df7ef05e2c9c63815ca358f41dfb1482b Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 May 2023 10:39:45 +0400 Subject: [PATCH 048/260] Fix good thumbnail generation in sibling stories. --- .../media/stories/media_stories_sibling.cpp | 55 ++++++++++++------- .../streaming/media_streaming_document.cpp | 5 +- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index ac3c43258..90a393954 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -75,6 +75,7 @@ public: private: void waitForGoodThumbnail(); bool updateAfterGoodCheck(); + void createStreamedPlayer(); void streamedFailed(); const not_null _video; @@ -148,38 +149,49 @@ QImage Sibling::LoaderVideo::blurred() { QImage Sibling::LoaderVideo::good() { if (const auto image = _media->goodThumbnail()) { return image->original(); - } else if (!_video->goodThumbnailChecked()) { + } else if (!_video->goodThumbnailChecked() + && !_video->goodThumbnailNoData()) { if (!_checkingGoodInCache) { waitForGoodThumbnail(); } } else if (_failed) { return QImage(); } else if (!_streamed) { - _streamed = std::make_unique( - _video, - _origin, - [] {}); // waitingCallback - _streamed->lockPlayer(); - _streamed->player().updates( - ) | rpl::start_with_next_error([=](Streaming::Update &&update) { - v::match(update.data, [&](Streaming::Information &update) { - _update(); - }, [](const auto &update) { - }); - }, [=](Streaming::Error &&error) { - streamedFailed(); - }, _streamed->lifetime()); - if (_streamed->ready()) { - _update(); - } else if (!_streamed->valid()) { - streamedFailed(); - } + createStreamedPlayer(); } else if (_streamed->ready()) { return _streamed->info().video.cover; } return QImage(); } +void Sibling::LoaderVideo::createStreamedPlayer() { + _streamed = std::make_unique( + _video, + _origin, + [] {}); // waitingCallback + _streamed->lockPlayer(); + _streamed->player().updates( + ) | rpl::start_with_next_error([=](Streaming::Update &&update) { + v::match(update.data, [&](Streaming::Information &update) { + _update(); + }, [](const auto &update) { + }); + }, [=](Streaming::Error &&error) { + streamedFailed(); + }, _streamed->lifetime()); + if (_streamed->ready()) { + _update(); + } else if (!_streamed->valid()) { + streamedFailed(); + } else if (!_streamed->player().active() + && !_streamed->player().finished()) { + _streamed->play({ + .mode = Streaming::Mode::Video, + }); + _streamed->pause(); + } +} + void Sibling::LoaderVideo::streamedFailed() { _failed = true; _streamed = nullptr; @@ -204,7 +216,8 @@ void Sibling::LoaderVideo::waitForGoodThumbnail() { } bool Sibling::LoaderVideo::updateAfterGoodCheck() { - if (!_video->goodThumbnailChecked()) { + if (!_video->goodThumbnailChecked() + && !_video->goodThumbnailNoData()) { return false; } _checkingGoodInCache = false; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp index 039b76261..27c090443 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp @@ -257,12 +257,15 @@ void Document::validateGoodThumbnail() { const auto length = bytes.size(); if (!length || length > Storage::kMaxFileInMemory) { LOG(("App Error: Bad thumbnail data for saving to cache.")); - bytes = "(failed)"; + bytes = "(failed)"_q; } crl::on_main(guard, [=] { if (const auto active = document->activeMediaView()) { active->setGoodThumbnail(image); } + if (bytes != "(failed)"_q) { + document->setGoodThumbnailChecked(true); + } document->owner().cache().putIfEmpty( document->goodThumbnailCacheKey(), Storage::Cache::Database::TaggedValue( From 16069db3e6c5235333ddea15fd6277b43d143c0a Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 31 May 2023 21:53:17 +0400 Subject: [PATCH 049/260] Fix build with Xcode. --- Telegram/SourceFiles/data/data_stories.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 39c5ae437..fa2c61664 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -597,8 +597,8 @@ void Stories::loadAround(FullStoryId id) { } const auto ignore = [&] { const auto side = kIgnorePreloadAroundIfLoaded; - const auto left = ranges::min(j - begin(i->ids), side); - const auto right = ranges::min(end(i->ids) - j, side); + const auto left = ranges::min(int(j - begin(i->ids)), side); + const auto right = ranges::min(int(end(i->ids) - j), side); for (auto k = j - left; k != j + right; ++k) { const auto maybeStory = lookup({ id.peer, *k }); if (!maybeStory && maybeStory.error() == NoStory::Unknown) { @@ -611,8 +611,8 @@ void Stories::loadAround(FullStoryId id) { return; } const auto side = kPreloadAroundCount; - const auto left = ranges::min(j - begin(i->ids), side); - const auto right = ranges::min(end(i->ids) - j, side); + const auto left = ranges::min(int(j - begin(i->ids)), side); + const auto right = ranges::min(int(end(i->ids) - j), side); const auto from = j - left; const auto till = j + right; for (auto k = from; k != till; ++k) { From d28bd36d223736ca82b6e77cc41a5928390dbf1f Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Jun 2023 20:01:29 +0400 Subject: [PATCH 050/260] Load and show list of users who viewed a story. --- Telegram/SourceFiles/api/api_who_reacted.cpp | 63 ++-- Telegram/SourceFiles/api/api_who_reacted.h | 1 + Telegram/SourceFiles/calls/calls_top_bar.cpp | 2 +- .../chat_helpers/chat_helpers.style | 43 +++ Telegram/SourceFiles/data/data_stories.cpp | 88 +++++ Telegram/SourceFiles/data/data_stories.h | 24 ++ .../SourceFiles/dialogs/dialogs_widget.cpp | 1 + .../admin_log/history_admin_log_section.cpp | 1 + .../controls/history_view_compose_search.cpp | 1 + .../controls/history_view_forward_panel.cpp | 1 + .../view/controls/history_view_ttl_button.cpp | 1 + .../history_view_voice_record_button.cpp | 1 + .../view/history_view_contact_status.cpp | 1 + .../view/history_view_context_menu.cpp | 1 + .../view/history_view_corner_buttons.cpp | 1 + .../view/history_view_group_call_bar.cpp | 1 + .../history/view/history_view_message.cpp | 1 + .../history/view/history_view_pinned_bar.cpp | 1 + .../view/history_view_pinned_section.cpp | 1 + .../view/history_view_replies_section.cpp | 1 + .../view/history_view_scheduled_section.cpp | 1 + .../view/history_view_translate_bar.cpp | 2 +- .../view/reactions/history_view_reactions.cpp | 1 + .../info/profile/info_profile_phone_menu.cpp | 1 + .../stories/media_stories_controller.cpp | 92 ++++- .../media/stories/media_stories_controller.h | 17 + .../stories/media_stories_recent_views.cpp | 346 +++++++++++++++--- .../stories/media_stories_recent_views.h | 46 +++ .../SourceFiles/media/view/media_view.style | 22 +- .../win/notifications_manager_win.cpp | 1 + .../settings/settings_privacy_controllers.cpp | 1 + Telegram/SourceFiles/ui/chat/chat.style | 41 --- .../SourceFiles/ui/chat/group_call_bar.cpp | 1 + .../ui/chat/group_call_userpics.cpp | 1 + .../SourceFiles/ui/chat/more_chats_bar.cpp | 2 +- .../ui/controls/invite_link_buttons.cpp | 2 +- .../SourceFiles/ui/controls/silent_toggle.cpp | 1 + .../controls/who_reacted_context_action.cpp | 117 +++--- .../ui/controls/who_reacted_context_action.h | 54 ++- 39 files changed, 784 insertions(+), 200 deletions(-) diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 6fc8800b9..f32358c06 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/who_reacted_context_action.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Api { namespace { @@ -357,37 +358,6 @@ struct State { }); } -[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now) { - if (!date) { - return {}; - } - const auto parsed = base::unixtime::parse(date); - const auto readDate = parsed.date(); - const auto nowDate = now.date(); - if (readDate == nowDate) { - return tr::lng_mediaview_today( - tr::now, - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); - } else if (readDate.addDays(1) == nowDate) { - return tr::lng_mediaview_yesterday( - tr::now, - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); - } - return tr::lng_mediaview_date_time( - tr::now, - lt_date, - tr::lng_month_day( - tr::now, - lt_month, - Lang::MonthDay(readDate.month())(tr::now), - lt_day, - QString::number(readDate.day())), - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); -} - bool UpdateUserpics( not_null state, not_null item, @@ -614,6 +584,37 @@ rpl::producer WhoReacted( } // namespace +QString FormatReadDate(TimeId date, const QDateTime &now) { + if (!date) { + return {}; + } + const auto parsed = base::unixtime::parse(date); + const auto readDate = parsed.date(); + const auto nowDate = now.date(); + if (readDate == nowDate) { + return tr::lng_mediaview_today( + tr::now, + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); + } else if (readDate.addDays(1) == nowDate) { + return tr::lng_mediaview_yesterday( + tr::now, + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); + } + return tr::lng_mediaview_date_time( + tr::now, + lt_date, + tr::lng_month_day( + tr::now, + lt_month, + Lang::MonthDay(readDate.month())(tr::now), + lt_day, + QString::number(readDate.day())), + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); +} + bool WhoReadExists(not_null item) { if (!item->out()) { return false; diff --git a/Telegram/SourceFiles/api/api_who_reacted.h b/Telegram/SourceFiles/api/api_who_reacted.h index 3fdcd8a56..9a9100535 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -29,6 +29,7 @@ enum class WhoReactedList { One, }; +[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now); [[nodiscard]] bool WhoReadExists(not_null item); [[nodiscard]] bool WhoReactedExists( not_null item, diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index e43af9373..f06f78cfd 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -34,7 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" #include "base/timer.h" #include "styles/style_calls.h" -#include "styles/style_chat.h" // style::GroupCallUserpics +#include "styles/style_chat_helpers.h" // style::GroupCallUserpics #include "styles/style_layers.h" namespace Calls { diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 8ad939619..5b16daf5d 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -201,6 +201,31 @@ ComposeControls { premium: PremiumLimits; } +WhoRead { + userpics: GroupCallUserpics; + photoLeft: pixels; + photoSize: pixels; + photoSkip: pixels; + nameLeft: pixels; + iconPosition: point; + itemPadding: margins; +} + +defaultWhoRead: WhoRead { + userpics: GroupCallUserpics { + size: 22px; + shift: 8px; + stroke: 4px; + align: align(right); + } + photoLeft: 13px; + photoSize: 30px; + photoSkip: 5px; + nameLeft: 57px; + iconPosition: point(15px, 7px); + itemPadding: margins(44px, 9px, 17px, 7px); +} + switchPmButton: RoundButton(defaultBoxButton) { width: 320px; height: 34px; @@ -1091,3 +1116,21 @@ defaultComposeControls: ComposeControls { files: defaultComposeFiles; premium: defaultPremiumLimits; } + +moreChatsBarHeight: 48px; +moreChatsBarTextPosition: point(12px, 4px); +moreChatsBarStatusPosition: point(12px, 24px); +moreChatsBarClose: IconButton(defaultIconButton) { + width: 48px; + height: 48px; + + icon: boxTitleCloseIcon; + iconOver: boxTitleCloseIconOver; + iconPosition: point(12px, -1px); + + rippleAreaPosition: point(0px, 4px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index fa2c61664..5cc08e8b5 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -181,10 +181,48 @@ const std::vector> &Story::recentViewers() const { return _recentViewers; } +const std::vector &Story::viewsList() const { + return _viewsList; +} + int Story::views() const { return _views; } +void Story::applyViewsSlice( + const std::optional &offset, + const std::vector &slice, + int total) { + _views = total; + if (!offset) { + const auto i = _viewsList.empty() + ? end(slice) + : ranges::find(slice, _viewsList.front()); + const auto merge = (i != end(slice)) + && !ranges::contains(slice, _viewsList.back()); + if (merge) { + _viewsList.insert(begin(_viewsList), begin(slice), i); + } else { + _viewsList = slice; + } + } else if (!slice.empty()) { + const auto i = ranges::find(_viewsList, *offset); + const auto merge = (i != end(_viewsList)) + && !ranges::contains(_viewsList, slice.back()); + if (merge) { + const auto after = i + 1; + if (after == end(_viewsList)) { + _viewsList.insert(after, begin(slice), end(slice)); + } else { + const auto j = ranges::find(slice, _viewsList.back()); + if (j != end(slice)) { + _viewsList.insert(end(_viewsList), j + 1, end(slice)); + } + } + } + } +} + bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { const auto pinned = data.is_pinned(); auto caption = TextWithEntities{ @@ -683,6 +721,56 @@ void Stories::sendMarkAsReadRequests() { } } +void Stories::loadViewsSlice( + StoryId id, + std::optional offset, + Fn)> done) { + _viewsDone = std::move(done); + if (_viewsStoryId == id && _viewsOffset == offset) { + return; + } + _viewsStoryId = id; + _viewsOffset = offset; + + const auto api = &_owner->session().api(); + api->request(_viewsRequestId).cancel(); + _viewsRequestId = api->request(MTPstories_GetStoryViewsList( + MTP_int(id), + MTP_int(offset ? offset->date : 0), + MTP_long(offset ? peerToUser(offset->peer->id).bare : 0), + MTP_int(2) + )).done([=](const MTPstories_StoryViewsList &result) { + _viewsRequestId = 0; + + auto slice = std::vector(); + + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + slice.reserve(data.vviews().v.size()); + for (const auto &view : data.vviews().v) { + slice.push_back({ + .peer = _owner->peer(peerFromUser(view.data().vuser_id())), + .date = view.data().vdate().v, + }); + } + const auto fullId = FullStoryId{ + .peer = _owner->session().userPeerId(), + .story = _viewsStoryId, + }; + if (const auto story = lookup(fullId)) { + (*story)->applyViewsSlice(_viewsOffset, slice, data.vcount().v); + } + if (const auto done = base::take(_viewsDone)) { + done(std::move(slice)); + } + }).fail([=] { + _viewsRequestId = 0; + if (const auto done = base::take(_viewsDone)) { + done({}); + } + }).send(); +} + bool Stories::isQuitPrevent() { if (!_markReadPending.empty()) { sendMarkAsReadRequests(); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index d8d7dcee3..b4a53f859 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -28,6 +28,13 @@ struct StoryMedia { friend inline bool operator==(StoryMedia, StoryMedia) = default; }; +struct StoryView { + not_null peer; + TimeId date = 0; + + friend inline bool operator==(StoryView, StoryView) = default; +}; + class Story { public: Story( @@ -60,7 +67,12 @@ public: void setViewsData(std::vector> recent, int total); [[nodiscard]] auto recentViewers() const -> const std::vector> &; + [[nodiscard]] const std::vector &viewsList() const; [[nodiscard]] int views() const; + void applyViewsSlice( + const std::optional &offset, + const std::vector &slice, + int total); bool applyChanges(StoryMedia media, const MTPDstoryItem &data); @@ -70,6 +82,7 @@ private: StoryMedia _media; TextWithEntities _caption; std::vector> _recentViewers; + std::vector _viewsList; int _views = 0; const TimeId _date = 0; bool _pinned = false; @@ -124,6 +137,12 @@ public: [[nodiscard]] bool isQuitPrevent(); void markAsRead(FullStoryId id, bool viewed); + static constexpr auto kViewsPerPage = 50; + void loadViewsSlice( + StoryId id, + std::optional offset, + Fn)> done); + private: [[nodiscard]] StoriesList parse(const MTPUserStories &stories); [[nodiscard]] Story *parseAndApply( @@ -172,6 +191,11 @@ private: base::Timer _markReadTimer; base::flat_set _markReadRequests; + StoryId _viewsStoryId = 0; + std::optional _viewsOffset; + Fn)> _viewsDone; + mtpRequestId _viewsRequestId = 0; + }; } // namespace Data diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 24ff876e9..008102df0 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -67,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_memento.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_info.h" #include "styles/style_window.h" #include "base/qt/qt_common_adapters.h" diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp index fb504a8b3..a64f98f47 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp index 45ba94da2..0786b0745 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_info.h" 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 30e3b26e0..b00624867 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Controls { namespace { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp index f0b2eda2f..945253ec6 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_ttl_validator.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "styles/style_chat_helpers.h" #include "styles/style_chat.h" namespace HistoryView::Controls { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index 9bed92be9..4dbc3cd10 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/paint/blobs.h" #include "ui/painter.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" namespace HistoryView::Controls { diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 6f6e14d4a..dffcdde95 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/peers/edit_contact_box.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_info.h" #include "styles/style_menu_icons.h" diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 14f211bac..99ebf2929 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/spellcheck_types.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" #include diff --git a/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp b/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp index 5bd763c48..0ea99bc50 100644 --- a/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp +++ b/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/toast/toast.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp index bce9881b4..7fc19532a 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_instance.h" #include "core/application.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 18f699b21..e2892d105 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp index 7ec3b365c..037819498 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 0fc999bec..2a5cd65be 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_specific.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 0c1b268af..fc94a905c 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -85,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 77922c6c1..998909301 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/inline_bot_result.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index 456e5cd87..4207c59ca 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -284,7 +284,7 @@ void TranslateBar::setup(not_null history) { button->paintRequest( ) | rpl::start_with_next([=](QRect clip) { - QPainter(button).fillRect(clip, st::historyComposeButton.bgColor); + QPainter(button).fillRect(clip, st::historyComposeButtonBg); }, button->lifetime()); button->setClickedCallback([=] { diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 9160e2703..911472560 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Reactions { namespace { diff --git a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp index 0fea4cdb6..54bf15735 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/menu/menu_action.h" #include "ui/widgets/popup_menu.h" #include "styles/style_chat.h" // expandedMenuSeparator. +#include "styles/style_chat_helpers.h" namespace Info { namespace Profile { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 6d11fd247..3fc642b0a 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -426,7 +426,7 @@ void Controller::show( _shown = storyId; _captionText = story->caption(); _captionFullView = nullptr; - + invalidate_weak_ptrs(&_viewsLoadGuard); if (_replyFocused) { unfocusReply(); } @@ -680,6 +680,96 @@ SiblingView Controller::sibling(SiblingType type) const { return {}; } +ViewsSlice Controller::views(PeerId offset) { + invalidate_weak_ptrs(&_viewsLoadGuard); + if (!offset) { + refreshViewsFromData(); + } else if (!sliceViewsTo(offset)) { + return { .left = _viewsSlice.left }; + } + return _viewsSlice; +} + +rpl::producer<> Controller::moreViewsLoaded() const { + return _moreViewsLoaded.events(); +} + +Fn)> Controller::viewsGotMoreCallback() { + return crl::guard(&_viewsLoadGuard, [=]( + const std::vector &result) { + if (_viewsSlice.list.empty()) { + auto &stories = _list->user->owner().stories(); + if (const auto maybeStory = stories.lookup(_shown)) { + _viewsSlice = { + .list = result, + .left = (*maybeStory)->views() - int(result.size()), + }; + } else { + _viewsSlice = {}; + } + } else { + _viewsSlice.list.insert( + end(_viewsSlice.list), + begin(result), + end(result)); + _viewsSlice.left + = std::max(_viewsSlice.left - int(result.size()), 0); + } + _moreViewsLoaded.fire({}); + }); +} + +void Controller::refreshViewsFromData() { + Expects(_list.has_value()); + + auto &stories = _list->user->owner().stories(); + const auto maybeStory = stories.lookup(_shown); + if (!maybeStory || !_list->user->isSelf()) { + _viewsSlice = {}; + return; + } + const auto story = *maybeStory; + const auto &list = story->viewsList(); + const auto total = story->views(); + _viewsSlice.list = list + | ranges::views::take(Data::Stories::kViewsPerPage) + | ranges::to_vector; + _viewsSlice.left = total - int(_viewsSlice.list.size()); + if (_viewsSlice.list.empty() && _viewsSlice.left > 0) { + const auto done = viewsGotMoreCallback(); + stories.loadViewsSlice(_shown.story, std::nullopt, done); + } +} + +bool Controller::sliceViewsTo(PeerId offset) { + Expects(_list.has_value()); + + auto &stories = _list->user->owner().stories(); + const auto maybeStory = stories.lookup(_shown); + if (!maybeStory || !_list->user->isSelf()) { + _viewsSlice = {}; + return true; + } + const auto story = *maybeStory; + const auto &list = story->viewsList(); + const auto proj = [&](const Data::StoryView &single) { + return single.peer->id; + }; + const auto i = ranges::find(list, _viewsSlice.list.back()); + const auto add = (i != end(list)) ? int(end(list) - i - 1) : 0; + const auto j = ranges::find(_viewsSlice.list, offset, proj); + Assert(j != end(_viewsSlice.list)); + if (!add && (j + 1) == end(_viewsSlice.list)) { + const auto done = viewsGotMoreCallback(); + stories.loadViewsSlice(_shown.story, _viewsSlice.list.back(), done); + return false; + } + _viewsSlice.list.erase(begin(_viewsSlice.list), j + 1); + _viewsSlice.list.insert(end(_viewsSlice.list), i + 1, end(list)); + _viewsSlice.left -= add; + return true; +} + void Controller::unfocusReply() { _wrap->setFocus(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index eef7cc1a3..161a6971e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -78,6 +78,11 @@ struct Layout { friend inline bool operator==(Layout, Layout) = default; }; +struct ViewsSlice { + std::vector list; + int left = 0; +}; + class Controller final { public: explicit Controller(not_null delegate); @@ -114,6 +119,9 @@ public: void repaintSibling(not_null sibling); [[nodiscard]] SiblingView sibling(SiblingType type) const; + [[nodiscard]] ViewsSlice views(PeerId offset); + [[nodiscard]] rpl::producer<> moreViewsLoaded() const; + void unfocusReply(); [[nodiscard]] rpl::lifetime &lifetime(); @@ -142,6 +150,11 @@ private: void subjumpTo(int index); void checkWaitingFor(); + void refreshViewsFromData(); + bool sliceViewsTo(PeerId offset); + [[nodiscard]] auto viewsGotMoreCallback() + -> Fn)>; + const not_null _delegate; rpl::variable> _layout; @@ -170,6 +183,10 @@ private: int _index = 0; bool _started = false; + ViewsSlice _viewsSlice; + rpl::event_stream<> _moreViewsLoaded; + base::has_weak_ptr _viewsLoadGuard; + std::unique_ptr _siblingLeft; std::unique_ptr _siblingRight; diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp index 9148e6b6f..3703d474f 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -7,11 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_recent_views.h" +#include "api/api_who_reacted.h" // FormatReadDate. #include "data/data_peer.h" +#include "data/data_stories.h" #include "main/main_session.h" #include "media/stories/media_stories_controller.h" #include "lang/lang_keys.h" #include "ui/chat/group_call_userpics.h" +#include "ui/widgets/popup_menu.h" +#include "ui/controls/who_reacted_context_action.h" #include "ui/painter.h" #include "ui/rp_widget.h" #include "ui/userpic_view.h" @@ -21,6 +25,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media::Stories { namespace { +constexpr auto kAddPerPage = 50; +constexpr auto kLoadViewsPages = 2; + [[nodiscard]] rpl::producer> ContentByUsers( const std::vector> &list) { struct Userpic { @@ -37,7 +44,7 @@ namespace { bool scheduled = false; }; - static const auto size = st::storiesRecentViewsUserpics.size; + static const auto size = st::storiesWhoViewed.userpics.size; static const auto GenerateUserpic = [](Userpic &userpic) { auto result = userpic.peer->generateUserpicImage( @@ -132,57 +139,302 @@ void RecentViews::show(RecentViewsData data) { return; } if (!_widget) { - const auto parent = _controller->wrap(); - auto widget = std::make_unique(parent); - const auto raw = widget.get(); - raw->show(); - - _controller->layoutValue( - ) | rpl::start_with_next([=](const Layout &layout) { - raw->setGeometry(layout.views); - }, raw->lifetime()); - - raw->paintRequest( - ) | rpl::start_with_next([=](QRect clip) { - auto p = Painter(raw); - const auto skip = st::storiesRecentViewsSkip; - const auto full = _userpicsWidth + skip + _text.maxWidth(); - const auto use = std::min(full, raw->width()); - const auto ux = (raw->width() - use) / 2; - const auto height = st::storiesRecentViewsUserpics.size; - const auto uy = (raw->height() - height) / 2; - const auto tx = ux + _userpicsWidth + skip; - const auto ty = (raw->height() - st::normalFont->height) / 2; - _userpics->paint(p, ux, uy, height); - p.setPen(st::storiesComposeWhiteText); - _text.drawElided(p, tx, ty, use - _userpicsWidth - skip); - }, raw->lifetime()); - - _widget = std::move(widget); - } - if (totalChanged) { - _text.setText(st::defaultTextStyle, data.total - ? tr::lng_stories_views(tr::now, lt_count, data.total) - : tr::lng_stories_no_views(tr::now)); + setupWidget(); } if (!_userpics) { - _userpics = std::make_unique( - st::storiesRecentViewsUserpics, - rpl::single(true), - [=] { _widget->update(); }); - - _userpics->widthValue() | rpl::start_with_next([=](int width) { - _userpicsWidth = width; - }, _widget->lifetime()); + setupUserpics(); + } + if (totalChanged) { + updateText(); } if (usersChanged) { - _userpicsLifetime = ContentByUsers( - data.list - ) | rpl::start_with_next([=]( - const std::vector &list) { - _userpics->update(list, true); - }); + updateUserpics(); } } +void RecentViews::updateUserpics() { + _userpicsLifetime = ContentByUsers( + _data.list + ) | rpl::start_with_next([=]( + const std::vector &list) { + _userpics->update(list, true); + }); +} + +void RecentViews::setupUserpics() { + _userpics = std::make_unique( + st::storiesWhoViewed.userpics, + rpl::single(true), + [=] { _widget->update(); }); + + _userpics->widthValue() | rpl::start_with_next([=](int width) { + if (_userpicsWidth != width) { + _userpicsWidth = width; + updatePartsGeometry(); + } + }, _widget->lifetime()); +} + +void RecentViews::setupWidget() { + _widget = std::make_unique(_controller->wrap()); + const auto raw = _widget.get(); + raw->show(); + + _controller->layoutValue( + ) | rpl::start_with_next([=](const Layout &layout) { + _outer = layout.views; + updatePartsGeometry(); + }, raw->lifetime()); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(raw); + _userpics->paint( + p, + _userpicsPosition.x(), + _userpicsPosition.y(), + st::storiesWhoViewed.userpics.size); + p.setPen(st::storiesComposeWhiteText); + _text.drawElided( + p, + _textPosition.x(), + _textPosition.y(), + raw->width() - _userpicsWidth - st::storiesRecentViewsSkip); + }, raw->lifetime()); + + raw->events( + ) | rpl::filter([=](not_null e) { + return (_data.total > 0) + && (e->type() == QEvent::MouseButtonPress) + && (static_cast(e.get())->button() + == Qt::LeftButton); + }) | rpl::start_with_next([=] { + showMenu(); + }, raw->lifetime()); + + raw->setCursor(style::cur_pointer); +} + +void RecentViews::updatePartsGeometry() { + const auto skip = st::storiesRecentViewsSkip; + const auto full = _userpicsWidth + skip + _text.maxWidth(); + const auto use = std::min(full, _outer.width()); + const auto ux = _outer.x() + (_outer.width() - use) / 2; + const auto uheight = st::storiesWhoViewed.userpics.size; + const auto uy = _outer.y() + (_outer.height() - uheight) / 2; + const auto tx = ux + _userpicsWidth + skip; + const auto theight = st::normalFont->height; + const auto ty = _outer.y() + (_outer.height() - theight) / 2; + const auto my = std::min(uy, ty); + const auto mheight = std::max(uheight, theight); + const auto padding = skip; + _userpicsPosition = QPoint(padding, uy - my); + _textPosition = QPoint(tx - ux + padding, ty - my); + _widget->setGeometry(ux - padding, my, use + 2 * padding, mheight); + _widget->update(); +} + +void RecentViews::updateText() { + _text.setText(st::defaultTextStyle, _data.total + ? tr::lng_stories_views(tr::now, lt_count, _data.total) + : tr::lng_stories_no_views(tr::now)); + updatePartsGeometry(); +} + +void RecentViews::showMenu() { + if (_menu) { + return; + } + + const auto views = _controller->views(PeerId()); + if (views.list.empty() && !views.left) { + return; + } + + using namespace Ui; + _menuShortLifetime.destroy(); + _menu = base::make_unique_q( + _widget.get(), + st::storiesViewsMenu); + auto count = 0; + const auto added = std::min(int(views.list.size()), kAddPerPage); + const auto add = std::min(added + views.left, kAddPerPage); + const auto now = QDateTime::currentDateTime(); + for (const auto &entry : views.list) { + addMenuRow(entry, now); + if (++count >= add) { + break; + } + } + while (count++ < add) { + addMenuRowPlaceholder(); + } + rpl::merge( + _controller->moreViewsLoaded(), + rpl::combine( + _menu->scrollTopValue(), + _menuEntriesCount.value() + ) | rpl::filter([=](int scrollTop, int count) { + const auto fullHeight = count + * (st::defaultWhoRead.photoSkip * 2 + + st::defaultWhoRead.photoSize); + return fullHeight + < (scrollTop + + st::storiesViewsMenu.maxHeight * kLoadViewsPages); + }) | rpl::to_empty + ) | rpl::start_with_next([=] { + rebuildMenuTail(); + }, _menuShortLifetime); + + _menu->setDestroyedCallback(crl::guard(_widget.get(), [=] { + _menuShortLifetime.destroy(); + _menuEntries.clear(); + _menuEntriesCount = 0; + _menuPlaceholderCount = 0; + })); + + const auto size = _menu->size(); + const auto geometry = _widget->mapToGlobal(_widget->rect()); + _menu->setForcedVerticalOrigin(PopupMenu::VerticalOrigin::Bottom); + _menu->popup(QPoint( + geometry.x() + (_widget->width() - size.width()) / 2, + geometry.y() + _widget->height())); + + _menuEntriesCount = _menuEntriesCount.current() + added; +} + +void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { + Expects(_menu != nullptr); + + const auto peer = entry.peer; + const auto date = Api::FormatReadDate(entry.date, now); + const auto prepare = [&](Ui::PeerUserpicView &view) { + const auto size = st::storiesWhoViewed.photoSize; + auto userpic = peer->generateUserpicImage( + view, + size * style::DevicePixelRatio()); + userpic.setDevicePixelRatio(style::DevicePixelRatio()); + return Ui::WhoReactedEntryData{ + .text = peer->name(), + .date = date, + .userpic = std::move(userpic), + .callback = [] {}, + }; + }; + if (_menuPlaceholderCount > 0) { + const auto i = _menuEntries.end() - (_menuPlaceholderCount--); + i->peer = peer; + i->date = date; + i->action->setData(prepare(i->view)); + } else { + auto view = Ui::PeerUserpicView(); + auto action = base::make_unique_q( + _menu->menu(), + nullptr, + _menu->menu()->st(), + prepare(view)); + const auto raw = action.get(); + _menu->addAction(std::move(action)); + _menuEntries.push_back({ + .action = raw, + .peer = peer, + .date = date, + .view = std::move(view), + }); + } + const auto i = end(_menuEntries) - _menuPlaceholderCount - 1; + i->key = peer->userpicUniqueKey(i->view); + if (peer->hasUserpic() && peer->useEmptyUserpic(i->view)) { + if (_waitingForUserpics.emplace(i - begin(_menuEntries)).second + && _waitingForUserpics.size() == 1) { + subscribeToMenuUserpicsLoading(&peer->session()); + } + } +} + +void RecentViews::addMenuRowPlaceholder() { + auto action = base::make_unique_q( + _menu->menu(), + nullptr, + _menu->menu()->st(), + Ui::WhoReactedEntryData{ .preloader = true }); + const auto raw = action.get(); + _menu->addAction(std::move(action)); + _menuEntries.push_back({ .action = raw }); + ++_menuPlaceholderCount; +} + +void RecentViews::rebuildMenuTail() { + const auto offset = (_menuPlaceholderCount < _menuEntries.size()) + ? (end(_menuEntries) - _menuPlaceholderCount - 1)->peer->id + : PeerId(); + const auto views = _controller->views(offset); + if (views.list.empty()) { + return; + } + const auto now = QDateTime::currentDateTime(); + const auto added = std::min( + _menuPlaceholderCount + kAddPerPage, + int(views.list.size())); + auto add = added; + for (const auto &entry : views.list) { + addMenuRow(entry, now); + if (!--add) { + break; + } + } + _menuEntriesCount = _menuEntriesCount.current() + added; +} + +void RecentViews::subscribeToMenuUserpicsLoading( + not_null session) { + _shortAnimationPlaying = style::ShortAnimationPlaying(); + _waitingForUserpicsLifetime = rpl::merge( + _shortAnimationPlaying.changes() | rpl::filter([=](bool playing) { + return !playing && _waitingUserpicsCheck; + }) | rpl::to_empty, + session->downloaderTaskFinished( + ) | rpl::filter([=] { + if (_shortAnimationPlaying.current()) { + _waitingUserpicsCheck = true; + return false; + } + return true; + }) + ) | rpl::start_with_next([=] { + _waitingUserpicsCheck = false; + for (auto i = begin(_waitingForUserpics) + ; i != end(_waitingForUserpics) + ;) { + auto &entry = _menuEntries[*i]; + auto &view = entry.view; + const auto peer = entry.peer; + const auto key = peer->userpicUniqueKey(view); + const auto update = (entry.key != key); + if (update) { + const auto size = st::storiesWhoViewed.photoSize; + auto userpic = peer->generateUserpicImage( + view, + size * style::DevicePixelRatio()); + userpic.setDevicePixelRatio(style::DevicePixelRatio()); + entry.action->setData({ + .text = peer->name(), + .date = entry.date, + .userpic = std::move(userpic), + .callback = [] {}, + }); + entry.key = key; + if (!peer->hasUserpic() || !peer->useEmptyUserpic(view)) { + i = _waitingForUserpics.erase(i); + continue; + } + } + ++i; + } + if (_waitingForUserpics.empty()) { + _waitingForUserpicsLifetime.destroy(); + } + }); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h index 64a6d4e82..79b4ee3fc 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h @@ -7,13 +7,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/unique_qptr.h" #include "ui/text/text.h" +#include "ui/userpic_view.h" + +namespace Data { +struct StoryView; +} // namespace Data namespace Ui { class RpWidget; class GroupCallUserpics; +class PopupMenu; +class WhoReactedEntryAction; } // namespace Ui +namespace Main { +class Session; +} // namespace Main + namespace Media::Stories { class Controller; @@ -39,6 +51,26 @@ public: void show(RecentViewsData data); private: + struct MenuEntry { + not_null action; + PeerData *peer = nullptr; + QString date; + Ui::PeerUserpicView view; + InMemoryKey key; + }; + + void setupWidget(); + void setupUserpics(); + void updateUserpics(); + void updateText(); + void updatePartsGeometry(); + void showMenu(); + + void addMenuRow(Data::StoryView entry, const QDateTime &now); + void addMenuRowPlaceholder(); + void rebuildMenuTail(); + void subscribeToMenuUserpicsLoading(not_null session); + const not_null _controller; std::unique_ptr _widget; @@ -46,6 +78,20 @@ private: Ui::Text::String _text; RecentViewsData _data; rpl::lifetime _userpicsLifetime; + + base::unique_qptr _menu; + rpl::lifetime _menuShortLifetime; + std::vector _menuEntries; + rpl::variable _menuEntriesCount = 0; + int _menuPlaceholderCount = 0; + base::flat_set _waitingForUserpics; + rpl::variable _shortAnimationPlaying; + bool _waitingUserpicsCheck = false; + rpl::lifetime _waitingForUserpicsLifetime; + + QRect _outer; + QPoint _userpicsPosition; + QPoint _textPosition; int _userpicsWidth = 0; }; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 2a3c3f7b2..b1674e41b 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -729,11 +729,21 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } premium: storiesComposePremium; } -storiesRecentViewsUserpics: GroupCallUserpics { - size: 24px; - shift: 9px; - stroke: 4px; - align: align(left); +storiesViewsMenu: PopupMenu(storiesPopupMenuWithIcons) { + scrollPadding: margins(0px, 6px, 0px, 4px); + maxHeight: 320px; + menu: Menu(storiesMenuWithIcons) { + widthMin: 215px; + widthMax: 215px; + } + radius: 7px; } storiesRecentViewsSkip: 8px; - +storiesWhoViewed: WhoRead(defaultWhoRead) { + userpics: GroupCallUserpics { + size: 24px; + shift: 9px; + stroke: 4px; + align: align(left); + } +} diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp index 156bef531..83c543fe0 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "windows_quiethours_h.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 8f6b00900..5fbb5b4c3 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -54,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_boxes.h" #include "styles/style_settings.h" #include "styles/style_info.h" diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index d4b66ab07..6b718137e 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -727,29 +727,6 @@ popupMenuExpandedSeparator: PopupMenu(popupMenuWithIcons) { } } -WhoRead { - userpics: GroupCallUserpics; - photoLeft: pixels; - photoSize: pixels; - photoSkip: pixels; - nameLeft: pixels; - iconPosition: point; - itemPadding: margins; -} -defaultWhoRead: WhoRead { - userpics: GroupCallUserpics { - size: 22px; - shift: 8px; - stroke: 4px; - align: align(right); - } - photoLeft: 13px; - photoSize: 30px; - photoSkip: 5px; - nameLeft: 57px; - iconPosition: point(15px, 7px); - itemPadding: margins(44px, 9px, 17px, 7px); -} whoReadMenu: PopupMenu(popupMenuExpandedSeparator) { scrollPadding: margins(0px, 6px, 0px, 4px); maxHeight: 400px; @@ -934,24 +911,6 @@ historySendDisabledIcon: icon {{ "emoji/premium_lock", placeholderFgActive }}; historySendDisabledIconSkip: 20px; historySendDisabledPosition: point(0px, 0px); -moreChatsBarHeight: 48px; -moreChatsBarTextPosition: point(12px, 4px); -moreChatsBarStatusPosition: point(12px, 24px); -moreChatsBarClose: IconButton(defaultIconButton) { - width: 48px; - height: 48px; - - icon: boxTitleCloseIcon; - iconOver: boxTitleCloseIconOver; - iconPosition: point(12px, -1px); - - rippleAreaPosition: point(0px, 4px); - rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } -} - backgroundSwitchToDark: IconButton(defaultIconButton) { width: 48px; height: 48px; diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp index 7087bbbd4..f4da62406 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "base/unixtime.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_calls.h" #include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget. #include "styles/style_window.h" // st::columnMinimalWidthLeft diff --git a/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp index a88f2074d..b5aadd41e 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp +++ b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/power_saving.h" #include "base/random.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Ui { namespace { diff --git a/Telegram/SourceFiles/ui/chat/more_chats_bar.cpp b/Telegram/SourceFiles/ui/chat/more_chats_bar.cpp index e645801a2..d78a60696 100644 --- a/Telegram/SourceFiles/ui/chat/more_chats_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/more_chats_bar.cpp @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_options.h" #include "ui/painter.h" #include "lang/lang_keys.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" // st::columnMinimalWidthLeft namespace Ui { diff --git a/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp b/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp index 5d9a4f80d..ac13cab17 100644 --- a/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp +++ b/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "lang/lang_keys.h" -#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_info.h" namespace Ui { diff --git a/Telegram/SourceFiles/ui/controls/silent_toggle.cpp b/Telegram/SourceFiles/ui/controls/silent_toggle.cpp index 393ae3c2d..e583af790 100644 --- a/Telegram/SourceFiles/ui/controls/silent_toggle.cpp +++ b/Telegram/SourceFiles/ui/controls/silent_toggle.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Ui { namespace { diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index f4914b389..ae9acc05d 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" namespace Lang { @@ -69,16 +70,9 @@ StringWithReacted ReplaceTag::Call( namespace Ui { namespace { -using Text::CustomEmojiFactory; +constexpr auto kPreloaderAlpha = 0.2; -struct EntryData { - QString text; - QString date; - bool dateReacted = false; - QString customEntityData; - QImage userpic; - Fn callback; -}; +using Text::CustomEmojiFactory; class Action final : public Menu::ItemBase { public: @@ -465,44 +459,11 @@ void Action::handleKeyPress(not_null e) { } // namespace -class WhoReactedListMenu::EntryAction final : public Menu::ItemBase { -public: - EntryAction( - not_null parent, - CustomEmojiFactory factory, - const style::Menu &st, - EntryData &&data); - - void setData(EntryData &&data); - - not_null action() const override; - bool isEnabled() const override; - -private: - int contentHeight() const override; - - void paint(Painter &&p); - - const not_null _dummyAction; - const CustomEmojiFactory _customEmojiFactory; - const style::Menu &_st; - const int _height = 0; - - Text::String _text; - Text::String _date; - std::unique_ptr _custom; - QImage _userpic; - int _textWidth = 0; - int _customSize = 0; - bool _dateReacted = false; - -}; - -WhoReactedListMenu::EntryAction::EntryAction( +WhoReactedEntryAction::WhoReactedEntryAction( not_null parent, CustomEmojiFactory customEmojiFactory, const style::Menu &st, - EntryData &&data) + Data &&data) : ItemBase(parent, st) , _dummyAction(CreateChild(parent.get())) , _customEmojiFactory(std::move(customEmojiFactory)) @@ -521,19 +482,19 @@ WhoReactedListMenu::EntryAction::EntryAction( enableMouseSelecting(); } -not_null WhoReactedListMenu::EntryAction::action() const { +not_null WhoReactedEntryAction::action() const { return _dummyAction.get(); } -bool WhoReactedListMenu::EntryAction::isEnabled() const { +bool WhoReactedEntryAction::isEnabled() const { return true; } -int WhoReactedListMenu::EntryAction::contentHeight() const { +int WhoReactedEntryAction::contentHeight() const { return _height; } -void WhoReactedListMenu::EntryAction::setData(EntryData &&data) { +void WhoReactedEntryAction::setData(Data &&data) { setClickedCallback(std::move(data.callback)); _userpic = std::move(data.userpic); _text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions); @@ -546,7 +507,10 @@ void WhoReactedListMenu::EntryAction::setData(EntryData &&data) { MenuTextOptions); } _dateReacted = data.dateReacted; - _custom = _customEmojiFactory(data.customEntityData, [=] { update(); }); + _preloader = data.preloader; + _custom = _customEmojiFactory + ? _customEmojiFactory(data.customEntityData, [=] { update(); }) + : nullptr; const auto ratio = style::DevicePixelRatio(); const auto size = Emoji::GetSizeNormal() / ratio; _customSize = Text::AdjustCustomEmojiSize(size); @@ -565,7 +529,7 @@ void WhoReactedListMenu::EntryAction::setData(EntryData &&data) { update(); } -void WhoReactedListMenu::EntryAction::paint(Painter &&p) { +void WhoReactedEntryAction::paint(Painter &&p) { const auto enabled = isEnabled(); const auto selected = isSelected(); if (selected && _st.itemBgOver->c.alpha() < 255) { @@ -578,7 +542,18 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) { const auto photoSize = st::defaultWhoRead.photoSize; const auto photoLeft = st::defaultWhoRead.photoLeft; const auto photoTop = (height() - photoSize) / 2; - if (!_userpic.isNull()) { + const auto preloaderBrush = _preloader + ? [&] { + auto color = _st.itemFg->c; + color.setAlphaF(color.alphaF() * kPreloaderAlpha); + return QBrush(color); + }() : QBrush(); + if (_preloader) { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(preloaderBrush); + p.drawEllipse(photoLeft, photoTop, photoSize, photoSize); + } else if (!_userpic.isNull()) { p.drawImage(photoLeft, photoTop, _userpic); } else if (!_custom) { st::menuIconReactions.paintInCenter( @@ -590,17 +565,31 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) { const auto textTop = withDate ? st::whoReadNameWithDateTop : (height() - _st.itemStyle.font->height) / 2; - p.setPen(selected - ? _st.itemFgOver - : enabled - ? _st.itemFg - : _st.itemFgDisabled); - _text.drawLeftElided( - p, - st::defaultWhoRead.nameLeft, - textTop, - _textWidth, - width()); + if (_preloader) { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(preloaderBrush); + const auto height = _st.itemStyle.font->height / 2; + p.drawRoundedRect( + st::defaultWhoRead.nameLeft, + textTop + (_st.itemStyle.font->height - height) / 2, + _textWidth, + height, + height / 2., + height / 2.); + } else { + p.setPen(selected + ? _st.itemFgOver + : enabled + ? _st.itemFg + : _st.itemFgDisabled); + _text.drawLeftElided( + p, + st::defaultWhoRead.nameLeft, + textTop, + _textWidth, + width()); + } if (withDate) { const auto iconPosition = QPoint( st::defaultWhoRead.nameLeft, @@ -690,11 +679,11 @@ void WhoReactedListMenu::populate( addedToBottom = 0; } auto index = 0; - const auto append = [&](EntryData &&data) { + const auto append = [&](WhoReactedEntryData &&data) { if (index < _actions.size()) { _actions[index]->setData(std::move(data)); } else { - auto item = base::make_unique_q( + auto item = base::make_unique_q( menu->menu(), _customEmojiFactory, menu->menu()->st(), diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index c46f3e804..fa4d3a08f 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -9,11 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" #include "ui/text/text_block.h" +#include "ui/widgets/menu/menu_item_base.h" namespace Ui { -namespace Menu { -class ItemBase; -} // namespace Menu class PopupMenu; @@ -56,6 +54,52 @@ struct WhoReadContent { Fn participantChosen, Fn showAllChosen); +struct WhoReactedEntryData { + QString text; + QString date; + bool dateReacted = false; + bool preloader = false; + QString customEntityData; + QImage userpic; + Fn callback; +}; + +class WhoReactedEntryAction final : public Menu::ItemBase { +public: + using Data = WhoReactedEntryData; + + WhoReactedEntryAction( + not_null parent, + Text::CustomEmojiFactory factory, + const style::Menu &st, + Data &&data); + + void setData(Data &&data); + + not_null action() const override; + bool isEnabled() const override; + +private: + int contentHeight() const override; + + void paint(Painter &&p); + + const not_null _dummyAction; + const Text::CustomEmojiFactory _customEmojiFactory; + const style::Menu &_st; + const int _height = 0; + + Text::String _text; + Text::String _date; + std::unique_ptr _custom; + QImage _userpic; + int _textWidth = 0; + int _customSize = 0; + bool _dateReacted = false; + bool _preloader = false; + +}; + class WhoReactedListMenu final { public: WhoReactedListMenu( @@ -72,13 +116,11 @@ public: Fn appendBottomActions = nullptr); private: - class EntryAction; - const Text::CustomEmojiFactory _customEmojiFactory; const Fn _participantChosen; const Fn _showAllChosen; - std::vector> _actions; + std::vector> _actions; }; From 41edd41b924c38d08a3faef23dea24b96fe635fa Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 1 Jun 2023 20:12:42 +0400 Subject: [PATCH 051/260] Pause story while viewing the viewers list. --- .../media/stories/media_stories_controller.cpp | 10 +++++++++- .../media/stories/media_stories_controller.h | 2 ++ .../media/stories/media_stories_recent_views.cpp | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 3fc642b0a..55062a7d3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -476,7 +476,8 @@ void Controller::updatePlayingAllowed() { && _windowActive && !_paused && !_replyActive - && !_layerShown); + && !_layerShown + && !_menuShown); } void Controller::setPlayingAllowed(bool allowed) { @@ -656,6 +657,13 @@ void Controller::togglePaused(bool paused) { } } +void Controller::setMenuShown(bool shown) { + if (_menuShown != shown) { + _menuShown = shown; + updatePlayingAllowed(); + } +} + bool Controller::canDownload() const { return _list && _list->user->isSelf(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 161a6971e..b8954e9e3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -113,6 +113,7 @@ public: [[nodiscard]] bool jumpFor(int delta); [[nodiscard]] bool paused() const; void togglePaused(bool paused); + void setMenuShown(bool shown); [[nodiscard]] bool canDownload() const; @@ -174,6 +175,7 @@ private: bool _replyFocused = false; bool _replyActive = false; bool _layerShown = false; + bool _menuShown = false; bool _paused = false; FullStoryId _shown; diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp index 3703d474f..98f8ca9a8 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -286,7 +286,9 @@ void RecentViews::showMenu() { rebuildMenuTail(); }, _menuShortLifetime); + _controller->setMenuShown(true); _menu->setDestroyedCallback(crl::guard(_widget.get(), [=] { + _controller->setMenuShown(false); _menuShortLifetime.destroy(); _menuEntries.clear(); _menuEntriesCount = 0; From 17a5c27658b903ef675024e3524e5caf7c102d2d Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 2 Jun 2023 11:46:19 +0400 Subject: [PATCH 052/260] Update API scheme on layer 160. Leave plain scheme in api.tl. --- Telegram/SourceFiles/api/api_updates.cpp | 4 +- Telegram/SourceFiles/data/data_stories.cpp | 77 +++++++++++-------- Telegram/SourceFiles/data/data_stories.h | 6 +- .../tl => SourceFiles/mtproto/scheme}/api.tl | 41 ++-------- Telegram/SourceFiles/mtproto/scheme/layer.tl | 1 + .../mtproto/scheme}/mtproto.tl | 0 Telegram/cmake/td_scheme.cmake | 12 +-- 7 files changed, 63 insertions(+), 78 deletions(-) rename Telegram/{Resources/tl => SourceFiles/mtproto/scheme}/api.tl (98%) create mode 100644 Telegram/SourceFiles/mtproto/scheme/layer.tl rename Telegram/{Resources/tl => SourceFiles/mtproto/scheme}/mtproto.tl (100%) diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 62bf94ba7..55a15d2a7 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2520,8 +2520,8 @@ void Updates::feedUpdate(const MTPUpdate &update) { _session->api().transcribes().apply(data); } break; - case mtpc_updateStories: { - _session->data().stories().apply(update.c_updateStories()); + case mtpc_updateStory: { + _session->data().stories().apply(update.c_updateStory()); } break; } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 5cc08e8b5..90d697126 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -276,8 +276,29 @@ Main::Session &Stories::session() const { return _owner->session(); } -void Stories::apply(const MTPDupdateStories &data) { - applyChanges(parse(data.vstories())); +void Stories::apply(const MTPDupdateStory &data) { + const auto peerId = peerFromUser(data.vuser_id()); + const auto peer = _owner->peer(peerId); + const auto i = ranges::find(_all, peer, &StoriesList::user); + const auto id = parseAndApply(peer, data.vstory()); + if (i != end(_all)) { + auto added = false; + if (id && !i->ids.contains(id)) { + i->ids.emplace(id); + ++i->total; + added = true; + } + if (added) { + ranges::rotate(begin(_all), i, i + 1); + } + } else if (id) { + _all.insert(begin(_all), StoriesList{ + .user = peer->asUser(), + .ids = { id }, + .readTill = 0, + .total = 1, + }); + } _allChanged.fire({}); } @@ -294,19 +315,11 @@ StoriesList Stories::parse(const MTPUserStories &stories) { const auto &list = data.vstories().v; result.ids.reserve(list.size()); for (const auto &story : list) { - story.match([&](const MTPDstoryItem &data) { - if (const auto story = parseAndApply(result.user, data)) { - result.ids.emplace(story->id()); - } else { - applyDeleted({ peerFromUser(userId), data.vid().v }); - --result.total; - } - }, [&](const MTPDstoryItemSkipped &data) { - result.ids.emplace(data.vid().v); - }, [&](const MTPDstoryItemDeleted &data) { - applyDeleted({ peerFromUser(userId), data.vid().v }); + if (const auto id = parseAndApply(result.user, story)) { + result.ids.emplace(id); + } else { --result.total; - }); + } } result.total = std::max(result.total, int(result.ids.size())); return result; @@ -339,6 +352,23 @@ Story *Stories::parseAndApply( return result; } +StoryId Stories::parseAndApply( + not_null peer, + const MTPstoryItem &story) { + return story.match([&](const MTPDstoryItem &data) { + if (const auto story = parseAndApply(peer, data)) { + return story->id(); + } + applyDeleted({ peer->id, data.vid().v }); + return StoryId(); + }, [&](const MTPDstoryItemSkipped &data) { + return StoryId(data.vid().v); + }, [&](const MTPDstoryItemDeleted &data) { + applyDeleted({ peer->id, data.vid().v }); + return StoryId(); + }); +} + void Stories::updateDependentMessages(not_null story) { const auto i = _dependentMessages.find(story); if (i != end(_dependentMessages)) { @@ -603,25 +633,6 @@ void Stories::pushToBack(StoriesList &&list) { } } -void Stories::applyChanges(StoriesList &&list) { - const auto i = ranges::find(_all, list.user, &StoriesList::user); - if (i != end(_all)) { - auto added = false; - for (const auto id : list.ids) { - if (!i->ids.contains(id)) { - i->ids.emplace(id); - ++i->total; - added = true; - } - } - if (added) { - ranges::rotate(begin(_all), i, i + 1); - } - } else if (!list.ids.empty()) { - _all.insert(begin(_all), std::move(list)); - } -} - void Stories::loadAround(FullStoryId id) { const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) { return list.user->id; diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index b4a53f859..aa0fcbda5 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -122,7 +122,7 @@ public: not_null dependency); void loadMore(); - void apply(const MTPDupdateStories &data); + void apply(const MTPDupdateStory &data); void loadAround(FullStoryId id); [[nodiscard]] const std::vector &all(); @@ -148,6 +148,9 @@ private: [[nodiscard]] Story *parseAndApply( not_null peer, const MTPDstoryItem &data); + StoryId parseAndApply( + not_null peer, + const MTPstoryItem &story); void processResolvedStories( not_null peer, const QVector &list); @@ -155,7 +158,6 @@ private: void finalizeResolve(FullStoryId id); void pushToBack(StoriesList &&list); - void applyChanges(StoriesList &&list); void applyDeleted(FullStoryId id); void removeDependencyStory(not_null story); diff --git a/Telegram/Resources/tl/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl similarity index 98% rename from Telegram/Resources/tl/api.tl rename to Telegram/SourceFiles/mtproto/scheme/api.tl index d5b1f3652..f603e31ae 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1,33 +1,3 @@ -/////////////////////////////// -/////////////////// Layer cons -/////////////////////////////// - -//invokeAfterMsg#cb9f372d msg_id:long query:!X = X; -//invokeAfterMsgs#3dc4b4f0 msg_ids:Vector query:!X = X; -//invokeWithLayer1#53835315 query:!X = X; -//invokeWithLayer2#289dd1f6 query:!X = X; -//invokeWithLayer3#b7475268 query:!X = X; -//invokeWithLayer4#dea0d430 query:!X = X; -//invokeWithLayer5#417a57ae query:!X = X; -//invokeWithLayer6#3a64d54d query:!X = X; -//invokeWithLayer7#a5be56d3 query:!X = X; -//invokeWithLayer8#e9abd9fd query:!X = X; -//invokeWithLayer9#76715a63 query:!X = X; -//invokeWithLayer10#39620c41 query:!X = X; -//invokeWithLayer11#a6b88fdf query:!X = X; -//invokeWithLayer12#dda60d3c query:!X = X; -//invokeWithLayer13#427c8ea2 query:!X = X; -//invokeWithLayer14#2b9b08fa query:!X = X; -//invokeWithLayer15#b4418b64 query:!X = X; -//invokeWithLayer16#cf5f0987 query:!X = X; -//invokeWithLayer17#50858a19 query:!X = X; -//invokeWithLayer18#1c900537 query:!X = X; -//invokeWithLayer#da9b0d0d layer:int query:!X = X; // after 18 layer - -/////////////////////////////// -///////// Main application API -/////////////////////////////// - boolFalse#bc799737 = Bool; boolTrue#997275b5 = Bool; @@ -110,7 +80,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#8f97c628 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_available:flags2.3?true stories_unavailable:flags2.4?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector = User; +user#8f97c628 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_available:flags2.3?true stories_unavailable:flags2.4?true stories_hidden:flags2.5?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -410,7 +380,7 @@ updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector< updateUser#20529438 user_id:long = Update; updateAutoSaveSettings#ec05b097 = Update; updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; -updateStories#66fad7b5 stories:UserStories = Update; +updateStory#205a4133 user_id:long story:StoryItem = Update; updateReadStories#feb5345a user_id:long max_id:int = Update; updateStoryID#1bf335b9 id:int random_id:long = Update; @@ -1557,7 +1527,7 @@ storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#a1d8cf8f id:int date:int = StoryItem; -storyItem#8fcd96ac flags:# pinned:flags.5?true expired:flags.6?true public:flags.7?true id:int date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; +storyItem#8fcd96ac flags:# pinned:flags.5?true expired:flags.6?true id:int date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; @@ -1727,6 +1697,7 @@ contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; contacts.exportContactToken#f8654027 = ExportedContactToken; contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; +contacts.toggleStoriesHidden#753fb865 id:InputUser hidden:Bool = Bool; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; @@ -2115,7 +2086,7 @@ stories.sendStory#8b5c6986 flags:# pinned:flags.2?true media:InputMedia caption: stories.editStory#2aae7a41 flags:# id:int media:flags.0?InputMedia caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#b5d501d7 id:Vector = Vector; stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; -stories.getAllStories#eeb0d625 flags:# next:flags.1?true state:flags.0?string = stories.AllStories; +stories.getAllStories#eeb0d625 flags:# next:flags.1?true include_hidden:flags.2?true state:flags.0?string = stories.AllStories; stories.getUserStories#96d528e0 user_id:InputUser = stories.UserStories; stories.getPinnedStories#b471137 user_id:InputUser offset_id:int limit:int = stories.Stories; stories.getExpiredStories#8f792f2 offset_id:int limit:int = stories.Stories; @@ -2124,5 +2095,3 @@ stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList; stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; - -// LAYER 160 diff --git a/Telegram/SourceFiles/mtproto/scheme/layer.tl b/Telegram/SourceFiles/mtproto/scheme/layer.tl new file mode 100644 index 000000000..5e910a52b --- /dev/null +++ b/Telegram/SourceFiles/mtproto/scheme/layer.tl @@ -0,0 +1 @@ +// LAYER 160 diff --git a/Telegram/Resources/tl/mtproto.tl b/Telegram/SourceFiles/mtproto/scheme/mtproto.tl similarity index 100% rename from Telegram/Resources/tl/mtproto.tl rename to Telegram/SourceFiles/mtproto/scheme/mtproto.tl diff --git a/Telegram/cmake/td_scheme.cmake b/Telegram/cmake/td_scheme.cmake index 20b956382..11129f20f 100644 --- a/Telegram/cmake/td_scheme.cmake +++ b/Telegram/cmake/td_scheme.cmake @@ -11,16 +11,18 @@ add_library(tdesktop::td_scheme ALIAS td_scheme) include(cmake/generate_scheme.cmake) set(scheme_files - ${res_loc}/tl/mtproto.tl - ${res_loc}/tl/api.tl + ${src_loc}/mtproto/scheme/api.tl + ${src_loc}/mtproto/scheme/layer.tl + ${src_loc}/mtproto/scheme/mtproto.tl ) generate_scheme(td_scheme ${src_loc}/codegen/scheme/codegen_scheme.py "${scheme_files}") -nice_target_sources(td_scheme ${res_loc} +nice_target_sources(td_scheme ${src_loc}/mtproto/scheme PRIVATE - tl/mtproto.tl - tl/api.tl + api.tl + layer.tl + mtproto.tl ) target_include_directories(td_scheme From d0e1ac123879917ee8b9703d6177f146c286a808 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 2 Jun 2023 13:20:27 +0400 Subject: [PATCH 053/260] Start hiding stories from chats list. --- Telegram/SourceFiles/data/data_session.cpp | 2 + Telegram/SourceFiles/data/data_stories.cpp | 8 ++-- Telegram/SourceFiles/data/data_stories.h | 1 + Telegram/SourceFiles/data/data_user.cpp | 4 ++ Telegram/SourceFiles/data/data_user.h | 2 + .../dialogs/dialogs_inner_widget.cpp | 21 +++++++++ .../dialogs/ui/dialogs_stories_list.cpp | 46 +++++++++++++++++++ .../dialogs/ui/dialogs_stories_list.h | 17 +++++++ 8 files changed, 97 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 42a8644d1..8df77697a 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -522,6 +522,7 @@ not_null Session::processUser(const MTPUser &data) { ? Flag::Contact | Flag::MutualContact | Flag::DiscardMinPhoto + | Flag::StoriesHidden : Flag()); const auto flagsSet = (data.is_deleted() ? Flag::Deleted : Flag()) | (data.is_verified() ? Flag::Verified : Flag()) @@ -534,6 +535,7 @@ not_null Session::processUser(const MTPUser &data) { ? (data.is_contact() ? Flag::Contact : Flag()) | (data.is_mutual_contact() ? Flag::MutualContact : Flag()) | (data.is_apply_min_photo() ? Flag() : Flag::DiscardMinPhoto) + | (data.is_stories_hidden() ? Flag::StoriesHidden : Flag()) : Flag()); result->setFlags((result->flags() & ~flagsMask) | flagsSet); if (minimal) { diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 90d697126..b9fe9d67e 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -71,10 +71,10 @@ Story::Story( not_null peer, StoryMedia media, TimeId date) - : _id(id) - , _peer(peer) - , _media(std::move(media)) - , _date(date) { +: _id(id) +, _peer(peer) +, _media(std::move(media)) +, _date(date) { } Session &Story::owner() const { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index aa0fcbda5..e41312f5e 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -94,6 +94,7 @@ struct StoriesList { base::flat_set ids; StoryId readTill = 0; int total = 0; + bool hidden = false; [[nodiscard]] bool unread() const; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index a5c6de084..c903008ae 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -321,6 +321,10 @@ bool UserData::hasPersonalPhoto() const { return (flags() & UserDataFlag::PersonalPhoto); } +bool UserData::hasStoriesHidden() const { + return (flags() & UserDataFlag::StoriesHidden); +} + bool UserData::canAddContact() const { return canShareThisContact() && !isContact(); } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index cc32613f9..ad0dec4d4 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -62,6 +62,7 @@ enum class UserDataFlag { CanReceiveGifts = (1 << 15), VoiceMessagesForbidden = (1 << 16), PersonalPhoto = (1 << 17), + StoriesHidden = (1 << 18), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; @@ -119,6 +120,7 @@ public: [[nodiscard]] bool isInaccessible() const; [[nodiscard]] bool applyMinPhoto() const; [[nodiscard]] bool hasPersonalPhoto() const; + [[nodiscard]] bool hasStoriesHidden() const; [[nodiscard]] bool canShareThisContact() const; [[nodiscard]] bool canAddContact() const; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 1b354b377..bbc9a2c49 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -342,6 +342,27 @@ InnerWidget::InnerWidget( _controller->openPeerStories(PeerId(int64(id))); }, lifetime()); + _stories->showProfileRequests( + ) | rpl::start_with_next([=](uint64 id) { + _controller->showPeerInfo(PeerId(int64(id))); + }, lifetime()); + + _stories->toggleShown( + ) | rpl::start_with_next([=](Stories::ToggleShownRequest request) { + const auto peerId = PeerId(int64(request.id)); + const auto user = session().data().peer(peerId)->asUser(); + Assert(user != nullptr); + if (user->hasStoriesHidden() == request.shown) { + user->setFlags(request.shown + ? (user->flags() & ~UserDataFlag::StoriesHidden) + : (user->flags() | UserDataFlag::StoriesHidden)); + session().api().request(MTPcontacts_ToggleStoriesHidden( + user->inputUser, + MTP_bool(!request.shown) + )).send(); + } + }, lifetime()); + _stories->loadMoreRequests( ) | rpl::start_with_next([=] { session().data().stories().loadMore(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 58625ddef..5b2e24b08 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_stories_list.h" #include "lang/lang_keys.h" +#include "ui/widgets/popup_menu.h" #include "ui/painter.h" #include "styles/style_dialogs.h" @@ -219,6 +220,14 @@ rpl::producer List::clicks() const { return _clicks.events(); } +rpl::producer List::showProfileRequests() const { + return _showProfileRequests.events(); +} + +rpl::producer List::toggleShown() const { + return _toggleShown.events(); +} + rpl::producer<> List::expandRequests() const { return _expandRequests.events(); } @@ -685,6 +694,43 @@ void List::mouseReleaseEvent(QMouseEvent *e) { } } +void List::contextMenuEvent(QContextMenuEvent *e) { + _menu = nullptr; + + if (e->reason() == QContextMenuEvent::Mouse) { + _lastMousePosition = e->globalPos(); + updateSelected(); + } + if (_selected < 0 || _data.empty()) { + return; + } + + auto &item = _data.items[_selected]; + _menu = base::make_unique_q(this); + + const auto id = item.user.id; + const auto hidden = item.user.hidden; + _menu->addAction(u"View Profile"_q, [=] { + _showProfileRequests.fire_copy(id); + }); + _menu->addAction(hidden ? u"Show in Chats"_q : u"Hide"_q, [=] { + _toggleShown.fire({ .id = id, .shown = hidden }); + }); + QObject::connect(_menu.get(), &QObject::destroyed, [=] { + const auto globalPosition = QCursor::pos(); + if (rect().contains(mapFromGlobal(globalPosition))) { + _lastMousePosition = globalPosition; + updateSelected(); + } + }); + if (_menu->empty()) { + _menu = nullptr; + } else { + _menu->popup(e->globalPos()); + e->accept(); + } +} + bool List::finishDragging() { if (!_dragging) { return false; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 06a0fc286..fd1dc3d5b 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class QPainter; +namespace Ui { +class PopupMenu; +} // namespace Ui + namespace Dialogs::Stories { class Userpic { @@ -25,6 +29,7 @@ struct User { QString name; std::shared_ptr userpic; bool unread = false; + bool hidden = false; friend inline bool operator==(const User &a, const User &b) = default; }; @@ -37,6 +42,11 @@ struct Content { const Content &b) = default; }; +struct ToggleShownRequest { + uint64 id = 0; + bool shown = false; +}; + class List final : public Ui::RpWidget { public: List( @@ -45,6 +55,8 @@ public: Fn shownHeight); [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer showProfileRequests() const; + [[nodiscard]] rpl::producer toggleShown() const; [[nodiscard]] rpl::producer<> expandRequests() const; [[nodiscard]] rpl::producer<> entered() const; [[nodiscard]] rpl::producer<> loadMoreRequests() const; @@ -103,6 +115,7 @@ private: void mousePressEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; void validateUserpic(not_null item); void validateName(not_null item); @@ -128,6 +141,8 @@ private: Data _hidingData; Fn _shownHeight = 0; rpl::event_stream _clicks; + rpl::event_stream _showProfileRequests; + rpl::event_stream _toggleShown; rpl::event_stream<> _expandRequests; rpl::event_stream<> _entered; rpl::event_stream<> _loadMoreRequests; @@ -144,6 +159,8 @@ private: int _selected = -1; int _pressed = -1; + base::unique_qptr _menu; + }; } // namespace Dialogs::Stories From f40391b4f0a5fc503d50647acfe17052213169fb Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 2 Jun 2023 18:26:39 +0400 Subject: [PATCH 054/260] Support two lists of stories sources. --- Telegram/SourceFiles/data/data_session.cpp | 2 +- Telegram/SourceFiles/data/data_stories.cpp | 342 ++++++++++++------ Telegram/SourceFiles/data/data_stories.h | 84 ++++- .../dialogs/dialogs_inner_widget.cpp | 9 +- .../dialogs/ui/dialogs_stories_content.cpp | 59 +-- .../dialogs/ui/dialogs_stories_content.h | 7 +- .../history/history_item_helpers.cpp | 3 +- .../stories/media_stories_controller.cpp | 138 +++---- .../media/stories/media_stories_controller.h | 14 +- .../media/stories/media_stories_sibling.cpp | 12 +- .../media/stories/media_stories_sibling.h | 4 +- .../media/stories/media_stories_view.cpp | 7 +- .../media/stories/media_stories_view.h | 8 +- .../media/view/media_view_open_common.h | 7 + .../media/view/media_view_overlay_widget.cpp | 14 +- .../media/view/media_view_overlay_widget.h | 2 + .../window/window_session_controller.cpp | 23 +- .../window/window_session_controller.h | 11 +- 18 files changed, 462 insertions(+), 284 deletions(-) diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 8df77697a..952f1e0fe 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -314,7 +314,7 @@ Session::Session(not_null session) } }, _lifetime); - _stories->loadMore(); + _stories->loadMore(Data::StorySourcesList::NotHidden); }); } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index b9fe9d67e..a3f17a0da 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -35,7 +35,7 @@ constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); using UpdateFlag = StoryUpdate::Flag; -std::optional ParseMedia( +[[nodiscard]] std::optional ParseMedia( not_null owner, const MTPMessageMedia &media) { return media.match([&](const MTPDmessageMediaPhoto &data) @@ -62,8 +62,18 @@ std::optional ParseMedia( } // namespace -bool StoriesList::unread() const { - return !ids.empty() && readTill < ids.back(); +StoriesSourceInfo StoriesSource::info() const { + return { + .id = user->id, + .last = ids.empty() ? 0 : ids.back().date, + .unread = unread(), + .premium = user->isPremium(), + .hidden = hidden, + }; +} + +bool StoriesSource::unread() const { + return !ids.empty() && readTill < ids.back().id; } Story::Story( @@ -93,6 +103,10 @@ StoryId Story::id() const { return _id; } +StoryIdDate Story::idDate() const { + return { _id, _date }; +} + FullStoryId Story::fullId() const { return { _peer->id, _id }; } @@ -278,51 +292,109 @@ Main::Session &Stories::session() const { void Stories::apply(const MTPDupdateStory &data) { const auto peerId = peerFromUser(data.vuser_id()); - const auto peer = _owner->peer(peerId); - const auto i = ranges::find(_all, peer, &StoriesList::user); - const auto id = parseAndApply(peer, data.vstory()); - if (i != end(_all)) { - auto added = false; - if (id && !i->ids.contains(id)) { - i->ids.emplace(id); - ++i->total; - added = true; - } - if (added) { - ranges::rotate(begin(_all), i, i + 1); - } - } else if (id) { - _all.insert(begin(_all), StoriesList{ - .user = peer->asUser(), - .ids = { id }, - .readTill = 0, - .total = 1, - }); + const auto user = not_null(_owner->peer(peerId)->asUser()); + const auto idDate = parseAndApply(user, data.vstory()); + if (!idDate) { + return; + } + const auto i = _all.find(peerId); + if (i == end(_all)) { + requestUserStories(user); + return; + } else if (i->second.ids.contains(idDate)) { + return; + } + const auto was = i->second.info(); + i->second.ids.emplace(idDate); + const auto now = i->second.info(); + if (was == now) { + return; + } + const auto refreshInList = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i != end(sources)) { + *i = now; + sort(list); + } + }; + refreshInList(StorySourcesList::All); + if (!user->hasStoriesHidden()) { + refreshInList(StorySourcesList::NotHidden); } - _allChanged.fire({}); } -StoriesList Stories::parse(const MTPUserStories &stories) { +void Stories::requestUserStories(not_null user) { + if (!_requestingUserStories.emplace(user).second) { + return; + } + _owner->session().api().request(MTPstories_GetUserStories( + user->inputUser + )).done([=](const MTPstories_UserStories &result) { + _requestingUserStories.remove(user); + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + parseAndApply(data.vstories()); + }).fail([=] { + _requestingUserStories.remove(user); + applyDeletedFromSources(user->id, StorySourcesList::All); + }).send(); + +} + +void Stories::parseAndApply(const MTPUserStories &stories) { const auto &data = stories.data(); - const auto userId = UserId(data.vuser_id()); + const auto peerId = peerFromUser(data.vuser_id()); const auto readTill = data.vmax_read_id().value_or_empty(); const auto count = int(data.vstories().v.size()); - auto result = StoriesList{ - .user = _owner->user(userId), + auto result = StoriesSource{ + .user = _owner->peer(peerId)->asUser(), .readTill = readTill, - .total = count, }; const auto &list = data.vstories().v; result.ids.reserve(list.size()); for (const auto &story : list) { if (const auto id = parseAndApply(result.user, story)) { result.ids.emplace(id); - } else { - --result.total; } } - result.total = std::max(result.total, int(result.ids.size())); - return result; + if (result.ids.empty()) { + applyDeletedFromSources(peerId, StorySourcesList::All); + return; + } + const auto info = result.info(); + const auto i = _all.find(peerId); + if (i != end(_all)) { + if (i->second != result) { + i->second = std::move(result); + } + } else { + _all.emplace(peerId, std::move(result)).first; + } + const auto add = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i == end(sources)) { + sources.push_back(info); + } else if (*i == info) { + return; + } else { + *i = info; + } + sort(list); + }; + add(StorySourcesList::All); + if (result.user->hasStoriesHidden()) { + applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + } else { + add(StorySourcesList::NotHidden); + } } Story *Stories::parseAndApply( @@ -352,20 +424,20 @@ Story *Stories::parseAndApply( return result; } -StoryId Stories::parseAndApply( +StoryIdDate Stories::parseAndApply( not_null peer, const MTPstoryItem &story) { return story.match([&](const MTPDstoryItem &data) { if (const auto story = parseAndApply(peer, data)) { - return story->id(); + return story->idDate(); } applyDeleted({ peer->id, data.vid().v }); - return StoryId(); + return StoryIdDate(); }, [&](const MTPDstoryItemSkipped &data) { - return StoryId(data.vid().v); + return StoryIdDate{ data.vid().v, data.vdate().v }; }, [&](const MTPDstoryItemDeleted &data) { applyDeleted({ peer->id, data.vid().v }); - return StoryId(); + return StoryIdDate(); }); } @@ -398,32 +470,34 @@ void Stories::unregisterDependentMessage( } } -void Stories::loadMore() { - if (_loadMoreRequestId || _allLoaded) { +void Stories::loadMore(StorySourcesList list) { + const auto index = static_cast(list); + if (_loadMoreRequestId[index] || _sourcesLoaded[index]) { return; } + const auto all = (list == StorySourcesList::All); const auto api = &_owner->session().api(); using Flag = MTPstories_GetAllStories::Flag; - _loadMoreRequestId = api->request(MTPstories_GetAllStories( - MTP_flags(_state.isEmpty() - ? Flag(0) - : (Flag::f_next | Flag::f_state)), - MTP_string(_state) + _loadMoreRequestId[index] = api->request(MTPstories_GetAllStories( + MTP_flags((all ? Flag::f_include_hidden : Flag()) + | (_sourcesStates[index].isEmpty() + ? Flag(0) + : (Flag::f_next | Flag::f_state))), + MTP_string(_sourcesStates[index]) )).done([=](const MTPstories_AllStories &result) { - _loadMoreRequestId = 0; + _loadMoreRequestId[index] = 0; result.match([&](const MTPDstories_allStories &data) { _owner->processUsers(data.vusers()); - _state = qs(data.vstate()); - _allLoaded = !data.is_has_more(); + _sourcesStates[index] = qs(data.vstate()); + _sourcesLoaded[index] = !data.is_has_more(); for (const auto &single : data.vuser_stories().v) { - pushToBack(parse(single)); + parseAndApply(single); } - _allChanged.fire({}); }, [](const MTPDstories_allStoriesNotModified &) { }); }).fail([=] { - _loadMoreRequestId = 0; + _loadMoreRequestId[index] = 0; }).send(); } @@ -519,37 +593,64 @@ void Stories::finalizeResolve(FullStoryId id) { } void Stories::applyDeleted(FullStoryId id) { - const auto i = _stories.find(id.peer); - if (i != end(_stories)) { - const auto j = i->second.find(id.story); - if (j != end(i->second)) { - auto story = std::move(j->second); - i->second.erase(j); + const auto removeFromList = [&](StorySourcesList list) { + const auto index = static_cast(list); + auto &sources = _sources[index]; + const auto i = ranges::find( + sources, + id.peer, + &StoriesSourceInfo::id); + if (i != end(sources)) { + sources.erase(i); + _sourcesChanged[index].fire({}); + } + }; + const auto i = _all.find(id.peer); + if (i != end(_all)) { + const auto j = i->second.ids.lower_bound(StoryIdDate{ id.story }); + if (j != end(i->second.ids) && j->id == id.story) { + i->second.ids.erase(j); + if (i->second.ids.empty()) { + _all.erase(i); + removeFromList(StorySourcesList::NotHidden); + removeFromList(StorySourcesList::All); + } + } + } + _deleted.emplace(id); + const auto j = _stories.find(id.peer); + if (j != end(_stories)) { + const auto k = j->second.find(id.story); + if (k != end(j->second)) { + auto story = std::move(k->second); + j->second.erase(k); session().changes().storyUpdated( story.get(), UpdateFlag::Destroyed); removeDependencyStory(story.get()); - if (i->second.empty()) { - _stories.erase(i); + if (j->second.empty()) { + _stories.erase(j); } } } - const auto j = ranges::find(_all, id.peer, [](const StoriesList &list) { - return list.user->id; - }); - if (j != end(_all)) { - const auto removed = j->ids.remove(id.story); - if (removed) { - if (j->ids.empty()) { - _all.erase(j); - } else { - Assert(j->total > 0); - --j->total; - } - _allChanged.fire({}); +} + +void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) { + const auto removeFromList = [&](StorySourcesList from) { + auto &sources = _sources[static_cast(from)]; + const auto i = ranges::find( + sources, + id, + &StoriesSourceInfo::id); + if (i != end(sources)) { + sources.erase(i); } + _sourcesChanged[static_cast(from)].fire({}); + }; + removeFromList(StorySourcesList::NotHidden); + if (list == StorySourcesList::All) { + removeFromList(StorySourcesList::All); } - _deleted.emplace(id); } void Stories::removeDependencyStory(not_null story) { @@ -564,16 +665,36 @@ void Stories::removeDependencyStory(not_null story) { } } -const std::vector &Stories::all() { +void Stories::sort(StorySourcesList list) { + const auto index = static_cast(list); + auto &sources = _sources[index]; + const auto self = _owner->session().user()->id; + const auto proj = [&](const StoriesSourceInfo &info) { + const auto key = int64(info.last) + + (info.premium ? (int64(1) << 48) : 0) + + (info.unread ? (int64(1) << 49) : 0) + + ((info.id == self) ? (int64(1) << 50) : 0); + return std::make_pair(key, info.id); + }; + ranges::sort(sources, ranges::greater(), proj); + _sourcesChanged[index].fire({}); +} + +const base::flat_map &Stories::all() const { return _all; } -bool Stories::allLoaded() const { - return _allLoaded; +const std::vector &Stories::sources( + StorySourcesList list) const { + return _sources[static_cast(list)]; } -rpl::producer<> Stories::allChanged() const { - return _allChanged.events(); +bool Stories::sourcesLoaded(StorySourcesList list) const { + return _sourcesLoaded[static_cast(list)]; +} + +rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const { + return _sourcesChanged[static_cast(list)].events(); } rpl::producer Stories::itemsChanged() const { @@ -621,35 +742,21 @@ void Stories::resolve(FullStoryId id, Fn done) { } } -void Stories::pushToBack(StoriesList &&list) { - const auto i = ranges::find(_all, list.user, &StoriesList::user); - if (i != end(_all)) { - if (*i == list) { - return; - } - *i = std::move(list); - } else { - _all.push_back(std::move(list)); - } -} - void Stories::loadAround(FullStoryId id) { - const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) { - return list.user->id; - }); + const auto i = _all.find(id.peer); if (i == end(_all)) { return; } - const auto j = ranges::find(i->ids, id.story); - if (j == end(i->ids)) { + const auto j = i->second.ids.lower_bound(StoryIdDate{ id.story }); + if (j == end(i->second.ids) || j->id != id.story) { return; } const auto ignore = [&] { const auto side = kIgnorePreloadAroundIfLoaded; - const auto left = ranges::min(int(j - begin(i->ids)), side); - const auto right = ranges::min(int(end(i->ids) - j), side); + const auto left = ranges::min(int(j - begin(i->second.ids)), side); + const auto right = ranges::min(int(end(i->second.ids) - j), side); for (auto k = j - left; k != j + right; ++k) { - const auto maybeStory = lookup({ id.peer, *k }); + const auto maybeStory = lookup({ id.peer, k->id }); if (!maybeStory && maybeStory.error() == NoStory::Unknown) { return false; } @@ -660,29 +767,43 @@ void Stories::loadAround(FullStoryId id) { return; } const auto side = kPreloadAroundCount; - const auto left = ranges::min(int(j - begin(i->ids)), side); - const auto right = ranges::min(int(end(i->ids) - j), side); + const auto left = ranges::min(int(j - begin(i->second.ids)), side); + const auto right = ranges::min(int(end(i->second.ids) - j), side); const auto from = j - left; const auto till = j + right; for (auto k = from; k != till; ++k) { - resolve({ id.peer, *k }, nullptr); + resolve({ id.peer, k->id }, nullptr); } } void Stories::markAsRead(FullStoryId id, bool viewed) { - const auto i = ranges::find(_all, id.peer, [](const StoriesList &list) { - return list.user->id; - }); + const auto i = _all.find(id.peer); Assert(i != end(_all)); - if (i->readTill >= id.story) { + if (i->second.readTill >= id.story) { return; } else if (!_markReadPending.contains(id.peer)) { sendMarkAsReadRequests(); } _markReadPending.emplace(id.peer); - i->readTill = id.story; + const auto wasUnread = i->second.unread(); + i->second.readTill = id.story; + const auto nowUnread = i->second.unread(); + if (wasUnread != nowUnread) { + const auto refreshInList = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + id.peer, + &StoriesSourceInfo::id); + if (i != end(sources)) { + i->unread = nowUnread; + sort(list); + } + }; + refreshInList(StorySourcesList::All); + refreshInList(StorySourcesList::NotHidden); + } _markReadTimer.callOnce(kMarkAsReadDelay); - _allChanged.fire({}); } void Stories::sendMarkAsReadRequest( @@ -721,12 +842,9 @@ void Stories::sendMarkAsReadRequests() { ++i; continue; } - const auto j = ranges::find(_all, peerId, []( - const StoriesList &list) { - return list.user->id; - }); + const auto j = _all.find(peerId); if (j != end(_all)) { - sendMarkAsReadRequest(j->user, j->readTill); + sendMarkAsReadRequest(j->second.user, j->second.readTill); } i = _markReadPending.erase(i); } diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index e41312f5e..dc7b99141 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -22,6 +22,21 @@ namespace Data { class Session; +struct StoryIdDate { + StoryId id = 0; + TimeId date = 0; + + [[nodiscard]] bool valid() const { + return id != 0; + } + explicit operator bool() const { + return valid(); + } + + friend inline auto operator<=>(StoryIdDate, StoryIdDate) = default; + friend inline bool operator==(StoryIdDate, StoryIdDate) = default; +}; + struct StoryMedia { std::variant, not_null> data; @@ -35,7 +50,7 @@ struct StoryView { friend inline bool operator==(StoryView, StoryView) = default; }; -class Story { +class Story final { public: Story( StoryId id, @@ -48,6 +63,7 @@ public: [[nodiscard]] not_null peer() const; [[nodiscard]] StoryId id() const; + [[nodiscard]] StoryIdDate idDate() const; [[nodiscard]] FullStoryId fullId() const; [[nodiscard]] TimeId date() const; [[nodiscard]] const StoryMedia &media() const; @@ -89,16 +105,28 @@ private: }; -struct StoriesList { - not_null user; - base::flat_set ids; - StoryId readTill = 0; - int total = 0; +struct StoriesSourceInfo { + PeerId id = 0; + TimeId last = 0; + bool unread = false; + bool premium = false; bool hidden = false; + friend inline bool operator==( + StoriesSourceInfo, + StoriesSourceInfo) = default; +}; + +struct StoriesSource { + not_null user; + base::flat_set ids; + StoryId readTill = 0; + bool hidden = false; + + [[nodiscard]] StoriesSourceInfo info() const; [[nodiscard]] bool unread() const; - friend inline bool operator==(StoriesList, StoriesList) = default; + friend inline bool operator==(StoriesSource, StoriesSource) = default; }; enum class NoStory : uchar { @@ -106,6 +134,13 @@ enum class NoStory : uchar { Deleted, }; +enum class StorySourcesList : uchar { + NotHidden, + All, +}; + +inline constexpr auto kStorySourcesListCount = 2; + class Stories final { public: explicit Stories(not_null owner); @@ -122,13 +157,16 @@ public: not_null dependent, not_null dependency); - void loadMore(); + void loadMore(StorySourcesList list); void apply(const MTPDupdateStory &data); void loadAround(FullStoryId id); - [[nodiscard]] const std::vector &all(); - [[nodiscard]] bool allLoaded() const; - [[nodiscard]] rpl::producer<> allChanged() const; + [[nodiscard]] const base::flat_map &all() const; + [[nodiscard]] const std::vector &sources( + StorySourcesList list) const; + [[nodiscard]] bool sourcesLoaded(StorySourcesList list) const; + [[nodiscard]] rpl::producer<> sourcesChanged( + StorySourcesList list) const; [[nodiscard]] rpl::producer itemsChanged() const; [[nodiscard]] base::expected, NoStory> lookup( @@ -145,11 +183,11 @@ public: Fn)> done); private: - [[nodiscard]] StoriesList parse(const MTPUserStories &stories); + void parseAndApply(const MTPUserStories &stories); [[nodiscard]] Story *parseAndApply( not_null peer, const MTPDstoryItem &data); - StoryId parseAndApply( + StoryIdDate parseAndApply( not_null peer, const MTPstoryItem &story); void processResolvedStories( @@ -158,13 +196,16 @@ private: void sendResolveRequests(); void finalizeResolve(FullStoryId id); - void pushToBack(StoriesList &&list); void applyDeleted(FullStoryId id); + void applyDeletedFromSources(PeerId id, StorySourcesList list); void removeDependencyStory(not_null story); + void sort(StorySourcesList list); void sendMarkAsReadRequests(); void sendMarkAsReadRequest(not_null peer, StoryId tillId); + void requestUserStories(not_null user); + const not_null _owner; base::flat_map< PeerId, @@ -182,17 +223,20 @@ private: not_null, base::flat_set>> _dependentMessages; - std::vector _all; - rpl::event_stream<> _allChanged; - rpl::event_stream _itemsChanged; - QString _state; - bool _allLoaded = false; + base::flat_map _all; + std::vector _sources[kStorySourcesListCount]; + rpl::event_stream<> _sourcesChanged[kStorySourcesListCount]; + bool _sourcesLoaded[kStorySourcesListCount] = { false }; + QString _sourcesStates[kStorySourcesListCount]; - mtpRequestId _loadMoreRequestId = 0; + mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 }; + + rpl::event_stream _itemsChanged; base::flat_set _markReadPending; base::Timer _markReadTimer; base::flat_set _markReadRequests; + base::flat_set> _requestingUserStories; StoryId _viewsStoryId = 0; std::optional _viewsOffset; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index bbc9a2c49..fb15560a3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -142,7 +142,9 @@ InnerWidget::InnerWidget( , _controller(controller) , _stories(std::make_unique( this, - Stories::ContentForSession(&controller->session()), + Stories::ContentForSession( + &controller->session(), + Data::StorySourcesList::NotHidden), [=] { return _stories->height() - _visibleTop; })) , _shownList(controller->session().data().chatsList()->indexed()) , _st(&st::defaultDialogRow) @@ -339,7 +341,7 @@ InnerWidget::InnerWidget( _stories->clicks( ) | rpl::start_with_next([=](uint64 id) { - _controller->openPeerStories(PeerId(int64(id))); + _controller->openPeerStories(PeerId(int64(id)), {}); }, lifetime()); _stories->showProfileRequests( @@ -365,7 +367,8 @@ InnerWidget::InnerWidget( _stories->loadMoreRequests( ) | rpl::start_with_next([=] { - session().data().stories().loadMore(); + session().data().stories().loadMore( + Data::StorySourcesList::NotHidden); }, lifetime()); handleChatListEntryRefreshes(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 7b127cd3c..37016cfa5 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -15,8 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "ui/painter.h" -#include "history/history.h" // #TODO stories testing - namespace Dialogs::Stories { namespace { @@ -51,12 +49,13 @@ private: class State final { public: - explicit State(not_null data); + State(not_null data, Data::StorySourcesList list); [[nodiscard]] Content next(); private: const not_null _data; + const Data::StorySourcesList _list; base::flat_map, std::shared_ptr> _userpics; }; @@ -122,27 +121,23 @@ void PeerUserpic::processNewPhoto() { }, _subscribed->downloadLifetime); } -State::State(not_null data) -: _data(data) { +State::State(not_null data, Data::StorySourcesList list) +: _data(data) +, _list(list) { } Content State::next() { auto result = Content(); -#if 1 // #TODO stories testing const auto &all = _data->all(); - result.users.reserve(all.size()); - for (const auto &list : all) { + const auto &sources = _data->sources(_list); + result.users.reserve(sources.size()); + for (const auto &info : sources) { + const auto i = all.find(info.id); + Assert(i != end(all)); + const auto &source = i->second; + auto userpic = std::shared_ptr(); - const auto user = list.user; -#else - const auto list = _data->owner().chatsList(); - const auto &all = list->indexed()->all(); - result.users.reserve(all.size()); - for (const auto &entry : all) { - if (const auto history = entry->history()) { - if (const auto user = history->peer->asUser(); user && !user->isBot()) { - auto userpic = std::shared_ptr(); -#endif + const auto user = source.user; if (const auto i = _userpics.find(user); i != end(_userpics)) { userpic = i->second; } else { @@ -153,41 +148,25 @@ Content State::next() { .id = uint64(user->id.value), .name = user->shortName(), .userpic = std::move(userpic), -#if 1 // #TODO stories testing - .unread = list.unread(), -#else - .unread = history->chatListBadgesState().unread + .unread = info.unread, }); } -#endif - }); - } - ranges::stable_partition(result.users, [](const User &user) { - return user.unread; - }); return result; } } // namespace -rpl::producer ContentForSession(not_null session) { +rpl::producer ContentForSession( + not_null session, + Data::StorySourcesList list) { return [=](auto consumer) { auto result = rpl::lifetime(); const auto stories = &session->data().stories(); - const auto state = result.make_state(stories); + const auto state = result.make_state(stories, list); rpl::single( rpl::empty ) | rpl::then( -#if 1 // #TODO stories testing - stories->allChanged() -#else - rpl::merge( - session->data().chatsListChanges( - ) | rpl::filter( - rpl::mappers::_1 == nullptr - ) | rpl::to_empty, - session->data().unreadBadgeChanges()) -#endif + stories->sourcesChanged(list) ) | rpl::start_with_next([=] { consumer.put_next(state->next()); }, result); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h index dc81f2528..1feb6a77a 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h @@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace Data { +enum class StorySourcesList : uchar; +} // namespace Data + namespace Main { class Session; } // namespace Main @@ -16,6 +20,7 @@ namespace Dialogs::Stories { struct Content; [[nodiscard]] rpl::producer ContentForSession( - not_null session); + not_null session, + Data::StorySourcesList list); } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 68eaa0d1e..0a1b1a8a0 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -297,7 +297,8 @@ ClickHandlerPtr JumpToStoryClickHandler( ? separate->sessionController() : peer->session().tryResolveWindow(); if (controller) { - controller->openPeerStory(peer, storyId); + // #TODO stories decide context + controller->openPeerStory(peer, storyId, {}); } }); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 55062a7d3..716da7fee 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -386,25 +386,30 @@ auto Controller::stickerOrEmojiChosen() const } void Controller::show( - const std::vector &lists, - int index, - int subindex) { - Expects(index >= 0 && index < lists.size()); - Expects(subindex >= 0 && subindex < lists[index].ids.size()); - - showSiblings(lists, index); - - const auto &list = lists[index]; - const auto id = *(begin(list.ids) + subindex); - const auto storyId = FullStoryId{ - .peer = list.user->id, - .story = id, - }; - const auto maybeStory = list.user->owner().stories().lookup(storyId); - if (!maybeStory) { + not_null story, + Data::StorySourcesList list) { + auto &stories = story->owner().stories(); + const auto &all = stories.all(); + const auto &sources = stories.sources(list); + const auto storyId = story->fullId(); + const auto id = storyId.story; + const auto i = ranges::find( + sources, + storyId.peer, + &Data::StoriesSourceInfo::id); + if (i == end(sources)) { return; } - const auto story = *maybeStory; + const auto j = all.find(storyId.peer); + if (j == end(all)) { + return; + } + const auto &source = j->second; + const auto k = source.ids.lower_bound(Data::StoryIdDate{ id }); + if (k == end(source.ids) || k->id != id) { + return; + } + showSiblings(&story->session(), sources, (i - begin(sources))); const auto guard = gsl::finally([&] { _paused = false; _started = false; @@ -414,10 +419,10 @@ void Controller::show( _photoPlayback = nullptr; } }); - if (_list != list) { - _list = list; + if (_source != source) { + _source = source; } - _index = subindex; + _index = (k - begin(source.ids)); _waitingForId = {}; if (_shown == storyId) { @@ -431,16 +436,16 @@ void Controller::show( unfocusReply(); } - _header->show({ .user = list.user, .date = story->date() }); - _slider->show({ .index = _index, .total = list.total }); - _replyArea->show({ .user = list.user, .id = id }); + _header->show({ .user = source.user, .date = story->date() }); + _slider->show({ .index = _index, .total = int(source.ids.size()) }); + _replyArea->show({ .user = source.user, .id = id }); _recentViews->show({ .list = story->recentViewers(), .total = story->views(), - .valid = list.user->isSelf(), + .valid = source.user->isSelf(), }); - const auto session = &list.user->session(); + const auto session = &story->session(); if (_session != session) { _session = session; _sessionLifetime = session->changes().storyUpdates( @@ -458,14 +463,13 @@ void Controller::show( }, _sessionLifetime); } - auto &stories = session->data().stories(); - if (int(lists.size()) - index < kPreloadUsersCount) { - stories.loadMore(); + if (int(sources.end() - i) < kPreloadUsersCount) { + stories.loadMore(list); } stories.loadAround(storyId); updatePlayingAllowed(); - list.user->updateFull(); + source.user->updateFull(); } void Controller::updatePlayingAllowed() { @@ -492,21 +496,33 @@ void Controller::setPlayingAllowed(bool allowed) { } void Controller::showSiblings( - const std::vector &lists, + not_null session, + const std::vector &sources, int index) { - showSibling(_siblingLeft, (index > 0) ? &lists[index - 1] : nullptr); + showSibling( + _siblingLeft, + session, + (index > 0) ? sources[index - 1].id : PeerId()); showSibling( _siblingRight, - (index + 1 < lists.size()) ? &lists[index + 1] : nullptr); + session, + (index + 1 < sources.size()) ? sources[index + 1].id : PeerId()); } void Controller::showSibling( std::unique_ptr &sibling, - const Data::StoriesList *list) { - if (!list || list->ids.empty()) { + not_null session, + PeerId peerId) { + if (!peerId) { sibling = nullptr; - } else if (!sibling || !sibling->shows(*list)) { - sibling = std::make_unique(this, *list); + return; + } + const auto &all = session->data().stories().all(); + const auto i = all.find(peerId); + if (i == end(all)) { + sibling = nullptr; + } else if (!sibling || !sibling->shows(i->second)) { + sibling = std::make_unique(this, i->second); } } @@ -552,19 +568,19 @@ void Controller::maybeMarkAsRead(const Player::TrackState &state) { } void Controller::markAsRead() { - Expects(_list.has_value()); + Expects(_source.has_value()); - _list->user->owner().stories().markAsRead(_shown, _started); + _source->user->owner().stories().markAsRead(_shown, _started); } bool Controller::subjumpAvailable(int delta) const { const auto index = _index + delta; if (index < 0) { return _siblingLeft && _siblingLeft->shownId().valid(); - } else if (index >= _list->total) { + } else if (index >= int(_source->ids.size())) { return _siblingRight && _siblingRight->shownId().valid(); } - return index >= 0 && index < _list->total; + return index >= 0 && index < int(_source->ids.size()); } bool Controller::subjumpFor(int delta) { @@ -575,32 +591,32 @@ bool Controller::subjumpFor(int delta) { if (index < 0) { if (_siblingLeft && _siblingLeft->shownId().valid()) { return jumpFor(-1); - } else if (!_list || _list->ids.empty()) { + } else if (!_source || _source->ids.empty()) { return false; } subjumpTo(0); return true; - } else if (index >= _list->total) { + } else if (index >= int(_source->ids.size())) { return _siblingRight && _siblingRight->shownId().valid() && jumpFor(1); - } else if (index < _list->ids.size()) { + } else { subjumpTo(index); } return true; } void Controller::subjumpTo(int index) { - Expects(_list.has_value()); - Expects(index >= 0 && index < _list->ids.size()); + Expects(_source.has_value()); + Expects(index >= 0 && index < _source->ids.size()); const auto id = FullStoryId{ - .peer = _list->user->id, - .story = *(begin(_list->ids) + index) + .peer = _source->user->id, + .story = (begin(_source->ids) + index)->id, }; - auto &stories = _list->user->owner().stories(); + auto &stories = _source->user->owner().stories(); if (stories.lookup(id)) { - _delegate->storiesJumpTo(&_list->user->session(), id); + _delegate->storiesJumpTo(&_source->user->session(), id); } else if (_waitingForId != id) { _waitingForId = id; stories.loadAround(id); @@ -609,9 +625,9 @@ void Controller::subjumpTo(int index) { void Controller::checkWaitingFor() { Expects(_waitingForId.valid()); - Expects(_list.has_value()); + Expects(_source.has_value()); - auto &stories = _list->user->owner().stories(); + auto &stories = _source->user->owner().stories(); const auto maybe = stories.lookup(_waitingForId); if (!maybe) { if (maybe.error() == Data::NoStory::Deleted) { @@ -620,7 +636,7 @@ void Controller::checkWaitingFor() { return; } _delegate->storiesJumpTo( - &_list->user->session(), + &_source->user->session(), base::take(_waitingForId)); } @@ -633,7 +649,7 @@ bool Controller::jumpFor(int delta) { return true; } } else if (delta == 1) { - if (_list && _index + 1 >= _list->total) { + if (_source && _index + 1 >= int(_source->ids.size())) { markAsRead(); } if (const auto right = _siblingRight.get()) { @@ -665,7 +681,7 @@ void Controller::setMenuShown(bool shown) { } bool Controller::canDownload() const { - return _list && _list->user->isSelf(); + return _source && _source->user->isSelf(); } void Controller::repaintSibling(not_null sibling) { @@ -706,7 +722,7 @@ Fn)> Controller::viewsGotMoreCallback() { return crl::guard(&_viewsLoadGuard, [=]( const std::vector &result) { if (_viewsSlice.list.empty()) { - auto &stories = _list->user->owner().stories(); + auto &stories = _source->user->owner().stories(); if (const auto maybeStory = stories.lookup(_shown)) { _viewsSlice = { .list = result, @@ -728,11 +744,11 @@ Fn)> Controller::viewsGotMoreCallback() { } void Controller::refreshViewsFromData() { - Expects(_list.has_value()); + Expects(_source.has_value()); - auto &stories = _list->user->owner().stories(); + auto &stories = _source->user->owner().stories(); const auto maybeStory = stories.lookup(_shown); - if (!maybeStory || !_list->user->isSelf()) { + if (!maybeStory || !_source->user->isSelf()) { _viewsSlice = {}; return; } @@ -750,11 +766,11 @@ void Controller::refreshViewsFromData() { } bool Controller::sliceViewsTo(PeerId offset) { - Expects(_list.has_value()); + Expects(_source.has_value()); - auto &stories = _list->user->owner().stories(); + auto &stories = _source->user->owner().stories(); const auto maybeStory = stories.lookup(_shown); - if (!maybeStory || !_list->user->isSelf()) { + if (!maybeStory || !_source->user->isSelf()) { _viewsSlice = {}; return true; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index b8954e9e3..d6081d84b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -20,7 +20,6 @@ struct FileChosen; } // namespace ChatHelpers namespace Data { -struct StoriesList; struct FileOrigin; } // namespace Data @@ -100,10 +99,7 @@ public: [[nodiscard]] auto stickerOrEmojiChosen() const -> rpl::producer; - void show( - const std::vector &lists, - int index, - int subindex); + void show(not_null story, Data::StorySourcesList list); void ready(); void updateVideoPlayback(const Player::TrackState &state); @@ -142,11 +138,13 @@ private: void setPlayingAllowed(bool allowed); void showSiblings( - const std::vector &lists, + not_null session, + const std::vector &lists, int index); void showSibling( std::unique_ptr &sibling, - const Data::StoriesList *list); + not_null session, + PeerId peerId); void subjumpTo(int index); void checkWaitingFor(); @@ -180,7 +178,7 @@ private: FullStoryId _shown; TextWithEntities _captionText; - std::optional _list; + std::optional _source; FullStoryId _waitingForId; int _index = 0; bool _started = false; diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index 90a393954..375bf1f23 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -228,10 +228,10 @@ bool Sibling::LoaderVideo::updateAfterGoodCheck() { Sibling::Sibling( not_null controller, - const Data::StoriesList &list) + const Data::StoriesSource &source) : _controller(controller) -, _id{ list.user->id, list.ids.front() } -, _peer(list.user) { +, _id{ source.user->id, source.ids.front().id } +, _peer(source.user) { checkStory(); _goodShown.stop(); } @@ -279,10 +279,10 @@ not_null Sibling::peer() const { return _peer; } -bool Sibling::shows(const Data::StoriesList &list) const { - Expects(!list.ids.empty()); +bool Sibling::shows(const Data::StoriesSource &source) const { + Expects(!source.ids.empty()); - return _id == FullStoryId{ list.user->id, list.ids.front() }; + return _id == FullStoryId{ source.user->id, source.ids.front().id }; } SiblingView Sibling::view(const SiblingLayout &layout, float64 over) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h index f6eb4edc7..ed4b8789c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.h +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h @@ -26,12 +26,12 @@ class Sibling final : public base::has_weak_ptr { public: Sibling( not_null controller, - const Data::StoriesList &list); + const Data::StoriesSource &source); ~Sibling(); [[nodiscard]] FullStoryId shownId() const; [[nodiscard]] not_null peer() const; - [[nodiscard]] bool shows(const Data::StoriesList &list) const; + [[nodiscard]] bool shows(const Data::StoriesSource &source) const; [[nodiscard]] SiblingView view( const SiblingLayout &layout, diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 18a4bb499..0fbc38721 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -22,11 +22,8 @@ View::View(not_null delegate) View::~View() = default; -void View::show( - const std::vector &lists, - int index, - int subindex) { - _controller->show(lists, index, subindex); +void View::show(not_null story, Data::StorySourcesList list) { + _controller->show(story, list); } void View::ready() { diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index d01bd3d61..a671bdbde 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -8,7 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once namespace Data { -struct StoriesList; +class Story; +enum class StorySourcesList : uchar; struct FileOrigin; } // namespace Data @@ -52,10 +53,7 @@ public: explicit View(not_null delegate); ~View(); - void show( - const std::vector &lists, - int index, - int subindex); + void show(not_null story, Data::StorySourcesList list); void ready(); [[nodiscard]] bool canDownload() const; diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index 79b835461..aed2fde58 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -16,6 +16,7 @@ class HistoryItem; namespace Data { class Story; +enum class StorySourcesList : uchar; } // namespace Data namespace Window { @@ -74,10 +75,12 @@ public: OpenRequest( Window::SessionController *controller, not_null story, + Data::StorySourcesList list, bool continueStreaming = false, crl::time startTime = 0) : _controller(controller) , _story(story) + , _storiesList(list) , _continueStreaming(continueStreaming) , _startTime(startTime) { } @@ -105,6 +108,9 @@ public: [[nodiscard]] Data::Story *story() const { return _story; } + [[nodiscard]] Data::StorySourcesList storiesList() const { + return _storiesList; + } [[nodiscard]] std::optional cloudTheme() const { return _cloudTheme; @@ -127,6 +133,7 @@ private: DocumentData *_document = nullptr; PhotoData *_photo = nullptr; Data::Story *_story = nullptr; + Data::StorySourcesList _storiesList = {}; PeerData *_peer = nullptr; HistoryItem *_item = nullptr; MsgId _topicRootId = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index df290b574..00bb0f645 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -4973,15 +4973,13 @@ void OverlayWidget::setContext( _topicRootId = MsgId(); _history = nullptr; _peer = nullptr; - const auto &all = story->peer->owner().stories().all(); - const auto i = ranges::find( - all, - story->peer, - &Data::StoriesList::user); - Assert(i != end(all)); - const auto j = ranges::find(i->ids, story->id); setStoriesPeer(story->peer); - _stories->show(all, (i - begin(all)), j - begin(i->ids)); + auto &stories = story->peer->owner().stories(); + const auto maybeStory = stories.lookup( + { story->peer->id, story->id }); + if (maybeStory) { + _stories->show(*maybeStory, story->list); + } } else { _message = nullptr; _topicRootId = MsgId(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index a32c0ef59..837678675 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -30,6 +30,7 @@ enum class activation : uchar; namespace Data { class PhotoMedia; class DocumentMedia; +enum class StorySourcesList : uchar; } // namespace Data namespace Ui { @@ -306,6 +307,7 @@ private: struct StoriesContext { not_null peer; StoryId id = 0; + Data::StorySourcesList list = {}; }; void setContext(std::variant< v::null_t, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index b30e9dd13..793964d9e 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2466,28 +2466,33 @@ Ui::ChatThemeBackgroundData SessionController::backgroundData( void SessionController::openPeerStory( not_null peer, - StoryId storyId) { + StoryId storyId, + Data::StorySourcesList list) { using namespace Media::View; using namespace Data; auto &stories = session().data().stories(); if (const auto from = stories.lookup({ peer->id, storyId })) { - window().openInMediaView(OpenRequest(this, *from)); + window().openInMediaView(OpenRequest(this, *from, list)); } } -void SessionController::openPeerStories(PeerId peerId) { +void SessionController::openPeerStories( + PeerId peerId, + Data::StorySourcesList list) { using namespace Media::View; using namespace Data; auto &stories = session().data().stories(); const auto &all = stories.all(); - const auto i = ranges::find(all, peerId, [](const StoriesList &list) { - return list.user->id; - }); - if (i != end(all) && !i->ids.empty()) { - const auto j = i->ids.lower_bound(i->readTill + 1); - openPeerStory(i->user, j != i->ids.end() ? *j : i->ids.front()); + const auto i = all.find(peerId); + if (i != end(all)) { + const auto j = i->second.ids.lower_bound( + StoryIdDate{ i->second.readTill + 1 }); + openPeerStory( + i->second.user, + j != i->second.ids.end() ? j->id : i->second.ids.front().id, + list); } } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 07f763ca8..589169655 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -29,6 +29,10 @@ namespace Adaptive { enum class WindowLayout; } // namespace Adaptive +namespace Data { +enum class StorySourcesList : uchar; +} // namespace Data + namespace ChatHelpers { class TabbedSelector; class EmojiInteractions; @@ -564,8 +568,11 @@ public: return _peerThemeOverride.value(); } - void openPeerStory(not_null peer, StoryId storyId); - void openPeerStories(PeerId peerId); + void openPeerStory( + not_null peer, + StoryId storyId, + Data::StorySourcesList list); + void openPeerStories(PeerId peerId, Data::StorySourcesList list); struct PaintContextArgs { not_null theme; From e7c0385aea8ddaca856a16b18e6958b0d021e586 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 2 Jun 2023 21:14:15 +0400 Subject: [PATCH 055/260] Show hidden stories above contacts list. --- Telegram/Resources/langs/lang.strings | 2 + .../boxes/peer_list_controllers.cpp | 48 +++++++++++++++- Telegram/SourceFiles/data/data_stories.cpp | 56 ++++++++++++++++++- Telegram/SourceFiles/data/data_stories.h | 2 + .../dialogs/dialogs_inner_widget.cpp | 15 +---- .../dialogs/ui/dialogs_stories_content.cpp | 1 + .../dialogs/ui/dialogs_stories_list.cpp | 20 ++++--- .../dialogs/ui/dialogs_stories_list.h | 2 + .../media/view/media_view_overlay_widget.cpp | 6 +- 9 files changed, 130 insertions(+), 22 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 892983de5..220683f8e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3789,6 +3789,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_userpic_builder_color_subtitle" = "Choose background"; "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji"; +"lng_stories_hide_to_contacts" = "Archive"; +"lng_stories_show_in_chats" = "Unarchive"; "lng_stories_row_count#one" = "{count} Story"; "lng_stories_row_count#other" = "{count} Stories"; "lng_stories_row_unread_and_one" = "{accumulated}, {user}"; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 0679d4dc9..dc757dc81 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -37,6 +37,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_profile.h" #include "styles/style_dialogs.h" +#include "data/data_stories.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" + + namespace { constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000); @@ -51,10 +56,14 @@ object_ptr PrepareContactsBox( &sessionController->session()); const auto raw = controller.get(); auto init = [=](not_null box) { + using namespace Dialogs; + struct State { - QPointer toggleSort; + Stories::List *stories = nullptr; + QPointer<::Ui::IconButton> toggleSort; Mode mode = ContactsBoxController::SortMode::Online; }; + const auto state = box->lifetime().make_state(); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); box->addLeftButton( @@ -69,6 +78,43 @@ object_ptr PrepareContactsBox( online ? &st::contactsSortOnlineIconOver : nullptr); }); raw->setSortMode(Mode::Online); + + auto stories = object_ptr( + box, + Stories::ContentForSession( + &sessionController->session(), + Data::StorySourcesList::All), + [=] { return state->stories->height() - box->scrollTop(); }); + const auto raw = state->stories = stories.data(); + box->peerListSetAboveWidget(std::move(stories)); + + raw->entered( + ) | rpl::start_with_next([=] { + //clearSelection(); + }, raw->lifetime()); + + raw->clicks( + ) | rpl::start_with_next([=](uint64 id) { + sessionController->openPeerStories(PeerId(int64(id)), {}); + }, raw->lifetime()); + + raw->showProfileRequests( + ) | rpl::start_with_next([=](uint64 id) { + sessionController->showPeerInfo(PeerId(int64(id))); + }, raw->lifetime()); + + raw->toggleShown( + ) | rpl::start_with_next([=](Stories::ToggleShownRequest request) { + sessionController->session().data().stories().toggleHidden( + PeerId(int64(request.id)), + !request.shown); + }, raw->lifetime()); + + raw->loadMoreRequests( + ) | rpl::start_with_next([=] { + sessionController->session().data().stories().loadMore( + Data::StorySourcesList::All); + }, raw->lifetime()); }; return Box(std::move(controller), std::move(init)); } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index a3f17a0da..b801dad8d 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -350,9 +350,11 @@ void Stories::parseAndApply(const MTPUserStories &stories) { const auto peerId = peerFromUser(data.vuser_id()); const auto readTill = data.vmax_read_id().value_or_empty(); const auto count = int(data.vstories().v.size()); + const auto user = _owner->peer(peerId)->asUser(); auto result = StoriesSource{ - .user = _owner->peer(peerId)->asUser(), + .user = user, .readTill = readTill, + .hidden = user->hasStoriesHidden(), }; const auto &list = data.vstories().v; result.ids.reserve(list.size()); @@ -806,6 +808,58 @@ void Stories::markAsRead(FullStoryId id, bool viewed) { _markReadTimer.callOnce(kMarkAsReadDelay); } +void Stories::toggleHidden(PeerId peerId, bool hidden) { + const auto user = _owner->peer(peerId)->asUser(); + Assert(user != nullptr); + if (user->hasStoriesHidden() != hidden) { + user->setFlags(hidden + ? (user->flags() | UserDataFlag::StoriesHidden) + : (user->flags() & ~UserDataFlag::StoriesHidden)); + session().api().request(MTPcontacts_ToggleStoriesHidden( + user->inputUser, + MTP_bool(hidden) + )).send(); + } + + const auto i = _all.find(peerId); + if (i == end(_all)) { + return; + } + i->second.hidden = hidden; + const auto main = static_cast(StorySourcesList::NotHidden); + const auto all = static_cast(StorySourcesList::All); + if (hidden) { + const auto i = ranges::find( + _sources[main], + peerId, + &StoriesSourceInfo::id); + if (i != end(_sources[main])) { + _sources[main].erase(i); + _sourcesChanged[main].fire({}); + } + const auto j = ranges::find(_sources[all], peerId, &StoriesSourceInfo::id); + if (j != end(_sources[all])) { + j->hidden = hidden; + _sourcesChanged[all].fire({}); + } + } else { + const auto i = ranges::find( + _sources[all], + peerId, + &StoriesSourceInfo::id); + if (i != end(_sources[all])) { + i->hidden = hidden; + _sourcesChanged[all].fire({}); + + auto &sources = _sources[main]; + if (!ranges::contains(sources, peerId, &StoriesSourceInfo::id)) { + sources.push_back(*i); + sort(StorySourcesList::NotHidden); + } + } + } +} + void Stories::sendMarkAsReadRequest( not_null peer, StoryId tillId) { diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index dc7b99141..ded86dce3 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -176,6 +176,8 @@ public: [[nodiscard]] bool isQuitPrevent(); void markAsRead(FullStoryId id, bool viewed); + void toggleHidden(PeerId peerId, bool hidden); + static constexpr auto kViewsPerPage = 50; void loadViewsSlice( StoryId id, diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index fb15560a3..cb559842e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -351,18 +351,9 @@ InnerWidget::InnerWidget( _stories->toggleShown( ) | rpl::start_with_next([=](Stories::ToggleShownRequest request) { - const auto peerId = PeerId(int64(request.id)); - const auto user = session().data().peer(peerId)->asUser(); - Assert(user != nullptr); - if (user->hasStoriesHidden() == request.shown) { - user->setFlags(request.shown - ? (user->flags() & ~UserDataFlag::StoriesHidden) - : (user->flags() | UserDataFlag::StoriesHidden)); - session().api().request(MTPcontacts_ToggleStoriesHidden( - user->inputUser, - MTP_bool(!request.shown) - )).send(); - } + session().data().stories().toggleHidden( + PeerId(int64(request.id)), + !request.shown); }, lifetime()); _stories->loadMoreRequests( diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 37016cfa5..bb9fd7578 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -149,6 +149,7 @@ Content State::next() { .name = user->shortName(), .userpic = std::move(userpic), .unread = info.unread, + .hidden = info.hidden, }); } return result; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 5b2e24b08..78a3dc3a3 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -99,6 +99,7 @@ void List::showContent(Content &&content) { item.nameCache = QImage(); } item.user.unread = user.unread; + item.user.hidden = user.hidden; } else { _data.items.emplace_back(Item{ .user = user }); } @@ -429,8 +430,8 @@ void List::paintEvent(QPaintEvent *e) { }); p.setBrush(gradient); p.drawEllipse(outer); - p.setOpacity(1.); } + p.setOpacity(1.); }, [&](Single single) { Expects(single.itemSmall || single.itemFull); @@ -710,19 +711,24 @@ void List::contextMenuEvent(QContextMenuEvent *e) { const auto id = item.user.id; const auto hidden = item.user.hidden; - _menu->addAction(u"View Profile"_q, [=] { + _menu->addAction(tr::lng_context_view_profile(tr::now), [=] { _showProfileRequests.fire_copy(id); }); - _menu->addAction(hidden ? u"Show in Chats"_q : u"Hide"_q, [=] { - _toggleShown.fire({ .id = id, .shown = hidden }); - }); - QObject::connect(_menu.get(), &QObject::destroyed, [=] { + _menu->addAction(hidden + ? tr::lng_stories_show_in_chats(tr::now) + : tr::lng_stories_hide_to_contacts(tr::now), + [=] { _toggleShown.fire({ .id = id, .shown = hidden }); }); + const auto updateAfterMenuDestroyed = [=] { const auto globalPosition = QCursor::pos(); if (rect().contains(mapFromGlobal(globalPosition))) { _lastMousePosition = globalPosition; updateSelected(); } - }); + }; + QObject::connect( + _menu.get(), + &QObject::destroyed, + crl::guard(&_menuGuard, updateAfterMenuDestroyed)); if (_menu->empty()) { _menu = nullptr; } else { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index fd1dc3d5b..6789765db 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/qt/qt_compare.h" +#include "base/weak_ptr.h" #include "ui/rp_widget.h" class QPainter; @@ -160,6 +161,7 @@ private: int _pressed = -1; base::unique_qptr _menu; + base::has_weak_ptr _menuGuard; }; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 00bb0f645..75c7a43bb 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -3061,7 +3061,11 @@ void OverlayWidget::show(OpenRequest request) { setSession(&photo->session()); if (story) { - setContext(StoriesContext{ story->peer(), story->id() }); + setContext(StoriesContext{ + story->peer(), + story->id(), + request.storiesList(), + }); } else if (contextPeer) { setContext(contextPeer); } else if (contextItem) { From b71d72ca7c028e0cf5a7c34f481c515c480f03da Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 5 Jun 2023 16:10:34 +0400 Subject: [PATCH 056/260] Allow showing stories in different contexts. --- .../boxes/peer_list_controllers.cpp | 4 +- Telegram/SourceFiles/data/data_stories.cpp | 8 +- Telegram/SourceFiles/data/data_stories.h | 28 ++++- .../dialogs/ui/dialogs_stories_content.cpp | 2 +- .../dialogs/ui/dialogs_stories_list.cpp | 15 +-- .../dialogs/ui/dialogs_stories_list.h | 1 + .../SourceFiles/ffmpeg/ffmpeg_utility.cpp | 5 +- .../stories/media_stories_controller.cpp | 101 +++++++++++------- .../media/stories/media_stories_controller.h | 4 +- .../media/stories/media_stories_delegate.h | 7 +- .../media/stories/media_stories_view.cpp | 6 +- .../media/stories/media_stories_view.h | 4 +- .../media/streaming/media_streaming_common.h | 2 + .../media/streaming/media_streaming_file.cpp | 42 +++++--- .../media/streaming/media_streaming_file.h | 16 +-- .../streaming/media_streaming_player.cpp | 10 +- .../media/view/media_view_open_common.h | 20 ++-- .../media/view/media_view_overlay_widget.cpp | 43 ++++++-- .../media/view/media_view_overlay_widget.h | 16 +-- .../window/window_session_controller.cpp | 6 +- .../window/window_session_controller.h | 3 +- 21 files changed, 231 insertions(+), 112 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index dc757dc81..7fa6e5384 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -95,7 +95,9 @@ object_ptr PrepareContactsBox( raw->clicks( ) | rpl::start_with_next([=](uint64 id) { - sessionController->openPeerStories(PeerId(int64(id)), {}); + sessionController->openPeerStories( + PeerId(int64(id)), + Data::StorySourcesList::All); }, raw->lifetime()); raw->showProfileRequests( diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index b801dad8d..69b14f0a7 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -744,7 +744,13 @@ void Stories::resolve(FullStoryId id, Fn done) { } } -void Stories::loadAround(FullStoryId id) { +void Stories::loadAround(FullStoryId id, StoriesContext context) { + if (v::is(context.data)) { + return; + } else if (v::is(context.data) + || v::is(context.data)) { + return; + } const auto i = _all.find(id.peer); if (i == end(_all)) { return; diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index ded86dce3..f8f152b77 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -139,6 +139,32 @@ enum class StorySourcesList : uchar { All, }; +struct StoriesContextSingle { +}; + +struct StoriesContextPeer { +}; + +struct StoriesContextSaved { +}; + +struct StoriesContextArchive { +}; + +struct StoriesContext { + std::variant< + StoriesContextSingle, + StoriesContextPeer, + StoriesContextSaved, + StoriesContextArchive, + StorySourcesList> data; + + friend inline auto operator<=>( + StoriesContext, + StoriesContext) = default; + friend inline bool operator==(StoriesContext, StoriesContext) = default; +}; + inline constexpr auto kStorySourcesListCount = 2; class Stories final { @@ -159,7 +185,7 @@ public: void loadMore(StorySourcesList list); void apply(const MTPDupdateStory &data); - void loadAround(FullStoryId id); + void loadAround(FullStoryId id, StoriesContext context); [[nodiscard]] const base::flat_map &all() const; [[nodiscard]] const std::vector &sources( diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index bb9fd7578..830950416 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -127,7 +127,7 @@ State::State(not_null data, Data::StorySourcesList list) } Content State::next() { - auto result = Content(); + auto result = Content{ .full = (_list == Data::StorySourcesList::All) }; const auto &all = _data->all(); const auto &sources = _data->sources(_list); result.users.reserve(sources.size()); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 78a3dc3a3..ec470a92d 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -265,7 +265,8 @@ List::Layout List::computeLayout() const { + st::defaultDialogRow.photoSize + st::defaultDialogRow.padding.left(); const auto narrow = (width() <= narrowWidth); - const auto smallWidth = st.photo + (itemsCount - 1) * st.shift; + const auto smallCount = std::min(kSmallUserpicsShown, itemsCount); + const auto smallWidth = st.photo + (smallCount - 1) * st.shift; const auto leftSmall = narrow ? ((narrowWidth - smallWidth) / 2 - st.photoLeft) : st.left; @@ -278,7 +279,7 @@ List::Layout List::computeLayout() const { (width() - leftFull + singleFull - 1) / singleFull, itemsCount); const auto startIndexSmall = 0; - const auto endIndexSmall = std::min(kSmallUserpicsShown, itemsCount); + const auto endIndexSmall = smallCount; const auto cellLeftSmall = leftSmall; const auto userpicLeftFull = cellLeftFull + full.photoLeft; const auto userpicLeftSmall = cellLeftSmall + st.photoLeft; @@ -714,10 +715,12 @@ void List::contextMenuEvent(QContextMenuEvent *e) { _menu->addAction(tr::lng_context_view_profile(tr::now), [=] { _showProfileRequests.fire_copy(id); }); - _menu->addAction(hidden - ? tr::lng_stories_show_in_chats(tr::now) - : tr::lng_stories_hide_to_contacts(tr::now), - [=] { _toggleShown.fire({ .id = id, .shown = hidden }); }); + if (!_content.full || hidden) { + _menu->addAction(hidden + ? tr::lng_stories_show_in_chats(tr::now) + : tr::lng_stories_hide_to_contacts(tr::now), + [=] { _toggleShown.fire({ .id = id, .shown = hidden }); }); + } const auto updateAfterMenuDestroyed = [=] { const auto globalPosition = QCursor::pos(); if (rect().contains(mapFromGlobal(globalPosition))) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 6789765db..4221dcbc3 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -37,6 +37,7 @@ struct User { struct Content { std::vector users; + bool full = false; friend inline bool operator==( const Content &a, diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index a674a4b82..d1642c0d4 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -230,6 +230,7 @@ FormatPointer MakeFormatPointer( if (!io) { return {}; } + io->seekable = (seek != nullptr); auto result = avformat_alloc_context(); if (!result) { LogError(u"avformat_alloc_context"_q); @@ -250,7 +251,9 @@ FormatPointer MakeFormatPointer( LogError(u"avformat_open_input"_q, error); return {}; } - result->flags |= AVFMT_FLAG_FAST_SEEK; + if (seek) { + result->flags |= AVFMT_FLAG_FAST_SEEK; + } // Now FormatPointer will own and free the IO context. io.release(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 716da7fee..c343a6214 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -387,29 +387,54 @@ auto Controller::stickerOrEmojiChosen() const void Controller::show( not_null story, - Data::StorySourcesList list) { + Data::StoriesContext context) { + using namespace Data; + auto &stories = story->owner().stories(); - const auto &all = stories.all(); - const auto &sources = stories.sources(list); const auto storyId = story->fullId(); const auto id = storyId.story; - const auto i = ranges::find( - sources, - storyId.peer, - &Data::StoriesSourceInfo::id); - if (i == end(sources)) { + const auto &all = stories.all(); + const auto inAll = all.find(storyId.peer); + auto source = (inAll != end(all)) ? &inAll->second : nullptr; + auto single = StoriesSource{ story->peer()->asUser() }; + v::match(context.data, [&](StoriesContextSingle) { + source = &single; + hideSiblings(); + }, [&](StoriesContextPeer) { + hideSiblings(); + }, [&](StoriesContextSaved) { + hideSiblings(); + }, [&](StoriesContextArchive) { + hideSiblings(); + }, [&](StorySourcesList list) { + const auto &sources = stories.sources(list); + const auto i = ranges::find( + sources, + storyId.peer, + &StoriesSourceInfo::id); + if (i == end(sources)) { + source = nullptr; + return; + } + showSiblings(&story->session(), sources, (i - begin(sources))); + + if (int(sources.end() - i) < kPreloadUsersCount) { + stories.loadMore(list); + } + }); + const auto idDate = story->idDate(); + if (!source) { return; + } else if (source == &single) { + single.ids.emplace(idDate); + _index = 0; + } else { + const auto k = source->ids.find(idDate); + if (k == end(source->ids)) { + return; + } + _index = (k - begin(source->ids)); } - const auto j = all.find(storyId.peer); - if (j == end(all)) { - return; - } - const auto &source = j->second; - const auto k = source.ids.lower_bound(Data::StoryIdDate{ id }); - if (k == end(source.ids) || k->id != id) { - return; - } - showSiblings(&story->session(), sources, (i - begin(sources))); const auto guard = gsl::finally([&] { _paused = false; _started = false; @@ -419,12 +444,11 @@ void Controller::show( _photoPlayback = nullptr; } }); - if (_source != source) { - _source = source; + if (_source != *source) { + _source = *source; } - _index = (k - begin(source.ids)); + _context = context; _waitingForId = {}; - if (_shown == storyId) { return; } @@ -436,13 +460,13 @@ void Controller::show( unfocusReply(); } - _header->show({ .user = source.user, .date = story->date() }); - _slider->show({ .index = _index, .total = int(source.ids.size()) }); - _replyArea->show({ .user = source.user, .id = id }); + _header->show({ .user = source->user, .date = story->date() }); + _slider->show({ .index = _index, .total = int(source->ids.size()) }); + _replyArea->show({ .user = source->user, .id = id }); _recentViews->show({ .list = story->recentViewers(), .total = story->views(), - .valid = source.user->isSelf(), + .valid = source->user->isSelf(), }); const auto session = &story->session(); @@ -463,13 +487,10 @@ void Controller::show( }, _sessionLifetime); } - if (int(sources.end() - i) < kPreloadUsersCount) { - stories.loadMore(list); - } - stories.loadAround(storyId); + stories.loadAround(storyId, context); updatePlayingAllowed(); - source.user->updateFull(); + source->user->updateFull(); } void Controller::updatePlayingAllowed() { @@ -509,6 +530,11 @@ void Controller::showSiblings( (index + 1 < sources.size()) ? sources[index + 1].id : PeerId()); } +void Controller::hideSiblings() { + _siblingLeft = nullptr; + _siblingRight = nullptr; +} + void Controller::showSibling( std::unique_ptr &sibling, not_null session, @@ -616,10 +642,10 @@ void Controller::subjumpTo(int index) { }; auto &stories = _source->user->owner().stories(); if (stories.lookup(id)) { - _delegate->storiesJumpTo(&_source->user->session(), id); + _delegate->storiesJumpTo(&_source->user->session(), id, _context); } else if (_waitingForId != id) { _waitingForId = id; - stories.loadAround(id); + stories.loadAround(id, _context); } } @@ -637,7 +663,8 @@ void Controller::checkWaitingFor() { } _delegate->storiesJumpTo( &_source->user->session(), - base::take(_waitingForId)); + base::take(_waitingForId), + _context); } bool Controller::jumpFor(int delta) { @@ -645,7 +672,8 @@ bool Controller::jumpFor(int delta) { if (const auto left = _siblingLeft.get()) { _delegate->storiesJumpTo( &left->peer()->session(), - left->shownId()); + left->shownId(), + _context); return true; } } else if (delta == 1) { @@ -655,7 +683,8 @@ bool Controller::jumpFor(int delta) { if (const auto right = _siblingRight.get()) { _delegate->storiesJumpTo( &right->peer()->session(), - right->shownId()); + right->shownId(), + _context); return true; } } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index d6081d84b..4fd02ac39 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -99,7 +99,7 @@ public: [[nodiscard]] auto stickerOrEmojiChosen() const -> rpl::producer; - void show(not_null story, Data::StorySourcesList list); + void show(not_null story, Data::StoriesContext context); void ready(); void updateVideoPlayback(const Player::TrackState &state); @@ -137,6 +137,7 @@ private: void updatePlayingAllowed(); void setPlayingAllowed(bool allowed); + void hideSiblings(); void showSiblings( not_null session, const std::vector &lists, @@ -178,6 +179,7 @@ private: FullStoryId _shown; TextWithEntities _captionText; + Data::StoriesContext _context; std::optional _source; FullStoryId _waitingForId; int _index = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index 549788c84..9f8d7ce4d 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -12,6 +12,10 @@ class Show; struct FileChosen; } // namespace ChatHelpers +namespace Data { +struct StoriesContext; +} // namespace Data + namespace Main { class Session; } // namespace Main @@ -41,7 +45,8 @@ public: -> rpl::producer = 0; virtual void storiesJumpTo( not_null session, - FullStoryId id) = 0; + FullStoryId id, + Data::StoriesContext context) = 0; virtual void storiesClose() = 0; [[nodiscard]] virtual bool storiesPaused() = 0; [[nodiscard]] virtual rpl::producer storiesLayerShown() = 0; diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 0fbc38721..de254b870 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -22,8 +22,10 @@ View::View(not_null delegate) View::~View() = default; -void View::show(not_null story, Data::StorySourcesList list) { - _controller->show(story, list); +void View::show( + not_null story, + Data::StoriesContext context) { + _controller->show(story, context); } void View::ready() { diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index a671bdbde..b25011de5 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { class Story; -enum class StorySourcesList : uchar; +struct StoriesContext; struct FileOrigin; } // namespace Data @@ -53,7 +53,7 @@ public: explicit View(not_null delegate); ~View(); - void show(not_null story, Data::StorySourcesList list); + void show(not_null story, Data::StoriesContext context); void ready(); [[nodiscard]] bool canDownload() const; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h index d1abf2011..f91afead2 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h @@ -40,11 +40,13 @@ enum class Mode { struct PlaybackOptions { Mode mode = Mode::Both; crl::time position = 0; + crl::time durationOverride = 0; float64 speed = 1.; // Valid values between 0.5 and 2. AudioMsgId audioId; bool syncVideoByAudio = true; bool waitForMarkAsShown = false; bool hwAllowed = false; + bool seekable = true; bool loop = false; }; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp index 6a815e888..22427c2ff 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp @@ -150,7 +150,7 @@ Stream File::Context::initStream( not_null format, AVMediaType type, Mode mode, - bool hwAllowed) { + StartOptions options) { auto result = Stream(); const auto index = result.index = av_find_best_stream( format, @@ -171,7 +171,7 @@ Stream File::Context::initStream( } result.codec = FFmpeg::MakeCodecPointer({ .stream = info, - .hwAllowed = hwAllowed, + .hwAllowed = options.hwAllow, }); if (!result.codec) { return result; @@ -196,7 +196,9 @@ Stream File::Context::initStream( return result; } result.timeBase = info->time_base; - result.duration = (info->duration != AV_NOPTS_VALUE) + result.duration = options.durationOverride + ? options.durationOverride + : (info->duration != AV_NOPTS_VALUE) ? FFmpeg::PtsToTime(info->duration, result.timeBase) : UnreliableFormatDuration(format, info, mode) ? kTimeUnknown @@ -269,17 +271,19 @@ std::variant File::Context::readPacket() { return error; } -void File::Context::start(crl::time position, bool hwAllow) { +void File::Context::start(StartOptions options) { + Expects(options.seekable || !options.position); + auto error = FFmpeg::AvErrorWrap(); if (unroll()) { return; } auto format = FFmpeg::MakeFormatPointer( - static_cast(this), + static_cast(this), &Context::Read, nullptr, - &Context::Seek); + options.seekable ? &Context::Seek : nullptr); if (!format) { return fail(Error::OpenFailed); } @@ -289,12 +293,20 @@ void File::Context::start(crl::time position, bool hwAllow) { } const auto mode = _delegate->fileOpenMode(); - auto video = initStream(format.get(), AVMEDIA_TYPE_VIDEO, mode, hwAllow); + auto video = initStream( + format.get(), + AVMEDIA_TYPE_VIDEO, + mode, + options); if (unroll()) { return; } - auto audio = initStream(format.get(), AVMEDIA_TYPE_AUDIO, mode, false); + auto audio = initStream( + format.get(), + AVMEDIA_TYPE_AUDIO, + mode, + options); if (unroll()) { return; } @@ -303,8 +315,11 @@ void File::Context::start(crl::time position, bool hwAllow) { if (_reader->isRemoteLoader()) { sendFullInCache(true); } - if (video.codec || audio.codec) { - seekToPosition(format.get(), video.codec ? video : audio, position); + if (options.seekable && (video.codec || audio.codec)) { + seekToPosition( + format.get(), + video.codec ? video : audio, + options.position); } if (unroll()) { return; @@ -434,10 +449,7 @@ File::File(std::shared_ptr reader) : _reader(std::move(reader)) { } -void File::start( - not_null delegate, - crl::time position, - bool hwAllow) { +void File::start(not_null delegate, StartOptions options) { stop(true); _reader->startStreaming(); @@ -445,7 +457,7 @@ void File::start( _thread = std::thread([=, context = &*_context] { crl::toggle_fp_exceptions(true); - context->start(position, hwAllow); + context->start(options); while (!context->finished()) { context->readNextPacket(); } diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.h b/Telegram/SourceFiles/media/streaming/media_streaming_file.h index 8ffea9430..38af45537 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_file.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.h @@ -21,6 +21,13 @@ namespace Streaming { class FileDelegate; +struct StartOptions { + crl::time position = 0; + crl::time durationOverride = 0; + bool seekable = true; + bool hwAllow = false; +}; + class File final { public: explicit File(std::shared_ptr reader); @@ -28,10 +35,7 @@ public: File(const File &other) = delete; File &operator=(const File &other) = delete; - void start( - not_null delegate, - crl::time position, - bool hwAllow); + void start(not_null delegate, StartOptions options); void wake(); void stop(bool stillActive = false); @@ -46,7 +50,7 @@ private: Context(not_null delegate, not_null reader); ~Context(); - void start(crl::time position, bool hwAllow); + void start(StartOptions options); void readNextPacket(); void interrupt(); @@ -79,7 +83,7 @@ private: not_null format, AVMediaType type, Mode mode, - bool hwAllowed); + StartOptions options); void seekToPosition( not_null format, const Stream &stream, diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp index 6662ec862..d30566072 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp @@ -544,8 +544,16 @@ void Player::play(const PlaybackOptions &options) { if (!Media::Audio::SupportsSpeedControl()) { _options.speed = 1.; } + if (!_options.seekable) { + _options.position = 0; + } _stage = Stage::Initializing; - _file->start(delegate(), _options.position, _options.hwAllowed); + _file->start(delegate(), { + .position = _options.position, + .durationOverride = options.durationOverride, + .seekable = _options.seekable, + .hwAllow = _options.hwAllowed, + }); } void Player::savePreviousReceivedTill( diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index aed2fde58..ff56af25d 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -8,17 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "data/data_cloud_themes.h" +#include "data/data_stories.h" class DocumentData; class PeerData; class PhotoData; class HistoryItem; -namespace Data { -class Story; -enum class StorySourcesList : uchar; -} // namespace Data - namespace Window { class SessionController; } // namespace Window @@ -75,14 +71,10 @@ public: OpenRequest( Window::SessionController *controller, not_null story, - Data::StorySourcesList list, - bool continueStreaming = false, - crl::time startTime = 0) + Data::StoriesContext context) : _controller(controller) , _story(story) - , _storiesList(list) - , _continueStreaming(continueStreaming) - , _startTime(startTime) { + , _storiesContext(context) { } [[nodiscard]] PeerData *peer() const { @@ -108,8 +100,8 @@ public: [[nodiscard]] Data::Story *story() const { return _story; } - [[nodiscard]] Data::StorySourcesList storiesList() const { - return _storiesList; + [[nodiscard]] Data::StoriesContext storiesContext() const { + return _storiesContext; } [[nodiscard]] std::optional cloudTheme() const { @@ -133,7 +125,7 @@ private: DocumentData *_document = nullptr; PhotoData *_photo = nullptr; Data::Story *_story = nullptr; - Data::StorySourcesList _storiesList = {}; + Data::StoriesContext _storiesContext; PeerData *_peer = nullptr; HistoryItem *_item = nullptr; MsgId _topicRootId = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 75c7a43bb..2bbda9a43 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -287,6 +287,17 @@ struct OverlayWidget::PipWrap { rpl::lifetime lifetime; }; +struct OverlayWidget::ItemContext { + not_null item; + MsgId topicRootId = 0; +}; + +struct OverlayWidget::StoriesContext { + not_null peer; + StoryId id = 0; + Data::StoriesContext within; +}; + class OverlayWidget::Show final : public ChatHelpers::Show { public: explicit Show(not_null widget) : _widget(widget) { @@ -3064,7 +3075,7 @@ void OverlayWidget::show(OpenRequest request) { setContext(StoriesContext{ story->peer(), story->id(), - request.storiesList(), + request.storiesContext(), }); } else if (contextPeer) { setContext(contextPeer); @@ -3085,7 +3096,11 @@ void OverlayWidget::show(OpenRequest request) { setSession(&document->session()); if (story) { - setContext(StoriesContext{ story->peer(), story->id() }); + setContext(StoriesContext{ + story->peer(), + story->id(), + request.storiesContext(), + }); } else if (contextItem) { setContext(ItemContext{ contextItem, contextTopicRootId }); } else { @@ -3868,9 +3883,16 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { _rotation = saved; updateContentRect(); } - auto options = Streaming::PlaybackOptions(); - options.position = position; - options.hwAllowed = Core::App().settings().hardwareAcceleratedVideo(); + auto options = Streaming::PlaybackOptions{ + .position = position, + .durationOverride = ((_stories + && _document + && _document->getDuration() > 0) + ? (_document->getDuration() * crl::time(1000) + crl::time(999)) + : crl::time(0)), + .hwAllowed = Core::App().settings().hardwareAcceleratedVideo(), + .seekable = !_stories, + }; if (!_streamed->withSound) { options.mode = Streaming::Mode::Video; options.loop = true; @@ -4025,7 +4047,8 @@ auto OverlayWidget::storiesStickerOrEmojiChosen() void OverlayWidget::storiesJumpTo( not_null session, - FullStoryId id) { + FullStoryId id, + Data::StoriesContext context) { Expects(_stories != nullptr); Expects(id.valid()); @@ -4035,7 +4058,11 @@ void OverlayWidget::storiesJumpTo( return; } const auto story = *maybeStory; - setContext(StoriesContext{ story->peer(), story->id() }); + setContext(StoriesContext{ + story->peer(), + story->id(), + context, + }); clearStreaming(); _streamingStartPaused = false; v::match(story->media().data, [&](not_null photo) { @@ -4982,7 +5009,7 @@ void OverlayWidget::setContext( const auto maybeStory = stories.lookup( { story->peer->id, story->id }); if (maybeStory) { - _stories->show(*maybeStory, story->list); + _stories->show(*maybeStory, story->within); } } else { _message = nullptr; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 837678675..2085fef99 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -30,7 +30,7 @@ enum class activation : uchar; namespace Data { class PhotoMedia; class DocumentMedia; -enum class StorySourcesList : uchar; +struct StoriesContext; } // namespace Data namespace Ui { @@ -134,6 +134,8 @@ private: class Show; struct Streamed; struct PipWrap; + struct ItemContext; + struct StoriesContext; class Renderer; class RendererSW; class RendererGL; @@ -245,7 +247,8 @@ private: -> rpl::producer override; void storiesJumpTo( not_null session, - FullStoryId id) override; + FullStoryId id, + Data::StoriesContext context) override; void storiesClose() override; bool storiesPaused() override; rpl::producer storiesLayerShown() override; @@ -300,15 +303,6 @@ private: Entity entityForItemId(const FullMsgId &itemId) const; bool moveToEntity(const Entity &entity, int preloadDelta = 0); - struct ItemContext { - not_null item; - MsgId topicRootId = 0; - }; - struct StoriesContext { - not_null peer; - StoryId id = 0; - Data::StorySourcesList list = {}; - }; void setContext(std::variant< v::null_t, ItemContext, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 793964d9e..fd26e5e28 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2467,13 +2467,13 @@ Ui::ChatThemeBackgroundData SessionController::backgroundData( void SessionController::openPeerStory( not_null peer, StoryId storyId, - Data::StorySourcesList list) { + Data::StoriesContext context) { using namespace Media::View; using namespace Data; auto &stories = session().data().stories(); if (const auto from = stories.lookup({ peer->id, storyId })) { - window().openInMediaView(OpenRequest(this, *from, list)); + window().openInMediaView(OpenRequest(this, *from, context)); } } @@ -2492,7 +2492,7 @@ void SessionController::openPeerStories( openPeerStory( i->second.user, j != i->second.ids.end() ? j->id : i->second.ids.front().id, - list); + { list }); } } diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 589169655..5e1139ad1 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -30,6 +30,7 @@ enum class WindowLayout; } // namespace Adaptive namespace Data { +struct StoriesContext; enum class StorySourcesList : uchar; } // namespace Data @@ -571,7 +572,7 @@ public: void openPeerStory( not_null peer, StoryId storyId, - Data::StorySourcesList list); + Data::StoriesContext context); void openPeerStories(PeerId peerId, Data::StorySourcesList list); struct PaintContextArgs { From aba84a6010690bf4d87f2441a75f09dd8c0841dc Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 5 Jun 2023 16:50:43 +0400 Subject: [PATCH 057/260] Update API scheme on layer 160: Duration. --- Telegram/SourceFiles/api/api_media.cpp | 9 +++--- Telegram/SourceFiles/api/api_ringtones.cpp | 4 +-- Telegram/SourceFiles/api/api_ringtones.h | 2 +- .../SourceFiles/data/data_audio_msg_id.cpp | 2 +- Telegram/SourceFiles/data/data_document.cpp | 28 ++++++++----------- Telegram/SourceFiles/data/data_document.h | 7 ++--- .../export/data/export_data_types.cpp | 2 +- .../history_view_voice_record_bar.cpp | 1 - .../view/history_view_context_menu.cpp | 4 +-- .../view/media/history_view_document.cpp | 20 +++++++------ .../history/view/media/history_view_gif.cpp | 6 ++-- .../history/view/media/history_view_media.cpp | 2 +- .../inline_bot_layout_internal.cpp | 14 ++++------ .../media/clip/media_clip_reader.cpp | 4 +-- .../media/player/media_player_widget.cpp | 2 +- .../streaming/media_streaming_player.cpp | 4 +-- .../media/view/media_view_overlay_widget.cpp | 4 +-- Telegram/SourceFiles/mtproto/scheme/api.tl | 11 +++++--- .../SourceFiles/overview/overview_layout.cpp | 14 ++++------ .../SourceFiles/overview/overview_layout.h | 3 +- .../SourceFiles/storage/localimageloader.cpp | 2 +- .../storage/serialize_document.cpp | 28 +++++++++++++------ .../ui/chat/attach/attach_prepare.h | 2 +- 23 files changed, 88 insertions(+), 87 deletions(-) diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp index 72af0ba66..ec8c1e4b2 100644 --- a/Telegram/SourceFiles/api/api_media.cpp +++ b/Telegram/SourceFiles/api/api_media.cpp @@ -22,8 +22,7 @@ MTPVector ComposeSendingDocumentAttributes( const auto dimensions = document->dimensions; auto attributes = QVector(1, filenameAttribute); if (dimensions.width() > 0 && dimensions.height() > 0) { - const auto duration = document->getDuration(); - if (duration >= 0 && !document->hasMimeType(u"image/gif"_q)) { + if (document->hasDuration() && !document->hasMimeType(u"image/gif"_q)) { auto flags = MTPDdocumentAttributeVideo::Flags(0); using VideoFlag = MTPDdocumentAttributeVideo::Flag; if (document->isVideoMessage()) { @@ -34,7 +33,7 @@ MTPVector ComposeSendingDocumentAttributes( } attributes.push_back(MTP_documentAttributeVideo( MTP_flags(flags), - MTP_int(duration), + MTP_double(document->duration() / 1000.), MTP_int(dimensions.width()), MTP_int(dimensions.height()), MTPint())); // preload_prefix_size @@ -57,7 +56,7 @@ MTPVector ComposeSendingDocumentAttributes( | MTPDdocumentAttributeAudio::Flag::f_performer; attributes.push_back(MTP_documentAttributeAudio( MTP_flags(flags), - MTP_int(song->duration), + MTP_int(document->duration() / 1000), MTP_string(song->title), MTP_string(song->performer), MTPstring())); @@ -66,7 +65,7 @@ MTPVector ComposeSendingDocumentAttributes( | MTPDdocumentAttributeAudio::Flag::f_waveform; attributes.push_back(MTP_documentAttributeAudio( MTP_flags(flags), - MTP_int(voice->duration), + MTP_int(document->duration() / 1000), MTPstring(), MTPstring(), MTP_bytes(documentWaveformEncode5bit(voice->waveform)))); diff --git a/Telegram/SourceFiles/api/api_ringtones.cpp b/Telegram/SourceFiles/api/api_ringtones.cpp index d9ad40176..5b471851e 100644 --- a/Telegram/SourceFiles/api/api_ringtones.cpp +++ b/Telegram/SourceFiles/api/api_ringtones.cpp @@ -202,8 +202,8 @@ int Ringtones::maxSavedCount() const { 100); } -int Ringtones::maxDuration() const { - return _session->account().appConfig().get( +crl::time Ringtones::maxDuration() const { + return crl::time(1000) * _session->account().appConfig().get( "ringtone_duration_max", 5); } diff --git a/Telegram/SourceFiles/api/api_ringtones.h b/Telegram/SourceFiles/api/api_ringtones.h index 08918a89e..ea97db349 100644 --- a/Telegram/SourceFiles/api/api_ringtones.h +++ b/Telegram/SourceFiles/api/api_ringtones.h @@ -40,7 +40,7 @@ public: [[nodiscard]] int64 maxSize() const; [[nodiscard]] int maxSavedCount() const; - [[nodiscard]] int maxDuration() const; + [[nodiscard]] crl::time maxDuration() const; private: struct UploadedData { diff --git a/Telegram/SourceFiles/data/data_audio_msg_id.cpp b/Telegram/SourceFiles/data/data_audio_msg_id.cpp index 4c95989ac..d8365bee2 100644 --- a/Telegram/SourceFiles/data/data_audio_msg_id.cpp +++ b/Telegram/SourceFiles/data/data_audio_msg_id.cpp @@ -27,7 +27,7 @@ AudioMsgId::AudioMsgId( , _externalPlayId(externalPlayId) , _changeablePlaybackSpeed(_audio->isVoiceMessage() || _audio->isVideoMessage() - || (_audio->getDuration() >= kMinLengthForChangeablePlaybackSpeed)) { + || (_audio->duration() >= kMinLengthForChangeablePlaybackSpeed)) { setTypeFromAudio(); } diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 2adfe69e4..7af141841 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -326,6 +326,7 @@ Main::Session &DocumentData::session() const { void DocumentData::setattributes( const QVector &attributes) { + _duration = -1; _flags &= ~(Flag::ImageType | Flag::HasAttachedStickers | Flag::UseTextColor @@ -389,12 +390,12 @@ void DocumentData::setattributes( : VideoDocument; if (data.is_round_message()) { _additional = std::make_unique(); - round()->duration = data.vduration().v; } } else if (const auto info = sticker()) { info->type = StickerType::Webm; } - _duration = data.vduration().v; + _duration = crl::time( + base::SafeRound(data.vduration().v * 1000)); setMaybeSupportsStreaming(data.is_supports_streaming()); dimensions = QSize(data.vw().v, data.vh().v); }, [&](const MTPDdocumentAttributeAudio &data) { @@ -408,14 +409,14 @@ void DocumentData::setattributes( } } if (const auto voiceData = voice() ? voice() : round()) { - voiceData->duration = data.vduration().v; + _duration = data.vduration().v * crl::time(1000); voiceData->waveform = documentWaveformDecode( data.vwaveform().value_or_empty()); voiceData->wavemax = voiceData->waveform.empty() ? uchar(0) : *ranges::max_element(voiceData->waveform); } else if (const auto songData = song()) { - songData->duration = data.vduration().v; + _duration = data.vduration().v * crl::time(1000); songData->title = qs(data.vtitle().value_or_empty()); songData->performer = qs(data.vperformer().value_or_empty()); refreshPossibleCoverThumbnail(); @@ -1516,19 +1517,12 @@ bool DocumentData::isVideoFile() const { return (type == VideoDocument); } -TimeId DocumentData::getDuration() const { - if (const auto song = this->song()) { - return std::max(song->duration, 0); - } else if (const auto voice = this->voice()) { - return std::max(voice->duration, 0); - } else if (isAnimation() || isVideoFile()) { - return std::max(_duration, 0); - } else if (const auto sticker = this->sticker()) { - if (sticker->isWebm()) { - return std::max(_duration, 0); - } - } - return -1; +crl::time DocumentData::duration() const { + return std::max(_duration, crl::time()); +} + +bool DocumentData::hasDuration() const { + return _duration >= 0; } bool DocumentData::isImage() const { diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 42d49058d..b4bc0cad3 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -79,14 +79,12 @@ struct StickerData : public DocumentAdditionalData { }; struct SongData : public DocumentAdditionalData { - int32 duration = 0; QString title, performer; }; struct VoiceData : public DocumentAdditionalData { ~VoiceData(); - int duration = 0; VoiceWaveform waveform; char wavemax = 0; }; @@ -172,7 +170,8 @@ public: [[nodiscard]] bool isGifv() const; [[nodiscard]] bool isTheme() const; [[nodiscard]] bool isSharedMediaMusic() const; - [[nodiscard]] TimeId getDuration() const; + [[nodiscard]] crl::time duration() const; + [[nodiscard]] bool hasDuration() const; [[nodiscard]] bool isImage() const; void recountIsImage(); [[nodiscard]] bool supportsStreaming() const; @@ -356,10 +355,10 @@ private: std::unique_ptr _replyPreview; std::weak_ptr _media; PhotoData *_goodThumbnailPhoto = nullptr; + crl::time _duration = -1; Core::FileLocation _location; std::unique_ptr _additional; - int32 _duration = -1; mutable Flags _flags = kStreamingSupportedUnknown; GoodThumbnailState _goodThumbnailState = GoodThumbnailState(); std::unique_ptr _loader; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 1b66a550f..7e0612860 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -295,7 +295,7 @@ void ParseAttributes( } result.width = data.vw().v; result.height = data.vh().v; - result.duration = data.vduration().v; + result.duration = int(data.vduration().v); }, [&](const MTPDdocumentAttributeAudio &data) { if (data.is_voice()) { result.isVoiceMessage = true; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 63e4f6ee8..c49495542 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -90,7 +90,6 @@ enum class FilterType { [[nodiscard]] std::unique_ptr ProcessCaptureResult( const ::Media::Capture::Result &data) { auto voiceData = std::make_unique(); - voiceData->duration = Duration(data.samples); voiceData->waveform = data.waveform; voiceData->wavemax = voiceData->waveform.empty() ? uchar(0) diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 99ebf2929..2fd7c2ca8 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1199,11 +1199,11 @@ void AddSaveSoundForNotifications( } else if (int(ringtones.list().size()) >= ringtones.maxSavedCount()) { return; } else if (const auto song = document->song()) { - if (song->duration > ringtones.maxDuration()) { + if (document->duration() > ringtones.maxDuration()) { return; } } else if (const auto voice = document->voice()) { - if (voice->duration > ringtones.maxDuration()) { + if (document->duration() > ringtones.maxDuration()) { return; } } else { diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 19832fc23..d1ddb2b96 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -172,7 +172,7 @@ void PaintWaveform( accumulate_max(result, st::normalFont->width(text)); }; add(FormatDownloadText(document->size, document->size)); - const auto duration = document->getDuration(); + const auto duration = document->duration() / 1000; if (const auto song = document->song()) { add(FormatPlayedText(duration, duration)); add(FormatDurationAndSizeText(duration, document->size)); @@ -1212,14 +1212,16 @@ bool Document::uploading() const { } void Document::setStatusSize(int64 newSize, TimeId realDuration) const { - TimeId duration = _data->isSong() - ? _data->song()->duration - : (_data->isVoiceMessage() - ? _data->voice()->duration - : _transcribedRound - ? _data->round()->duration - : -1); - File::setStatusSize(newSize, _data->size, duration, realDuration); + const auto duration = (_data->isSong() + || _data->isVoiceMessage() + || _transcribedRound) + ? _data->duration() + : -1; + File::setStatusSize( + newSize, + _data->size, + (duration >= 0) ? duration / 1000 : -1, + realDuration); if (auto thumbed = Get()) { if (_statusSize == Ui::FileStatusSizeReady) { thumbed->link = tr::lng_media_download(tr::now).toUpper(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index a395f8f09..67e4c8f3f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -1600,12 +1600,12 @@ void Gif::setStatusSize(int64 newSize) const { _statusText = Ui::FormatDurationText(-newSize - 1); } else if (_data->isVideoMessage()) { _statusSize = newSize; - _statusText = Ui::FormatDurationText(_data->getDuration()); + _statusText = Ui::FormatDurationText(_data->duration() / 1000); } else { File::setStatusSize( newSize, _data->size, - _data->isVideoFile() ? _data->getDuration() : -2, + _data->isVideoFile() ? (_data->duration() / 1000) : -2, 0); } } @@ -1638,7 +1638,7 @@ void Gif::updateStatusText() const { } statusSize = -1 - int((state.length - position) / state.frequency + 1); } else { - statusSize = -1 - _data->getDuration(); + statusSize = -1 - (_data->duration() / 1000); } } if (statusSize != _statusSize) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index da0f56871..22b5ef296 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -56,7 +56,7 @@ TimeId DurationForTimestampLinks(not_null document) { && !document->isVoiceMessage()) { return TimeId(0); } - return std::max(document->getDuration(), TimeId(0)); + return std::max(document->duration(), crl::time(0)) / 1000; } QString TimestampLinkBase( diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index 2d77babdb..9cf84d338 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -88,8 +88,8 @@ int FileBase::content_height() const { int FileBase::content_duration() const { if (const auto document = getShownDocument()) { - if (document->getDuration() > 0) { - return document->getDuration(); + if (document->hasDuration()) { + return document->duration() / 1000; } } return getResultDuration(); @@ -1143,12 +1143,10 @@ bool File::updateStatusText() const { } if (statusSize != _statusSize) { - TimeId duration = _document->isSong() - ? _document->song()->duration - : (_document->isVoiceMessage() - ? _document->voice()->duration - : -1); - setStatusSize(statusSize, _document->size, duration, realDuration); + const auto duration = _document->isSong() + ? _document->duration() + : (_document->isVoiceMessage() ? _document->duration() : -1); + setStatusSize(statusSize, _document->size, (duration >= 0) ? (duration / 1000) : -1, realDuration); } return showPause; } diff --git a/Telegram/SourceFiles/media/clip/media_clip_reader.cpp b/Telegram/SourceFiles/media/clip/media_clip_reader.cpp index b7c429114..7227ef7bc 100644 --- a/Telegram/SourceFiles/media/clip/media_clip_reader.cpp +++ b/Telegram/SourceFiles/media/clip/media_clip_reader.cpp @@ -974,7 +974,7 @@ Ui::PreparedFileInformation PrepareForSending( auto seekPositionMs = crl::time(0); auto reader = std::make_unique(&localLocation, &localData); if (reader->start(internal::ReaderImplementation::Mode::Inspecting, seekPositionMs)) { - auto durationMs = reader->durationMs(); + const auto durationMs = reader->durationMs(); if (durationMs > 0) { result.isGifv = reader->isGifv(); result.isWebmSticker = reader->isWebmSticker(); @@ -994,7 +994,7 @@ Ui::PreparedFileInformation PrepareForSending( if (hasAlpha && !result.isWebmSticker) { result.thumbnail = Images::Opaque(std::move(result.thumbnail)); } - result.duration = static_cast(durationMs / 1000); + result.duration = durationMs; } result.supportsStreaming = CheckStreamingSupport( diff --git a/Telegram/SourceFiles/media/player/media_player_widget.cpp b/Telegram/SourceFiles/media/player/media_player_widget.cpp index 0d375b603..d9be06b6d 100644 --- a/Telegram/SourceFiles/media/player/media_player_widget.cpp +++ b/Telegram/SourceFiles/media/player/media_player_widget.cpp @@ -632,7 +632,7 @@ void Widget::updateTimeText(const TrackState &state) { } else if (state.length) { display = state.length; } else if (const auto song = document->song()) { - display = (song->duration * frequency); + display = (document->duration() * frequency) / 1000; } _lastDurationMs = (state.length * 1000LL) / frequency; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp index d30566072..699cbdf58 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp @@ -954,9 +954,9 @@ Media::Player::TrackState Player::prepareLegacyState() const { if (result.length == kTimeUnknown) { const auto document = _options.audioId.audio(); - const auto duration = document ? document->getDuration() : 0; + const auto duration = document ? document->duration() : 0; if (duration > 0) { - result.length = duration * crl::time(1000); + result.length = duration; } else { result.length = std::max(crl::time(result.position), crl::time(0)); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 2bbda9a43..1d1880c83 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -3887,8 +3887,8 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { .position = position, .durationOverride = ((_stories && _document - && _document->getDuration() > 0) - ? (_document->getDuration() * crl::time(1000) + crl::time(999)) + && _document->hasDuration()) + ? _document->duration() : crl::time(0)), .hwAllowed = Core::App().settings().hardwareAcceleratedVideo(), .seekable = !_stories, diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index f603e31ae..9e3340c1c 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -527,7 +527,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; -documentAttributeVideo#e9407793 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int preload_prefix_size:flags.2?int = DocumentAttribute; +documentAttributeVideo#d38ff1c2 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:double w:int h:int preload_prefix_size:flags.2?int = DocumentAttribute; documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; @@ -1526,8 +1526,8 @@ sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Phot storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; -storyItemSkipped#a1d8cf8f id:int date:int = StoryItem; -storyItem#8fcd96ac flags:# pinned:flags.5?true expired:flags.6?true id:int date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; +storyItemSkipped#693206a2 id:int date:int expire_date:int = StoryItem; +storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; @@ -1547,6 +1547,8 @@ stories.storyViews#de9eed1d views:Vector users:Vector = storie inputReplyToMessage#9c5386e4 flags:# reply_to_msg_id:int top_msg_id:flags.0?int = InputReplyTo; inputReplyToStory#15b0f283 user_id:InputUser story_id:int = InputReplyTo; +exportedStoryLink#3fc9053b link:string = ExportedStoryLink; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2082,7 +2084,7 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.sendStory#8b5c6986 flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long = Updates; +stories.sendStory#424cd47a flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; stories.editStory#2aae7a41 flags:# id:int media:flags.0?InputMedia caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#b5d501d7 id:Vector = Vector; stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; @@ -2095,3 +2097,4 @@ stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList; stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; +stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink; diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 369a6ff5d..599dd8f1b 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -448,7 +448,7 @@ Video::Video( bool spoiler) : RadialProgressItem(delegate, parent) , _data(video) -, _duration(Ui::FormatDurationText(_data->getDuration())) +, _duration(Ui::FormatDurationText(_data->duration() / 1000)) , _spoiler(spoiler ? std::make_unique([=] { delegate->repaintItem(this); }) : nullptr) { @@ -693,7 +693,7 @@ Voice::Voice( lt_date, dateText, lt_duration, - { .text = Ui::FormatDurationText(duration()) }, + { .text = Ui::FormatDurationText(_data->duration() / 1000) }, Ui::Text::WithEntities)); _details.setLink(1, JumpToMessageClickHandler(parent)); } @@ -958,10 +958,6 @@ void Voice::updateName() { _nameVersion = parent()->fromOriginal()->nameVersion(); } -int Voice::duration() const { - return std::max(_data->getDuration(), 0); -} - bool Voice::updateStatusText() { auto showPause = false; auto statusSize = int64(); @@ -983,7 +979,7 @@ bool Voice::updateStatusText() { } if (statusSize != _status.size()) { - _status.update(statusSize, _data->size, duration(), realDuration); + _status.update(statusSize, _data->size, _data->duration() / 1000, realDuration); } return showPause; } @@ -1026,7 +1022,7 @@ Document::Document( _status.update( Ui::FileStatusSizeReady, _data->size, - songLayout() ? _data->song()->duration : -1, + songLayout() ? (_data->duration() / 1000) : -1, 0); if (withThumb()) { @@ -1541,7 +1537,7 @@ bool Document::updateStatusText() { _status.update( statusSize, _data->size, - isSong ? _data->song()->duration : -1, + isSong ? (_data->duration() / 1000) : -1, realDuration); } return showPause; diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index e49dcc813..53dc9bdb0 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -339,9 +339,8 @@ protected: private: void ensureDataMediaCreated() const; - int duration() const; - not_null _data; + const not_null _data; mutable std::shared_ptr _dataMedia; StatusText _status; ClickHandlerPtr _namel; diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 1516594a9..20a911d2e 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -868,7 +868,7 @@ void FileLoadTask::process(Args &&args) { } attributes.push_back(MTP_documentAttributeVideo( MTP_flags(flags), - MTP_int(video->duration), + MTP_double(video->duration / 1000.), MTP_int(coverWidth), MTP_int(coverHeight), MTPint())); // preload_prefix_size diff --git a/Telegram/SourceFiles/storage/serialize_document.cpp b/Telegram/SourceFiles/storage/serialize_document.cpp index 04d8936e7..dd87b0c5f 100644 --- a/Telegram/SourceFiles/storage/serialize_document.cpp +++ b/Telegram/SourceFiles/storage/serialize_document.cpp @@ -18,7 +18,7 @@ namespace Serialize { namespace { constexpr auto kVersionTag = int32(0x7FFFFFFF); -constexpr auto kVersion = 5; +constexpr auto kVersion = 6; enum StickerSetType { StickerSetTypeEmpty = 0, @@ -58,7 +58,7 @@ void Document::writeToStream(QDataStream &stream, DocumentData *document) { stream << qint32(StickerSetTypeEmpty); } } - stream << qint32(document->getDuration()); + stream << qint64(document->hasDuration() ? document->duration() : -1); if (document->type == StickerDocument) { const auto premium = document->isPremiumSticker() || document->isPremiumEmoji(); @@ -110,15 +110,19 @@ DocumentData *Document::readFromStreamHelper( attributes.push_back(MTP_documentAttributeFilename(MTP_string(name))); } - qint32 duration = -1; + qint64 duration = -1; qint32 isPremiumSticker = 0; qint32 useTextColor = 0; if (type == StickerDocument) { QString alt; qint32 typeOfSet; stream >> alt >> typeOfSet; - if (version >= 3) { - stream >> duration; + if (version >= 6) { + stream >> duration >> isPremiumSticker >> useTextColor; + } else if (version >= 3) { + qint32 oldDuration = -1; + stream >> oldDuration; + duration = (oldDuration < 0) ? oldDuration : oldDuration * 1000; if (version >= 4) { stream >> isPremiumSticker; if (version >= 5) { @@ -166,7 +170,13 @@ DocumentData *Document::readFromStreamHelper( } } } else { - stream >> duration; + if (version >= 6) { + stream >> duration; + } else { + qint32 oldDuration = -1; + stream >> oldDuration; + duration = (oldDuration < 0) ? oldDuration : oldDuration * 1000; + } if (type == AnimatedDocument) { attributes.push_back(MTP_documentAttributeAnimated()); } @@ -194,12 +204,14 @@ DocumentData *Document::readFromStreamHelper( } attributes.push_back(MTP_documentAttributeVideo( MTP_flags(flags), - MTP_int(duration), + MTP_double(duration / 1000.), MTP_int(width), MTP_int(height), MTPint())); // preload_prefix_size } else { - attributes.push_back(MTP_documentAttributeImageSize(MTP_int(width), MTP_int(height))); + attributes.push_back(MTP_documentAttributeImageSize( + MTP_int(width), + MTP_int(height))); } } diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index 727258f85..f78655c66 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -36,7 +36,7 @@ struct PreparedFileInformation { bool isGifv = false; bool isWebmSticker = false; bool supportsStreaming = false; - int duration = -1; + crl::time duration = -1; QImage thumbnail; }; From 8eac04cb90b2b3e141d6e8047f163dc3f9f51d84 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 5 Jun 2023 19:23:49 +0400 Subject: [PATCH 058/260] Track and load ids of expired mine stories. --- Telegram/SourceFiles/data/data_stories.cpp | 293 +++++++++++++++--- Telegram/SourceFiles/data/data_stories.h | 53 +++- .../history/history_item_helpers.cpp | 6 +- .../stories/media_stories_controller.cpp | 6 +- .../SourceFiles/window/window_main_menu.cpp | 32 ++ .../window/window_session_controller.cpp | 2 +- 6 files changed, 331 insertions(+), 61 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 69b14f0a7..5eef52f69 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_stories.h" +#include "base/unixtime.h" #include "api/api_text_entities.h" #include "apiwrap.h" #include "core/application.h" @@ -32,6 +33,8 @@ constexpr auto kMaxResolveTogether = 100; constexpr auto kIgnorePreloadAroundIfLoaded = 15; constexpr auto kPreloadAroundCount = 30; constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); +constexpr auto kExpiredMineFirstPerPage = 30; +constexpr auto kExpiredMinePerPage = 100; using UpdateFlag = StoryUpdate::Flag; @@ -80,11 +83,13 @@ Story::Story( StoryId id, not_null peer, StoryMedia media, - TimeId date) + TimeId date, + TimeId expires) : _id(id) , _peer(peer) , _media(std::move(media)) -, _date(date) { +, _date(date) +, _expires(expires) { } Session &Story::owner() const { @@ -103,8 +108,12 @@ StoryId Story::id() const { return _id; } -StoryIdDate Story::idDate() const { - return { _id, _date }; +bool Story::mine() const { + return _peer->isSelf(); +} + +StoryIdDates Story::idDates() const { + return { _id, _date, _expires }; } FullStoryId Story::fullId() const { @@ -115,6 +124,14 @@ TimeId Story::date() const { return _date; } +TimeId Story::expires() const { + return _expires; +} + +bool Story::expired(TimeId now) const { + return _expires <= (now ? now : base::unixtime::now()); +} + const StoryMedia &Story::media() const { return _media; } @@ -276,6 +293,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { Stories::Stories(not_null owner) : _owner(owner) +, _expireTimer([=] { processExpired(); }) , _markReadTimer([=] { sendMarkAsReadRequests(); }) { } @@ -293,21 +311,27 @@ Main::Session &Stories::session() const { void Stories::apply(const MTPDupdateStory &data) { const auto peerId = peerFromUser(data.vuser_id()); const auto user = not_null(_owner->peer(peerId)->asUser()); - const auto idDate = parseAndApply(user, data.vstory()); - if (!idDate) { + const auto now = base::unixtime::now(); + const auto idDates = parseAndApply(user, data.vstory(), now); + if (!idDates) { + return; + } + const auto expired = (idDates.expires <= now); + if (expired) { + applyExpired({ peerId, idDates.id }); return; } const auto i = _all.find(peerId); if (i == end(_all)) { requestUserStories(user); return; - } else if (i->second.ids.contains(idDate)) { + } else if (i->second.ids.contains(idDates)) { return; } - const auto was = i->second.info(); - i->second.ids.emplace(idDate); - const auto now = i->second.info(); - if (was == now) { + const auto wasInfo = i->second.info(); + i->second.ids.emplace(idDates); + const auto nowInfo = i->second.info(); + if (wasInfo == nowInfo) { return; } const auto refreshInList = [&](StorySourcesList list) { @@ -317,7 +341,7 @@ void Stories::apply(const MTPDupdateStory &data) { peerId, &StoriesSourceInfo::id); if (i != end(sources)) { - *i = now; + *i = nowInfo; sort(list); } }; @@ -342,7 +366,61 @@ void Stories::requestUserStories(not_null user) { _requestingUserStories.remove(user); applyDeletedFromSources(user->id, StorySourcesList::All); }).send(); +} +void Stories::registerExpiring(TimeId expires, FullStoryId id) { + for (auto i = _expiring.findFirst(expires) + ; (i != end(_expiring)) && (i->first == expires) + ; ++i) { + if (i->second == id) { + return; + } + } + const auto reschedule = _expiring.empty() + || (_expiring.front().first > expires); + _expiring.emplace(expires, id); + if (reschedule) { + scheduleExpireTimer(); + } +} + +void Stories::scheduleExpireTimer() { + if (_expireSchedulePosted) { + return; + } + _expireSchedulePosted = true; + crl::on_main(this, [=] { + if (!_expireSchedulePosted) { + return; + } + _expireSchedulePosted = false; + if (_expiring.empty()) { + _expireTimer.cancel(); + } else { + const auto nearest = _expiring.front().first; + const auto now = base::unixtime::now(); + const auto delay = (nearest > now) + ? (nearest - now) + : 0; + _expireTimer.callOnce(delay * crl::time(1000)); + } + }); +} + +void Stories::processExpired() { + const auto now = base::unixtime::now(); + auto expired = base::flat_set(); + auto i = begin(_expiring); + for (; i != end(_expiring) && i->first <= now; ++i) { + expired.emplace(i->second); + } + _expiring.erase(begin(_expiring), i); + for (const auto &id : expired) { + applyExpired(id); + } + if (!_expiring.empty()) { + scheduleExpireTimer(); + } } void Stories::parseAndApply(const MTPUserStories &stories) { @@ -357,9 +435,10 @@ void Stories::parseAndApply(const MTPUserStories &stories) { .hidden = user->hasStoriesHidden(), }; const auto &list = data.vstories().v; + const auto now = base::unixtime::now(); result.ids.reserve(list.size()); for (const auto &story : list) { - if (const auto id = parseAndApply(result.user, story)) { + if (const auto id = parseAndApply(result.user, story, now)) { result.ids.emplace(id); } } @@ -391,21 +470,31 @@ void Stories::parseAndApply(const MTPUserStories &stories) { } sort(list); }; - add(StorySourcesList::All); - if (result.user->hasStoriesHidden()) { - applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + if (result.user->isContact()) { + add(StorySourcesList::All); + if (result.user->hasStoriesHidden()) { + applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + } else { + add(StorySourcesList::NotHidden); + } } else { - add(StorySourcesList::NotHidden); + applyDeletedFromSources(peerId, StorySourcesList::All); } } Story *Stories::parseAndApply( not_null peer, - const MTPDstoryItem &data) { + const MTPDstoryItem &data, + TimeId now) { const auto media = ParseMedia(_owner, data.vmedia()); if (!media) { return nullptr; } + const auto expires = data.vexpire_date().v; + const auto expired = (expires >= now); + if (expired && !data.is_pinned() && !peer->isSelf()) { + return nullptr; + } const auto id = data.vid().v; auto &stories = _stories[peer->id]; const auto i = stories.find(id); @@ -421,25 +510,51 @@ Story *Stories::parseAndApply( id, peer, StoryMedia{ *media }, - data.vdate().v)).first->second.get(); + data.vdate().v, + data.vexpire_date().v)).first->second.get(); result->applyChanges(*media, data); + + if (expired) { + _expiring.remove(expires, result->fullId()); + applyExpired(result->fullId()); + } else { + registerExpiring(expires, result->fullId()); + } + return result; } -StoryIdDate Stories::parseAndApply( +StoryIdDates Stories::parseAndApply( not_null peer, - const MTPstoryItem &story) { + const MTPstoryItem &story, + TimeId now) { return story.match([&](const MTPDstoryItem &data) { - if (const auto story = parseAndApply(peer, data)) { - return story->idDate(); + if (const auto story = parseAndApply(peer, data, now)) { + return story->idDates(); } applyDeleted({ peer->id, data.vid().v }); - return StoryIdDate(); + return StoryIdDates(); }, [&](const MTPDstoryItemSkipped &data) { - return StoryIdDate{ data.vid().v, data.vdate().v }; + const auto expires = data.vexpire_date().v; + const auto expired = (expires >= now); + const auto fullId = FullStoryId{ peer->id, data.vid().v }; + if (!expired) { + registerExpiring(expires, fullId); + } else if (!peer->isSelf()) { + applyDeleted(fullId); + return StoryIdDates(); + } else { + _expiring.remove(expires, fullId); + applyExpired(fullId); + } + return StoryIdDates{ + data.vid().v, + data.vdate().v, + data.vexpire_date().v, + }; }, [&](const MTPDstoryItemDeleted &data) { applyDeleted({ peer->id, data.vid().v }); - return StoryIdDate(); + return StoryIdDates(); }); } @@ -571,9 +686,10 @@ void Stories::sendResolveRequests() { void Stories::processResolvedStories( not_null peer, const QVector &list) { + const auto now = base::unixtime::now(); for (const auto &item : list) { item.match([&](const MTPDstoryItem &data) { - if (!parseAndApply(peer, data)) { + if (!parseAndApply(peer, data, now)) { applyDeleted({ peer->id, data.vid().v }); } }, [&](const MTPDstoryItemSkipped &data) { @@ -595,6 +711,48 @@ void Stories::finalizeResolve(FullStoryId id) { } void Stories::applyDeleted(FullStoryId id) { + applyRemovedFromActive(id); + + _deleted.emplace(id); + const auto j = _stories.find(id.peer); + if (j != end(_stories)) { + const auto k = j->second.find(id.story); + if (k != end(j->second)) { + auto story = std::move(k->second); + _expiring.remove(story->expires(), story->fullId()); + j->second.erase(k); + session().changes().storyUpdated( + story.get(), + UpdateFlag::Destroyed); + removeDependencyStory(story.get()); + if (j->second.empty()) { + _stories.erase(j); + } + } + } +} + +void Stories::applyExpired(FullStoryId id) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + if (story->peer()->isSelf()) { + addToExpiredMine(story); + } else if (!story->pinned()) { + applyDeleted(id); + return; + } + } + applyRemovedFromActive(id); +} + +void Stories::addToExpiredMine(not_null story) { + const auto added = _expiredMine.emplace(story->id()).second; + if (added && _expiredMineTotal >= 0) { + ++_expiredMineTotal; + } +} + +void Stories::applyRemovedFromActive(FullStoryId id) { const auto removeFromList = [&](StorySourcesList list) { const auto index = static_cast(list); auto &sources = _sources[index]; @@ -609,7 +767,7 @@ void Stories::applyDeleted(FullStoryId id) { }; const auto i = _all.find(id.peer); if (i != end(_all)) { - const auto j = i->second.ids.lower_bound(StoryIdDate{ id.story }); + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); if (j != end(i->second.ids) && j->id == id.story) { i->second.ids.erase(j); if (i->second.ids.empty()) { @@ -619,22 +777,6 @@ void Stories::applyDeleted(FullStoryId id) { } } } - _deleted.emplace(id); - const auto j = _stories.find(id.peer); - if (j != end(_stories)) { - const auto k = j->second.find(id.story); - if (k != end(j->second)) { - auto story = std::move(k->second); - j->second.erase(k); - session().changes().storyUpdated( - story.get(), - UpdateFlag::Destroyed); - removeDependencyStory(story.get()); - if (j->second.empty()) { - _stories.erase(j); - } - } - } } void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) { @@ -755,7 +897,7 @@ void Stories::loadAround(FullStoryId id, StoriesContext context) { if (i == end(_all)) { return; } - const auto j = i->second.ids.lower_bound(StoryIdDate{ id.story }); + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); if (j == end(i->second.ids) || j->id != id.story) { return; } @@ -960,6 +1102,67 @@ void Stories::loadViewsSlice( }).send(); } +const base::flat_set &Stories::expiredMine() const { + return _expiredMine; +} + +rpl::producer<> Stories::expiredMineChanged() const { + return _expiredMineChanged.events(); +} + +int Stories::expiredMineCount() const { + return std::max(_expiredMineTotal, 0); +} + +bool Stories::expiredMineCountKnown() const { + return _expiredMineTotal >= 0; +} + +bool Stories::expiredMineLoaded() const { + return _expiredMineLoaded; +} + +void Stories::expiredMineLoadMore() { + if (_expiredMineRequestId) { + return; + } + const auto api = &_owner->session().api(); + _expiredMineRequestId = api->request(MTPstories_GetExpiredStories( + MTP_int(_expiredMineLastId), + MTP_int(_expiredMineLastId + ? kExpiredMinePerPage + : kExpiredMineFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + _expiredMineRequestId = 0; + + const auto &data = result.data(); + _expiredMineTotal = std::max( + data.vcount().v, + int(_expiredMine.size())); + _expiredMineLoaded = data.vstories().v.empty(); + const auto self = _owner->session().user(); + const auto now = base::unixtime::now(); + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + _expiredMine.emplace(id); + _expiredMineLastId = id; + if (!parseAndApply(self, story, now)) { + _expiredMine.remove(id); + if (_expiredMineTotal > 0) { + --_expiredMineTotal; + } + } + } + _expiredMineChanged.fire({}); + }).fail([=] { + _expiredMineRequestId = 0; + _expiredMineLoaded = true; + _expiredMineTotal = int(_expiredMine.size()); + }).send(); +} + bool Stories::isQuitPrevent() { if (!_markReadPending.empty()) { sendMarkAsReadRequests(); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index f8f152b77..a05a1de67 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/expected.h" #include "base/timer.h" +#include "base/weak_ptr.h" class Image; class PhotoData; @@ -22,9 +23,10 @@ namespace Data { class Session; -struct StoryIdDate { +struct StoryIdDates { StoryId id = 0; TimeId date = 0; + TimeId expires = 0; [[nodiscard]] bool valid() const { return id != 0; @@ -33,8 +35,8 @@ struct StoryIdDate { return valid(); } - friend inline auto operator<=>(StoryIdDate, StoryIdDate) = default; - friend inline bool operator==(StoryIdDate, StoryIdDate) = default; + friend inline auto operator<=>(StoryIdDates, StoryIdDates) = default; + friend inline bool operator==(StoryIdDates, StoryIdDates) = default; }; struct StoryMedia { @@ -56,16 +58,20 @@ public: StoryId id, not_null peer, StoryMedia media, - TimeId date); + TimeId date, + TimeId expires); [[nodiscard]] Session &owner() const; [[nodiscard]] Main::Session &session() const; [[nodiscard]] not_null peer() const; [[nodiscard]] StoryId id() const; - [[nodiscard]] StoryIdDate idDate() const; + [[nodiscard]] bool mine() const; + [[nodiscard]] StoryIdDates idDates() const; [[nodiscard]] FullStoryId fullId() const; [[nodiscard]] TimeId date() const; + [[nodiscard]] TimeId expires() const; + [[nodiscard]] bool expired(TimeId now = 0) const; [[nodiscard]] const StoryMedia &media() const; [[nodiscard]] PhotoData *photo() const; [[nodiscard]] DocumentData *document() const; @@ -101,6 +107,7 @@ private: std::vector _viewsList; int _views = 0; const TimeId _date = 0; + const TimeId _expires = 0; bool _pinned = false; }; @@ -119,7 +126,7 @@ struct StoriesSourceInfo { struct StoriesSource { not_null user; - base::flat_set ids; + base::flat_set ids; StoryId readTill = 0; bool hidden = false; @@ -167,7 +174,7 @@ struct StoriesContext { inline constexpr auto kStorySourcesListCount = 2; -class Stories final { +class Stories final : public base::has_weak_ptr { public: explicit Stories(not_null owner); ~Stories(); @@ -210,14 +217,23 @@ public: std::optional offset, Fn)> done); + [[nodiscard]] const base::flat_set &expiredMine() const; + [[nodiscard]] rpl::producer<> expiredMineChanged() const; + [[nodiscard]] int expiredMineCount() const; + [[nodiscard]] bool expiredMineCountKnown() const; + [[nodiscard]] bool expiredMineLoaded() const; + [[nodiscard]] void expiredMineLoadMore(); + private: void parseAndApply(const MTPUserStories &stories); [[nodiscard]] Story *parseAndApply( not_null peer, - const MTPDstoryItem &data); - StoryIdDate parseAndApply( + const MTPDstoryItem &data, + TimeId now); + StoryIdDates parseAndApply( not_null peer, - const MTPstoryItem &story); + const MTPstoryItem &story, + TimeId now); void processResolvedStories( not_null peer, const QVector &list); @@ -225,20 +241,30 @@ private: void finalizeResolve(FullStoryId id); void applyDeleted(FullStoryId id); + void applyExpired(FullStoryId id); + void applyRemovedFromActive(FullStoryId id); void applyDeletedFromSources(PeerId id, StorySourcesList list); void removeDependencyStory(not_null story); void sort(StorySourcesList list); + void addToExpiredMine(not_null story); + void sendMarkAsReadRequests(); void sendMarkAsReadRequest(not_null peer, StoryId tillId); void requestUserStories(not_null user); + void registerExpiring(TimeId expires, FullStoryId id); + void scheduleExpireTimer(); + void processExpired(); const not_null _owner; base::flat_map< PeerId, base::flat_map>> _stories; + base::flat_multi_map _expiring; base::flat_set _deleted; + base::Timer _expireTimer; + bool _expireSchedulePosted = false; base::flat_map< PeerId, @@ -261,6 +287,13 @@ private: rpl::event_stream _itemsChanged; + base::flat_set _expiredMine; + int _expiredMineTotal = -1; + StoryId _expiredMineLastId = 0; + bool _expiredMineLoaded = false; + rpl::event_stream<> _expiredMineChanged; + mtpRequestId _expiredMineRequestId = 0; + base::flat_set _markReadPending; base::Timer _markReadTimer; base::flat_set _markReadRequests; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 0a1b1a8a0..31c73afa8 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -297,8 +297,10 @@ ClickHandlerPtr JumpToStoryClickHandler( ? separate->sessionController() : peer->session().tryResolveWindow(); if (controller) { - // #TODO stories decide context - controller->openPeerStory(peer, storyId, {}); + controller->openPeerStory( + peer, + storyId, + { Data::StoriesContextSingle() }); } }); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index c343a6214..bb7337650 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -422,14 +422,14 @@ void Controller::show( stories.loadMore(list); } }); - const auto idDate = story->idDate(); + const auto idDates = story->idDates(); if (!source) { return; } else if (source == &single) { - single.ids.emplace(idDate); + single.ids.emplace(idDates); _index = 0; } else { - const auto k = source->ids.find(idDate); + const auto k = source->ids.find(idDates); if (k == end(source->ids)) { return; } diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 305b88329..1a03e874f 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -59,6 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_user.h" #include "data/data_changes.h" +#include "data/data_stories.h" #include "mainwidget.h" #include "styles/style_window.h" #include "styles/style_widgets.h" @@ -759,6 +760,37 @@ void MainMenu::setupMenu() { )->setClickedCallback([=] { controller->showPeerHistory(controller->session().user()); }); + + const auto wrap = _menu->add( + object_ptr>( + _menu, + CreateButton( + _menu, + rpl::single(u"My Stories"_q), + st::mainMenuButton, + IconDescriptor{ + &st::settingsIconSavedMessages, + kIconLightOrange + }))); + const auto stories = &controller->session().data().stories(); + const auto &all = stories->all(); + const auto mine = all.find(controller->session().userPeerId()); + if ((mine != end(all) && !mine->second.ids.empty()) + || stories->expiredMineCount() > 0) { + wrap->toggle(true, anim::type::instant); + } else { + wrap->toggle(false, anim::type::instant); + if (!stories->expiredMineCountKnown()) { + stories->expiredMineLoadMore(); + wrap->toggleOn(stories->expiredMineChanged( + ) | rpl::map([=] { + return stories->expiredMineCount() > 0; + }) | rpl::filter(rpl::mappers::_1) | rpl::take(1)); + } + } + wrap->entity()->setClickedCallback([=] { + controller->showToast(u"My Stories"_q); + }); } else { addAction( tr::lng_profile_add_contact(), diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index fd26e5e28..a39f552d2 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2488,7 +2488,7 @@ void SessionController::openPeerStories( const auto i = all.find(peerId); if (i != end(all)) { const auto j = i->second.ids.lower_bound( - StoryIdDate{ i->second.readTill + 1 }); + StoryIdDates{ i->second.readTill + 1 }); openPeerStory( i->second.user, j != i->second.ids.end() ? j->id : i->second.ids.front().id, From 7f8a985067dc0e6a38c423527089e739869f1f9e Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 5 Jun 2023 20:31:15 +0400 Subject: [PATCH 059/260] Start stories overview in profile / My Stories. --- Telegram/CMakeLists.txt | 8 + Telegram/Resources/langs/lang.strings | 1 + .../data/data_abstract_sparse_ids.h | 6 +- Telegram/SourceFiles/data/data_msg_id.h | 82 ++-- Telegram/SourceFiles/data/data_stories.cpp | 164 ++++++- Telegram/SourceFiles/data/data_stories.h | 53 ++- .../SourceFiles/data/data_stories_ids.cpp | 142 ++++++ Telegram/SourceFiles/data/data_stories_ids.h | 32 ++ Telegram/SourceFiles/data/data_types.h | 2 + Telegram/SourceFiles/data/data_user.cpp | 3 + .../dialogs/ui/dialogs_stories_content.cpp | 8 +- .../export/data/export_data_types.cpp | 2 + Telegram/SourceFiles/history/history_item.cpp | 39 ++ Telegram/SourceFiles/history/history_item.h | 3 + .../history/history_item_helpers.cpp | 2 + .../info/downloads/info_downloads_widget.cpp | 2 +- .../SourceFiles/info/info_content_widget.cpp | 7 + .../SourceFiles/info/info_content_widget.h | 25 +- Telegram/SourceFiles/info/info_controller.cpp | 17 + Telegram/SourceFiles/info/info_controller.h | 50 ++- .../info/media/info_media_buttons.h | 27 ++ .../info/media/info_media_list_section.cpp | 2 + .../info/media/info_media_list_widget.cpp | 7 + .../info/media/info_media_list_widget.h | 1 - .../info/media/info_media_widget.cpp | 4 + .../profile/info_profile_inner_widget.cpp | 16 + .../stories/info_stories_inner_widget.cpp | 191 ++++++++ .../info/stories/info_stories_inner_widget.h | 78 ++++ .../info/stories/info_stories_provider.cpp | 407 ++++++++++++++++++ .../info/stories/info_stories_provider.h | 132 ++++++ .../info/stories/info_stories_widget.cpp | 112 +++++ .../info/stories/info_stories_widget.h | 70 +++ .../stories/media_stories_controller.cpp | 27 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 2 + .../SourceFiles/window/window_main_menu.cpp | 13 +- .../window/window_session_controller.cpp | 44 +- .../window/window_session_controller.h | 1 + 37 files changed, 1677 insertions(+), 105 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_stories_ids.cpp create mode 100644 Telegram/SourceFiles/data/data_stories_ids.h create mode 100644 Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp create mode 100644 Telegram/SourceFiles/info/stories/info_stories_inner_widget.h create mode 100644 Telegram/SourceFiles/info/stories/info_stories_provider.cpp create mode 100644 Telegram/SourceFiles/info/stories/info_stories_provider.h create mode 100644 Telegram/SourceFiles/info/stories/info_stories_widget.cpp create mode 100644 Telegram/SourceFiles/info/stories/info_stories_widget.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c02353676..c42e141da 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -551,6 +551,8 @@ PRIVATE data/data_sponsored_messages.h data/data_stories.cpp data/data_stories.h + data/data_stories_ids.cpp + data/data_stories_ids.h data/data_streaming.cpp data/data_streaming.h data/data_thread.cpp @@ -871,6 +873,12 @@ PRIVATE info/profile/info_profile_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h + info/stories/info_stories_inner_widget.cpp + info/stories/info_stories_inner_widget.h + info/stories/info_stories_provider.cpp + info/stories/info_stories_provider.h + info/stories/info_stories_widget.cpp + info/stories/info_stories_widget.h info/userpic/info_userpic_colors_editor.cpp info/userpic/info_userpic_colors_editor.h info/userpic/info_userpic_emoji_builder.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 220683f8e..40783dbab 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_menu_activate" = "Activate"; "lng_menu_set_status" = "Set Emoji Status"; "lng_menu_change_status" = "Change Emoji Status"; +"lng_menu_my_stories" = "My Stories"; "lng_disable_notifications_from_tray" = "Disable notifications"; "lng_enable_notifications_from_tray" = "Enable notifications"; diff --git a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h index d48542d19..4a4d62d17 100644 --- a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h @@ -57,7 +57,7 @@ public: return std::nullopt; } [[nodiscard]] std::optional nearest(Id id) const { - static_assert(std::is_same_v>); + static_assert(std::is_same_v>); if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) { return *it; } else if (_ids.empty()) { @@ -70,6 +70,10 @@ public: std::swap(_skippedBefore, _skippedAfter); } + friend inline bool operator==( + const AbstractSparseIds&, + const AbstractSparseIds&) = default; + private: IdsContainer _ids; std::optional _fullCount; diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index af6b73fcc..aa3bf1785 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -51,14 +51,49 @@ Q_DECLARE_METATYPE(MsgId); return MsgId(a.bare - b.bare); } +using StoryId = int32; + +struct FullStoryId { + PeerId peer = 0; + StoryId story = 0; + + [[nodiscard]] bool valid() const { + return peer != 0 && story != 0; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullStoryId, FullStoryId) = default; + friend inline bool operator==(FullStoryId, FullStoryId) = default; +}; + +struct FullReplyTo { + MsgId msgId = 0; + MsgId topicRootId = 0; + FullStoryId storyId; + + [[nodiscard]] bool valid() const { + return msgId || storyId; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; + friend inline bool operator==(FullReplyTo, FullReplyTo) = default; +}; + constexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58)); constexpr auto ClientMsgIds = (1LL << 31); constexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds); +constexpr auto StartStoryMsgId = MsgId(EndClientMsgId.bare + 1); +constexpr auto ServerMaxStoryId = StoryId(1 << 30); +constexpr auto StoryMsgIds = int64(ServerMaxStoryId); +constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds); constexpr auto ServerMaxMsgId = MsgId(1LL << 56); constexpr auto ScheduledMsgIdsRange = (1LL << 32); constexpr auto ShowAtUnreadMsgId = MsgId(0); -constexpr auto SpecialMsgIdShift = EndClientMsgId.bare; +constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare; constexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1); constexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2); constexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4); @@ -81,6 +116,20 @@ static_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId); return MsgId(StartClientMsgId.bare + index); } +[[nodiscrd]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept { + return (id >= StartStoryMsgId && id < EndStoryMsgId); +} +[[nodiscard]] constexpr inline StoryId StoryIdFromMsgId(MsgId id) noexcept { + Expects(IsStoryMsgId(id)); + + return StoryId(id.bare - StartStoryMsgId.bare); +} +[[nodiscard]] constexpr inline MsgId StoryIdToMsgId(StoryId id) noexcept { + Expects(id >= 0); + + return MsgId(StartStoryMsgId.bare + id); +} + [[nodiscard]] constexpr inline bool IsServerMsgId(MsgId id) noexcept { return (id > 0 && id < ServerMaxMsgId); } @@ -136,37 +185,6 @@ struct GlobalMsgId { } }; -using StoryId = int32; - -struct FullStoryId { - PeerId peer = 0; - StoryId story = 0; - - [[nodiscard]] bool valid() const { - return peer != 0 && story != 0; - } - explicit operator bool() const { - return valid(); - } - friend inline auto operator<=>(FullStoryId, FullStoryId) = default; - friend inline bool operator==(FullStoryId, FullStoryId) = default; -}; - -struct FullReplyTo { - MsgId msgId = 0; - MsgId topicRootId = 0; - FullStoryId storyId; - - [[nodiscard]] bool valid() const { - return msgId || storyId; - } - explicit operator bool() const { - return valid(); - } - friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; - friend inline bool operator==(FullReplyTo, FullReplyTo) = default; -}; - namespace std { template <> diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 5eef52f69..fc164eb17 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -35,6 +35,8 @@ constexpr auto kPreloadAroundCount = 30; constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); constexpr auto kExpiredMineFirstPerPage = 30; constexpr auto kExpiredMinePerPage = 100; +constexpr auto kSavedFirstPerPage = 30; +constexpr auto kSavedPerPage = 100; using UpdateFlag = StoryUpdate::Flag; @@ -351,6 +353,30 @@ void Stories::apply(const MTPDupdateStory &data) { } } +void Stories::apply(not_null peer, const MTPUserStories *data) { + if (!data) { + applyDeletedFromSources(peer->id, StorySourcesList::All); + _all.erase(peer->id); + const auto i = _stories.find(peer->id); + if (i != end(_stories)) { + auto stories = base::take(i->second); + _stories.erase(i); + for (const auto &[id, story] : stories) { + // Duplicated in Stories::applyDeleted. + _deleted.emplace(FullStoryId{ peer->id, id }); + _expiring.remove(story->expires(), story->fullId()); + session().changes().storyUpdated( + story.get(), + UpdateFlag::Destroyed); + removeDependencyStory(story.get()); + } + } + _sourceChanged.fire_copy(peer->id); + } else { + parseAndApply(*data); + } +} + void Stories::requestUserStories(not_null user) { if (!_requestingUserStories.emplace(user).second) { return; @@ -480,6 +506,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) { } else { applyDeletedFromSources(peerId, StorySourcesList::All); } + _sourceChanged.fire_copy(peerId); } Story *Stories::parseAndApply( @@ -503,6 +530,9 @@ Story *Stories::parseAndApply( session().changes().storyUpdated( i->second.get(), UpdateFlag::Edited); + if (const auto item = lookupItem(i->second.get())) { + item->applyChanges(i->second.get()); + } } return i->second.get(); } @@ -718,6 +748,7 @@ void Stories::applyDeleted(FullStoryId id) { if (j != end(_stories)) { const auto k = j->second.find(id.story); if (k != end(j->second)) { + // Duplicated in Stories::apply(peer, const MTPUserStories*). auto story = std::move(k->second); _expiring.remove(story->expires(), story->fullId()); j->second.erase(k); @@ -746,7 +777,7 @@ void Stories::applyExpired(FullStoryId id) { } void Stories::addToExpiredMine(not_null story) { - const auto added = _expiredMine.emplace(story->id()).second; + const auto added = _expiredMine.list.emplace(story->id()).second; if (added && _expiredMineTotal >= 0) { ++_expiredMineTotal; } @@ -775,6 +806,7 @@ void Stories::applyRemovedFromActive(FullStoryId id) { removeFromList(StorySourcesList::NotHidden); removeFromList(StorySourcesList::All); } + _sourceChanged.fire_copy(id.peer); } } } @@ -824,8 +856,42 @@ void Stories::sort(StorySourcesList list) { _sourcesChanged[index].fire({}); } -const base::flat_map &Stories::all() const { - return _all; +std::shared_ptr Stories::lookupItem(not_null story) { + const auto i = _items.find(story->peer()->id); + if (i == end(_items)) { + return nullptr; + } + const auto j = i->second.find(story->id()); + if (j == end(i->second)) { + return nullptr; + } + return j->second.lock(); +} + +std::shared_ptr Stories::resolveItem(not_null story) { + auto &items = _items[story->peer()->id]; + auto i = items.find(story->id()); + if (i == end(items)) { + i = items.emplace(story->id()).first; + } else if (const auto result = i->second.lock()) { + return result; + } + const auto history = _owner->history(story->peer()); + auto result = std::shared_ptr( + history->makeMessage(story).get(), + HistoryItem::Destroyer()); + i->second = result; + return result; +} + +std::shared_ptr Stories::resolveItem(FullStoryId id) { + const auto story = lookup(id); + return story ? resolveItem(*story) : std::shared_ptr(); +} + +const StoriesSource *Stories::source(PeerId id) const { + const auto i = _all.find(id); + return (i != end(_all)) ? &i->second : nullptr; } const std::vector &Stories::sources( @@ -841,6 +907,10 @@ rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const { return _sourcesChanged[static_cast(list)].events(); } +rpl::producer Stories::sourceChanged() const { + return _sourceChanged.events(); +} + rpl::producer Stories::itemsChanged() const { return _itemsChanged.events(); } @@ -1102,7 +1172,7 @@ void Stories::loadViewsSlice( }).send(); } -const base::flat_set &Stories::expiredMine() const { +const StoriesIds &Stories::expiredMine() const { return _expiredMine; } @@ -1122,6 +1192,30 @@ bool Stories::expiredMineLoaded() const { return _expiredMineLoaded; } +const StoriesIds *Stories::saved(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? &i->second.ids : nullptr; +} + +rpl::producer Stories::savedChanged() const { + return _savedChanged.events(); +} + +int Stories::savedCount(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? i->second.total : 0; +} + +bool Stories::savedCountKnown(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && (i->second.total >= 0); +} + +bool Stories::savedLoaded(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && i->second.loaded; +} + void Stories::expiredMineLoadMore() { if (_expiredMineRequestId) { return; @@ -1136,33 +1230,81 @@ void Stories::expiredMineLoadMore() { _expiredMineRequestId = 0; const auto &data = result.data(); - _expiredMineTotal = std::max( - data.vcount().v, - int(_expiredMine.size())); - _expiredMineLoaded = data.vstories().v.empty(); const auto self = _owner->session().user(); const auto now = base::unixtime::now(); + _expiredMineTotal = data.vcount().v; for (const auto &story : data.vstories().v) { const auto id = story.match([&](const auto &id) { return id.vid().v; }); - _expiredMine.emplace(id); + _expiredMine.list.emplace(id); _expiredMineLastId = id; if (!parseAndApply(self, story, now)) { - _expiredMine.remove(id); + _expiredMine.list.remove(id); if (_expiredMineTotal > 0) { --_expiredMineTotal; } } } + _expiredMineTotal = std::max( + _expiredMineTotal, + int(_expiredMine.list.size())); + _expiredMineLoaded = data.vstories().v.empty(); _expiredMineChanged.fire({}); }).fail([=] { _expiredMineRequestId = 0; _expiredMineLoaded = true; - _expiredMineTotal = int(_expiredMine.size()); + _expiredMineTotal = int(_expiredMine.list.size()); + _expiredMineChanged.fire({}); }).send(); } +void Stories::savedLoadMore(PeerId peerId) { + Expects(peerIsUser(peerId)); + + auto &saved = _saved[peerId]; + if (saved.requestId) { + return; + } + const auto api = &_owner->session().api(); + const auto peer = _owner->peer(peerId); + saved.requestId = api->request(MTPstories_GetPinnedStories( + peer->asUser()->inputUser, + MTP_int(saved.lastId), + MTP_int(saved.lastId ? kSavedPerPage : kSavedFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + auto &saved = _saved[peerId]; + saved.requestId = 0; + + const auto &data = result.data(); + const auto now = base::unixtime::now(); + saved.total = data.vcount().v; + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + saved.ids.list.emplace(id); + saved.lastId = id; + if (!parseAndApply(peer, story, now)) { + saved.ids.list.remove(id); + if (saved.total > 0) { + --saved.total; + } + } + } + saved.total = std::max(saved.total, int(saved.ids.list.size())); + saved.loaded = data.vstories().v.empty(); + _savedChanged.fire_copy(peerId); + }).fail([=] { + auto &saved = _saved[peerId]; + saved.requestId = 0; + saved.loaded = true; + saved.total = int(saved.ids.list.size()); + _savedChanged.fire_copy(peerId); + }).send(); + +} + bool Stories::isQuitPrevent() { if (!_markReadPending.empty()) { sendMarkAsReadRequests(); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index a05a1de67..5d26f7474 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -39,6 +39,14 @@ struct StoryIdDates { friend inline bool operator==(StoryIdDates, StoryIdDates) = default; }; +struct StoriesIds { + base::flat_set> list; + + friend inline bool operator==( + const StoriesIds&, + const StoriesIds&) = default; +}; + struct StoryMedia { std::variant, not_null> data; @@ -192,19 +200,24 @@ public: void loadMore(StorySourcesList list); void apply(const MTPDupdateStory &data); + void apply(not_null peer, const MTPUserStories *data); void loadAround(FullStoryId id, StoriesContext context); - [[nodiscard]] const base::flat_map &all() const; + const StoriesSource *source(PeerId id) const; [[nodiscard]] const std::vector &sources( StorySourcesList list) const; [[nodiscard]] bool sourcesLoaded(StorySourcesList list) const; [[nodiscard]] rpl::producer<> sourcesChanged( StorySourcesList list) const; + [[nodiscard]] rpl::producer sourceChanged() const; [[nodiscard]] rpl::producer itemsChanged() const; [[nodiscard]] base::expected, NoStory> lookup( FullStoryId id) const; void resolve(FullStoryId id, Fn done); + [[nodiscard]] std::shared_ptr resolveItem(FullStoryId id); + [[nodiscard]] std::shared_ptr resolveItem( + not_null story); [[nodiscard]] bool isQuitPrevent(); void markAsRead(FullStoryId id, bool viewed); @@ -217,14 +230,29 @@ public: std::optional offset, Fn)> done); - [[nodiscard]] const base::flat_set &expiredMine() const; + [[nodiscard]] const StoriesIds &expiredMine() const; [[nodiscard]] rpl::producer<> expiredMineChanged() const; [[nodiscard]] int expiredMineCount() const; [[nodiscard]] bool expiredMineCountKnown() const; [[nodiscard]] bool expiredMineLoaded() const; - [[nodiscard]] void expiredMineLoadMore(); + void expiredMineLoadMore(); + + [[nodiscard]] const StoriesIds *saved(PeerId peerId) const; + [[nodiscard]] rpl::producer savedChanged() const; + [[nodiscard]] int savedCount(PeerId peerId) const; + [[nodiscard]] bool savedCountKnown(PeerId peerId) const; + [[nodiscard]] bool savedLoaded(PeerId peerId) const; + void savedLoadMore(PeerId peerId); private: + struct Saved { + StoriesIds ids; + int total = -1; + StoryId lastId = 0; + bool loaded = false; + mtpRequestId requestId = 0; + }; + void parseAndApply(const MTPUserStories &stories); [[nodiscard]] Story *parseAndApply( not_null peer, @@ -247,20 +275,25 @@ private: void removeDependencyStory(not_null story); void sort(StorySourcesList list); - void addToExpiredMine(not_null story); + [[nodiscard]] std::shared_ptr lookupItem( + not_null story); void sendMarkAsReadRequests(); void sendMarkAsReadRequest(not_null peer, StoryId tillId); void requestUserStories(not_null user); + void addToExpiredMine(not_null story); void registerExpiring(TimeId expires, FullStoryId id); void scheduleExpireTimer(); void processExpired(); const not_null _owner; - base::flat_map< + std::unordered_map< PeerId, base::flat_map>> _stories; + std::unordered_map< + PeerId, + base::flat_map>> _items; base::flat_multi_map _expiring; base::flat_set _deleted; base::Timer _expireTimer; @@ -273,11 +306,11 @@ private: PeerId, base::flat_map>>> _resolveSent; - std::map< + std::unordered_map< not_null, base::flat_set>> _dependentMessages; - base::flat_map _all; + std::unordered_map _all; std::vector _sources[kStorySourcesListCount]; rpl::event_stream<> _sourcesChanged[kStorySourcesListCount]; bool _sourcesLoaded[kStorySourcesListCount] = { false }; @@ -285,15 +318,19 @@ private: mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 }; + rpl::event_stream _sourceChanged; rpl::event_stream _itemsChanged; - base::flat_set _expiredMine; + StoriesIds _expiredMine; int _expiredMineTotal = -1; StoryId _expiredMineLastId = 0; bool _expiredMineLoaded = false; rpl::event_stream<> _expiredMineChanged; mtpRequestId _expiredMineRequestId = 0; + std::unordered_map _saved; + rpl::event_stream _savedChanged; + base::flat_set _markReadPending; base::Timer _markReadTimer; base::flat_set _markReadRequests; diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp new file mode 100644 index 000000000..610d51c3a --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.cpp @@ -0,0 +1,142 @@ +/* +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 "data/data_stories_ids.h" + +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "main/main_session.h" + +namespace Data { + +rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + const auto stories = &peer->owner().stories(); + if (!stories->savedCountKnown(peer->id)) { + return; + } + + const auto source = stories->source(peer->id); + const auto saved = stories->saved(peer->id); + const auto count = stories->savedCount(peer->id); + Assert(saved != nullptr); + auto ids = base::flat_set(); + ids.reserve(saved->list.size() + 1); + auto total = count; + if (source && !source->ids.empty()) { + ++total; + const auto current = source->ids.front().id; + for (const auto id : ranges::views::reverse(saved->list)) { + const auto i = source->ids.lower_bound( + StoryIdDates{ id }); + if (i != end(source->ids) && i->id == id) { + --total; + } else { + ids.emplace(id); + } + } + ids.emplace(current); + } else { + auto all = saved->list | ranges::views::reverse; + ids = { begin(all), end(all) }; + } + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + total, + 0, + total - added); + consumer.put_next_copy(state->slice); + }; + + const auto stories = &peer->owner().stories(); + stories->sourceChanged( + ) | rpl::filter( + rpl::mappers::_1 == peer->id + ) | rpl::start_with_next([=] { + push(); + }, lifetime); + + stories->savedChanged( + ) | rpl::filter( + rpl::mappers::_1 == peer->id + ) | rpl::start_with_next([=] { + push(); + }, lifetime); + + if (!stories->savedCountKnown(peer->id)) { + stories->savedLoadMore(peer->id); + } + + push(); + + return lifetime; + }; +} + +rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + const auto stories = &session->data().stories(); + if (!stories->expiredMineCountKnown()) { + return; + } + + const auto expired = stories->expiredMine(); + const auto count = stories->expiredMineCount(); + auto ids = base::flat_set(); + ids.reserve(expired.list.size() + 1); + auto all = expired.list | ranges::views::reverse; + ids = { begin(all), end(all) }; + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + count, + 0, + count - added); + consumer.put_next_copy(state->slice); + }; + + const auto stories = &session->data().stories(); + stories->expiredMineChanged( + ) | rpl::start_with_next([=] { + push(); + }, lifetime); + + if (!stories->expiredMineCountKnown()) { + stories->expiredMineLoadMore(); + } + + push(); + + return lifetime; + }; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories_ids.h b/Telegram/SourceFiles/data/data_stories_ids.h new file mode 100644 index 000000000..26f827f96 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.h @@ -0,0 +1,32 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_abstract_sparse_ids.h" + +class PeerData; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +using StoriesIdsSlice = AbstractSparseIds>; + +[[nodiscard]] rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit); + +[[nodiscard]] rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 710cc4e1f..a296e6cf4 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -301,6 +301,8 @@ enum class MessageFlag : uint64 { // Fake message with bot cover and information. FakeBotAbout = (1ULL << 36), + + StoryItem = (1ULL << 37), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index c903008ae..148ffb246 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_peer_bot_command.h" #include "data/data_photo.h" +#include "data/data_stories.h" #include "data/data_emoji_statuses.h" #include "data/data_user_names.h" #include "data/data_wall_paper.h" @@ -474,6 +475,8 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setWallPaper({}); } + user->owner().stories().apply(user, update.vstories()); + user->fullUpdated(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 830950416..0e6f05b3e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -128,16 +128,14 @@ State::State(not_null data, Data::StorySourcesList list) Content State::next() { auto result = Content{ .full = (_list == Data::StorySourcesList::All) }; - const auto &all = _data->all(); const auto &sources = _data->sources(_list); result.users.reserve(sources.size()); for (const auto &info : sources) { - const auto i = all.find(info.id); - Assert(i != end(all)); - const auto &source = i->second; + const auto source = _data->source(info.id); + Assert(source != nullptr); auto userpic = std::shared_ptr(); - const auto user = source.user; + const auto user = source->user; if (const auto i = _userpics.find(user); i != end(_userpics)) { userpic = i->second; } else { diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 7e0612860..e2840acc4 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -954,6 +954,8 @@ Media ParseMedia( result.content = ParsePoll(data); }, [](const MTPDmessageMediaDice &data) { // #TODO dice + }, [](const MTPDmessageMediaStory &data) { + // #TODO stories export }, [](const MTPDmessageMediaEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 1a99b7024..e886b7ed7 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" // Data::GroupCall::id(). #include "data/data_poll.h" // PollData::publicVotes. #include "data/data_sponsored_messages.h" +#include "data/data_stories.h" #include "data/data_wall_paper.h" #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" @@ -289,6 +290,8 @@ std::unique_ptr HistoryItem::CreateMedia( item, qs(media.vemoticon()), media.vvalue().v); + }, [&](const MTPDmessageMediaStory &media) -> Result { + return nullptr; // #TODO stories }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { @@ -673,6 +676,17 @@ HistoryItem::HistoryItem( } } +HistoryItem::HistoryItem( + not_null history, + not_null story) +: id(StoryIdToMsgId(story->id())) +, _history(history) +, _from(history->peer) +, _flags(MessageFlag::Local | MessageFlag::Outgoing | MessageFlag::FakeHistoryItem | MessageFlag::StoryItem) +, _date(story->date()) { + setStoryFields(story); +} + HistoryItem::~HistoryItem() { _media = nullptr; clearSavedMedia(); @@ -1491,6 +1505,31 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { finishEdition(keyboardTop); } +void HistoryItem::applyChanges(not_null story) { + Expects(_flags & MessageFlag::StoryItem); + Expects(StoryIdFromMsgId(id) == story->id()); + + _media = nullptr; + setStoryFields(story); + + finishEdition(-1); +} + +void HistoryItem::setStoryFields(not_null story) { + const auto spoiler = false; + if (const auto photo = story->photo()) { + _media = std::make_unique(this, photo, spoiler); + } else { + const auto document = story->document(); + _media = std::make_unique( + this, + document, + /*skipPremiumEffect=*/false, + spoiler); + } + setText(story->caption()); +} + void HistoryItem::applyEdition(const MTPDmessageService &message) { if (message.vaction().type() == mtpc_messageActionHistoryClear) { const auto wasGrouped = history()->owner().groups().isGrouped(this); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index bbc074879..c2f26fb44 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -179,6 +179,7 @@ public: const QString &postAuthor, not_null game, HistoryMessageMarkupData &&markup); + HistoryItem(not_null history, not_null story); ~HistoryItem(); struct Destroyer { @@ -332,6 +333,7 @@ public: [[nodiscard]] bool isService() const; void applyEdition(HistoryMessageEdition &&edition); + void applyChanges(not_null story); void applyEdition(const MTPDmessageService &message); void applyEdition(const MTPMessageExtendedMedia &media); @@ -559,6 +561,7 @@ private: bool updateServiceDependent(bool force = false); void setServiceText(PreparedServiceText &&prepared); + void setStoryFields(not_null story); void finishEdition(int oldKeyboardTop); void finishEditionToEmpty(); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 31c73afa8..275b6ebcb 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -443,6 +443,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return Result::Good; }, [](const MTPDmessageMediaDice &) { return Result::Good; + }, [](const MTPDmessageMediaStory &data) { + return Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp index 26a46d22a..6efd528f6 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -26,7 +26,7 @@ Memento::Memento(not_null controller) } Memento::Memento(not_null self) -: ContentMemento(Downloads::Tag{}) +: ContentMemento(Tag{}) , _media(self, 0, Media::Type::File) { } diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index fcd405a5e..640c75d67 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -332,6 +332,8 @@ Key ContentMemento::key() const { return Key(poll, pollContextId()); } else if (const auto self = settingsSelf()) { return Settings::Tag{ self }; + } else if (const auto peer = storiesPeer()) { + return Stories::Tag{ peer, storiesTab() }; } else { return Downloads::Tag(); } @@ -363,4 +365,9 @@ ContentMemento::ContentMemento(Settings::Tag settings) ContentMemento::ContentMemento(Downloads::Tag downloads) { } +ContentMemento::ContentMemento(Stories::Tag stories) +: _storiesPeer(stories.peer) +, _storiesTab(stories.tab) { +} + } // namespace Info diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 44f4e2d8c..1a54f5fdf 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -24,14 +24,20 @@ template class PaddingWrap; } // namespace Ui -namespace Info { -namespace Settings { +namespace Info::Settings { struct Tag; -} // namespace Settings +} // namespace Info::Settings -namespace Downloads { +namespace Info::Downloads { struct Tag; -} // namespace Downloads +} // namespace Info::Downloads + +namespace Info::Stories { +struct Tag; +enum class Tab; +} // namespace Info::Stories + +namespace Info { class ContentMemento; class Controller; @@ -150,6 +156,7 @@ public: PeerId migratedPeerId); explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); + explicit ContentMemento(Stories::Tag stories); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) , _pollContextId(contextId) { @@ -172,6 +179,12 @@ public: UserData *settingsSelf() const { return _settingsSelf; } + PeerData *storiesPeer() const { + return _storiesPeer; + } + Stories::Tab storiesTab() const { + return _storiesTab; + } PollData *poll() const { return _poll; } @@ -214,6 +227,8 @@ private: const PeerId _migratedPeerId = 0; Data::ForumTopic *_topic = nullptr; UserData * const _settingsSelf = nullptr; + PeerData * const _storiesPeer = nullptr; + Stories::Tab _storiesTab = {}; PollData * const _poll = nullptr; const FullMsgId _pollContextId; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 58dda92fe..a9c57a75f 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -40,6 +40,9 @@ Key::Key(Settings::Tag settings) : _value(settings) { Key::Key(Downloads::Tag downloads) : _value(downloads) { } +Key::Key(Stories::Tag stories) : _value(stories) { +} + Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } @@ -72,6 +75,20 @@ bool Key::isDownloads() const { return v::is(_value); } +PeerData *Key::storiesPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + +Stories::Tab Key::storiesTab() const { + if (const auto tag = std::get_if(&_value)) { + return tag->tab; + } + return Stories::Tab(); +} + PollData *Key::poll() const { if (const auto data = std::get_if(&_value)) { return data->poll; diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index cdbf53396..82eb6f8eb 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -36,6 +36,25 @@ struct Tag { } // namespace Info::Downloads +namespace Info::Stories { + +enum class Tab { + Saved, + Archive, +}; + +struct Tag { + explicit Tag(not_null peer, Tab tab = {}) + : peer(peer) + , tab(tab) { + } + + not_null peer; + Tab tab = {}; +}; + +} // namespace Info::Stories + namespace Info { class Key { @@ -44,12 +63,15 @@ public: explicit Key(not_null topic); Key(Settings::Tag settings); Key(Downloads::Tag downloads); + Key(Stories::Tag stories); Key(not_null poll, FullMsgId contextId); PeerData *peer() const; Data::ForumTopic *topic() const; UserData *settingsSelf() const; bool isDownloads() const; + PeerData *storiesPeer() const; + Stories::Tab storiesTab() const; PollData *poll() const; FullMsgId pollContextId() const; @@ -63,6 +85,7 @@ private: not_null, Settings::Tag, Downloads::Tag, + Stories::Tag, PollKey> _value; }; @@ -81,6 +104,7 @@ public: Members, Settings, Downloads, + Stories, PollResults, }; using SettingsType = ::Settings::Type; @@ -123,23 +147,29 @@ class AbstractController : public Window::SessionNavigation { public: AbstractController(not_null parent); - virtual Key key() const = 0; - virtual PeerData *migrated() const = 0; - virtual Section section() const = 0; + [[nodiscard]] virtual Key key() const = 0; + [[nodiscard]] virtual PeerData *migrated() const = 0; + [[nodiscard]] virtual Section section() const = 0; - PeerData *peer() const; - PeerId migratedPeerId() const; - Data::ForumTopic *topic() const { + [[nodiscard]] PeerData *peer() const; + [[nodiscard]] PeerId migratedPeerId() const; + [[nodiscard]] Data::ForumTopic *topic() const { return key().topic(); } - UserData *settingsSelf() const { + [[nodiscard]] UserData *settingsSelf() const { return key().settingsSelf(); } - bool isDownloads() const { + [[nodiscard]] bool isDownloads() const { return key().isDownloads(); } - PollData *poll() const; - FullMsgId pollContextId() const { + [[nodiscard]] PeerData *storiesPeer() const { + return key().storiesPeer(); + } + [[nodiscard]] Stories::Tab storiesTab() const { + return key().storiesTab(); + } + [[nodiscard]] PollData *poll() const; + [[nodiscard]] FullMsgId pollContextId() const { return key().pollContextId(); } diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index 1780d741a..b460edada 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include "lang/lang_keys.h" +#include "data/data_stories_ids.h" #include "storage/storage_shared_media.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" @@ -124,4 +126,29 @@ inline auto AddCommonGroupsButton( return result; }; +inline auto AddStoriesButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null user, + Ui::MultiSlideTracker &tracker) { + auto count = Data::SavedStoriesIds( + user, + ServerMaxStoryId - 1, + 0 + ) | rpl::map([](const Data::StoriesIdsSlice &slice) { + return slice.fullCount(); + }) | rpl::filter_optional(); + auto result = AddCountedButton( + parent, + std::move(count), + [](int count) { + return tr::lng_stories_row_count(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection(Info::Stories::Make(user)); + }); + return result; +}; + } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp index d956d4d76..970b63d02 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp @@ -338,6 +338,7 @@ void ListSection::resizeToWidth(int newWidth) { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: // #TODO stories case Type::RoundFile: { _itemsLeft = st::infoMediaSkip; _itemsTop = st::infoMediaSkip; @@ -375,6 +376,7 @@ int ListSection::recountHeight() { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: // #TODO stories case Type::RoundFile: { auto itemHeight = _itemWidth + st::infoMediaSkip; auto index = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 48e00a41b..9909cb23a 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_provider.h" #include "info/media/info_media_list_section.h" #include "info/downloads/info_downloads_provider.h" +#include "info/stories/info_stories_provider.h" #include "info/info_controller.h" #include "layout/layout_mosaic.h" #include "layout/layout_selection.h" @@ -88,6 +89,8 @@ struct ListWidget::DateBadge { not_null controller) { if (controller->isDownloads()) { return std::make_unique(controller); + } else if (controller->storiesPeer()) { + return std::make_unique(controller); } return std::make_unique(controller); } @@ -126,6 +129,7 @@ ListWidget::DateBadge::DateBadge( , hideTimer(std::move(hideCallback)) , goodType(type == Type::Photo || type == Type::Video + || type == Type::PhotoVideo || type == Type::GIF) { } @@ -171,6 +175,9 @@ void ListWidget::start() { ) | rpl::start_with_next([this](QString &&query) { _provider->setSearchQuery(std::move(query)); }, lifetime()); + } else if (_controller->storiesPeer()) { + trackSession(&session()); + restart(); } else { trackSession(&session()); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index e9149d336..701754c13 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -169,7 +169,6 @@ private: void itemRemoved(not_null item); void itemLayoutChanged(not_null item); - void refreshViewer(); void refreshRows(); void trackSession(not_null session); diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index a02453a0e..6a480a6d2 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -44,11 +44,15 @@ Memento::Memento(not_null controller) : Memento( (controller->peer() ? controller->peer() + : controller->storiesPeer() + ? controller->storiesPeer() : controller->parentController()->session().user()), controller->topic(), controller->migratedPeerId(), (controller->section().type() == Section::Type::Downloads ? Type::File + : controller->section().type() == Section::Type::Stories + ? Type::PhotoVideo : controller->section().mediaType())) { } diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 855a0244c..3b85f8989 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -186,7 +186,23 @@ object_ptr InnerWidget::setupSharedMedia( icon, st::infoSharedMediaButtonIconPosition); }; + auto addStoriesButton = [&]( + not_null user, + const style::icon &icon) { + auto result = Media::AddStoriesButton( + content, + _controller, + user, + tracker); + object_ptr( + result, + icon, + st::infoSharedMediaButtonIconPosition); + }; + if (auto user = _peer->asUser()) { + addStoriesButton(user, st::infoIconMediaGroup); + } addMediaButton(MediaType::Photo, st::infoIconMediaPhoto); addMediaButton(MediaType::Video, st::infoIconMediaVideo); addMediaButton(MediaType::File, st::infoIconMediaFile); diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp new file mode 100644 index 000000000..a7739a77b --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -0,0 +1,191 @@ +/* +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/stories/info_stories_inner_widget.h" + +#include "info/stories/info_stories_widget.h" +#include "info/media/info_media_list_widget.h" +#include "info/info_controller.h" +#include "ui/widgets/labels.h" +#include "styles/style_info.h" + +namespace Info::Stories { + +class EmptyWidget : public Ui::RpWidget { +public: + EmptyWidget(QWidget *parent); + + void setFullHeight(rpl::producer fullHeightValue); + +protected: + int resizeGetHeight(int newWidth) override; + + void paintEvent(QPaintEvent *e) override; + +private: + object_ptr _text; + int _height = 0; + +}; + +EmptyWidget::EmptyWidget(QWidget *parent) +: RpWidget(parent) +, _text(this, st::infoEmptyLabel) { +} + +void EmptyWidget::setFullHeight(rpl::producer fullHeightValue) { + std::move( + fullHeightValue + ) | rpl::start_with_next([this](int fullHeight) { + // Make icon center be on 1/3 height. + auto iconCenter = fullHeight / 3; + auto iconHeight = st::infoEmptyFile.height(); + auto iconTop = iconCenter - iconHeight / 2; + _height = iconTop + st::infoEmptyIconTop; + resizeToWidth(width()); + }, lifetime()); +} + +int EmptyWidget::resizeGetHeight(int newWidth) { + auto labelTop = _height - st::infoEmptyLabelTop; + auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip; + _text->resizeToNaturalWidth(labelWidth); + + auto labelLeft = (newWidth - _text->width()) / 2; + _text->moveToLeft(labelLeft, labelTop, newWidth); + + update(); + return _height; +} + +void EmptyWidget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto iconLeft = (width() - st::infoEmptyFile.width()) / 2; + const auto iconTop = height() - st::infoEmptyIconTop; + st::infoEmptyFile.paint(p, iconLeft, iconTop, width()); +} + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller) +: RpWidget(parent) +, _controller(controller) +, _empty(this) { + _empty->heightValue( + ) | rpl::start_with_next( + [this] { refreshHeight(); }, + _empty->lifetime()); + _list = setupList(); +} + +void InnerWidget::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +bool InnerWidget::showInternal(not_null memento) { + if (memento->section().type() == Section::Type::Stories) { + restoreState(memento); + return true; + } + return false; +} + +object_ptr InnerWidget::setupList() { + auto result = object_ptr( + this, + _controller); + result->heightValue( + ) | rpl::start_with_next( + [this] { refreshHeight(); }, + result->lifetime()); + using namespace rpl::mappers; + result->scrollToRequests( + ) | rpl::map([widget = result.data()](int to) { + return Ui::ScrollToRequest { + widget->y() + to, + -1 + }; + }) | rpl::start_to_stream( + _scrollToRequests, + result->lifetime()); + _selectedLists.fire(result->selectedListValue()); + _listTops.fire(result->topValue()); + return result; +} + +void InnerWidget::saveState(not_null memento) { + _list->saveState(&memento->media()); +} + +void InnerWidget::restoreState(not_null memento) { + _list->restoreState(&memento->media()); +} + +rpl::producer InnerWidget::selectedListValue() const { + return _selectedLists.events_starting_with( + _list->selectedListValue() + ) | rpl::flatten_latest(); +} + +void InnerWidget::selectionAction(SelectionAction action) { + _list->selectionAction(action); +} + +InnerWidget::~InnerWidget() = default; + +int InnerWidget::resizeGetHeight(int newWidth) { + _inResize = true; + auto guard = gsl::finally([this] { _inResize = false; }); + + _list->resizeToWidth(newWidth); + _empty->resizeToWidth(newWidth); + return recountHeight(); +} + +void InnerWidget::refreshHeight() { + if (_inResize) { + return; + } + resize(width(), recountHeight()); +} + +int InnerWidget::recountHeight() { + auto top = 0; + auto listHeight = 0; + if (_list) { + _list->moveToLeft(0, top); + listHeight = _list->heightNoMargins(); + top += listHeight; + } + if (listHeight > 0) { + _empty->hide(); + } else { + _empty->show(); + _empty->moveToLeft(0, top); + top += _empty->heightNoMargins(); + } + return top; +} + +void InnerWidget::setScrollHeightValue(rpl::producer value) { + using namespace rpl::mappers; + _empty->setFullHeight(rpl::combine( + std::move(value), + _listTops.events_starting_with( + _list->topValue() + ) | rpl::flatten_latest(), + _1 - _2)); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +} // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h new file mode 100644 index 000000000..f874b00f2 --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h @@ -0,0 +1,78 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/rp_widget.h" +#include "ui/widgets/scroll_area.h" +#include "base/unique_qptr.h" + +namespace Ui { +class SettingsSlider; +class VerticalLayout; +} // namespace Ui + +namespace Info { +class Controller; +struct SelectedItems; +enum class SelectionAction; +} // namespace Info + +namespace Info::Media { +class ListWidget; +} // namespace Info::Media + +namespace Info::Stories { + +class Memento; +class EmptyWidget; + +class InnerWidget final : public Ui::RpWidget { +public: + InnerWidget( + QWidget *parent, + not_null controller); + + bool showInternal(not_null memento); + + void saveState(not_null memento); + void restoreState(not_null memento); + + void setScrollHeightValue(rpl::producer value); + + rpl::producer scrollToRequests() const; + rpl::producer selectedListValue() const; + void selectionAction(SelectionAction action); + + ~InnerWidget(); + +protected: + int resizeGetHeight(int newWidth) override; + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + int recountHeight(); + void refreshHeight(); + + object_ptr setupList(); + + const not_null _controller; + + object_ptr _list = { nullptr }; + object_ptr _empty; + + bool _inResize = false; + + rpl::event_stream _scrollToRequests; + rpl::event_stream> _selectedLists; + rpl::event_stream> _listTops; + +}; + +} // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp new file mode 100644 index 000000000..bd5d88f93 --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -0,0 +1,407 @@ +/* +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/stories/info_stories_provider.h" + +#include "info/media/info_media_widget.h" +#include "info/media/info_media_list_section.h" +#include "info/info_controller.h" +#include "data/data_document.h" +#include "data/data_media_types.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_stories_ids.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "history/history.h" +#include "core/application.h" +#include "storage/storage_shared_media.h" +#include "layout/layout_selection.h" +#include "styles/style_info.h" + +namespace Info::Stories { +namespace { + +using namespace Media; + +constexpr auto kPreloadedScreensCount = 4; +constexpr auto kPreloadedScreensCountFull + = kPreloadedScreensCount + 1 + kPreloadedScreensCount; + +[[nodiscard]] int MinStoryHeight(int width) { + auto itemsLeft = st::infoMediaSkip; + auto itemsInRow = (width - itemsLeft) + / (st::infoMediaMinGridSize + st::infoMediaSkip); + return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow; +} + +} // namespace + +Provider::Provider(not_null controller) +: _controller(controller) +, _peer(controller->key().storiesPeer()) +, _history(_peer->owner().history(_peer)) +, _tab(controller->key().storiesTab()) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + for (auto &layout : _layouts) { + layout.second.item->invalidateCache(); + } + }, _lifetime); +} + +Type Provider::type() { + return Type::PhotoVideo; +} + +bool Provider::hasSelectRestriction() { + return false; +} + +rpl::producer Provider::hasSelectRestrictionChanges() { + return rpl::never(); +} + +bool Provider::sectionHasFloatingHeader() { + return false; +} + +QString Provider::sectionTitle(not_null item) { + return QString(); +} + +bool Provider::sectionItemBelongsHere( + not_null item, + not_null previous) { + return true; +} + +bool Provider::isPossiblyMyItem(not_null item) { + return true; +} + +std::optional Provider::fullCount() { + return _slice.fullCount(); +} + +void Provider::restart() { + _layouts.clear(); + _aroundId = kDefaultAroundId; + _idsLimit = kMinimalIdsLimit; + _slice = Data::StoriesIdsSlice(); + refreshViewer(); +} + +void Provider::checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) { + const auto visibleWidth = viewport.width(); + const auto visibleHeight = viewport.height(); + const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight; + const auto minItemHeight = MinStoryHeight(visibleWidth); + const auto preloadedCount = preloadedHeight / minItemHeight; + const auto preloadIdsLimitMin = (preloadedCount / 2) + 1; + const auto preloadIdsLimit = preloadIdsLimitMin + + (visibleHeight / minItemHeight); + const auto after = _slice.skippedAfter(); + const auto topLoaded = after && (*after == 0); + const auto before = _slice.skippedBefore(); + const auto bottomLoaded = before && (*before == 0); + + const auto minScreenDelta = kPreloadedScreensCount + - kPreloadIfLessThanScreens; + const auto minIdDelta = (minScreenDelta * visibleHeight) + / minItemHeight; + const auto preloadAroundItem = [&](not_null layout) { + auto preloadRequired = false; + const auto id = StoryIdFromMsgId(layout->getItem()->id); + if (!preloadRequired) { + preloadRequired = (_idsLimit < preloadIdsLimitMin); + } + if (!preloadRequired) { + auto delta = _slice.distance(_aroundId, id); + Assert(delta != std::nullopt); + preloadRequired = (qAbs(*delta) >= minIdDelta); + } + if (preloadRequired) { + _idsLimit = preloadIdsLimit; + _aroundId = id; + refreshViewer(); + } + }; + + if (preloadTop && !topLoaded) { + preloadAroundItem(topLayout); + } else if (preloadBottom && !bottomLoaded) { + preloadAroundItem(bottomLayout); + } +} + +void Provider::setSearchQuery(QString query) { +} + +void Provider::refreshViewer() { + _viewerLifetime.destroy(); + const auto idForViewer = _aroundId; + const auto session = &_peer->session(); + auto ids = (_tab == Tab::Saved) + ? Data::SavedStoriesIds(_peer, idForViewer, _idsLimit) + : Data::ArchiveStoriesIds(session, idForViewer, _idsLimit); + std::move( + ids + ) | rpl::start_with_next([=](Data::StoriesIdsSlice &&slice) { + if (!slice.fullCount()) { + // Don't display anything while full count is unknown. + return; + } + _slice = std::move(slice); + if (const auto nearest = _slice.nearest(idForViewer)) { + _aroundId = *nearest; + } + _refreshed.fire({}); + }, _viewerLifetime); +} + +rpl::producer<> Provider::refreshed() { + return _refreshed.events(); +} + +std::vector Provider::fillSections( + not_null delegate) { + markLayoutsStale(); + const auto guard = gsl::finally([&] { clearStaleLayouts(); }); + + auto result = std::vector(); + auto section = ListSection(Type::PhotoVideo, sectionDelegate()); + auto count = _slice.size(); + for (auto i = count; i != 0;) { + const auto storyId = _slice[--i]; + if (const auto layout = getLayout(storyId, delegate)) { + if (!section.addItem(layout)) { + section.finishSection(); + result.push_back(std::move(section)); + section = ListSection(Type::PhotoVideo, sectionDelegate()); + section.addItem(layout); + } + } + } + if (!section.empty()) { + section.finishSection(); + result.push_back(std::move(section)); + } + return result; +} + +void Provider::markLayoutsStale() { + for (auto &layout : _layouts) { + layout.second.stale = true; + } +} + +void Provider::clearStaleLayouts() { + for (auto i = _layouts.begin(); i != _layouts.end();) { + if (i->second.stale) { + _layoutRemoved.fire(i->second.item.get()); + const auto taken = _items.take(i->first); + i = _layouts.erase(i); + } else { + ++i; + } + } +} + +rpl::producer> Provider::layoutRemoved() { + return _layoutRemoved.events(); +} + +BaseLayout *Provider::lookupLayout(const HistoryItem *item) { + return nullptr; +} + +bool Provider::isMyItem(not_null item) { + return IsStoryMsgId(item->id) && (item->history()->peer == _peer); +} + +bool Provider::isAfter( + not_null a, + not_null b) { + return (a->id < b->id); +} + +void Provider::itemRemoved(not_null item) { + const auto id = StoryIdFromMsgId(item->id); + if (const auto i = _layouts.find(id); i != end(_layouts)) { + _layoutRemoved.fire(i->second.item.get()); + _layouts.erase(i); + } +} + +BaseLayout *Provider::getLayout( + StoryId id, + not_null delegate) { + auto it = _layouts.find(id); + if (it == _layouts.end()) { + if (auto layout = createLayout(id, delegate)) { + layout->initDimensions(); + it = _layouts.emplace(id, std::move(layout)).first; + } else { + return nullptr; + } + } + it->second.stale = false; + return it->second.item.get(); +} + +HistoryItem *Provider::ensureItem(StoryId id) { + const auto i = _items.find(id); + if (i != end(_items)) { + return i->second.get(); + } + auto item = _peer->owner().stories().resolveItem({ _peer->id, id }); + if (!item) { + return nullptr; + } + return _items.emplace(id, std::move(item)).first->second.get(); +} + +std::unique_ptr Provider::createLayout( + StoryId id, + not_null delegate) { + const auto item = ensureItem(id); + if (!item) { + return nullptr; + } + const auto getPhoto = [&]() -> PhotoData* { + if (const auto media = item->media()) { + return media->photo(); + } + return nullptr; + }; + const auto getFile = [&]() -> DocumentData* { + if (const auto media = item->media()) { + return media->document(); + } + return nullptr; + }; + // #TODO stories + const auto maybeStory = item->history()->owner().stories().lookup( + { item->history()->peer->id, StoryIdFromMsgId(item->id) }); + const auto spoiler = maybeStory && !(*maybeStory)->expired(); + + using namespace Overview::Layout; + if (const auto photo = getPhoto()) { + return std::make_unique(delegate, item, photo, spoiler); + } else if (const auto file = getFile()) { + return std::make_unique