From 2534fab7e7ddc67588792c55a426c78135af6bfd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= <dnovomesky@gmail.com>
Date: Mon, 17 Jul 2023 17:22:19 +0200
Subject: [PATCH 001/259] 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?= <dnovomesky@gmail.com>
Date: Mon, 17 Jul 2023 17:29:07 +0200
Subject: [PATCH 002/259] 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 <fedin-ilja2010@ya.ru>
Date: Thu, 1 Jun 2023 07:20:11 +0400
Subject: [PATCH 003/259] 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<Glib::ustring, Glib::VariantBase> 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<Glib::ustring>::create("im.received");
 
 	_hints["desktop-entry"] = Glib::Variant<Glib::ustring>::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<void(bool)> 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 <fedin-ilja2010@ya.ru>
Date: Wed, 19 Jul 2023 22:58:22 +0400
Subject: [PATCH 004/259] 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 <johnprestonmail@gmail.com>
Date: Fri, 28 Apr 2023 17:43:57 +0400
Subject: [PATCH 005/259] 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<int> = 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<bytes> qts:int = Update;
+updateMessagePollVote#24f40e77 poll_id:long peer:Peer options:Vector<bytes> qts:int = Update;
 updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update;
 updateDialogFilterOrder#a5d72105 order:Vector<int> = 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<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<long> solution:flags.4?string solution_entities:flags.4?Vector<MessageEntity> = PollResults;
+pollResults#7adf2420 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<Peer> solution:flags.4?string solution_entities:flags.4?Vector<MessageEntity> = 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<Document> 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<bytes> date:int = MessageUserVote;
-
-messages.votesList#823f649 flags:# count:int votes:Vector<MessageUserVote> users:Vector<User> next_offset:flags.0?string = messages.VotesList;
+messages.votesList#4899484e flags:# count:int votes:Vector<MessagePeerVote> chats:Vector<Chat> users:Vector<User> 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<MessageEntity> 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<MessageEntity> sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage;
 
 messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = 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<AvailableReaction> = 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<Peer> chats:Vector<Chat>
 
 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<bytes> 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<string> = Bool;
 
 users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
 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<long> = 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<InputDialogPeer> = 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<Peer>;
 chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = 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<UserPrivacy::Key> 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<UserData*>(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<PollAnswer> answers;
-	std::vector<not_null<UserData*>> recentVoters;
+	std::vector<not_null<PeerData*>> recentVoters;
 	std::vector<QByteArray> 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<UserData*> user;
+	not_null<PeerData*> 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<UserData*> user) {
-			return RecentVoter{ user };
+		) | ranges::views::transform([](not_null<PeerData*> 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<not_null<UserData*>> preloaded;
+		std::vector<not_null<PeerData*>> preloaded;
 		bool wasLoading = false;
 	};
 
-	bool appendRow(not_null<UserData*> user);
-	std::unique_ptr<PeerListRow> createRow(not_null<UserData*> user) const;
+	bool appendRow(not_null<PeerData*> peer);
+	std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer) const;
 	void addPreloaded();
 	bool addPreloadedPage();
 	void preloadedAdded();
@@ -163,7 +163,7 @@ private:
 	QString _offset;
 	mtpRequestId _loadRequestId = 0;
 	QString _loadForOffset;
-	std::vector<not_null<UserData*>> _preloaded;
+	std::vector<not_null<PeerData*>> _preloaded;
 	rpl::variable<int> _count = 0;
 	rpl::variable<int> _fullCount;
 	rpl::variable<int> _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<PeerListRow*> row) {
 	_showPeerInfoRequests.fire(row->peer());
 }
 
-bool ListController::appendRow(not_null<UserData*> user) {
-	if (delegate()->peerListFindRow(user->id.value)) {
+bool ListController::appendRow(not_null<PeerData*> peer) {
+	if (delegate()->peerListFindRow(peer->id.value)) {
 		return false;
 	}
-	delegate()->peerListAppendRow(createRow(user));
+	delegate()->peerListAppendRow(createRow(peer));
 	return true;
 }
 
 std::unique_ptr<PeerListRow> ListController::createRow(
-		not_null<UserData*> user) const {
-	auto row = std::make_unique<PeerListRow>(user);
+		not_null<PeerData*> peer) const {
+	auto row = std::make_unique<PeerListRow>(peer);
 	row->setCustomStatus(QString());
 	return row;
 }

From 918af601cf92c3931f548c32662f03c210827a1d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 28 Apr 2023 20:16:22 +0400
Subject: [PATCH 006/259] 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<int>("reactions_user_max_default", 1);
 }
 
+bool IsMyRecent(
+		const MTPDmessagePeerReaction &data,
+		const ReactionId &id,
+		not_null<PeerData*> peer,
+		const base::flat_map<
+			ReactionId,
+			std::vector<RecentReaction>> &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<MTPReactionCount> &list,
-		const QVector<MTPMessagePeerReaction> &recent) const {
+		const QVector<MTPMessagePeerReaction> &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<MTPReactionCount> &list,
 		const QVector<MTPMessagePeerReaction> &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<int>::min();
+	if (!min && !order.empty()) {
+		const auto minimal = std::numeric_limits<int>::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<ReactionId, std::vector<RecentReaction>>();
 	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<PeerData*> 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<MTPReactionCount> &list,
-		const QVector<MTPMessagePeerReaction> &recent) const;
+		const QVector<MTPMessagePeerReaction> &recent,
+		bool ignoreChosen) const;
 	[[nodiscard]] const std::vector<MessageReaction> &list() const;
 	[[nodiscard]] auto recent() const
 		-> const base::flat_map<ReactionId, std::vector<RecentReaction>> &;
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 <johnprestonmail@gmail.com>
Date: Mon, 1 May 2023 10:44:57 +0400
Subject: [PATCH 007/259] 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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> = 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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> = 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<PremiumGiftOption> 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<PremiumGiftOption> 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<InputUser> = InputPrivacyRule;
 inputPrivacyValueAllowChatParticipants#840649cf chats:Vector<long> = InputPrivacyRule;
 inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector<long> = InputPrivacyRule;
+inputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule;
 
 privacyValueAllowContacts#fffe1bac = PrivacyRule;
 privacyValueAllowAll#65427b82 = PrivacyRule;
@@ -544,6 +547,7 @@ privacyValueDisallowAll#8b73e763 = PrivacyRule;
 privacyValueDisallowUsers#e4621141 users:Vector<long> = PrivacyRule;
 privacyValueAllowChatParticipants#6b134e8e chats:Vector<long> = PrivacyRule;
 privacyValueDisallowChatParticipants#41c87565 chats:Vector<long> = PrivacyRule;
+privacyValueAllowCloseFriends#f7e8d89b = PrivacyRule;
 
 account.privacyRules#50a04e45 rules:Vector<PrivacyRule> chats:Vector<Chat> users:Vector<User> = 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<bytes> date:int = Mess
 
 sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage;
 
+storyViews#518b47d8 recent_viewers:Vector<long> 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
+
+userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = UserStories;
+
+stories.allStoriesNotModified#47e0a07e state:string = stories.AllStories;
+stories.allStories#5b1aa68c flags:# has_more:flags.0?true state:string user_stories:Vector<UserStories> users:Vector<User> = stories.AllStories;
+
+stories.stories#4fe57df1 count:int stories:Vector<StoryItem> users:Vector<User> = stories.Stories;
+
+storyView#a71aacc2 user_id:long date:int = StoryView;
+
+stories.storyViewsList#fb3f77ac count:int views:Vector<StoryView> users:Vector<User> = stories.StoryViewsList;
+
+stories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = 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<long> = Bool;
 
 messages.getMessages#63c66506 id:Vector<InputMessage> = 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<Peer>;
 chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = Updates;
 
-// LAYER 159
+stories.sendStory#8e826f5d flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> = Updates;
+stories.deleteStories#b5d501d7 id:Vector<int> = Vector<int>;
+stories.editStoryPrivacy#78981875 id:int privacy_rules:Vector<InputPrivacyRule> = Updates;
+stories.togglePinned#51602944 id:Vector<int> pinned:Bool = Vector<int>;
+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<int> = stories.Stories;
+stories.getExpiredStories#8f792f2 offset_id:int limit:int = stories.Stories;
+stories.readStories#edc5105b user_id:InputUser max_id:int = Vector<int>;
+stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList;
+stories.getStoriesViews#9a75d6a6 id:Vector<int> = 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<MTPDocumentAttribute> 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<QString> 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 <johnprestonmail@gmail.com>
Date: Wed, 3 May 2023 22:30:37 +0400
Subject: [PATCH 008/259] 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<PhotoData*> 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<Main::Session*> session)
 , _emojiStatuses(std::make_unique<EmojiStatuses>(this))
 , _forumIcons(std::make_unique<ForumIcons>(this))
 , _notifySettings(std::make_unique<NotifySettings>(this))
-, _customEmojiManager(std::make_unique<CustomEmojiManager>(this)) {
+, _customEmojiManager(std::make_unique<CustomEmojiManager>(this))
+, _stories(std::make_unique<Stories>(this)) {
 	_cache->open(_session->local().cacheKey());
 	_bigFileCache->open(_session->local().cacheBigFileKey());
 
@@ -311,6 +313,8 @@ Session::Session(not_null<Main::Session*> 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> _forumIcons;
 	const std::unique_ptr<NotifySettings> _notifySettings;
 	const std::unique_ptr<CustomEmojiManager> _customEmojiManager;
+	const std::unique_ptr<Stories> _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<Session*> 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<StoryItem> Stories::parse(const MTPDstoryItem &data) {
+	const auto id = data.vid().v;
+	using MaybeMedia = std::optional<
+		std::variant<not_null<PhotoData*>, not_null<DocumentData*>>>;
+	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<StoriesList> &Stories::all() {
+	return _all;
+}
+
+bool Stories::allLoaded() const {
+	return _allLoaded;
+}
+
+// #TODO stories testing
+StoryId Stories::generate(
+	not_null<HistoryItem*> item,
+	std::variant<
+		v::null_t,
+		not_null<PhotoData*>,
+		not_null<DocumentData*>> media) {
+	if (v::is_null(media)
+		|| !item->from()->isUser()
+		|| !item->isRegular()) {
+		return {};
+	}
+	const auto document = v::is<not_null<DocumentData*>>(media)
+		? v::get<not_null<DocumentData*>>(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<not_null<PhotoData*>>(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<PhotoData*>, not_null<DocumentData*>> data;
+};
+
+struct StoryItem {
+	StoryId id = 0;
+	StoryMedia media;
+	TextWithEntities caption;
+	TimeId date = 0;
+	StoryPrivacy privacy;
+};
+
+struct StoriesList {
+	not_null<UserData*> user;
+	std::vector<StoryItem> items;
+	int total = 0;
+};
+
+class Stories final {
+public:
+	explicit Stories(not_null<Session*> owner);
+	~Stories();
+
+	void loadMore();
+	void apply(const MTPDupdateStories &data);
+
+	[[nodiscard]] const std::vector<StoriesList> &all();
+	[[nodiscard]] bool allLoaded() const;
+
+	// #TODO stories testing
+	[[nodiscard]] StoryId generate(
+		not_null<HistoryItem*> item,
+		std::variant<
+			v::null_t,
+			not_null<PhotoData*>,
+			not_null<DocumentData*>> media);
+
+private:
+	[[nodiscard]] StoriesList parse(const MTPUserStories &data);
+	[[nodiscard]] std::optional<StoryItem> parse(const MTPDstoryItem &data);
+
+	void pushToBack(StoriesList &&list);
+	void pushToFront(StoriesList &&list);
+
+	const not_null<Session*> _owner;
+
+	std::vector<StoriesList> _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<TabbedSelector>::fromRaw(_ownedSelector.release())
+			: object_ptr<TabbedSelector>(nullptr)),
+		.nonOwnedSelector = _ownedSelector ? nullptr : _selector.get(),
 	};
-	setTabbedPanel(std::make_unique<ChatHelpers::TabbedPanel>(
+	setTabbedPanel(std::make_unique<TabbedPanel>(
 		_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<Main::Session*> _session;
 
 	Window::SessionController * const _regularWindow = nullptr;
-	const std::unique_ptr<ChatHelpers::TabbedSelector> _ownedSelector;
+	std::unique_ptr<ChatHelpers::TabbedSelector> _ownedSelector;
 	const not_null<ChatHelpers::TabbedSelector*> _selector;
 	rpl::event_stream<ChatHelpers::FileChosen> _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<Ui::RpWidget*> storiesWrap() = 0;
+	[[nodiscard]] virtual auto storiesShow()
+		-> std::shared_ptr<ChatHelpers::Show> = 0;
+	[[nodiscard]] virtual auto storiesStickerOrEmojiChosen()
+		-> rpl::producer<ChatHelpers::FileChosen> = 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(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<Ui::RpWidget>(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<Ui::UserpicButton>(
+			raw,
+			data.user,
+			st::defaultUserpicButton);
+		userpic->move(0, 0);
+		const auto name = Ui::CreateChild<Ui::FlatLabel>(
+			raw,
+			data.user->firstName,
+			st::defaultFlatLabel);
+		name->move(100, 0);
+		raw->show();
+		_widget = std::move(widget);
+	}
+	_date = std::make_unique<Ui::FlatLabel>(
+		_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<UserData*> user;
+	TimeId date = 0;
+
+	friend inline auto operator<=>(HeaderData, HeaderData) = default;
+};
+
+class Header final {
+public:
+	explicit Header(not_null<Delegate*> delegate);
+	~Header();
+
+	void show(HeaderData data);
+
+private:
+	const not_null<Delegate*> _delegate;
+	std::unique_ptr<Ui::RpWidget> _widget;
+	std::unique_ptr<Ui::FlatLabel> _date;
+	std::optional<HeaderData> _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(delegate)
+, _controls(std::make_unique<HistoryView::ComposeControls>(
+	_delegate->storiesWrap(),
+	HistoryView::ComposeControlsDescriptor{
+		.show = _delegate->storiesShow(),
+		.unavailableEmojiPasted = [=](not_null<DocumentData*> 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<DocumentData*> 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<UserData*> user;
+
+	friend inline auto operator<=>(ReplyAreaData, ReplyAreaData) = default;
+};
+
+class ReplyArea final {
+public:
+	explicit ReplyArea(not_null<Delegate*> delegate);
+	~ReplyArea();
+
+private:
+	void showPremiumToast(not_null<DocumentData*> emoji);
+
+	const not_null<Delegate*> _delegate;
+	const std::unique_ptr<HistoryView::ComposeControls> _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(delegate)
+, _wrap(_delegate->storiesWrap())
+, _header(std::make_unique<Header>(_delegate))
+, _slider(std::make_unique<Slider>())
+, _replyArea(std::make_unique<ReplyArea>(_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*> delegate);
+	~View();
+
+	void show(const Data::StoriesList &list, int index);
+
+private:
+	const not_null<Delegate*> _delegate;
+	const not_null<Ui::RpWidget*> _wrap;
+
+	std::unique_ptr<Header> _header;
+	std::unique_ptr<Slider> _slider;
+	std::unique_ptr<ReplyArea> _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<Ui::LayerManager>(_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<Ui::RpWidget*> OverlayWidget::storiesWrap() {
+	return _body;
+}
+
+std::shared_ptr<ChatHelpers::Show> OverlayWidget::storiesShow() {
+	class Show final : public ChatHelpers::Show {
+	public:
+		explicit Show(not_null<OverlayWidget*> widget) : _widget(widget) {
+		}
+
+		void showBox(
+			object_ptr<Ui::BoxContent> 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<QWidget*> 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<bool> adjustShadowLeft() const override {
+			return rpl::single(false);
+		}
+		SendMenu::Type sendMenuType() const override {
+			return SendMenu::Type::SilentOnly;
+		}
+
+		bool showMediaPreview(
+				Data::FileOrigin origin,
+				not_null<DocumentData*> document) const override {
+			return false; // #TODO stories
+		}
+		bool showMediaPreview(
+				Data::FileOrigin origin,
+				not_null<PhotoData*> photo) const override {
+			return false; // #TODO stories
+		}
+
+		void processChosenSticker(
+				ChatHelpers::FileChosen &&chosen) const override {
+			_widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen));
+		}
+
+	private:
+		not_null<OverlayWidget*> _widget;
+
+	};
+	return std::make_shared<Show>(this);
+}
+
+auto OverlayWidget::storiesStickerOrEmojiChosen()
+-> rpl::producer<ChatHelpers::FileChosen> {
+	return _storiesStickerOrEmojiChosen.events();
+}
+
 void OverlayWidget::playbackToggleFullScreen() {
 	Expects(_streamed != nullptr);
 
@@ -4619,22 +4724,49 @@ void OverlayWidget::setContext(
 	std::variant<
 		v::null_t,
 		ItemContext,
-		not_null<PeerData*>> context) {
+		not_null<PeerData*>,
+		StoriesContext> context) {
 	if (const auto item = std::get_if<ItemContext>(&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<not_null<PeerData*>>(&context)) {
 		_peer = *peer;
 		_history = _peer->owner().history(_peer);
 		_message = nullptr;
 		_topicRootId = MsgId();
+		_stories = nullptr;
+		_storiesUser = nullptr;
+	} else if (const auto story = std::get_if<StoriesContext>(&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<Stories::View>(
+				static_cast<Stories::Delegate*>(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<Ui::RpWidget*> storiesWrap() override;
+	std::shared_ptr<ChatHelpers::Show> storiesShow() override;
+	auto storiesStickerOrEmojiChosen()
+		-> rpl::producer<ChatHelpers::FileChosen> override;
+
 	void hideControls(bool force = false);
 	void subscribeToScreenGeometry();
 
@@ -268,10 +279,15 @@ private:
 		not_null<HistoryItem*> item;
 		MsgId topicRootId = 0;
 	};
+	struct StoriesContext {
+		not_null<UserData*> user;
+		StoryId id = 0;
+	};
 	void setContext(std::variant<
 		v::null_t,
 		ItemContext,
-		not_null<PeerData*>> context);
+		not_null<PeerData*>,
+		StoriesContext> context);
 
 	void refreshLang();
 	void showSaveMsgFile();
@@ -551,6 +567,11 @@ private:
 	int _streamedCreated = 0;
 	bool _showAsPip = false;
 
+	std::unique_ptr<Stories::View> _stories;
+	UserData *_storiesUser = nullptr;
+	rpl::event_stream<ChatHelpers::FileChosen> _storiesStickerOrEmojiChosen;
+	std::unique_ptr<Ui::LayerManager> _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<PhotoData*> photo) const override;
 
 	void processChosenSticker(
-		ChatHelpers::FileChosen chosen) const override;
+		ChatHelpers::FileChosen &&chosen) const override;
 
 private:
 	const base::weak_ptr<SessionController> _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 <johnprestonmail@gmail.com>
Date: Fri, 5 May 2023 14:40:51 +0400
Subject: [PATCH 009/259] 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<QString> title,
 		rpl::producer<QString> description,
 		rpl::producer<WebPageData*> 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<FullMsgId> _editMsgId;
 	rpl::variable<FullMsgId> _replyToId;
@@ -677,17 +679,18 @@ void FieldHeader::resolveMessageData() {
 }
 
 void FieldHeader::previewRequested(
-	rpl::producer<QString> title,
-	rpl::producer<QString> description,
-	rpl::producer<WebPageData*> page) {
+		rpl::producer<QString> title,
+		rpl::producer<QString> description,
+		rpl::producer<WebPageData*> 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<PeerData*> 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<PeerData*> peer) {
 			updateControlsGeometry(_wrap->size());
 			orderControls();
 		}
-	}, _wrap->lifetime());
+	}, _historyLifetime);
 
 	updateSendAsButton();
 }
@@ -2218,7 +2224,7 @@ void ComposeControls::initVoiceRecordBar() {
 	_voiceRecordBar->setStartRecordingFilter([=] {
 		const auto error = [&]() -> std::optional<QString> {
 			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<WebpageProcessor>(_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<WebpageProcessor> _preview;
 
-	rpl::lifetime _uploaderSubscriptions;
-
 	Fn<void()> _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(delegate)
+, _wrap(_delegate->storiesWrap())
+, _header(std::make_unique<Header>(this))
+, _slider(std::make_unique<Slider>(this))
+, _replyArea(std::make_unique<ReplyArea>(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<Ui::RpWidget*> Controller::wrap() const {
+	return _wrap;
+}
+
+Layout Controller::layout() const {
+	Expects(_layout.current().has_value());
+
+	return *_layout.current();
+}
+
+rpl::producer<Layout> Controller::layoutValue() const {
+	return _layout.value() | rpl::filter_optional();
+}
+
+std::shared_ptr<ChatHelpers::Show> Controller::uiShow() const {
+	return _delegate->storiesShow();
+}
+
+auto Controller::stickerOrEmojiChosen() const
+->rpl::producer<ChatHelpers::FileChosen> {
+	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*> delegate);
+
+	[[nodiscard]] not_null<Ui::RpWidget*> wrap() const;
+	[[nodiscard]] Layout layout() const;
+	[[nodiscard]] rpl::producer<Layout> layoutValue() const;
+
+	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
+	[[nodiscard]] auto stickerOrEmojiChosen() const
+	-> rpl::producer<ChatHelpers::FileChosen>;
+
+	void show(const Data::StoriesList &list, int index);
+
+private:
+	void initLayout();
+
+	const not_null<Delegate*> _delegate;
+
+	rpl::variable<std::optional<Layout>> _layout;
+
+	const not_null<Ui::RpWidget*> _wrap;
+	const std::unique_ptr<Header> _header;
+	const std::unique_ptr<Slider> _slider;
+	const std::unique_ptr<ReplyArea> _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(delegate) {
+Header::Header(not_null<Controller*> 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<Ui::RpWidget>(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<Ui::UserpicButton>(
 			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<Ui::FlatLabel>(
 			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<Ui::FlatLabel>(
 		_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<UserData*> 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*> delegate);
+	explicit Header(not_null<Controller*> controller);
 	~Header();
 
 	void show(HeaderData data);
 
 private:
-	const not_null<Delegate*> _delegate;
+	const not_null<Controller*> _controller;
+
 	std::unique_ptr<Ui::RpWidget> _widget;
 	std::unique_ptr<Ui::FlatLabel> _date;
 	std::optional<HeaderData> _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(delegate)
+ReplyArea::ReplyArea(not_null<Controller*> controller)
+: _controller(controller)
 , _controls(std::make_unique<HistoryView::ComposeControls>(
-	_delegate->storiesWrap(),
+	_controller->wrap(),
 	HistoryView::ComposeControlsDescriptor{
-		.show = _delegate->storiesShow(),
+		.show = _controller->uiShow(),
 		.unavailableEmojiPasted = [=](not_null<DocumentData*> 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<bool> 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<bool> 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<const QMimeData*> 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<DocumentData*> 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<UserData*> 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*> delegate);
+	explicit ReplyArea(not_null<Controller*> 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<bool> overrideSendImagesAsPhotos);
+
 	void showPremiumToast(not_null<DocumentData*> emoji);
 
-	const not_null<Delegate*> _delegate;
+	const not_null<Controller*> _controller;
 	const std::unique_ptr<HistoryView::ComposeControls> _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(controller) {
 }
 
 Slider::~Slider() {
 }
 
+void Slider::show(SliderData data) {
+	if (_data == data) {
+		return;
+	}
+	_data = data;
+
+	const auto parent = _controller->wrap();
+	auto widget = std::make_unique<Ui::RpWidget>(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*> controller);
 	~Slider();
 
+	void show(SliderData data);
+
+private:
+	const not_null<Controller*> _controller;
+
+	std::unique_ptr<Ui::RpWidget> _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(delegate)
-, _wrap(_delegate->storiesWrap())
-, _header(std::make_unique<Header>(_delegate))
-, _slider(std::make_unique<Slider>())
-, _replyArea(std::make_unique<ReplyArea>(_delegate)) {
+: _controller(std::make_unique<Controller>(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*> _delegate;
-	const not_null<Ui::RpWidget*> _wrap;
-
-	std::unique_ptr<Header> _header;
-	std::unique_ptr<Slider> _slider;
-	std::unique_ptr<ReplyArea> _replyArea;
+	const std::unique_ptr<Controller> _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 <johnprestonmail@gmail.com>
Date: Sun, 7 May 2023 00:20:21 +0400
Subject: [PATCH 010/259] 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<PhotoData*>, not_null<DocumentData*>> 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<UserData*> user;
 	std::vector<StoryItem> 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*> controller);
+
+	[[nodiscard]] bool paused() const;
+	void togglePaused(bool paused);
+
+private:
+	void callback();
+
+	const not_null<Controller*> _controller;
+
+	base::Timer _timer;
+	crl::time _started = 0;
+	crl::time _paused = 0;
+
+};
+
+Controller::PhotoPlayback::PhotoPlayback(not_null<Controller*> 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(delegate)
@@ -28,12 +106,14 @@ Controller::Controller(not_null<Delegate*> 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<not_null<PhotoData*>>(item.media.data)) {
+			_photoPlayback = std::make_unique<PhotoPlayback>(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*> delegate);
+	~Controller();
 
 	[[nodiscard]] not_null<Ui::RpWidget*> wrap() const;
 	[[nodiscard]] Layout layout() const;
@@ -69,9 +69,22 @@ public:
 	-> rpl::producer<ChatHelpers::FileChosen>;
 
 	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*> _delegate;
 
@@ -82,7 +95,12 @@ private:
 	const std::unique_ptr<Slider> _slider;
 	const std::unique_ptr<ReplyArea> _replyArea;
 
-	ShownId _shown;
+	Data::FullStoryId _shown;
+	std::optional<Data::StoriesList> _list;
+	int _index = 0;
+	std::unique_ptr<PhotoPlayback> _photoPlayback;
+
+	std::unique_ptr<base::PowerSaveBlocker> _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<Ui::RpWidget*> storiesWrap() = 0;
@@ -25,6 +34,9 @@ public:
 		-> std::shared_ptr<ChatHelpers::Show> = 0;
 	[[nodiscard]] virtual auto storiesStickerOrEmojiChosen()
 		-> rpl::producer<ChatHelpers::FileChosen> = 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(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(controller)
+, _progress(std::make_unique<View::PlaybackProgress>()) {
 }
 
 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<Ui::RpWidget>(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*> _controller;
+	const std::unique_ptr<Media::View::PlaybackProgress> _progress;
 
 	std::unique_ptr<Ui::RpWidget> _widget;
+	std::vector<QRectF> _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> _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<DocumentData*> document,
 		Data::FileOrigin origin,
-		not_null<QWidget*> controlsParent,
-		not_null<PlaybackControls::Delegate*> controlsDelegate,
 		Fn<void()> waitingCallback);
 	Streamed(
 		not_null<PhotoData*> photo,
 		Data::FileOrigin origin,
-		not_null<QWidget*> controlsParent,
-		not_null<PlaybackControls::Delegate*> controlsDelegate,
 		Fn<void()> waitingCallback);
 
 	Streaming::Instance instance;
-	PlaybackControls controls;
+	std::unique_ptr<PlaybackControls> controls;
 	std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
 
 	bool withSound = false;
@@ -289,21 +285,15 @@ struct OverlayWidget::PipWrap {
 OverlayWidget::Streamed::Streamed(
 	not_null<DocumentData*> document,
 	Data::FileOrigin origin,
-	not_null<QWidget*> controlsParent,
-	not_null<PlaybackControls::Delegate*> controlsDelegate,
 	Fn<void()> waitingCallback)
-: instance(document, origin, std::move(waitingCallback))
-, controls(controlsParent, controlsDelegate) {
+: instance(document, origin, std::move(waitingCallback)) {
 }
 
 OverlayWidget::Streamed::Streamed(
 	not_null<PhotoData*> photo,
 	Data::FileOrigin origin,
-	not_null<QWidget*> controlsParent,
-	not_null<PlaybackControls::Delegate*> controlsDelegate,
 	Fn<void()> 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<DocumentData*> 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<PhotoData*> 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<Streamed>(
-			_document,
-			fileOrigin(),
-			_body,
-			static_cast<PlaybackControls::Delegate*>(this),
-			[=] { waitingAnimationCallback(); });
+		_streamed = std::make_unique<Streamed>(_document, origin, callback);
 	} else {
-		_streamed = std::make_unique<Streamed>(
-			_photo,
-			fileOrigin(),
-			_body,
-			static_cast<PlaybackControls::Delegate*>(this),
-			[=] { waitingAnimationCallback(); });
+		_streamed = std::make_unique<Streamed>(_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<PlaybackControls>(
+			_body,
+			static_cast<PlaybackControls::Delegate*>(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<not_null<PhotoData*>>(&data)) {
+		displayPhoto(*photo);
+	} else {
+		displayDocument(v::get<not_null<DocumentData*>>(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) {
 			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*> 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<QKeyEvent*> 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<Main::Session*> 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<ChatHelpers::Show> storiesShow() override;
 	auto storiesStickerOrEmojiChosen()
 		-> rpl::producer<ChatHelpers::FileChosen> 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 <johnprestonmail@gmail.com>
Date: Tue, 9 May 2023 14:31:48 +0400
Subject: [PATCH 011/259] 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+<lXk`<Ok&fj3U#9`bZYuMs#h!g&7AefB5zij{now%Kl}v#
zd+gZlImsdAc3I@K)stTz;IK~px$nI1<N_J@^Pl}9Hv|WWxH<{2{oZGvdeG0&HSmN(
zc*KU_3kE#w3v^GH-rXxy*6>bb*^{L@TjkBId7NkUZ@RB<Jt1=1!(e$Cj}=#A1RJKr
zMs2t<MX-V6b&{c_R!&Q+%*OabNe<_>thw@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@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2Neyz%;?r#W5s;
z^X<&NU0Q}B4cBX#Hr}~3J)nARjhOVoL$|!RH-<!S+_FV%i%>12pw9Za9ARA#{=S=g
zS5Y`wt$sJp9qZP#NuNLa78$9Z+_U%Z{x7x@E}Lsd7w?Q&8M5m7>!V2@AHS}$kvsnQ
z<MsQm3oOn&|NOPecj+ak4X3iUT1oX5^en%^ZP*gX`7F*PL~r`^)2Ui5K@6@}T(jd+
zkENQ;*0hZbO=1a-)6ls5aQWqvr#8At1ji}g$S`^LefP0OXJIzO*O&a84<0x;e<9bg
zw=*g@oG+hC68n3^fL(rNdR~mgQDI@l1QGk<4pR%S`Q?sjJ6?pwADgQ;Lu+bRTzt;n
zxb<(#c89I@RTJKLZ`+#c^mSp{#l?T??CW;qZO>k(;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??r<cgMDeB4nyzTk%PYzF6n=9Zj^R~tz
zmyXm7ZC-)!ZG6&7%W`CoyZC$w{;=`zf}@T;M-=}F`7`o6*Ytl;GI|y}p`6j}5`X-W
za~GzcJQn6In8f82==ZED{n^?c1<M-+mwP1+>3_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
zcmV<I1QYv-P)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS<R!KxbRA>e5n>#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$<J^Uv1@
zTYtYkkB^TR7Z+z|XD25oM@L83*VjeBkkQ%P+&nosxwW-*et!PX@q-A0h@(UXjfj?>
zo}Sg!)vK#3M>>Y)C_xnpMS$g3(%;{|y}kYR_GZB7QH4U(qC7vc+>-6>?Hd~#@9*y(
z_%lRNjtLCq2I(v~Jw45G&%h=07(<wIhUhBL+S<CgxhaI%K@CPR?TXg6J!|^-_}BrC
zXj4oV;#5XPMnwFc4_N>J!*(2O+Kr8kJv==4V3Lvr24HM3XSbn~>V#F)JAe@wH>P#X
zXP`dXK=N8|IHDSQ3!RTz1F-ylvL>EgMW0><UiL!LTd*yr>#c{U&U-fpnP6uNpAG_K
z(wXbGK33GMtgK8;O&K3KRX#mEy}Z1%wY51(**EU!=wPKC92{7QaIE$}-9A4*&&<q3
z_UNPA+gqQJNs$Gr?LA;;XD3CG<)ej#1+^E4hK6o#ZY*V8n*mt0WiGQnh}^GwdwZ)4
z6vqv~`nFbm6`!A<Z)s_%!j$7HagHjf)6>(5i3#RpRlWaL00TQs4<QGsbH?7@p3zc4
z;|A=scnRSY*ZG~&etmsCBsaht&3#r9DoO#G<55U%fKMcrBXX9>HW)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<Dlo12^S(F_FxvDepE
zAEmCcMsB=C`}p{9RTZ~~LQJgU72~S16-ql5OY1yJ>+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<U`XPXYAdlU@*0V>
z#JYQp1Y@$Z)UnQ8CgJk#?k=~ujUZ4Hhe&@=f>*UVsqdO{zFd#Ul`|azZlz6Nfg9VJ
z)T=-nPhi;wNcy|k?<HjaubfF@5&v?g^+ntmx0=sM00<=X>2sB79VU`xTxpmVSXVl$
zs0Ke-lRW^V`IGpP$msR#-N91G0;Q+NBsp_&Nb^s*GS0fFvIajWi0N?;;Ep4bqNFlJ
zA!=<?zhX4v1=e)gezc?Z5%ChjDTIo0#PAS75b?CMA=Q{gx$(m|i%x1ZY4m_c5BvwQ
WX4EjbQt9vj0000<MNUMnLSTY#<u-8u

literal 0
HcmV?d00001

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index c5634e4ac..e7274fb04 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -173,7 +173,7 @@ StoryId Stories::generate(
 	const auto itemId = item->id;
 	const auto peer = item->history()->peer;
 	const auto session = &peer->session();
-	auto stories = StoriesList{ .user = item->from()->asUser() };
+	auto full = std::vector<StoriesList>();
 	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<not_null<PhotoData*>>(media) }),
+							: StoryMedia{
+								v::get<not_null<PhotoData*>>(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<Data::StoriesList> &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<not_null<PhotoData*>>(item.media.data)) {
 			_photoPlayback = std::make_unique<PhotoPlayback>(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<Data::StoriesList> &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> &sibling,
+		const Data::StoriesList *list) {
+	if (!list || list->items.empty()) {
+		sibling = nullptr;
+	} else if (!sibling || !sibling->shows(*list)) {
+		sibling = std::make_unique<Sibling>(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*> 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<ChatHelpers::FileChosen>;
 
-	void show(const Data::StoriesList &list, int index);
+	void show(
+		const std::vector<Data::StoriesList> &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*> 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<Data::StoriesList> &lists,
+		int index);
+	void showSibling(
+		std::unique_ptr<Sibling> &sibling,
+		const Data::StoriesList *list);
+
 	const not_null<Delegate*> _delegate;
 
 	rpl::variable<std::optional<Layout>> _layout;
@@ -94,11 +115,15 @@ private:
 	const std::unique_ptr<Header> _header;
 	const std::unique_ptr<Slider> _slider;
 	const std::unique_ptr<ReplyArea> _replyArea;
+	std::unique_ptr<PhotoPlayback> _photoPlayback;
 
 	Data::FullStoryId _shown;
 	std::optional<Data::StoriesList> _list;
 	int _index = 0;
-	std::unique_ptr<PhotoPlayback> _photoPlayback;
+	bool _started = false;
+
+	std::unique_ptr<Sibling> _siblingLeft;
+	std::unique_ptr<Sibling> _siblingRight;
 
 	std::unique_ptr<base::PowerSaveBlocker> _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<PhotoData*> photo,
+		Data::FileOrigin origin,
+		Fn<void()> update);
+
+	QImage blurred() override;
+	QImage good() override;
+
+private:
+	const not_null<PhotoData*> _photo;
+	const Fn<void()> _update;
+	std::shared_ptr<Data::PhotoMedia> _media;
+	rpl::lifetime _waitingLoading;
+
+};
+
+class Sibling::LoaderVideo final
+	: public Sibling::Loader
+	, public base::has_weak_ptr {
+public:
+	LoaderVideo(
+		not_null<DocumentData*> video,
+		Data::FileOrigin origin,
+		Fn<void()> update);
+
+	QImage blurred() override;
+	QImage good() override;
+
+private:
+	void waitForGoodThumbnail();
+	bool updateAfterGoodCheck();
+	void streamedFailed();
+
+	const not_null<DocumentData*> _video;
+	const Data::FileOrigin _origin;
+	const Fn<void()> _update;
+	std::shared_ptr<Data::DocumentMedia> _media;
+	std::unique_ptr<Streaming::Instance> _streamed;
+	rpl::lifetime _waitingGoodGeneration;
+	bool _checkingGoodInCache = false;
+	bool _failed = false;
+
+};
+
+Sibling::LoaderPhoto::LoaderPhoto(
+	not_null<PhotoData*> photo,
+	Data::FileOrigin origin,
+	Fn<void()> 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<DocumentData*> video,
+	Data::FileOrigin origin,
+	Fn<void()> 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<Streaming::Instance>(
+			_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*> 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<not_null<DocumentData*>>(&data)) {
+		_loader = std::make_unique<LoaderVideo>((*video), origin, [=] {
+			check();
+		});
+	} else if (const auto photo = std::get_if<not_null<PhotoData*>>(&data)) {
+		_loader = std::make_unique<LoaderPhoto>((*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*> 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*> _controller;
+
+	Data::FullStoryId _id;
+	QImage _blurred;
+	QImage _good;
+	Ui::Animations::Simple _goodShown;
+
+	std::unique_ptr<Loader> _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*> delegate)
 
 View::~View() = default;
 
-void View::show(const Data::StoriesList &list, int index) {
-	_controller->show(list, index);
+void View::show(
+		const std::vector<Data::StoriesList> &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<QRect> 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*> delegate);
 	~View();
 
-	void show(const Data::StoriesList &list, int index);
+	void show(
+		const std::vector<Data::StoriesList> &lists,
+		int index,
+		int subindex);
 	void ready();
 
 	[[nodiscard]] QRect contentGeometry() const;
+	[[nodiscard]] rpl::producer<QRect> 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> _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<OverlayWidget*> 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<QRect, kControlsCount + 1> _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<ChatHelpers::Show> 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*> 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<not_null<PeerData*>>(&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<StoriesContext>(&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<Stories::View>(
-				static_cast<Stories::Delegate*>(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<Stories::Delegate*>(this);
+		_stories = std::make_unique<Stories::View>(delegate);
+		_stories->contentGeometryValue(
+		) | rpl::skip(1) | rpl::start_with_next([=] {
+			updateControlsGeometry();
+		}, _stories->lifetime());
+		_storiesChanged.fire({});
+	}
+}
+
 void OverlayWidget::setSession(not_null<Main::Session*> session) {
 	if (_session == session) {
 		return;
@@ -4908,7 +4973,7 @@ void OverlayWidget::setSession(not_null<Main::Session*> 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<PeerData*>,
 		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<void(
 		const QString &,
@@ -572,7 +577,8 @@ private:
 	bool _showAsPip = false;
 
 	std::unique_ptr<Stories::View> _stories;
-	UserData *_storiesUser = nullptr;
+	rpl::event_stream<> _storiesChanged;
+	Main::Session *_storiesSession = nullptr;
 	rpl::event_stream<ChatHelpers::FileChosen> _storiesStickerOrEmojiChosen;
 	std::unique_ptr<Ui::LayerManager> _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 <johnprestonmail@gmail.com>
Date: Tue, 9 May 2023 19:34:04 +0400
Subject: [PATCH 012/259] 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<PrivacyExceptionsBoxController::Row> 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<QString> title() const = 0;
-	[[nodiscard]] virtual bool hasOption(Option option) const {
-		return true;
-	}
+	[[nodiscard]] virtual bool hasOption(Option option) const;
 	[[nodiscard]] virtual rpl::producer<QString> optionsTitleKey() const = 0;
 	[[nodiscard]] virtual QString optionLabel(Option option) const;
 	[[nodiscard]] virtual rpl::producer<TextWithEntities> warning() const {

From 2212b55b1377a5b40214bf5090d829787fa5a97b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 9 May 2023 19:37:40 +0400
Subject: [PATCH 013/259] 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*> 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*> 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<QRect> 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 <johnprestonmail@gmail.com>
Date: Tue, 9 May 2023 21:40:06 +0400
Subject: [PATCH 014/259] 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<StoryItem> 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 <johnprestonmail@gmail.com>
Date: Wed, 10 May 2023 14:33:17 +0400
Subject: [PATCH 015/259] 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<bool> 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<DocumentData*> 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<bool> focusedValue() const;
 	[[nodiscard]] rpl::producer<> cancelRequests() const;
 	[[nodiscard]] rpl::producer<Api::SendOptions> sendRequests() const;
 	[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
@@ -390,6 +391,7 @@ private:
 	std::unique_ptr<WebpageProcessor> _preview;
 
 	Fn<void()> _raiseEmojiSuggestions;
+	rpl::event_stream<bool> _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*> delegate)
 , _slider(std::make_unique<Slider>(this))
 , _replyArea(std::make_unique<ReplyArea>(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<Layout> Controller::layoutValue() const {
 	return _layout.value() | rpl::filter_optional();
 }
 
+float64 Controller::contentFade() const {
+	return _contentFadeAnimation.value(_contentFaded ? 1. : 0.)
+		* kFullContentFade;
+}
+
 std::shared_ptr<ChatHelpers::Show> 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<Ui::RpWidget*> wrap() const;
 	[[nodiscard]] Layout layout() const;
 	[[nodiscard]] rpl::producer<Layout> layoutValue() const;
+	[[nodiscard]] float64 contentFade() const;
 
 	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> 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> _replyArea;
 	std::unique_ptr<PhotoPlayback> _photoPlayback;
 
+	Ui::Animations::Simple _contentFadeAnimation;
+	bool _contentFaded = false;
+
 	Data::FullStoryId _shown;
 	std::optional<Data::StoriesList> _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<bool> 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<bool> ReplyArea::focusedValue() const {
+	return _controls->focusedValue();
+}
+
 void ReplyArea::showPremiumToast(not_null<DocumentData*> 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<bool> 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<QRect> 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<QRect> 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<OverlayWidget*> 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<OverlayWidget*> 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 <johnprestonmail@gmail.com>
Date: Wed, 10 May 2023 23:32:32 +0400
Subject: [PATCH 016/259] 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<const char*>(
 		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<QOpenGLShaderProgram*> 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<QOpenGLShaderProgram*> 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*> 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 <johnprestonmail@gmail.com>
Date: Thu, 11 May 2023 18:36:59 +0400
Subject: [PATCH 017/259] 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<Layout> 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<ChatHelpers::Show> 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*> 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<Ui::RpWidget*> wrap() const;
 	[[nodiscard]] Layout layout() const;
 	[[nodiscard]] rpl::producer<Layout> layoutValue() const;
-	[[nodiscard]] float64 contentFade() const;
+	[[nodiscard]] ContentLayout contentLayout() const;
 
 	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
 	[[nodiscard]] auto stickerOrEmojiChosen() const
@@ -90,8 +99,7 @@ public:
 	[[nodiscard]] bool canDownload() const;
 
 	void repaintSibling(not_null<Sibling*> 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<Ui::RpWidget*> storiesWrap() = 0;
@@ -36,6 +41,7 @@ public:
 		-> rpl::producer<ChatHelpers::FileChosen> = 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>(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*> _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<style::TextStyle> _nameStyle;
+	std::optional<Ui::Text::String> _name;
+	QString _nameText;
+	int _nameAvailableWidth = 0;
+	int _nameFontSize = 0;
+
 	std::unique_ptr<Loader> _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<QRect> View::contentGeometryValue() const {
+rpl::producer<QRect> 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<QRect> contentGeometryValue() const;
-	[[nodiscard]] float64 contentFade() const;
-	[[nodiscard]] SiblingView siblingLeft() const;
-	[[nodiscard]] SiblingView siblingRight() const;
+	[[nodiscard]] QRect finalShownGeometry() const;
+	[[nodiscard]] rpl::producer<QRect> 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<QOpenGLShaderProgram*> 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<QOpenGLShaderProgram> _fillProgram;
 	std::optional<QOpenGLShaderProgram> _controlsProgram;
 	std::optional<QOpenGLShaderProgram> _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<OverlayWidget*> 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<Main::Session*> 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*> 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<Stories::Delegate*>(this);
 		_stories = std::make_unique<Stories::View>(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 <johnprestonmail@gmail.com>
Date: Fri, 12 May 2023 10:07:37 +0400
Subject: [PATCH 018/259] 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<OverlayWidget*> 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 <johnprestonmail@gmail.com>
Date: Fri, 12 May 2023 13:41:25 +0400
Subject: [PATCH 019/259] 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<Layout> Controller::layoutValue() const {
 }
 
 ContentLayout Controller::contentLayout() const {
+	const auto &current = _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<ChatHelpers::Show> 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<Layout> layoutValue() const;
 	[[nodiscard]] ContentLayout contentLayout() const;
+	[[nodiscard]] TextWithEntities captionText() const;
 
 	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
 	[[nodiscard]] auto stickerOrEmojiChosen() const
@@ -134,6 +135,7 @@ private:
 	bool _contentFaded = false;
 
 	Data::FullStoryId _shown;
+	TextWithEntities _captionText;
 	std::optional<Data::StoriesList> _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<QRect> 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<QRect, kControlsCount + 1> _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 <johnprestonmail@gmail.com>
Date: Fri, 12 May 2023 21:10:00 +0400
Subject: [PATCH 020/259] 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<Ui::RpWidget*> parent,
+	not_null<Main::Session*> session,
+	const TextWithEntities &text,
+	Fn<void()> close)
+: RpWidget(parent)
+, _scroll(std::make_unique<Ui::ScrollArea>((RpWidget*)this))
+, _text(_scroll->setOwnedWidget(
+	object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
+		_scroll.get(),
+		object_ptr<Ui::FlatLabel>(_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<Ui::RpWidget*> parent,
+		not_null<Main::Session*> session,
+		const TextWithEntities &text,
+		Fn<void()> 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<Ui::ScrollArea> _scroll;
+	const not_null<Ui::FlatLabel*> _text;
+	Fn<void()> _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*> 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<CaptionFullView>(
+		wrap(),
+		&_delegate->storiesShow()->session(),
+		_captionText,
+		[=] { togglePaused(false); });
+}
+
 std::shared_ptr<ChatHelpers::Show> 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<Layout> layoutValue() const;
 	[[nodiscard]] ContentLayout contentLayout() const;
 	[[nodiscard]] TextWithEntities captionText() const;
+	void showFullCaption();
 
 	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
 	[[nodiscard]] auto stickerOrEmojiChosen() const
@@ -130,6 +132,7 @@ private:
 	const std::unique_ptr<Slider> _slider;
 	const std::unique_ptr<ReplyArea> _replyArea;
 	std::unique_ptr<PhotoPlayback> _photoPlayback;
+	std::unique_ptr<CaptionFullView> _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<PhotoData*> photo) {
+void OverlayWidget::displayPhoto(
+		not_null<PhotoData*> 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<PhotoData*> 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<not_null<PhotoData*>>(&data)) {
-		displayPhoto(*photo);
+		displayPhoto(*photo, activation);
 	} else {
-		displayDocument(v::get<not_null<DocumentData*>>(data));
+		displayDocument(v::get<not_null<DocumentData*>>(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<LambdaClickHandler>(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<PhotoData*> photo);
+	void displayPhoto(
+		not_null<PhotoData*> 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 <johnprestonmail@gmail.com>
Date: Tue, 16 May 2023 19:48:49 +0400
Subject: [PATCH 021/259] 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<EmojiPtr> _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<ChatHelpers::Show> show,
+		const style::EmojiPan &st,
 		not_null<FieldAutocomplete*> parent,
 		not_null<MentionRows*> mrows,
 		not_null<HashtagRows*> hrows,
@@ -126,11 +127,13 @@ private:
 
 	const std::shared_ptr<ChatHelpers::Show> _show;
 	const not_null<Main::Session*> _session;
+	const style::EmojiPan &_st;
 	const not_null<FieldAutocomplete*> _parent;
 	const not_null<MentionRows*> _mrows;
 	const not_null<HashtagRows*> _hrows;
 	const not_null<BotCommandRows*> _brows;
 	const not_null<StickerRows*> _srows;
+	Ui::RoundRect _overBg;
 	rpl::lifetime _stickersLifetime;
 	std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
 	base::unique_qptr<Ui::PopupMenu> _menu;
@@ -192,10 +195,12 @@ FieldAutocomplete::FieldAutocomplete(
 
 FieldAutocomplete::FieldAutocomplete(
 	QWidget *parent,
-	std::shared_ptr<ChatHelpers::Show> show)
+	std::shared_ptr<ChatHelpers::Show> 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<Inner>(
 			_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<ChatHelpers::Show> show,
+	const style::EmojiPan &st,
 	not_null<FieldAutocomplete*> parent,
 	not_null<MentionRows*> mrows,
 	not_null<HashtagRows*> hrows,
@@ -824,14 +831,16 @@ FieldAutocomplete::Inner::Inner(
 	not_null<StickerRows*> 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<Ui::PathShiftGradient>(
-	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<Window::SessionController*> controller);
 	FieldAutocomplete(
 		QWidget *parent,
-		std::shared_ptr<ChatHelpers::Show> show);
+		std::shared_ptr<ChatHelpers::Show> show,
+		const style::EmojiPan *stOverride = nullptr);
 	~FieldAutocomplete();
 
 	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
@@ -153,6 +158,7 @@ private:
 
 	const std::shared_ptr<ChatHelpers::Show> _show;
 	const not_null<Main::Session*> _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 <QtCore/QMimeData>
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<LocalStickersManager>(&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<Ui::PathShiftGradient>(
-	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<TabbedSelector::InnerFooter> 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<bool()> 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> _show;
+	Ui::RoundRect _overBg;
 	std::unique_ptr<Ui::TabbedSearch> _search;
 	MTP::Sender _api;
 	std::unique_ptr<LocalStickersManager> _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> 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> show;
+	const style::EmojiPan &st;
+	PauseReason level = {};
+	TabbedSelectorMode mode = TabbedSelectorMode::Full;
+	bool stickersSettingsHidden = false;
+};
+
 [[nodiscard]] std::unique_ptr<Ui::TabbedSearch> MakeSearch(
 	not_null<Ui::RpWidget*> 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> 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<Support::Autocomplete>(this, &session())
 	: nullptr)
-, _send(std::make_shared<Ui::SendButton>(this))
+, _send(std::make_shared<Ui::SendButton>(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<Ui::RpWidget*> 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<ChatHelpers::TabbedSelector>(
 		_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<Ui::RpWidget>(parent))
 , _writeRestricted(std::make_unique<Ui::RpWidget>(parent))
-, _send(std::make_shared<Ui::SendButton>(_wrap.get()))
+, _send(std::make_shared<Ui::SendButton>(_wrap.get(), _st.send))
 , _attachToggle(Ui::CreateChild<Ui::IconButton>(
 	_wrap.get(),
-	st::historyAttach))
+	_st.attach))
 , _tabbedSelectorToggle(Ui::CreateChild<Ui::EmojiButton>(
 	_wrap.get(),
-	st::historyAttachEmoji))
+	_st.emoji))
 , _field(
 	Ui::CreateChild<Ui::InputField>(
 		_wrap.get(),
-		st::historyComposeField,
+		_st.field,
 		Ui::InputField::Mode::MultiLine,
 		tr::lng_message_ph()))
-, _botCommandStart(Ui::CreateChild<Ui::IconButton>(
-	_wrap.get(),
-	st::historyBotCommandStart))
+, _botCommandStart(_features.botCommandSend
+	? Ui::CreateChild<Ui::IconButton>(
+		_wrap.get(),
+		st::historyBotCommandStart)
+	: nullptr)
 , _autocomplete(std::make_unique<FieldAutocomplete>(parent, _show))
 , _header(std::make_unique<FieldHeader>(_wrap.get(), _show))
 , _voiceRecordBar(std::make_unique<VoiceRecordBar>(
@@ -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<void()> &&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<ChatHelpers::Show> show;
 	Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted;
 	ComposeControlsMode mode = ComposeControlsMode::Normal;
 	SendMenu::Type sendMenuType = {};
 	Window::SessionController *regularWindow = nullptr;
 	rpl::producer<ChatHelpers::FileChosen> 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<QWidget*> _parent;
 	const std::shared_ptr<ChatHelpers::Show> _show;
 	const not_null<Main::Session*> _session;
@@ -332,12 +351,14 @@ private:
 	const std::unique_ptr<Ui::RpWidget> _wrap;
 	const std::unique_ptr<Ui::RpWidget> _writeRestricted;
 
+	std::optional<Ui::RoundRect> _backgroundRect;
+
 	const std::shared_ptr<Ui::SendButton> _send;
 	const not_null<Ui::IconButton*> _attachToggle;
 	std::unique_ptr<Ui::IconButton> _replaceMedia;
 	const not_null<Ui::EmojiButton*> _tabbedSelectorToggle;
 	const not_null<Ui::InputField*> _field;
-	const not_null<Ui::IconButton*> _botCommandStart;
+	Ui::IconButton * const _botCommandStart = nullptr;
 	std::unique_ptr<Ui::SendAsButton> _sendAs;
 	std::unique_ptr<Ui::SilentToggle> _silent;
 	std::unique_ptr<Controls::TTLButton> _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*> controller)
 , _controls(std::make_unique<HistoryView::ComposeControls>(
 	_controller->wrap(),
 	HistoryView::ComposeControlsDescriptor{
+		.stOverride = &st::storiesComposeControls,
 		.show = _controller->uiShow(),
 		.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
 			showPremiumToast(emoji);
@@ -33,6 +34,14 @@ ReplyArea::ReplyArea(not_null<Controller*> 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 <QtCore/QFileInfo>
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 <QtGui/QtEvents>
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<Ui::InfiniteRadialAnimation> _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 <johnprestonmail@gmail.com>
Date: Wed, 17 May 2023 15:51:04 +0400
Subject: [PATCH 022/259] 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(&`*|3<m+3V)lOs}2rWY@FK2`@Ie
rYfBxIe0hC)o$~%a$-<?7KmO$Fd~}?va^3cOpfL4x^>bP0l+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)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>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~Ou<DsEE)WWs5Js4}Sv49B(DdTjY-V)3+6W=ExJw9~$1JE;s{v(aIGs+q
zj~@$!(83@gbSkr;Y&Hv&eM)!mZxLDvp|csbVj8?`jqIAb0~Xd)I-ORx6m=$(QAP%v
z&BmBup(O-?Je{A^BrO(;=K*9aYf2`j`!6fcxbC2JSp?>i={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_u<K^c5E=?WNyAF9fdOq^LAkg{2^vfav{kcv~A8S~gTvWB@`Z=w1T>ept
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?;g327cMa6dxD2Z29uPZmAa<SYwh7XKs5x`<-rqjhWdr*AO=0
z0$J&Gn?JdV>u4?sF{%oiWvjPDP0&b^S;FLN#nZogFMZ6Abf4tQ@k4(1Oo0w&E4{bY
z*^%ETU-<B{Pjk+h84_80zPx$&r$GI&v`Ni5^_lZdJx-D+eXcU=?%lif_WCTDCE^8I
zEDDeFW-K|`<n-)~PtB#Xx*VqroIQ%d9?JQpo@14qZtAy^=}P(MCr_5x{5^ZtH!R@(
z_c=@d{i|E7-L&~-yOQ7bZQFV{Qx>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<vuAr@LTBdR2+N$(H?Py|x4|Vj
zZG#`{)~!2M_F@yCL&sbD7ZPG(VlyI+D9v``a9F%`>(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|<ARu5Q;H{RR>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?<Z=7%P@1B33O31OiXR1ceC*9a`;2w8&oF2<vxrvO7r~W%`F7Q<}
zn4hvvX5|Ww_bu}!lHy*QWpLf=Q(IG5cd~s(WNloU-Q1<g556?`O31ZVUe<86o4a?{
vu0=XOr*)j(C02GOGk{x?0|{nCu>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>Dwv<jIq&Dk>gcUiWqsHizux`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@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2GvA7J0fjhD30_
zy>gnjK|#PZP}w$E#P<RNZvZ>%i|8GSGb1PPOgx&J_{FpB;g6kqY)l*q0@@RTPFZfh
zc1*(RRgC8wA?~YN%GQ3J**@uh)uiKh&Xg~$NsMj`{@Qb<Z3_FkO#xo-dv8uCT7T;8
zU5=C0@6%-UuRqz#J2mJ`+nzgfWF+OjPt7cT^>V@gfF)*I1soU{S<r|wjy+r0C;UsR
Ro#zB{wWq6}%Q~loCIFR5UvU5c

diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png b/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png
deleted file mode 100644
index ce7f1aa76eb4ee42144092d0b2aaa463c795be08..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 494
zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|Wxo#^NA%Cx&(BWL^R}E~ycoX}-P;
zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz1|Sip>6gA`6MbbAj}7UAjQ7?Q#I
z_L^Z<i-X9qkG?r9wv83+*{u1STBm;y+a~AAeXzJAA}euY>)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)f<oVR=3>Cb;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<qmfg-7q+jO$6<`$i-DX@{No>#s%F7~=8OQXM3lag1U$mgFm96P5scTG55
zzgWTMS5ueO^P2lhnGd-<j`+Yo^K4pZ=*HTgJGoyM@4kC$r}_3YiDP-&t>>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)<ENX!i`i${?hCMR
x`FvTrev!Dm^s%`<cVCtql08~@#`4?Uh4DA;t&Q)WH$4^<8J@0wF6*2UngG>#%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
zcmV<L0}%X)P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NFSxH1eR9Fe^m_17~K@i6^hNLl~
z=?wUiLb0;2kTilMg0JZ3$OrH<DEJMuw^u<;ARwd*X;MZ66?_39jfoZ_KF^N~Y))@4
z*}F|hlPkjR?96}vnc110yP=`MGw{H`1HJXYh&}x1=;-Y1?8L;x=jZ45_qRQuZ-n9D
z;Z!PhaBxtmRNmj;|2dwwx3_Y+ytA|88#I_fI2_JqvrkV?4i1`OKA#VUk5$;*a(8$4
z=;&yEeqK!K_4?J-RjE{Zd3kw#eZ_6l)6-K^Q)6Rer>Cdput79Kq0r&sp>jpBSlry)
zj6@<qu<UI(vlkZ^!shPo4nNsr21md*icKz;8y_DJj@1=Q6{+Ln<B^dr)Cen9L`Ncp
zZ~D)M46jzJ5aQhQKT<fBdL!;JImOXvw6{q|QIYCLhIfoQR34j;A*ZFKrHze^j<CNJ
zxEaJf)SF)mx(TIHlvT~tX|-e@A0OziuCBI3j#0=lZVmVL_LxwM3xz^U+NBY9Fi6|m
z+gdz5p{=bgw7D6+zP_|(Ho}{m8_d(`v=&59D49&6%{`^Hu@uU=TwGk_q`ILrmn(Ng
zEEYqRThncTgq)h0nNdr^o+(|C5;Ayrc+fid`T3bu%1S&QuhnW!6a82nQti2uoRU-A
z{+G<t=H1H5iWn{|EVvoyL*~}&qC^$_pPwH$tR`eCQj?RDZUdq)H#g^u4k5d~zD9F>
zeVs5V6R(gdShbNdVW1(@Xf#N8j!`0!aH^^?oNjM#)l>~&pp`2H83-&}u+g6-ruvZG
z-`}&gyu9o-u$Ks|Y~e=J6S};-M70-fA;(;*zieSj=<MtaRic*3WZX73lC`xpsVTCB
zDOB$+SE?d8&&{o^itYIMYCi}BPdm8gn(Fuf@2LD&q3aDAT^_w2?xF7k{O^7ybM=Sp
zTUd%AAX1=BIzCT&U1{=Rb8>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)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>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<i_cvOpprLCIvYy}iAy
zt*y1SwW+D8w6wIOq~!DS^Xcj7`T6<s^78oj_~_{9@bHkvfH2r(TU1muGBUEdx_W<q
z4`SnQXJ;orKi?(->%5qg`T6<B$48?wjt(6i9oE6vVOUsLI59DCdwc6ZtUtcIyqun%
z;(k|GSJ3k6`uZ9dwS&Vhmw|zSv$Hb+D#XRb#p2>(Z*MPaEFF4nZ7r|H#>Q%DYV6{%
zz%7wTOixdXCW?4?c$l4?C2Uv_5DAOo;$k9%uGQPy+rq*^d3kvxc<czpkq!<HbgkCb
z*6Qo)?QoBZ3v;Dw_3`mBH8m9#7;AD_SGotjzP=6)4qD?LAtOCnpw;i)f;`!X$S&&&
z5ByK&id|UWul|R1rDm1fLZ*ut=xM5dZ)fsA$(Y!${`tr*mL|KE>0%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)Zf<Uf
zib`~Kb+tQ&0{7SC9I6AkJVhAy&TPJOb8`(14e8zF8~~9q;!`7;oSY=TNgpdM@9*za
z^(rbVXsN2Il6F7D?(VMII;b%j$IC9PhIsv(%ieu*a-!H$kaWjma7FjQ;lE}3Q9=s}
z48CS~E$){~oeW<>YBpC?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
zbZkGOqb<L&w5a(h#mJH+FaMEBGC0D5DfL;IE?@{GJ493^ZffbG71;(b?C7I&;ZlMi
ztHgwJDZ#ymE)f7a9-DNS*GL>mIo-;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<Ui::RpWidget*> 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<LocalStickersManager>(&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<Ui::RippleAnimation> 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<Ui::RippleAnimation>(
 		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<void()>)> customRecentFactory;
 	const style::EmojiPan *st = nullptr;
+	ComposeFeatures features;
 };
 
 class EmojiListWidget final
@@ -345,6 +347,7 @@ private:
 	void applyNextSearchQuery();
 
 	const std::shared_ptr<Show> _show;
+	const ComposeFeatures _features;
 	Mode _mode = Mode::Full;
 	std::unique_ptr<Ui::TabbedSearch> _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<Main::Session*> session,
+		bool suggestCustomEmoji,
+		Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium);
+	~SuggestionsWidget();
+
+	void showWithQuery(SuggestionsQuery query, bool force = false);
+	void selectFirstResult();
+	bool handleKeyEvent(int key);
+
+	[[nodiscard]] rpl::producer<bool> toggleAnimated() const;
+
+	struct Chosen {
+		QString emoji;
+		QString customData;
+	};
+	[[nodiscard]] rpl::producer<Chosen> triggered() const;
+
+private:
+	struct Row {
+		Row(not_null<EmojiPtr> emoji, const QString &replacement);
+
+		Ui::Text::CustomEmoji *custom = nullptr;
+		DocumentData *document = nullptr;
+		not_null<EmojiPtr> emoji;
+		QString replacement;
+	};
+	struct Custom {
+		not_null<DocumentData*> document;
+		not_null<EmojiPtr> 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<QWheelEvent*> e);
+	void paintFadings(QPainter &p) const;
+
+	[[nodiscard]] std::vector<Row> getRowsByQuery(const QString &text) const;
+	[[nodiscard]] base::flat_multi_map<int, Custom> lookupCustom(
+		const std::vector<Row> &rows) const;
+	[[nodiscard]] std::vector<Row> appendCustom(
+		std::vector<Row> rows);
+	[[nodiscard]] std::vector<Row> appendCustom(
+		std::vector<Row> rows,
+		const base::flat_multi_map<int, Custom> &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<Ui::Text::CustomEmoji*> resolveCustomEmoji(
+		not_null<DocumentData*> document);
+	void customEmojiRepaint();
+
+	const style::EmojiSuggestions &_st;
+	const not_null<Main::Session*> _session;
+	SuggestionsQuery _query;
+	std::vector<Row> _rows;
+	bool _suggestCustomEmoji = false;
+	Fn<bool(not_null<DocumentData*>)> _allowCustomWithoutPremium;
+
+	Ui::RoundRect _overRect;
+
+	base::flat_map<
+		not_null<DocumentData*>,
+		std::unique_ptr<Ui::Text::CustomEmoji>> _customEmoji;
+	bool _repaintScheduled = false;
+
+	std::optional<QPoint> _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<bool> _toggleAnimated;
+	rpl::event_stream<Chosen> _triggered;
+
+};
+
 SuggestionsWidget::SuggestionsWidget(
 	QWidget *parent,
+	const style::EmojiSuggestions &st,
 	not_null<Main::Session*> session,
 	bool suggestCustomEmoji,
 	Fn<bool(not_null<DocumentData*>)> 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<QTextEdit*> field,
 	not_null<Main::Session*> 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<InnerDropdown>(
-		outer,
-		st::emojiSuggestionsDropdown);
+	_container = base::make_unique_q<InnerDropdown>(outer, _st.dropdown);
 	_container->setAutoHiding(false);
 	_suggestions = _container->setOwnedWidget(
 		object_ptr<Ui::Emoji::SuggestionsWidget>(
 			_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 <QtWidgets/QTextEdit>
 
+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<QString, EmojiPtr>;
 
-class SuggestionsWidget final : public Ui::RpWidget {
-public:
-	SuggestionsWidget(
-		QWidget *parent,
-		not_null<Main::Session*> session,
-		bool suggestCustomEmoji,
-		Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium);
-	~SuggestionsWidget();
-
-	void showWithQuery(SuggestionsQuery query, bool force = false);
-	void selectFirstResult();
-	bool handleKeyEvent(int key);
-
-	[[nodiscard]] rpl::producer<bool> toggleAnimated() const;
-
-	struct Chosen {
-		QString emoji;
-		QString customData;
-	};
-	[[nodiscard]] rpl::producer<Chosen> triggered() const;
-
-private:
-	struct Row {
-		Row(not_null<EmojiPtr> emoji, const QString &replacement);
-
-		Ui::Text::CustomEmoji *custom = nullptr;
-		DocumentData *document = nullptr;
-		not_null<EmojiPtr> emoji;
-		QString replacement;
-	};
-	struct Custom {
-		not_null<DocumentData*> document;
-		not_null<EmojiPtr> 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<QWheelEvent*> e);
-	void paintFadings(QPainter &p) const;
-
-	[[nodiscard]] std::vector<Row> getRowsByQuery(const QString &text) const;
-	[[nodiscard]] base::flat_multi_map<int, Custom> lookupCustom(
-		const std::vector<Row> &rows) const;
-	[[nodiscard]] std::vector<Row> appendCustom(
-		std::vector<Row> rows);
-	[[nodiscard]] std::vector<Row> appendCustom(
-		std::vector<Row> rows,
-		const base::flat_multi_map<int, Custom> &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<Ui::Text::CustomEmoji*> resolveCustomEmoji(
-		not_null<DocumentData*> document);
-	void customEmojiRepaint();
-
-	const not_null<Main::Session*> _session;
-	SuggestionsQuery _query;
-	std::vector<Row> _rows;
-	bool _suggestCustomEmoji = false;
-	Fn<bool(not_null<DocumentData*>)> _allowCustomWithoutPremium;
-
-	base::flat_map<
-		not_null<DocumentData*>,
-		std::unique_ptr<Ui::Text::CustomEmoji>> _customEmoji;
-	bool _repaintScheduled = false;
-
-	std::optional<QPoint> _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<bool> _toggleAnimated;
-	rpl::event_stream<Chosen> _triggered;
-
-};
-
 class SuggestionsController {
 public:
 	struct Options {
 		bool suggestExactFirstWord = true;
 		bool suggestCustomEmoji = false;
 		Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium;
+		const style::EmojiSuggestions *st = nullptr;
 	};
 
 	SuggestionsController(
@@ -189,6 +85,7 @@ private:
 	bool fieldFilter(not_null<QEvent*> event);
 	bool outerFilter(not_null<QEvent*> 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<void(QString, Fn<void()> &&, const style::icon*)> callback,
 		std::shared_ptr<Show> show,
-		not_null<DocumentData*> document) {
+		not_null<DocumentData*> 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<Ui::PopupMenu> GifsListWidget::fillContextMenu(
 		return nullptr;
 	}
 
-	auto menu = base::make_unique_q<Ui::PopupMenu>(
-		this,
-		st::popupMenuWithIcons);
+	auto menu = base::make_unique_q<Ui::PopupMenu>(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<Ui::PopupMenu> 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 <QtCore/QTimer>
 
+namespace style {
+struct ComposeIcons;
+} // namespace style
+
 namespace Api {
 struct SendOptions;
 } // namespace Api
@@ -48,7 +52,8 @@ namespace ChatHelpers {
 void AddGifAction(
 	Fn<void(QString, Fn<void()> &&, const style::icon*)> callback,
 	std::shared_ptr<Show> show,
-	not_null<DocumentData*> document);
+	not_null<DocumentData*> 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<const Ui::InputField*> field) {
+		not_null<const Ui::InputField*> 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<const Ui::InputField*> field);
+	not_null<const Ui::InputField*> 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<Main::Session*> session;
 		Fn<bool()> paused;
 		not_null<RpWidget*> parent;
-		bool settingsButtonVisible = false;
 		const style::EmojiPan *st = nullptr;
+		ComposeFeatures features;
 	};
 	explicit StickersListFooter(Descriptor &&descriptor);
 
@@ -130,7 +131,6 @@ public:
 		uint64 activeSetId,
 		Fn<std::shared_ptr<Lottie::FrameRenderer>()> renderer,
 		ValidateIconAnimations animations);
-	[[nodiscard]] bool hasOnlyFeaturedSets() const;
 
 	void leaveToChildEvent(QEvent *e, QWidget *child) override;
 
@@ -270,7 +270,7 @@ private:
 
 	const not_null<Main::Session*> _session;
 	const Fn<bool()> _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<LocalStickersManager>(&session()))
 , _section(Section::Stickers)
 , _isMasks(_mode == Mode::Masks)
-, _settingsHidden(descriptor.settingsHidden)
 , _updateItemsTimer([=] { updateItems(); })
 , _updateSetsTimer([=] { updateSets(); })
 , _trendingAddBgOver(
@@ -286,8 +286,8 @@ object_ptr<TabbedSelector::InnerFooter> StickersListWidget::createFooter() {
 		.session = &session(),
 		.paused = footerPaused,
 		.parent = this,
-		.settingsButtonVisible = !_settingsHidden,
 		.st = &st(),
+		.features = _features,
 	});
 	_footer = result;
 
@@ -298,7 +298,7 @@ object_ptr<TabbedSelector::InnerFooter> StickersListWidget::createFooter() {
 
 	_footer->openSettingsRequests(
 	) | rpl::start_with_next([=] {
-		const auto onlyFeatured = _footer->hasOnlyFeaturedSets();
+		const auto onlyFeatured = !_isMasks && _mySets.empty();
 		_show->showBox(Box<StickersBox>(
 			_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<Ui::RippleAnimation> 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<Ui::RippleAnimation>(
-		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<DocumentData*> document) {
@@ -1604,9 +1608,7 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
 	auto &set = sets[section];
 	Assert(index >= 0 && index < set.stickers.size());
 
-	auto menu = base::make_unique_q<Ui::PopupMenu>(
-		this,
-		st::popupMenuWithIcons);
+	auto menu = base::make_unique_q<Ui::PopupMenu>(this, st().menu);
 
 	const auto document = set.stickers[sticker->index].document;
 	const auto send = [=](Api::SendOptions options) {
@@ -1618,12 +1620,14 @@ base::unique_qptr<Ui::PopupMenu> 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<Ui::PopupMenu> 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<Ui::PopupMenu> 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<Sticker> {
@@ -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<void()> &&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<Ui::BoxContent> MakeConfirmRemoveSetBox(
 		not_null<Main::Session*> 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<Ui::BoxContent> 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<bool()> 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> _show;
+	const ComposeFeatures _features;
 	Ui::RoundRect _overBg;
 	std::unique_ptr<Ui::TabbedSearch> _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<Ui::BoxContent> MakeConfirmRemoveSetBox(
 	not_null<Main::Session*> 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<Ui::PanelAnimation>(st::emojiPanAnimation, _dropDown ? Ui::PanelAnimation::Origin::TopRight : Ui::PanelAnimation::Origin::BottomRight);
+		_showAnimation = std::make_unique<Ui::PanelAnimation>(
+			_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<Ui::TabbedSearch> 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<StickersListWidget*> masks() const;
 
 	const style::EmojiPan &_st;
+	const ComposeFeatures _features;
 	const std::shared_ptr<Show> _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<FieldAutocomplete>(parent, _show))
+, _autocomplete(std::make_unique<FieldAutocomplete>(
+	parent,
+	_show,
+	&_st.tabbed))
 , _header(std::make_unique<FieldHeader>(_wrap.get(), _show))
 , _voiceRecordBar(std::make_unique<VoiceRecordBar>(
 	_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<ChatHelpers::Show> show;
@@ -111,7 +102,7 @@ struct ComposeControlsDescriptor {
 	SendMenu::Type sendMenuType = {};
 	Window::SessionController *regularWindow = nullptr;
 	rpl::producer<ChatHelpers::FileChosen> 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<QWidget*> _parent;
 	const std::shared_ptr<ChatHelpers::Show> _show;
 	const not_null<Main::Session*> _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*> 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 <QtWidgets/QApplication>
@@ -56,10 +57,14 @@ FillMenuResult FillSendMenu(
 		Type type,
 		Fn<void()> silent,
 		Fn<void()> schedule,
-		Fn<void()> whenOnline) {
+		Fn<void()> 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<void()> silent,
 	Fn<void()> schedule,
-	Fn<void()> whenOnline);
+	Fn<void()> whenOnline,
+	const style::ComposeIcons *iconsOverride = nullptr);
 
 void SetupMenuAndShortcuts(
 	not_null<Ui::RpWidget*> button,

From 00b4f7738427c52ed0e85170cdee92178fa87130 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 May 2023 12:56:15 +0400
Subject: [PATCH 023/259] 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<QString> {
 			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<FieldHeader>(_wrap.get(), _show))
 , _voiceRecordBar(std::make_unique<VoiceRecordBar>(
 	_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<ChatHelpers::FileChosen> 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<Ui::RpWidget*> parent,
+		const style::RecordBar &st,
 		not_null<Main::Session*> session,
 		::Media::Capture::Result &&data,
 		const style::font &font);
@@ -231,12 +232,12 @@ private:
 
 	not_null<Ui::RpWidget*> _parent;
 
+	const style::RecordBar &_st;
 	const not_null<Main::Session*> _session;
 	const not_null<DocumentData*> _document;
 	const std::unique_ptr<VoiceData> _voiceData;
 	const std::shared_ptr<Data::DocumentMedia> _mediaView;
 	const std::unique_ptr<::Media::Capture::Result> _data;
-	const style::IconButton &_stDelete;
 	const base::unique_qptr<Ui::IconButton> _delete;
 	const style::font &_durationFont;
 	const QString _duration;
@@ -264,17 +265,18 @@ private:
 
 ListenWrap::ListenWrap(
 	not_null<Ui::RpWidget*> parent,
+	const style::RecordBar &st,
 	not_null<Main::Session*> 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<Ui::IconButton>(parent, _stDelete))
+, _delete(base::make_unique_q<Ui::IconButton>(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<Ui::RpWidget*> parent);
+	RecordLock(
+		not_null<Ui::RpWidget*> 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<float64> _progress = 0.;
+	int _visibleTopPart = -1;
+
 };
 
-RecordLock::RecordLock(not_null<Ui::RpWidget*> parent)
-: RippleButton(parent, st::defaultRippleAnimation)
+RecordLock::RecordLock(
+	not_null<Ui::RpWidget*> parent,
+	const style::RecordBarLock &st)
+: RippleButton(parent, st.ripple)
+, _st(st)
 , _rippleRect(QRect(
 	0,
 	0,
@@ -653,6 +665,10 @@ RecordLock::RecordLock(not_null<Ui::RpWidget*> 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<Ui::RpWidget*> parent, int height);
+	CancelButton(
+		not_null<Ui::RpWidget*> 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<Ui::RpWidget*> parent, int height)
-: Ui::RippleButton(parent, st::defaultLightButton.ripple)
+CancelButton::CancelButton(
+	not_null<Ui::RpWidget*> 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<Ui::RpWidget*> parent,
-	not_null<Ui::RpWidget*> sectionWidget,
-	std::shared_ptr<ChatHelpers::Show> show,
-	std::shared_ptr<Ui::SendButton> send,
-	int recorderHeight)
+	VoiceRecordBarDescriptor &&descriptor)
 : RpWidget(parent)
-, _sectionWidget(sectionWidget)
-, _show(std::move(show))
-, _send(send)
-, _lock(std::make_unique<RecordLock>(sectionWidget))
-, _level(std::make_unique<VoiceRecordButton>(sectionWidget))
-, _cancel(std::make_unique<CancelButton>(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<RecordLock>(_outerContainer, _st.lock))
+, _level(std::make_unique<VoiceRecordButton>(_outerContainer, _st))
+, _cancel(std::make_unique<CancelButton>(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<ChatHelpers::Show> show,
 	std::shared_ptr<Ui::SendButton> 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<bool()> &&callback) {
 	_startRecordingFilter = std::move(callback);
 }
 
-void VoiceRecordBar::setLockBottom(rpl::producer<int> &&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<Ui::RpWidget*>(parentWidget())->geometryValue()
+	) | rpl::start_with_next([=] {
+		updateLockGeometry();
 	}, lifetime());
 }
 
-void VoiceRecordBar::setSendButtonGeometryValue(
-		rpl::producer<QRect> &&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<Ui::RpWidget*>(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<ListenWrap>(
 				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<Ui::RpWidget*> outerContainer;
+	std::shared_ptr<ChatHelpers::Show> show;
+	std::shared_ptr<Ui::SendButton> 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<Ui::RpWidget*> parent,
-		not_null<Ui::RpWidget*> sectionWidget,
-		std::shared_ptr<ChatHelpers::Show> show,
-		std::shared_ptr<Ui::SendButton> send,
-		int recorderHeight);
+		VoiceRecordBarDescriptor &&descriptor);
 	VoiceRecordBar(
 		not_null<Ui::RpWidget*> parent,
 		std::shared_ptr<ChatHelpers::Show> show,
@@ -75,8 +86,6 @@ public:
 
 	void requestToSendWithOptions(Api::SendOptions options);
 
-	void setLockBottom(rpl::producer<int> &&bottom);
-	void setSendButtonGeometryValue(rpl::producer<QRect> &&geometry);
 	void setStartRecordingFilter(Fn<bool()> &&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<Ui::RpWidget*> _sectionWidget;
+	const style::RecordBar &_st;
+	const not_null<Ui::RpWidget*> _outerContainer;
 	const std::shared_ptr<ChatHelpers::Show> _show;
 	const std::shared_ptr<Ui::SendButton> _send;
 	const std::unique_ptr<RecordLock> _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<Ui::RoundRect> _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<Ui::RpWidget*> parent)
+VoiceRecordButton::VoiceRecordButton(
+	not_null<Ui::RpWidget*> parent,
+	const style::RecordBar &st)
 : AbstractButton(parent)
+, _st(st)
 , _blobs(std::make_unique<Ui::Paint::Blobs>(
 	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<Ui::RpWidget*> parent);
+	VoiceRecordButton(
+		not_null<Ui::RpWidget*> parent,
+		const style::RecordBar &st);
 	~VoiceRecordButton();
 
 	enum class Type {
@@ -43,6 +47,7 @@ public:
 private:
 	void init();
 
+	const style::RecordBar &_st;
 	std::unique_ptr<Ui::Paint::Blobs> _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*> 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 <johnprestonmail@gmail.com>
Date: Thu, 18 May 2023 14:04:14 +0400
Subject: [PATCH 024/259] 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<TabbedPanel>(
 		_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 <johnprestonmail@gmail.com>
Date: Thu, 18 May 2023 17:46:24 +0400
Subject: [PATCH 025/259] 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<Ui::ScrollArea>(this, st::boxScroll))
 , _field(base::make_unique_q<Ui::InputField>(
 	this,
-	st::confirmCaptionArea,
+	st::defaultComposeFiles.caption,
 	Ui::InputField::Mode::MultiLine,
 	tr::lng_photo_caption()))
 , _emojiToggle(base::make_unique_q<Ui::EmojiButton>(
 	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<Ui::ItemSingleMediaPreview>(
 				this,
+				st::defaultComposeControls,
 				gifPaused,
 				_historyItem,
 				Ui::AttachControls::Type::EditOnly);
@@ -410,6 +411,7 @@ void EditCaptionBox::rebuildPreview() {
 		} else {
 			_content.reset(Ui::CreateChild<Ui::ItemSingleFilePreview>(
 				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<Ui::SingleFilePreview>(
 				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<PeerListRow> PublicsController::createRow(
 
 void SimpleLimitBox(
 		not_null<Ui::GenericBox*> box,
+		const style::PremiumLimits *stOverride,
 		not_null<Main::Session*> session,
 		bool premiumPossible,
 		rpl::producer<QString> 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<Ui::GenericBox*> box,
+		const style::PremiumLimits *stOverride,
 		not_null<Main::Session*> session,
 		rpl::producer<QString> title,
 		rpl::producer<TextWithEntities> 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<Ui::GenericBox*> box,
 		not_null<Main::Session*> 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<Ui::GenericBox*> box,
 		not_null<Main::Session*> 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<Ui::GenericBox*> box,
 		not_null<Main::Session*> 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<Ui::GenericBox*> box,
 	not_null<Main::Session*> session,
-	int remove);
+	int remove,
+	const style::PremiumLimits *stOverride = nullptr);
 void CaptionLimitReachedBox(
 	not_null<Ui::GenericBox*> box,
 	not_null<Main::Session*> session,
-	int remove);
+	int remove,
+	const style::PremiumLimits *stOverride = nullptr);
 void FileSizeLimitBox(
 	not_null<Ui::GenericBox*> box,
 	not_null<Main::Session*> session,
-	uint64 fileSizeBytes);
+	uint64 fileSizeBytes,
+	const style::PremiumLimits *stOverride = nullptr);
 void AccountsLimitBox(
 	not_null<Ui::GenericBox*> box,
 	not_null<Main::Session*> 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<Data::DocumentMedia> media;
-	base::weak_ptr<Window::SessionController> controller;
+	std::weak_ptr<ChatHelpers::Show> show;
 };
 
 [[nodiscard]] std::vector<Preload> &Preloads() {
@@ -168,7 +168,7 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
 
 [[nodiscard]] not_null<Ui::RpWidget*> StickerPreview(
 		not_null<Ui::RpWidget*> parent,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		const std::shared_ptr<Data::DocumentMedia> &media,
 		Fn<void()> readyCallback = nullptr) {
 	using namespace HistoryView;
@@ -194,6 +194,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
 	struct State {
 		std::unique_ptr<Lottie::SinglePlayer> lottie;
 		std::unique_ptr<Lottie::SinglePlayer> effect;
+		style::owned_color pathFg = style::owned_color(
+			QColor(255, 255, 255, 64));
 		std::unique_ptr<Ui::PathShiftGradient> pathGradient;
 		bool readyInvoked = false;
 	};
@@ -239,15 +241,17 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &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<Ui::PathShiftGradient>(
+		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<Data::DocumentMedia> &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<Data::DocumentMedia> &media) {
 
 [[nodiscard]] not_null<Ui::RpWidget*> StickersPreview(
 		not_null<Ui::RpWidget*> parent,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		Fn<void()> readyCallback) {
 	const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
 	result->show();
@@ -327,7 +331,7 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &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<State>();
 	const auto create = [=](std::shared_ptr<Data::DocumentMedia> media) {
 		const auto outer = Ui::CreateChild<Ui::RpWidget>(result);
@@ -340,7 +344,7 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
 
 		[[maybe_unused]] const auto sticker = StickerPreview(
 			outer,
-			controller,
+			show,
 			media,
 			state->singleReadyCallback);
 
@@ -520,7 +524,7 @@ struct VideoPreviewDocument {
 
 [[nodiscard]] not_null<Ui::RpWidget*> VideoPreview(
 		not_null<Ui::RpWidget*> parent,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		not_null<DocumentData*> document,
 		bool alignToBottom,
 		Fn<void()> readyCallback) {
@@ -683,7 +687,7 @@ struct VideoPreviewDocument {
 
 [[nodiscard]] not_null<Ui::RpWidget*> GenericPreview(
 		not_null<Ui::RpWidget*> parent,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		PremiumPreview section,
 		Fn<void()> readyCallback) {
 	const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
@@ -699,7 +703,7 @@ struct VideoPreviewDocument {
 		std::vector<std::shared_ptr<Data::DocumentMedia>> medias;
 		Ui::RpWidget *single = nullptr;
 	};
-	const auto session = &controller->session();
+	const auto session = &show->session();
 	const auto state = lifetime.make_state<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<Ui::RpWidget*> GenerateDefaultPreview(
 		not_null<Ui::RpWidget*> parent,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		PremiumPreview section,
 		Fn<void()> 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<Ui::GenericBox*> box,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		const Descriptor &descriptor,
 		const std::shared_ptr<Data::DocumentMedia> &media,
 		const QImage &back) {
@@ -825,7 +833,7 @@ void PreviewBox(
 	};
 	const auto state = outer->lifetime().make_state<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<Ui::GradientButton>::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<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		const Descriptor &descriptor,
 		const std::shared_ptr<Data::DocumentMedia> &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<Window::SessionController*> controller, QImage back) {
+void Show(std::shared_ptr<ChatHelpers::Show> 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<Window::SessionController*> controller, QImage back) {
 }
 
 void Show(
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> 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<Window::SessionController*> controller,
 		not_null<DocumentData*> document) {
-	Show(controller, Descriptor{
+	Show(controller->uiShow(), Descriptor{
 		.section = PremiumPreview::Stickers,
 		.requestedSticker = document,
 	});
@@ -1188,7 +1204,14 @@ void ShowPremiumPreviewBox(
 		not_null<Window::SessionController*> controller,
 		PremiumPreview section,
 		Fn<void(not_null<Ui::BoxContent*>)> shown) {
-	Show(controller, Descriptor{
+	ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown));
+}
+
+void ShowPremiumPreviewBox(
+		std::shared_ptr<ChatHelpers::Show> show,
+		PremiumPreview section,
+		Fn<void(not_null<Ui::BoxContent*>)> shown) {
+	Show(std::move(show), Descriptor{
 		.section = section,
 		.shownCallback = std::move(shown),
 	});
@@ -1198,7 +1221,7 @@ void ShowPremiumPreviewToBuy(
 		not_null<Window::SessionController*> controller,
 		PremiumPreview section,
 		Fn<void()> 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<Ui::GradientButton> 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<void(not_null<Ui::BoxContent*>)> shown = nullptr);
 
+void ShowPremiumPreviewBox(
+	std::shared_ptr<ChatHelpers::Show> show,
+	PremiumPreview section,
+	Fn<void(not_null<Ui::BoxContent*>)> shown = nullptr);
+
 void ShowPremiumPreviewToBuy(
 	not_null<Window::SessionController*> 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<PeerData*> peer) {
 SendFilesCheck DefaultCheckForPeer(
 		not_null<Window::SessionController*> controller,
 		not_null<PeerData*> peer) {
+	return DefaultCheckForPeer(controller->uiShow(), peer);
+}
+
+SendFilesCheck DefaultCheckForPeer(
+		std::shared_ptr<Ui::Show> show,
+		not_null<PeerData*> 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<QWidget*> parent,
+	const style::ComposeControls &st,
 	not_null<std::vector<Ui::PreparedFile>*> items,
 	int from,
 	int till,
@@ -170,20 +177,24 @@ SendFilesBox::Block::Block(
 	if (_isAlbum) {
 		const auto preview = Ui::CreateChild<Ui::AlbumPreview>(
 			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<Ui::SingleFilePreview>(parent.get(), first));
+			_preview.reset(Ui::CreateChild<Ui::SingleFilePreview>(
+				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<Ui::PopupMenu>(
-			top,
-			st::popupMenuExpandedSeparator);
+		const auto &tabbed = _st.tabbed;
+		const auto &icons = tabbed.icons;
+		_menu = base::make_unique_q<Ui::PopupMenu>(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<bool>(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<ChatHelpers::TabbedPanel>(
 		container,
-		_controller,
-		object_ptr<Selector>(
-			nullptr,
-			_controller->uiShow(),
-			Window::GifPauseReason::Layer,
-			Selector::Mode::EmojiOnly));
+		ChatHelpers::TabbedPanelDescriptor{
+			.ownedSelector = object_ptr<Selector>(
+				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<const QMimeData*> data) const {
 }
 
 bool SendFilesBox::addFiles(not_null<const QMimeData*> 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<bool(
 [[nodiscard]] SendFilesCheck DefaultCheckForPeer(
 	not_null<Window::SessionController*> controller,
 	not_null<PeerData*> peer);
+[[nodiscard]] SendFilesCheck DefaultCheckForPeer(
+	std::shared_ptr<Ui::Show> show,
+	not_null<PeerData*> peer);
+
+using SendFilesConfirmed = Fn<void(
+	Ui::PreparedList &&list,
+	Ui::SendFilesWay way,
+	TextWithTags &&caption,
+	Api::SendOptions options,
+	bool ctrlShiftEnter)>;
+
+struct SendFilesBoxDescriptor {
+	std::shared_ptr<ChatHelpers::Show> show;
+	Ui::PreparedList list;
+	TextWithTags caption;
+	SendFilesLimits limits = {};
+	SendFilesCheck check;
+	Api::SendType sendType = {};
+	SendMenu::Type sendMenuType = {};
+	const style::ComposeControls *stOverride = nullptr;
+	SendFilesConfirmed confirmed;
+	Fn<void()> 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<void(
-			Ui::PreparedList &&list,
-			Ui::SendFilesWay way,
-			TextWithTags &&caption,
-			Api::SendOptions options,
-			bool ctrlShiftEnter)> callback) {
+	void setConfirmedCallback(SendFilesConfirmed callback) {
 		_confirmedCallback = std::move(callback);
 	}
 	void setCancelledCallback(Fn<void()> callback) {
@@ -116,6 +139,7 @@ private:
 	public:
 		Block(
 			not_null<QWidget*> parent,
+			const style::ComposeControls &st,
 			not_null<std::vector<Ui::PreparedFile>*> items,
 			int from,
 			int till,
@@ -201,7 +225,8 @@ private:
 	void enqueueNextPrepare();
 	void addPreparedAsyncFile(Ui::PreparedFile &&file);
 
-	const not_null<Window::SessionController*> _controller;
+	const std::shared_ptr<ChatHelpers::Show> _show;
+	const style::ComposeControls &_st;
 	const Api::SendType _sendType = Api::SendType();
 
 	QString _titleText;
@@ -211,15 +236,10 @@ private:
 	std::optional<int> _removingIndex;
 
 	SendFilesLimits _limits = {};
-	SendMenu::Type _sendMenuType = SendMenu::Type();
+	SendMenu::Type _sendMenuType = {};
 
 	SendFilesCheck _check;
-	Fn<void(
-		Ui::PreparedList &&list,
-		Ui::SendFilesWay way,
-		TextWithTags &&caption,
-		Api::SendOptions options,
-		bool ctrlShiftEnter)> _confirmedCallback;
+	SendFilesConfirmed _confirmedCallback;
 	Fn<void()> _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<QWidget*> 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<TabbedSelector::InnerFooter> 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<TabbedSelector::InnerFooter> 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<StickersBox>(_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<StickersBox>(_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<TabbedSelector> 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<Ui::RpWidget*> panelContainer,
-	not_null<Window::SessionController*> controller)
+	std::shared_ptr<ChatHelpers::Show> show)
 : _stickersPanel(
 	base::make_unique_q<ChatHelpers::TabbedPanel>(
 		panelContainer,
-		controller,
-		object_ptr<ChatHelpers::TabbedSelector>(
-			nullptr,
-			controller->uiShow(),
-			Window::GifPauseReason::Layer,
-			ChatHelpers::TabbedSelector::Mode::MediaEditor))) {
+		ChatHelpers::TabbedPanelDescriptor{
+			.ownedSelector = object_ptr<ChatHelpers::TabbedSelector>(
+				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<Ui::RpWidget*> panelContainer,
-		not_null<Window::SessionController*> controller);
+		std::shared_ptr<ChatHelpers::Show> show);
 
 	[[nodiscard]] auto stickerChosen() const
 	-> rpl::producer<not_null<DocumentData*>>;
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<Image> 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<QWidget*> parent,
+	std::shared_ptr<Ui::Show> show,
+	std::shared_ptr<ChatHelpers::Show> sessionShow,
+	std::shared_ptr<Image> photo,
+	PhotoModifications modifications,
+	EditorData data)
 : RpWidget(parent)
 , _modifications(std::move(modifications))
 , _controllers(std::make_shared<Controllers>(
-	controller->sessionController()
+	sessionShow
 		? std::make_unique<StickersPanelController>(
 			this,
-			controller->sessionController())
+			std::move(sessionShow))
 		: nullptr,
 	std::make_unique<UndoController>(),
-	controller->uiShow()))
+	std::move(show)))
 , _content(base::make_unique_q<PhotoEditorContent>(
 	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<Image> photo,
 		PhotoModifications modifications,
 		EditorData data = EditorData());
+	PhotoEditor(
+		not_null<QWidget*> parent,
+		std::shared_ptr<Ui::Show> show,
+		std::shared_ptr<ChatHelpers::Show> sessionShow,
+		std::shared_ptr<Image> photo,
+		PhotoModifications modifications,
+		EditorData data = EditorData());
 
 	void save();
 	[[nodiscard]] rpl::producer<PhotoModifications> 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<QWidget*> parent,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		not_null<Ui::PreparedFile*> file,
 		int previewWidth,
 		Fn<void()> &&doneCallback) {
@@ -56,13 +56,14 @@ void OpenWithPreparedFile(
 	const auto fileImage = std::make_shared<Image>(std::move(copy));
 	auto editor = base::make_unique_q<PhotoEditor>(
 		parent,
-		&controller->window(),
+		show,
+		show,
 		fileImage,
 		image->modifications);
 	const auto raw = editor.get();
 	auto layer = std::make_unique<LayerWidget>(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<QWidget*> parent,
-	not_null<Window::SessionController*> controller,
+	std::shared_ptr<ChatHelpers::Show> show,
 	not_null<Ui::PreparedFile*> file,
 	int previewWidth,
 	Fn<void()> &&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<bool> 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<bool> 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<bool> 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<const QMimeData*> data,
+		std::optional<bool> 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<decltype(args)>(args)...);
+	};
+	auto box = Box<SendFilesBox>(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<SendingAlbum>()
+			: nullptr;
+		session().api().sendFiles(
+			std::move(group.list),
+			type,
+			base::take(caption),
+			album,
+			action);
+	}
+	finishSending();
+}
+
+bool ReplyArea::confirmSendingFiles(
+		QImage &&image,
+		QByteArray &&content,
+		std::optional<bool> 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<bool> 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<const QMimeData*> data);
+
+	void uploadFile(const QByteArray &fileContent, SendMediaType type);
+	bool confirmSendingFiles(
+		QImage &&image,
+		QByteArray &&content,
+		std::optional<bool> 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<const QMimeData*> data,
+		std::optional<bool> overrideSendImagesAsPhotos,
+		const QString &insertTextOnCancel = QString());
+	bool showSendingFilesError(const Ui::PreparedList &list) const;
+	bool showSendingFilesError(
+		const Ui::PreparedList &list,
+		std::optional<bool> 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<bool> overrideSendImagesAsPhotos);
@@ -58,6 +105,7 @@ private:
 	const std::unique_ptr<HistoryView::ComposeControls> _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<OverlayWidget*> widget) : _widget(widget) {
+	}
+
+	void activate() override {
+		if (!_widget->isHidden()) {
+			_widget->activate();
+		}
+	}
+
+	void showOrHideBoxOrLayer(
+			std::variant<
+				v::null_t,
+				object_ptr<Ui::BoxContent>,
+				std::unique_ptr<Ui::LayerWidget>> &&layer,
+			Ui::LayerOptions options,
+			anim::type animated) const override {
+		_widget->_layerBg->uiShow()->showOrHideBoxOrLayer(
+			std::move(layer),
+			options,
+			anim::type::normal);
+	}
+	not_null<QWidget*> 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<bool> adjustShadowLeft() const override {
+		return rpl::single(false);
+	}
+	SendMenu::Type sendMenuType() const override {
+		return SendMenu::Type::SilentOnly;
+	}
+
+	bool showMediaPreview(
+			Data::FileOrigin origin,
+			not_null<DocumentData*> document) const override {
+		return false; // #TODO stories
+	}
+	bool showMediaPreview(
+			Data::FileOrigin origin,
+			not_null<PhotoData*> photo) const override {
+		return false; // #TODO stories
+	}
+
+	void processChosenSticker(
+			ChatHelpers::FileChosen &&chosen) const override {
+		_widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen));
+	}
+
+private:
+	not_null<OverlayWidget*> _widget;
+
+};
+
 OverlayWidget::Streamed::Streamed(
 	not_null<DocumentData*> document,
 	Data::FileOrigin origin,
@@ -3936,81 +4017,10 @@ not_null<Ui::RpWidget*> OverlayWidget::storiesWrap() {
 }
 
 std::shared_ptr<ChatHelpers::Show> OverlayWidget::storiesShow() {
-	class Show final : public ChatHelpers::Show {
-	public:
-		explicit Show(not_null<OverlayWidget*> widget) : _widget(widget) {
-		}
-
-		void showBox(
-			object_ptr<Ui::BoxContent> 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<QWidget*> 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<bool> adjustShadowLeft() const override {
-			return rpl::single(false);
-		}
-		SendMenu::Type sendMenuType() const override {
-			return SendMenu::Type::SilentOnly;
-		}
-
-		bool showMediaPreview(
-				Data::FileOrigin origin,
-				not_null<DocumentData*> document) const override {
-			return false; // #TODO stories
-		}
-		bool showMediaPreview(
-				Data::FileOrigin origin,
-				not_null<PhotoData*> photo) const override {
-			return false; // #TODO stories
-		}
-
-		void processChosenSticker(
-				ChatHelpers::FileChosen &&chosen) const override {
-			_widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen));
-		}
-
-	private:
-		not_null<OverlayWidget*> _widget;
-
-	};
-	return std::make_shared<Show>(this);
+	if (!_cachedShow) {
+		_cachedShow = std::make_shared<Show>(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::View> _stories;
+	std::shared_ptr<Show> _cachedShow;
 	rpl::event_stream<> _storiesChanged;
 	Main::Session *_storiesSession = nullptr;
 	rpl::event_stream<ChatHelpers::FileChosen> _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<Ui::GradientButton*> CreateSubscribeButton(
 		SubscribeButtonArgs &&args) {
+	Expects(args.show || args.controller);
+
+	if (!args.show && args.controller) {
+		args.show = args.controller->uiShow();
+	}
 	const auto result = Ui::CreateChild<Ui::GradientButton>(
 		args.parent.get(),
 		args.gradientStops
@@ -1789,9 +1794,14 @@ not_null<Ui::GradientButton*> 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<Ui::GradientButton*> 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<Window::SessionController*> controller;
+	Window::SessionController *controller = nullptr;
 	not_null<Ui::RpWidget*> parent;
 	Fn<QString()> computeRef;
 	std::optional<rpl::producer<QString>> text;
 	std::optional<QGradientStops> gradientStops;
 	Fn<QString()> computeBotUrl; // nullable
+	std::shared_ptr<ChatHelpers::Show> show;
 };
 
 [[nodiscard]] not_null<Ui::GradientButton*> 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<AttachControlsWidget>(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<Ui::PopupMenu>(
 		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<Ui::PreparedFile> items,
 	SendFilesWay way)
 : RpWidget(parent)
+, _st(st)
 , _sendWay(way)
 , _dragTimer([=] { switchToDrag(); }) {
 	setMouseTracking(true);
@@ -135,6 +137,7 @@ void AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> items) {
 	_thumbs.reserve(count);
 	for (auto i = 0; i != count; ++i) {
 		_thumbs.push_back(std::make_unique<AlbumThumbnail>(
+			_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<Ui::PreparedFile> items,
 		SendFilesWay way);
 	~AlbumPreview();
@@ -86,6 +91,7 @@ private:
 
 	void showContextMenu(not_null<AlbumThumbnail*> thumb, QPoint position);
 
+	const style::ComposeControls &_st;
 	SendFilesWay _sendWay;
 	style::cursor _cursor = style::cur_default;
 	std::vector<int> _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<void()> repaint,
 	Fn<void()> editCallback,
 	Fn<void()> 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<QRect> _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<HistoryItem*> 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<HistoryItem*> 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<bool()> gifPaused,
 	not_null<HistoryItem*> 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<bool()> gifPaused,
 		not_null<HistoryItem*> 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<bool()> gifPaused,
 		const PreparedFile &file,
 		AttachControls::Type type) {
@@ -43,6 +44,7 @@ SingleMediaPreview *SingleMediaPreview::Create(
 	}
 	return CreateChild<SingleMediaPreview>(
 		parent,
+		st,
 		std::move(gifPaused),
 		preview,
 		animated,
@@ -54,6 +56,7 @@ SingleMediaPreview *SingleMediaPreview::Create(
 
 SingleMediaPreview::SingleMediaPreview(
 	QWidget *parent,
+	const style::ComposeControls &st,
 	Fn<bool()> 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<bool()> gifPaused,
 		const PreparedFile &file,
 		AttachControls::Type type = AttachControls::Type::Full);
 
 	SingleMediaPreview(
 		QWidget *parent,
+		const style::ComposeControls &st,
 		Fn<bool()> 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<Ui::RpWidget*> parent,
+		const style::PremiumLimits &st,
 		int max,
 		TextFactory textFactory,
 		int min,
 		float64 ratio);
 	Line(
 		not_null<Ui::RpWidget*> 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<Ui::RpWidget*> 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<Ui::RpWidget*> 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<Ui::VerticalLayout*> parent,
+		const style::PremiumLimits &st,
 		QString max,
 		QString min,
 		float64 ratio) {
 	parent->add(
-		object_ptr<Line>(parent, max, min, ratio),
+		object_ptr<Line>(parent, st, max, min, ratio),
 		st::boxRowPadding);
 }
 
 void AddLimitRow(
 		not_null<Ui::VerticalLayout*> parent,
+		const style::PremiumLimits &st,
 		int max,
 		std::optional<tr::phrase<lngtag_count>> 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<Ui::GenericBox*> box,
+		const style::PremiumLimits &st,
 		std::vector<ListEntry> entries) {
 
 	const auto &stLabel = st::defaultFlatLabel;
@@ -1036,6 +1048,7 @@ void ShowListBox(
 		const auto limitRow = content->add(
 			object_ptr<Line>(
 				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 <typename ...>
 struct phrase;
@@ -48,12 +52,14 @@ void AddBubbleRow(
 
 void AddLimitRow(
 	not_null<Ui::VerticalLayout*> parent,
+	const style::PremiumLimits &st,
 	QString max,
 	QString min = {},
 	float64 ratio = kLimitRowRatio);
 
 void AddLimitRow(
 	not_null<Ui::VerticalLayout*> parent,
+	const style::PremiumLimits &st,
 	int max,
 	std::optional<tr::phrase<lngtag_count>> phrase,
 	int min = 0,
@@ -90,6 +96,7 @@ struct ListEntry final {
 };
 void ShowListBox(
 	not_null<Ui::GenericBox*> box,
+	const style::PremiumLimits &st,
 	std::vector<ListEntry> 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<SessionController*> controller);
 
+	void activate() override;
+
 	void showOrHideBoxOrLayer(
 		std::variant<
 			v::null_t,
@@ -151,6 +153,12 @@ MainWindowShow::MainWindowShow(not_null<SessionController*> 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<SessionController*> 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 <johnprestonmail@gmail.com>
Date: Mon, 22 May 2023 19:59:16 +0400
Subject: [PATCH 026/259] 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<HistoryItem*> 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<UserData*> user;
 	std::vector<StoryItem> 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<Session*> owner);
 	~Stories();
 
+	[[nodiscard]] Session &owner() const;
+
 	void loadMore();
 	void apply(const MTPDupdateStories &data);
 
 	[[nodiscard]] const std::vector<StoriesList> &all();
 	[[nodiscard]] bool allLoaded() const;
+	[[nodiscard]] rpl::producer<> allChanged() const;
 
 	// #TODO stories testing
 	[[nodiscard]] StoryId generate(
@@ -76,7 +83,7 @@ public:
 			not_null<DocumentData*>> media);
 
 private:
-	[[nodiscard]] StoriesList parse(const MTPUserStories &data);
+	[[nodiscard]] StoriesList parse(const MTPUserStories &stories);
 	[[nodiscard]] std::optional<StoryItem> parse(const MTPDstoryItem &data);
 
 	void pushToBack(StoriesList &&list);
@@ -85,6 +92,7 @@ private:
 	const not_null<Session*> _owner;
 
 	std::vector<StoriesList> _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> childListShown)
 : RpWidget(parent)
 , _controller(controller)
+, _stories(std::make_unique<Stories::List>(
+	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<Data::Folder*> folder);
 	void refreshWithCollapsedRows(bool toTop = false);
@@ -309,7 +314,9 @@ private:
 	void fillArchiveSearchMenu(not_null<Ui::PopupMenu*> 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<Window::SessionController*> _controller;
 
+	const std::unique_ptr<Stories::List> _stories;
+
 	not_null<IndexedList*> _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<PeerData*> peer);
+
+	QImage image(int size) override;
+	void subscribeToUpdates(Fn<void()> callback) override;
+
+private:
+	struct Subscribed {
+		explicit Subscribed(Fn<void()> callback)
+		: callback(std::move(callback)) {
+		}
+
+		Ui::PeerUserpicView view;
+		Fn<void()> callback;
+		InMemoryKey key;
+		rpl::lifetime photoLifetime;
+		rpl::lifetime downloadLifetime;
+	};
+
+	[[nodiscard]] bool waitingUserpicLoad() const;
+	void processNewPhoto();
+
+	const not_null<PeerData*> _peer;
+	QImage _frame;
+	std::unique_ptr<Subscribed> _subscribed;
+
+};
+
+class State final {
+public:
+	explicit State(not_null<Data::Stories*> data);
+
+	[[nodiscard]] Content next();
+
+private:
+	const not_null<Data::Stories*> _data;
+	base::flat_map<not_null<UserData*>, std::shared_ptr<Userpic>> _userpics;
+
+};
+
+PeerUserpic::PeerUserpic(not_null<PeerData*> 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<void()> callback) {
+	if (!callback) {
+		_subscribed = nullptr;
+		return;
+	}
+	_subscribed = std::make_unique<Subscribed>(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::Stories*> 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<Userpic>();
+		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<Userpic>();
+		if (const auto i = _userpics.find(user); i != end(_userpics)) {
+			userpic = i->second;
+		} else {
+			userpic = std::make_shared<PeerUserpic>(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<Content> ContentForSession(not_null<Main::Session*> session) {
+	return [=](auto consumer) {
+		auto result = rpl::lifetime();
+		const auto stories = &session->data().stories();
+		const auto state = result.make_state<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<Content> ContentForSession(
+	not_null<Main::Session*> 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<QWidget*> parent,
+	rpl::producer<Content> content,
+	Fn<int()> 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<uint64> 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*> 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<void()> callback) = 0;
+};
+
+struct User {
+	uint64 id = 0;
+	QString name;
+	std::shared_ptr<Userpic> userpic;
+	bool unread = false;
+
+	friend inline bool operator==(const User &a, const User &b) = default;
+};
+
+struct Content {
+	std::vector<User> users;
+
+	friend inline bool operator==(
+		const Content &a,
+		const Content &b) = default;
+};
+
+class List final : public Ui::RpWidget {
+public:
+	List(
+		not_null<QWidget*> parent,
+		rpl::producer<Content> content,
+		Fn<int()> shownHeight);
+
+	[[nodiscard]] rpl::producer<uint64> 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<Item> _items;
+	Fn<int()> _shownHeight = 0;
+	rpl::event_stream<uint64> _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 <johnprestonmail@gmail.com>
Date: Tue, 23 May 2023 20:13:02 +0400
Subject: [PATCH 027/259] 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<Stories::List>(
 	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<MTPPeer> &my,
 		const QVector<MTPPeer> &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*> row) const;
 	void setupOnlineStatusCheck();
+	void jumpToTop();
 
 	void updateRowCornerStatusShown(not_null<History*> history);
 	void repaintDialogRowCornerStatus(not_null<History*> history);
@@ -402,6 +407,7 @@ private:
 	const not_null<Window::SessionController*> _controller;
 
 	const std::unique_ptr<Stories::List> _stories;
+	int _viewportHeight = 0;
 
 	not_null<IndexedList*> _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<QEvent*> 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<Content> ContentForSession(not_null<Main::Session*> 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 <QtWidgets/QApplication>
+
 namespace Dialogs::Stories {
 namespace {
 
@@ -24,19 +26,31 @@ List::List(
 	Fn<int()> 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<uint64> 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<Item> _items;
+	std::vector<Item> _hidingItems;
 	Fn<int()> _shownHeight = 0;
 	rpl::event_stream<uint64> _clicks;
 	rpl::event_stream<> _expandRequests;
+	rpl::event_stream<> _entered;
+
+	Ui::Animations::Simple _shownAnimation;
+
+	QPoint _lastMousePosition;
+	std::optional<QPoint> _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<Data::Folder*> 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 <johnprestonmail@gmail.com>
Date: Tue, 23 May 2023 21:11:58 +0400
Subject: [PATCH 028/259] 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*> 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*> item) {
+	if (!item->subscribed) {
+		item->subscribed = true;
+		//const auto id = item.user.id;
+		item->user.userpic->subscribeToUpdates([=] {
+			update();
+		});
+	}
+}
+
+void List::validateName(not_null<Item*> 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*> item);
+	void validateName(not_null<Item*> item);
 	void updateScrollMax();
 	void checkDragging();
 	bool finishDragging();

From d57ada8a6418d435bfd0bccffdd2d2cd762319b7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 23 May 2023 22:54:13 +0400
Subject: [PATCH 029/259] 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*> 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<Item> 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*> item);
 	void validateName(not_null<Item*> 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<Item> _items;
-	std::vector<Item> _hidingItems;
+	Data _data;
+	Data _hidingData;
 	Fn<int()> _shownHeight = 0;
 	rpl::event_stream<uint64> _clicks;
 	rpl::event_stream<> _expandRequests;

From 455cb0d21bd3b72eb8d1c1735bc6e795970dd20a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 19 May 2023 21:36:54 +0400
Subject: [PATCH 030/259] 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<Ui::RpWidget*> parent,
 	const style::RecordBar &st)
 : AbstractButton(parent)
-, _st(st)
 , _blobs(std::make_unique<Ui::Paint::Blobs>(
 	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<Ui::Paint::Blobs> _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 <johnprestonmail@gmail.com>
Date: Thu, 25 May 2023 13:32:13 +0400
Subject: [PATCH 031/259] 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<MessageViews> chats:Vector<Chat> use
 messages.discussionMessage#a6341782 flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector<Chat> users:Vector<User> = 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<Peer> 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<bytes> date:int = Mess
 
 sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage;
 
-storyViews#518b47d8 recent_viewers:Vector<long> views_count:int = StoryViews;
+storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector<long> = 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
 
 userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = UserStories;
 
 stories.allStoriesNotModified#47e0a07e state:string = stories.AllStories;
-stories.allStories#5b1aa68c flags:# has_more:flags.0?true state:string user_stories:Vector<UserStories> users:Vector<User> = stories.AllStories;
+stories.allStories#839e0428 flags:# has_more:flags.0?true count:int state:string user_stories:Vector<UserStories> users:Vector<User> = stories.AllStories;
 
 stories.stories#4fe57df1 count:int stories:Vector<StoryItem> users:Vector<User> = stories.Stories;
 
+stories.userStories#37a6ff5f stories:UserStories users:Vector<User> = stories.UserStories;
+
 storyView#a71aacc2 user_id:long date:int = StoryView;
 
 stories.storyViewsList#fb3f77ac count:int views:Vector<StoryView> users:Vector<User> = stories.StoryViewsList;
 
 stories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = 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<int> = messages.AffectedMessages;
 messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;
 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<MessageEntity> 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<MessageEntity> 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<MessageEntity> 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<MessageEntity> 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<int> random_id:Vector<long> 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<InputBotInlineResult> 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<MessageEntity> 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<MessageEntity> = 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<ShippingOption> = 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<InputSingleMedia> 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<InputSingleMedia> 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<MessageRange>;
@@ -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<Peer>;
 chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = Updates;
 
-stories.sendStory#8e826f5d flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> = Updates;
+stories.sendStory#8b5c6986 flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> random_id:long = Updates;
+stories.editStory#2aae7a41 flags:# id:int media:flags.0?InputMedia caption:flags.1?string entities:flags.1?Vector<MessageEntity> privacy_rules:flags.2?Vector<InputPrivacyRule> = Updates;
 stories.deleteStories#b5d501d7 id:Vector<int> = Vector<int>;
-stories.editStoryPrivacy#78981875 id:int privacy_rules:Vector<InputPrivacyRule> = Updates;
 stories.togglePinned#51602944 id:Vector<int> pinned:Bool = Vector<int>;
 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<int> = 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<int> = stories.Stories;
 stories.readStories#edc5105b user_id:InputUser max_id:int = Vector<int>;
+stories.incrementStoryViews#22126127 user_id:InputUser id:Vector<int> = Bool;
 stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList;
 stories.getStoriesViews#9a75d6a6 id:Vector<int> = 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*> 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<MTPVideoSize> 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<MTPmessages_SendMedia>(
 			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<MTPmessages_SendMedia>(
 				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<MTPmessages_SendMedia>(
 			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<MTPmessages_SendMessage>(
 				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<uint64>();
-	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<MTPmessages_SendInlineBotResult>(
 			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<MTPmessages_SendMedia>(
 			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<SendingAlbum*> 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<SendingAlbum*> album) {
 	histories.sendPreparedMessage(
 		history,
 		replyTo,
-		topicRootId,
 		uint64(0), // randomId
 		Data::Histories::PrepareMessage<MTPmessages_SendMultiMedia>(
 			MTP_flags(flags),
 			peer->input,
 			Data::Histories::ReplyToPlaceholder(),
-			Data::Histories::TopicRootPlaceholder(),
 			MTP_vector<MTPInputSingleMedia>(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<HistoryItem*> 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<Session*> 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<Session*> owner)
 : _owner(owner)
 , _readRequestsTimer([=] { sendReadRequests(); }) {
@@ -885,23 +909,24 @@ bool Histories::isCreatingTopic(
 
 int Histories::sendPreparedMessage(
 		not_null<History*> history,
-		MsgId replyTo,
-		MsgId topicRootId,
+		FullReplyTo replyTo,
 		uint64 randomId,
-		Fn<PreparedMessage(MsgId replyTo, MsgId topicRootId)> message,
+		Fn<PreparedMessage(not_null<Session*>, FullReplyTo)> message,
 		Fn<void(const MTPUpdates&, const MTP::Response&)> done,
 		Fn<void(const MTP::Error&, const MTP::Response&)> 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<void()> 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*> 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*> 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<Session*> owner,
+	FullReplyTo replyTo);
+
 class Histories final {
 public:
 	enum class RequestType : uchar {
@@ -102,29 +106,27 @@ public:
 		MTPmessages_SendMultiMedia>;
 	int sendPreparedMessage(
 		not_null<History*> history,
-		MsgId replyTo,
-		MsgId topicRootId,
+		FullReplyTo replyTo,
 		uint64 randomId,
-		Fn<PreparedMessage(MsgId replyTo, MsgId topicRootId)> message,
+		Fn<PreparedMessage(not_null<Session*>, FullReplyTo)> message,
 		Fn<void(const MTPUpdates&, const MTP::Response&)> done,
 		Fn<void(const MTP::Error&, const MTP::Response&)> fail);
 
 	struct ReplyToPlaceholder {
 	};
-	struct TopicRootPlaceholder {
-	};
 	template <typename RequestType, typename ...Args>
-	static Fn<Histories::PreparedMessage(MsgId, MsgId)> PrepareMessage(
-			const Args &...args) {
-		return [=](MsgId replyTo, MsgId topicRootId) -> RequestType {
-			return { ReplaceReplyIds(args, replyTo, topicRootId)... };
+	static auto PrepareMessage(const Args &...args)
+	-> Fn<Histories::PreparedMessage(not_null<Session*>, FullReplyTo)> {
+		return [=](not_null<Session*> owner, FullReplyTo replyTo)
+		-> RequestType {
+			return { ReplaceReplyIds(owner, args, replyTo)... };
 		};
 	}
 
 	void checkTopicCreated(FullMsgId rootId, MsgId realRoot);
-	[[nodiscard]] MsgId convertTopicReplyTo(
+	[[nodiscard]] MsgId convertTopicReplyToId(
 		not_null<History*> history,
-		MsgId replyTo) const;
+		MsgId replyToId) const;
 
 private:
 	struct PostponedHistoryRequest {
@@ -151,7 +153,7 @@ private:
 	struct DelayedByTopicMessage {
 		uint64 randomId = 0;
 		MsgId replyTo = 0;
-		Fn<PreparedMessage(MsgId replyTo, MsgId topicRootId)> message;
+		Fn<PreparedMessage(not_null<Session*>, FullReplyTo)> message;
 		Fn<void(const MTPUpdates&, const MTP::Response&)> done;
 		Fn<void(const MTP::Error&, const MTP::Response&)> fail;
 		int requestId = 0;
@@ -166,11 +168,12 @@ private:
 	};
 
 	template <typename Arg>
-	static auto ReplaceReplyIds(Arg arg, MsgId replyTo, MsgId topicRootId) {
+	static auto ReplaceReplyIds(
+			not_null<Session*> owner,
+			Arg arg,
+			FullReplyTo replyTo) {
 		if constexpr (std::is_same_v<Arg, ReplyToPlaceholder>) {
-			return MTP_int(replyTo);
-		} else if constexpr (std::is_same_v<Arg, TopicRootPlaceholder>) {
-			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<Session*> 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<HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> 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*> history,
 	MsgId id,
 	MessageFlags flags,
-	MsgId replyTo,
+	FullReplyTo replyTo,
 	UserId viaBotId,
 	TimeId date,
 	PeerId from,
@@ -541,7 +542,7 @@ HistoryItem::HistoryItem(
 	not_null<History*> history,
 	MsgId id,
 	MessageFlags flags,
-	MsgId replyTo,
+	FullReplyTo replyTo,
 	UserId viaBotId,
 	TimeId date,
 	PeerId from,
@@ -576,7 +577,7 @@ HistoryItem::HistoryItem(
 	not_null<History*> history,
 	MsgId id,
 	MessageFlags flags,
-	MsgId replyTo,
+	FullReplyTo replyTo,
 	UserId viaBotId,
 	TimeId date,
 	PeerId from,
@@ -606,7 +607,7 @@ HistoryItem::HistoryItem(
 	not_null<History*> 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<HistoryMessageReply>()) {
+		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<ClickHandlerPtr> &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<HistoryServiceTopicInfo>())
@@ -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*> history,
 		MsgId id,
 		MessageFlags flags,
-		MsgId replyTo,
+		FullReplyTo replyTo,
 		UserId viaBotId,
 		TimeId date,
 		PeerId from,
@@ -147,7 +147,7 @@ public:
 		not_null<History*> history,
 		MsgId id,
 		MessageFlags flags,
-		MsgId replyTo,
+		FullReplyTo replyTo,
 		UserId viaBotId,
 		TimeId date,
 		PeerId from,
@@ -159,7 +159,7 @@ public:
 		not_null<History*> history,
 		MsgId id,
 		MessageFlags flags,
-		MsgId replyTo,
+		FullReplyTo replyTo,
 		UserId viaBotId,
 		TimeId date,
 		PeerId from,
@@ -171,7 +171,7 @@ public:
 		not_null<History*> 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<PeerData*> 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*> 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<Data::StoriesList> _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<ChatHelpers::Show> = 0;
 	[[nodiscard]] virtual auto storiesStickerOrEmojiChosen()
 		-> rpl::producer<ChatHelpers::FileChosen> = 0;
-	virtual void storiesJumpTo(Data::FullStoryId id) = 0;
+	virtual void storiesJumpTo(
+		not_null<Main::Session*> 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*> 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<PeerData*> 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<PeerData*> peer() const;
 	[[nodiscard]] bool shows(const Data::StoriesList &list) const;
 
 	[[nodiscard]] SiblingView view(
@@ -51,7 +52,8 @@ private:
 
 	const not_null<Controller*> _controller;
 
-	Data::FullStoryId _id;
+	FullStoryId _id;
+	not_null<PeerData*> _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<Main::Session*> 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<ChatHelpers::Show> storiesShow() override;
 	auto storiesStickerOrEmojiChosen()
 		-> rpl::producer<ChatHelpers::FileChosen> override;
-	void storiesJumpTo(Data::FullStoryId id) override;
+	void storiesJumpTo(
+		not_null<Main::Session*> 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<FileLoadResult>(
 		TaskId(),
 		file.uploadData->fileId,
-		FileLoadTo(PeerId(), Api::SendOptions(), MsgId(), MsgId(), MsgId()),
+		FileLoadTo(PeerId(), Api::SendOptions(), FullReplyTo(), MsgId()),
 		TextWithTags(),
 		false,
 		std::shared_ptr<SendingAlbum>(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<PhotoId>())
-, file(file)
-, peer(peer)
-, type(type)
-, replyTo(replyTo) {
-}
-
-SendMediaPrepare::SendMediaPrepare(
-	const QImage &img,
-	const PeerId &peer,
-	SendMediaType type,
-	MsgId replyTo)
-: id(base::RandomValue<PhotoId>())
-, img(img)
-, peer(peer)
-, type(type)
-, replyTo(replyTo) {
-}
-
-SendMediaPrepare::SendMediaPrepare(
-	const QByteArray &data,
-	const PeerId &peer,
-	SendMediaType type,
-	MsgId replyTo)
-: id(base::RandomValue<PhotoId>())
-, 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<PhotoId>())
-, 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<SendMediaPrepare>;
-
 using UploadFileParts =  QMap<int, QByteArray>;
 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<HistoryView::ElementDelegate*> delegate,
 		not_null<History*> 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<void()> 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<MTPmessages_SendMedia>(
 			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 <johnprestonmail@gmail.com>
Date: Thu, 25 May 2023 14:56:32 +0400
Subject: [PATCH 032/259] 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 <johnprestonmail@gmail.com>
Date: Fri, 26 May 2023 11:21:19 +0400
Subject: [PATCH 033/259] 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<PeerData*> 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<PeerData*> 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<not_null<PhotoData*>>(&_media.data);
+	return result ? result->get() : nullptr;
+}
+
+DocumentData *Story::document() const {
+	const auto result = std::get_if<not_null<DocumentData*>>(&_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<Session*> 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<StoryItem> Stories::parse(const MTPDstoryItem &data) {
+Story *Stories::parse(not_null<PeerData*> 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<PhotoData*>, not_null<DocumentData*>>>;
 	const auto media = data.vmedia().match([&](
@@ -95,24 +178,15 @@ std::optional<StoryItem> 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<Story>(
+		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<HistoryItem*> item,
-	std::variant<
-		v::null_t,
-		not_null<PhotoData*>,
-		not_null<DocumentData*>> media) {
-	if (v::is_null(media)
-		|| !item->from()->isUser()
-		|| !item->isRegular()) {
-		return {};
+base::expected<not_null<Story*>, 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<not_null<DocumentData*>>(media)
-		? v::get<not_null<DocumentData*>>(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<StoriesList>();
-	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<not_null<PhotoData*>>(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<void()> 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<PhotoData*>, not_null<DocumentData*>> 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<PeerData*> peer,
+		StoryMedia media,
+		TimeId date);
+
+	[[nodiscard]] Session &owner() const;
+	[[nodiscard]] Main::Session &session() const;
+	[[nodiscard]] not_null<PeerData*> 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<PeerData*> _peer;
+	const StoryMedia _media;
+	TextWithEntities _caption;
+	const TimeId _date = 0;
+	bool _pinned = false;
 
-	friend inline bool operator==(StoryItem, StoryItem) = default;
 };
 
 struct StoriesList {
 	not_null<UserData*> user;
-	std::vector<StoryItem> items;
+	std::vector<StoryId> 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<Session*> owner);
@@ -60,22 +92,24 @@ public:
 	[[nodiscard]] bool allLoaded() const;
 	[[nodiscard]] rpl::producer<> allChanged() const;
 
-	// #TODO stories testing
-	[[nodiscard]] StoryId generate(
-		not_null<HistoryItem*> item,
-		std::variant<
-			v::null_t,
-			not_null<PhotoData*>,
-			not_null<DocumentData*>> media);
+	[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
+		FullStoryId id) const;
+	void resolve(FullStoryId id, Fn<void()> done);
 
 private:
 	[[nodiscard]] StoriesList parse(const MTPUserStories &stories);
-	[[nodiscard]] std::optional<StoryItem> parse(const MTPDstoryItem &data);
+	[[nodiscard]] Story *parse(
+		not_null<PeerData*> peer,
+		const MTPDstoryItem &data);
 
 	void pushToBack(StoriesList &&list);
 	void pushToFront(StoriesList &&list);
 
 	const not_null<Session*> _owner;
+	base::flat_map<
+		PeerId,
+		base::flat_map<StoryId, std::unique_ptr<Story>>> _stories;
+	base::flat_set<FullStoryId> _deleted;
 
 	std::vector<StoriesList> _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::Stories*> 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<Userpic>();
 		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<Userpic>();
+#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<Content> ContentForSession(not_null<Main::Session*> 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<QWidget*> parent,
 	rpl::producer<Content> 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*> 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*> 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<not_null<PhotoData*>>(item.media.data)) {
+		if (story->photo()) {
 			_photoPlayback = std::make_unique<PhotoPlayback>(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> &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<Sibling>(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*> 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<not_null<DocumentData*>>(&data)) {
-		_loader = std::make_unique<LoaderVideo>((*video), origin, [=] {
-			check();
-		});
-	} else if (const auto photo = std::get_if<not_null<PhotoData*>>(&data)) {
-		_loader = std::make_unique<LoaderPhoto>((*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<PhotoData*> photo) {
+		_loader = std::make_unique<LoaderPhoto>(photo, origin, [=] {
+			check();
+		});
+	}, [&](not_null<DocumentData*> document) {
+		_loader = std::make_unique<LoaderVideo>(document, origin, [=] {
+			check();
+		});
+	});
+	_blurred = _loader->blurred();
+	check();
+}
+
 FullStoryId Sibling::shownId() const {
 	return _id;
 }
@@ -249,9 +267,9 @@ not_null<PeerData*> 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*> 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<Data::Story*> 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<Data::CloudTheme> 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<not_null<PhotoData*>>(&data)) {
-		displayPhoto(*photo, activation);
-	} else {
-		displayDocument(v::get<not_null<DocumentData*>>(data), activation);
-	}
+	v::match(story->media().data, [&](not_null<PhotoData*> photo) {
+		displayPhoto(photo, anim::activation::background);
+	}, [&](not_null<DocumentData*> 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<not_null<PeerData*>>(&context)) {
 		_peer = *peer;
 		_history = _peer->owner().history(_peer);
 		_message = nullptr;
 		_topicRootId = MsgId();
-		setStoriesUser(nullptr);
+		setStoriesPeer(nullptr);
 	} else if (const auto story = std::get_if<StoriesContext>(&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<UserData*> user;
+		not_null<PeerData*> peer;
 		StoryId id = 0;
 	};
 	void setContext(std::variant<
@@ -311,7 +311,7 @@ private:
 		ItemContext,
 		not_null<PeerData*>,
 		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<Ui::ChatTheme*> theme;
 		int visibleAreaTop = 0;

From b195ec4fd5832a6bf57aef33915c78ff0da52d51 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 May 2023 12:45:50 +0400
Subject: [PATCH 034/259] 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<MTPint>(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<Layout> 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<QRect> 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<SharedMediaKey> {
 }
 
 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 <johnprestonmail@gmail.com>
Date: Fri, 26 May 2023 13:27:34 +0400
Subject: [PATCH 035/259] 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<Dialogs::Entry*> entry) {
 	_entryChanges.drop(entry);
 }
 
+void Changes::storyUpdated(
+		not_null<Story*> story,
+		StoryUpdate::Flags flags) {
+	const auto drop = (flags & StoryUpdate::Flag::Destroyed);
+	_storyChanges.updated(story, flags, drop);
+	if (!drop) {
+		scheduleNotifications();
+	}
+}
+
+rpl::producer<StoryUpdate> Changes::storyUpdates(
+		StoryUpdate::Flags flags) const {
+	return _storyChanges.updates(flags);
+}
+
+rpl::producer<StoryUpdate> Changes::storyUpdates(
+		not_null<Story*> story,
+		StoryUpdate::Flags flags) const {
+	return _storyChanges.updates(story, flags);
+}
+
+rpl::producer<StoryUpdate> Changes::storyFlagsValue(
+		not_null<Story*> story,
+		StoryUpdate::Flags flags) const {
+	return _storyChanges.flagsValue(story, flags);
+}
+
+rpl::producer<StoryUpdate> 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<Flag>;
+	friend inline constexpr auto is_flag_type(Flag) { return true; }
+
+	not_null<Story*> story;
+	Flags flags = 0;
+
+};
+
 class Changes final {
 public:
 	explicit Changes(not_null<Main::Session*> session);
@@ -298,6 +317,20 @@ public:
 		EntryUpdate::Flag flag) const;
 	void entryRemoved(not_null<Dialogs::Entry*> entry);
 
+	void storyUpdated(
+		not_null<Story*> story,
+		StoryUpdate::Flags flags);
+	[[nodiscard]] rpl::producer<StoryUpdate> storyUpdates(
+		StoryUpdate::Flags flags) const;
+	[[nodiscard]] rpl::producer<StoryUpdate> storyUpdates(
+		not_null<Story*> story,
+		StoryUpdate::Flags flags) const;
+	[[nodiscard]] rpl::producer<StoryUpdate> storyFlagsValue(
+		not_null<Story*> story,
+		StoryUpdate::Flags flags) const;
+	[[nodiscard]] rpl::producer<StoryUpdate> realtimeStoryUpdates(
+		StoryUpdate::Flag flag) const;
+
 	void sendNotifications();
 
 private:
@@ -348,6 +381,7 @@ private:
 	Manager<ForumTopic, TopicUpdate> _topicChanges;
 	Manager<HistoryItem, MessageUpdate> _messageChanges;
 	Manager<Dialogs::Entry, EntryUpdate> _entryChanges;
+	Manager<Story, StoryUpdate> _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<PhotoData*> photo) {
+		return !photo->isNull();
+	}, [](not_null<DocumentData*> document) {
+		return document->hasThumbnail();
+	});
+}
+
+Image *Story::replyPreview() const {
+	return v::match(_media.data, [&](not_null<PhotoData*> photo) {
+		return photo->getReplyPreview(
+			Data::FileOriginStory(_peer->id, _id),
+			_peer,
+			false);
+	}, [&](not_null<DocumentData*> 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<PeerData*> peer, const MTPDstoryItem &data) {
 	return result;
 }
 
+void Stories::updateDependentMessages(not_null<Data::Story*> 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<HistoryItem*> dependent,
+		not_null<Data::Story*> dependency) {
+	_dependentMessages[dependency].emplace(dependent);
+}
+
+void Stories::unregisterDependentMessage(
+		not_null<HistoryItem*> dependent,
+		not_null<Data::Story*> 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<MTPint> ids;
+		std::vector<Fn<void()>> callbacks;
+	};
+	auto leftToSend = kMaxResolveTogether;
+	auto byPeer = base::flat_map<PeerId, Prepared>();
+	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<MTPint>(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<PeerData*> peer,
+		const QVector<MTPStoryItem> &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<StoriesList> &Stories::all() {
 	return _all;
 }
@@ -242,6 +436,21 @@ base::expected<not_null<Story*>, NoStory> Stories::lookup(
 }
 
 void Stories::resolve(FullStoryId id, Fn<void()> 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<PeerData*> 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<PeerData*> _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<Data::Story*> story);
+	void registerDependentMessage(
+		not_null<HistoryItem*> dependent,
+		not_null<Data::Story*> dependency);
+	void unregisterDependentMessage(
+		not_null<HistoryItem*> dependent,
+		not_null<Data::Story*> dependency);
 
 	void loadMore();
 	void apply(const MTPDupdateStories &data);
@@ -101,9 +116,15 @@ private:
 	[[nodiscard]] Story *parse(
 		not_null<PeerData*> peer,
 		const MTPDstoryItem &data);
+	void processResolvedStories(
+		not_null<PeerData*> peer,
+		const QVector<MTPStoryItem> &list);
+	void sendResolveRequests();
+	void finalizeResolve(FullStoryId id);
 
 	void pushToBack(StoriesList &&list);
 	void pushToFront(StoriesList &&list);
+	void applyDeleted(FullStoryId id);
 
 	const not_null<Session*> _owner;
 	base::flat_map<
@@ -111,6 +132,15 @@ private:
 		base::flat_map<StoryId, std::unique_ptr<Story>>> _stories;
 	base::flat_set<FullStoryId> _deleted;
 
+	base::flat_map<
+		PeerId,
+		base::flat_map<StoryId, std::vector<Fn<void()>>>> _resolves;
+	base::flat_map<mtpRequestId, std::vector<Fn<void()>>> _resolveRequests;
+
+	std::map<
+		not_null<Data::Story*>,
+		base::flat_set<not_null<HistoryItem*>>> _dependentMessages;
+
 	std::vector<StoriesList> _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<HistoryMessageVia>()) {
@@ -3518,7 +3527,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
 				dependent->topicPost = data.is_forum_topic()
 					|| Has<HistoryServiceTopicInfo>();
 				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<HistoryMessageForwarded>()) {
+		if (replyToMsg && !replyToMsg->Has<HistoryMessageForwarded>()) {
 			if (auto bot = replyToMsg->viaBot()) {
 				replyToVia = std::make_unique<HistoryMessageVia>();
 				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<HistoryItem*> 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<HistoryItem*> holder) const {
-	if (!replyToMsg) {
+	if (replyToStory) {
+		return replyToFromName(replyToStory->peer());
+	} else if (!replyToMsg) {
 		return QString();
 	} else if (holder->Has<HistoryMessageForwarded>()) {
 		if (const auto fwd = replyToMsg->Get<HistoryMessageForwarded>()) {
@@ -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, HistoryItem> {
 	HistoryMessageReply() = default;
@@ -236,6 +275,7 @@ struct HistoryMessageReply
 	[[nodiscard]] ClickHandlerPtr replyToLink() const {
 		return replyToLnk;
 	}
+	[[nodiscard]] QString statePhrase() const;
 	void setReplyToLinkFrom(not_null<HistoryItem*> holder);
 
 	void refreshReplyToMedia();
@@ -249,6 +289,7 @@ struct HistoryMessageReply
 	DocumentId replyToDocumentId = 0;
 	WebPageId replyToWebPageId = 0;
 	ReplyToMessagePointer replyToMsg;
+	ReplyToStoryPointer replyToStory;
 	std::unique_ptr<HistoryMessageVia> replyToVia;
 	std::unique_ptr<Ui::SpoilerAnimation> 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<HistoryItem*> item,
 		PeerId peerId,
 		MsgId msgId) {
@@ -153,6 +154,23 @@ void RequestDependentMessageData(
 		done);
 }
 
+void RequestDependentMessageStory(
+		not_null<HistoryItem*> 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<PeerData*> peer) {
 	return MessageFlag::BeingSent
 		| (peer->isSelf() ? MessageFlag() : MessageFlag::Outgoing);
@@ -266,6 +284,24 @@ ClickHandlerPtr JumpToMessageClickHandler(
 	});
 }
 
+ClickHandlerPtr JumpToStoryClickHandler(not_null<Data::Story*> story) {
+	return JumpToStoryClickHandler(story->peer(), story->id());
+}
+
+ClickHandlerPtr JumpToStoryClickHandler(
+		not_null<PeerData*> peer,
+		StoryId storyId) {
+	return std::make_shared<LambdaClickHandler>([=] {
+		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<HistoryItem*> item,
 	PeerId peerId,
 	MsgId msgId);
+void RequestDependentMessageStory(
+	not_null<HistoryItem*> item,
+	PeerId peerId,
+	StoryId storyId);
 [[nodiscard]] MessageFlags NewMessageFlags(not_null<PeerData*> peer);
 [[nodiscard]] bool ShouldSendSilent(
 	not_null<PeerData*> peer,
@@ -115,6 +120,11 @@ struct SendingErrorRequest {
 [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler(
 	not_null<HistoryItem*> item,
 	FullMsgId returnToId = FullMsgId());
+[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler(
+	not_null<Data::Story*> story);
+ClickHandlerPtr JumpToStoryClickHandler(
+	not_null<PeerData*> peer,
+	StoryId storyId);
 
 [[nodiscard]] not_null<HistoryItem*> GenerateJoinedMessage(
 	not_null<History*> 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<base::PowerSaveBlocker> _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<PeerData*> 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<PeerData*> peer, StoryId storyId);
 	void openPeerStories(PeerId peerId);
 
 	struct PaintContextArgs {

From 9f548b523e6cb4e52b1bc937bb0fa305a58d048c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 May 2023 18:48:33 +0400
Subject: [PATCH 036/259] 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*> 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<StoriesList> &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*> story);
 
 	const not_null<Session*> _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<HistoryItem*> dependency) {
 	}
 }
 
+void HistoryItem::dependencyStoryRemoved(
+		not_null<Data::Story*> dependency) {
+	if (const auto reply = Get<HistoryMessageReply>()) {
+		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<HistoryMessageReply>()) {
 		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<HistoryItem*> dependency);
+	void dependencyStoryRemoved(not_null<Data::Story*> 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<HistoryItem*> 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<HistoryItem*> holder,
+		not_null<HistoryItem*> removed) {
 	if (replyToMsg.get() == removed) {
 		clearData(holder);
 		holder->history()->owner().requestItemResize(holder);
 	}
 }
 
+void HistoryMessageReply::storyRemoved(
+		not_null<HistoryItem*> holder,
+		not_null<Data::Story*> removed) {
+	if (replyToStory.get() == removed) {
+		clearData(holder);
+		holder->history()->owner().requestItemResize(holder);
+	}
+}
+
 void HistoryMessageReply::paint(
 		Painter &p,
 		not_null<const HistoryView::Element*> 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<HistoryItem*> holder) const;
 	void updateName(not_null<HistoryItem*> holder) const;
 	void resize(int width) const;
-	void itemRemoved(HistoryItem *holder, HistoryItem *removed);
+	void itemRemoved(
+		not_null<HistoryItem*> holder,
+		not_null<HistoryItem*> removed);
+	void storyRemoved(
+		not_null<HistoryItem*> holder,
+		not_null<Data::Story*> removed);
 
 	void paint(
 		Painter &p,

From 0edbb91b72b056dca43517677fa5f4d46b1998a7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 May 2023 19:18:10 +0400
Subject: [PATCH 037/259] 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<StoryMedia> ParseMedia(
+		not_null<Session*> owner,
+		const MTPMessageMedia &media) {
+	return media.match([&](const MTPDmessageMediaPhoto &data)
+	-> std::optional<StoryMedia> {
+		if (const auto photo = data.vphoto()) {
+			const auto result = owner->processPhoto(*photo);
+			if (!result->isNull()) {
+				return StoryMedia{ result };
+			}
+		}
+		return {};
+	}, [&](const MTPDmessageMediaDocument &data)
+	-> std::optional<StoryMedia> {
+		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<StoryMedia>(); });
+}
+
 } // 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<Session*> 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<PeerData*> peer, const MTPDstoryItem &data) {
+Story *Stories::parseAndApply(
+		not_null<PeerData*> 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<PhotoData*>, not_null<DocumentData*>>>;
-	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<Story>(
 		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<MTPStoryItem> &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<PeerData*> peer,
 		const MTPDstoryItem &data);
 	void processResolvedStories(

From cdd4774bb8761b1d35c9c628f64760e73ba0ba37 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 May 2023 19:51:56 +0400
Subject: [PATCH 038/259] 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 <johnprestonmail@gmail.com>
Date: Fri, 26 May 2023 20:30:02 +0400
Subject: [PATCH 039/259] 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<Window::SessionController*> controller,
+		std::shared_ptr<ChatHelpers::Show> show,
 		not_null<DocumentData*> 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<Window::SessionController*> controller,
+	std::shared_ptr<ChatHelpers::Show> show,
 	not_null<DocumentData*> 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<ChatHelpers::Show> Controller::uiShow() const {
 }
 
 auto Controller::stickerOrEmojiChosen() const
-->rpl::producer<ChatHelpers::FileChosen> {
+-> rpl::producer<ChatHelpers::FileChosen> {
 	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<DocumentData*> document,
+		Api::SendOptions options,
+		std::optional<MsgId> 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<PhotoData*> photo) {
+	sendExistingPhoto(photo, {});
+}
+
+bool ReplyArea::sendExistingPhoto(
+		not_null<PhotoData*> 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<InlineBots::Result*> result,
+		not_null<UserData*> 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<InlineBots::Result*> result,
+		not_null<UserData*> bot,
+		Api::SendOptions options,
+		std::optional<MsgId> 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<History*> 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*> history() const;
 
 	bool confirmSendingFiles(const QStringList &files);
 	bool confirmSendingFiles(not_null<const QMimeData*> data);
@@ -90,6 +96,24 @@ private:
 		bool ctrlShiftEnter);
 	void finishSending();
 
+	void sendExistingDocument(not_null<DocumentData*> document);
+	bool sendExistingDocument(
+		not_null<DocumentData*> document,
+		Api::SendOptions options,
+		std::optional<MsgId> localId);
+	void sendExistingPhoto(not_null<PhotoData*> photo);
+	bool sendExistingPhoto(
+		not_null<PhotoData*> photo,
+		Api::SendOptions options);
+	void sendInlineResult(
+		not_null<InlineBots::Result*> result,
+		not_null<UserData*> bot);
+	void sendInlineResult(
+		not_null<InlineBots::Result*> result,
+		not_null<UserData*> bot,
+		Api::SendOptions options,
+		std::optional<MsgId> 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<SessionController*> controller,
 		not_null<DocumentData*> document) {
+	return ShowSendPremiumError(controller->uiShow(), document);
+}
+
+bool ShowSendPremiumError(
+		std::shared_ptr<ChatHelpers::Show> show,
+		not_null<DocumentData*> 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<SessionController*> controller,
 	not_null<DocumentData*> document);
+[[nodiscard]] bool ShowSendPremiumError(
+	std::shared_ptr<ChatHelpers::Show> show,
+	not_null<DocumentData*> document);
 
 [[nodiscard]] bool ShowReactPremiumError(
 	not_null<SessionController*> controller,

From 4e165a2107d133e6346b27ff3bba72e91b814e8c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 29 May 2023 11:45:04 +0400
Subject: [PATCH 040/259] 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 <johnprestonmail@gmail.com>
Date: Mon, 29 May 2023 12:06:21 +0400
Subject: [PATCH 041/259] 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<uint64> 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<uint64> _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 <johnprestonmail@gmail.com>
Date: Mon, 29 May 2023 16:03:23 +0400
Subject: [PATCH 042/259] 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<MTPint> ids;
-		std::vector<Fn<void()>> callbacks;
-	};
 	auto leftToSend = kMaxResolveTogether;
-	auto byPeer = base::flat_map<PeerId, Prepared>();
-	for (auto i = begin(_resolves); i != end(_resolves);) {
+	auto byPeer = base::flat_map<PeerId, QVector<MTPint>>();
+	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<MTPint>(std::move(prepared.ids))
-		)).done([=](const MTPstories_Stories &result, mtpRequestId id) {
+			MTP_vector<MTPint>(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<PeerId> Stories::itemsChanged() const {
+	return _itemsChanged.events();
+}
+
 base::expected<not_null<Story*>, NoStory> Stories::lookup(
 		FullStoryId id) const {
 	const auto i = _stories.find(id.peer);
@@ -492,10 +492,20 @@ base::expected<not_null<Story*>, NoStory> Stories::lookup(
 void Stories::resolve(FullStoryId id, Fn<void()> 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<StoriesList> &all();
 	[[nodiscard]] bool allLoaded() const;
 	[[nodiscard]] rpl::producer<> allChanged() const;
+	[[nodiscard]] rpl::producer<PeerId> itemsChanged() const;
 
 	[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
 		FullStoryId id) const;
@@ -135,8 +137,10 @@ private:
 
 	base::flat_map<
 		PeerId,
-		base::flat_map<StoryId, std::vector<Fn<void()>>>> _resolves;
-	base::flat_map<mtpRequestId, std::vector<Fn<void()>>> _resolveRequests;
+		base::flat_map<StoryId, std::vector<Fn<void()>>>> _resolvePending;
+	base::flat_map<
+		PeerId,
+		base::flat_map<StoryId, std::vector<Fn<void()>>>> _resolveSent;
 
 	std::map<
 		not_null<Data::Story*>,
@@ -144,6 +148,7 @@ private:
 
 	std::vector<StoriesList> _all;
 	rpl::event_stream<> _allChanged;
+	rpl::event_stream<PeerId> _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> &sibling,
 		const Data::StoriesList *list);
 
+	void subjumpTo(int index);
+	void checkWaitingFor();
+
 	const not_null<Delegate*> _delegate;
 
 	rpl::variable<std::optional<Layout>> _layout;
@@ -148,6 +151,7 @@ private:
 	FullStoryId _shown;
 	TextWithEntities _captionText;
 	std::optional<Data::StoriesList> _list;
+	FullStoryId _waitingForId;
 	int _index = 0;
 	bool _started = false;
 

From f814e401b9a7c5d3311bed76cd8d091f0e0ec24e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 29 May 2023 19:09:36 +0400
Subject: [PATCH 043/259] 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<StoryMedia> 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<Session*> owner) : _owner(owner) {
+Stories::Stories(not_null<Session*> 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<PeerData*> 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<UserData*> user;
-	std::vector<StoryId> ids;
+	base::flat_set<StoryId> ids;
 	StoryId readTill = 0;
 	int total = 0;
 
@@ -113,6 +114,9 @@ public:
 		FullStoryId id) const;
 	void resolve(FullStoryId id, Fn<void()> 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*> story);
 
+	void sendMarkAsReadRequests();
+	void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
+
 	const not_null<Session*> _owner;
 	base::flat_map<
 		PeerId,
@@ -154,6 +161,10 @@ private:
 
 	mtpRequestId _loadMoreRequestId = 0;
 
+	base::flat_set<PeerId> _markReadPending;
+	base::Timer _markReadTimer;
+	base::flat_set<PeerId> _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<Data::StoriesList> &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 <johnprestonmail@gmail.com>
Date: Tue, 30 May 2023 09:44:31 +0400
Subject: [PATCH 044/259] 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<Session*> 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 <johnprestonmail@gmail.com>
Date: Tue, 30 May 2023 17:57:11 +0400
Subject: [PATCH 045/259] 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<void()> 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<bool> ComposeControls::focusedValue() const {
 		| rpl::then(_focusChanges.events());
 }
 
+rpl::producer<bool> 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<bool> ComposeControls::recordingValue() const {
+	return _recording.value();
+}
+
 bool ComposeControls::preventsClose(Fn<void()> &&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<bool> focusedValue() const;
+	[[nodiscard]] rpl::producer<bool> tabbedPanelShownValue() const;
 	[[nodiscard]] rpl::producer<> cancelRequests() const;
 	[[nodiscard]] rpl::producer<Api::SendOptions> sendRequests() const;
 	[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
@@ -213,6 +214,7 @@ public:
 	[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
 	[[nodiscard]] bool isLockPresent() const;
 	[[nodiscard]] bool isRecording() const;
+	[[nodiscard]] rpl::producer<bool> recordingValue() const;
 
 	void applyCloudDraft();
 	void applyDraft(
@@ -379,6 +381,7 @@ private:
 	rpl::event_stream<std::optional<bool>> _attachRequests;
 	rpl::event_stream<ReplyNextRequest> _replyNextRequests;
 	rpl::event_stream<> _focusRequests;
+	rpl::variable<bool> _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 <QtGui/QWindow>
+
 namespace Media::Stories {
 namespace {
 
@@ -123,27 +126,52 @@ Controller::Controller(not_null<Delegate*> delegate)
 , _replyArea(std::make_unique<ReplyArea>(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<PhotoPlayback>(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<Data::StoriesList> &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<Data::StoriesList> _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<bool> 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<bool> 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<bool> overrideCompress) {
-		_choosingAttach = true;
+		_chooseAttachRequest = true;
 		base::call_delayed(
 			st::storiesAttach.ripple.hideDuration,
 			this,
@@ -569,6 +574,17 @@ rpl::producer<bool> ReplyArea::focusedValue() const {
 	return _controls->focusedValue();
 }
 
+rpl::producer<bool> 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<DocumentData*> 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<bool> focusedValue() const;
+	[[nodiscard]] rpl::producer<bool> 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<bool> _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<QOpenGLShaderProgram> _controlsProgram;
 	std::optional<QOpenGLShaderProgram> _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<bool> 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<bool> 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 <johnprestonmail@gmail.com>
Date: Tue, 30 May 2023 18:41:02 +0400
Subject: [PATCH 046/259] 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<QString> AboutPrivacyController::title() const {
+	return tr::lng_edit_privacy_about_title();
+}
+
+rpl::producer<QString> AboutPrivacyController::optionsTitleKey() const {
+	return tr::lng_edit_privacy_about_header();
+}
+
+rpl::producer<QString> 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<QString> 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<QString> {
+	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<QString> title() const override;
+	rpl::producer<QString> optionsTitleKey() const override;
+	rpl::producer<QString> exceptionButtonTextKey(
+		Exception exception) const override;
+	rpl::producer<QString> exceptionBoxTitle(
+		Exception exception) const override;
+	rpl::producer<QString> 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<VoicesPrivacyController>(session); });
+	add(
+		tr::lng_settings_bio_privacy(),
+		{ &st::settingsIconAccount, kIconDarkOrange },
+		Key::About,
+		[] { return std::make_unique<AboutPrivacyController>(); });
 
 	session->api().userPrivacy().reload(Api::UserPrivacy::Key::AddedByPhone);
 

From d76c80bf0e3099b0d00c2725beeceb228b01f969 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 May 2023 21:12:15 +0400
Subject: [PATCH 047/259] 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<QUrl> ReadMimeUrls(not_null<const QMimeData*> data) {
 		: QList<QUrl>();
 }
 
+bool CanSendFiles(not_null<const QMimeData*> 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<const QMimeData*> data);
 [[nodiscard]] QString ReadMimeText(not_null<const QMimeData*> data);
 [[nodiscard]] QList<QUrl> ReadMimeUrls(not_null<const QMimeData*> data);
+[[nodiscard]] bool CanSendFiles(not_null<const QMimeData*> 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<StoryMedia> ParseMedia(
-		not_null<Session*> owner,
-		const MTPMessageMedia &media) {
+	not_null<Session*> owner,
+	const MTPMessageMedia &media) {
 	return media.match([&](const MTPDmessageMediaPhoto &data)
-	-> std::optional<StoryMedia> {
+		-> std::optional<StoryMedia> {
 		if (const auto photo = data.vphoto()) {
 			const auto result = owner->processPhoto(*photo);
 			if (!result->isNull()) {
@@ -48,7 +48,7 @@ std::optional<StoryMedia> ParseMedia(
 		}
 		return {};
 	}, [&](const MTPDmessageMediaDocument &data)
-	-> std::optional<StoryMedia> {
+		-> std::optional<StoryMedia> {
 		if (const auto document = data.vdocument()) {
 			const auto result = owner->processDocument(*document);
 			if (!result->isNull()
@@ -71,10 +71,10 @@ Story::Story(
 	not_null<PeerData*> 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<not_null<PeerData*>> recent,
+		int total) {
+	_recentViewers = std::move(recent);
+	_views = total;
+}
+
+const std::vector<not_null<PeerData*>> &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<not_null<PeerData*>>();
+	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<not_null<PeerData*>> recent, int total);
+	[[nodiscard]] auto recentViewers() const
+		-> const std::vector<not_null<PeerData*>> &;
+	[[nodiscard]] int views() const;
+
 	bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
 
 private:
@@ -64,6 +69,8 @@ private:
 	const not_null<PeerData*> _peer;
 	StoryMedia _media;
 	TextWithEntities _caption;
+	std::vector<not_null<PeerData*>> _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<const QMimeData*> 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<Ui::MessageBarContent> RootViewContent(
 		not_null<History*> history,
 		MsgId rootId,
@@ -819,7 +808,7 @@ void RepliesWidget::setupComposeControls() {
 			not_null<const QMimeData*> 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 <QtCore/QMimeData>
 
 namespace HistoryView {
-namespace {
-
-bool CanSendFiles(not_null<const QMimeData*> 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<Window::SectionWidget> ScheduledMemento::createWidget(
 		QWidget *parent,
@@ -308,7 +294,7 @@ void ScheduledWidget::setupComposeControls() {
 			not_null<const QMimeData*> 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*> delegate)
 , _wrap(_delegate->storiesWrap())
 , _header(std::make_unique<Header>(this))
 , _slider(std::make_unique<Slider>(this))
-, _replyArea(std::make_unique<ReplyArea>(this)) {
+, _replyArea(std::make_unique<ReplyArea>(this))
+, _recentViews(std::make_unique<RecentViews>(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> _header;
 	const std::unique_ptr<Slider> _slider;
 	const std::unique_ptr<ReplyArea> _replyArea;
+	const std::unique_ptr<RecentViews> _recentViews;
 	std::unique_ptr<PhotoPlayback> _photoPlayback;
 	std::unique_ptr<CaptionFullView> _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<std::vector<Ui::GroupCallUser>> ContentByUsers(
+		const std::vector<not_null<PeerData*>> &list) {
+	struct Userpic {
+		not_null<PeerData*> peer;
+		mutable Ui::PeerUserpicView view;
+		mutable InMemoryKey uniqueKey;
+	};
+
+	struct State {
+		std::vector<Userpic> userpics;
+		std::vector<Ui::GroupCallUser> 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*> 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<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(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<Ui::RpWidget>(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<Ui::GroupCallUserpics>(
+			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<Ui::GroupCallUser> &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<not_null<PeerData*>> 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*> controller);
+	~RecentViews();
+
+	void show(RecentViewsData data);
+
+private:
+	const not_null<Controller*> _controller;
+
+	std::unique_ptr<Ui::RpWidget> _widget;
+	std::unique_ptr<Ui::GroupCallUserpics> _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<const QMimeData*> 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*> history() const;
 
-	bool confirmSendingFiles(const QStringList &files);
-	bool confirmSendingFiles(not_null<const QMimeData*> data);
-
 	void uploadFile(const QByteArray &fileContent, SendMediaType type);
 	bool confirmSendingFiles(
 		QImage &&image,
 		QByteArray &&content,
 		std::optional<bool> 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 <johnprestonmail@gmail.com>
Date: Wed, 31 May 2023 10:39:45 +0400
Subject: [PATCH 048/259] 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<DocumentData*> _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<Streaming::Instance>(
-			_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<Streaming::Instance>(
+		_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 <johnprestonmail@gmail.com>
Date: Wed, 31 May 2023 21:53:17 +0400
Subject: [PATCH 049/259] 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 <johnprestonmail@gmail.com>
Date: Thu, 1 Jun 2023 20:01:29 +0400
Subject: [PATCH 050/259] 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*> state,
 		not_null<HistoryItem*> item,
@@ -614,6 +584,37 @@ rpl::producer<Ui::WhoReadContent> 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<HistoryItem*> 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<HistoryItem*> item);
 [[nodiscard]] bool WhoReactedExists(
 	not_null<HistoryItem*> 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<not_null<PeerData*>> &Story::recentViewers() const {
 	return _recentViewers;
 }
 
+const std::vector<StoryView> &Story::viewsList() const {
+	return _viewsList;
+}
+
 int Story::views() const {
 	return _views;
 }
 
+void Story::applyViewsSlice(
+		const std::optional<StoryView> &offset,
+		const std::vector<StoryView> &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<StoryView> offset,
+		Fn<void(std::vector<StoryView>)> 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<StoryView>();
+
+		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<PeerData*> peer;
+	TimeId date = 0;
+
+	friend inline bool operator==(StoryView, StoryView) = default;
+};
+
 class Story {
 public:
 	Story(
@@ -60,7 +67,12 @@ public:
 	void setViewsData(std::vector<not_null<PeerData*>> recent, int total);
 	[[nodiscard]] auto recentViewers() const
 		-> const std::vector<not_null<PeerData*>> &;
+	[[nodiscard]] const std::vector<StoryView> &viewsList() const;
 	[[nodiscard]] int views() const;
+	void applyViewsSlice(
+		const std::optional<StoryView> &offset,
+		const std::vector<StoryView> &slice,
+		int total);
 
 	bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
 
@@ -70,6 +82,7 @@ private:
 	StoryMedia _media;
 	TextWithEntities _caption;
 	std::vector<not_null<PeerData*>> _recentViewers;
+	std::vector<StoryView> _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<StoryView> offset,
+		Fn<void(std::vector<StoryView>)> done);
+
 private:
 	[[nodiscard]] StoriesList parse(const MTPUserStories &stories);
 	[[nodiscard]] Story *parseAndApply(
@@ -172,6 +191,11 @@ private:
 	base::Timer _markReadTimer;
 	base::flat_set<PeerId> _markReadRequests;
 
+	StoryId _viewsStoryId = 0;
+	std::optional<StoryView> _viewsOffset;
+	Fn<void(std::vector<StoryView>)> _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 <QtGui/QGuiApplication>
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*> 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<void(std::vector<Data::StoryView>)> Controller::viewsGotMoreCallback() {
+	return crl::guard(&_viewsLoadGuard, [=](
+			const std::vector<Data::StoryView> &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<Data::StoryView> list;
+	int left = 0;
+};
+
 class Controller final {
 public:
 	explicit Controller(not_null<Delegate*> delegate);
@@ -114,6 +119,9 @@ public:
 	void repaintSibling(not_null<Sibling*> 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<void(std::vector<Data::StoryView>)>;
+
 	const not_null<Delegate*> _delegate;
 
 	rpl::variable<std::optional<Layout>> _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<Sibling> _siblingLeft;
 	std::unique_ptr<Sibling> _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<std::vector<Ui::GroupCallUser>> ContentByUsers(
 		const std::vector<not_null<PeerData*>> &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<Ui::RpWidget>(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<Ui::GroupCallUserpics>(
-			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<Ui::GroupCallUser> &list) {
-			_userpics->update(list, true);
-		});
+		updateUserpics();
 	}
 }
 
+void RecentViews::updateUserpics() {
+	_userpicsLifetime = ContentByUsers(
+		_data.list
+	) | rpl::start_with_next([=](
+		const std::vector<Ui::GroupCallUser> &list) {
+		_userpics->update(list, true);
+	});
+}
+
+void RecentViews::setupUserpics() {
+	_userpics = std::make_unique<Ui::GroupCallUserpics>(
+		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<Ui::RpWidget>(_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<QEvent*> e) {
+		return (_data.total > 0)
+			&& (e->type() == QEvent::MouseButtonPress)
+			&& (static_cast<QMouseEvent*>(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<PopupMenu>(
+		_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<Ui::WhoReactedEntryAction>(
+			_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<Ui::WhoReactedEntryAction>(
+		_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<Main::Session*> 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<Ui::WhoReactedEntryAction*> 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<Main::Session*> session);
+
 	const not_null<Controller*> _controller;
 
 	std::unique_ptr<Ui::RpWidget> _widget;
@@ -46,6 +78,20 @@ private:
 	Ui::Text::String _text;
 	RecentViewsData _data;
 	rpl::lifetime _userpicsLifetime;
+
+	base::unique_qptr<Ui::PopupMenu> _menu;
+	rpl::lifetime _menuShortLifetime;
+	std::vector<MenuEntry> _menuEntries;
+	rpl::variable<int> _menuEntriesCount = 0;
+	int _menuPlaceholderCount = 0;
+	base::flat_set<int> _waitingForUserpics;
+	rpl::variable<bool> _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 <QtCore/QOperatingSystemVersion>
 
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<StringWithReacted>::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<void()> callback;
-};
+using Text::CustomEmojiFactory;
 
 class Action final : public Menu::ItemBase {
 public:
@@ -465,44 +459,11 @@ void Action::handleKeyPress(not_null<QKeyEvent*> e) {
 
 } // namespace
 
-class WhoReactedListMenu::EntryAction final : public Menu::ItemBase {
-public:
-	EntryAction(
-		not_null<RpWidget*> parent,
-		CustomEmojiFactory factory,
-		const style::Menu &st,
-		EntryData &&data);
-
-	void setData(EntryData &&data);
-
-	not_null<QAction*> action() const override;
-	bool isEnabled() const override;
-
-private:
-	int contentHeight() const override;
-
-	void paint(Painter &&p);
-
-	const not_null<QAction*> _dummyAction;
-	const CustomEmojiFactory _customEmojiFactory;
-	const style::Menu &_st;
-	const int _height = 0;
-
-	Text::String _text;
-	Text::String _date;
-	std::unique_ptr<Ui::Text::CustomEmoji> _custom;
-	QImage _userpic;
-	int _textWidth = 0;
-	int _customSize = 0;
-	bool _dateReacted = false;
-
-};
-
-WhoReactedListMenu::EntryAction::EntryAction(
+WhoReactedEntryAction::WhoReactedEntryAction(
 	not_null<RpWidget*> parent,
 	CustomEmojiFactory customEmojiFactory,
 	const style::Menu &st,
-	EntryData &&data)
+	Data &&data)
 : ItemBase(parent, st)
 , _dummyAction(CreateChild<QAction>(parent.get()))
 , _customEmojiFactory(std::move(customEmojiFactory))
@@ -521,19 +482,19 @@ WhoReactedListMenu::EntryAction::EntryAction(
 	enableMouseSelecting();
 }
 
-not_null<QAction*> WhoReactedListMenu::EntryAction::action() const {
+not_null<QAction*> 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<EntryAction>(
+			auto item = base::make_unique_q<WhoReactedEntryAction>(
 				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<void(uint64)> participantChosen,
 	Fn<void()> showAllChosen);
 
+struct WhoReactedEntryData {
+	QString text;
+	QString date;
+	bool dateReacted = false;
+	bool preloader = false;
+	QString customEntityData;
+	QImage userpic;
+	Fn<void()> callback;
+};
+
+class WhoReactedEntryAction final : public Menu::ItemBase {
+public:
+	using Data = WhoReactedEntryData;
+
+	WhoReactedEntryAction(
+		not_null<RpWidget*> parent,
+		Text::CustomEmojiFactory factory,
+		const style::Menu &st,
+		Data &&data);
+
+	void setData(Data &&data);
+
+	not_null<QAction*> action() const override;
+	bool isEnabled() const override;
+
+private:
+	int contentHeight() const override;
+
+	void paint(Painter &&p);
+
+	const not_null<QAction*> _dummyAction;
+	const Text::CustomEmojiFactory _customEmojiFactory;
+	const style::Menu &_st;
+	const int _height = 0;
+
+	Text::String _text;
+	Text::String _date;
+	std::unique_ptr<Ui::Text::CustomEmoji> _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<void()> appendBottomActions = nullptr);
 
 private:
-	class EntryAction;
-
 	const Text::CustomEmojiFactory _customEmojiFactory;
 	const Fn<void(uint64)> _participantChosen;
 	const Fn<void()> _showAllChosen;
 
-	std::vector<not_null<EntryAction*>> _actions;
+	std::vector<not_null<WhoReactedEntryAction*>> _actions;
 
 };
 

From 41edd41b924c38d08a3faef23dea24b96fe635fa Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Jun 2023 20:12:42 +0400
Subject: [PATCH 051/259] 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 <johnprestonmail@gmail.com>
Date: Fri, 2 Jun 2023 11:46:19 +0400
Subject: [PATCH 052/259] 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<PeerData*> 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<Data::Story*> 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<Data::Story*> dependency);
 
 	void loadMore();
-	void apply(const MTPDupdateStories &data);
+	void apply(const MTPDupdateStory &data);
 	void loadAround(FullStoryId id);
 
 	[[nodiscard]] const std::vector<StoriesList> &all();
@@ -148,6 +148,9 @@ private:
 	[[nodiscard]] Story *parseAndApply(
 		not_null<PeerData*> peer,
 		const MTPDstoryItem &data);
+	StoryId parseAndApply(
+		not_null<PeerData*> peer,
+		const MTPstoryItem &story);
 	void processResolvedStories(
 		not_null<PeerData*> peer,
 		const QVector<MTPStoryItem> &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*> 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<long> 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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> = 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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> = 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<long>
 
 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
 
 userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = 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<long> = Bool;
+contacts.toggleStoriesHidden#753fb865 id:InputUser hidden:Bool = Bool;
 
 messages.getMessages#63c66506 id:Vector<InputMessage> = 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<MessageEntity> privacy_rules:flags.2?Vector<InputPrivacyRule> = Updates;
 stories.deleteStories#b5d501d7 id:Vector<int> = Vector<int>;
 stories.togglePinned#51602944 id:Vector<int> pinned:Bool = Vector<int>;
-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<int>;
 stories.incrementStoryViews#22126127 user_id:InputUser id:Vector<int> = Bool;
 stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList;
 stories.getStoriesViews#9a75d6a6 id:Vector<int> = 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 <johnprestonmail@gmail.com>
Date: Fri, 2 Jun 2023 13:20:27 +0400
Subject: [PATCH 053/259] 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<UserData*> 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<UserData*> 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<PeerData*> 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<StoryId> 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<UserDataFlag>;
@@ -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<uint64> List::clicks() const {
 	return _clicks.events();
 }
 
+rpl::producer<uint64> List::showProfileRequests() const {
+	return _showProfileRequests.events();
+}
+
+rpl::producer<ToggleShownRequest> 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<Ui::PopupMenu>(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> 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<int()> shownHeight);
 
 	[[nodiscard]] rpl::producer<uint64> clicks() const;
+	[[nodiscard]] rpl::producer<uint64> showProfileRequests() const;
+	[[nodiscard]] rpl::producer<ToggleShownRequest> 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*> item);
 	void validateName(not_null<Item*> item);
@@ -128,6 +141,8 @@ private:
 	Data _hidingData;
 	Fn<int()> _shownHeight = 0;
 	rpl::event_stream<uint64> _clicks;
+	rpl::event_stream<uint64> _showProfileRequests;
+	rpl::event_stream<ToggleShownRequest> _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<Ui::PopupMenu> _menu;
+
 };
 
 } // namespace Dialogs::Stories

From f40391b4f0a5fc503d50647acfe17052213169fb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 2 Jun 2023 18:26:39 +0400
Subject: [PATCH 054/259] 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<Main::Session*> 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<StoryMedia> ParseMedia(
+[[nodiscard]] std::optional<StoryMedia> ParseMedia(
 	not_null<Session*> owner,
 	const MTPMessageMedia &media) {
 	return media.match([&](const MTPDmessageMediaPhoto &data)
@@ -62,8 +62,18 @@ std::optional<StoryMedia> 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<int>(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<UserData*> 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<int>(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<PeerData*> 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<int>(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<int>(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<int>(from)];
+		const auto i = ranges::find(
+			sources,
+			id,
+			&StoriesSourceInfo::id);
+		if (i != end(sources)) {
+			sources.erase(i);
 		}
+		_sourcesChanged[static_cast<int>(from)].fire({});
+	};
+	removeFromList(StorySourcesList::NotHidden);
+	if (list == StorySourcesList::All) {
+		removeFromList(StorySourcesList::All);
 	}
-	_deleted.emplace(id);
 }
 
 void Stories::removeDependencyStory(not_null<Story*> story) {
@@ -564,16 +665,36 @@ void Stories::removeDependencyStory(not_null<Story*> story) {
 	}
 }
 
-const std::vector<StoriesList> &Stories::all() {
+void Stories::sort(StorySourcesList list) {
+	const auto index = static_cast<int>(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<PeerId, StoriesSource> &Stories::all() const {
 	return _all;
 }
 
-bool Stories::allLoaded() const {
-	return _allLoaded;
+const std::vector<StoriesSourceInfo> &Stories::sources(
+		StorySourcesList list) const {
+	return _sources[static_cast<int>(list)];
 }
 
-rpl::producer<> Stories::allChanged() const {
-	return _allChanged.events();
+bool Stories::sourcesLoaded(StorySourcesList list) const {
+	return _sourcesLoaded[static_cast<int>(list)];
+}
+
+rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const {
+	return _sourcesChanged[static_cast<int>(list)].events();
 }
 
 rpl::producer<PeerId> Stories::itemsChanged() const {
@@ -621,35 +742,21 @@ void Stories::resolve(FullStoryId id, Fn<void()> 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<int>(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<PhotoData*>, not_null<DocumentData*>> 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<PeerData*> 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<UserData*> user;
-	base::flat_set<StoryId> 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<UserData*> user;
+	base::flat_set<StoryIdDate> 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<Session*> owner);
@@ -122,13 +157,16 @@ public:
 		not_null<HistoryItem*> dependent,
 		not_null<Data::Story*> dependency);
 
-	void loadMore();
+	void loadMore(StorySourcesList list);
 	void apply(const MTPDupdateStory &data);
 	void loadAround(FullStoryId id);
 
-	[[nodiscard]] const std::vector<StoriesList> &all();
-	[[nodiscard]] bool allLoaded() const;
-	[[nodiscard]] rpl::producer<> allChanged() const;
+	[[nodiscard]] const base::flat_map<PeerId, StoriesSource> &all() const;
+	[[nodiscard]] const std::vector<StoriesSourceInfo> &sources(
+		StorySourcesList list) const;
+	[[nodiscard]] bool sourcesLoaded(StorySourcesList list) const;
+	[[nodiscard]] rpl::producer<> sourcesChanged(
+		StorySourcesList list) const;
 	[[nodiscard]] rpl::producer<PeerId> itemsChanged() const;
 
 	[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
@@ -145,11 +183,11 @@ public:
 		Fn<void(std::vector<StoryView>)> done);
 
 private:
-	[[nodiscard]] StoriesList parse(const MTPUserStories &stories);
+	void parseAndApply(const MTPUserStories &stories);
 	[[nodiscard]] Story *parseAndApply(
 		not_null<PeerData*> peer,
 		const MTPDstoryItem &data);
-	StoryId parseAndApply(
+	StoryIdDate parseAndApply(
 		not_null<PeerData*> 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*> story);
+	void sort(StorySourcesList list);
 
 	void sendMarkAsReadRequests();
 	void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
 
+	void requestUserStories(not_null<UserData*> user);
+
 	const not_null<Session*> _owner;
 	base::flat_map<
 		PeerId,
@@ -182,17 +223,20 @@ private:
 		not_null<Data::Story*>,
 		base::flat_set<not_null<HistoryItem*>>> _dependentMessages;
 
-	std::vector<StoriesList> _all;
-	rpl::event_stream<> _allChanged;
-	rpl::event_stream<PeerId> _itemsChanged;
-	QString _state;
-	bool _allLoaded = false;
+	base::flat_map<PeerId, StoriesSource> _all;
+	std::vector<StoriesSourceInfo> _sources[kStorySourcesListCount];
+	rpl::event_stream<> _sourcesChanged[kStorySourcesListCount];
+	bool _sourcesLoaded[kStorySourcesListCount] = { false };
+	QString _sourcesStates[kStorySourcesListCount];
 
-	mtpRequestId _loadMoreRequestId = 0;
+	mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 };
+
+	rpl::event_stream<PeerId> _itemsChanged;
 
 	base::flat_set<PeerId> _markReadPending;
 	base::Timer _markReadTimer;
 	base::flat_set<PeerId> _markReadRequests;
+	base::flat_set<not_null<UserData*>> _requestingUserStories;
 
 	StoryId _viewsStoryId = 0;
 	std::optional<StoryView> _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<Stories::List>(
 	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::Stories*> data);
+	State(not_null<Data::Stories*> data, Data::StorySourcesList list);
 
 	[[nodiscard]] Content next();
 
 private:
 	const not_null<Data::Stories*> _data;
+	const Data::StorySourcesList _list;
 	base::flat_map<not_null<UserData*>, std::shared_ptr<Userpic>> _userpics;
 
 };
@@ -122,27 +121,23 @@ void PeerUserpic::processNewPhoto() {
 	}, _subscribed->downloadLifetime);
 }
 
-State::State(not_null<Data::Stories*> data)
-: _data(data) {
+State::State(not_null<Data::Stories*> 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<Userpic>();
-		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<Userpic>();
-#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<Content> ContentForSession(not_null<Main::Session*> session) {
+rpl::producer<Content> ContentForSession(
+		not_null<Main::Session*> session,
+		Data::StorySourcesList list) {
 	return [=](auto consumer) {
 		auto result = rpl::lifetime();
 		const auto stories = &session->data().stories();
-		const auto state = result.make_state<State>(stories);
+		const auto state = result.make_state<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<Content> ContentForSession(
-	not_null<Main::Session*> session);
+	not_null<Main::Session*> 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<Data::StoriesList> &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<Data::Story*> 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<Data::StoriesList> &lists,
+		not_null<Main::Session*> session,
+		const std::vector<Data::StoriesSourceInfo> &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> &sibling,
-		const Data::StoriesList *list) {
-	if (!list || list->ids.empty()) {
+		not_null<Main::Session*> session,
+		PeerId peerId) {
+	if (!peerId) {
 		sibling = nullptr;
-	} else if (!sibling || !sibling->shows(*list)) {
-		sibling = std::make_unique<Sibling>(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<Sibling>(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*> sibling) {
@@ -706,7 +722,7 @@ Fn<void(std::vector<Data::StoryView>)> Controller::viewsGotMoreCallback() {
 	return crl::guard(&_viewsLoadGuard, [=](
 			const std::vector<Data::StoryView> &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<void(std::vector<Data::StoryView>)> 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<ChatHelpers::FileChosen>;
 
-	void show(
-		const std::vector<Data::StoriesList> &lists,
-		int index,
-		int subindex);
+	void show(not_null<Data::Story*> 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<Data::StoriesList> &lists,
+		not_null<Main::Session*> session,
+		const std::vector<Data::StoriesSourceInfo> &lists,
 		int index);
 	void showSibling(
 		std::unique_ptr<Sibling> &sibling,
-		const Data::StoriesList *list);
+		not_null<Main::Session*> session,
+		PeerId peerId);
 
 	void subjumpTo(int index);
 	void checkWaitingFor();
@@ -180,7 +178,7 @@ private:
 
 	FullStoryId _shown;
 	TextWithEntities _captionText;
-	std::optional<Data::StoriesList> _list;
+	std::optional<Data::StoriesSource> _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*> 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<PeerData*> 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*> controller,
-		const Data::StoriesList &list);
+		const Data::StoriesSource &source);
 	~Sibling();
 
 	[[nodiscard]] FullStoryId shownId() const;
 	[[nodiscard]] not_null<PeerData*> 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*> delegate)
 
 View::~View() = default;
 
-void View::show(
-		const std::vector<Data::StoriesList> &lists,
-		int index,
-		int subindex) {
-	_controller->show(lists, index, subindex);
+void View::show(not_null<Data::Story*> 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*> delegate);
 	~View();
 
-	void show(
-		const std::vector<Data::StoriesList> &lists,
-		int index,
-		int subindex);
+	void show(not_null<Data::Story*> 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<Data::Story*> 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<Data::CloudTheme> 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<PeerData*> 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<PeerData*> 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<PeerData*> peer, StoryId storyId);
-	void openPeerStories(PeerId peerId);
+	void openPeerStory(
+		not_null<PeerData*> peer,
+		StoryId storyId,
+		Data::StorySourcesList list);
+	void openPeerStories(PeerId peerId, Data::StorySourcesList list);
 
 	struct PaintContextArgs {
 		not_null<Ui::ChatTheme*> theme;

From e7c0385aea8ddaca856a16b18e6958b0d021e586 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 2 Jun 2023 21:14:15 +0400
Subject: [PATCH 055/259] 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<Ui::BoxContent> PrepareContactsBox(
 		&sessionController->session());
 	const auto raw = controller.get();
 	auto init = [=](not_null<PeerListBox*> box) {
+		using namespace Dialogs;
+
 		struct State {
-			QPointer<Ui::IconButton> toggleSort;
+			Stories::List *stories = nullptr;
+			QPointer<::Ui::IconButton> toggleSort;
 			Mode mode = ContactsBoxController::SortMode::Online;
 		};
+
 		const auto state = box->lifetime().make_state<State>();
 		box->addButton(tr::lng_close(), [=] { box->closeBox(); });
 		box->addLeftButton(
@@ -69,6 +78,43 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 				online ? &st::contactsSortOnlineIconOver : nullptr);
 		});
 		raw->setSortMode(Mode::Online);
+
+		auto stories = object_ptr<Stories::List>(
+			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<PeerListBox>(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<int>(StorySourcesList::NotHidden);
+	const auto all = static_cast<int>(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<PeerData*> 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<Ui::PopupMenu> _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 <johnprestonmail@gmail.com>
Date: Mon, 5 Jun 2023 16:10:34 +0400
Subject: [PATCH 056/259] 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<Ui::BoxContent> 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<void()> done) {
 	}
 }
 
-void Stories::loadAround(FullStoryId id) {
+void Stories::loadAround(FullStoryId id, StoriesContext context) {
+	if (v::is<StoriesContextSingle>(context.data)) {
+		return;
+	} else if (v::is<StoriesContextSaved>(context.data)
+		|| v::is<StoriesContextArchive>(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<PeerId, StoriesSource> &all() const;
 	[[nodiscard]] const std::vector<StoriesSourceInfo> &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::Stories*> 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<User> 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<Data::Story*> 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> &sibling,
 		not_null<Main::Session*> 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<ChatHelpers::FileChosen>;
 
-	void show(not_null<Data::Story*> story, Data::StorySourcesList list);
+	void show(not_null<Data::Story*> 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<Main::Session*> session,
 		const std::vector<Data::StoriesSourceInfo> &lists,
@@ -178,6 +179,7 @@ private:
 
 	FullStoryId _shown;
 	TextWithEntities _captionText;
+	Data::StoriesContext _context;
 	std::optional<Data::StoriesSource> _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<ChatHelpers::FileChosen> = 0;
 	virtual void storiesJumpTo(
 		not_null<Main::Session*> session,
-		FullStoryId id) = 0;
+		FullStoryId id,
+		Data::StoriesContext context) = 0;
 	virtual void storiesClose() = 0;
 	[[nodiscard]] virtual bool storiesPaused() = 0;
 	[[nodiscard]] virtual rpl::producer<bool> 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*> delegate)
 
 View::~View() = default;
 
-void View::show(not_null<Data::Story*> story, Data::StorySourcesList list) {
-	_controller->show(story, list);
+void View::show(
+		not_null<Data::Story*> 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*> delegate);
 	~View();
 
-	void show(not_null<Data::Story*> story, Data::StorySourcesList list);
+	void show(not_null<Data::Story*> 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<AVFormatContext*> 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<FFmpeg::Packet, FFmpeg::AvErrorWrap> 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<void *>(this),
+		static_cast<void*>(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)
 : _reader(std::move(reader)) {
 }
 
-void File::start(
-		not_null<FileDelegate*> delegate,
-		crl::time position,
-		bool hwAllow) {
+void File::start(not_null<FileDelegate*> 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> reader);
@@ -28,10 +35,7 @@ public:
 	File(const File &other) = delete;
 	File &operator=(const File &other) = delete;
 
-	void start(
-		not_null<FileDelegate*> delegate,
-		crl::time position,
-		bool hwAllow);
+	void start(not_null<FileDelegate*> delegate, StartOptions options);
 	void wake();
 	void stop(bool stillActive = false);
 
@@ -46,7 +50,7 @@ private:
 		Context(not_null<FileDelegate*> delegate, not_null<Reader*> reader);
 		~Context();
 
-		void start(crl::time position, bool hwAllow);
+		void start(StartOptions options);
 		void readNextPacket();
 
 		void interrupt();
@@ -79,7 +83,7 @@ private:
 			not_null<AVFormatContext *> format,
 			AVMediaType type,
 			Mode mode,
-			bool hwAllowed);
+			StartOptions options);
 		void seekToPosition(
 			not_null<AVFormatContext *> 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<Data::Story*> 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<Data::CloudTheme> 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<HistoryItem*> item;
+	MsgId topicRootId = 0;
+};
+
+struct OverlayWidget::StoriesContext {
+	not_null<PeerData*> peer;
+	StoryId id = 0;
+	Data::StoriesContext within;
+};
+
 class OverlayWidget::Show final : public ChatHelpers::Show {
 public:
 	explicit Show(not_null<OverlayWidget*> 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<Main::Session*> 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<PhotoData*> 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<ChatHelpers::FileChosen> override;
 	void storiesJumpTo(
 		not_null<Main::Session*> session,
-		FullStoryId id) override;
+		FullStoryId id,
+		Data::StoriesContext context) override;
 	void storiesClose() override;
 	bool storiesPaused() override;
 	rpl::producer<bool> 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<HistoryItem*> item;
-		MsgId topicRootId = 0;
-	};
-	struct StoriesContext {
-		not_null<PeerData*> 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<PeerData*> 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<PeerData*> 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 <johnprestonmail@gmail.com>
Date: Mon, 5 Jun 2023 16:50:43 +0400
Subject: [PATCH 057/259] 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<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
 	const auto dimensions = document->dimensions;
 	auto attributes = QVector<MTPDocumentAttribute>(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<MTPDocumentAttribute> 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<MTPDocumentAttribute> 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<MTPDocumentAttribute> 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<int>(
+crl::time Ringtones::maxDuration() const {
+	return crl::time(1000) * _session->account().appConfig().get<int>(
 		"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<MTPDocumentAttribute> &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<RoundData>();
-					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<Data::ReplyPreview> _replyPreview;
 	std::weak_ptr<Data::DocumentMedia> _media;
 	PhotoData *_goodThumbnailPhoto = nullptr;
+	crl::time _duration = -1;
 
 	Core::FileLocation _location;
 	std::unique_ptr<DocumentAdditionalData> _additional;
-	int32 _duration = -1;
 	mutable Flags _flags = kStreamingSupportedUnknown;
 	GoodThumbnailState _goodThumbnailState = GoodThumbnailState();
 	std::unique_ptr<FileLoader> _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<VoiceData> ProcessCaptureResult(
 		const ::Media::Capture::Result &data) {
 	auto voiceData = std::make_unique<VoiceData>();
-	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<HistoryDocumentThumbed>()) {
 		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<DocumentData*> 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<internal::FFMpegReaderImplementation>(&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<int>(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<long> = 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
 
 userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = UserStories;
 
@@ -1547,6 +1547,8 @@ stories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = 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<Peer>;
 chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = Updates;
 
-stories.sendStory#8b5c6986 flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> random_id:long = Updates;
+stories.sendStory#424cd47a flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> 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<MessageEntity> privacy_rules:flags.2?Vector<InputPrivacyRule> = Updates;
 stories.deleteStories#b5d501d7 id:Vector<int> = Vector<int>;
 stories.togglePinned#51602944 id:Vector<int> pinned:Bool = Vector<int>;
@@ -2095,3 +2097,4 @@ stories.readStories#edc5105b user_id:InputUser max_id:int = Vector<int>;
 stories.incrementStoryViews#22126127 user_id:InputUser id:Vector<int> = Bool;
 stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList;
 stories.getStoriesViews#9a75d6a6 id:Vector<int> = 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<Ui::SpoilerAnimation>([=] {
 	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<DocumentData*> _data;
+	const not_null<DocumentData*> _data;
 	mutable std::shared_ptr<Data::DocumentMedia> _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 <johnprestonmail@gmail.com>
Date: Mon, 5 Jun 2023 19:23:49 +0400
Subject: [PATCH 058/259] 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<PeerData*> 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<Session*> 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<UserData*> 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<FullStoryId>();
+	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<PeerData*> 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<PeerData*> 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<PeerData*> peer,
 		const QVector<MTPStoryItem> &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*> 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<int>(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<StoryId> &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<PeerData*> peer,
 		StoryMedia media,
-		TimeId date);
+		TimeId date,
+		TimeId expires);
 
 	[[nodiscard]] Session &owner() const;
 	[[nodiscard]] Main::Session &session() const;
 	[[nodiscard]] not_null<PeerData*> 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<StoryView> _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<UserData*> user;
-	base::flat_set<StoryIdDate> ids;
+	base::flat_set<StoryIdDates> 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<Session*> owner);
 	~Stories();
@@ -210,14 +217,23 @@ public:
 		std::optional<StoryView> offset,
 		Fn<void(std::vector<StoryView>)> done);
 
+	[[nodiscard]] const base::flat_set<StoryId> &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<PeerData*> peer,
-		const MTPDstoryItem &data);
-	StoryIdDate parseAndApply(
+		const MTPDstoryItem &data,
+		TimeId now);
+	StoryIdDates parseAndApply(
 		not_null<PeerData*> peer,
-		const MTPstoryItem &story);
+		const MTPstoryItem &story,
+		TimeId now);
 	void processResolvedStories(
 		not_null<PeerData*> peer,
 		const QVector<MTPStoryItem> &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*> story);
 	void sort(StorySourcesList list);
 
+	void addToExpiredMine(not_null<Story*> story);
+
 	void sendMarkAsReadRequests();
 	void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
 
 	void requestUserStories(not_null<UserData*> user);
+	void registerExpiring(TimeId expires, FullStoryId id);
+	void scheduleExpireTimer();
+	void processExpired();
 
 	const not_null<Session*> _owner;
 	base::flat_map<
 		PeerId,
 		base::flat_map<StoryId, std::unique_ptr<Story>>> _stories;
+	base::flat_multi_map<TimeId, FullStoryId> _expiring;
 	base::flat_set<FullStoryId> _deleted;
+	base::Timer _expireTimer;
+	bool _expireSchedulePosted = false;
 
 	base::flat_map<
 		PeerId,
@@ -261,6 +287,13 @@ private:
 
 	rpl::event_stream<PeerId> _itemsChanged;
 
+	base::flat_set<StoryId> _expiredMine;
+	int _expiredMineTotal = -1;
+	StoryId _expiredMineLastId = 0;
+	bool _expiredMineLoaded = false;
+	rpl::event_stream<> _expiredMineChanged;
+	mtpRequestId _expiredMineRequestId = 0;
+
 	base::flat_set<PeerId> _markReadPending;
 	base::Timer _markReadTimer;
 	base::flat_set<PeerId> _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<Ui::SlideWrap<Ui::SettingsButton>>(
+				_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 <johnprestonmail@gmail.com>
Date: Mon, 5 Jun 2023 20:31:15 +0400
Subject: [PATCH 059/259] 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<Id> nearest(Id id) const {
-		static_assert(std::is_same_v<IdsContainer, base::flat_set<MsgId>>);
+		static_assert(std::is_same_v<IdsContainer, base::flat_set<Id>>);
 		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<int> _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<PeerData*> 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<UserData*> 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*> 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<PeerId, StoriesSource> &Stories::all() const {
-	return _all;
+std::shared_ptr<HistoryItem> Stories::lookupItem(not_null<Story*> 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<HistoryItem> Stories::resolveItem(not_null<Story*> 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<HistoryItem>(
+		history->makeMessage(story).get(),
+		HistoryItem::Destroyer());
+	i->second = result;
+	return result;
+}
+
+std::shared_ptr<HistoryItem> Stories::resolveItem(FullStoryId id) {
+	const auto story = lookup(id);
+	return story ? resolveItem(*story) : std::shared_ptr<HistoryItem>();
+}
+
+const StoriesSource *Stories::source(PeerId id) const {
+	const auto i = _all.find(id);
+	return (i != end(_all)) ? &i->second : nullptr;
 }
 
 const std::vector<StoriesSourceInfo> &Stories::sources(
@@ -841,6 +907,10 @@ rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const {
 	return _sourcesChanged[static_cast<int>(list)].events();
 }
 
+rpl::producer<PeerId> Stories::sourceChanged() const {
+	return _sourceChanged.events();
+}
+
 rpl::producer<PeerId> Stories::itemsChanged() const {
 	return _itemsChanged.events();
 }
@@ -1102,7 +1172,7 @@ void Stories::loadViewsSlice(
 	}).send();
 }
 
-const base::flat_set<StoryId> &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<PeerId> 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<StoryId, std::greater<>> list;
+
+	friend inline bool operator==(
+		const StoriesIds&,
+		const StoriesIds&) = default;
+};
+
 struct StoryMedia {
 	std::variant<not_null<PhotoData*>, not_null<DocumentData*>> data;
 
@@ -192,19 +200,24 @@ public:
 
 	void loadMore(StorySourcesList list);
 	void apply(const MTPDupdateStory &data);
+	void apply(not_null<PeerData*> peer, const MTPUserStories *data);
 	void loadAround(FullStoryId id, StoriesContext context);
 
-	[[nodiscard]] const base::flat_map<PeerId, StoriesSource> &all() const;
+	const StoriesSource *source(PeerId id) const;
 	[[nodiscard]] const std::vector<StoriesSourceInfo> &sources(
 		StorySourcesList list) const;
 	[[nodiscard]] bool sourcesLoaded(StorySourcesList list) const;
 	[[nodiscard]] rpl::producer<> sourcesChanged(
 		StorySourcesList list) const;
+	[[nodiscard]] rpl::producer<PeerId> sourceChanged() const;
 	[[nodiscard]] rpl::producer<PeerId> itemsChanged() const;
 
 	[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
 		FullStoryId id) const;
 	void resolve(FullStoryId id, Fn<void()> done);
+	[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(FullStoryId id);
+	[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(
+		not_null<Story*> story);
 
 	[[nodiscard]] bool isQuitPrevent();
 	void markAsRead(FullStoryId id, bool viewed);
@@ -217,14 +230,29 @@ public:
 		std::optional<StoryView> offset,
 		Fn<void(std::vector<StoryView>)> done);
 
-	[[nodiscard]] const base::flat_set<StoryId> &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<PeerId> 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<PeerData*> peer,
@@ -247,20 +275,25 @@ private:
 	void removeDependencyStory(not_null<Story*> story);
 	void sort(StorySourcesList list);
 
-	void addToExpiredMine(not_null<Story*> story);
+	[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
+		not_null<Story*> story);
 
 	void sendMarkAsReadRequests();
 	void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
 
 	void requestUserStories(not_null<UserData*> user);
+	void addToExpiredMine(not_null<Story*> story);
 	void registerExpiring(TimeId expires, FullStoryId id);
 	void scheduleExpireTimer();
 	void processExpired();
 
 	const not_null<Session*> _owner;
-	base::flat_map<
+	std::unordered_map<
 		PeerId,
 		base::flat_map<StoryId, std::unique_ptr<Story>>> _stories;
+	std::unordered_map<
+		PeerId,
+		base::flat_map<StoryId, std::weak_ptr<HistoryItem>>> _items;
 	base::flat_multi_map<TimeId, FullStoryId> _expiring;
 	base::flat_set<FullStoryId> _deleted;
 	base::Timer _expireTimer;
@@ -273,11 +306,11 @@ private:
 		PeerId,
 		base::flat_map<StoryId, std::vector<Fn<void()>>>> _resolveSent;
 
-	std::map<
+	std::unordered_map<
 		not_null<Data::Story*>,
 		base::flat_set<not_null<HistoryItem*>>> _dependentMessages;
 
-	base::flat_map<PeerId, StoriesSource> _all;
+	std::unordered_map<PeerId, StoriesSource> _all;
 	std::vector<StoriesSourceInfo> _sources[kStorySourcesListCount];
 	rpl::event_stream<> _sourcesChanged[kStorySourcesListCount];
 	bool _sourcesLoaded[kStorySourcesListCount] = { false };
@@ -285,15 +318,19 @@ private:
 
 	mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 };
 
+	rpl::event_stream<PeerId> _sourceChanged;
 	rpl::event_stream<PeerId> _itemsChanged;
 
-	base::flat_set<StoryId> _expiredMine;
+	StoriesIds _expiredMine;
 	int _expiredMineTotal = -1;
 	StoryId _expiredMineLastId = 0;
 	bool _expiredMineLoaded = false;
 	rpl::event_stream<> _expiredMineChanged;
 	mtpRequestId _expiredMineRequestId = 0;
 
+	std::unordered_map<PeerId, Saved> _saved;
+	rpl::event_stream<PeerId> _savedChanged;
+
 	base::flat_set<PeerId> _markReadPending;
 	base::Timer _markReadTimer;
 	base::flat_set<PeerId> _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<StoriesIdsSlice> SavedStoriesIds(
+		not_null<PeerData*> peer,
+		StoryId aroundId,
+		int limit) {
+	return [=](auto consumer) {
+		auto lifetime = rpl::lifetime();
+
+		struct State {
+			StoriesIdsSlice slice;
+		};
+		const auto state = lifetime.make_state<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<StoryId>();
+			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<StoriesIdsSlice> ArchiveStoriesIds(
+		not_null<Main::Session*> session,
+		StoryId aroundId,
+		int limit) {
+	return [=](auto consumer) {
+		auto lifetime = rpl::lifetime();
+
+		struct State {
+			StoriesIdsSlice slice;
+		};
+		const auto state = lifetime.make_state<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<StoryId>();
+			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<base::flat_set<StoryId>>;
+
+[[nodiscard]] rpl::producer<StoriesIdsSlice> SavedStoriesIds(
+	not_null<PeerData*> peer,
+	StoryId aroundId,
+	int limit);
+
+[[nodiscard]] rpl::producer<StoriesIdsSlice> ArchiveStoriesIds(
+	not_null<Main::Session*> 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<MessageFlag>;
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<UserData*> 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::Stories*> 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<Userpic>();
-		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<Data::Media> 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*> history,
+	not_null<Data::Story*> 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<Data::Story*> story) {
+	Expects(_flags & MessageFlag::StoryItem);
+	Expects(StoryIdFromMsgId(id) == story->id());
+
+	_media = nullptr;
+	setStoryFields(story);
+
+	finishEdition(-1);
+}
+
+void HistoryItem::setStoryFields(not_null<Data::Story*> story) {
+	const auto spoiler = false;
+	if (const auto photo = story->photo()) {
+		_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);
+	} else {
+		const auto document = story->document();
+		_media = std::make_unique<Data::MediaFile>(
+			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<GameData*> game,
 		HistoryMessageMarkupData &&markup);
+	HistoryItem(not_null<History*> history, not_null<Data::Story*> story);
 	~HistoryItem();
 
 	struct Destroyer {
@@ -332,6 +333,7 @@ public:
 
 	[[nodiscard]] bool isService() const;
 	void applyEdition(HistoryMessageEdition &&edition);
+	void applyChanges(not_null<Data::Story*> 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<Data::Story*> 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*> controller)
 }
 
 Memento::Memento(not_null<UserData*> 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 <typename Widget>
 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<PollData*> 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<PollData*> poll, FullMsgId contextId)
 : _value(PollKey{ poll, contextId }) {
 }
@@ -72,6 +75,20 @@ bool Key::isDownloads() const {
 	return v::is<Downloads::Tag>(_value);
 }
 
+PeerData *Key::storiesPeer() const {
+	if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
+		return tag->peer;
+	}
+	return nullptr;
+}
+
+Stories::Tab Key::storiesTab() const {
+	if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
+		return tag->tab;
+	}
+	return Stories::Tab();
+}
+
 PollData *Key::poll() const {
 	if (const auto data = std::get_if<PollKey>(&_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<PeerData*> peer, Tab tab = {})
+	: peer(peer)
+	, tab(tab) {
+	}
+
+	not_null<PeerData*> peer;
+	Tab tab = {};
+};
+
+} // namespace Info::Stories
+
 namespace Info {
 
 class Key {
@@ -44,12 +63,15 @@ public:
 	explicit Key(not_null<Data::ForumTopic*> topic);
 	Key(Settings::Tag settings);
 	Key(Downloads::Tag downloads);
+	Key(Stories::Tag stories);
 	Key(not_null<PollData*> 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<Data::ForumTopic*>,
 		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<Window::SessionController*> 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 <rpl/mappers.h>
 #include <rpl/map.h>
 #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<Window::SessionNavigation*> navigation,
+		not_null<UserData*> 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<AbstractController*> controller) {
 	if (controller->isDownloads()) {
 		return std::make_unique<Downloads::Provider>(controller);
+	} else if (controller->storiesPeer()) {
+		return std::make_unique<Stories::Provider>(controller);
 	}
 	return std::make_unique<Provider>(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<const HistoryItem*> item);
 	void itemLayoutChanged(not_null<const HistoryItem*> item);
 
-	void refreshViewer();
 	void refreshRows();
 	void trackSession(not_null<Main::Session*> 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*> 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<Ui::RpWidget> InnerWidget::setupSharedMedia(
 			icon,
 			st::infoSharedMediaButtonIconPosition);
 	};
+	auto addStoriesButton = [&](
+			not_null<UserData*> user,
+			const style::icon &icon) {
+		auto result = Media::AddStoriesButton(
+			content,
+			_controller,
+			user,
+			tracker);
+		object_ptr<Profile::FloatingIcon>(
+			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<int> fullHeightValue);
+
+protected:
+	int resizeGetHeight(int newWidth) override;
+
+	void paintEvent(QPaintEvent *e) override;
+
+private:
+	object_ptr<Ui::FlatLabel> _text;
+	int _height = 0;
+
+};
+
+EmptyWidget::EmptyWidget(QWidget *parent)
+: RpWidget(parent)
+, _text(this, st::infoEmptyLabel) {
+}
+
+void EmptyWidget::setFullHeight(rpl::producer<int> 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*> 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*> memento) {
+	if (memento->section().type() == Section::Type::Stories) {
+		restoreState(memento);
+		return true;
+	}
+	return false;
+}
+
+object_ptr<Media::ListWidget> InnerWidget::setupList() {
+	auto result = object_ptr<Media::ListWidget>(
+		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*> memento) {
+	_list->saveState(&memento->media());
+}
+
+void InnerWidget::restoreState(not_null<Memento*> memento) {
+	_list->restoreState(&memento->media());
+}
+
+rpl::producer<SelectedItems> 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<int> 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<Ui::ScrollToRequest> 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*> controller);
+
+	bool showInternal(not_null<Memento*> memento);
+
+	void saveState(not_null<Memento*> memento);
+	void restoreState(not_null<Memento*> memento);
+
+	void setScrollHeightValue(rpl::producer<int> value);
+
+	rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
+	rpl::producer<SelectedItems> 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<Media::ListWidget> setupList();
+
+	const not_null<Controller*> _controller;
+
+	object_ptr<Media::ListWidget> _list = { nullptr };
+	object_ptr<EmptyWidget> _empty;
+
+	bool _inResize = false;
+
+	rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
+	rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
+	rpl::event_stream<rpl::producer<int>> _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<AbstractController*> 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<bool> Provider::hasSelectRestrictionChanges() {
+	return rpl::never<bool>();
+}
+
+bool Provider::sectionHasFloatingHeader() {
+	return false;
+}
+
+QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
+	return QString();
+}
+
+bool Provider::sectionItemBelongsHere(
+		not_null<const BaseLayout*> item,
+		not_null<const BaseLayout*> previous) {
+	return true;
+}
+
+bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
+	return true;
+}
+
+std::optional<int> 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<BaseLayout*> topLayout,
+		not_null<BaseLayout*> 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<BaseLayout*> 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<ListSection> Provider::fillSections(
+		not_null<Overview::Layout::Delegate*> delegate) {
+	markLayoutsStale();
+	const auto guard = gsl::finally([&] { clearStaleLayouts(); });
+
+	auto result = std::vector<ListSection>();
+	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<not_null<BaseLayout*>> Provider::layoutRemoved() {
+	return _layoutRemoved.events();
+}
+
+BaseLayout *Provider::lookupLayout(const HistoryItem *item) {
+	return nullptr;
+}
+
+bool Provider::isMyItem(not_null<const HistoryItem*> item) {
+	return IsStoryMsgId(item->id) && (item->history()->peer == _peer);
+}
+
+bool Provider::isAfter(
+		not_null<const HistoryItem*> a,
+		not_null<const HistoryItem*> b) {
+	return (a->id < b->id);
+}
+
+void Provider::itemRemoved(not_null<const HistoryItem*> 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<Overview::Layout::Delegate*> 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<BaseLayout> Provider::createLayout(
+		StoryId id,
+		not_null<Overview::Layout::Delegate*> 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<Photo>(delegate, item, photo, spoiler);
+	} else if (const auto file = getFile()) {
+		return std::make_unique<Video>(delegate, item, file, spoiler);
+	}
+	return nullptr;
+}
+
+ListItemSelectionData Provider::computeSelectionData(
+		not_null<const HistoryItem*> item,
+		TextSelection selection) {
+	auto result = ListItemSelectionData(selection);
+	result.canDelete = true;
+	result.canForward = item->allowsForward()
+		&& (&item->history()->session() == &_controller->session());
+	return result;
+}
+
+void Provider::applyDragSelection(
+		ListSelectedMap &selected,
+		not_null<const HistoryItem*> fromItem,
+		bool skipFrom,
+		not_null<const HistoryItem*> tillItem,
+		bool skipTill) {
+	const auto fromId = fromItem->id - (skipFrom ? 1 : 0);
+	const auto tillId = tillItem->id - (skipTill ? 0 : 1);
+	for (auto i = selected.begin(); i != selected.end();) {
+		const auto itemId = i->first->id;
+		if (itemId > fromId || itemId <= tillId) {
+			i = selected.erase(i);
+		} else {
+			++i;
+		}
+	}
+	for (auto &layoutItem : _layouts) {
+		const auto id = StoryIdToMsgId(layoutItem.first);
+		if (id <= fromId && id > tillId) {
+			const auto i = _items.find(id);
+			Assert(i != end(_items));
+			const auto item = i->second.get();
+			ChangeItemSelection(
+				selected,
+				item,
+				computeSelectionData(item, FullSelection));
+		}
+	}
+}
+
+bool Provider::allowSaveFileAs(
+		not_null<const HistoryItem*> item,
+		not_null<DocumentData*> document) {
+	return false;
+}
+
+QString Provider::showInFolderPath(
+		not_null<const HistoryItem*> item,
+		not_null<DocumentData*> document) {
+	return QString();
+}
+
+int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
+	return StoryIdFromMsgId(item->id);
+}
+
+HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {
+	if (state.item && _slice.indexOf(StoryIdFromMsgId(state.item->id))) {
+		return state.item;
+	} else if (const auto id = _slice.nearest(state.position)) {
+		const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id));
+		if (const auto item = _controller->session().data().message(full)) {
+			return item;
+		}
+	}
+	return state.item;
+}
+
+void Provider::saveState(
+		not_null<Media::Memento*> memento,
+		ListScrollTopState scrollState) {
+	if (_aroundId != kDefaultAroundId && scrollState.item) {
+		memento->setAroundId({ _peer->id, _aroundId });
+		memento->setIdsLimit(_idsLimit);
+		memento->setScrollTopItem(scrollState.item->globalId());
+		memento->setScrollTopItemPosition(scrollState.position);
+		memento->setScrollTopShift(scrollState.shift);
+	}
+}
+
+void Provider::restoreState(
+		not_null<Media::Memento*> memento,
+		Fn<void(ListScrollTopState)> restoreScrollState) {
+	if (const auto limit = memento->idsLimit()) {
+		const auto wasAroundId = memento->aroundId();
+		if (wasAroundId.peer == _peer->id) {
+			_idsLimit = limit;
+			_aroundId = StoryIdFromMsgId(wasAroundId.msg);
+			restoreScrollState({
+				.position = memento->scrollTopItemPosition(),
+				.item = MessageByGlobalId(memento->scrollTopItem()),
+				.shift = memento->scrollTopShift(),
+			});
+			refreshViewer();
+		}
+	}
+}
+
+} // namespace Info::Stories
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.h b/Telegram/SourceFiles/info/stories/info_stories_provider.h
new file mode 100644
index 000000000..f1fb535d3
--- /dev/null
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.h
@@ -0,0 +1,132 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/weak_ptr.h"
+#include "data/data_stories_ids.h"
+#include "info/media/info_media_common.h"
+
+class DocumentData;
+class HistoryItem;
+class PeerData;
+class History;
+
+namespace Info {
+class AbstractController;
+} // namespace Info
+
+namespace Info::Stories {
+
+enum class Tab;
+
+class Provider final
+	: public Media::ListProvider
+	, private Media::ListSectionDelegate
+	, public base::has_weak_ptr {
+public:
+	explicit Provider(not_null<AbstractController*> controller);
+
+	Media::Type type() override;
+	bool hasSelectRestriction() override;
+	rpl::producer<bool> hasSelectRestrictionChanges() override;
+	bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
+
+	std::optional<int> fullCount() override;
+
+	void restart() override;
+	void checkPreload(
+		QSize viewport,
+		not_null<Media::BaseLayout*> topLayout,
+		not_null<Media::BaseLayout*> bottomLayout,
+		bool preloadTop,
+		bool preloadBottom) override;
+	void refreshViewer() override;
+	rpl::producer<> refreshed() override;
+
+	void setSearchQuery(QString query) override;
+
+	std::vector<Media::ListSection> fillSections(
+		not_null<Overview::Layout::Delegate*> delegate) override;
+	rpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;
+	Media::BaseLayout *lookupLayout(const HistoryItem *item) override;
+	bool isMyItem(not_null<const HistoryItem*> item) override;
+	bool isAfter(
+		not_null<const HistoryItem*> a,
+		not_null<const HistoryItem*> b) override;
+
+	Media::ListItemSelectionData computeSelectionData(
+		not_null<const HistoryItem*> item,
+		TextSelection selection) override;
+	void applyDragSelection(
+		Media::ListSelectedMap &selected,
+		not_null<const HistoryItem*> fromItem,
+		bool skipFrom,
+		not_null<const HistoryItem*> tillItem,
+		bool skipTill) override;
+
+	bool allowSaveFileAs(
+		not_null<const HistoryItem*> item,
+		not_null<DocumentData*> document) override;
+	QString showInFolderPath(
+		not_null<const HistoryItem*> item,
+		not_null<DocumentData*> document) override;
+
+	int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
+	HistoryItem *scrollTopStateItem(
+		Media::ListScrollTopState state) override;
+	void saveState(
+		not_null<Media::Memento*> memento,
+		Media::ListScrollTopState scrollState) override;
+	void restoreState(
+		not_null<Media::Memento*> memento,
+		Fn<void(Media::ListScrollTopState)> restoreScrollState) override;
+
+private:
+	static constexpr auto kMinimalIdsLimit = 16;
+	static constexpr auto kDefaultAroundId = ServerMaxStoryId - 1;
+
+	bool sectionHasFloatingHeader() override;
+	QString sectionTitle(not_null<const Media::BaseLayout*> item) override;
+	bool sectionItemBelongsHere(
+		not_null<const Media::BaseLayout*> item,
+		not_null<const Media::BaseLayout*> previous) override;
+
+	void itemRemoved(not_null<const HistoryItem*> item);
+	void markLayoutsStale();
+	void clearStaleLayouts();
+
+	[[nodiscard]] HistoryItem *ensureItem(StoryId id);
+	[[nodiscard]] Media::BaseLayout *getLayout(
+		StoryId id,
+		not_null<Overview::Layout::Delegate*> delegate);
+	[[nodiscard]] std::unique_ptr<Media::BaseLayout> createLayout(
+		StoryId id,
+		not_null<Overview::Layout::Delegate*> delegate);
+
+	const not_null<AbstractController*> _controller;
+	const not_null<PeerData*> _peer;
+	const not_null<History*> _history;
+	const Tab _tab;
+
+	StoryId _aroundId = kDefaultAroundId;
+	int _idsLimit = kMinimalIdsLimit;
+	Data::StoriesIdsSlice _slice;
+
+	base::flat_map<StoryId, std::shared_ptr<HistoryItem>> _items;
+	std::unordered_map<StoryId, Media::CachedItem> _layouts;
+	rpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;
+	rpl::event_stream<> _refreshed;
+
+	bool _started = false;
+
+	rpl::lifetime _lifetime;
+	rpl::lifetime _viewerLifetime;
+
+};
+
+} // namespace Info::Stories
diff --git a/Telegram/SourceFiles/info/stories/info_stories_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_widget.cpp
new file mode 100644
index 000000000..67b8f602b
--- /dev/null
+++ b/Telegram/SourceFiles/info/stories/info_stories_widget.cpp
@@ -0,0 +1,112 @@
+/*
+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_widget.h"
+
+#include "info/stories/info_stories_inner_widget.h"
+#include "info/info_controller.h"
+#include "info/info_memento.h"
+#include "ui/widgets/scroll_area.h"
+#include "lang/lang_keys.h"
+
+namespace Info::Stories {
+
+Memento::Memento(not_null<Controller*> controller)
+: ContentMemento(Tag{ controller->storiesPeer(), controller->storiesTab() })
+, _media(controller) {
+}
+
+Memento::Memento(not_null<PeerData*> peer)
+: ContentMemento(Tag{ peer })
+, _media(peer, 0, Media::Type::PhotoVideo) {
+}
+
+Memento::~Memento() = default;
+
+Section Memento::section() const {
+	return Section(Section::Type::Stories);
+}
+
+object_ptr<ContentWidget> Memento::createWidget(
+		QWidget *parent,
+		not_null<Controller*> controller,
+		const QRect &geometry) {
+	auto result = object_ptr<Widget>(parent, controller);
+	result->setInternalState(geometry, this);
+	return result;
+}
+
+Widget::Widget(
+	QWidget *parent,
+	not_null<Controller*> controller)
+: ContentWidget(parent, controller) {
+	_inner = setInnerWidget(object_ptr<InnerWidget>(
+		this,
+		controller));
+	_inner->setScrollHeightValue(scrollHeightValue());
+	_inner->scrollToRequests(
+	) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
+		scrollTo(request);
+	}, _inner->lifetime());
+}
+
+bool Widget::showInternal(not_null<ContentMemento*> memento) {
+	if (!controller()->validateMementoPeer(memento)) {
+		return false;
+	}
+	if (auto storiesMemento = dynamic_cast<Memento*>(memento.get())) {
+		restoreState(storiesMemento);
+		return true;
+	}
+	return false;
+}
+
+void Widget::setInternalState(
+		const QRect &geometry,
+		not_null<Memento*> memento) {
+		setGeometry(geometry);
+	Ui::SendPendingMoveResizeEvents(this);
+	restoreState(memento);
+}
+
+std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
+	auto result = std::make_shared<Memento>(controller());
+	saveState(result.get());
+	return result;
+}
+
+void Widget::saveState(not_null<Memento*> memento) {
+	memento->setScrollTop(scrollTopSave());
+	_inner->saveState(memento);
+}
+
+void Widget::restoreState(not_null<Memento*> memento) {
+	_inner->restoreState(memento);
+	scrollTopRestore(memento->scrollTop());
+}
+
+rpl::producer<SelectedItems> Widget::selectedListValue() const {
+	return _inner->selectedListValue();
+}
+
+void Widget::selectionAction(SelectionAction action) {
+	_inner->selectionAction(action);
+}
+
+rpl::producer<QString> Widget::title() {
+	return tr::lng_menu_my_stories(); // #TODO stories
+}
+
+std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
+	return std::make_shared<Info::Memento>(
+		std::vector<std::shared_ptr<ContentMemento>>(
+			1,
+			std::make_shared<Memento>(peer)));
+}
+
+} // namespace Info::Stories
+
diff --git a/Telegram/SourceFiles/info/stories/info_stories_widget.h b/Telegram/SourceFiles/info/stories/info_stories_widget.h
new file mode 100644
index 000000000..17e96a1d6
--- /dev/null
+++ b/Telegram/SourceFiles/info/stories/info_stories_widget.h
@@ -0,0 +1,70 @@
+/*
+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 "info/info_content_widget.h"
+#include "info/media/info_media_widget.h"
+
+namespace Info::Stories {
+
+class InnerWidget;
+
+class Memento final : public ContentMemento {
+public:
+	Memento(not_null<Controller*> controller);
+	Memento(not_null<PeerData*> peer);
+	~Memento();
+
+	object_ptr<ContentWidget> createWidget(
+		QWidget *parent,
+		not_null<Controller*> controller,
+		const QRect &geometry) override;
+
+	Section section() const override;
+
+	[[nodiscard]] Media::Memento &media() {
+		return _media;
+	}
+	[[nodiscard]] const Media::Memento &media() const {
+		return _media;
+	}
+
+private:
+	Media::Memento _media;
+
+};
+
+class Widget final : public ContentWidget {
+public:
+	Widget(QWidget *parent, not_null<Controller*> controller);
+
+	bool showInternal(
+		not_null<ContentMemento*> memento) override;
+
+	void setInternalState(
+		const QRect &geometry,
+		not_null<Memento*> memento);
+
+	rpl::producer<SelectedItems> selectedListValue() const override;
+	void selectionAction(SelectionAction action) override;
+
+	rpl::producer<QString> title() override;
+
+private:
+	void saveState(not_null<Memento*> memento);
+	void restoreState(not_null<Memento*> memento);
+
+	std::shared_ptr<ContentMemento> doCreateMemento() override;
+
+	InnerWidget *_inner = nullptr;
+
+};
+
+[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
+
+} // namespace Info::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index bb7337650..76f04f871 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -393,12 +393,10 @@ void Controller::show(
 	auto &stories = story->owner().stories();
 	const auto storyId = story->fullId();
 	const auto id = storyId.story;
-	const auto &all = stories.all();
-	const auto inAll = all.find(storyId.peer);
-	auto source = (inAll != end(all)) ? &inAll->second : nullptr;
+	auto source = stories.source(storyId.peer);
 	auto single = StoriesSource{ story->peer()->asUser() };
 	v::match(context.data, [&](StoriesContextSingle) {
-		source = &single;
+		source = nullptr;
 		hideSiblings();
 	}, [&](StoriesContextPeer) {
 		hideSiblings();
@@ -423,17 +421,11 @@ void Controller::show(
 		}
 	});
 	const auto idDates = story->idDates();
-	if (!source) {
-		return;
-	} else if (source == &single) {
+	_index = source ? (source->ids.find(idDates) - begin(source->ids)) : 0;
+	if (!source || _index == source->ids.size()) {
+		source = &single;
 		single.ids.emplace(idDates);
 		_index = 0;
-	} else {
-		const auto k = source->ids.find(idDates);
-		if (k == end(source->ids)) {
-			return;
-		}
-		_index = (k - begin(source->ids));
 	}
 	const auto guard = gsl::finally([&] {
 		_paused = false;
@@ -543,12 +535,11 @@ void Controller::showSibling(
 		sibling = nullptr;
 		return;
 	}
-	const auto &all = session->data().stories().all();
-	const auto i = all.find(peerId);
-	if (i == end(all)) {
+	const auto source = session->data().stories().source(peerId);
+	if (!source) {
 		sibling = nullptr;
-	} else if (!sibling || !sibling->shows(i->second)) {
-		sibling = std::make_unique<Sibling>(this, i->second);
+	} else if (!sibling || !sibling->shows(*source)) {
+		sibling = std::make_unique<Sibling>(this, *source);
 	}
 }
 
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 9e3340c1c..c51984f6a 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -42,6 +42,7 @@ inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0
 inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;
 inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
 inputMediaDice#e66fbf7b emoticon:string = InputMedia;
+inputMediaStory#9a86b58f user_id:InputUser id:int = InputMedia;
 
 inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
 inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto;
@@ -128,6 +129,7 @@ messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true tes
 messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
 messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
 messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
+messageMediaStory#c79aee11 user_id:long id:int = MessageMedia;
 
 messageActionEmpty#b6aef7b0 = MessageAction;
 messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;
diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp
index 1a03e874f..b2f213527 100644
--- a/Telegram/SourceFiles/window/window_main_menu.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu.cpp
@@ -39,6 +39,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "settings/settings_information.h"
 #include "info/profile/info_profile_badge.h"
 #include "info/profile/info_profile_emoji_status_panel.h"
+#include "info/stories/info_stories_widget.h"
+#include "info/info_memento.h"
 #include "base/qt_signal_producer.h"
 #include "boxes/about_box.h"
 #include "ui/boxes/confirm_box.h"
@@ -766,16 +768,16 @@ void MainMenu::setupMenu() {
 				_menu,
 				CreateButton(
 					_menu,
-					rpl::single(u"My Stories"_q),
+					tr::lng_menu_my_stories(),
 					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())
+		const auto mine = stories->source(
+			controller->session().userPeerId());
+		if ((mine && !mine->ids.empty())
 			|| stories->expiredMineCount() > 0) {
 			wrap->toggle(true, anim::type::instant);
 		} else {
@@ -789,7 +791,8 @@ void MainMenu::setupMenu() {
 			}
 		}
 		wrap->entity()->setClickedCallback([=] {
-			controller->showToast(u"My Stories"_q);
+			controller->showSection(
+				Info::Stories::Make(controller->session().user()));
 		});
 	} else {
 		addAction(
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index a39f552d2..d6214affc 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -2143,6 +2143,9 @@ void SessionController::openPhoto(
 		not_null<PhotoData*> photo,
 		FullMsgId contextId,
 		MsgId topicRootId) {
+	if (openStory(contextId)) {
+		return;
+	}
 	_window->openInMediaView(Media::View::OpenRequest(
 		this,
 		photo,
@@ -2161,7 +2164,9 @@ void SessionController::openDocument(
 		FullMsgId contextId,
 		MsgId topicRootId,
 		bool showInMediaView) {
-	if (showInMediaView) {
+	if (openStory(contextId)) {
+		return;
+	} else if (showInMediaView) {
 		_window->openInMediaView(Media::View::OpenRequest(
 			this,
 			document,
@@ -2176,6 +2181,31 @@ void SessionController::openDocument(
 		topicRootId);
 }
 
+bool SessionController::openStory(
+		FullMsgId fakeItemId,
+		bool forceArchiveContext) {
+	if (!peerIsUser(fakeItemId.peer)
+		|| !IsStoryMsgId(fakeItemId.msg)) {
+		return false;
+	}
+	const auto maybeStory = session().data().stories().lookup({
+		fakeItemId.peer,
+		StoryIdFromMsgId(fakeItemId.msg),
+	});
+	if (maybeStory) {
+		using namespace Data;
+		const auto story = *maybeStory;
+		const auto context = !story->expired()
+			? StoriesContext{ StoriesContextPeer() }
+			: (story->pinned() && !forceArchiveContext)
+			? StoriesContext{ StoriesContextSaved() }
+			: StoriesContext{ StoriesContextArchive() };
+		_window->openInMediaView(
+			::Media::View::OpenRequest(this, *maybeStory, context));
+	}
+	return true;
+}
+
 auto SessionController::cachedChatThemeValue(
 	const Data::CloudTheme &data,
 	const Data::WallPaper &paper,
@@ -2484,14 +2514,12 @@ void SessionController::openPeerStories(
 	using namespace Data;
 
 	auto &stories = session().data().stories();
-	const auto &all = stories.all();
-	const auto i = all.find(peerId);
-	if (i != end(all)) {
-		const auto j = i->second.ids.lower_bound(
-			StoryIdDates{ i->second.readTill + 1 });
+	if (const auto source = stories.source(peerId)) {
+		const auto j = source->ids.lower_bound(
+			StoryIdDates{ source->readTill + 1 });
 		openPeerStory(
-			i->second.user,
-			j != i->second.ids.end() ? j->id : i->second.ids.front().id,
+			source->user,
+			j != source->ids.end() ? j->id : source->ids.front().id,
 			{ list });
 	}
 }
diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h
index 5e1139ad1..f92f7d98f 100644
--- a/Telegram/SourceFiles/window/window_session_controller.h
+++ b/Telegram/SourceFiles/window/window_session_controller.h
@@ -491,6 +491,7 @@ public:
 		FullMsgId contextId,
 		MsgId topicRootId,
 		bool showInMediaView = false);
+	bool openStory(FullMsgId fakeItemId, bool forceArchiveContext = false);
 
 	void showChooseReportMessages(
 		not_null<PeerData*> peer,

From 5e5b252f2fb01a34ed9d5ac9024e3329e061bf76 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 7 Jun 2023 19:56:38 +0400
Subject: [PATCH 060/259] Support correct saved stories / archive loading.

---
 Telegram/Resources/langs/lang.strings         |   4 +
 Telegram/SourceFiles/data/data_stories.cpp    | 203 ++++++++++++------
 Telegram/SourceFiles/data/data_stories.h      |  27 +--
 .../SourceFiles/data/data_stories_ids.cpp     | 156 ++++++++++----
 Telegram/SourceFiles/info/info_controller.cpp |   3 +-
 .../info/media/info_media_buttons.h           |   6 +-
 .../info/media/info_media_inner_widget.h      |   7 +-
 .../stories/info_stories_inner_widget.cpp     |  59 ++++-
 .../info/stories/info_stories_inner_widget.h  |   9 +
 .../info/stories/info_stories_widget.cpp      |  24 ++-
 .../info/stories/info_stories_widget.h        |   9 +-
 Telegram/SourceFiles/mtproto/scheme/api.tl    |   2 +-
 Telegram/SourceFiles/settings/settings.style  |   3 -
 .../settings/settings_privacy_security.cpp    |  19 +-
 .../SourceFiles/window/window_main_menu.cpp   |  13 +-
 15 files changed, 382 insertions(+), 162 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 40783dbab..154400cb6 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3800,6 +3800,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_views#other" = "{count} views";
 "lng_stories_no_views" = "No views";
 
+"lng_stories_my_title" = "My Stories";
+"lng_stories_archive_button" = "Archive";
+"lng_stories_archive_title" = "Stories Archive";
+
 // Wnd specific
 
 "lng_wnd_choose_program_menu" = "Choose Default Program...";
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index fc164eb17..972e3c69a 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -33,8 +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;
+constexpr auto kArchiveFirstPerPage = 30;
+constexpr auto kArchivePerPage = 100;
 constexpr auto kSavedFirstPerPage = 30;
 constexpr auto kSavedPerPage = 100;
 
@@ -333,6 +333,9 @@ void Stories::apply(const MTPDupdateStory &data) {
 	const auto wasInfo = i->second.info();
 	i->second.ids.emplace(idDates);
 	const auto nowInfo = i->second.info();
+	if (user->isSelf() && i->second.readTill < idDates.id) {
+		i->second.readTill = idDates.id;
+	}
 	if (wasInfo == nowInfo) {
 		return;
 	}
@@ -361,15 +364,44 @@ void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {
 		if (i != end(_stories)) {
 			auto stories = base::take(i->second);
 			_stories.erase(i);
+
+			auto archiveChanged = false;
+			auto savedChanged = false;
+			if (peer->isSelf()) {
+				for (const auto &[id, story] : stories) {
+					if (_archive.list.remove(id)) {
+						archiveChanged = true;
+						if (_archiveTotal > 0) {
+							--_archiveTotal;
+						}
+					}
+				}
+			}
+			const auto j = _saved.find(peer->id);
+			const auto saved = (j != end(_saved)) ? &j->second : nullptr;
 			for (const auto &[id, story] : stories) {
 				// Duplicated in Stories::applyDeleted.
 				_deleted.emplace(FullStoryId{ peer->id, id });
 				_expiring.remove(story->expires(), story->fullId());
+				if (story->pinned() && saved) {
+					if (saved->ids.list.remove(id)) {
+						savedChanged = true;
+						if (saved->total > 0) {
+							--saved->total;
+						}
+					}
+				}
 				session().changes().storyUpdated(
 					story.get(),
 					UpdateFlag::Destroyed);
 				removeDependencyStory(story.get());
 			}
+			if (archiveChanged) {
+				_archiveChanged.fire({});
+			}
+			if (savedChanged) {
+				_savedChanged.fire_copy(peer->id);
+			}
 		}
 		_sourceChanged.fire_copy(peer->id);
 	} else {
@@ -471,6 +503,8 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 	if (result.ids.empty()) {
 		applyDeletedFromSources(peerId, StorySourcesList::All);
 		return;
+	} else if (user->isSelf()) {
+		result.readTill = result.ids.back().id;
 	}
 	const auto info = result.info();
 	const auto i = _all.find(peerId);
@@ -526,15 +560,20 @@ Story *Stories::parseAndApply(
 	auto &stories = _stories[peer->id];
 	const auto i = stories.find(id);
 	if (i != end(stories)) {
-		if (i->second->applyChanges(*media, data)) {
+		const auto result = i->second.get();
+		const auto pinned = result->pinned();
+		if (result->applyChanges(*media, data)) {
+			if (result->pinned() != pinned) {
+				savedStateUpdated(result);
+			}
 			session().changes().storyUpdated(
-				i->second.get(),
+				result,
 				UpdateFlag::Edited);
-			if (const auto item = lookupItem(i->second.get())) {
-				item->applyChanges(i->second.get());
+			if (const auto item = lookupItem(result)) {
+				item->applyChanges(result);
 			}
 		}
-		return i->second.get();
+		return result;
 	}
 	const auto result = stories.emplace(id, std::make_unique<Story>(
 		id,
@@ -543,6 +582,19 @@ Story *Stories::parseAndApply(
 		data.vdate().v,
 		data.vexpire_date().v)).first->second.get();
 	result->applyChanges(*media, data);
+	if (result->pinned()) {
+		savedStateUpdated(result);
+	}
+
+	if (peer->isSelf()) {
+		const auto added = _archive.list.emplace(id).second;
+		if (added) {
+			if (_archiveTotal >= 0 && id > _archiveLastId) {
+				++_archiveTotal;
+			}
+			_archiveChanged.fire({});
+		}
+	}
 
 	if (expired) {
 		_expiring.remove(expires, result->fullId());
@@ -617,6 +669,30 @@ void Stories::unregisterDependentMessage(
 	}
 }
 
+void Stories::savedStateUpdated(not_null<Story*> story) {
+	const auto id = story->id();
+	const auto peer = story->peer()->id;
+	const auto pinned = story->pinned();
+	if (pinned) {
+		auto &saved = _saved[peer];
+		const auto added = saved.ids.list.emplace(id).second;
+		if (added) {
+			if (saved.total >= 0 && id > saved.lastId) {
+				++saved.total;
+			}
+			_savedChanged.fire_copy(peer);
+		}
+	} else if (const auto i = _saved.find(peer); i != end(_saved)) {
+		auto &saved = i->second;
+		if (saved.ids.list.remove(id)) {
+			if (saved.total > 0) {
+				--saved.total;
+			}
+			_savedChanged.fire_copy(peer);
+		}
+	}
+}
+
 void Stories::loadMore(StorySourcesList list) {
 	const auto index = static_cast<int>(list);
 	if (_loadMoreRequestId[index] || _sourcesLoaded[index]) {
@@ -744,20 +820,38 @@ 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)) {
+	const auto i = _stories.find(id.peer);
+	if (i != end(_stories)) {
+		const auto j = i->second.find(id.story);
+		if (j != end(i->second)) {
 			// Duplicated in Stories::apply(peer, const MTPUserStories*).
-			auto story = std::move(k->second);
+			auto story = std::move(j->second);
 			_expiring.remove(story->expires(), story->fullId());
-			j->second.erase(k);
+			i->second.erase(j);
 			session().changes().storyUpdated(
 				story.get(),
 				UpdateFlag::Destroyed);
 			removeDependencyStory(story.get());
-			if (j->second.empty()) {
-				_stories.erase(j);
+			if (id.peer == session().userPeerId()
+				&& _archive.list.remove(id.story)) {
+				if (_archiveTotal > 0) {
+					--_archiveTotal;
+				}
+				_archiveChanged.fire({});
+			}
+			if (story->pinned()) {
+				if (const auto k = _saved.find(id.peer); k != end(_saved)) {
+					const auto saved = &k->second;
+					if (saved->ids.list.remove(id.story)) {
+						if (saved->total > 0) {
+							--saved->total;
+						}
+						_savedChanged.fire_copy(id.peer);
+					}
+				}
+			}
+			if (i->second.empty()) {
+				_stories.erase(i);
 			}
 		}
 	}
@@ -766,9 +860,7 @@ void Stories::applyDeleted(FullStoryId id) {
 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()) {
+		if (!story->peer()->isSelf() && !story->pinned()) {
 			applyDeleted(id);
 			return;
 		}
@@ -776,13 +868,6 @@ void Stories::applyExpired(FullStoryId id) {
 	applyRemovedFromActive(id);
 }
 
-void Stories::addToExpiredMine(not_null<Story*> story) {
-	const auto added = _expiredMine.list.emplace(story->id()).second;
-	if (added && _expiredMineTotal >= 0) {
-		++_expiredMineTotal;
-	}
-}
-
 void Stories::applyRemovedFromActive(FullStoryId id) {
 	const auto removeFromList = [&](StorySourcesList list) {
 		const auto index = static_cast<int>(list);
@@ -1172,24 +1257,24 @@ void Stories::loadViewsSlice(
 	}).send();
 }
 
-const StoriesIds &Stories::expiredMine() const {
-	return _expiredMine;
+const StoriesIds &Stories::archive() const {
+	return _archive;
 }
 
-rpl::producer<> Stories::expiredMineChanged() const {
-	return _expiredMineChanged.events();
+rpl::producer<> Stories::archiveChanged() const {
+	return _archiveChanged.events();
 }
 
-int Stories::expiredMineCount() const {
-	return std::max(_expiredMineTotal, 0);
+int Stories::archiveCount() const {
+	return std::max(_archiveTotal, 0);
 }
 
-bool Stories::expiredMineCountKnown() const {
-	return _expiredMineTotal >= 0;
+bool Stories::archiveCountKnown() const {
+	return _archiveTotal >= 0;
 }
 
-bool Stories::expiredMineLoaded() const {
-	return _expiredMineLoaded;
+bool Stories::archiveLoaded() const {
+	return _archiveLoaded;
 }
 
 const StoriesIds *Stories::saved(PeerId peerId) const {
@@ -1216,46 +1301,43 @@ bool Stories::savedLoaded(PeerId peerId) const {
 	return (i != end(_saved)) && i->second.loaded;
 }
 
-void Stories::expiredMineLoadMore() {
-	if (_expiredMineRequestId) {
+void Stories::archiveLoadMore() {
+	if (_archiveRequestId || _archiveLoaded) {
 		return;
 	}
 	const auto api = &_owner->session().api();
-	_expiredMineRequestId = api->request(MTPstories_GetExpiredStories(
-		MTP_int(_expiredMineLastId),
-		MTP_int(_expiredMineLastId
-			? kExpiredMinePerPage
-			: kExpiredMineFirstPerPage)
+	_archiveRequestId = api->request(MTPstories_GetStoriesArchive(
+		MTP_int(_archiveLastId),
+		MTP_int(_archiveLastId ? kArchivePerPage : kArchiveFirstPerPage)
 	)).done([=](const MTPstories_Stories &result) {
-		_expiredMineRequestId = 0;
+		_archiveRequestId = 0;
 
 		const auto &data = result.data();
 		const auto self = _owner->session().user();
 		const auto now = base::unixtime::now();
-		_expiredMineTotal = data.vcount().v;
+		_archiveTotal = data.vcount().v;
 		for (const auto &story : data.vstories().v) {
 			const auto id = story.match([&](const auto &id) {
 				return id.vid().v;
 			});
-			_expiredMine.list.emplace(id);
-			_expiredMineLastId = id;
+			_archive.list.emplace(id);
+			_archiveLastId = id;
 			if (!parseAndApply(self, story, now)) {
-				_expiredMine.list.remove(id);
-				if (_expiredMineTotal > 0) {
-					--_expiredMineTotal;
+				_archive.list.remove(id);
+				if (_archiveTotal > 0) {
+					--_archiveTotal;
 				}
 			}
 		}
-		_expiredMineTotal = std::max(
-			_expiredMineTotal,
-			int(_expiredMine.list.size()));
-		_expiredMineLoaded = data.vstories().v.empty();
-		_expiredMineChanged.fire({});
+		const auto ids = int(_archive.list.size());
+		_archiveLoaded = data.vstories().v.empty();
+		_archiveTotal = _archiveLoaded ? ids : std::max(_archiveTotal, ids);
+		_archiveChanged.fire({});
 	}).fail([=] {
-		_expiredMineRequestId = 0;
-		_expiredMineLoaded = true;
-		_expiredMineTotal = int(_expiredMine.list.size());
-		_expiredMineChanged.fire({});
+		_archiveRequestId = 0;
+		_archiveLoaded = true;
+		_archiveTotal = int(_archive.list.size());
+		_archiveChanged.fire({});
 	}).send();
 }
 
@@ -1263,7 +1345,7 @@ void Stories::savedLoadMore(PeerId peerId) {
 	Expects(peerIsUser(peerId));
 
 	auto &saved = _saved[peerId];
-	if (saved.requestId) {
+	if (saved.requestId || saved.loaded) {
 		return;
 	}
 	const auto api = &_owner->session().api();
@@ -1292,8 +1374,9 @@ void Stories::savedLoadMore(PeerId peerId) {
 				}
 			}
 		}
-		saved.total = std::max(saved.total, int(saved.ids.list.size()));
+		const auto ids = int(saved.ids.list.size());
 		saved.loaded = data.vstories().v.empty();
+		saved.total = saved.loaded ? ids : std::max(saved.total, ids);
 		_savedChanged.fire_copy(peerId);
 	}).fail([=] {
 		auto &saved = _saved[peerId];
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 5d26f7474..a94b6ef12 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -230,12 +230,12 @@ public:
 		std::optional<StoryView> offset,
 		Fn<void(std::vector<StoryView>)> done);
 
-	[[nodiscard]] const StoriesIds &expiredMine() const;
-	[[nodiscard]] rpl::producer<> expiredMineChanged() const;
-	[[nodiscard]] int expiredMineCount() const;
-	[[nodiscard]] bool expiredMineCountKnown() const;
-	[[nodiscard]] bool expiredMineLoaded() const;
-	void expiredMineLoadMore();
+	[[nodiscard]] const StoriesIds &archive() const;
+	[[nodiscard]] rpl::producer<> archiveChanged() const;
+	[[nodiscard]] int archiveCount() const;
+	[[nodiscard]] bool archiveCountKnown() const;
+	[[nodiscard]] bool archiveLoaded() const;
+	void archiveLoadMore();
 
 	[[nodiscard]] const StoriesIds *saved(PeerId peerId) const;
 	[[nodiscard]] rpl::producer<PeerId> savedChanged() const;
@@ -273,6 +273,7 @@ private:
 	void applyRemovedFromActive(FullStoryId id);
 	void applyDeletedFromSources(PeerId id, StorySourcesList list);
 	void removeDependencyStory(not_null<Story*> story);
+	void savedStateUpdated(not_null<Story*> story);
 	void sort(StorySourcesList list);
 
 	[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
@@ -282,7 +283,7 @@ private:
 	void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
 
 	void requestUserStories(not_null<UserData*> user);
-	void addToExpiredMine(not_null<Story*> story);
+	void addToArchive(not_null<Story*> story);
 	void registerExpiring(TimeId expires, FullStoryId id);
 	void scheduleExpireTimer();
 	void processExpired();
@@ -321,12 +322,12 @@ private:
 	rpl::event_stream<PeerId> _sourceChanged;
 	rpl::event_stream<PeerId> _itemsChanged;
 
-	StoriesIds _expiredMine;
-	int _expiredMineTotal = -1;
-	StoryId _expiredMineLastId = 0;
-	bool _expiredMineLoaded = false;
-	rpl::event_stream<> _expiredMineChanged;
-	mtpRequestId _expiredMineRequestId = 0;
+	StoriesIds _archive;
+	int _archiveTotal = -1;
+	StoryId _archiveLastId = 0;
+	bool _archiveLoaded = false;
+	rpl::event_stream<> _archiveChanged;
+	mtpRequestId _archiveRequestId = 0;
 
 	std::unordered_map<PeerId, Saved> _saved;
 	rpl::event_stream<PeerId> _savedChanged;
diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp
index 610d51c3a..21d0878a6 100644
--- a/Telegram/SourceFiles/data/data_stories_ids.cpp
+++ b/Telegram/SourceFiles/data/data_stories_ids.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "data/data_stories_ids.h"
 
+#include "data/data_changes.h"
 #include "data/data_peer.h"
 #include "data/data_session.h"
 #include "data/data_stories.h"
@@ -23,62 +24,108 @@ rpl::producer<StoriesIdsSlice> SavedStoriesIds(
 
 		struct State {
 			StoriesIdsSlice slice;
+			base::has_weak_ptr guard;
+			bool scheduled = false;
 		};
 		const auto state = lifetime.make_state<State>();
 
 		const auto push = [=] {
+			state->scheduled = false;
+
 			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);
+			const auto around = saved->list.lower_bound(aroundId);
 			Assert(saved != nullptr);
-			auto ids = base::flat_set<StoryId>();
-			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 {
+			const auto source = stories->source(peer->id);
+			if (!source || source->ids.empty()) {
+				const auto hasBefore = int(around - begin(saved->list));
+				const auto hasAfter = int(end(saved->list) - around);
+				if (hasAfter < limit) {
+					stories->savedLoadMore(peer->id);
+				}
+				const auto takeBefore = std::min(hasBefore, limit);
+				const auto takeAfter = std::min(hasAfter, limit);
+				auto ids = base::flat_set<StoryId>{
+					std::make_reverse_iterator(around + takeAfter),
+					std::make_reverse_iterator(around - takeBefore)
+				};
+				const auto added = int(ids.size());
+				state->slice = StoriesIdsSlice(
+					std::move(ids),
+					count,
+					(hasBefore - takeBefore),
+					count - hasBefore - added);
+			} else {
+				auto ids = base::flat_set<StoryId>();
+				auto added = 0;
+				auto skipped = 0;
+				auto skippedBefore = (around - begin(saved->list));
+				auto skippedAfter = (end(saved->list) - around);
+				const auto &active = source->ids;
+				const auto process = [&](StoryId id) {
+					const auto i = active.lower_bound(StoryIdDates{ id });
+					if (i == end(active) || i->id != id) {
 						ids.emplace(id);
+						++added;
+					} else {
+						++skipped;
+					}
+					return (added < limit);
+				};
+				ids.reserve(2 * limit + 1);
+				for (auto i = around, b = begin(saved->list); i != b;) {
+					--skippedBefore;
+					if (!process(*--i)) {
+						break;
 					}
 				}
-				ids.emplace(current);
-			} else {
-				auto all = saved->list | ranges::views::reverse;
-				ids = { begin(all), end(all) };
+				if (ids.size() < limit) {
+					ids.emplace(active.back().id); // #TODO stories fake max story id
+				} else {
+					++skippedBefore;
+				}
+				added = 0;
+				for (auto i = around, e = end(saved->list); i != e; ++i) {
+					--skippedAfter;
+					if (!process(*i)) {
+						break;
+					}
+				}
+				state->slice = StoriesIdsSlice(
+					std::move(ids),
+					count - skipped + 1,
+					skippedBefore,
+					skippedAfter);
 			}
-			const auto added = int(ids.size());
-			state->slice = StoriesIdsSlice(
-				std::move(ids),
-				total,
-				0,
-				total - added);
 			consumer.put_next_copy(state->slice);
 		};
+		const auto schedule = [=] {
+			if (state->scheduled) {
+				return;
+			}
+			state->scheduled = true;
+			Ui::PostponeCall(&state->guard, [=] {
+				if (state->scheduled) {
+					push();
+				}
+			});
+		};
 
 		const auto stories = &peer->owner().stories();
 		stories->sourceChanged(
 		) | rpl::filter(
 			rpl::mappers::_1 == peer->id
-		) | rpl::start_with_next([=] {
-			push();
-		}, lifetime);
+		) | rpl::start_with_next(schedule, lifetime);
 
 		stories->savedChanged(
 		) | rpl::filter(
 			rpl::mappers::_1 == peer->id
-		) | rpl::start_with_next([=] {
-			push();
-		}, lifetime);
+		) | rpl::start_with_next(schedule, lifetime);
 
 		if (!stories->savedCountKnown(peer->id)) {
 			stories->savedLoadMore(peer->id);
@@ -99,38 +146,59 @@ rpl::producer<StoriesIdsSlice> ArchiveStoriesIds(
 
 		struct State {
 			StoriesIdsSlice slice;
+			base::has_weak_ptr guard;
+			bool scheduled = false;
 		};
 		const auto state = lifetime.make_state<State>();
 
 		const auto push = [=] {
+			state->scheduled = false;
+
 			const auto stories = &session->data().stories();
-			if (!stories->expiredMineCountKnown()) {
+			if (!stories->archiveCountKnown()) {
 				return;
 			}
 
-			const auto expired = stories->expiredMine();
-			const auto count = stories->expiredMineCount();
-			auto ids = base::flat_set<StoryId>();
-			ids.reserve(expired.list.size() + 1);
-			auto all = expired.list | ranges::views::reverse;
-			ids = { begin(all), end(all) };
+			const auto &archive = stories->archive();
+			const auto count = stories->archiveCount();
+			const auto i = archive.list.lower_bound(aroundId);
+			const auto hasBefore = int(i - begin(archive.list));
+			const auto hasAfter = int(end(archive.list) - i);
+			if (hasAfter < limit) {
+				stories->archiveLoadMore();
+			}
+			const auto takeBefore = std::min(hasBefore, limit);
+			const auto takeAfter = std::min(hasAfter, limit);
+			auto ids = base::flat_set<StoryId>{
+				std::make_reverse_iterator(i + takeAfter),
+				std::make_reverse_iterator(i - takeBefore)
+			};
 			const auto added = int(ids.size());
 			state->slice = StoriesIdsSlice(
 				std::move(ids),
 				count,
-				0,
-				count - added);
+				(hasBefore - takeBefore),
+				count - hasBefore - added);
 			consumer.put_next_copy(state->slice);
 		};
+		const auto schedule = [=] {
+			if (state->scheduled) {
+				return;
+			}
+			state->scheduled = true;
+			Ui::PostponeCall(&state->guard, [=] {
+				if (state->scheduled) {
+					push();
+				}
+			});
+		};
 
 		const auto stories = &session->data().stories();
-		stories->expiredMineChanged(
-		) | rpl::start_with_next([=] {
-			push();
-		}, lifetime);
+		stories->archiveChanged(
+		) | rpl::start_with_next(schedule, lifetime);
 
-		if (!stories->expiredMineCountKnown()) {
-			stories->expiredMineLoadMore();
+		if (!stories->archiveCountKnown()) {
+			stories->archiveLoadMore();
 		}
 
 		push();
diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp
index a9c57a75f..d7beb981e 100644
--- a/Telegram/SourceFiles/info/info_controller.cpp
+++ b/Telegram/SourceFiles/info/info_controller.cpp
@@ -266,7 +266,8 @@ bool Controller::validateMementoPeer(
 		not_null<ContentMemento*> memento) const {
 	return memento->peer() == peer()
 		&& memento->migratedPeerId() == migratedPeerId()
-		&& memento->settingsSelf() == settingsSelf();
+		&& memento->settingsSelf() == settingsSelf()
+		&& memento->storiesPeer() == storiesPeer();
 }
 
 void Controller::setSection(not_null<ContentMemento*> memento) {
diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h
index b460edada..8ba60b121 100644
--- a/Telegram/SourceFiles/info/media/info_media_buttons.h
+++ b/Telegram/SourceFiles/info/media/info_media_buttons.h
@@ -131,13 +131,13 @@ inline auto AddStoriesButton(
 		not_null<Window::SessionNavigation*> navigation,
 		not_null<UserData*> user,
 		Ui::MultiSlideTracker &tracker) {
-	auto count = Data::SavedStoriesIds(
+	auto count = rpl::single(0) | rpl::then(Data::SavedStoriesIds(
 		user,
 		ServerMaxStoryId - 1,
 		0
 	) | rpl::map([](const Data::StoriesIdsSlice &slice) {
-		return slice.fullCount();
-	}) | rpl::filter_optional();
+		return slice.fullCount().value_or(0);
+	}));
 	auto result = AddCountedButton(
 		parent,
 		std::move(count),
diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.h b/Telegram/SourceFiles/info/media/info_media_inner_widget.h
index a7a2ce61d..4e305de40 100644
--- a/Telegram/SourceFiles/info/media/info_media_inner_widget.h
+++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.h
@@ -20,10 +20,10 @@ class SearchFieldController;
 } // namespace Ui
 
 namespace Info {
-
 class Controller;
+} // namespace Info
 
-namespace Media {
+namespace Info::Media {
 
 class Memento;
 class ListWidget;
@@ -86,5 +86,4 @@ private:
 
 };
 
-} // namespace Media
-} // namespace Info
+} // namespace Info::Media
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index a7739a77b..fe32be04d 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -7,10 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "info/stories/info_stories_inner_widget.h"
 
-#include "info/stories/info_stories_widget.h"
+#include "data/data_peer.h"
 #include "info/media/info_media_list_widget.h"
+#include "info/profile/info_profile_icon.h"
+#include "info/stories/info_stories_widget.h"
 #include "info/info_controller.h"
+#include "info/info_memento.h"
+#include "lang/lang_keys.h"
+#include "settings/settings_common.h"
+#include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
+#include "ui/wrap/vertical_layout.h"
 #include "styles/style_info.h"
 
 namespace Info::Stories {
@@ -83,6 +90,49 @@ InnerWidget::InnerWidget(
 	_list = setupList();
 }
 
+void InnerWidget::setupArchive() {
+	const auto key = _controller->key();
+	const auto peer = key.storiesPeer();
+	if (peer
+		&& peer->isSelf()
+		&& key.storiesTab() == Stories::Tab::Saved
+		&& _isStackBottom) {
+		createArchiveButton();
+	} else {
+		_archive.destroy();
+		refreshHeight();
+	}
+}
+
+void InnerWidget::createArchiveButton() {
+	_archive.create(this);
+	_archive->show();
+
+	const auto button = ::Settings::AddButton(
+		_archive,
+		tr::lng_stories_archive_button(),
+		st::infoSharedMediaButton);
+	button->addClickHandler([=] {
+		_controller->showSection(Info::Stories::Make(
+			_controller->key().storiesPeer(),
+			Stories::Tab::Archive));
+	});
+	object_ptr<Profile::FloatingIcon>(
+		button,
+		st::infoIconMediaGroup,
+		st::infoSharedMediaButtonIconPosition)->show();
+	_archive->add(object_ptr<Ui::FixedHeightWidget>(
+		_archive,
+		st::infoProfileSkip));
+	_archive->add(object_ptr<Ui::BoxContentDivider>(_archive));
+
+	_archive->resizeToWidth(width());
+	_archive->heightValue(
+	) | rpl::start_with_next([=] {
+		refreshHeight();
+	}, _archive->lifetime());
+}
+
 void InnerWidget::visibleTopBottomUpdated(
 		int visibleTop,
 		int visibleBottom) {
@@ -144,6 +194,9 @@ int InnerWidget::resizeGetHeight(int newWidth) {
 	_inResize = true;
 	auto guard = gsl::finally([this] { _inResize = false; });
 
+	if (_archive) {
+		_archive->resizeToWidth(newWidth);
+	}
 	_list->resizeToWidth(newWidth);
 	_empty->resizeToWidth(newWidth);
 	return recountHeight();
@@ -158,6 +211,10 @@ void InnerWidget::refreshHeight() {
 
 int InnerWidget::recountHeight() {
 	auto top = 0;
+	if (_archive) {
+		_archive->moveToLeft(0, top);
+		top += _archive->heightNoMargins() - st::lineWidth;
+	}
 	auto listHeight = 0;
 	if (_list) {
 		_list->moveToLeft(0, top);
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
index f874b00f2..e683013fe 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
@@ -38,6 +38,10 @@ public:
 		not_null<Controller*> controller);
 
 	bool showInternal(not_null<Memento*> memento);
+	void setIsStackBottom(bool isStackBottom) {
+		_isStackBottom = isStackBottom;
+		setupArchive();
+	}
 
 	void saveState(not_null<Memento*> memento);
 	void restoreState(not_null<Memento*> memento);
@@ -60,14 +64,19 @@ private:
 	int recountHeight();
 	void refreshHeight();
 
+	void setupArchive();
+	void createArchiveButton();
+
 	object_ptr<Media::ListWidget> setupList();
 
 	const not_null<Controller*> _controller;
 
+	object_ptr<Ui::VerticalLayout> _archive = { nullptr };
 	object_ptr<Media::ListWidget> _list = { nullptr };
 	object_ptr<EmptyWidget> _empty;
 
 	bool _inResize = false;
+	bool _isStackBottom = false;
 
 	rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
 	rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
diff --git a/Telegram/SourceFiles/info/stories/info_stories_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_widget.cpp
index 67b8f602b..bca32b356 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_widget.cpp
@@ -20,8 +20,8 @@ Memento::Memento(not_null<Controller*> controller)
 , _media(controller) {
 }
 
-Memento::Memento(not_null<PeerData*> peer)
-: ContentMemento(Tag{ peer })
+Memento::Memento(not_null<PeerData*> peer, Tab tab)
+: ContentMemento(Tag{ peer, tab })
 , _media(peer, 0, Media::Type::PhotoVideo) {
 }
 
@@ -54,13 +54,21 @@ Widget::Widget(
 	}, _inner->lifetime());
 }
 
+void Widget::setIsStackBottom(bool isStackBottom) {
+	ContentWidget::setIsStackBottom(isStackBottom);
+	_inner->setIsStackBottom(isStackBottom);
+}
+
 bool Widget::showInternal(not_null<ContentMemento*> memento) {
 	if (!controller()->validateMementoPeer(memento)) {
 		return false;
 	}
 	if (auto storiesMemento = dynamic_cast<Memento*>(memento.get())) {
-		restoreState(storiesMemento);
-		return true;
+		const auto tab = controller()->key().storiesTab();
+		if (storiesMemento->storiesTab() == tab) {
+			restoreState(storiesMemento);
+			return true;
+		}
 	}
 	return false;
 }
@@ -98,14 +106,16 @@ void Widget::selectionAction(SelectionAction action) {
 }
 
 rpl::producer<QString> Widget::title() {
-	return tr::lng_menu_my_stories(); // #TODO stories
+	return (controller()->key().storiesTab() == Tab::Archive)
+		? tr::lng_stories_archive_title()
+		: tr::lng_stories_my_title();
 }
 
-std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
+std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer, Tab tab) {
 	return std::make_shared<Info::Memento>(
 		std::vector<std::shared_ptr<ContentMemento>>(
 			1,
-			std::make_shared<Memento>(peer)));
+			std::make_shared<Memento>(peer, tab)));
 }
 
 } // namespace Info::Stories
diff --git a/Telegram/SourceFiles/info/stories/info_stories_widget.h b/Telegram/SourceFiles/info/stories/info_stories_widget.h
index 17e96a1d6..45df73f2b 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_widget.h
+++ b/Telegram/SourceFiles/info/stories/info_stories_widget.h
@@ -13,11 +13,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Info::Stories {
 
 class InnerWidget;
+enum class Tab;
 
 class Memento final : public ContentMemento {
 public:
 	Memento(not_null<Controller*> controller);
-	Memento(not_null<PeerData*> peer);
+	Memento(not_null<PeerData*> peer, Tab tab);
 	~Memento();
 
 	object_ptr<ContentWidget> createWidget(
@@ -43,6 +44,8 @@ class Widget final : public ContentWidget {
 public:
 	Widget(QWidget *parent, not_null<Controller*> controller);
 
+	void setIsStackBottom(bool isStackBottom) override;
+
 	bool showInternal(
 		not_null<ContentMemento*> memento) override;
 
@@ -65,6 +68,8 @@ private:
 
 };
 
-[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
+[[nodiscard]] std::shared_ptr<Info::Memento> Make(
+	not_null<PeerData*> peer,
+	Tab tab = {});
 
 } // namespace Info::Stories
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index c51984f6a..577a291a1 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -2093,7 +2093,7 @@ stories.togglePinned#51602944 id:Vector<int> pinned:Bool = Vector<int>;
 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;
+stories.getStoriesArchive#1f5bc5d2 offset_id:int limit:int = stories.Stories;
 stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector<int> = stories.Stories;
 stories.readStories#edc5105b user_id:InputUser max_id:int = Vector<int>;
 stories.incrementStoryViews#22126127 user_id:InputUser id:Vector<int> = Bool;
diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style
index 17aa1c405..bfdc586d6 100644
--- a/Telegram/SourceFiles/settings/settings.style
+++ b/Telegram/SourceFiles/settings/settings.style
@@ -104,10 +104,7 @@ settingsIconMinus: icon {{ "settings/minus", settingsIconFg }};
 settingsIconTimer: icon {{ "settings/timer", settingsIconFg }};
 settingsIconLaptop: icon {{ "settings/laptop", settingsIconFg }};
 settingsIconArrows: icon {{ "settings/arrows", settingsIconFg }};
-settingsIconOnline: icon {{ "settings/online", settingsIconFg }};
-settingsIconVideoCalls: icon {{ "settings/video_calls", settingsIconFg }};
 settingsIconEmail: icon {{ "settings/email", settingsIconFg }};
-settingsIconForward: icon {{ "settings/forward", settingsIconFg }};
 settingsIconSound: icon {{ "settings/sound", settingsIconFg }};
 settingsIconDock: icon {{ "settings/dock", settingsIconFg }};
 settingsIconPin: icon {{ "settings/pin", settingsIconFg }};
diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
index 7b8b3e73c..92f8229fe 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
@@ -125,17 +125,15 @@ void AddPremiumPrivacyButton(
 		not_null<Window::SessionController*> controller,
 		not_null<Ui::VerticalLayout*> container,
 		rpl::producer<QString> label,
-		IconDescriptor &&descriptor,
 		Privacy::Key key,
 		Fn<std::unique_ptr<EditPrivacyController>()> controllerFactory) {
 	const auto shower = Ui::CreateChild<rpl::lifetime>(container.get());
 	const auto session = &controller->session();
-	const auto &st = st::settingsButton;
+	const auto &st = st::settingsButtonNoIcon;
 	const auto button = AddButton(
 		container,
 		rpl::duplicate(label),
-		st,
-		std::move(descriptor));
+		st);
 	struct State {
 		State(QWidget *parent) : widget(parent) {
 			widget.setAttribute(Qt::WA_TransparentForMouseEvents);
@@ -260,59 +258,50 @@ void SetupPrivacy(
 	using Key = Privacy::Key;
 	const auto add = [&](
 			rpl::producer<QString> label,
-			IconDescriptor &&descriptor,
 			Key key,
 			auto controllerFactory) {
 		AddPrivacyButton(
 			controller,
 			container,
 			std::move(label),
-			std::move(descriptor),
+			{},
 			key,
 			controllerFactory);
 	};
 	add(
 		tr::lng_settings_phone_number_privacy(),
-		{ &st::settingsIconCalls, kIconGreen },
 		Key::PhoneNumber,
 		[=] { return std::make_unique<PhoneNumberPrivacyController>(
 			controller); });
 	add(
 		tr::lng_settings_last_seen(),
-		{ &st::settingsIconOnline, kIconLightBlue },
 		Key::LastSeen,
 		[=] { return std::make_unique<LastSeenPrivacyController>(session); });
 	add(
 		tr::lng_settings_profile_photo_privacy(),
-		{ &st::settingsIconAccount, kIconRed },
 		Key::ProfilePhoto,
 		[] { return std::make_unique<ProfilePhotoPrivacyController>(); });
 	add(
 		tr::lng_settings_forwards_privacy(),
-		{ &st::settingsIconForward, kIconLightOrange },
 		Key::Forwards,
 		[=] { return std::make_unique<ForwardsPrivacyController>(
 			controller); });
 	add(
 		tr::lng_settings_calls(),
-		{ &st::settingsIconVideoCalls, kIconGreen },
 		Key::Calls,
 		[] { return std::make_unique<CallsPrivacyController>(); });
 	add(
 		tr::lng_settings_groups_invite(),
-		{ &st::settingsIconGroup, kIconDarkBlue },
 		Key::Invites,
 		[] { return std::make_unique<GroupsInvitePrivacyController>(); });
 	AddPremiumPrivacyButton(
 		controller,
 		container,
 		tr::lng_settings_voices_privacy(),
-		{ &st::settingsPremiumIconVoice, kIconRed },
 		Key::Voices,
 		[=] { return std::make_unique<VoicesPrivacyController>(session); });
 	add(
 		tr::lng_settings_bio_privacy(),
-		{ &st::settingsIconAccount, kIconDarkOrange },
 		Key::About,
 		[] { return std::make_unique<AboutPrivacyController>(); });
 
@@ -832,7 +821,7 @@ void AddPrivacyButton(
 		container,
 		std::move(label),
 		PrivacyString(session, key),
-		st::settingsButton,
+		st::settingsButtonNoIcon,
 		std::move(descriptor)
 	)->addClickHandler([=] {
 		*shower = session->api().userPrivacy().value(
diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp
index b2f213527..227a00d42 100644
--- a/Telegram/SourceFiles/window/window_main_menu.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu.cpp
@@ -775,18 +775,15 @@ void MainMenu::setupMenu() {
 						kIconLightOrange
 					})));
 		const auto stories = &controller->session().data().stories();
-		const auto mine = stories->source(
-			controller->session().userPeerId());
-		if ((mine && !mine->ids.empty())
-			|| stories->expiredMineCount() > 0) {
+		if (stories->archiveCount() > 0) {
 			wrap->toggle(true, anim::type::instant);
 		} else {
 			wrap->toggle(false, anim::type::instant);
-			if (!stories->expiredMineCountKnown()) {
-				stories->expiredMineLoadMore();
-				wrap->toggleOn(stories->expiredMineChanged(
+			if (!stories->archiveCountKnown()) {
+				stories->archiveLoadMore();
+				wrap->toggleOn(stories->archiveChanged(
 				) | rpl::map([=] {
-					return stories->expiredMineCount() > 0;
+					return stories->archiveCount() > 0;
 				}) | rpl::filter(rpl::mappers::_1) | rpl::take(1));
 			}
 		}

From 087c27c5e04ba6a0c6e4aa90684d8e4fdf95a436 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 8 Jun 2023 11:11:07 +0400
Subject: [PATCH 061/259] Fix expire calculations for the stories.

---
 Telegram/SourceFiles/boxes/peer_list_controllers.cpp  | 11 +++++------
 Telegram/SourceFiles/data/data_stories.cpp            |  4 ++--
 .../SourceFiles/dialogs/ui/dialogs_stories_list.cpp   |  4 ++--
 3 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 7fa6e5384..86150e820 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/random.h"
 #include "ui/boxes/confirm_box.h"
 #include "ui/widgets/checkbox.h"
+#include "ui/wrap/padding_wrap.h"
 #include "ui/painter.h"
 #include "ui/ui_utility.h"
 #include "main/main_session.h"
@@ -86,12 +87,10 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 				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());
+		box->peerListSetAboveWidget(object_ptr<::Ui::PaddingWrap<>>(
+			box,
+			std::move(stories),
+			style::margins(0, st::membersMarginTop, 0, 0)));
 
 		raw->clicks(
 		) | rpl::start_with_next([=](uint64 id) {
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 972e3c69a..52eb79672 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -552,7 +552,7 @@ Story *Stories::parseAndApply(
 		return nullptr;
 	}
 	const auto expires = data.vexpire_date().v;
-	const auto expired = (expires >= now);
+	const auto expired = (expires <= now);
 	if (expired && !data.is_pinned() && !peer->isSelf()) {
 		return nullptr;
 	}
@@ -618,7 +618,7 @@ StoryIdDates Stories::parseAndApply(
 		return StoryIdDates();
 	}, [&](const MTPDstoryItemSkipped &data) {
 		const auto expires = data.vexpire_date().v;
-		const auto expired = (expires >= now);
+		const auto expired = (expires <= now);
 		const auto fullId = FullStoryId{ peer->id, data.vid().v };
 		if (!expired) {
 			registerExpiring(expires, fullId);
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index ec470a92d..213af18b7 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -337,8 +337,6 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto drawFull = (ratio > 0.);
 	auto hq = PainterHighQualityEnabler(p);
 
-	paintSummary(p, rendering, summaryTop, ratio);
-
 	const auto count = std::max(
 		layout.endIndexFull - layout.startIndexFull,
 		layout.endIndexSmall - layout.startIndexSmall);
@@ -485,6 +483,8 @@ void List::paintEvent(QPaintEvent *e) {
 		}
 		p.setOpacity(1.);
 	});
+
+	paintSummary(p, rendering, summaryTop, ratio);
 }
 
 void List::validateUserpic(not_null<Item*> item) {

From c1be4d645180980ab15fc4e452b938c3e2dcefc0 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 8 Jun 2023 12:20:19 +0400
Subject: [PATCH 062/259] Hide self in small stories userpics.

---
 .../dialogs/ui/dialogs_stories_content.cpp    |  1 +
 .../dialogs/ui/dialogs_stories_list.cpp       | 79 +++++++++++++------
 .../dialogs/ui/dialogs_stories_list.h         |  2 +
 3 files changed, 59 insertions(+), 23 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index 0e6f05b3e..c47282c26 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -148,6 +148,7 @@ Content State::next() {
 			.userpic = std::move(userpic),
 			.unread = info.unread,
 			.hidden = info.hidden,
+			.self = user->isSelf(),
 		});
 	}
 	return result;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 213af18b7..c53282395 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -39,6 +39,7 @@ struct List::Layout {
 	float64 photoLeft = 0.;
 	float64 left = 0.;
 	float64 single = 0.;
+	int smallSkip = 0;
 	int leftFull = 0;
 	int leftSmall = 0;
 	int singleFull = 0;
@@ -114,17 +115,18 @@ void List::showContent(Content &&content) {
 
 List::Summaries List::ComposeSummaries(Data &data) {
 	const auto total = int(data.items.size());
+	const auto skip = (total > 1 && data.items[0].user.self) ? 1 : 0;
 	auto unreadInFirst = 0;
 	auto unreadTotal = 0;
-	for (auto i = 0; i != total; ++i) {
+	for (auto i = skip; i != total; ++i) {
 		if (data.items[i].user.unread) {
 			++unreadTotal;
-			if (i < kSmallUserpicsShown) {
+			if (i < skip + kSmallUserpicsShown) {
 				++unreadInFirst;
 			}
 		}
 	}
-	auto result = Summaries();
+	auto result = Summaries{ .skipSelf = (skip > 0) };
 	result.total.string
 		= tr::lng_stories_row_count(tr::now, lt_count, total);
 	const auto append = [&](QString &to, int index, bool last) {
@@ -143,13 +145,13 @@ List::Summaries List::ComposeSummaries(Data &data) {
 	};
 	if (!total) {
 		return result;
-	} else if (total <= kSmallUserpicsShown) {
-		for (auto i = 0; i != total; ++i) {
+	} else if (total <= skip + kSmallUserpicsShown) {
+		for (auto i = skip; i != total; ++i) {
 			append(result.allNames.string, i, i == total - 1);
 		}
 	}
 	if (unreadInFirst > 0 && unreadInFirst == unreadTotal) {
-		for (auto i = 0; i != total; ++i) {
+		for (auto i = skip; i != total; ++i) {
 			if (data.items[i].user.unread) {
 				append(result.unreadNames.string, i, !--unreadTotal);
 			}
@@ -265,11 +267,16 @@ List::Layout List::computeLayout() const {
 		+ st::defaultDialogRow.photoSize
 		+ st::defaultDialogRow.padding.left();
 	const auto narrow = (width() <= narrowWidth);
-	const auto smallCount = std::min(kSmallUserpicsShown, itemsCount);
+	const auto smallSkip = (itemsCount > 1 && rendering.items[0].user.self)
+		? 1
+		: 0;
+	const auto smallCount = std::min(
+		kSmallUserpicsShown,
+		itemsCount - smallSkip);
 	const auto smallWidth = st.photo + (smallCount - 1) * st.shift;
-	const auto leftSmall = narrow
+	const auto leftSmall = (narrow
 		? ((narrowWidth - smallWidth) / 2 - st.photoLeft)
-		: st.left;
+		: st.left) - (smallSkip ? st.shift : 0);
 	const auto leftFull = (narrow
 		? ((narrowWidth - full.photo) / 2 - full.photoLeft)
 		: full.left) - _scrollLeft;
@@ -278,9 +285,9 @@ List::Layout List::computeLayout() const {
 	const auto endIndexFull = std::min(
 		(width() - leftFull + singleFull - 1) / singleFull,
 		itemsCount);
-	const auto startIndexSmall = 0;
-	const auto endIndexSmall = smallCount;
-	const auto cellLeftSmall = leftSmall;
+	const auto startIndexSmall = std::min(startIndexFull, smallSkip);
+	const auto endIndexSmall = smallSkip + smallCount;
+	const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift);
 	const auto userpicLeftFull = cellLeftFull + full.photoLeft;
 	const auto userpicLeftSmall = cellLeftSmall + st.photoLeft;
 	const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull);
@@ -293,6 +300,7 @@ List::Layout List::computeLayout() const {
 		.photoLeft = photoLeft,
 		.left = userpicLeft - photoLeft,
 		.single = lerp(st.shift, singleFull),
+		.smallSkip = smallSkip,
 		.leftFull = leftFull,
 		.leftSmall = leftSmall,
 		.singleFull = singleFull,
@@ -355,7 +363,9 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto lookup = [&](int index) {
 		const auto indexSmall = layout.startIndexSmall + index;
 		const auto indexFull = layout.startIndexFull + index;
-		const auto small = (drawSmall && indexSmall < layout.endIndexSmall)
+		const auto small = (drawSmall
+			&& indexSmall < layout.endIndexSmall
+			&& indexSmall >= layout.smallSkip)
 			? &rendering.items[indexSmall]
 			: nullptr;
 		const auto full = (drawFull && indexFull < layout.endIndexFull)
@@ -370,20 +380,33 @@ void List::paintEvent(QPaintEvent *e) {
 	};
 	const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
 		auto nextGradientPainted = false;
-		for (auto i = count; i != 0;) {
+		auto skippedPainted = false;
+		const auto first = layout.smallSkip - layout.startIndexSmall;
+		for (auto i = count; i != first;) {
 			--i;
+			const auto next = (i > 0) ? lookup(i - 1) : Single();
 			const auto gradientPainted = nextGradientPainted;
 			nextGradientPainted = false;
 			if (const auto current = lookup(i)) {
+				if (i == first && next && !skippedPainted) {
+					skippedPainted = true;
+					paintGradient(next);
+					paintOther(next);
+				}
 				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);
+				if (i > first && hasUnread(current) && next) {
+					if (current.itemSmall || !next.itemSmall) {
+						if (i - 1 == first && first > 0 && !skippedPainted) {
+							if (const auto skipped = lookup(i - 2)) {
+								skippedPainted = true;
+								paintGradient(skipped);
+								paintOther(skipped);
+							}
 						}
+						nextGradientPainted = true;
+						paintGradient(next);
 					}
 				}
 				paintOther(current);
@@ -523,7 +546,9 @@ List::Summary &List::ChooseSummary(
 		int totalItems,
 		int fullWidth) {
 	const auto &st = st::dialogsStories;
-	const auto used = std::min(totalItems, kSmallUserpicsShown);
+	const auto used = std::min(
+		totalItems - (summaries.skipSelf ? 1 : 0),
+		kSmallUserpicsShown);
 	const auto taken = st.left
 		+ st.photoLeft
 		+ st.photo
@@ -579,7 +604,9 @@ void List::paintSummary(
 	};
 	const auto &st = st::dialogsStories;
 	const auto &full = st::dialogsStoriesFull;
-	const auto used = std::min(total, kSmallUserpicsShown);
+	const auto used = std::min(
+		total - (data.summaries.skipSelf ? 1 : 0),
+		kSmallUserpicsShown);
 	const auto fullLeft = st.left
 		+ st.photoLeft
 		+ st.photo
@@ -760,15 +787,20 @@ void List::updateSelected() {
 	const auto layout = computeLayout();
 	const auto firstRightFull = layout.leftFull
 		+ (layout.startIndexFull + 1) * layout.singleFull;
+	const auto secondLeftFull = firstRightFull;
 	const auto firstRightSmall = layout.leftSmall
 		+ st.photoLeft
 		+ st.photo;
+	const auto secondLeftSmall = layout.smallSkip
+		? (layout.leftSmall + st.photoLeft + st.shift)
+		: firstRightSmall;
 	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 secondLeft = lerp(secondLeftSmall, secondLeftFull);
 	const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull);
 	const auto activateFull = (layout.ratio >= 0.5);
 	const auto startIndex = activateFull
@@ -778,9 +810,10 @@ void List::updateSelected() {
 		? layout.endIndexFull
 		: layout.endIndexSmall;
 	const auto x = p.x();
-	const auto infiniteIndex = (x < firstRight)
+	const auto infiniteIndex = (x < secondLeft)
 		? 0
-		: int(std::floor(((x - firstRight) / layout.single) + 1));
+		: int(
+			std::floor((std::max(x - firstRight, 0.)) / layout.single) + 1);
 	const auto index = (endIndex == startIndex)
 		? -1
 		: (infiniteIndex == endIndex - startIndex
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 4221dcbc3..72fd67a9a 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -31,6 +31,7 @@ struct User {
 	std::shared_ptr<Userpic> userpic;
 	bool unread = false;
 	bool hidden = false;
+	bool self = false;
 
 	friend inline bool operator==(const User &a, const User &b) = default;
 };
@@ -87,6 +88,7 @@ private:
 		Summary total;
 		Summary allNames;
 		Summary unreadNames;
+		bool skipSelf = false;
 	};
 	struct Data {
 		std::vector<Item> items;

From 0ed200beee2ff0755ce57e8bfc9feae3f659febf Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 8 Jun 2023 19:54:21 +0400
Subject: [PATCH 063/259] Show / expand / collapse / hide reactions strip.

---
 Telegram/CMakeLists.txt                       |   2 +
 .../chat_helpers/emoji_list_widget.cpp        |  11 +-
 .../chat_helpers/emoji_list_widget.h          |   4 +-
 Telegram/SourceFiles/data/data_stories.cpp    |   2 +-
 .../history_view_compose_controls.cpp         |   7 +-
 .../controls/history_view_compose_controls.h  |   2 +
 .../history_view_reactions_selector.cpp       |  69 ++---
 .../history_view_reactions_selector.h         |  27 +-
 .../stories/media_stories_controller.cpp      |  50 +++-
 .../media/stories/media_stories_controller.h  |  11 +
 .../media/stories/media_stories_delegate.h    |   6 +
 .../media/stories/media_stories_reactions.cpp | 239 ++++++++++++++++++
 .../media/stories/media_stories_reactions.h   |  57 +++++
 .../media/stories/media_stories_reply.cpp     |   4 +
 .../media/stories/media_stories_reply.h       |   1 +
 .../media/stories/media_stories_view.cpp      |   4 +
 .../media/stories/media_stories_view.h        |   1 +
 .../SourceFiles/media/view/media_view.style   |   3 +
 .../media/view/media_view_overlay_widget.cpp  |  17 +-
 .../media/view/media_view_overlay_widget.h    |  14 +-
 .../SourceFiles/ui/controls/tabbed_search.cpp |  11 +
 .../SourceFiles/ui/controls/tabbed_search.h   |   2 +
 22 files changed, 490 insertions(+), 54 deletions(-)
 create mode 100644 Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
 create mode 100644 Telegram/SourceFiles/media/stories/media_stories_reactions.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index c42e141da..8f6ea9975 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -981,6 +981,8 @@ PRIVATE
     media/stories/media_stories_delegate.h
     media/stories/media_stories_header.cpp
     media/stories/media_stories_header.h
+    media/stories/media_stories_reactions.cpp
+    media/stories/media_stories_reactions.h
     media/stories/media_stories_recent_views.cpp
     media/stories/media_stories_recent_views.h
     media/stories/media_stories_reply.cpp
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
index 9f29463ce..f41482c19 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
@@ -623,6 +623,10 @@ rpl::producer<> EmojiListWidget::jumpedToPremium() const {
 	return _jumpedToPremium.events();
 }
 
+rpl::producer<> EmojiListWidget::escapes() const {
+	return _search ? _search->escapes() : rpl::never<>();
+}
+
 void EmojiListWidget::prepareExpanding() {
 	if (_search) {
 		_searchExpandCache = _search->grab();
@@ -633,13 +637,14 @@ void EmojiListWidget::paintExpanding(
 		Painter &p,
 		QRect clip,
 		int finalBottom,
-		float64 progress,
+		float64 geometryProgress,
+		float64 fullProgress,
 		RectPart origin) {
 	const auto searchShift = _search
 		? anim::interpolate(
 			st().padding.top() - _search->height(),
 			0,
-			progress)
+			geometryProgress)
 		: 0;
 	const auto shift = clip.topLeft() + QPoint(0, searchShift);
 	const auto adjusted = clip.translated(-shift);
@@ -654,7 +659,7 @@ void EmojiListWidget::paintExpanding(
 	p.translate(shift);
 	p.setClipRect(adjusted);
 	paint(p, ExpandingContext{
-		.progress = progress,
+		.progress = fullProgress,
 		.finalHeight = finalHeight,
 		.expanding = true,
 	}, adjusted);
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
index cc97d64db..374b6e7b3 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
+++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
@@ -125,6 +125,7 @@ public:
 	[[nodiscard]] rpl::producer<EmojiChosen> chosen() const;
 	[[nodiscard]] rpl::producer<FileChosen> customChosen() const;
 	[[nodiscard]] rpl::producer<> jumpedToPremium() const;
+	[[nodiscard]] rpl::producer<> escapes() const;
 
 	void provideRecent(const std::vector<DocumentId> &customRecentList);
 
@@ -133,7 +134,8 @@ public:
 		Painter &p,
 		QRect clip,
 		int finalBottom,
-		float64 progress,
+		float64 geometryProgress,
+		float64 fullProgress,
 		RectPart origin);
 
 	base::unique_qptr<Ui::PopupMenu> fillContextMenu(
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 52eb79672..c1b08a657 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -1224,7 +1224,7 @@ void Stories::loadViewsSlice(
 		MTP_int(id),
 		MTP_int(offset ? offset->date : 0),
 		MTP_long(offset ? peerToUser(offset->peer->id).bare : 0),
-		MTP_int(2)
+		MTP_int(kViewsPerPage)
 	)).done([=](const MTPstories_StoryViewsList &result) {
 		_viewsRequestId = 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 45c07c9ee..c53650919 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -1854,7 +1854,8 @@ void ComposeControls::fieldChanged() {
 		&& !_header->isEditingMessage()
 		&& (_textUpdateEvents & TextUpdateEvent::SendTyping));
 	updateSendButtonType();
-	if (!HasSendText(_field) && _preview) {
+	_hasSendText = HasSendText(_field);
+	if (!_hasSendText.current() && _preview) {
 		_preview->setState(Data::PreviewState::Allowed);
 	}
 	if (updateBotCommandShown()) {
@@ -2953,6 +2954,10 @@ rpl::producer<bool> ComposeControls::recordingValue() const {
 	return _recording.value();
 }
 
+rpl::producer<bool> ComposeControls::hasSendTextValue() const {
+	return _hasSendText.value();
+}
+
 bool ComposeControls::preventsClose(Fn<void()> &&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 11b2efe22..f8c887593 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
@@ -215,6 +215,7 @@ public:
 	[[nodiscard]] bool isLockPresent() const;
 	[[nodiscard]] bool isRecording() const;
 	[[nodiscard]] rpl::producer<bool> recordingValue() const;
+	[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
 
 	void applyCloudDraft();
 	void applyDraft(
@@ -382,6 +383,7 @@ private:
 	rpl::event_stream<ReplyNextRequest> _replyNextRequests;
 	rpl::event_stream<> _focusRequests;
 	rpl::variable<bool> _recording;
+	rpl::variable<bool> _hasSendText;
 
 	TextUpdateEvents _textUpdateEvents = TextUpdateEvents()
 		| TextUpdateEvent::SaveDraft
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
index ca6ddf226..502c3d1e5 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
@@ -105,48 +105,53 @@ bool StripEmoji::readyInDefaultState() {
 
 Selector::Selector(
 	not_null<QWidget*> parent,
-	not_null<Window::SessionController*> parentController,
+	std::shared_ptr<ChatHelpers::Show> show,
 	const Data::PossibleItemReactionsRef &reactions,
 	IconFactory iconFactory,
-	Fn<void(bool fast)> close)
+	Fn<void(bool fast)> close,
+	bool child)
 : Selector(
 	parent,
-	parentController,
+	std::move(show),
 	reactions,
 	(reactions.customAllowed
 		? ChatHelpers::EmojiListMode::FullReactions
 		: ChatHelpers::EmojiListMode::RecentReactions),
 	{},
 	iconFactory,
-	close) {
+	close,
+	child) {
 }
 
 Selector::Selector(
 	not_null<QWidget*> parent,
-	not_null<Window::SessionController*> parentController,
+	std::shared_ptr<ChatHelpers::Show> show,
 	ChatHelpers::EmojiListMode mode,
 	std::vector<DocumentId> recent,
-	Fn<void(bool fast)> close)
+	Fn<void(bool fast)> close,
+	bool child)
 : Selector(
 	parent,
-	parentController,
+	std::move(show),
 	{ .customAllowed = true },
 	mode,
 	std::move(recent),
 	nullptr,
-	close) {
+	close,
+	child) {
 }
 
 Selector::Selector(
 	not_null<QWidget*> parent,
-	not_null<Window::SessionController*> parentController,
+	std::shared_ptr<ChatHelpers::Show> show,
 	const Data::PossibleItemReactionsRef &reactions,
 	ChatHelpers::EmojiListMode mode,
 	std::vector<DocumentId> recent,
 	IconFactory iconFactory,
-	Fn<void(bool fast)> close)
+	Fn<void(bool fast)> close,
+	bool child)
 : RpWidget(parent)
-, _parentController(parentController.get())
+, _show(std::move(show))
 , _reactions(reactions)
 , _recent(std::move(recent))
 , _listMode(mode)
@@ -167,12 +172,7 @@ Selector::Selector(
 , _skipy((st::reactStripHeight - st::reactStripSize) / 2) {
 	setMouseTracking(true);
 
-	_useTransparency = Ui::Platform::TranslucentWindowsSupported();
-
-	parentController->content()->alive(
-	) | rpl::start_with_done([=] {
-		close(true);
-	}, lifetime());
+	_useTransparency = child || Ui::Platform::TranslucentWindowsSupported();
 }
 
 bool Selector::useTransparency() const {
@@ -288,6 +288,10 @@ void Selector::beforeDestroy() {
 	}
 }
 
+rpl::producer<> Selector::escapes() const {
+	return _escapes.events();
+}
+
 void Selector::updateShowState(
 		float64 progress,
 		float64 opacity,
@@ -424,6 +428,7 @@ void Selector::paintExpanding(Painter &p, float64 progress) {
 		p,
 		rects.list.marginsRemoved(st::reactPanelEmojiPan.margin),
 		rects.finalBottom,
+		rects.expanding,
 		progress,
 		RectPart::TopRight);
 	paintFadingExpandIcon(p, progress);
@@ -479,6 +484,7 @@ auto Selector::paintExpandingBg(QPainter &p, float64 progress)
 		.categories = QRect(inner.x(), inner.y(), inner.width(), categories),
 		.list = inner.marginsRemoved({ 0, categories, 0, 0 }),
 		.radius = radius,
+		.expanding = expanding,
 		.finalBottom = height() - extents.bottom(),
 	};
 }
@@ -538,10 +544,7 @@ void Selector::finishExpand() {
 	}
 	_scroll->show();
 	_list->afterShown();
-
-	if (const auto controller = _parentController.get()) {
-		controller->session().api().updateCustomEmoji();
-	}
+	_show->session().api().updateCustomEmoji();
 }
 
 void Selector::paintBubble(QPainter &p, int innerWidth) {
@@ -662,6 +665,7 @@ void Selector::expand() {
 		return;
 	}
 	_expandScheduled = true;
+	_willExpand.fire({});
 	const auto parent = parentWidget()->geometry();
 	const auto extents = extentsForShadow();
 	const auto heightLimit = _reactions.customAllowed
@@ -672,15 +676,14 @@ void Selector::expand() {
 		extents.top() + heightLimit + extents.bottom());
 	const auto additionalBottom = willBeHeight - height();
 	const auto additional = _specialExpandTopSkip + additionalBottom;
-	const auto strong = _parentController.get();
-	if (additionalBottom < 0 || additional <= 0 || !strong) {
+	if (additionalBottom < 0 || additional <= 0) {
 		return;
 	} else if (additionalBottom > 0) {
 		resize(width(), height() + additionalBottom);
 		raise();
 	}
 
-	createList(strong);
+	createList();
 	cacheExpandIcon();
 
 	[[maybe_unused]] const auto grabbed = Ui::GrabWidget(_scroll);
@@ -705,7 +708,7 @@ void Selector::cacheExpandIcon() {
 	_strip->paintOne(q, _strip->count() - 1, { 0, 0 }, 1.);
 }
 
-void Selector::createList(not_null<Window::SessionController*> controller) {
+void Selector::createList() {
 	using namespace ChatHelpers;
 	auto recent = _recent;
 	auto defaultReactionIds = base::flat_map<DocumentId, QString>();
@@ -725,7 +728,7 @@ void Selector::createList(not_null<Window::SessionController*> controller) {
 			}
 		};
 	}
-	const auto manager = &controller->session().data().customEmojiManager();
+	const auto manager = &_show->session().data().customEmojiManager();
 	_stripPaintOneShift = [&] {
 		// See EmojiListWidget custom emoji position resolving.
 		const auto area = st::emojiPanArea;
@@ -777,7 +780,7 @@ void Selector::createList(not_null<Window::SessionController*> controller) {
 	}
 	_list = _scroll->setOwnedWidget(
 		object_ptr<EmojiListWidget>(_scroll, EmojiListDescriptor{
-			.show = controller->uiShow(),
+			.show = _show,
 			.mode = _listMode,
 			.paused = [] { return false; },
 			.customRecentList = std::move(recent),
@@ -786,6 +789,8 @@ void Selector::createList(not_null<Window::SessionController*> controller) {
 		})
 	).data();
 
+	_list->escapes() | rpl::start_to_stream(_escapes, _list->lifetime());
+
 	_list->customChosen(
 	) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
 		const auto id = DocumentId{ data.document->id };
@@ -937,10 +942,11 @@ AttachSelectorResult MakeJustSelectorMenu(
 		Fn<void(ChosenReaction)> chosen) {
 	const auto selector = Ui::CreateChild<Selector>(
 		menu.get(),
-		controller,
+		controller->uiShow(),
 		mode,
 		std::move(recent),
-		[=](bool fast) { menu->hideMenu(fast); });
+		[=](bool fast) { menu->hideMenu(fast); },
+		false); // child
 	if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {
 		return AttachSelectorResult::Failed;
 	}
@@ -1011,10 +1017,11 @@ AttachSelectorResult AttachSelectorToMenu(
 	const auto withSearch = reactions.customAllowed;
 	const auto selector = Ui::CreateChild<Selector>(
 		menu.get(),
-		controller,
+		controller->uiShow(),
 		std::move(reactions),
 		std::move(iconFactory),
-		[=](bool fast) { menu->hideMenu(fast); });
+		[=](bool fast) { menu->hideMenu(fast); },
+		false); // child
 	if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {
 		return AttachSelectorResult::Failed;
 	}
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
index 6e6d119cd..5cf7ca294 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
@@ -19,6 +19,7 @@ struct ReactionId;
 } // namespace Data
 
 namespace ChatHelpers {
+class Show;
 class TabbedPanel;
 class EmojiListWidget;
 class StickersListFooter;
@@ -41,16 +42,18 @@ class Selector final : public Ui::RpWidget {
 public:
 	Selector(
 		not_null<QWidget*> parent,
-		not_null<Window::SessionController*> parentController,
+		std::shared_ptr<ChatHelpers::Show> show,
 		const Data::PossibleItemReactionsRef &reactions,
 		IconFactory iconFactory,
-		Fn<void(bool fast)> close);
+		Fn<void(bool fast)> close,
+		bool child = false);
 	Selector(
 		not_null<QWidget*> parent,
-		not_null<Window::SessionController*> parentController,
+		std::shared_ptr<ChatHelpers::Show> show,
 		ChatHelpers::EmojiListMode mode,
 		std::vector<DocumentId> recent,
-		Fn<void(bool fast)> close);
+		Fn<void(bool fast)> close,
+		bool child = false);
 
 	[[nodiscard]] bool useTransparency() const;
 
@@ -68,6 +71,10 @@ public:
 	[[nodiscard]] rpl::producer<> premiumPromoChosen() const {
 		return _premiumPromoChosen.events();
 	}
+	[[nodiscard]] rpl::producer<> willExpand() const {
+		return _willExpand.events();
+	}
+	[[nodiscard]] rpl::producer<> escapes() const;
 
 	void updateShowState(
 		float64 progress,
@@ -82,17 +89,19 @@ private:
 		QRect categories;
 		QRect list;
 		float64 radius = 0.;
+		float64 expanding = 0.;
 		int finalBottom = 0;
 	};
 
 	Selector(
 		not_null<QWidget*> parent,
-		not_null<Window::SessionController*> parentController,
+		std::shared_ptr<ChatHelpers::Show> show,
 		const Data::PossibleItemReactionsRef &reactions,
 		ChatHelpers::EmojiListMode mode,
 		std::vector<DocumentId> recent,
 		IconFactory iconFactory,
-		Fn<void(bool fast)> close);
+		Fn<void(bool fast)> close,
+		bool child);
 
 	void paintEvent(QPaintEvent *e) override;
 	void mouseMoveEvent(QMouseEvent *e) override;
@@ -116,11 +125,11 @@ private:
 
 	void expand();
 	void cacheExpandIcon();
-	void createList(not_null<Window::SessionController*> controller);
+	void createList();
 	void finishExpand();
 	ChosenReaction lookupChosen(const Data::ReactionId &id) const;
 
-	const base::weak_ptr<Window::SessionController> _parentController;
+	const std::shared_ptr<ChatHelpers::Show> _show;
 	const Data::PossibleItemReactions _reactions;
 	const std::vector<DocumentId> _recent;
 	const ChatHelpers::EmojiListMode _listMode;
@@ -133,6 +142,8 @@ private:
 
 	rpl::event_stream<ChosenReaction> _chosen;
 	rpl::event_stream<> _premiumPromoChosen;
+	rpl::event_stream<> _willExpand;
+	rpl::event_stream<> _escapes;
 
 	Ui::ScrollArea *_scroll = nullptr;
 	ChatHelpers::EmojiListWidget *_list = nullptr;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 76f04f871..6ea4a3f82 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_reactions.h"
 #include "media/stories/media_stories_recent_views.h"
 #include "media/stories/media_stories_reply.h"
 #include "media/stories/media_stories_view.h"
@@ -125,10 +126,17 @@ Controller::Controller(not_null<Delegate*> delegate)
 , _header(std::make_unique<Header>(this))
 , _slider(std::make_unique<Slider>(this))
 , _replyArea(std::make_unique<ReplyArea>(this))
+, _reactions(std::make_unique<Reactions>(this))
 , _recentViews(std::make_unique<RecentViews>(this)) {
 	initLayout();
 
-	_replyArea->activeValue(
+	using namespace rpl::mappers;
+
+	rpl::combine(
+		_replyArea->activeValue(),
+		_reactions->expandedValue(),
+		_1 || _2
+	) | rpl::distinct_until_changed(
 	) | rpl::start_with_next([=](bool active) {
 		if (active) {
 			_captionFullView = nullptr;
@@ -140,6 +148,23 @@ Controller::Controller(not_null<Delegate*> delegate)
 	_replyArea->focusedValue(
 	) | rpl::start_with_next([=](bool focused) {
 		_replyFocused = focused;
+		if (!_replyFocused) {
+			_reactions->hideIfCollapsed();
+		} else if (!_hasSendText) {
+			_reactions->show();
+		}
+	}, _lifetime);
+
+	_replyArea->hasSendTextValue(
+	) | rpl::start_with_next([=](bool has) {
+		_hasSendText = has;
+		if (_replyFocused) {
+			if (_hasSendText) {
+				_reactions->hide();
+			} else {
+				_reactions->show();
+			}
+		}
 	}, _lifetime);
 
 	_delegate->storiesLayerShown(
@@ -165,6 +190,9 @@ Controller::Controller(not_null<Delegate*> delegate)
 Controller::~Controller() = default;
 
 void Controller::updateContentFaded() {
+	if (_contentFaded == _replyActive) {
+		return;
+	}
 	_contentFaded = _replyActive;
 	_contentFadeAnimation.start(
 		[=] { _delegate->storiesRepaint(); },
@@ -227,6 +255,13 @@ void Controller::initLayout() {
 			contentWidth,
 			contentHeight);
 
+		const auto reactionsWidth = st::storiesReactionsWidth;
+		layout.reactions = QRect(
+			(size.width() - reactionsWidth) / 2,
+			layout.content.y(),
+			reactionsWidth,
+			contentHeight);
+
 		if (layout.headerLayout == HeaderLayout::Outside) {
 			layout.header = QRect(
 				layout.content.topLeft() - QPoint(0, outsideHeaderHeight),
@@ -385,6 +420,11 @@ auto Controller::stickerOrEmojiChosen() const
 	return _delegate->storiesStickerOrEmojiChosen();
 }
 
+auto Controller::cachedReactionIconFactory() const
+-> HistoryView::Reactions::CachedIconFactory & {
+	return _delegate->storiesCachedReactionIconFactory();
+}
+
 void Controller::show(
 		not_null<Data::Story*> story,
 		Data::StoriesContext context) {
@@ -448,6 +488,7 @@ void Controller::show(
 	_captionText = story->caption();
 	_captionFullView = nullptr;
 	invalidate_weak_ptrs(&_viewsLoadGuard);
+	_reactions->hide();
 	if (_replyFocused) {
 		unfocusReply();
 	}
@@ -693,6 +734,13 @@ void Controller::togglePaused(bool paused) {
 	}
 }
 
+void Controller::contentPressed(bool pressed) {
+	togglePaused(pressed);
+	if (pressed) {
+		_reactions->collapse();
+	}
+}
+
 void Controller::setMenuShown(bool shown) {
 	if (_menuShown != shown) {
 		_menuShown = shown;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 4fd02ac39..f59cb0f36 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -23,6 +23,10 @@ namespace Data {
 struct FileOrigin;
 } // namespace Data
 
+namespace HistoryView::Reactions {
+class CachedIconFactory;
+} // namespace HistoryView::Reactions
+
 namespace Ui {
 class RpWidget;
 } // namespace Ui
@@ -40,6 +44,7 @@ namespace Media::Stories {
 class Header;
 class Slider;
 class ReplyArea;
+class Reactions;
 class RecentViews;
 class Sibling;
 class Delegate;
@@ -66,6 +71,7 @@ struct Layout {
 	QRect content;
 	QRect header;
 	QRect slider;
+	QRect reactions;
 	int controlsWidth = 0;
 	QPoint controlsBottomPosition;
 	QRect views;
@@ -98,6 +104,8 @@ public:
 	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
 	[[nodiscard]] auto stickerOrEmojiChosen() const
 	-> rpl::producer<ChatHelpers::FileChosen>;
+	[[nodiscard]] auto cachedReactionIconFactory() const
+		-> HistoryView::Reactions::CachedIconFactory &;
 
 	void show(not_null<Data::Story*> story, Data::StoriesContext context);
 	void ready();
@@ -109,6 +117,7 @@ public:
 	[[nodiscard]] bool jumpFor(int delta);
 	[[nodiscard]] bool paused() const;
 	void togglePaused(bool paused);
+	void contentPressed(bool pressed);
 	void setMenuShown(bool shown);
 
 	[[nodiscard]] bool canDownload() const;
@@ -163,6 +172,7 @@ private:
 	const std::unique_ptr<Header> _header;
 	const std::unique_ptr<Slider> _slider;
 	const std::unique_ptr<ReplyArea> _replyArea;
+	const std::unique_ptr<Reactions> _reactions;
 	const std::unique_ptr<RecentViews> _recentViews;
 	std::unique_ptr<PhotoPlayback> _photoPlayback;
 	std::unique_ptr<CaptionFullView> _captionFullView;
@@ -173,6 +183,7 @@ private:
 	bool _windowActive = false;
 	bool _replyFocused = false;
 	bool _replyActive = false;
+	bool _hasSendText = false;
 	bool _layerShown = false;
 	bool _menuShown = false;
 	bool _paused = false;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h
index 9f8d7ce4d..6dfbf3acd 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h
@@ -16,6 +16,10 @@ namespace Data {
 struct StoriesContext;
 } // namespace Data
 
+namespace HistoryView::Reactions {
+class CachedIconFactory;
+} // namespace HistoryView::Reactions
+
 namespace Main {
 class Session;
 } // namespace Main
@@ -43,6 +47,8 @@ public:
 		-> std::shared_ptr<ChatHelpers::Show> = 0;
 	[[nodiscard]] virtual auto storiesStickerOrEmojiChosen()
 		-> rpl::producer<ChatHelpers::FileChosen> = 0;
+	[[nodiscard]] virtual auto storiesCachedReactionIconFactory()
+		-> HistoryView::Reactions::CachedIconFactory & = 0;
 	virtual void storiesJumpTo(
 		not_null<Main::Session*> session,
 		FullStoryId id,
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
new file mode 100644
index 000000000..446c8f91b
--- /dev/null
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -0,0 +1,239 @@
+/*
+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_reactions.h"
+
+#include "boxes/premium_preview_box.h"
+#include "chat_helpers/compose/compose_show.h"
+#include "data/data_message_reactions.h"
+#include "data/data_session.h"
+#include "history/view/reactions/history_view_reactions_selector.h"
+#include "main/main_session.h"
+#include "media/stories/media_stories_controller.h"
+#include "styles/style_chat_helpers.h"
+#include "styles/style_media_view.h"
+#include "styles/style_widgets.h"
+
+namespace Media::Stories {
+namespace {
+
+[[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleReactions(
+		not_null<Main::Session*> session) {
+	auto result = Data::PossibleItemReactionsRef();
+	const auto reactions = &session->data().reactions();
+	const auto &full = reactions->list(Data::Reactions::Type::Active);
+	const auto &top = reactions->list(Data::Reactions::Type::Top);
+	const auto &recent = reactions->list(Data::Reactions::Type::Recent);
+	const auto premiumPossible = session->premiumPossible();
+	auto added = base::flat_set<Data::ReactionId>();
+	result.recent.reserve(full.size());
+	for (const auto &reaction : ranges::views::concat(top, recent, full)) {
+		if (premiumPossible || !reaction.id.custom()) {
+			if (added.emplace(reaction.id).second) {
+				result.recent.push_back(&reaction);
+			}
+		}
+	}
+	result.customAllowed = premiumPossible;
+	const auto i = ranges::find(
+		result.recent,
+		reactions->favoriteId(),
+		&Data::Reaction::id);
+	if (i != end(result.recent) && i != begin(result.recent)) {
+		std::rotate(begin(result.recent), i, i + 1);
+	}
+	return result;
+}
+
+} // namespace
+
+struct Reactions::Hiding {
+	explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
+	}
+
+	Ui::RpWidget widget;
+	Ui::Animations::Simple animation;
+	QImage frame;
+};
+
+Reactions::Reactions(not_null<Controller*> controller)
+: _controller(controller) {
+}
+
+Reactions::~Reactions() = default;
+
+void Reactions::show() {
+	if (_shown) {
+		return;
+	}
+	create();
+	if (!_selector) {
+		return;
+	}
+	const auto duration = st::defaultPanelAnimation.heightDuration
+		* st::defaultPopupMenu.showDuration;
+	_shown = true;
+	_showing.start([=] { updateShowState(); }, 0., 1., duration);
+	updateShowState();
+	_parent->show();
+}
+
+void Reactions::hide() {
+	if (!_selector) {
+		return;
+	}
+	_selector->beforeDestroy();
+	if (!anim::Disabled()) {
+		fadeOutSelector();
+	}
+	_shown = false;
+	_expanded = false;
+	_showing.stop();
+	_selector = nullptr;
+	_parent = nullptr;
+}
+
+void Reactions::hideIfCollapsed() {
+	if (!_expanded.current()) {
+		hide();
+	}
+}
+
+void Reactions::collapse() {
+	if (_expanded.current()) {
+		hide();
+		show();
+	}
+}
+
+void Reactions::create() {
+	auto reactions = LookupPossibleReactions(
+		&_controller->uiShow()->session());
+	if (reactions.recent.empty() && !reactions.morePremiumAvailable) {
+		return;
+	}
+	_parent = std::make_unique<Ui::RpWidget>(_controller->wrap().get());
+	_parent->show();
+
+	_parent->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
+		if (e->type() == QEvent::MouseButtonPress) {
+			const auto event = static_cast<QMouseEvent*>(e.get());
+			if (event->button() == Qt::LeftButton) {
+				if (!_selector
+					|| !_selector->geometry().contains(event->pos())) {
+					collapse();
+				}
+			}
+		}
+	}, _parent->lifetime());
+
+	const auto withSearch = reactions.customAllowed;
+	_selector = std::make_unique<HistoryView::Reactions::Selector>(
+		_parent.get(),
+		_controller->uiShow(),
+		std::move(reactions),
+		_controller->cachedReactionIconFactory().createMethod(),
+		[=](bool fast) { hide(); });
+
+	_selector->chosen(
+	) | rpl::start_with_next([=](
+			HistoryView::Reactions::ChosenReaction reaction) {
+		hide();
+		//reaction.context = itemId;
+		//chosen(std::move(reaction));
+	}, _selector->lifetime());
+
+	_selector->premiumPromoChosen() | rpl::start_with_next([=] {
+		hide();
+		ShowPremiumPreviewBox(
+			_controller->uiShow(),
+			PremiumPreview::InfiniteReactions);
+	}, _selector->lifetime());
+
+	const auto desiredWidth = st::storiesReactionsWidth;
+	const auto maxWidth = desiredWidth * 2;
+	const auto width = _selector->countWidth(desiredWidth, maxWidth);
+	const auto extents = _selector->extentsForShadow();
+	const auto categoriesTop = _selector->extendTopForCategories();
+	const auto full = extents.left() + width + extents.right();
+
+	_shownValue = 0.;
+	rpl::combine(
+		_controller->layoutValue(),
+		_shownValue.value()
+	) | rpl::start_with_next([=](const Layout &layout, float64 shown) {
+		const auto shift = int(base::SafeRound((full / 2.) * shown));
+		_parent->setGeometry(QRect(
+			layout.reactions.x() + layout.reactions.width() / 2 - shift,
+			layout.reactions.y(),
+			full,
+			layout.reactions.height()));
+		const auto innerTop = layout.reactions.height()
+			- st::storiesReactionsBottomSkip
+			- st::reactStripHeight;
+		const auto maxAdded = innerTop - extents.top() - categoriesTop;
+		const auto added = std::min(maxAdded, st::storiesReactionsAddedTop);
+		_selector->setSpecialExpandTopSkip(added);
+		_selector->initGeometry(innerTop);
+	}, _selector->lifetime());
+
+	_selector->willExpand(
+	) | rpl::start_with_next([=] {
+		_expanded = true;
+	}, _selector->lifetime());
+
+	_selector->escapes() | rpl::start_with_next([=] {
+		collapse();
+	}, _selector->lifetime());
+}
+
+void Reactions::fadeOutSelector() {
+	const auto wrap = _controller->wrap().get();
+	const auto geometry = Ui::MapFrom(
+		wrap,
+		_parent.get(),
+		_selector->geometry());
+	_hiding.push_back(std::make_unique<Hiding>(wrap));
+	const auto raw = _hiding.back().get();
+	raw->frame = Ui::GrabWidgetToImage(_selector.get());
+	raw->widget.setGeometry(geometry);
+	raw->widget.show();
+	raw->widget.paintRequest(
+	) | rpl::start_with_next([=] {
+		if (const auto opacity = raw->animation.value(0.)) {
+			auto p = QPainter(&raw->widget);
+			p.setOpacity(opacity);
+			p.drawImage(0, 0, raw->frame);
+		}
+	}, raw->widget.lifetime());
+	Ui::PostponeCall(&raw->widget, [=] {
+		raw->animation.start([=] {
+			if (raw->animation.animating()) {
+				raw->widget.update();
+			} else {
+				const auto i = ranges::find(
+					_hiding,
+					raw,
+					&std::unique_ptr<Hiding>::get);
+				if (i != end(_hiding)) {
+					_hiding.erase(i);
+				}
+			}
+		}, 1., 0., st::slideWrapDuration);
+	});
+}
+
+void Reactions::updateShowState() {
+	const auto progress = _showing.value(_shown ? 1. : 0.);
+	const auto opacity = 1.;
+	const auto appearing = _showing.animating();
+	const auto toggling = false;
+	_shownValue = progress;
+	_selector->updateShowState(progress, opacity, appearing, toggling);
+}
+
+} // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
new file mode 100644
index 000000000..e9a89a09c
--- /dev/null
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
@@ -0,0 +1,57 @@
+/*
+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/effects/animations.h"
+
+namespace HistoryView::Reactions {
+class Selector;
+} // namespace HistoryView::Reactions
+
+namespace Ui {
+class RpWidget;
+} // namespace Ui
+
+namespace Media::Stories {
+
+class Controller;
+
+class Reactions final {
+public:
+	explicit Reactions(not_null<Controller*> controller);
+	~Reactions();
+
+	[[nodiscard]] rpl::producer<bool> expandedValue() const {
+		return _expanded.value();
+	}
+
+	void show();
+	void hide();
+	void hideIfCollapsed();
+	void collapse();
+
+private:
+	struct Hiding;
+
+	void create();
+	void updateShowState();
+	void fadeOutSelector();
+
+	const not_null<Controller*> _controller;
+
+	std::unique_ptr<Ui::RpWidget> _parent;
+	std::unique_ptr<HistoryView::Reactions::Selector> _selector;
+	std::vector<std::unique_ptr<Hiding>> _hiding;
+	Ui::Animations::Simple _showing;
+	rpl::variable<float64> _shownValue;
+	rpl::variable<bool> _expanded;
+	bool _shown = false;
+
+};
+
+} // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index cbff23d73..db914d816 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -579,6 +579,10 @@ rpl::producer<bool> ReplyArea::focusedValue() const {
 	return _controls->focusedValue();
 }
 
+rpl::producer<bool> ReplyArea::hasSendTextValue() const {
+	return _controls->hasSendTextValue();
+}
+
 rpl::producer<bool> ReplyArea::activeValue() const {
 	using namespace rpl::mappers;
 	return rpl::combine(
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h
index d50496d5d..30d15cb28 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h
@@ -59,6 +59,7 @@ public:
 
 	[[nodiscard]] rpl::producer<bool> focusedValue() const;
 	[[nodiscard]] rpl::producer<bool> activeValue() const;
+	[[nodiscard]] rpl::producer<bool> hasSendTextValue() 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 de254b870..47f967160 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
@@ -75,6 +75,10 @@ void View::togglePaused(bool paused) {
 	_controller->togglePaused(paused);
 }
 
+void View::contentPressed(bool pressed) {
+	_controller->contentPressed(pressed);
+}
+
 SiblingView View::sibling(SiblingType type) const {
 	return _controller->sibling(type);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h
index b25011de5..40ee13c91 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -73,6 +73,7 @@ public:
 
 	[[nodiscard]] bool paused() const;
 	void togglePaused(bool paused);
+	void contentPressed(bool pressed);
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index b1674e41b..cd88a570f 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -747,3 +747,6 @@ storiesWhoViewed: WhoRead(defaultWhoRead) {
 		align: align(left);
 	}
 }
+storiesReactionsWidth: 210px;
+storiesReactionsBottomSkip: 29px;
+storiesReactionsAddedTop: 200px;
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 1d1880c83..8aa3612c6 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -54,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/history_item.h"
 #include "history/history_item_helpers.h"
 #include "history/view/media/history_view_media.h"
+#include "history/view/reactions/history_view_reactions_strip.h"
 #include "data/data_media_types.h"
 #include "data/data_session.h"
 #include "data/data_stories.h"
@@ -421,6 +422,7 @@ OverlayWidget::OverlayWidget()
 , _widget(_surface->rpWidget())
 , _fullscreen(Core::App().settings().mediaViewPosition().maximized == 2)
 , _windowed(Core::App().settings().mediaViewPosition().maximized == 0)
+, _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
 , _layerBg(std::make_unique<Ui::LayerManager>(_body))
 , _docDownload(_body, tr::lng_media_download(tr::now), st::mediaviewFileLink)
 , _docSaveAs(_body, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink)
@@ -1600,9 +1602,11 @@ void OverlayWidget::waitingAnimationCallback() {
 }
 
 void OverlayWidget::updateCursor() {
-	setCursor(_controlsState == ControlsHidden
+	setCursor((_controlsState == ControlsHidden)
 		? Qt::BlankCursor
-		: (_over == OverNone ? style::cur_default : style::cur_pointer));
+		: (_over == OverNone || (_over == OverVideo && _stories))
+		? style::cur_default
+		: style::cur_pointer);
 }
 
 int OverlayWidget::finalContentRotation() const {
@@ -4045,6 +4049,11 @@ auto OverlayWidget::storiesStickerOrEmojiChosen()
 	return _storiesStickerOrEmojiChosen.events();
 }
 
+auto OverlayWidget::storiesCachedReactionIconFactory()
+-> HistoryView::Reactions::CachedIconFactory & {
+	return *_cachedReactionIconFactory;
+}
+
 void OverlayWidget::storiesJumpTo(
 		not_null<Main::Session*> session,
 		FullStoryId id,
@@ -5192,7 +5201,7 @@ void OverlayWidget::handleMousePress(
 				|| _over == OverVideo) {
 				_down = _over;
 				if (_over == OverVideo && _stories) {
-					_stories->togglePaused(true);
+					_stories->contentPressed(true);
 				}
 			} else if (!_saveMsg.contains(position) || !isSaveMsgShown()) {
 				_pressed = true;
@@ -5491,7 +5500,7 @@ void OverlayWidget::handleMouseRelease(
 		InvokeQueued(_widget, [=] { showDropdown(); });
 	} else if (_over == OverVideo && _down == OverVideo) {
 		if (_stories) {
-			_stories->togglePaused(false);
+			_stories->contentPressed(false);
 		} else if (_streamed) {
 			playbackPauseResume();
 		}
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index 2085fef99..01cc71f3d 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -51,11 +51,13 @@ namespace Platform {
 class OverlayWidgetHelper;
 } // namespace Platform
 
-namespace Window {
-namespace Theme {
+namespace Window::Theme {
 struct Preview;
-} // namespace Theme
-} // namespace Window
+} // namespace Window::Theme
+
+namespace HistoryView::Reactions {
+class CachedIconFactory;
+} // namespace HistoryView::Reactions
 
 namespace Media::Player {
 struct TrackState;
@@ -245,6 +247,8 @@ private:
 	std::shared_ptr<ChatHelpers::Show> storiesShow() override;
 	auto storiesStickerOrEmojiChosen()
 		-> rpl::producer<ChatHelpers::FileChosen> override;
+	auto storiesCachedReactionIconFactory()
+		-> HistoryView::Reactions::CachedIconFactory & override;
 	void storiesJumpTo(
 		not_null<Main::Session*> session,
 		FullStoryId id,
@@ -600,6 +604,8 @@ private:
 	bool _showAsPip = false;
 
 	std::unique_ptr<Stories::View> _stories;
+	using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;
+	std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;
 	std::shared_ptr<Show> _cachedShow;
 	rpl::event_stream<> _storiesChanged;
 	Main::Session *_storiesSession = nullptr;
diff --git a/Telegram/SourceFiles/ui/controls/tabbed_search.cpp b/Telegram/SourceFiles/ui/controls/tabbed_search.cpp
index a4bf604a5..a4022d514 100644
--- a/Telegram/SourceFiles/ui/controls/tabbed_search.cpp
+++ b/Telegram/SourceFiles/ui/controls/tabbed_search.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "ui/controls/tabbed_search.h"
 
+#include "base/qt_signal_producer.h"
 #include "lang/lang_keys.h"
 #include "ui/widgets/input_fields.h"
 #include "ui/wrap/fade_wrap.h"
@@ -517,6 +518,12 @@ void SearchWithGroups::ensureRounding(int size, float64 ratio) {
 	_rounding.setDevicePixelRatio(ratio);
 }
 
+rpl::producer<> SearchWithGroups::escapes() const {
+	return base::qt_signal_producer(
+		_field.get(),
+		&Ui::InputField::cancelled);
+}
+
 rpl::producer<std::vector<QString>> SearchWithGroups::queryValue() const {
 	return _query.value();
 }
@@ -659,6 +666,10 @@ void TabbedSearch::returnFocus() {
 	_search.returnFocus();
 }
 
+rpl::producer<> TabbedSearch::escapes() const {
+	return _search.escapes();
+}
+
 rpl::producer<std::vector<QString>> TabbedSearch::queryValue() const {
 	return _search.queryValue();
 }
diff --git a/Telegram/SourceFiles/ui/controls/tabbed_search.h b/Telegram/SourceFiles/ui/controls/tabbed_search.h
index ce5bb3e77..000cbccea 100644
--- a/Telegram/SourceFiles/ui/controls/tabbed_search.h
+++ b/Telegram/SourceFiles/ui/controls/tabbed_search.h
@@ -50,6 +50,7 @@ class SearchWithGroups final : public RpWidget {
 public:
 	SearchWithGroups(QWidget *parent, SearchDescriptor descriptor);
 
+	[[nodiscard]] rpl::producer<> escapes() const;
 	[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;
 	[[nodiscard]] auto debouncedQueryValue() const
 		-> rpl::producer<std::vector<QString>>;
@@ -116,6 +117,7 @@ public:
 	[[nodiscard]] int height() const;
 	[[nodiscard]] QImage grab();
 
+	[[nodiscard]] rpl::producer<> escapes() const;
 	[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;
 	[[nodiscard]] auto debouncedQueryValue() const
 		->rpl::producer<std::vector<QString>>;

From 39538e89e0a0fe23060ff168443e95567dc7a76a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 9 Jun 2023 17:02:23 +0400
Subject: [PATCH 064/259] Show stories reactions in dark theme.

---
 .../chat_helpers/chat_helpers.style           |  26 +-
 .../history_view_reactions_button.cpp         |   2 +
 .../history_view_reactions_selector.cpp       |  43 ++--
 .../history_view_reactions_selector.h         |   4 +
 .../history_view_reactions_strip.cpp          |  21 +-
 .../reactions/history_view_reactions_strip.h  |  12 +-
 .../media/stories/media_stories_reactions.cpp |   1 +
 .../SourceFiles/media/view/media_view.style   | 227 ++++++++++--------
 Telegram/SourceFiles/ui/chat/chat.style       |  13 -
 9 files changed, 206 insertions(+), 143 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 5b16daf5d..e5552ead4 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -69,6 +69,11 @@ ComposeIcons {
 	menuWhenOnline: icon;
 	menuSpoiler: icon;
 	menuSpoilerOff: icon;
+
+	stripBubble: icon;
+	stripPremiumLocked: icon;
+	stripExpandPanel: icon;
+	stripExpandDropdown: icon;
 }
 
 EmojiSuggestions {
@@ -532,6 +537,23 @@ defaultComposeIcons: ComposeIcons {
 	menuWhenOnline: menuIconWhenOnline;
 	menuSpoiler: menuIconSpoiler;
 	menuSpoilerOff: menuIconSpoilerOff;
+
+	stripBubble: icon{
+		{ "chat/reactions_bubble_shadow", windowShadowFg },
+		{ "chat/reactions_bubble", windowBg },
+	};
+	stripPremiumLocked: icon{
+		{ "chat/reactions_premium_bg", historyPeerArchiveUserpicBg },
+		{ "chat/reactions_premium_star", historyPeerUserpicFg },
+	};
+	stripExpandPanel: icon{
+		{ "chat/reactions_round_big", windowBgRipple },
+		{ "chat/reactions_expand_panel", windowSubTextFg },
+	};
+	stripExpandDropdown: icon{
+		{ "chat/reactions_round_small", windowBgRipple },
+		{ "chat/reactions_expand_panel", windowSubTextFg },
+	};
 }
 defaultEmojiPan: EmojiPan {
 	margin: margins(7px, 0px, 7px, 0px);
@@ -646,10 +668,6 @@ reactStripSize: 32px;
 reactStripMinWidth: 60px;
 reactStripImage: 26px;
 reactStripSkip: 7px;
-reactStripBubble: icon{
-	{ "chat/reactions_bubble_shadow", windowShadowFg },
-	{ "chat/reactions_bubble", windowBg },
-};
 reactStripBubbleRight: 20px;
 userpicBuilderEmojiPan: EmojiPan(statusEmojiPan) {
 	margin: margins(reactStripSkip, 0px, reactStripSkip, 0px);
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp
index 0117ad4a5..345512ea1 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "main/main_session.h"
 #include "base/event_filter.h"
 #include "styles/style_chat.h"
+#include "styles/style_chat_helpers.h"
 #include "styles/style_menu_icons.h"
 
 namespace HistoryView::Reactions {
@@ -318,6 +319,7 @@ Manager::Manager(
 : _outer(CountOuterSize())
 , _inner(QRect({}, st::reactionCornerSize))
 , _strip(
+	st::reactPanelEmojiPan,
 	_inner,
 	st::reactionCornerImage,
 	crl::guard(this, [=] { updateCurrentButton(); }),
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
index 502c3d1e5..fa0dec3f9 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
@@ -105,6 +105,7 @@ bool StripEmoji::readyInDefaultState() {
 
 Selector::Selector(
 	not_null<QWidget*> parent,
+	const style::EmojiPan &st,
 	std::shared_ptr<ChatHelpers::Show> show,
 	const Data::PossibleItemReactionsRef &reactions,
 	IconFactory iconFactory,
@@ -112,6 +113,7 @@ Selector::Selector(
 	bool child)
 : Selector(
 	parent,
+	st,
 	std::move(show),
 	reactions,
 	(reactions.customAllowed
@@ -125,6 +127,7 @@ Selector::Selector(
 
 Selector::Selector(
 	not_null<QWidget*> parent,
+	const style::EmojiPan &st,
 	std::shared_ptr<ChatHelpers::Show> show,
 	ChatHelpers::EmojiListMode mode,
 	std::vector<DocumentId> recent,
@@ -132,6 +135,7 @@ Selector::Selector(
 	bool child)
 : Selector(
 	parent,
+	st,
 	std::move(show),
 	{ .customAllowed = true },
 	mode,
@@ -143,6 +147,7 @@ Selector::Selector(
 
 Selector::Selector(
 	not_null<QWidget*> parent,
+	const style::EmojiPan &st,
 	std::shared_ptr<ChatHelpers::Show> show,
 	const Data::PossibleItemReactionsRef &reactions,
 	ChatHelpers::EmojiListMode mode,
@@ -151,6 +156,7 @@ Selector::Selector(
 	Fn<void(bool fast)> close,
 	bool child)
 : RpWidget(parent)
+, _st(st)
 , _show(std::move(show))
 , _reactions(reactions)
 , _recent(std::move(recent))
@@ -162,6 +168,7 @@ Selector::Selector(
 	st::reactStripHeight)
 , _strip(iconFactory
 	? std::make_unique<Strip>(
+		_st,
 		QRect(0, 0, st::reactStripSize, st::reactStripSize),
 		st::reactStripImage,
 		crl::guard(this, [=] { update(_inner); }),
@@ -241,14 +248,14 @@ QMargins Selector::extentsForShadow() const {
 }
 
 int Selector::extendTopForCategories() const {
-	return _reactions.customAllowed ? st::reactPanelEmojiPan.footer : 0;
+	return _reactions.customAllowed ? _st.footer : 0;
 }
 
 int Selector::minimalHeight() const {
 	return _skipy
 		+ (_recentRows * _size)
 		+ st::emojiPanRadius
-		+ st::reactPanelEmojiPan.padding.bottom();
+		+ _st.padding.bottom();
 }
 
 void Selector::setSpecialExpandTopSkip(int skip) {
@@ -269,7 +276,7 @@ void Selector::initGeometry(int innerTop) {
 		? (extendTopForCategories() + _specialExpandTopSkip)
 		: 0;
 	const auto top = innerTop - extents.top() - _collapsedTopSkip;
-	const auto add = st::reactStripBubble.height() - extents.bottom();
+	const auto add = _st.icons.stripBubble.height() - extents.bottom();
 	_outer = QRect(0, _collapsedTopSkip, width, height);
 	_outerWithBubble = _outer.marginsAdded({ 0, 0, 0, add });
 	setGeometry(_outerWithBubble.marginsAdded(
@@ -329,7 +336,7 @@ void Selector::paintAppearing(QPainter &p) {
 	if (_paintBuffer.size() != _outerWithBubble.size() * factor) {
 		_paintBuffer = _cachedRound.PrepareImage(_outerWithBubble.size());
 	}
-	_paintBuffer.fill(st::defaultPopupMenu.menu.itemBg->c);
+	_paintBuffer.fill(_st.bg->c);
 	auto q = QPainter(&_paintBuffer);
 	const auto extents = extentsForShadow();
 	const auto appearedWidth = anim::interpolate(
@@ -348,7 +355,7 @@ void Selector::paintAppearing(QPainter &p) {
 		1.,
 		false);
 
-	_cachedRound.setBackgroundColor(st::defaultPopupMenu.menu.itemBg->c);
+	_cachedRound.setBackgroundColor(_st.bg->c);
 	_cachedRound.setShadowColor(st::shadowFg->c);
 	q.translate(QPoint(0, _collapsedTopSkip) - _inner.topLeft());
 	const auto radius = st::reactStripHeight / 2;
@@ -383,7 +390,7 @@ void Selector::paintBackgroundToBuffer() {
 	}
 	_paintBuffer.fill(Qt::transparent);
 
-	_cachedRound.setBackgroundColor(st::defaultPopupMenu.menu.itemBg->c);
+	_cachedRound.setBackgroundColor(_st.bg->c);
 	_cachedRound.setShadowColor(st::shadowFg->c);
 
 	auto p = QPainter(&_paintBuffer);
@@ -403,7 +410,7 @@ void Selector::paintCollapsed(QPainter &p) {
 		}
 		p.drawImage(_outer.topLeft(), _paintBuffer);
 	} else {
-		p.fillRect(_inner, st::defaultPopupMenu.menu.itemBg);
+		p.fillRect(_inner, _st.bg);
 	}
 	_strip->paint(
 		p,
@@ -426,7 +433,7 @@ void Selector::paintExpanding(Painter &p, float64 progress) {
 	}
 	_list->paintExpanding(
 		p,
-		rects.list.marginsRemoved(st::reactPanelEmojiPan.margin),
+		rects.list.marginsRemoved(_st.margin),
 		rects.finalBottom,
 		rects.expanding,
 		progress,
@@ -458,11 +465,11 @@ auto Selector::paintExpandingBg(QPainter &p, float64 progress)
 		const auto pattern = _cachedRound.validateFrame(frame, 1., radius);
 		const auto fill = _cachedRound.FillWithImage(p, outer, pattern);
 		if (!fill.isEmpty()) {
-			p.fillRect(fill, st::defaultPopupMenu.menu.itemBg);
+			p.fillRect(fill, _st.bg);
 		}
 	} else {
 		const auto inner = outer.marginsRemoved(extentsForShadow());
-		p.fillRect(inner, st::defaultPopupMenu.menu.itemBg);
+		p.fillRect(inner, _st.bg);
 		p.fillRect(
 			inner.x(),
 			inner.y() + inner.height(),
@@ -513,7 +520,7 @@ void Selector::paintExpanded(QPainter &p) {
 		p.drawImage(0, 0, _paintBuffer);
 	} else {
 		const auto inner = rect().marginsRemoved(extentsForShadow());
-		p.fillRect(inner, st::defaultPopupMenu.menu.itemBg);
+		p.fillRect(inner, _st.bg);
 		p.fillRect(
 			inner.x(),
 			inner.y() + inner.height(),
@@ -536,7 +543,7 @@ void Selector::finishExpand() {
 			st::emojiPanRadius);
 		const auto fill = _cachedRound.FillWithImage(q, rect(), pattern);
 		if (!fill.isEmpty()) {
-			q.fillRect(fill, st::defaultPopupMenu.menu.itemBg);
+			q.fillRect(fill, _st.bg);
 		}
 	}
 	if (_footer) {
@@ -548,7 +555,7 @@ void Selector::finishExpand() {
 }
 
 void Selector::paintBubble(QPainter &p, int innerWidth) {
-	const auto &bubble = st::reactStripBubble;
+	const auto &bubble = _st.icons.stripBubble;
 	const auto bubbleRight = std::min(
 		st::reactStripBubbleRight,
 		(innerWidth - bubble.width()) / 2);
@@ -772,8 +779,7 @@ void Selector::createList() {
 	_scroll = Ui::CreateChild<Ui::ScrollArea>(this, st::reactPanelScroll);
 	_scroll->hide();
 
-	const auto st = lifetime().make_state<style::EmojiPan>(
-		st::reactPanelEmojiPan);
+	const auto st = lifetime().make_state<style::EmojiPan>(_st);
 	st->padding.setTop(_skipy);
 	if (!_reactions.customAllowed) {
 		st->bg = st::transparent;
@@ -835,8 +841,7 @@ void Selector::createList() {
 		}, _shadow->lifetime());
 		_shadow->show();
 	}
-	const auto geometry = inner.marginsRemoved(
-		st::reactPanelEmojiPan.margin);
+	const auto geometry = inner.marginsRemoved(_st.margin);
 	_list->move(0, 0);
 	_list->resizeToWidth(geometry.width());
 	_list->refreshEmoji();
@@ -857,7 +862,7 @@ void Selector::createList() {
 	}, _list->lifetime());
 
 	_scroll->setGeometry(inner.marginsRemoved({
-		st::reactPanelEmojiPan.margin.left(),
+		_st.margin.left(),
 		_footer ? _footer->height() : 0,
 		0,
 		0,
@@ -942,6 +947,7 @@ AttachSelectorResult MakeJustSelectorMenu(
 		Fn<void(ChosenReaction)> chosen) {
 	const auto selector = Ui::CreateChild<Selector>(
 		menu.get(),
+		st::reactPanelEmojiPan,
 		controller->uiShow(),
 		mode,
 		std::move(recent),
@@ -1017,6 +1023,7 @@ AttachSelectorResult AttachSelectorToMenu(
 	const auto withSearch = reactions.customAllowed;
 	const auto selector = Ui::CreateChild<Selector>(
 		menu.get(),
+		st::reactPanelEmojiPan,
 		controller->uiShow(),
 		std::move(reactions),
 		std::move(iconFactory),
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
index 5cf7ca294..648fe4adb 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
@@ -42,6 +42,7 @@ class Selector final : public Ui::RpWidget {
 public:
 	Selector(
 		not_null<QWidget*> parent,
+		const style::EmojiPan &st,
 		std::shared_ptr<ChatHelpers::Show> show,
 		const Data::PossibleItemReactionsRef &reactions,
 		IconFactory iconFactory,
@@ -49,6 +50,7 @@ public:
 		bool child = false);
 	Selector(
 		not_null<QWidget*> parent,
+		const style::EmojiPan &st,
 		std::shared_ptr<ChatHelpers::Show> show,
 		ChatHelpers::EmojiListMode mode,
 		std::vector<DocumentId> recent,
@@ -95,6 +97,7 @@ private:
 
 	Selector(
 		not_null<QWidget*> parent,
+		const style::EmojiPan &st,
 		std::shared_ptr<ChatHelpers::Show> show,
 		const Data::PossibleItemReactionsRef &reactions,
 		ChatHelpers::EmojiListMode mode,
@@ -129,6 +132,7 @@ private:
 	void finishExpand();
 	ChosenReaction lookupChosen(const Data::ReactionId &id) const;
 
+	const style::EmojiPan &_st;
 	const std::shared_ptr<ChatHelpers::Show> _show;
 	const Data::PossibleItemReactions _reactions;
 	const std::vector<DocumentId> _recent;
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp
index 25d7fa4dd..f9c27b15c 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/animated_icon.h"
 #include "ui/painter.h"
 #include "styles/style_chat.h"
+#include "styles/style_chat_helpers.h"
 
 namespace HistoryView::Reactions {
 namespace {
@@ -44,11 +45,13 @@ constexpr auto kHoverScale = 1.24;
 } // namespace
 
 Strip::Strip(
+	const style::EmojiPan &st,
 	QRect inner,
 	int size,
 	Fn<void()> update,
 	IconFactory iconFactory)
-: _iconFactory(std::move(iconFactory))
+: _st(st)
+, _iconFactory(std::move(iconFactory))
 , _inner(inner)
 , _finalSize(size)
 , _update(std::move(update)) {
@@ -173,7 +176,7 @@ void Strip::paintOne(
 	} else {
 		const auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {
 			const auto size = int(std::floor(target.width() + 0.01));
-			const auto &textColor = st::windowFg->c;
+			const auto &textColor = _st.textFg->c;
 			const auto frame = animation->frame(
 				textColor,
 				{ size, size },
@@ -238,9 +241,9 @@ int Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const {
 	}
 	const auto &icon = *i;
 	if (const auto &appear = icon.appear; appear && appear->animating()) {
-		chosen.icon = appear->frame(st::windowFg->c);
+		chosen.icon = appear->frame(_st.textFg->c);
 	} else if (const auto &select = icon.select; select && select->valid()) {
-		chosen.icon = select->frame(st::windowFg->c);
+		chosen.icon = select->frame(_st.textFg->c);
 	}
 	return (i - begin(_icons));
 }
@@ -263,7 +266,7 @@ void Strip::paintPremiumIcon(
 		p.translate(-target.center());
 	}
 	auto hq = PainterHighQualityEnabler(p);
-	st::reactionPremiumLocked.paintInCenter(p, to);
+	_st.icons.stripPremiumLocked.paintInCenter(p, to);
 	if (scale != 1.) {
 		p.restore();
 	}
@@ -288,8 +291,8 @@ void Strip::paintExpandIcon(
 	}
 	auto hq = PainterHighQualityEnabler(p);
 	((_finalSize == st::reactionCornerImage)
-		? st::reactionsExpandDropdown
-		: st::reactionExpandPanel).paintInCenter(p, to);
+		? _st.icons.stripExpandDropdown
+		: _st.icons.stripExpandPanel).paintInCenter(p, to);
 	if (scale != 1.) {
 		p.restore();
 	}
@@ -495,7 +498,7 @@ void Strip::setMainReactionIcon() {
 	if (i != end(_loadCache) && i->second.icon) {
 		const auto &icon = i->second.icon;
 		if (!icon->frameIndex() && icon->width() == MainReactionSize()) {
-			_mainReactionImage = i->second.icon->frame(st::windowFg->c);
+			_mainReactionImage = i->second.icon->frame(_st.textFg->c);
 			return;
 		}
 	}
@@ -543,7 +546,7 @@ Ui::ImageSubrect Strip::validateEmoji(int frameIndex, float64 scale) {
 	if (_mainReactionImage.isNull()
 		&& _mainReactionIcon) {
 		_mainReactionImage = base::take(_mainReactionIcon)->frame(
-			st::windowFg->c);
+			_st.textFg->c);
 	}
 	if (!_mainReactionImage.isNull()) {
 		const auto target = QRect(
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h
index 9447b0d77..e8a98fd49 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h
@@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/round_area_with_shadow.h"
 #include "data/data_message_reaction_id.h"
 
+namespace style {
+struct EmojiPan;
+} // namespace style
+
 class HistoryItem;
 
 namespace Data {
@@ -44,7 +48,12 @@ class Strip final {
 public:
 	using ReactionId = Data::ReactionId;
 
-	Strip(QRect inner, int size, Fn<void()> update, IconFactory iconFactory);
+	Strip(
+		const style::EmojiPan &st,
+		QRect inner,
+		int size,
+		Fn<void()> update,
+		IconFactory iconFactory);
 
 	enum class AddedButton : uchar {
 		None,
@@ -121,6 +130,7 @@ private:
 	void resolveMainReactionIcon();
 	void setMainReactionIcon();
 
+	const style::EmojiPan &_st;
 	const IconFactory _iconFactory;
 	const QRect _inner;
 	const int _finalSize = 0;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
index 446c8f91b..49434bd53 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -134,6 +134,7 @@ void Reactions::create() {
 	const auto withSearch = reactions.customAllowed;
 	_selector = std::make_unique<HistoryView::Reactions::Selector>(
 		_parent.get(),
+		st::storiesReactionsPan,
 		_controller->uiShow(),
 		std::move(reactions),
 		_controller->cachedReactionIconFactory().createMethod(),
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index cd88a570f..cfc78f8b2 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -525,6 +525,122 @@ storiesComposePremium: PremiumLimits(defaultPremiumLimits) {
 	nonPremiumBg: storiesComposeBgOver;
 	nonPremiumFg: storiesComposeWhiteText;
 }
+storiesEmojiTabbedSearch: 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) {
+		textFg: storiesComposeWhiteText;
+		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;
+	}
+}
+storiesEmojiPan: 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;
+	categoriesBg: storiesComposeBg;
+	categoriesBgOver: storiesComposeBgOver;
+	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;
+		labelFgActive: storiesComposeBlue;
+		rippleBg: storiesComposeBgOver;
+		rippleBgActive: storiesComposeBgOver;
+	}
+	search: storiesEmojiTabbedSearch;
+	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 }};
+		menuSpoiler: icon {{ "menu/spoiler_on", storiesComposeWhiteText }};
+		menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }};
+
+		stripBubble: icon{
+			{ "chat/reactions_bubble_shadow", windowShadowFg },
+			{ "chat/reactions_bubble", storiesComposeBg },
+		};
+		stripPremiumLocked: icon{
+			{ "chat/reactions_premium_bg", storiesComposeBgRipple },
+			{ "chat/reactions_premium_star", storiesComposeGrayIcon },
+		};
+		stripExpandPanel: icon{
+			{ "chat/reactions_round_big", storiesComposeBgRipple },
+			{ "chat/reactions_expand_panel", storiesComposeGrayIcon },
+		};
+		stripExpandDropdown: icon{
+			{ "chat/reactions_round_small", storiesComposeBgRipple },
+			{ "chat/reactions_expand_panel", storiesComposeGrayIcon },
+		};
+	}
+	autocompleteBottomSkip: 10px;
+}
 storiesComposeControls: ComposeControls(defaultComposeControls) {
 	bg: storiesComposeBg;
 	radius: storiesRadius;
@@ -560,104 +676,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
 		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;
-		categoriesBg: storiesComposeBg;
-		categoriesBgOver: storiesComposeBgOver;
-		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;
-			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) {
-				textFg: storiesComposeWhiteText;
-				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;
-			}
-		}
-		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 }};
-			menuSpoiler: icon {{ "menu/spoiler_on", storiesComposeWhiteText }};
-			menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }};
-		}
-		autocompleteBottomSkip: 10px;
-	}
+	tabbed: storiesEmojiPan;
 	tabbedHeightMin: 220px;
 	tabbedHeightMax: 480px;
 	record: RecordBar(defaultRecordBar) {
@@ -747,6 +766,18 @@ storiesWhoViewed: WhoRead(defaultWhoRead) {
 		align: align(left);
 	}
 }
+storiesReactionsPan: EmojiPan(storiesEmojiPan) {
+	margin: margins(reactStripSkip, 0px, reactStripSkip, 0px);
+	padding: margins(reactStripSkip, 0px, reactStripSkip, reactStripSkip);
+	desiredSize: reactStripSize;
+	verticalSizeSub: 0px;
+	overBg: transparent;
+	search: TabbedSearch(storiesEmojiTabbedSearch) {
+		defaultFieldWidth: 88px;
+	}
+	searchMargin: margins(1px, 10px, 2px, 6px);
+}
+
 storiesReactionsWidth: 210px;
 storiesReactionsBottomSkip: 29px;
 storiesReactionsAddedTop: 200px;
diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style
index 6b718137e..56f84a8e6 100644
--- a/Telegram/SourceFiles/ui/chat/chat.style
+++ b/Telegram/SourceFiles/ui/chat/chat.style
@@ -827,19 +827,6 @@ reactionMainAppearShift: 20px;
 reactionCollapseFadeThreshold: 40px;
 reactionFlyUp: 50px;
 
-reactionPremiumLocked: icon{
-	{ "chat/reactions_premium_bg", historyPeerArchiveUserpicBg },
-	{ "chat/reactions_premium_star", historyPeerUserpicFg },
-};
-reactionExpandPanel: icon{
-	{ "chat/reactions_round_big", windowBgRipple },
-	{ "chat/reactions_expand_panel", windowSubTextFg },
-};
-reactionsExpandDropdown: icon{
-	{ "chat/reactions_round_small", windowBgRipple },
-	{ "chat/reactions_expand_panel", windowSubTextFg },
-};
-
 searchInChatMultiSelectItem: MultiSelectItem(defaultMultiSelectItem) {
 	maxWidth: 200px;
 }

From 0a54325db92d386b9e43dff9868e4c2c5e16126f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 9 Jun 2023 18:48:29 +0400
Subject: [PATCH 065/259] Add a separate icon for My Stories.

---
 Telegram/Resources/icons/settings/stories.png    | Bin 0 -> 692 bytes
 Telegram/Resources/icons/settings/stories@2x.png | Bin 0 -> 1311 bytes
 Telegram/Resources/icons/settings/stories@3x.png | Bin 0 -> 1923 bytes
 Telegram/SourceFiles/settings/settings.style     |   1 +
 Telegram/SourceFiles/window/window_main_menu.cpp |   2 +-
 5 files changed, 2 insertions(+), 1 deletion(-)
 create mode 100644 Telegram/Resources/icons/settings/stories.png
 create mode 100644 Telegram/Resources/icons/settings/stories@2x.png
 create mode 100644 Telegram/Resources/icons/settings/stories@3x.png

diff --git a/Telegram/Resources/icons/settings/stories.png b/Telegram/Resources/icons/settings/stories.png
new file mode 100644
index 0000000000000000000000000000000000000000..5cc794ddc38ecf373529878ce1bea92f69f52c2d
GIT binary patch
literal 692
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfl1EO#WBP}
z@M-Y=)f*EfzD1u{ufsOmfMaq{+hn5&BF!RVsRkO|Ml$IO<``+UHScfG&}?m0>7MZ<
z+>1wyc~X$f{ktE(?s;E$@B1Id``>-8*T!D|yytxF-SgfH|7`B|yCyl+>+0*TcJt4_
zEi;zr`B~%l>`!mGh0NrWPwMvTi*>s#eps~gfbZgqufFcO|Neij`lSbtXZk!V+bzn)
zx-_V>f}!Aee?Py)-UyvXl}U5Xrt!BoA5Bu6rYW-e>MReH6Ib@{x%{$a_n#uJTl~3Q
zN0UP8m<vAN%$aun`TmV6t=C`QeOs1eHrv5mg6G2i7`^%ouGS<Isa&(!1r{<KEMcp!
zR@hw2+R7igG^kNB&8gtR>({SKtX!S8MzOxW{Hnz2#D(|Y@87!@aMHm;ZL;ok?+_uk
z2KD&3IE8>G%N1Fg_s6YYp)=KsL-~V?(;AOM4mNV_0Rg_tFF(|L6#e>Z)sK?fZ@>Ni
zZT%_vK#o~`U7gp`AeWC7d-Asbw#c@SdGxzU=D?ee6%!<xebkC~-&LFJ*|R8MM@-(*
zpiHybP74Lj-_F@~`K5{g#|M?R=7SG@)?9m8BH`MkAlvEUq*V8J+SaJty>Y9rW*JR(
zTikgxDMjnp=btqvepJ}FDGBnmH*>~oh;YT~O+T2xu>Ztlmw-ci7F~Q%x4&KFLDS2U
zTNx&HHEYAPr+RISSTp~2huop*-YP;j-<C~2$-=BP{q)WlzGL5u|NlSmhq3X?6E)q+
R-gBT7<LT<>vd$@?2>=mPDY^gv

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/settings/stories@2x.png b/Telegram/Resources/icons/settings/stories@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..eafc4902550e5b57acdfaa4733ff903a722dad2e
GIT binary patch
literal 1311
zcmV+)1>pLLP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGiAh93R9Fe^SXn4_Q53%BxyU@0
zAw`5j9?WGZ9w<p3NaR8CB3>vSl<;DBP+rK85~7g#futmnAu<mkV<DOSU;q9)j<feU
z?z#8sf4TSJw$Iw%xAxj=uf5hfN1@REv>woU;CDTsr>1&cU0nwU2XAlh&(F_?hX*ya
zsYut?*N=^jZES4Z+1Yt}dxI(Xy}Z15d3mX*QDvHukx_AR@#W<uF+%X~=jX@QoSK?i
zQ&Z#X>&q8va-5Noad2?JH)3sVt)-=<va&KiKVPu0v9a;T#|OZ#udns>^(H1JnluGx
z)YR0(nR0%9URqk}>FFu41}JlLa~x%Pd0ArzYH4XXF)=|@BWx=wD$LBxBuwF^0K+zj
zpFp3VpXa7jA7G=Clas`n{r!D^e}B2UqNAht_V%D*YHCU@mx1R_3j4FQwPhfb5E&X8
z#>B*EAdE6IGdbVb*w`qs^lw6NO^%L^U=ZR)g_9C94S^LA5rOm}1`w5(l#~$nl$Di<
zg5*#1^z=aO(9n=PDo*|8<_65u(^FGZQ+Z@b^vui**=Jy2AQPy|l%g_NSy^GKuCDI!
z@lk+5{VFOdLTcIG-X0trjEsyV0Ibp7-MzK7wYj-jUS7_O;<2$Y0?y0J6R4GXY;JBQ
z`*3q}qj3!l4P=)4M{>%|&E@JEk@xra!*l^1>6FYO7YL@L9*>WYL0ei{qCrtnQADFM
z|5;gCw1&mSMZ$G;b){#8I{*T%c5!jRV0LzvCL|;z&_Xc@6I@+g4Ls89?d>g1)Tc#`
zj*di#h^A*fJUnQjn1l)L>+AFJ@i{p;VfIf9gPfe4Bn(0iB(^*)-`(A%g^(cXnQ{mj
zN5*BX=jUgdhI{n>{!Yu8l8%lJrd&eEFzD>;j1ai9<MY?o*HPg360z~XzyNp21sEI|
znh1eV^bt^U3()T|K^7JkI5_EBc6D{dOc_v!NCy|&wzIQi>SqY=@9)7G85v>B{S_fv
zp;62xL~Cehs2F#Ae4G$vlt_3RoTsNJb8~Ysy#WCMxVN-Th`jjtcrg|V0Ge8`z@^is
z7)z0plS5WYOG^_4DHK6LK^GSne49{b5)%_8DB0QB1mD}+D*+Quq5C0q0DZA=?)x*z
zC}fShyF0F~eMfygwYRquaB_0;msFWiTU#4prlh1u<K5lehlhtz#)E@{rIDDet*wRH
zm6a7eJ(=&QxFd)mkWsxQBI!p91OPPYXwYQbc|IS#2XPW&Tt`QTKlOjcsIpvThO5;v
z0_DiSz(5@h|G3W1&Ro9f?CeCNEmtY-c~@5#-p*E6S2g42kdP3hU2=8{3k$gF%G(qd
z7l*1&xJb52zG42XA9)$C@kB573RmI#%a63e!b0w~_4@i6-yVLX$N->zq0SSV$Ulyt
zs;Vj}DG9B#y}doHYhz=BAV)_>xt4~8sU-;u3*%lJ_{L$37RTD!ny)=QJ$-t5x}czd
zFZ`7;+G>2QIy^iS*c2WfuKs#zWfy#*LU`dg@WZF0*49>-sJ=~F4O$OqJ@Bg@_yaIs
VOHNxE+6Vvu002ovPDHLkV1i$hSO5S3

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/settings/stories@3x.png b/Telegram/Resources/icons/settings/stories@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1d6678e0911991ab32a1ebfd118d9799ee0efd5
GIT binary patch
literal 1923
zcmV-}2YmR6P)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS>?MXyIRA>e5T3bj}T@cPxx~P?A
zqCS|ZdBL(C@<9th1cegw_MoL^SrmqdbkhPYA_#mbLA*n1_25HMhG-U1lBJ^EFJ7`x
zRI;Md#C!c8|G{Z-_C9N0{(TPquzhjPS~K6wti9IknOS?u68%qK0euDZ70_2eUjcmu
z^cAqw3Rsy*(TWu-LPJBhZr$qd@9*vHy?XU(2M33rKYyM)dGf)72WC=gM%XoL)*L!?
z=>GltQ&Ura#QsJ`M$D+*9MCQ<F2%*glarHT?))A5@#BY;mB==6baXs=^yr>Fdu(lO
z&0(GzOglTf{QUg!@$tXro1ngZ`v%j6I&5rg-n@BZ5bEyk-nnz9kjK&^si~>hU80V>
zeEIUig$w)l?~jU#+Pr!5+O=y<85a~3WQ1_*)-7jeXH)#<9k;f&zI^$zkwN(R<jIqq
zoSY3CHpo|xxbpSuS0kvw!NHJ_5cwe5a3V<Dy?fWllvl4_?b@|Voq{b}wsduM83E4B
z%p@izssmIb<>TXnlfW7=JUomow{+=JHE;_u5JyD)^5qMQ+}GE)FmLAn2w%Ouy{zu1
zPoKKFx@x7?-QB&VrG@aZ3$#M!;UIi9H#d`35Vf_n%a<?b$ta(Lmh<P&PfSb<3=D*Y
zg((M6E>Toe#Cmn^+&SeKawSAxcXEXl@ujDyvtCtKS1S@<q|B>UtzwTgV*0XW%ND7v
zip;{Mr^hukG^Am)kZFd8hsUvF$KvARwA`X@Zf-cr<k;T5du3Xq6&tb^+An9$oY4w)
zNoi>*dG+ksGcDIlo{hVA?<NDjfB(L6<w|*STrT8n$N&g8GBT3OZk9<eFR$Off0GRv
z85tU=$;!$i#D@<bN+NoCdZK=L`}Qr4B4W&m6DN=gvQj8a5MWVmf}y{^|Jt={+qP|E
z8AR!O_wJDy9UUE_Jjw=JTU!b7$dMy_;e><)VM&I~bNKLKgCHI_Ub_TnAn-IKPfmP%
zJS&A`&$mQk7Eyj~ZjRI=-{A;s-@bi*ex6m&(kNk4Qc~Dm&CY@*=S5ZEIkP3-zJ2>d
z+2jsJM@N(Twzf9D*z4D?iCF~!yHPZ7RMgkk6YSNiS2>)RlE}x|-49$27Fsuffq@(b
z$tin#`{d+g4qIbmBh7NkRs)ZOV}5>qg!S&-JEDaIy9B~nnIpl$!5q%(*ROy2^a%_&
zlVxRP98OHx@TWI!+(-;4x`-AMl1nHQ5so-HISB`81VIQ*gz)h2_wV2L^z_iUC?Vk(
zsxK1bJBHexI8_tq_V8tK4sYMS&F3-9N^C{!dqUy21W|4hr)okI5Lw#s^bLUsVVW<1
zJPQYaqLvC`dV1P&xi$MTl{`fU4<1zE4~ZaO;jf*Rv((3gdh`bQ&?xEK+uO-dMa1XN
zpVzHh$H&JknzN`}G>E`WO-*DU`ks=C5k=YjfModu!nd5h9hxghK@Cun$(@9$Y?DJ~
zPK{)RlURv*c>45d2|NCsMvsxiQ9a2KG0B64AuP!(hEOsyGf7I4C@(K3K|WDZQbK5G
zmhqV-W&;8O2n!Jsr$|CzVfy&-<0Qx-(7lu2X2pA%pkzMe1#`syvv&>=5fNf6;$x>y
zogzC#2~=XZ01y`zjBeVri3QQ1t<UxaaO4CUc>^d?*~*u-tg5O?{<DIPOP4N@_~_`U
z{Jmq+K2%wRh`Uy$w>t+89H5GhaBxkdsBwY^i)@R;3kwTPnyYAnEwr$5ie$lz;ul#3
znRWK;S)~Z*y^%Nw+-oaE5a*AHi6LB=sOav?fVgAFj>nH5<0>#MElpepcNCkpv$NA+
z!;Kp^xV#D{*&C3$x;ll@8nENLgY4{V+%sw1fkuH$4-EvOf(FJe5(w-8@@;l@R#E6*
zBrb@Xfq3}w<45uhVG389;;jF>QB>w^j*E^bze}OoLG*?2;lQIuk1W@CM%BQ+bI~5<
zx0Y1+mXwtA>(?*C){yMUslYcWlAD`L8`3~rym(QQ3tRMOQEnQ6vz;=NHfc41Zv@zP
zE=)nKjdzryEXl|ie6+*nr3Ux#S(c(4#RyZ)Po!GN*^E~A#fulQv9T&%HR#<NLE-0>
zD_1nx2#VcXzkdDm=g)-(jg5`rC5|V+Z&|bYK79DV;$S=An=zKb(loMcl$GdMux1gB
zFoP*nyA2Hum6etFSkaWLH*ek~DSY$e>+5T2Pek>h{fJK^D3p<jLISCoC<49;tf;8C
zbLWmlzcCUuML3AAU_n6vJ`xi8LnBC8{!4%nDz)ec-r9M2dDu~OBTkOh)YMqU6f1M9
z!94@M3C7XFc14|v8(KJNZlyZDz5@CR=qsSFfW89y3M|eQ_y?gq>GBQ5Gob(g002ov
JPDHLkV1kA2lMMg>

literal 0
HcmV?d00001

diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style
index bfdc586d6..853af9e43 100644
--- a/Telegram/SourceFiles/settings/settings.style
+++ b/Telegram/SourceFiles/settings/settings.style
@@ -94,6 +94,7 @@ settingsIconGroup: icon {{ "settings/group", settingsIconFg }};
 settingsIconChannel: icon {{ "settings/channel", settingsIconFg }};
 settingsIconUser: icon {{ "settings/user", settingsIconFg }};
 settingsIconSavedMessages: icon {{ "settings/saved_messages", settingsIconFg }};
+settingsIconStories: icon {{ "settings/stories", settingsIconFg }};
 settingsIconKey: icon {{ "settings/key", settingsIconFg }};
 settingsIconReload: icon {{ "settings/reload", settingsIconFg }};
 settingsIconNight: icon {{ "settings/night", settingsIconFg }};
diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp
index 227a00d42..816f918cf 100644
--- a/Telegram/SourceFiles/window/window_main_menu.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu.cpp
@@ -771,7 +771,7 @@ void MainMenu::setupMenu() {
 					tr::lng_menu_my_stories(),
 					st::mainMenuButton,
 					IconDescriptor{
-						&st::settingsIconSavedMessages,
+						&st::settingsIconStories,
 						kIconLightOrange
 					})));
 		const auto stories = &controller->session().data().stories();

From 43af9fd87e639d65ea35fe6c16cad66decf015b4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 9 Jun 2023 18:48:32 +0400
Subject: [PATCH 066/259] Fix controls fading in raster stories backend.

---
 .../media/view/media_view_overlay_opengl.cpp  |  2 +-
 .../media/view/media_view_overlay_raster.cpp  | 77 +++++++++++--------
 .../media/view/media_view_overlay_raster.h    |  5 +-
 3 files changed, 48 insertions(+), 36 deletions(-)

diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
index a1bdce930..85211c74d 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
@@ -749,8 +749,8 @@ void OverlayWidget::RendererGL::invalidateControls() {
 }
 
 void OverlayWidget::RendererGL::validateControlsFade() {
-	const auto flip = !_owner->topShadowOnTheRight();
 	const auto forStories = (_owner->_stories != nullptr);
+	const auto flip = !forStories && !_owner->topShadowOnTheRight();
 	if (!_controlsFadeImage.image().isNull()
 		&& _shadowTopFlip == flip
 		&& _shadowsForStories == forStories) {
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
index 13109e828..e7cc17d74 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
@@ -76,7 +76,7 @@ void OverlayWidget::RendererSW::paintTransformedVideoFrame(
 		return;
 	}
 	paintTransformedImage(_owner->videoFrame(), rect, rotation);
-	paintControlsFade(rect, geometry.controlsOpacity, geometry.fade);
+	paintControlsFade(rect, geometry);
 }
 
 void OverlayWidget::RendererSW::paintTransformedStaticContent(
@@ -97,56 +97,71 @@ void OverlayWidget::RendererSW::paintTransformedStaticContent(
 	if (!image.isNull()) {
 		paintTransformedImage(image, rect, rotation);
 	}
-	paintControlsFade(rect, geometry.controlsOpacity, geometry.fade);
+	paintControlsFade(rect, geometry);
 }
 
 void OverlayWidget::RendererSW::paintControlsFade(
-		QRect geometry,
-		float64 opacity,
-		float64 fullFade) {
-	if (fullFade > 0.) {
-		_p->setOpacity(fullFade);
-		_p->fillRect(geometry, Qt::black);
-		opacity *= 1. - fullFade;
+	QRect content,
+	const ContentGeometry &geometry) {
+	auto opacity = geometry.controlsOpacity;
+	if (geometry.fade > 0.) {
+		_p->setOpacity(geometry.fade);
+		_p->fillRect(content, Qt::black);
+		opacity *= 1. - geometry.fade;
 	}
 
 	_p->setOpacity(opacity);
-	_p->setClipRect(geometry);
+	_p->setClipRect(content);
 	const auto width = _owner->width();
-	const auto flip = !_owner->topShadowOnTheRight();
 	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()
-				|| _topShadowColor != st::windowShadowFg->c) {
-				_topShadowColor = st::windowShadowFg->c;
-				_topShadowCache = top.instance(
-					_topShadowColor).mirrored(true, false);
+	if (!stories || geometry.topShadowShown) {
+		const auto flip = !stories && !_owner->topShadowOnTheRight();
+		const auto &top = stories
+			? st::storiesShadowTop
+			: st::mediaviewShadowTop;
+		const auto topShadow = stories
+			? QRect(
+				content.topLeft(),
+				QSize(content.width(), top.height()))
+			: QRect(
+				QPoint(flip ? 0 : (width - top.width()), 0),
+				top.size());
+		if (topShadow.intersected(content).intersects(_clipOuter)) {
+			if (stories) {
+				top.fill(*_p, topShadow);
+			} else if (flip) {
+				if (_topShadowCache.isNull()
+					|| _topShadowColor != st::windowShadowFg->c) {
+					_topShadowColor = st::windowShadowFg->c;
+					_topShadowCache = top.instance(
+						_topShadowColor).mirrored(true, false);
+				}
+				_p->drawImage(0, 0, _topShadowCache);
+			} else {
+				top.paint(*_p, topShadow.topLeft(), width);
 			}
-			_p->drawImage(0, 0, _topShadowCache);
-		} else {
-			top.paint(*_p, topShadow.topLeft(), width);
 		}
 	}
 	const auto &bottom = stories
 		? st::storiesShadowBottom
 		: st::mediaviewShadowBottom;
+	const auto bottomStart = _owner->height() - geometry.bottomShadowSkip;
 	const auto bottomShadow = QRect(
-		QPoint(0, _owner->height() - bottom.height()),
+		QPoint(0, bottomStart - bottom.height()),
 		QSize(width, bottom.height()));
-	if (bottomShadow.intersected(geometry).intersects(_clipOuter)) {
+	if (bottomShadow.intersected(content).intersects(_clipOuter)) {
 		bottom.fill(*_p, bottomShadow);
 	}
 	_p->setClipping(false);
 	_p->setOpacity(1.);
+	if (bottomStart < content.y() + content.height()) {
+		_p->fillRect(
+			content.x(),
+			bottomStart,
+			content.width(),
+			content.y() + content.height() - bottomStart,
+			QColor(0, 0, 0, 88));
+	}
 }
 
 void OverlayWidget::RendererSW::paintTransformedImage(
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h
index 5c6a7880a..3d8abf4c4 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h
@@ -33,10 +33,7 @@ private:
 		const QImage &image,
 		QRect rect,
 		int rotation);
-	void paintControlsFade(
-		QRect geometry,
-		float64 opacity,
-		float64 fullFade);
+	void paintControlsFade(QRect content, const ContentGeometry &geometry);
 	void paintRadialLoading(
 		QRect inner,
 		bool radial,

From 10d64d6bdfebee4682ca1117e1319c1f68c6820c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 9 Jun 2023 18:47:03 +0400
Subject: [PATCH 067/259] Send story reactions as single-emoji messages.

---
 .../SourceFiles/history/history_widget.cpp    |  2 +-
 .../view/history_view_replies_section.cpp     |  5 ++-
 .../view/history_view_scheduled_section.cpp   |  4 +--
 .../stories/media_stories_controller.cpp      |  5 +++
 .../media/stories/media_stories_reactions.cpp |  3 +-
 .../media/stories/media_stories_reactions.h   |  8 +++++
 .../media/stories/media_stories_reply.cpp     | 31 ++++++++++++++++++-
 .../media/stories/media_stories_reply.h       |  8 +++++
 8 files changed, 57 insertions(+), 9 deletions(-)

diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 106dce71e..61c7c7237 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -3874,7 +3874,7 @@ void HistoryWidget::send(Api::SendOptions options) {
 			? _previewData->id
 			: WebPageId(0));
 
-	auto message = ApiWrap::MessageToSend(prepareSendAction(options));
+	auto message = Api::MessageToSend(prepareSendAction(options));
 	message.textWithTags = _field->getTextWithAppliedMarkdown();
 	message.webPageId = webPageId;
 
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index fc94a905c..4813dcc5b 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -1166,7 +1166,7 @@ void RepliesWidget::send(Api::SendOptions options) {
 
 	const auto webPageId = _composeControls->webPageId();
 
-	auto message = ApiWrap::MessageToSend(prepareSendAction(options));
+	auto message = Api::MessageToSend(prepareSendAction(options));
 	message.textWithTags = _composeControls->getTextWithAppliedMarkdown();
 	message.webPageId = webPageId;
 
@@ -2507,8 +2507,7 @@ void RepliesWidget::listSendBotCommand(
 		_history->peer,
 		command,
 		context);
-	auto message = ApiWrap::MessageToSend(
-		prepareSendAction({}));
+	auto message = Api::MessageToSend(prepareSendAction({}));
 	message.textWithTags = { text };
 	session().api().sendMessage(std::move(message));
 	finishSending();
diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
index 998909301..c8966ad9c 100644
--- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
@@ -587,7 +587,7 @@ void ScheduledWidget::send() {
 void ScheduledWidget::send(Api::SendOptions options) {
 	const auto webPageId = _composeControls->webPageId();
 
-	auto message = ApiWrap::MessageToSend(prepareSendAction(options));
+	auto message = Api::MessageToSend(prepareSendAction(options));
 	message.textWithTags = _composeControls->getTextWithAppliedMarkdown();
 	message.webPageId = webPageId;
 
@@ -1235,7 +1235,7 @@ void ScheduledWidget::listSendBotCommand(
 			_history->peer,
 			command,
 			context);
-		auto message = ApiWrap::MessageToSend(prepareSendAction(options));
+		auto message = Api::MessageToSend(prepareSendAction(options));
 		message.textWithTags = { text };
 		session().api().sendMessage(std::move(message));
 	};
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 6ea4a3f82..9131724f2 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -167,6 +167,11 @@ Controller::Controller(not_null<Delegate*> delegate)
 		}
 	}, _lifetime);
 
+	_reactions->chosen(
+	) | rpl::start_with_next([=](const Data::ReactionId &id) {
+		_replyArea->sendReaction(id);
+	}, _lifetime);
+
 	_delegate->storiesLayerShown(
 	) | rpl::start_with_next([=](bool shown) {
 		_layerShown = shown;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
index 49434bd53..d77e8274e 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -143,9 +143,8 @@ void Reactions::create() {
 	_selector->chosen(
 	) | rpl::start_with_next([=](
 			HistoryView::Reactions::ChosenReaction reaction) {
+		_chosen.fire_copy(reaction.id);
 		hide();
-		//reaction.context = itemId;
-		//chosen(std::move(reaction));
 	}, _selector->lifetime());
 
 	_selector->premiumPromoChosen() | rpl::start_with_next([=] {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
index e9a89a09c..1be44b34a 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
@@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "ui/effects/animations.h"
 
+namespace Data {
+struct ReactionId;
+} // namespace Data
+
 namespace HistoryView::Reactions {
 class Selector;
 } // namespace HistoryView::Reactions
@@ -29,6 +33,9 @@ public:
 	[[nodiscard]] rpl::producer<bool> expandedValue() const {
 		return _expanded.value();
 	}
+	[[nodiscard]] rpl::producer<Data::ReactionId> chosen() const {
+		return _chosen.events();
+	}
 
 	void show();
 	void hide();
@@ -47,6 +54,7 @@ private:
 	std::unique_ptr<Ui::RpWidget> _parent;
 	std::unique_ptr<HistoryView::Reactions::Selector> _selector;
 	std::vector<std::unique_ptr<Hiding>> _hiding;
+	rpl::event_stream<Data::ReactionId> _chosen;
 	Ui::Animations::Simple _showing;
 	rpl::variable<float64> _shownValue;
 	rpl::variable<bool> _expanded;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index db914d816..1ccce96a6 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -17,6 +17,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "chat_helpers/tabbed_selector.h"
 #include "core/file_utilities.h"
 #include "core/mime_type.h"
+#include "data/stickers/data_custom_emoji.h"
+#include "data/data_document.h"
+#include "data/data_message_reaction_id.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
 #include "history/view/controls/compose_controls_common.h"
@@ -101,13 +104,39 @@ void ReplyArea::initGeometry() {
 	}, _lifetime);
 }
 
+void ReplyArea::sendReaction(const Data::ReactionId &id) {
+	Expects(_data.user != nullptr);
+
+	auto message = Api::MessageToSend(prepareSendAction({}));
+	if (const auto emoji = id.emoji(); !emoji.isEmpty()) {
+		message.textWithTags = { emoji };
+	} else if (const auto customId = id.custom()) {
+		const auto document = _data.user->owner().document(customId);
+		if (const auto sticker = document->sticker()) {
+			const auto text = sticker->alt;
+			const auto id = Data::SerializeCustomEmojiId(customId);
+			message.textWithTags = {
+				text,
+				{ { 0, text.size(), Ui::InputField::CustomEmojiLink(id) } }
+			};
+		}
+	}
+	if (!message.textWithTags.empty()) {
+		send(std::move(message), {});
+	}
+}
+
 void ReplyArea::send(Api::SendOptions options) {
 	const auto webPageId = _controls->webPageId();
 
-	auto message = ApiWrap::MessageToSend(prepareSendAction(options));
+	auto message = Api::MessageToSend(prepareSendAction(options));
 	message.textWithTags = _controls->getTextWithAppliedMarkdown();
 	message.webPageId = webPageId;
 
+	send(std::move(message), options);
+}
+
+void ReplyArea::send(Api::MessageToSend message, Api::SendOptions options) {
 	const auto error = GetErrorTextForSending(
 		_data.user,
 		{
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h
index 30d15cb28..4dcd84013 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h
@@ -15,8 +15,13 @@ enum class SendMediaType;
 namespace Api {
 struct SendAction;
 struct SendOptions;
+struct MessageToSend;
 } // namespace Api
 
+namespace Data {
+struct ReactionId;
+} // namespace Data
+
 namespace HistoryView {
 class ComposeControls;
 } // namespace HistoryView
@@ -56,6 +61,7 @@ public:
 	~ReplyArea();
 
 	void show(ReplyAreaData data);
+	void sendReaction(const Data::ReactionId &id);
 
 	[[nodiscard]] rpl::producer<bool> focusedValue() const;
 	[[nodiscard]] rpl::producer<bool> activeValue() const;
@@ -67,6 +73,8 @@ private:
 	[[nodiscard]] Main::Session &session() const;
 	[[nodiscard]] not_null<History*> history() const;
 
+	void send(Api::MessageToSend message, Api::SendOptions options);
+
 	void uploadFile(const QByteArray &fileContent, SendMediaType type);
 	bool confirmSendingFiles(
 		QImage &&image,

From af5228771cd96ca4168cab2ce0ce04701ea86895 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 9 Jun 2023 18:49:09 +0400
Subject: [PATCH 068/259] Send views for expired pinned stories.

---
 Telegram/SourceFiles/data/data_stories.cpp    | 79 +++++++++++++++++--
 Telegram/SourceFiles/data/data_stories.h      |  6 ++
 .../stories/media_stories_controller.cpp      |  5 ++
 .../media/stories/media_stories_controller.h  |  1 +
 4 files changed, 83 insertions(+), 8 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index c1b08a657..5a51f0360 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -33,6 +33,7 @@ constexpr auto kMaxResolveTogether = 100;
 constexpr auto kIgnorePreloadAroundIfLoaded = 15;
 constexpr auto kPreloadAroundCount = 30;
 constexpr auto kMarkAsReadDelay = 3 * crl::time(1000);
+constexpr auto kIncrementViewsDelay = 5 * crl::time(1000);
 constexpr auto kArchiveFirstPerPage = 30;
 constexpr auto kArchivePerPage = 100;
 constexpr auto kSavedFirstPerPage = 30;
@@ -296,7 +297,8 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 Stories::Stories(not_null<Session*> owner)
 : _owner(owner)
 , _expireTimer([=] { processExpired(); })
-, _markReadTimer([=] { sendMarkAsReadRequests(); }) {
+, _markReadTimer([=] { sendMarkAsReadRequests(); })
+, _incrementViewsTimer([=] { sendIncrementViewsRequests(); }) {
 }
 
 Stories::~Stories() {
@@ -1082,6 +1084,20 @@ void Stories::loadAround(FullStoryId id, StoriesContext context) {
 }
 
 void Stories::markAsRead(FullStoryId id, bool viewed) {
+	if (id.peer == _owner->session().userPeerId()) {
+		return;
+	}
+	const auto maybeStory = lookup(id);
+	if (!maybeStory) {
+		return;
+	}
+	const auto story = *maybeStory;
+	if (story->expired() && story->pinned()) {
+		_incrementViewsPending[id.peer].emplace(id.story);
+		if (!_incrementViewsTimer.isActive()) {
+			_incrementViewsTimer.callOnce(kIncrementViewsDelay);
+		}
+	}
 	const auto i = _all.find(id.peer);
 	Assert(i != end(_all));
 	if (i->second.readTill >= id.story) {
@@ -1176,12 +1192,7 @@ void Stories::sendMarkAsReadRequest(
 			&& _markReadPending.contains(peerId)) {
 			sendMarkAsReadRequests();
 		}
-		if (_markReadRequests.empty()) {
-			if (Core::Quitting()) {
-				LOG(("Stories doesn't prevent quit any more."));
-			}
-			Core::App().quitPreventFinished();
-		}
+		checkQuitPreventFinished();
 	};
 
 	const auto api = &_owner->session().api();
@@ -1191,6 +1202,15 @@ void Stories::sendMarkAsReadRequest(
 	)).done(finish).fail(finish).send();
 }
 
+void Stories::checkQuitPreventFinished() {
+	if (_markReadRequests.empty() && _incrementViewsRequests.empty()) {
+		if (Core::Quitting()) {
+			LOG(("Stories doesn't prevent quit any more."));
+		}
+		Core::App().quitPreventFinished();
+	}
+}
+
 void Stories::sendMarkAsReadRequests() {
 	_markReadTimer.cancel();
 	for (auto i = begin(_markReadPending); i != end(_markReadPending);) {
@@ -1207,6 +1227,46 @@ void Stories::sendMarkAsReadRequests() {
 	}
 }
 
+void Stories::sendIncrementViewsRequests() {
+	if (_incrementViewsPending.empty()) {
+		return;
+	}
+	auto ids = QVector<MTPint>();
+	auto peer = PeerId();
+	struct Prepared {
+		PeerId peer = 0;
+		QVector<MTPint> ids;
+	};
+	auto prepared = std::vector<Prepared>();
+	for (const auto &[peer, ids] : _incrementViewsPending) {
+		if (_incrementViewsRequests.contains(peer)) {
+			continue;
+		}
+		prepared.push_back({ .peer = peer });
+		for (const auto &id : ids) {
+			prepared.back().ids.push_back(MTP_int(id));
+		}
+	}
+
+	const auto api = &_owner->session().api();
+	for (auto &[peer, ids] : prepared) {
+		_incrementViewsRequests.emplace(peer);
+		const auto finish = [=] {
+			_incrementViewsRequests.remove(peer);
+			if (!_incrementViewsTimer.isActive()
+				&& _incrementViewsPending.contains(peer)) {
+				sendIncrementViewsRequests();
+			}
+			checkQuitPreventFinished();
+		};
+		api->request(MTPstories_IncrementStoryViews(
+			_owner->peer(peer)->asUser()->inputUser,
+			MTP_vector<MTPint>(std::move(ids))
+		)).done(finish).fail(finish).send();
+		_incrementViewsPending.remove(peer);
+	}
+}
+
 void Stories::loadViewsSlice(
 		StoryId id,
 		std::optional<StoryView> offset,
@@ -1392,7 +1452,10 @@ bool Stories::isQuitPrevent() {
 	if (!_markReadPending.empty()) {
 		sendMarkAsReadRequests();
 	}
-	if (_markReadRequests.empty()) {
+	if (!_incrementViewsPending.empty()) {
+		sendIncrementViewsRequests();
+	}
+	if (_markReadRequests.empty() && _incrementViewsRequests.empty()) {
 		return false;
 	}
 	LOG(("Stories prevents quit, marking as read..."));
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index a94b6ef12..9cde3d245 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -281,6 +281,8 @@ private:
 
 	void sendMarkAsReadRequests();
 	void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
+	void sendIncrementViewsRequests();
+	void checkQuitPreventFinished();
 
 	void requestUserStories(not_null<UserData*> user);
 	void addToArchive(not_null<Story*> story);
@@ -337,6 +339,10 @@ private:
 	base::flat_set<PeerId> _markReadRequests;
 	base::flat_set<not_null<UserData*>> _requestingUserStories;
 
+	base::flat_map<PeerId, base::flat_set<StoryId>> _incrementViewsPending;
+	base::Timer _incrementViewsTimer;
+	base::flat_set<PeerId> _incrementViewsRequests;
+
 	StoryId _viewsStoryId = 0;
 	std::optional<StoryView> _viewsOffset;
 	Fn<void(std::vector<StoryView>)> _viewsDone;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 9131724f2..8889204d9 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -490,6 +490,7 @@ void Controller::show(
 		return;
 	}
 	_shown = storyId;
+	_viewed = false;
 	_captionText = story->caption();
 	_captionFullView = nullptr;
 	invalidate_weak_ptrs(&_viewsLoadGuard);
@@ -633,6 +634,10 @@ void Controller::maybeMarkAsRead(const Player::TrackState &state) {
 void Controller::markAsRead() {
 	Expects(_source.has_value());
 
+	if (_viewed) {
+		return;
+	}
+	_viewed = true;
 	_source->user->owner().stories().markAsRead(_shown, _started);
 }
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index f59cb0f36..52d780093 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -195,6 +195,7 @@ private:
 	FullStoryId _waitingForId;
 	int _index = 0;
 	bool _started = false;
+	bool _viewed = false;
 
 	ViewsSlice _viewsSlice;
 	rpl::event_stream<> _moreViewsLoaded;

From bafb4f91b46bce0466d53502e7554a799fe563cc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 9 Jun 2023 19:31:51 +0400
Subject: [PATCH 069/259] Update API scheme on layer 160.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 Telegram/SourceFiles/apiwrap.cpp              |  3 +-
 .../data/notify/data_notify_settings.cpp      | 29 ++++++---
 .../data/notify/data_notify_settings.h        |  9 ++-
 .../data/notify/data_peer_notify_settings.cpp | 61 +++++++++++++------
 .../data/notify/data_peer_notify_settings.h   |  3 +-
 .../dialogs/ui/dialogs_stories_content.cpp    |  5 +-
 Telegram/SourceFiles/mtproto/scheme/api.tl    |  6 +-
 8 files changed, 82 insertions(+), 35 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 154400cb6..c29a21018 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3790,6 +3790,7 @@ 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_my_name" = "My Story";
 "lng_stories_hide_to_contacts" = "Archive";
 "lng_stories_show_in_chats" = "Unarchive";
 "lng_stories_row_count#one" = "{count} Story";
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 7840bdcfa..922407988 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -1804,7 +1804,8 @@ void ApiWrap::requestNotifySettings(const MTPInputNotifyPeer &peer) {
 				MTPint(),
 				MTPNotificationSound(),
 				MTPNotificationSound(),
-				MTPNotificationSound()));
+				MTPNotificationSound(),
+				MTPBool()));
 		_notifySettingRequests.erase(key);
 	}).send();
 	_notifySettingRequests.emplace(key, requestId);
diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp
index cee0afe8a..3beba423d 100644
--- a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp
+++ b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp
@@ -174,8 +174,13 @@ void NotifySettings::update(
 		not_null<Data::Thread*> thread,
 		Data::MuteValue muteForSeconds,
 		std::optional<bool> silentPosts,
-		std::optional<NotifySound> sound) {
-	if (thread->notify().change(muteForSeconds, silentPosts, sound)) {
+		std::optional<NotifySound> sound,
+		std::optional<bool> storiesMuted) {
+	if (thread->notify().change(
+			muteForSeconds,
+			silentPosts,
+			sound,
+			storiesMuted)) {
 		updateLocal(thread);
 		thread->session().api().updateNotifySettingsDelayed(thread);
 	}
@@ -189,7 +194,8 @@ void NotifySettings::resetToDefault(not_null<Data::Thread*> thread) {
 		MTPint(),
 		MTPNotificationSound(),
 		MTPNotificationSound(),
-		MTPNotificationSound());
+		MTPNotificationSound(),
+		MTPBool());
 	if (thread->notify().change(empty)) {
 		updateLocal(thread);
 		thread->session().api().updateNotifySettingsDelayed(thread);
@@ -200,8 +206,13 @@ void NotifySettings::update(
 		not_null<PeerData*> peer,
 		Data::MuteValue muteForSeconds,
 		std::optional<bool> silentPosts,
-		std::optional<NotifySound> sound) {
-	if (peer->notify().change(muteForSeconds, silentPosts, sound)) {
+		std::optional<NotifySound> sound,
+		std::optional<bool> storiesMuted) {
+	if (peer->notify().change(
+			muteForSeconds,
+			silentPosts,
+			sound,
+			storiesMuted)) {
 		updateLocal(peer);
 		peer->session().api().updateNotifySettingsDelayed(peer);
 	}
@@ -215,7 +226,8 @@ void NotifySettings::resetToDefault(not_null<PeerData*> peer) {
 		MTPint(),
 		MTPNotificationSound(),
 		MTPNotificationSound(),
-		MTPNotificationSound());
+		MTPNotificationSound(),
+		MTPBool());
 	if (peer->notify().change(empty)) {
 		updateLocal(peer);
 		peer->session().api().updateNotifySettingsDelayed(peer);
@@ -262,9 +274,10 @@ void NotifySettings::defaultUpdate(
 		DefaultNotify type,
 		Data::MuteValue muteForSeconds,
 		std::optional<bool> silentPosts,
-		std::optional<NotifySound> sound) {
+		std::optional<NotifySound> sound,
+		std::optional<bool> storiesMuted) {
 	auto &settings = defaultValue(type).settings;
-	if (settings.change(muteForSeconds, silentPosts, sound)) {
+	if (settings.change(muteForSeconds, silentPosts, sound, storiesMuted)) {
 		updateLocal(type);
 		_owner->session().api().updateNotifySettingsDelayed(type);
 	}
diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.h b/Telegram/SourceFiles/data/notify/data_notify_settings.h
index be41ce8dc..6e51b87ac 100644
--- a/Telegram/SourceFiles/data/notify/data_notify_settings.h
+++ b/Telegram/SourceFiles/data/notify/data_notify_settings.h
@@ -57,13 +57,15 @@ public:
 		not_null<Data::Thread*> thread,
 		Data::MuteValue muteForSeconds,
 		std::optional<bool> silentPosts = std::nullopt,
-		std::optional<NotifySound> sound = std::nullopt);
+		std::optional<NotifySound> sound = std::nullopt,
+		std::optional<bool> storiesMuted = std::nullopt);
 	void resetToDefault(not_null<Data::Thread*> thread);
 	void update(
 		not_null<PeerData*> peer,
 		Data::MuteValue muteForSeconds,
 		std::optional<bool> silentPosts = std::nullopt,
-		std::optional<NotifySound> sound = std::nullopt);
+		std::optional<NotifySound> sound = std::nullopt,
+		std::optional<bool> storiesMuted = std::nullopt);
 	void resetToDefault(not_null<PeerData*> peer);
 
 	void forumParentMuteUpdated(not_null<Data::Forum*> forum);
@@ -84,7 +86,8 @@ public:
 		DefaultNotify type,
 		Data::MuteValue muteForSeconds,
 		std::optional<bool> silentPosts = std::nullopt,
-		std::optional<NotifySound> sound = std::nullopt);
+		std::optional<NotifySound> sound = std::nullopt,
+		std::optional<bool> storiesMuted = std::nullopt);
 
 	[[nodiscard]] bool isMuted(not_null<const Data::Thread*> thread) const;
 	[[nodiscard]] NotifySound sound(
diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp
index 6d5735624..da03d14f2 100644
--- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp
+++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp
@@ -18,7 +18,8 @@ namespace {
 		MTPBool(),
 		MTPBool(),
 		MTPint(),
-		MTPNotificationSound());
+		MTPNotificationSound(),
+		MTPBool());
 }
 
 [[nodiscard]] NotifySound ParseSound(const MTPNotificationSound &sound) {
@@ -73,7 +74,8 @@ public:
 	bool change(
 		MuteValue muteForSeconds,
 		std::optional<bool> silentPosts,
-		std::optional<NotifySound> sound);
+		std::optional<NotifySound> sound,
+		std::optional<bool> storiesMuted);
 
 	std::optional<TimeId> muteUntil() const;
 	std::optional<bool> silentPosts() const;
@@ -85,12 +87,14 @@ private:
 		std::optional<int> mute,
 		std::optional<NotifySound> sound,
 		std::optional<bool> showPreviews,
-		std::optional<bool> silentPosts);
+		std::optional<bool> silentPosts,
+		std::optional<bool> storiesMuted);
 
 	std::optional<TimeId> _mute;
 	std::optional<NotifySound> _sound;
 	std::optional<bool> _silent;
 	std::optional<bool> _showPreviews;
+	std::optional<bool> _storiesMuted;
 
 };
 
@@ -104,19 +108,24 @@ bool NotifyPeerSettingsValue::change(const MTPDpeerNotifySettings &data) {
 	const auto sound = data.vother_sound();
 	const auto showPreviews = data.vshow_previews();
 	const auto silent = data.vsilent();
+	const auto storiesMuted = data.vstories_muted();
 	return change(
 		mute ? std::make_optional(mute->v) : std::nullopt,
 		sound ? std::make_optional(ParseSound(*sound)) : std::nullopt,
 		(showPreviews
 			? std::make_optional(mtpIsTrue(*showPreviews))
 			: std::nullopt),
-		silent ? std::make_optional(mtpIsTrue(*silent)) : std::nullopt);
+		silent ? std::make_optional(mtpIsTrue(*silent)) : std::nullopt,
+		(storiesMuted
+			? std::make_optional(mtpIsTrue(*storiesMuted))
+			: std::nullopt));
 }
 
 bool NotifyPeerSettingsValue::change(
 		MuteValue muteForSeconds,
 		std::optional<bool> silentPosts,
-		std::optional<NotifySound> sound) {
+		std::optional<NotifySound> sound,
+		std::optional<bool> storiesMuted) {
 	const auto newMute = muteForSeconds
 		? base::make_optional(muteForSeconds.until())
 		: _mute;
@@ -126,28 +135,35 @@ bool NotifyPeerSettingsValue::change(
 	const auto newSound = sound
 		? base::make_optional(*sound)
 		: _sound;
+	const auto newStoriesMuted = storiesMuted
+		? base::make_optional(*storiesMuted)
+		: _storiesMuted;
 	return change(
 		newMute,
 		newSound,
 		_showPreviews,
-		newSilentPosts);
+		newSilentPosts,
+		newStoriesMuted);
 }
 
 bool NotifyPeerSettingsValue::change(
 		std::optional<int> mute,
 		std::optional<NotifySound> sound,
 		std::optional<bool> showPreviews,
-		std::optional<bool> silentPosts) {
+		std::optional<bool> silentPosts,
+		std::optional<bool> storiesMuted) {
 	if (_mute == mute
 		&& _sound == sound
 		&& _showPreviews == showPreviews
-		&& _silent == silentPosts) {
+		&& _silent == silentPosts
+		&& _storiesMuted == storiesMuted) {
 		return false;
 	}
 	_mute = mute;
 	_sound = sound;
 	_showPreviews = showPreviews;
 	_silent = silentPosts;
+	_storiesMuted = storiesMuted;
 	return true;
 }
 
@@ -172,11 +188,13 @@ MTPinputPeerNotifySettings NotifyPeerSettingsValue::serialize() const {
 		MTP_flags(flag(_mute, Flag::f_mute_until)
 			| flag(_sound, Flag::f_sound)
 			| flag(_silent, Flag::f_silent)
-			| flag(_showPreviews, Flag::f_show_previews)),
-		MTP_bool(_showPreviews ? *_showPreviews : true),
-		MTP_bool(_silent ? *_silent : false),
-		MTP_int(_mute ? *_mute : false),
-		SerializeSound(_sound));
+			| flag(_showPreviews, Flag::f_show_previews)
+			| flag(_storiesMuted, Flag::f_stories_muted)),
+		MTP_bool(_showPreviews.value_or(true)),
+		MTP_bool(_silent.value_or(false)),
+		MTP_int(_mute.value_or(false)),
+		SerializeSound(_sound),
+		MTP_bool(_storiesMuted.value_or(false)));
 }
 
 PeerNotifySettings::PeerNotifySettings() = default;
@@ -203,16 +221,22 @@ bool PeerNotifySettings::change(const MTPPeerNotifySettings &settings) {
 bool PeerNotifySettings::change(
 		MuteValue muteForSeconds,
 		std::optional<bool> silentPosts,
-		std::optional<NotifySound> sound) {
-	if (!muteForSeconds && !silentPosts && !sound) {
+		std::optional<NotifySound> sound,
+		std::optional<bool> storiesMuted) {
+	if (!muteForSeconds && !silentPosts && !sound && !storiesMuted) {
 		return false;
 	} else if (_value) {
-		return _value->change(muteForSeconds, silentPosts, sound);
+		return _value->change(
+			muteForSeconds,
+			silentPosts,
+			sound,
+			storiesMuted);
 	}
 	using Flag = MTPDpeerNotifySettings::Flag;
 	const auto flags = (muteForSeconds ? Flag::f_mute_until : Flag(0))
 		| (silentPosts ? Flag::f_silent : Flag(0))
-		| (sound ? Flag::f_other_sound : Flag(0));
+		| (sound ? Flag::f_other_sound : Flag(0))
+		| (storiesMuted ? Flag::f_stories_muted : Flag(0));
 	return change(MTP_peerNotifySettings(
 		MTP_flags(flags),
 		MTPBool(),
@@ -220,7 +244,8 @@ bool PeerNotifySettings::change(
 		MTP_int(muteForSeconds.until()),
 		MTPNotificationSound(),
 		MTPNotificationSound(),
-		SerializeSound(sound)));
+		SerializeSound(sound),
+		storiesMuted ? MTP_bool(*storiesMuted) : MTPBool()));
 }
 
 std::optional<TimeId> PeerNotifySettings::muteUntil() const {
diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h
index 3973d7820..76a8ecfd9 100644
--- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h
+++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h
@@ -44,7 +44,8 @@ public:
 	bool change(
 		MuteValue muteForSeconds,
 		std::optional<bool> silentPosts,
-		std::optional<NotifySound> sound);
+		std::optional<NotifySound> sound,
+		std::optional<bool> storiesMuted);
 
 	bool settingsUnknown() const;
 	std::optional<TimeId> muteUntil() const;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index c47282c26..332dec14a 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_user.h"
 #include "dialogs/ui/dialogs_stories_list.h"
 #include "main/main_session.h"
+#include "lang/lang_keys.h"
 #include "ui/painter.h"
 
 namespace Dialogs::Stories {
@@ -144,7 +145,9 @@ Content State::next() {
 		}
 		result.users.push_back({
 			.id = uint64(user->id.value),
-			.name = user->shortName(),
+			.name = (user->isSelf()
+				? tr::lng_stories_my_name(tr::now)
+				: user->shortName()),
 			.userpic = std::move(userpic),
 			.unread = info.unread,
 			.hidden = info.hidden,
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 577a291a1..a916847ce 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -129,7 +129,7 @@ messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true tes
 messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
 messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
 messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
-messageMediaStory#c79aee11 user_id:long id:int = MessageMedia;
+messageMediaStory#cbb20d88 flags:# user_id:long id:int story:flags.0?StoryItem = MessageMedia;
 
 messageActionEmpty#b6aef7b0 = MessageAction;
 messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;
@@ -201,9 +201,9 @@ inputNotifyChats#4a95e84e = InputNotifyPeer;
 inputNotifyBroadcasts#b1db7c7e = InputNotifyPeer;
 inputNotifyForumTopic#5c467992 peer:InputPeer top_msg_id:int = InputNotifyPeer;
 
-inputPeerNotifySettings#df1f002b flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound = InputPeerNotifySettings;
+inputPeerNotifySettings#e1e51e85 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound stories_muted:flags.6?Bool = InputPeerNotifySettings;
 
-peerNotifySettings#a83b0426 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound = PeerNotifySettings;
+peerNotifySettings#6cdc6e52 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound stories_muted:flags.6?Bool = PeerNotifySettings;
 
 peerSettings#a518110d flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int = PeerSettings;
 

From 0401364d71422e30df342648403e493dd305cc7c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 9 Jun 2023 21:25:26 +0400
Subject: [PATCH 070/259] Fix build with Xcode.

---
 Telegram/SourceFiles/data/data_stories.cpp      |  6 +++---
 Telegram/SourceFiles/data/data_stories.h        | 17 +++++++++++++++++
 .../media/stories/media_stories_reply.cpp       |  2 +-
 3 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 5a51f0360..c0f70b925 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -515,7 +515,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 			i->second = std::move(result);
 		}
 	} else {
-		_all.emplace(peerId, std::move(result)).first;
+		_all.emplace(peerId, std::move(result));
 	}
 	const auto add = [&](StorySourcesList list) {
 		auto &sources = _sources[static_cast<int>(list)];
@@ -1251,7 +1251,7 @@ void Stories::sendIncrementViewsRequests() {
 	const auto api = &_owner->session().api();
 	for (auto &[peer, ids] : prepared) {
 		_incrementViewsRequests.emplace(peer);
-		const auto finish = [=] {
+		const auto finish = [=, peer = peer] {
 			_incrementViewsRequests.remove(peer);
 			if (!_incrementViewsTimer.isActive()
 				&& _incrementViewsPending.contains(peer)) {
@@ -1462,4 +1462,4 @@ bool Stories::isQuitPrevent() {
 	return true;
 }
 
-} // namespace Data
\ No newline at end of file
+} // namespace Data
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 9cde3d245..b7de66759 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include "base/qt/qt_compare.h"
 #include "base/expected.h"
 #include "base/timer.h"
 #include "base/weak_ptr.h"
@@ -155,15 +156,31 @@ enum class StorySourcesList : uchar {
 };
 
 struct StoriesContextSingle {
+	friend inline auto operator<=>(
+		StoriesContextSingle,
+		StoriesContextSingle) = default;
+	friend inline bool operator==(StoriesContextSingle, StoriesContextSingle) = default;
 };
 
 struct StoriesContextPeer {
+	friend inline auto operator<=>(
+		StoriesContextPeer,
+		StoriesContextPeer) = default;
+	friend inline bool operator==(StoriesContextPeer, StoriesContextPeer) = default;
 };
 
 struct StoriesContextSaved {
+	friend inline auto operator<=>(
+		StoriesContextSaved,
+		StoriesContextSaved) = default;
+	friend inline bool operator==(StoriesContextSaved, StoriesContextSaved) = default;
 };
 
 struct StoriesContextArchive {
+	friend inline auto operator<=>(
+		StoriesContextArchive,
+		StoriesContextArchive) = default;
+	friend inline bool operator==(StoriesContextArchive, StoriesContextArchive) = default;
 };
 
 struct StoriesContext {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 1ccce96a6..becd18b49 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -117,7 +117,7 @@ void ReplyArea::sendReaction(const Data::ReactionId &id) {
 			const auto id = Data::SerializeCustomEmojiId(customId);
 			message.textWithTags = {
 				text,
-				{ { 0, text.size(), Ui::InputField::CustomEmojiLink(id) } }
+				{ { 0, int(text.size()), Ui::InputField::CustomEmojiLink(id) } }
 			};
 		}
 	}

From f828caf0d9858ce9c18bd212de77ab7d3bcb931d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 12 Jun 2023 18:04:10 +0400
Subject: [PATCH 071/259] Simplify saved stories list.

---
 Telegram/SourceFiles/data/data_stories.cpp    | 43 ----------
 .../SourceFiles/data/data_stories_ids.cpp     | 83 ++++---------------
 2 files changed, 17 insertions(+), 109 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index c0f70b925..e737e347e 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -362,49 +362,6 @@ void Stories::apply(not_null<PeerData*> 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);
-
-			auto archiveChanged = false;
-			auto savedChanged = false;
-			if (peer->isSelf()) {
-				for (const auto &[id, story] : stories) {
-					if (_archive.list.remove(id)) {
-						archiveChanged = true;
-						if (_archiveTotal > 0) {
-							--_archiveTotal;
-						}
-					}
-				}
-			}
-			const auto j = _saved.find(peer->id);
-			const auto saved = (j != end(_saved)) ? &j->second : nullptr;
-			for (const auto &[id, story] : stories) {
-				// Duplicated in Stories::applyDeleted.
-				_deleted.emplace(FullStoryId{ peer->id, id });
-				_expiring.remove(story->expires(), story->fullId());
-				if (story->pinned() && saved) {
-					if (saved->ids.list.remove(id)) {
-						savedChanged = true;
-						if (saved->total > 0) {
-							--saved->total;
-						}
-					}
-				}
-				session().changes().storyUpdated(
-					story.get(),
-					UpdateFlag::Destroyed);
-				removeDependencyStory(story.get());
-			}
-			if (archiveChanged) {
-				_archiveChanged.fire({});
-			}
-			if (savedChanged) {
-				_savedChanged.fire_copy(peer->id);
-			}
-		}
 		_sourceChanged.fire_copy(peer->id);
 	} else {
 		parseAndApply(*data);
diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp
index 21d0878a6..7506abea5 100644
--- a/Telegram/SourceFiles/data/data_stories_ids.cpp
+++ b/Telegram/SourceFiles/data/data_stories_ids.cpp
@@ -38,70 +38,26 @@ rpl::producer<StoriesIdsSlice> SavedStoriesIds(
 			}
 
 			const auto saved = stories->saved(peer->id);
+			Assert(saved != nullptr);
 			const auto count = stories->savedCount(peer->id);
 			const auto around = saved->list.lower_bound(aroundId);
-			Assert(saved != nullptr);
-			const auto source = stories->source(peer->id);
-			if (!source || source->ids.empty()) {
-				const auto hasBefore = int(around - begin(saved->list));
-				const auto hasAfter = int(end(saved->list) - around);
-				if (hasAfter < limit) {
-					stories->savedLoadMore(peer->id);
-				}
-				const auto takeBefore = std::min(hasBefore, limit);
-				const auto takeAfter = std::min(hasAfter, limit);
-				auto ids = base::flat_set<StoryId>{
-					std::make_reverse_iterator(around + takeAfter),
-					std::make_reverse_iterator(around - takeBefore)
-				};
-				const auto added = int(ids.size());
-				state->slice = StoriesIdsSlice(
-					std::move(ids),
-					count,
-					(hasBefore - takeBefore),
-					count - hasBefore - added);
-			} else {
-				auto ids = base::flat_set<StoryId>();
-				auto added = 0;
-				auto skipped = 0;
-				auto skippedBefore = (around - begin(saved->list));
-				auto skippedAfter = (end(saved->list) - around);
-				const auto &active = source->ids;
-				const auto process = [&](StoryId id) {
-					const auto i = active.lower_bound(StoryIdDates{ id });
-					if (i == end(active) || i->id != id) {
-						ids.emplace(id);
-						++added;
-					} else {
-						++skipped;
-					}
-					return (added < limit);
-				};
-				ids.reserve(2 * limit + 1);
-				for (auto i = around, b = begin(saved->list); i != b;) {
-					--skippedBefore;
-					if (!process(*--i)) {
-						break;
-					}
-				}
-				if (ids.size() < limit) {
-					ids.emplace(active.back().id); // #TODO stories fake max story id
-				} else {
-					++skippedBefore;
-				}
-				added = 0;
-				for (auto i = around, e = end(saved->list); i != e; ++i) {
-					--skippedAfter;
-					if (!process(*i)) {
-						break;
-					}
-				}
-				state->slice = StoriesIdsSlice(
-					std::move(ids),
-					count - skipped + 1,
-					skippedBefore,
-					skippedAfter);
+			const auto hasBefore = int(around - begin(saved->list));
+			const auto hasAfter = int(end(saved->list) - around);
+			if (hasAfter < limit) {
+				stories->savedLoadMore(peer->id);
 			}
+			const auto takeBefore = std::min(hasBefore, limit);
+			const auto takeAfter = std::min(hasAfter, limit);
+			auto ids = base::flat_set<StoryId>{
+				std::make_reverse_iterator(around + takeAfter),
+				std::make_reverse_iterator(around - takeBefore)
+			};
+			const auto added = int(ids.size());
+			state->slice = StoriesIdsSlice(
+				std::move(ids),
+				count,
+				(hasBefore - takeBefore),
+				count - hasBefore - added);
 			consumer.put_next_copy(state->slice);
 		};
 		const auto schedule = [=] {
@@ -117,11 +73,6 @@ rpl::producer<StoriesIdsSlice> SavedStoriesIds(
 		};
 
 		const auto stories = &peer->owner().stories();
-		stories->sourceChanged(
-		) | rpl::filter(
-			rpl::mappers::_1 == peer->id
-		) | rpl::start_with_next(schedule, lifetime);
-
 		stories->savedChanged(
 		) | rpl::filter(
 			rpl::mappers::_1 == peer->id

From 3ac7725111b86337c3f2b20665ff40097b9eee9a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 12 Jun 2023 18:40:13 +0400
Subject: [PATCH 072/259] Show relative time in stories, like last seen.

---
 Telegram/Resources/langs/lang.strings         |  5 ++
 .../media/stories/media_stories_header.cpp    | 71 ++++++++++++++++++-
 .../media/stories/media_stories_header.h      |  4 ++
 3 files changed, 78 insertions(+), 2 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index c29a21018..48c5590ad 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2477,6 +2477,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_mediaview_doc_image" = "File";
 "lng_mediaview_today" = "today at {time}";
 "lng_mediaview_yesterday" = "yesterday at {time}";
+"lng_mediaview_just_now" = "just now";
+"lng_mediaview_minutes_ago#one" = "{count} minute ago";
+"lng_mediaview_minutes_ago#other" = "{count} minutes ago";
+"lng_mediaview_hours_ago#one" = "{count} hour ago";
+"lng_mediaview_hours_ago#other" = "{count} hours ago";
 "lng_mediaview_date_time" = "{date} at {time}";
 "lng_mediaview_set_userpic" = "Set as Main";
 "lng_mediaview_report_profile_photo" = "Report";
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index fca65c8ca..3c089c9bd 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/unixtime.h"
 #include "data/data_user.h"
 #include "media/stories/media_stories_controller.h"
+#include "lang/lang_keys.h"
 #include "ui/controls/userpic_button.h"
 #include "ui/text/format_values.h"
 #include "ui/widgets/labels.h"
@@ -23,10 +24,60 @@ namespace {
 constexpr auto kNameOpacity = 1.;
 constexpr auto kDateOpacity = 0.8;
 
+struct Timestamp {
+	QString text;
+	TimeId changes = 0;
+};
+
+[[nodiscard]] Timestamp ComposeTimestamp(TimeId when, TimeId now) {
+	const auto minutes = (now - when) / 60;
+	if (!minutes) {
+		return { tr::lng_mediaview_just_now(tr::now), 61 - (now - when) };
+	} else if (minutes < 60) {
+		return {
+			tr::lng_mediaview_minutes_ago(tr::now, lt_count, minutes),
+			61 - ((now - when) % 60),
+		};
+	}
+	const auto hours = (now - when) / 3600;
+	if (hours < 12) {
+		return {
+			tr::lng_mediaview_hours_ago(tr::now, lt_count, hours),
+			3601 - ((now - when) % 3600),
+		};
+	}
+	const auto whenFull = base::unixtime::parse(when);
+	const auto nowFull = base::unixtime::parse(now);
+	const auto locale = QLocale();
+	auto tomorrow = nowFull;
+	tomorrow.setDate(nowFull.date().addDays(1));
+	tomorrow.setTime(QTime(0, 0, 1));
+	const auto seconds = int(nowFull.secsTo(tomorrow));
+	if (whenFull.date() == nowFull.date()) {
+		const auto whenTime = locale.toString(
+			whenFull.time(),
+			QLocale::ShortFormat);
+		return {
+			tr::lng_mediaview_today(tr::now, lt_time, whenTime),
+			seconds,
+		};
+	} else if (whenFull.date().addDays(1) == nowFull.date()) {
+		const auto whenTime = locale.toString(
+			whenFull.time(),
+			QLocale::ShortFormat);
+		return {
+			tr::lng_mediaview_yesterday(tr::now, lt_time, whenTime),
+			seconds,
+		};
+	}
+	return { Ui::FormatDateTime(whenFull) };
+}
+
 } // namespace
 
 Header::Header(not_null<Controller*> controller)
-: _controller(controller) {
+: _controller(controller)
+, _dateUpdateTimer([=] { updateDateText(); }) {
 }
 
 Header::~Header() {
@@ -66,13 +117,29 @@ void Header::show(HeaderData data) {
 			raw->setGeometry(layout.header);
 		}, raw->lifetime());
 	}
+	auto timestamp = ComposeTimestamp(data.date, base::unixtime::now());
 	_date = std::make_unique<Ui::FlatLabel>(
 		_widget.get(),
-		Ui::FormatDateTime(base::unixtime::parse(data.date)),
+		std::move(timestamp.text),
 		st::storiesHeaderDate);
 	_date->setOpacity(kDateOpacity);
 	_date->show();
 	_date->move(st::storiesHeaderDatePosition);
+
+	if (timestamp.changes > 0) {
+		_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
+	}
+}
+
+void Header::updateDateText() {
+	if (!_date || !_data || !_data->date) {
+		return;
+	}
+	auto timestamp = ComposeTimestamp(_data->date, base::unixtime::now());
+	_date->setText(timestamp.text);
+	if (timestamp.changes > 0) {
+		_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
+	}
 }
 
 } // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index 3f0be2e5c..32c310bff 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include "base/timer.h"
 #include "ui/userpic_view.h"
 
 namespace Ui {
@@ -34,11 +35,14 @@ public:
 	void show(HeaderData data);
 
 private:
+	void updateDateText();
+
 	const not_null<Controller*> _controller;
 
 	std::unique_ptr<Ui::RpWidget> _widget;
 	std::unique_ptr<Ui::FlatLabel> _date;
 	std::optional<HeaderData> _data;
+	base::Timer _dateUpdateTimer;
 
 };
 

From a933168ef71a6b7c025c7725c63a9ff0eddc3193 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 12 Jun 2023 20:41:26 +0400
Subject: [PATCH 073/259] Allow sharing stories and copying a link.

---
 Telegram/CMakeLists.txt                       |   2 +
 Telegram/SourceFiles/apiwrap.cpp              |  31 ++-
 Telegram/SourceFiles/apiwrap.h                |   3 +
 Telegram/SourceFiles/boxes/share_box.cpp      |  15 +-
 Telegram/SourceFiles/data/data_stories.cpp    |  52 +++++
 Telegram/SourceFiles/data/data_stories.h      |  14 +-
 Telegram/SourceFiles/data/data_user.cpp       |  14 ++
 Telegram/SourceFiles/data/data_user.h         |   5 +
 .../history/history_item_helpers.cpp          |   8 +-
 .../history/history_item_helpers.h            |   1 +
 .../view/history_view_context_menu.cpp        |  15 ++
 .../history/view/history_view_context_menu.h  |   4 +
 .../stories/media_stories_controller.cpp      |  26 ++-
 .../media/stories/media_stories_controller.h  |   2 +
 .../media/stories/media_stories_share.cpp     | 202 ++++++++++++++++
 .../media/stories/media_stories_share.h       |  26 +++
 .../media/stories/media_stories_view.cpp      |   8 +
 .../media/stories/media_stories_view.h        |   2 +
 .../SourceFiles/media/view/media_view.style   |   1 +
 .../media/view/media_view_overlay_opengl.cpp  |  26 ++-
 .../media/view/media_view_overlay_opengl.h    |   8 +-
 .../media/view/media_view_overlay_raster.cpp  |   2 +-
 .../media/view/media_view_overlay_raster.h    |   2 +-
 .../media/view/media_view_overlay_renderer.h  |   2 +-
 .../media/view/media_view_overlay_widget.cpp  | 219 ++++++++++--------
 .../media/view/media_view_overlay_widget.h    |  49 ++--
 26 files changed, 587 insertions(+), 152 deletions(-)
 create mode 100644 Telegram/SourceFiles/media/stories/media_stories_share.cpp
 create mode 100644 Telegram/SourceFiles/media/stories/media_stories_share.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 8f6ea9975..3a3d7e5a7 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -987,6 +987,8 @@ PRIVATE
     media/stories/media_stories_recent_views.h
     media/stories/media_stories_reply.cpp
     media/stories/media_stories_reply.h
+    media/stories/media_stories_share.cpp
+    media/stories/media_stories_share.h
     media/stories/media_stories_sibling.cpp
     media/stories/media_stories_sibling.h
     media/stories/media_stories_slider.cpp
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 922407988..44c7d507e 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_scheduled_messages.h"
 #include "data/data_channel_admins.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_channel.h"
 #include "data/data_chat.h"
 #include "data/data_user.h"
@@ -763,9 +764,7 @@ QString ApiWrap::exportDirectMessageLink(
 		channel->inputChannel,
 		MTP_int(item->id)
 	)).done([=](const MTPExportedMessageLink &result) {
-		const auto link = result.match([&](const auto &data) {
-			return qs(data.vlink());
-		});
+		const auto link = qs(result.data().vlink());
 		if (current != link) {
 			_unlikelyMessageLinks.emplace_or_assign(itemId, link);
 		}
@@ -773,6 +772,32 @@ QString ApiWrap::exportDirectMessageLink(
 	return current;
 }
 
+QString ApiWrap::exportDirectStoryLink(not_null<Data::Story*> story) {
+	const auto storyId = story->fullId();
+	const auto user = story->peer()->asUser();
+	Assert(user != nullptr);
+	const auto fallback = [&] {
+		const auto base = user->username();
+		const auto story = QString::number(storyId.story);
+		const auto query = base + "?story=" + story;
+		return session().createInternalLinkFull(query);
+	};
+	const auto i = _unlikelyStoryLinks.find(storyId);
+	const auto current = (i != end(_unlikelyStoryLinks))
+		? i->second
+		: fallback();
+	request(MTPstories_ExportStoryLink(
+		story->peer()->asUser()->inputUser,
+		MTP_int(story->id())
+	)).done([=](const MTPExportedStoryLink &result) {
+		const auto link = qs(result.data().vlink());
+		if (current != link) {
+			_unlikelyStoryLinks.emplace_or_assign(storyId, link);
+		}
+	}).send();
+	return current;
+}
+
 void ApiWrap::requestContacts() {
 	if (_session->data().contactsLoaded().current() || _contactsRequestId) {
 		return;
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index e747f0f07..dcab7cde8 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -35,6 +35,7 @@ enum class StickersType : uchar;
 class Forum;
 class ForumTopic;
 class Thread;
+class Story;
 } // namespace Data
 
 namespace InlineBots {
@@ -160,6 +161,7 @@ public:
 	QString exportDirectMessageLink(
 		not_null<HistoryItem*> item,
 		bool inRepliesContext);
+	QString exportDirectStoryLink(not_null<Data::Story*> item);
 
 	void requestContacts();
 	void requestDialogs(Data::Folder *folder = nullptr);
@@ -707,5 +709,6 @@ private:
 	base::flat_map<not_null<UserData*>, Fn<void()>> _botCommonGroupsRequests;
 
 	base::flat_map<FullMsgId, QString> _unlikelyMessageLinks;
+	base::flat_map<FullStoryId, QString> _unlikelyStoryLinks;
 
 };
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index d23e6142c..7cba7daba 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -1414,15 +1414,16 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
 				? MsgId(0)
 				: topicRootId;
 			const auto peer = thread->peer();
-			histories.sendRequest(history, requestType, [=](
+			const auto threadHistory = thread->owningHistory();
+			histories.sendRequest(threadHistory, requestType, [=](
 					Fn<void()> finish) {
-				auto &api = history->session().api();
+				auto &api = threadHistory->session().api();
 				const auto sendFlags = commonSendFlags
 					| (topMsgId ? Flag::f_top_msg_id : Flag(0))
 					| (ShouldSendSilent(peer, options)
 						? Flag::f_silent
 						: Flag(0));
-				history->sendRequestId = api.request(
+				threadHistory->sendRequestId = api.request(
 					MTPmessages_ForwardMessages(
 						MTP_flags(sendFlags),
 						history->peer->input,
@@ -1433,7 +1434,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
 						MTP_int(options.scheduled),
 						MTP_inputPeerEmpty() // send_as
 				)).done([=](const MTPUpdates &updates, mtpRequestId reqId) {
-					history->session().api().applyUpdates(updates);
+					threadHistory->session().api().applyUpdates(updates);
 					state->requests.remove(reqId);
 					if (state->requests.empty()) {
 						if (show->valid()) {
@@ -1451,10 +1452,10 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
 								peer->name()));
 					}
 					finish();
-				}).afterRequest(history->sendRequestId).send();
-				return history->sendRequestId;
+				}).afterRequest(threadHistory->sendRequestId).send();
+				return threadHistory->sendRequestId;
 			});
-			state->requests.insert(history->sendRequestId);
+			state->requests.insert(threadHistory->sendRequestId);
 		}
 	};
 }
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index e737e347e..0d2e64bfa 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "apiwrap.h"
 #include "core/application.h"
 #include "data/data_changes.h"
+#include "data/data_chat_participant_status.h"
 #include "data/data_document.h"
 #include "data/data_file_origin.h"
 #include "data/data_photo.h"
@@ -196,6 +197,51 @@ bool Story::pinned() const {
 	return _pinned;
 }
 
+void Story::setIsPublic(bool isPublic) {
+	_isPublic = isPublic;
+}
+
+bool Story::isPublic() const {
+	return _isPublic;
+}
+
+void Story::setCloseFriends(bool closeFriends) {
+	_closeFriends = closeFriends;
+}
+
+bool Story::closeFriends() const {
+	return _closeFriends;
+}
+
+bool Story::hasDirectLink() const {
+	if (!_isPublic || (!_pinned && expired())) {
+		return false;
+	}
+	const auto user = _peer->asUser();
+	return user && !user->username().isEmpty();
+}
+
+std::optional<QString> Story::errorTextForForward(
+		not_null<Thread*> to) const {
+	const auto peer = to->peer();
+	const auto holdsPhoto = v::is<not_null<PhotoData*>>(_media.data);
+	const auto first = holdsPhoto
+		? ChatRestriction::SendPhotos
+		: ChatRestriction::SendVideos;
+	const auto second = holdsPhoto
+		? ChatRestriction::SendVideos
+		: ChatRestriction::SendPhotos;
+	if (const auto error = Data::RestrictionError(peer, first)) {
+		return *error;
+	} else if (const auto error = Data::RestrictionError(peer, second)) {
+		return *error;
+	} else if (!Data::CanSend(to, first, false)
+		|| !Data::CanSend(to, second, false)) {
+		return tr::lng_forward_cant(tr::now);
+	}
+	return {};
+}
+
 void Story::setCaption(TextWithEntities &&caption) {
 	_caption = std::move(caption);
 }
@@ -259,6 +305,8 @@ void Story::applyViewsSlice(
 
 bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 	const auto pinned = data.is_pinned();
+	const auto isPublic = data.is_public();
+	const auto closeFriends = data.is_close_friends();
 	auto caption = TextWithEntities{
 		data.vcaption().value_or_empty(),
 		Api::EntitiesFromMTP(
@@ -280,6 +328,8 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 
 	const auto changed = (_media != media)
 		|| (_pinned != pinned)
+		|| (_isPublic != isPublic)
+		|| (_closeFriends != closeFriends)
 		|| (_caption != caption)
 		|| (_views != views)
 		|| (_recentViewers != recent);
@@ -288,6 +338,8 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 	}
 	_media = std::move(media);
 	_pinned = pinned;
+	_isPublic = isPublic;
+	_closeFriends = closeFriends;
 	_caption = std::move(caption);
 	_views = views;
 	_recentViewers = std::move(recent);
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index b7de66759..8fc341076 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 class Image;
 class PhotoData;
 class DocumentData;
+enum class ChatRestriction;
 
 namespace Main {
 class Session;
@@ -23,6 +24,7 @@ class Session;
 namespace Data {
 
 class Session;
+class Thread;
 
 struct StoryIdDates {
 	StoryId id = 0;
@@ -91,6 +93,14 @@ public:
 
 	void setPinned(bool pinned);
 	[[nodiscard]] bool pinned() const;
+	void setIsPublic(bool isPublic);
+	[[nodiscard]] bool isPublic() const;
+	void setCloseFriends(bool closeFriends);
+	[[nodiscard]] bool closeFriends() const;
+
+	[[nodiscard]] bool hasDirectLink() const;
+	[[nodiscard]] std::optional<QString> errorTextForForward(
+		not_null<Thread*> to) const;
 
 	void setCaption(TextWithEntities &&caption);
 	[[nodiscard]] const TextWithEntities &caption() const;
@@ -117,7 +127,9 @@ private:
 	int _views = 0;
 	const TimeId _date = 0;
 	const TimeId _expires = 0;
-	bool _pinned = false;
+	bool _pinned : 1 = false;
+	bool _isPublic : 1 = false;
+	bool _closeFriends : 1 = false;
 
 };
 
diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp
index 148ffb246..be4a9fa6f 100644
--- a/Telegram/SourceFiles/data/data_user.cpp
+++ b/Telegram/SourceFiles/data/data_user.cpp
@@ -113,6 +113,18 @@ void UserData::setCommonChatsCount(int count) {
 	}
 }
 
+bool UserData::hasPrivateForwardName() const {
+	return !_privateForwardName.isEmpty();
+}
+
+QString UserData::privateForwardName() const {
+	return _privateForwardName;
+}
+
+void UserData::setPrivateForwardName(const QString &name) {
+	_privateForwardName = name;
+}
+
 void UserData::setName(const QString &newFirstName, const QString &newLastName, const QString &newPhoneName, const QString &newUsername) {
 	bool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty();
 
@@ -449,6 +461,8 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
 	user->checkFolder(update.vfolder_id().value_or_empty());
 	user->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty()));
 	user->setTranslationDisabled(update.is_translations_disabled());
+	user->setPrivateForwardName(
+		update.vprivate_forward_name().value_or_empty());
 
 	if (const auto info = user->botInfo.get()) {
 		const auto group = update.vbot_group_admin_rights()
diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h
index ad0dec4d4..37005d124 100644
--- a/Telegram/SourceFiles/data/data_user.h
+++ b/Telegram/SourceFiles/data/data_user.h
@@ -170,6 +170,10 @@ public:
 	int commonChatsCount() const;
 	void setCommonChatsCount(int count);
 
+	[[nodiscard]] bool hasPrivateForwardName() const;
+	[[nodiscard]] QString privateForwardName() const;
+	void setPrivateForwardName(const QString &name);
+
 private:
 	auto unavailableReasons() const
 		-> const std::vector<Data::UnavailableReason> & override;
@@ -180,6 +184,7 @@ private:
 
 	std::vector<Data::UnavailableReason> _unavailableReasons;
 	QString _phone;
+	QString _privateForwardName;
 	ContactStatus _contactStatus = ContactStatus::Unknown;
 	CallsStatus _callsStatus = CallsStatus::Unknown;
 	int _commonChatsCount = 0;
diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp
index 275b6ebcb..df971938c 100644
--- a/Telegram/SourceFiles/history/history_item_helpers.cpp
+++ b/Telegram/SourceFiles/history/history_item_helpers.cpp
@@ -64,6 +64,11 @@ QString GetErrorTextForSending(
 	const auto thread = topic
 		? not_null<Data::Thread*>(topic)
 		: peer->owner().history(peer);
+	if (request.story) {
+		if (const auto error = request.story->errorTextForForward(thread)) {
+			return *error;
+		}
+	}
 	if (request.forward) {
 		for (const auto &item : *request.forward) {
 			if (const auto error = item->errorTextForForward(thread)) {
@@ -84,6 +89,7 @@ QString GetErrorTextForSending(
 	}
 	if (peer->slowmodeApplied()) {
 		const auto count = (hasText ? 1 : 0)
+			+ (request.story ? 1 : 0)
 			+ (request.forward ? int(request.forward->size()) : 0);
 		if (const auto history = peer->owner().historyLoaded(peer)) {
 			if (!request.ignoreSlowmodeCountdown
@@ -94,7 +100,7 @@ QString GetErrorTextForSending(
 		}
 		if (request.text && request.text->text.size() > MaxMessageSize) {
 			return tr::lng_slowmode_too_long(tr::now);
-		} else if (hasText && count > 1) {
+		} else if ((hasText || request.story) && count > 1) {
 			return tr::lng_slowmode_no_many(tr::now);
 		} else if (count > 1) {
 			const auto albumForward = [&] {
diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h
index f9f45d029..2d9765886 100644
--- a/Telegram/SourceFiles/history/history_item_helpers.h
+++ b/Telegram/SourceFiles/history/history_item_helpers.h
@@ -91,6 +91,7 @@ void RequestDependentMessageStory(
 struct SendingErrorRequest {
 	MsgId topicRootId = 0;
 	const HistoryItemsList *forward = nullptr;
+	const Data::Story *story = nullptr;
 	const TextWithTags *text = nullptr;
 	bool ignoreSlowmodeCountdown = false;
 };
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
index 2fd7c2ca8..4992cca85 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
@@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_media_types.h"
 #include "data/data_forum_topic.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_groups.h"
 #include "data/data_channel.h"
 #include "data/data_file_click_handler.h"
@@ -1131,6 +1132,20 @@ void CopyPostLink(
 		: tr::lng_context_about_private_link(tr::now));
 }
 
+void CopyStoryLink(
+		std::shared_ptr<Main::SessionShow> show,
+		FullStoryId storyId) {
+	const auto session = &show->session();
+	const auto maybeStory = session->data().stories().lookup(storyId);
+	if (!maybeStory) {
+		return;
+	}
+	const auto story = *maybeStory;
+	QGuiApplication::clipboard()->setText(
+		session->api().exportDirectStoryLink(story));
+	show->showToast(tr::lng_channel_public_link_copied(tr::now));
+}
+
 void AddPollActions(
 		not_null<Ui::PopupMenu*> menu,
 		not_null<PollData*> poll,
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h
index 84e443d41..d534d9c24 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.h
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h
@@ -15,6 +15,7 @@ struct ReactionId;
 
 namespace Main {
 class Session;
+class SessionShow;
 } // namespace Main
 
 namespace Ui {
@@ -58,6 +59,9 @@ void CopyPostLink(
 	not_null<Window::SessionController*> controller,
 	FullMsgId itemId,
 	Context context);
+void CopyStoryLink(
+	std::shared_ptr<Main::SessionShow> show,
+	FullStoryId storyId);
 void AddPollActions(
 	not_null<Ui::PopupMenu*> menu,
 	not_null<PollData*> poll,
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 8889204d9..3e6828ddf 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -25,9 +25,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/stories/media_stories_reactions.h"
 #include "media/stories/media_stories_recent_views.h"
 #include "media/stories/media_stories_reply.h"
+#include "media/stories/media_stories_share.h"
 #include "media/stories/media_stories_view.h"
 #include "media/audio/media_audio.h"
 #include "ui/rp_widget.h"
+#include "ui/layers/box_content.h"
 #include "styles/style_media_view.h"
 #include "styles/style_widgets.h"
 #include "styles/style_boxes.h" // UserpicButton
@@ -758,8 +760,23 @@ void Controller::setMenuShown(bool shown) {
 	}
 }
 
+bool Controller::canShare() const {
+	if (const auto maybeStory = _session->data().stories().lookup(_shown)) {
+		const auto story = *maybeStory;
+		const auto user = story->peer()->asUser();
+		return story->isPublic()
+			&& (story->pinned() || !story->expired())
+			&& (!user->username().isEmpty()
+				|| !user->hasPrivateForwardName());
+	}
+	return false;
+}
+
 bool Controller::canDownload() const {
-	return _source && _source->user->isSelf();
+	if (const auto maybeStory = _session->data().stories().lookup(_shown)) {
+		return (*maybeStory)->peer()->isSelf();
+	}
+	return false;
 }
 
 void Controller::repaintSibling(not_null<Sibling*> sibling) {
@@ -876,6 +893,13 @@ void Controller::unfocusReply() {
 	_wrap->setFocus();
 }
 
+void Controller::share() {
+	const auto show = _delegate->storiesShow();
+	if (auto box = PrepareShareBox(show, _shown)) {
+		show->show(std::move(box));
+	}
+}
+
 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 52d780093..5c83202c8 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -120,6 +120,7 @@ public:
 	void contentPressed(bool pressed);
 	void setMenuShown(bool shown);
 
+	[[nodiscard]] bool canShare() const;
 	[[nodiscard]] bool canDownload() const;
 
 	void repaintSibling(not_null<Sibling*> sibling);
@@ -129,6 +130,7 @@ public:
 	[[nodiscard]] rpl::producer<> moreViewsLoaded() const;
 
 	void unfocusReply();
+	void share();
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp
new file mode 100644
index 000000000..4d1774ff8
--- /dev/null
+++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp
@@ -0,0 +1,202 @@
+/*
+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_share.h"
+
+#include "api/api_common.h"
+#include "apiwrap.h"
+#include "base/random.h"
+#include "boxes/share_box.h"
+#include "chat_helpers/compose/compose_show.h"
+#include "data/data_chat_participant_status.h"
+#include "data/data_forum_topic.h"
+#include "data/data_histories.h"
+#include "data/data_session.h"
+#include "data/data_stories.h"
+#include "data/data_thread.h"
+#include "data/data_user.h"
+#include "history/history.h"
+#include "history/history_item_helpers.h" // GetErrorTextForSending.
+#include "history/view/history_view_context_menu.h" // CopyStoryLink.
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "ui/boxes/confirm_box.h"
+#include "ui/text/text_utilities.h"
+#include "styles/style_calls.h"
+
+namespace Media::Stories {
+
+[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(
+		std::shared_ptr<ChatHelpers::Show> show,
+		FullStoryId id) {
+	const auto session = &show->session();
+	const auto resolve = [=] {
+		const auto maybeStory = session->data().stories().lookup(id);
+		return maybeStory ? maybeStory->get() : nullptr;
+	};
+	const auto story = resolve();
+	if (!story) {
+		return { nullptr };
+	}
+	const auto canCopyLink = story->hasDirectLink();
+
+	auto copyCallback = [=] {
+		const auto story = resolve();
+		if (!story) {
+			return;
+		}
+		if (story->hasDirectLink()) {
+			using namespace HistoryView;
+			CopyStoryLink(show, story->fullId());
+		}
+	};
+
+	struct State {
+		int requests = 0;
+	};
+	const auto state = std::make_shared<State>();
+	auto filterCallback = [=](not_null<Data::Thread*> thread) {
+		return Data::CanSend(thread, ChatRestriction::SendPhotos)
+			&& Data::CanSend(thread, ChatRestriction::SendVideos);
+	};
+	auto copyLinkCallback = canCopyLink
+		? Fn<void()>(std::move(copyCallback))
+		: Fn<void()>();
+	auto submitCallback = [=](
+			std::vector<not_null<Data::Thread*>> &&result,
+			TextWithTags &&comment,
+			Api::SendOptions options,
+			Data::ForwardOptions forwardOptions) {
+		if (state->requests) {
+			return; // Share clicked already.
+		}
+		const auto story = resolve();
+		if (!story) {
+			return;
+		}
+		const auto user = story->peer()->asUser();
+		Assert(user != nullptr);
+
+		const auto error = [&] {
+			for (const auto thread : result) {
+				const auto error = GetErrorTextForSending(
+					thread,
+					{ .story = story, .text = &comment });
+				if (!error.isEmpty()) {
+					return std::make_pair(error, thread);
+				}
+			}
+			return std::make_pair(QString(), result.front());
+		}();
+		if (!error.first.isEmpty()) {
+			auto text = TextWithEntities();
+			if (result.size() > 1) {
+				text.append(
+					Ui::Text::Bold(error.second->chatListName())
+				).append("\n\n");
+			}
+			text.append(error.first);
+			show->showBox(Ui::MakeInformBox(text));
+			return;
+		}
+
+		const auto generateRandom = [&] {
+			return base::RandomValue<MTPlong>();
+		};
+		const auto api = &story->owner().session().api();
+		auto &histories = story->owner().histories();
+		const auto requestType = Data::Histories::RequestType::Send;
+		for (const auto thread : result) {
+			const auto action = Api::SendAction(thread, options);
+			if (!comment.text.isEmpty()) {
+				auto message = Api::MessageToSend(action);
+				message.textWithTags = comment;
+				message.action.clearDraft = false;
+				api->sendMessage(std::move(message));
+			}
+			const auto topicRootId = thread->topicRootId();
+			const auto kGeneralId = Data::ForumTopic::kGeneralId;
+			const auto topMsgId = (topicRootId == kGeneralId)
+				? MsgId(0)
+				: topicRootId;
+			const auto peer = thread->peer();
+			const auto threadHistory = thread->owningHistory();
+			const auto randomId = base::RandomValue<uint64>();
+			auto sendFlags = MTPmessages_SendMedia::Flags(0);
+			if (action.replyTo) {
+				sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
+			}
+			const auto anonymousPost = peer->amAnonymous();
+			const auto silentPost = ShouldSendSilent(peer, action.options);
+			if (silentPost) {
+				sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
+			}
+			const auto done = [=] {
+				if (!--state->requests) {
+					if (show->valid()) {
+						show->showToast(tr::lng_share_done(tr::now));
+						show->hideLayer();
+					}
+				}
+			};
+			histories.sendPreparedMessage(
+				threadHistory,
+				action.replyTo,
+				randomId,
+				Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
+					MTP_flags(sendFlags),
+					peer->input,
+					Data::Histories::ReplyToPlaceholder(),
+					MTP_inputMediaStory(
+						user->inputUser,
+						MTP_int(id.story)),
+					MTPstring(),
+					MTP_long(randomId),
+					MTPReplyMarkup(),
+					MTPVector<MTPMessageEntity>(),
+					MTP_int(action.options.scheduled),
+					MTP_inputPeerEmpty()
+				), [=](const MTPUpdates &result, const MTP::Response &response) {
+				done();
+			}, [=](const MTP::Error &error, const MTP::Response &response) {
+				api->sendMessageFail(error, peer, randomId);
+				done();
+			});
+			++state->requests;
+		}
+	};
+
+	const auto scheduleStyle = [&] {
+		auto date = Ui::ChooseDateTimeStyleArgs();
+		date.labelStyle = &st::groupCallBoxLabel;
+		date.dateFieldStyle = &st::groupCallScheduleDateField;
+		date.timeFieldStyle = &st::groupCallScheduleTimeField;
+		date.separatorStyle = &st::callMuteButtonLabel;
+		date.atStyle = &st::callMuteButtonLabel;
+		date.calendarStyle = &st::groupCallCalendarColors;
+
+		auto st = HistoryView::ScheduleBoxStyleArgs();
+		st.topButtonStyle = &st::groupCallMenuToggle;
+		st.popupMenuStyle = &st::groupCallPopupMenu;
+		st.chooseDateTimeArgs = std::move(date);
+		return st;
+	};
+
+	return Box<ShareBox>(ShareBox::Descriptor{
+		.session = session,
+		.copyCallback = std::move(copyLinkCallback),
+		.submitCallback = std::move(submitCallback),
+		.filterCallback = std::move(filterCallback),
+		.stMultiSelect = &st::groupCallMultiSelect,
+		.stComment = &st::groupCallShareBoxComment,
+		.st = &st::groupCallShareBoxList,
+		.stLabel = &st::groupCallField,
+		.scheduleBoxStyle = scheduleStyle(),
+	});
+}
+
+} // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.h b/Telegram/SourceFiles/media/stories/media_stories_share.h
new file mode 100644
index 000000000..e3dab6245
--- /dev/null
+++ b/Telegram/SourceFiles/media/stories/media_stories_share.h
@@ -0,0 +1,26 @@
+/*
+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/object_ptr.h"
+
+namespace ChatHelpers {
+class Show;
+} // namespace ChatHelpers
+
+namespace Ui {
+class BoxContent;
+} // namespace Ui
+
+namespace Media::Stories {
+
+[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(
+	std::shared_ptr<ChatHelpers::Show> show,
+	FullStoryId id);
+
+} // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
index 47f967160..1b2747ee4 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::canShare() const {
+	return _controller->canShare();
+}
+
 bool View::canDownload() const {
 	return _controller->canDownload();
 }
@@ -79,6 +83,10 @@ void View::contentPressed(bool pressed) {
 	_controller->contentPressed(pressed);
 }
 
+void View::share() {
+	_controller->share();
+}
+
 SiblingView View::sibling(SiblingType type) const {
 	return _controller->sibling(type);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h
index 40ee13c91..ba138ac5d 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -56,6 +56,7 @@ public:
 	void show(not_null<Data::Story*> story, Data::StoriesContext context);
 	void ready();
 
+	[[nodiscard]] bool canShare() const;
 	[[nodiscard]] bool canDownload() const;
 	[[nodiscard]] QRect finalShownGeometry() const;
 	[[nodiscard]] rpl::producer<QRect> finalShownGeometryValue() const;
@@ -74,6 +75,7 @@ public:
 	[[nodiscard]] bool paused() const;
 	void togglePaused(bool paused);
 	void contentPressed(bool pressed);
+	void share();
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index cfc78f8b2..e6e1805ea 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -109,6 +109,7 @@ mediaviewRight: icon {
 	{ "mediaview/next", mediaviewControlFg }
 };
 mediaviewSave: icon {{ "mediaview/download", mediaviewControlFg }};
+mediaviewShare: icon {{ "mediaview/stories_next", mediaviewControlFg }};
 mediaviewRotate: icon {{ "mediaview/rotate", mediaviewControlFg }};
 mediaviewMore: icon {{ "title_menu_dots", mediaviewControlFg }};
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
index 85211c74d..96da6d8ff 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
@@ -611,7 +611,7 @@ void OverlayWidget::RendererGL::paintControlsStart() {
 }
 
 void OverlayWidget::RendererGL::paintControl(
-		OverState control,
+		Over control,
 		QRect over,
 		float64 overOpacity,
 		QRect inner,
@@ -676,20 +676,21 @@ void OverlayWidget::RendererGL::paintControl(
 	FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset);
 }
 
-auto OverlayWidget::RendererGL::ControlMeta(OverState control, bool stories)
+auto OverlayWidget::RendererGL::ControlMeta(Over control, bool stories)
 -> Control {
 	switch (control) {
-	case OverLeftNav: return {
+	case Over::Left: return {
 		0,
 		stories ? &st::storiesLeft : &st::mediaviewLeft
 	};
-	case OverRightNav: return {
+	case Over::Right: 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 };
+	case Over::Save: return { 2, &st::mediaviewSave };
+	case Over::Share: return { 3, &st::mediaviewShare };
+	case Over::Rotate: return { 4, &st::mediaviewRotate };
+	case Over::More: return { 5, &st::mediaviewMore };
 	}
 	Unexpected("Control value in OverlayWidget::RendererGL::ControlIndex.");
 }
@@ -700,11 +701,12 @@ void OverlayWidget::RendererGL::validateControls() {
 	}
 	const auto stories = (_owner->_stories != nullptr);
 	const auto metas = {
-		ControlMeta(OverLeftNav, stories),
-		ControlMeta(OverRightNav, stories),
-		ControlMeta(OverSave, stories),
-		ControlMeta(OverRotate, stories),
-		ControlMeta(OverMore, stories),
+		ControlMeta(Over::Left, stories),
+		ControlMeta(Over::Right, stories),
+		ControlMeta(Over::Save, stories),
+		ControlMeta(Over::Share, stories),
+		ControlMeta(Over::Rotate, stories),
+		ControlMeta(Over::More, 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 df6f2aa71..4f769ce7b 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h
@@ -63,7 +63,7 @@ private:
 	void paintSaveMsg(QRect outer) override;
 	void paintControlsStart() override;
 	void paintControl(
-		OverState control,
+		Over control,
 		QRect over,
 		float64 overOpacity,
 		QRect inner,
@@ -145,10 +145,8 @@ private:
 	static constexpr auto kStoriesSiblingPartsCount = 4;
 	Ui::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount];
 
-	static constexpr auto kControlsCount = 5;
-	[[nodiscard]] static Control ControlMeta(
-		OverState control,
-		bool stories);
+	static constexpr auto kControlsCount = 6;
+	[[nodiscard]] static Control ControlMeta(Over control, bool stories);
 
 	// Last one is for the over circle image.
 	std::array<QRect, kControlsCount + 1> _controlsTextures;
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
index e7cc17d74..277d39d96 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
@@ -215,7 +215,7 @@ void OverlayWidget::RendererSW::paintControlsStart() {
 }
 
 void OverlayWidget::RendererSW::paintControl(
-		OverState control,
+		Over control,
 		QRect over,
 		float64 overOpacity,
 		QRect inner,
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h
index 3d8abf4c4..6399f2219 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h
@@ -43,7 +43,7 @@ private:
 	void paintSaveMsg(QRect outer) override;
 	void paintControlsStart() override;
 	void paintControl(
-		OverState control,
+		Over control,
 		QRect over,
 		float64 overOpacity,
 		QRect inner,
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h
index cd3ea698e..612bad39e 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h
@@ -34,7 +34,7 @@ public:
 	virtual void paintSaveMsg(QRect outer) = 0;
 	virtual void paintControlsStart() = 0;
 	virtual void paintControl(
-		OverState control,
+		Over control,
 		QRect over,
 		float64 overOpacity,
 		QRect inner,
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 8aa3612c6..42a5117b6 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -536,8 +536,8 @@ OverlayWidget::OverlayWidget()
 	base::install_event_filter(_widget, [=](not_null<QEvent*> e) {
 		const auto type = e->type();
 		if (type == QEvent::Leave) {
-			if (_over != OverNone) {
-				updateOverState(OverNone);
+			if (_over != Over::None) {
+				updateOverState(Over::None);
 			}
 		} else if (type == QEvent::MouseButtonPress) {
 			handleMousePress(mousePosition(e), mouseButton(e));
@@ -675,7 +675,7 @@ void OverlayWidget::setupWindow() {
 			|| _helper->skipTitleHitTest(widgetPoint)) {
 			return Flag::None | Flag(0);
 		}
-		const auto inControls = (_over != OverNone) && (_over != OverVideo);
+		const auto inControls = (_over != Over::None) && (_over != Over::Video);
 		if (inControls
 			|| (_streamed
 				&& _streamed->controls
@@ -970,13 +970,16 @@ QSize OverlayWidget::flipSizeByRotation(QSize size) const {
 }
 
 bool OverlayWidget::hasCopyMediaRestriction() const {
-	return (_history && !_history->peer->allowsForwarding())
+	return (_stories && !_stories->canDownload())
+		|| (_history && !_history->peer->allowsForwarding())
 		|| (_message && _message->forbidsSaving());
 }
 
 bool OverlayWidget::showCopyMediaRestriction() {
 	if (!hasCopyMediaRestriction()) {
 		return false;
+	} else if (!_history) {
+		return true;
 	}
 	Ui::Toast::Show(_widget, _history->peer->isBroadcast()
 		? tr::lng_error_nocopy_channel(tr::now)
@@ -1145,8 +1148,7 @@ void OverlayWidget::refreshNavVisibility() {
 }
 
 bool OverlayWidget::contentCanBeSaved() const {
-	if ((_stories && !_stories->canDownload())
-		|| hasCopyMediaRestriction()) {
+	if (hasCopyMediaRestriction()) {
 		return false;
 	} else if (_photo) {
 		return _photo->hasVideo() || _photoMedia->loaded();
@@ -1225,6 +1227,7 @@ void OverlayWidget::updateControls() {
 		QPoint(),
 		QSize(st::mediaviewIconOver, st::mediaviewIconOver));
 	_saveVisible = contentCanBeSaved();
+	_shareVisible = _stories && _stories->canShare();
 	_rotateVisible = !_themePreviewShown && !_stories;
 	const auto navRect = [&](int i) {
 		return QRect(width() - st::mediaviewIconSize.width() * i,
@@ -1232,15 +1235,26 @@ void OverlayWidget::updateControls() {
 			st::mediaviewIconSize.width(),
 			st::mediaviewIconSize.height());
 	};
-	_saveNav = navRect(_rotateVisible ? 3 : 2);
-	_saveNavOver = style::centerrect(_saveNav, overRect);
-	_saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave);
-	_rotateNav = navRect(2);
-	_rotateNavOver = style::centerrect(_rotateNav, overRect);
-	_rotateNavIcon = style::centerrect(_rotateNav, st::mediaviewRotate);
-	_moreNav = navRect(1);
+	auto index = 1;
+	_moreNav = navRect(index);
 	_moreNavOver = style::centerrect(_moreNav, overRect);
 	_moreNavIcon = style::centerrect(_moreNav, st::mediaviewMore);
+	++index;
+	_rotateNav = navRect(index);
+	_rotateNavOver = style::centerrect(_rotateNav, overRect);
+	_rotateNavIcon = style::centerrect(_rotateNav, st::mediaviewRotate);
+	if (_rotateVisible) {
+		++index;
+	}
+	_shareNav = navRect(index);
+	_shareNavOver = style::centerrect(_shareNav, overRect);
+	_shareNavIcon = style::centerrect(_shareNav, st::mediaviewSave);
+	if (_shareVisible) {
+		++index;
+	}
+	_saveNav = navRect(index);
+	_saveNavOver = style::centerrect(_saveNav, overRect);
+	_saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave);
 
 	const auto dNow = QDateTime::currentDateTime();
 	const auto d = [&] {
@@ -1571,17 +1585,18 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) {
 	}
 	_helper->setControlsOpacity(_controlsOpacity.current());
 	const auto content = finalContentRect();
-	const auto siblingType = (_over == OverLeftStories)
+	const auto siblingType = (_over == Over::LeftStories)
 		? 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)
+		+ (_over == Over::Left ? _leftNavOver : _leftNavIcon)
+		+ (_over == Over::Right ? _rightNavOver : _rightNavIcon)
+		+ (_over == Over::Save ? _saveNavOver : _saveNavIcon)
+		+ (_over == Over::Share ? _shareNavOver : _shareNavIcon)
+		+ (_over == Over::Rotate ? _rotateNavOver : _rotateNavIcon)
+		+ (_over == Over::More ? _moreNavOver : _moreNavIcon)
 		+ ((_stories
-			&& (_over == OverLeftStories || _over == OverRightStories))
+			&& (_over == Over::LeftStories || _over == Over::RightStories))
 			? _stories->sibling(siblingType).layout.geometry
 			: QRect())
 		+ _headerNav
@@ -1604,7 +1619,7 @@ void OverlayWidget::waitingAnimationCallback() {
 void OverlayWidget::updateCursor() {
 	setCursor((_controlsState == ControlsHidden)
 		? Qt::BlankCursor
-		: (_over == OverNone || (_over == OverVideo && _stories))
+		: (_over == Over::None || (_over == Over::Video && _stories))
 		? style::cur_default
 		: style::cur_pointer);
 }
@@ -2971,7 +2986,7 @@ void OverlayWidget::clearControlsState() {
 	_saveMsgAnimation.stop();
 	_saveMsgTimer.cancel();
 	_loadRequest = 0;
-	_over = _down = OverNone;
+	_over = _down = Over::None;
 	_pressed = false;
 	_dragging = 0;
 	setCursor(style::cur_default);
@@ -3158,7 +3173,7 @@ void OverlayWidget::displayPhoto(
 	refreshCaption();
 
 	_blurred = true;
-	_down = OverNone;
+	_down = Over::None;
 	if (!_staticContent.isNull()) {
 		// Video thumbnail.
 		const auto size = style::ConvertScale(
@@ -4117,8 +4132,8 @@ void OverlayWidget::storiesTogglePaused(bool paused) {
 
 float64 OverlayWidget::storiesSiblingOver(Stories::SiblingType type) {
 	return (type == Stories::SiblingType::Left)
-		? overLevel(OverLeftStories)
-		: overLevel(OverRightStories);
+		? overLevel(Over::LeftStories)
+		: overLevel(Over::RightStories);
 }
 void OverlayWidget::storiesRepaint() {
 	update();
@@ -4408,7 +4423,7 @@ void OverlayWidget::paintRadialLoadingContent(
 	if (_photo) {
 		paintBg(radialOpacity, st::radialBg);
 	} else {
-		const auto o = overLevel(OverIcon);
+		const auto o = overLevel(Over::Icon);
 		paintBg(
 			_documentMedia->loaded() ? radialOpacity : 1.,
 			anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, o));
@@ -4584,7 +4599,7 @@ void OverlayWidget::paintControls(
 		not_null<Renderer*> renderer,
 		float64 opacity) {
 	struct Control {
-		OverState state = OverNone;
+		Over state = Over::None;
 		bool visible = false;
 		const QRect &over;
 		const QRect &inner;
@@ -4595,33 +4610,39 @@ void OverlayWidget::paintControls(
 	// When adding / removing controls please update RendererGL.
 	const Control controls[] = {
 		{
-			OverLeftNav,
+			Over::Left,
 			_leftNavVisible,
 			_leftNavOver,
 			_leftNavIcon,
 			_stories ? st::storiesLeft : st::mediaviewLeft,
 			true },
 		{
-			OverRightNav,
+			Over::Right,
 			_rightNavVisible,
 			_rightNavOver,
 			_rightNavIcon,
 			_stories ? st::storiesRight : st::mediaviewRight,
 			true },
 		{
-			OverSave,
+			Over::Save,
 			_saveVisible,
 			_saveNavOver,
 			_saveNavIcon,
 			st::mediaviewSave },
 		{
-			OverRotate,
+			Over::Share,
+			_shareVisible,
+			_shareNavOver,
+			_shareNavIcon,
+			st::mediaviewShare },
+		{
+			Over::Rotate,
 			_rotateVisible,
 			_rotateNavOver,
 			_rotateNavIcon,
 			st::mediaviewRotate },
 		{
-			OverMore,
+			Over::More,
 			true,
 			_moreNavOver,
 			_moreNavIcon,
@@ -4673,7 +4694,7 @@ void OverlayWidget::paintFooterContent(
 	const auto name = _nameNav.translated(shift);
 	const auto date = _dateNav.translated(shift);
 	if (header.intersects(clip)) {
-		auto o = _headerHasLink ? overLevel(OverHeader) : 0;
+		auto o = _headerHasLink ? overLevel(Over::Header) : 0;
 		p.setOpacity(controlOpacity(o) * opacity);
 		p.drawText(header.left(), header.top() + st::mediaviewThickFont->ascent, _headerText);
 
@@ -4687,7 +4708,7 @@ void OverlayWidget::paintFooterContent(
 
 	// name
 	if (_nameNav.isValid() && name.intersects(clip)) {
-		float64 o = _from ? overLevel(OverName) : 0.;
+		float64 o = _from ? overLevel(Over::Name) : 0.;
 		p.setOpacity(controlOpacity(o) * opacity);
 		_fromNameLabel.drawElided(p, name.left(), name.top(), name.width());
 
@@ -4699,7 +4720,7 @@ void OverlayWidget::paintFooterContent(
 
 	// date
 	if (date.intersects(clip)) {
-		float64 o = overLevel(OverDate);
+		float64 o = overLevel(Over::Date);
 		p.setOpacity(controlOpacity(o) * opacity);
 		p.drawText(date.left(), date.top() + st::mediaviewFont->ascent, _dateText);
 
@@ -4768,7 +4789,7 @@ void OverlayWidget::handleKeyPress(not_null<QKeyEvent*> 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) {
+	if (_stories && key == Qt::Key_Space && _down != Over::Video) {
 		_stories->togglePaused(!_stories->paused());
 		return;
 	}
@@ -5180,27 +5201,28 @@ void OverlayWidget::handleMousePress(
 	ClickHandler::pressed();
 
 	if (button == Qt::LeftButton) {
-		_down = OverNone;
+		_down = Over::None;
 		if (!ClickHandler::getPressed()) {
-			if ((_over == OverLeftNav && moveToNext(-1))
-				|| (_over == OverRightNav && moveToNext(1))
+			if ((_over == Over::Left && moveToNext(-1))
+				|| (_over == Over::Right && moveToNext(1))
 				|| (_stories
-					&& _over == OverLeftStories
+					&& _over == Over::LeftStories
 					&& _stories->jumpFor(-1))
 				|| (_stories
-					&& _over == OverRightStories
+					&& _over == Over::RightStories
 					&& _stories->jumpFor(1))) {
 				_lastAction = position;
-			} else if (_over == OverName
-				|| _over == OverDate
-				|| _over == OverHeader
-				|| _over == OverSave
-				|| _over == OverRotate
-				|| _over == OverIcon
-				|| _over == OverMore
-				|| _over == OverVideo) {
+			} else if (_over == Over::Name
+				|| _over == Over::Date
+				|| _over == Over::Header
+				|| _over == Over::Save
+				|| _over == Over::Share
+				|| _over == Over::Rotate
+				|| _over == Over::Icon
+				|| _over == Over::More
+				|| _over == Over::Video) {
 				_down = _over;
-				if (_over == OverVideo && _stories) {
+				if (_over == Over::Video && _stories) {
 					_stories->contentPressed(true);
 				}
 			} else if (!_saveMsg.contains(position) || !isSaveMsgShown()) {
@@ -5223,7 +5245,7 @@ bool OverlayWidget::handleDoubleClick(
 		Qt::MouseButton button) {
 	updateOver(position);
 
-	if (_over != OverVideo || !_streamed || button != Qt::LeftButton) {
+	if (_over != Over::Video || !_streamed || button != Qt::LeftButton) {
 		return false;
 	} else if (_stories) {
 		toggleFullScreen(_windowed);
@@ -5276,46 +5298,47 @@ void OverlayWidget::handleMouseMove(QPoint position) {
 	}
 }
 
-void OverlayWidget::updateOverRect(OverState state) {
+void OverlayWidget::updateOverRect(Over state) {
 	using Type = Stories::SiblingType;
 	switch (state) {
-	case OverLeftNav:
+	case Over::Left:
 		update(_stories ? _leftNavIcon : _leftNavOver);
 		break;
-	case OverRightNav:
+	case Over::Right:
 		update(_stories ? _rightNavIcon : _rightNavOver);
 		break;
-	case OverLeftStories:
+	case Over::LeftStories:
 		update(_stories
 			? _stories->sibling(Type::Left).layout.geometry :
 			QRect());
 		break;
-	case OverRightStories:
+	case Over::RightStories:
 		update(_stories
 			? _stories->sibling(Type::Right).layout.geometry
 			: QRect());
 		break;
-	case OverName: update(_nameNav); break;
-	case OverDate: update(_dateNav); break;
-	case OverSave: update(_saveNavOver); break;
-	case OverRotate: update(_rotateNavOver); break;
-	case OverIcon: update(_docIconRect); break;
-	case OverHeader: update(_headerNav); break;
-	case OverMore: update(_moreNavOver); break;
+	case Over::Name: update(_nameNav); break;
+	case Over::Date: update(_dateNav); break;
+	case Over::Save: update(_saveNavOver); break;
+	case Over::Share: update(_shareNavOver); break;
+	case Over::Rotate: update(_rotateNavOver); break;
+	case Over::Icon: update(_docIconRect); break;
+	case Over::Header: update(_headerNav); break;
+	case Over::More: update(_moreNavOver); break;
 	}
 }
 
-bool OverlayWidget::updateOverState(OverState newState) {
+bool OverlayWidget::updateOverState(Over newState) {
 	bool result = true;
 	if (_over != newState) {
-		if (newState == OverMore && !_ignoringDropdown) {
+		if (newState == Over::More && !_ignoringDropdown) {
 			_dropdownShowTimer.callOnce(0);
 		} else {
 			_dropdownShowTimer.cancel();
 		}
 		updateOverRect(_over);
 		updateOverRect(newState);
-		if (_over != OverNone) {
+		if (_over != Over::None) {
 			_animations[_over] = crl::now();
 			const auto i = _animationOpacities.find(_over);
 			if (i != end(_animationOpacities)) {
@@ -5330,7 +5353,7 @@ bool OverlayWidget::updateOverState(OverState newState) {
 			result = false;
 		}
 		_over = newState;
-		if (newState != OverNone) {
+		if (newState != Over::None) {
 			_animations[_over] = crl::now();
 			const auto i = _animationOpacities.find(_over);
 			if (i != end(_animationOpacities)) {
@@ -5382,52 +5405,54 @@ void OverlayWidget::updateOver(QPoint pos) {
 
 	using SiblingType = Stories::SiblingType;
 	if (_fullScreenVideo) {
-		updateOverState(OverVideo);
+		updateOverState(Over::Video);
 	} else if (_leftNavVisible && _leftNav.contains(pos)) {
-		updateOverState(OverLeftNav);
+		updateOverState(Over::Left);
 	} else if (_rightNavVisible && _rightNav.contains(pos)) {
-		updateOverState(OverRightNav);
+		updateOverState(Over::Right);
 	} else if (_stories
 		&& _stories->sibling(
 			SiblingType::Left).layout.geometry.contains(pos)) {
-		updateOverState(OverLeftStories);
+		updateOverState(Over::LeftStories);
 	} else if (_stories
 		&& _stories->sibling(
 			SiblingType::Right).layout.geometry.contains(pos)) {
-		updateOverState(OverRightStories);
+		updateOverState(Over::RightStories);
 	} else if (!_stories && _from && _nameNav.contains(pos)) {
-		updateOverState(OverName);
+		updateOverState(Over::Name);
 	} else if (!_stories
 		&& _message
 		&& _message->isRegular()
 		&& _dateNav.contains(pos)) {
-		updateOverState(OverDate);
+		updateOverState(Over::Date);
 	} else if (!_stories && _headerHasLink && _headerNav.contains(pos)) {
-		updateOverState(OverHeader);
+		updateOverState(Over::Header);
 	} else if (_saveVisible && _saveNav.contains(pos)) {
-		updateOverState(OverSave);
+		updateOverState(Over::Save);
+	} else if (_shareVisible && _shareNav.contains(pos)) {
+		updateOverState(Over::Share);
 	} else if (_rotateVisible && _rotateNav.contains(pos)) {
-		updateOverState(OverRotate);
+		updateOverState(Over::Rotate);
 	} else if (_document
 		&& documentBubbleShown()
 		&& _docIconRect.contains(pos)) {
-		updateOverState(OverIcon);
+		updateOverState(Over::Icon);
 	} else if (_moreNav.contains(pos)) {
-		updateOverState(OverMore);
+		updateOverState(Over::More);
 	} else if (contentShown() && finalContentRect().contains(pos)) {
 		if (_stories) {
-			updateOverState(OverVideo);
+			updateOverState(Over::Video);
 		} else if (_streamed
 			&& _document
 			&& (_document->isVideoFile() || _document->isVideoMessage())) {
-			updateOverState(OverVideo);
+			updateOverState(Over::Video);
 		} else if (!_streamed && _document && !_documentMedia->loaded()) {
-			updateOverState(OverIcon);
-		} else if (_over != OverNone) {
-			updateOverState(OverNone);
+			updateOverState(Over::Icon);
+		} else if (_over != Over::None) {
+			updateOverState(Over::None);
 		}
-	} else if (_over != OverNone) {
-		updateOverState(OverNone);
+	} else if (_over != Over::None) {
+		updateOverState(Over::None);
 	}
 }
 
@@ -5476,7 +5501,7 @@ void OverlayWidget::handleMouseRelease(
 		return;
 	}
 
-	if (_over == OverName && _down == OverName) {
+	if (_over == Over::Name && _down == Over::Name) {
 		if (_from) {
 			if (!_windowed) {
 				close();
@@ -5486,19 +5511,21 @@ void OverlayWidget::handleMouseRelease(
 				window->window().activate();
 			}
 		}
-	} else if (_over == OverDate && _down == OverDate) {
+	} else if (_over == Over::Date && _down == Over::Date) {
 		toMessage();
-	} else if (_over == OverHeader && _down == OverHeader) {
+	} else if (_over == Over::Header && _down == Over::Header) {
 		showMediaOverview();
-	} else if (_over == OverSave && _down == OverSave) {
+	} else if (_over == Over::Save && _down == Over::Save) {
 		downloadMedia();
-	} else if (_over == OverRotate && _down == OverRotate) {
+	} else if (_over == Over::Share && _down == Over::Share && _stories) {
+		_stories->share();
+	} else if (_over == Over::Rotate && _down == Over::Rotate) {
 		playbackControlsRotate();
-	} else if (_over == OverIcon && _down == OverIcon) {
+	} else if (_over == Over::Icon && _down == Over::Icon) {
 		handleDocumentClick();
-	} else if (_over == OverMore && _down == OverMore) {
+	} else if (_over == Over::More && _down == Over::More) {
 		InvokeQueued(_widget, [=] { showDropdown(); });
-	} else if (_over == OverVideo && _down == OverVideo) {
+	} else if (_over == Over::Video && _down == Over::Video) {
 		if (_stories) {
 			_stories->contentPressed(false);
 		} else if (_streamed) {
@@ -5530,7 +5557,7 @@ void OverlayWidget::handleMouseRelease(
 		}
 		_pressed = false;
 	}
-	_down = OverNone;
+	_down = Over::None;
 	if (!isHidden()) {
 		activateControls();
 	}
@@ -5921,7 +5948,7 @@ void OverlayWidget::updateHeader() {
 	_headerNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewHeaderTop, hwidth, st::mediaviewThickFont->height);
 }
 
-float64 OverlayWidget::overLevel(OverState control) const {
+float64 OverlayWidget::overLevel(Over control) const {
 	auto i = _animationOpacities.find(control);
 	return (i == end(_animationOpacities))
 		? (_over == control ? 1. : 0.)
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index 01cc71f3d..d323a640e 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -109,7 +109,7 @@ public:
 
 	//void leaveToChildEvent(QEvent *e, QWidget *child) override {
 	//	// e -- from enterEvent() of child TWidget
-	//	updateOverState(OverNone);
+	//	updateOverState(Over::None);
 	//}
 	//void enterFromChildEvent(QEvent *e, QWidget *child) override {
 	//	// e -- from leaveEvent() of child TWidget
@@ -143,21 +143,22 @@ private:
 	class RendererGL;
 
 	// If changing, see paintControls()!
-	enum OverState {
-		OverNone,
-		OverLeftNav,
-		OverRightNav,
-		OverLeftStories,
-		OverRightStories,
-		OverHeader,
-		OverName,
-		OverDate,
-		OverSave,
-		OverRotate,
-		OverMore,
-		OverIcon,
-		OverVideo,
-		OverCaption,
+	enum class Over {
+		None,
+		Left,
+		Right,
+		LeftStories,
+		RightStories,
+		Header,
+		Name,
+		Date,
+		Save,
+		Share,
+		Rotate,
+		More,
+		Icon,
+		Video,
+		Caption,
 	};
 	struct Entity {
 		std::variant<
@@ -471,9 +472,9 @@ private:
 		bool nonbright = false) const;
 	[[nodiscard]] bool isSaveMsgShown() const;
 
-	void updateOverRect(OverState state);
-	bool updateOverState(OverState newState);
-	float64 overLevel(OverState control) const;
+	void updateOverRect(Over state);
+	bool updateOverState(Over newState);
+	float64 overLevel(Over control) const;
 
 	void checkGroupThumbsAnimation();
 	void initGroupThumbs();
@@ -549,11 +550,13 @@ private:
 	QRect _rightNav, _rightNavOver, _rightNavIcon;
 	QRect _headerNav, _nameNav, _dateNav;
 	QRect _rotateNav, _rotateNavOver, _rotateNavIcon;
+	QRect _shareNav, _shareNavOver, _shareNavIcon;
 	QRect _saveNav, _saveNavOver, _saveNavIcon;
 	QRect _moreNav, _moreNavOver, _moreNavIcon;
 	bool _leftNavVisible = false;
 	bool _rightNavVisible = false;
 	bool _saveVisible = false;
+	bool _shareVisible = false;
 	bool _rotateVisible = false;
 	bool _headerHasLink = false;
 	QString _dateText;
@@ -653,8 +656,8 @@ private:
 
 	mtpRequestId _loadRequest = 0;
 
-	OverState _over = OverNone;
-	OverState _down = OverNone;
+	Over _over = Over::None;
+	Over _down = Over::None;
 	QPoint _lastAction, _lastMouseMovePos;
 	bool _ignoringDropdown = false;
 
@@ -693,8 +696,8 @@ private:
 	Ui::Animations::Simple _saveMsgAnimation;
 	base::Timer _saveMsgTimer;
 
-	base::flat_map<OverState, crl::time> _animations;
-	base::flat_map<OverState, anim::value> _animationOpacities;
+	base::flat_map<Over, crl::time> _animations;
+	base::flat_map<Over, anim::value> _animationOpacities;
 
 	rpl::event_stream<Media::Player::TrackState> _touchbarTrackState;
 	rpl::event_stream<TouchBarItemType> _touchbarDisplay;

From d7186e68e276d2e59ec2800ed4b4a0cef8933b7d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 12 Jun 2023 21:06:28 +0400
Subject: [PATCH 074/259] Open stories by t.me/username?story=id links.

---
 Telegram/SourceFiles/core/local_url_handlers.cpp     |  2 ++
 .../SourceFiles/window/window_session_controller.cpp | 12 ++++++++++++
 .../SourceFiles/window/window_session_controller.h   |  1 +
 3 files changed, 15 insertions(+)

diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 1e6cda40f..2d013e612 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -383,6 +383,7 @@ bool ResolveUsernameOrPhone(
 	if (const auto postId = postParam.toInt()) {
 		post = postId;
 	}
+	const auto storyId = params.value(u"story"_q).toInt();
 	const auto appname = params.value(u"appname"_q);
 	const auto appstart = params.value(u"startapp"_q);
 	const auto commentParam = params.value(u"comment"_q);
@@ -408,6 +409,7 @@ bool ResolveUsernameOrPhone(
 		.usernameOrId = domain,
 		.phone = phone,
 		.messageId = post,
+		.storyId = storyId,
 		.repliesInfo = commentId
 			? Navigation::RepliesByLinkInfo{
 				Navigation::CommentId{ commentId }
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index d6214affc..0e3277348 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -503,6 +503,18 @@ void SessionNavigation::showPeerByLinkResolved(
 				info.messageId,
 				callback);
 		}
+	} else if (peer->isUser() && info.storyId) {
+		const auto storyId = FullStoryId{ peer->id, info.storyId };
+		peer->owner().stories().resolve(storyId, crl::guard(this, [=] {
+			if (peer->owner().stories().lookup(storyId)) {
+				parentController()->openPeerStory(
+					peer,
+					storyId.story,
+					Data::StoriesContext{ Data::StoriesContextSingle() });
+			} else {
+				showToast(tr::lng_confirm_phone_link_invalid(tr::now));
+			}
+		}));
 	} else if (bot && resolveType == ResolveType::BotApp) {
 		const auto itemId = info.clickFromMessageId;
 		const auto item = _session->data().message(itemId);
diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h
index f92f7d98f..1fb50ac2a 100644
--- a/Telegram/SourceFiles/window/window_session_controller.h
+++ b/Telegram/SourceFiles/window/window_session_controller.h
@@ -204,6 +204,7 @@ public:
 		std::variant<QString, ChannelId> usernameOrId;
 		QString phone;
 		MsgId messageId = ShowAtUnreadMsgId;
+		StoryId storyId = 0;
 		RepliesByLinkInfo repliesInfo;
 		ResolveType resolveType = ResolveType::Default;
 		QString startToken;

From c133f4de6901fd3cc45e81ec6c7073db3805392c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 12 Jun 2023 22:37:17 +0400
Subject: [PATCH 075/259] Display shared stories in message history.

---
 Telegram/Resources/langs/lang.strings         |  3 +
 .../SourceFiles/data/data_media_types.cpp     | 84 +++++++++++++++++++
 Telegram/SourceFiles/data/data_media_types.h  | 26 ++++++
 Telegram/SourceFiles/history/history_item.cpp | 25 +++++-
 Telegram/SourceFiles/history/history_item.h   |  1 +
 .../history/history_item_components.h         |  1 +
 .../history/view/history_view_element.cpp     | 23 +++--
 .../history/view/media/history_view_gif.cpp   |  9 +-
 .../history/view/media/history_view_gif.h     |  5 +-
 .../history/view/media/history_view_photo.cpp | 39 +++++++--
 .../history/view/media/history_view_photo.h   |  5 +-
 .../window/window_session_controller.cpp      | 46 +++++-----
 .../window/window_session_controller.h        |  5 +-
 13 files changed, 229 insertions(+), 43 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 48c5590ad..fdd35ffdd 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1503,6 +1503,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_action_pinned_media_sticker" = "a sticker";
 "lng_action_pinned_media_emoji_sticker" = "a {emoji} sticker";
 "lng_action_pinned_media_game" = "the game «{game}»";
+"lng_action_pinned_media_story" = "a story";
 "lng_action_game_score#one" = "{from} scored {count} in {game}";
 "lng_action_game_score#other" = "{from} scored {count} in {game}";
 "lng_action_game_you_scored#one" = "You scored {count} in {game}";
@@ -3810,6 +3811,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_archive_button" = "Archive";
 "lng_stories_archive_title" = "Stories Archive";
 
+"lng_stories_link_invalid" = "This link is broken or has expired.";
+
 // Wnd specific
 
 "lng_wnd_choose_program_menu" = "Choose Default Program...";
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 4e311e1f1..5a13cf2d5 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -56,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_poll.h"
 #include "data/data_channel.h"
 #include "data/data_file_origin.h"
+#include "data/data_stories.h"
 #include "main/main_session.h"
 #include "main/main_session_settings.h"
 #include "core/application.h"
@@ -72,6 +73,7 @@ namespace {
 
 constexpr auto kFastRevokeRestriction = 24 * 60 * TimeId(60);
 constexpr auto kMaxPreviewImages = 3;
+constexpr auto kLoadingStoryPhotoId = PhotoId(0x7FFF'DEAD'FFFF'FFFFULL);
 
 using ItemPreview = HistoryView::ItemPreview;
 using ItemPreviewImage = HistoryView::ItemPreviewImage;
@@ -404,6 +406,10 @@ const WallPaper *Media::paper() const {
 	return nullptr;
 }
 
+FullStoryId Media::storyId() const {
+	return {};
+}
+
 bool Media::uploading() const {
 	return false;
 }
@@ -1968,4 +1974,82 @@ std::unique_ptr<HistoryView::Media> MediaWallPaper::createView(
 		std::make_unique<HistoryView::ThemeDocumentBox>(message, _paper));
 }
 
+MediaStory::MediaStory(not_null<HistoryItem*> parent, FullStoryId storyId)
+: Media(parent)
+, _storyId(storyId) {
+	const auto stories = &parent->history()->owner().stories();
+	if (!stories->lookup(storyId)) {
+		stories->resolve(storyId, crl::guard(this, [=] {
+			if (stories->lookup(storyId)) {
+				parent->history()->owner().requestItemViewRefresh(parent);
+			}
+		}));
+	}
+}
+
+std::unique_ptr<Media> MediaStory::clone(not_null<HistoryItem*> parent) {
+	return std::make_unique<MediaStory>(parent, _storyId);
+}
+
+FullStoryId MediaStory::storyId() const {
+	return _storyId;
+}
+
+TextWithEntities MediaStory::notificationText() const {
+	const auto stories = &parent()->history()->owner().stories();
+	const auto maybeStory = stories->lookup(_storyId);
+	return WithCaptionNotificationText(
+		tr::lng_in_dlg_story(tr::now),
+		(maybeStory
+			? (*maybeStory)->caption()
+			: TextWithEntities()));
+}
+
+QString MediaStory::pinnedTextSubstring() const {
+	return tr::lng_action_pinned_media_story(tr::now);
+}
+
+TextForMimeData MediaStory::clipboardText() const {
+	return WithCaptionClipboardText(
+		tr::lng_in_dlg_story(tr::now),
+		parent()->clipboardText());
+}
+
+bool MediaStory::updateInlineResultMedia(const MTPMessageMedia &media) {
+	return false;
+}
+
+bool MediaStory::updateSentMedia(const MTPMessageMedia &media) {
+	return false;
+}
+
+std::unique_ptr<HistoryView::Media> MediaStory::createView(
+		not_null<HistoryView::Element*> message,
+		not_null<HistoryItem*> realParent,
+		HistoryView::Element *replacing) {
+	const auto spoiler = false;
+	const auto stories = &parent()->history()->owner().stories();
+	const auto maybeStory = stories->lookup(_storyId);
+	if (const auto story = maybeStory ? maybeStory->get() : nullptr) {
+		if (const auto photo = story->photo()) {
+			return std::make_unique<HistoryView::Photo>(
+				message,
+				realParent,
+				photo,
+				spoiler);
+		} else {
+			return std::make_unique<HistoryView::Gif>(
+				message,
+				realParent,
+				story->document(),
+				spoiler);
+		}
+	}
+	return std::make_unique<HistoryView::Photo>(
+		message,
+		realParent,
+		realParent->history()->owner().photo(kLoadingStoryPhotoId),
+		spoiler);
+}
+
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 985e2c8aa..614d0333a 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include "base/weak_ptr.h"
 #include "data/data_location.h"
 #include "data/data_wall_paper.h"
 
@@ -110,6 +111,7 @@ public:
 	virtual CloudImage *location() const;
 	virtual PollData *poll() const;
 	virtual const WallPaper *paper() const;
+	virtual FullStoryId storyId() const;
 
 	virtual bool uploading() const;
 	virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;
@@ -563,6 +565,30 @@ private:
 
 };
 
+class MediaStory final : public Media, public base::has_weak_ptr {
+public:
+	MediaStory(not_null<HistoryItem*> parent, FullStoryId storyId);
+
+	std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
+
+	[[nodiscard]] FullStoryId storyId() const override;
+
+	TextWithEntities notificationText() const override;
+	QString pinnedTextSubstring() const override;
+	TextForMimeData clipboardText() const override;
+
+	bool updateInlineResultMedia(const MTPMessageMedia &media) override;
+	bool updateSentMedia(const MTPMessageMedia &media) override;
+	std::unique_ptr<HistoryView::Media> createView(
+		not_null<HistoryView::Element*> message,
+		not_null<HistoryItem*> realParent,
+		HistoryView::Element *replacing = nullptr) override;
+
+private:
+	const FullStoryId _storyId;
+
+};
+
 [[nodiscard]] TextForMimeData WithCaptionClipboardText(
 	const QString &attachType,
 	TextForMimeData &&caption);
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index e886b7ed7..2e73286a8 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -291,7 +291,10 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
 			qs(media.vemoticon()),
 			media.vvalue().v);
 	}, [&](const MTPDmessageMediaStory &media) -> Result {
-		return nullptr; // #TODO stories
+		return std::make_unique<Data::MediaStory>(item, FullStoryId{
+			peerFromUser(media.vuser_id()),
+			media.vid().v,
+		});
 	}, [](const MTPDmessageMediaEmpty &) -> Result {
 		return nullptr;
 	}, [](const MTPDmessageMediaUnsupported &) -> Result {
@@ -3594,9 +3597,29 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
 
 void HistoryItem::setMedia(const MTPMessageMedia &media) {
 	_media = CreateMedia(this, media);
+	checkStoryForwardInfo();
 	checkBuyButton();
 }
 
+void HistoryItem::checkStoryForwardInfo() {
+	if (const auto storyId = _media ? _media->storyId() : FullStoryId()) {
+		const auto adding = !Has<HistoryMessageForwarded>();
+		if (adding) {
+			AddComponents(HistoryMessageForwarded::Bit());
+		}
+		const auto forwarded = Get<HistoryMessageForwarded>();
+		if (forwarded->story || adding) {
+			const auto peer = history()->owner().peer(storyId.peer);
+			forwarded->story = true;
+			forwarded->originalSender = peer;
+		}
+	} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
+		if (forwarded->story) {
+			RemoveComponents(HistoryMessageForwarded::Bit());
+		}
+	}
+}
+
 void HistoryItem::applyServiceDateEdition(const MTPDmessageService &data) {
 	const auto date = data.vdate().v;
 	if (_date == date) {
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index c2f26fb44..a5bbb3ced 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -192,6 +192,7 @@ public:
 	[[nodiscard]] MsgId dependencyMsgId() const;
 	[[nodiscard]] bool notificationReady() const;
 	[[nodiscard]] PeerData *specialNotificationPeer() const;
+	void checkStoryForwardInfo();
 	void checkBuyButton();
 
 	void updateServiceText(PreparedServiceText &&text);
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 99100f6b0..57d38514e 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -129,6 +129,7 @@ struct HistoryMessageForwarded : public RuntimeComponent<HistoryMessageForwarded
 	PeerData *savedFromPeer = nullptr;
 	MsgId savedFromMsgId = 0;
 	bool imported = false;
+	bool story = false;
 };
 
 struct HistoryMessageSponsored : public RuntimeComponent<HistoryMessageSponsored, HistoryItem> {
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index d6e7c4099..4d734bfcb 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -248,6 +248,7 @@ TextSelection ShiftItemSelection(
 QString DateTooltipText(not_null<Element*> view) {
 	const auto locale = QLocale();
 	const auto format = QLocale::LongFormat;
+	const auto item = view->data();
 	auto dateText = locale.toString(view->dateTime(), format);
 	if (const auto editedDate = view->displayedEditDate()) {
 		dateText += '\n' + tr::lng_edited_date(
@@ -255,18 +256,22 @@ QString DateTooltipText(not_null<Element*> view) {
 			lt_date,
 			locale.toString(base::unixtime::parse(editedDate), format));
 	}
-	if (const auto forwarded = view->data()->Get<HistoryMessageForwarded>()) {
-		dateText += '\n' + tr::lng_forwarded_date(
-			tr::now,
-			lt_date,
-			locale.toString(base::unixtime::parse(forwarded->originalDate), format));
-		if (forwarded->imported) {
-			dateText = tr::lng_forwarded_imported(tr::now)
-				+ "\n\n" + dateText;
+	if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
+		if (!forwarded->story && forwarded->psaType.isEmpty()) {
+			dateText += '\n' + tr::lng_forwarded_date(
+				tr::now,
+				lt_date,
+				locale.toString(
+					base::unixtime::parse(forwarded->originalDate),
+					format));
+			if (forwarded->imported) {
+				dateText = tr::lng_forwarded_imported(tr::now)
+					+ "\n\n" + dateText;
+			}
 		}
 	}
 	if (view->isSignedAuthorElided()) {
-		if (const auto msgsigned = view->data()->Get<HistoryMessageSigned>()) {
+		if (const auto msgsigned = item->Get<HistoryMessageSigned>()) {
 			dateText += '\n'
 				+ tr::lng_signed_author(tr::now, lt_user, msgsigned->author);
 		}
diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
index 67e4c8f3f..e1c78f8b5 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
@@ -90,6 +90,11 @@ Gif::Gif(
 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right())
 , _spoiler(spoiler ? std::make_unique<MediaSpoiler>() : nullptr)
 , _downloadSize(Ui::FormatSizeText(_data->size)) {
+	if (const auto media = realParent->media()) {
+		if (media->storyId()) {
+			_story = true;
+		}
+	}
 	setDocumentLinks(_data, realParent, [=] {
 		if (!_data->createMediaView()->canBePlayed(realParent)
 			|| !_data->isAnimation()
@@ -1441,7 +1446,9 @@ void Gif::hideSpoilers() {
 }
 
 bool Gif::needsBubble() const {
-	if (_data->isVideoMessage()) {
+	if (_story) {
+		return true;
+	} else if (_data->isVideoMessage()) {
 		return false;
 	} else if (!_caption.isEmpty()) {
 		return true;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h
index 7900f9e46..6a8fca3b5 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_gif.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h
@@ -219,8 +219,9 @@ private:
 	mutable QImage _thumbCache;
 	mutable QImage _roundingMask;
 	mutable std::optional<Ui::BubbleRounding> _thumbCacheRounding;
-	mutable bool _thumbCacheBlurred = false;
-	mutable bool _thumbIsEllipse = false;
+	mutable bool _thumbCacheBlurred : 1 = false;
+	mutable bool _thumbIsEllipse : 1 = false;
+	mutable bool _story : 1 = false;
 
 };
 
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
index 78c4be6f3..efa945cc4 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
@@ -40,6 +40,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace HistoryView {
 namespace {
 
+constexpr auto kStoryWidth = 720;
+constexpr auto kStoryHeight = 1280;
+
 using Data::PhotoSize;
 
 } // namespace
@@ -67,6 +70,11 @@ Photo::Photo(
 , _data(photo)
 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right())
 , _spoiler(spoiler ? std::make_unique<MediaSpoiler>() : nullptr) {
+	if (const auto media = realParent->media()) {
+		if (media->storyId()) {
+			_story = true;
+		}
+	}
 	_caption = createCaption(realParent);
 	create(realParent->fullId());
 }
@@ -167,7 +175,7 @@ QSize Photo::countOptimalSize() {
 			_parent->skipBlockHeight());
 	}
 
-	const auto dimensions = QSize(_data->width(), _data->height());
+	const auto dimensions = photoSize();
 	const auto scaled = CountDesiredMediaSize(dimensions);
 	const auto minWidth = std::clamp(
 		_parent->minWidthForMedia(),
@@ -210,7 +218,7 @@ QSize Photo::countCurrentSize(int newWidth) {
 			? st::historyPhotoBubbleMinWidth
 			: st::minPhotoSize),
 		thumbMaxWidth);
-	const auto dimensions = QSize(_data->width(), _data->height());
+	const auto dimensions = photoSize();
 	auto pix = CountPhotoMediaSize(
 		CountDesiredMediaSize(dimensions),
 		newWidth,
@@ -255,7 +263,11 @@ int Photo::adjustHeightForLessCrop(QSize dimensions, QSize current) const {
 }
 
 void Photo::draw(Painter &p, const PaintContext &context) const {
-	if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
+	if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
+		return;
+	} else if (_story && _data->isNull()) {
+		return;
+	}
 
 	ensureDataMediaCreated();
 	_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
@@ -590,11 +602,20 @@ void Photo::paintUserpicFrame(
 	}
 }
 
+QSize Photo::photoSize() const {
+	if (_story) {
+		return { kStoryWidth, kStoryHeight };
+	}
+	return QSize(_data->width(), _data->height());
+}
+
 TextState Photo::textState(QPoint point, StateRequest request) const {
 	auto result = TextState(_parent);
 
 	if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
 		return result;
+	} else if (_story && _data->isNull()) {
+		return result;
 	}
 	auto paintx = 0, painty = 0, paintw = width(), painth = height();
 	auto bubble = _parent->hasBubble();
@@ -657,9 +678,8 @@ TextState Photo::textState(QPoint point, StateRequest request) const {
 }
 
 QSize Photo::sizeForGroupingOptimal(int maxWidth) const {
-	const auto width = _data->width();
-	const auto height = _data->height();
-	return { std::max(width, 1), std::max(height, 1) };
+	const auto size = photoSize();
+	return { std::max(size.width(), 1), std::max(size.height(), 1)};
 }
 
 QSize Photo::sizeForGrouping(int width) const {
@@ -848,8 +868,9 @@ void Photo::validateGroupedCache(
 		return;
 	}
 
-	const auto originalWidth = style::ConvertScale(_data->width());
-	const auto originalHeight = style::ConvertScale(_data->height());
+	const auto unscaled = photoSize();
+	const auto originalWidth = style::ConvertScale(unscaled.width());
+	const auto originalHeight = style::ConvertScale(unscaled.height());
 	const auto pixSize = Ui::GetImageScaleSizeForGeometry(
 		{ originalWidth, originalHeight },
 		{ width, height });
@@ -1012,7 +1033,7 @@ void Photo::hideSpoilers() {
 }
 
 bool Photo::needsBubble() const {
-	if (!_caption.isEmpty()) {
+	if (_story || !_caption.isEmpty()) {
 		return true;
 	}
 	const auto item = _parent->data();
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h
index 9440d1421..31d2b08bc 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h
@@ -158,6 +158,8 @@ private:
 		const PaintContext &context,
 		QPoint photoPosition) const;
 
+	[[nodiscard]] QSize photoSize() const;
+
 	const not_null<PhotoData*> _data;
 	Ui::Text::String _caption;
 	mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
@@ -165,9 +167,10 @@ private:
 	const std::unique_ptr<MediaSpoiler> _spoiler;
 	mutable QImage _imageCache;
 	mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
-	int _serviceWidth : 30 = 0;
+	int _serviceWidth : 29 = 0;
 	mutable int _imageCacheForum : 1 = 0;
 	mutable int _imageCacheBlurred : 1 = 0;
+	mutable int _story : 1 = 0;
 
 };
 
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index 0e3277348..2d447621c 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -512,7 +512,7 @@ void SessionNavigation::showPeerByLinkResolved(
 					storyId.story,
 					Data::StoriesContext{ Data::StoriesContextSingle() });
 			} else {
-				showToast(tr::lng_confirm_phone_link_invalid(tr::now));
+				showToast(tr::lng_stories_link_invalid(tr::now));
 			}
 		}));
 	} else if (bot && resolveType == ResolveType::BotApp) {
@@ -2155,14 +2155,12 @@ void SessionController::openPhoto(
 		not_null<PhotoData*> photo,
 		FullMsgId contextId,
 		MsgId topicRootId) {
-	if (openStory(contextId)) {
+	const auto item = session().data().message(contextId);
+	if (openSharedStory(item) || openFakeItemStory(contextId)) {
 		return;
 	}
-	_window->openInMediaView(Media::View::OpenRequest(
-		this,
-		photo,
-		session().data().message(contextId),
-		topicRootId));
+	_window->openInMediaView(
+		Media::View::OpenRequest(this, photo, item, topicRootId));
 }
 
 void SessionController::openPhoto(
@@ -2176,24 +2174,34 @@ void SessionController::openDocument(
 		FullMsgId contextId,
 		MsgId topicRootId,
 		bool showInMediaView) {
-	if (openStory(contextId)) {
+	const auto item = session().data().message(contextId);
+	if (openSharedStory(item) || openFakeItemStory(contextId)) {
 		return;
 	} else if (showInMediaView) {
-		_window->openInMediaView(Media::View::OpenRequest(
-			this,
-			document,
-			session().data().message(contextId),
-			topicRootId));
+		_window->openInMediaView(
+			Media::View::OpenRequest(this, document, item, topicRootId));
 		return;
 	}
-	Data::ResolveDocument(
-		this,
-		document,
-		session().data().message(contextId),
-		topicRootId);
+	Data::ResolveDocument(this, document, item, topicRootId);
 }
 
-bool SessionController::openStory(
+bool SessionController::openSharedStory(HistoryItem *item) {
+	if (const auto media = item ? item->media() : nullptr) {
+		if (const auto storyId = media->storyId()) {
+			const auto story = session().data().stories().lookup(storyId);
+			if (story) {
+				_window->openInMediaView(::Media::View::OpenRequest(
+					this,
+					*story,
+					Data::StoriesContext{ Data::StoriesContextSingle() }));
+			}
+			return true;
+		}
+	}
+	return false;
+}
+
+bool SessionController::openFakeItemStory(
 		FullMsgId fakeItemId,
 		bool forceArchiveContext) {
 	if (!peerIsUser(fakeItemId.peer)
diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h
index 1fb50ac2a..845cdb223 100644
--- a/Telegram/SourceFiles/window/window_session_controller.h
+++ b/Telegram/SourceFiles/window/window_session_controller.h
@@ -492,7 +492,10 @@ public:
 		FullMsgId contextId,
 		MsgId topicRootId,
 		bool showInMediaView = false);
-	bool openStory(FullMsgId fakeItemId, bool forceArchiveContext = false);
+	bool openSharedStory(HistoryItem *item);
+	bool openFakeItemStory(
+		FullMsgId fakeItemId,
+		bool forceArchiveContext = false);
 
 	void showChooseReportMessages(
 		not_null<PeerData*> peer,

From 1c41df364ccaf08dc4f8100548b7702dc4df4e69 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 13 Jun 2023 13:36:17 +0400
Subject: [PATCH 076/259] Improved shared story layout.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 .../SourceFiles/data/data_media_types.cpp     | 69 ++++++++++++-------
 Telegram/SourceFiles/data/data_media_types.h  |  6 +-
 Telegram/SourceFiles/data/data_stories.cpp    |  5 +-
 .../history/history_item_components.cpp       |  8 ++-
 .../history/view/history_view_element.cpp     | 54 ++++++++-------
 .../history/view/history_view_element.h       |  3 +
 .../history/view/history_view_message.cpp     | 15 ++--
 .../history/view/media/history_view_photo.cpp |  2 +-
 9 files changed, 107 insertions(+), 56 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index fdd35ffdd..9a0a660b4 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1699,6 +1699,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_context_about_private_link" = "This link will only work for members of this chat.";
 
 "lng_forwarded" = "Forwarded from {user}";
+"lng_forwarded_story" = "Story from {user}";
 "lng_forwarded_date" = "Original: {date}";
 "lng_forwarded_channel" = "Forwarded from {channel}";
 "lng_forwarded_psa_default" = "Forwarded from {channel}";
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 5a13cf2d5..cd9d863dc 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -410,6 +410,10 @@ FullStoryId Media::storyId() const {
 	return {};
 }
 
+bool Media::storyExpired() const {
+	return false;
+}
+
 bool Media::uploading() const {
 	return false;
 }
@@ -1978,12 +1982,16 @@ MediaStory::MediaStory(not_null<HistoryItem*> parent, FullStoryId storyId)
 : Media(parent)
 , _storyId(storyId) {
 	const auto stories = &parent->history()->owner().stories();
-	if (!stories->lookup(storyId)) {
-		stories->resolve(storyId, crl::guard(this, [=] {
-			if (stories->lookup(storyId)) {
+	const auto maybeStory = stories->lookup(storyId);
+	if (!maybeStory) {
+		if (maybeStory.error() == NoStory::Unknown) {
+			stories->resolve(storyId, crl::guard(this, [=] {
+				_expired = !stories->lookup(storyId);
 				parent->history()->owner().requestItemViewRefresh(parent);
-			}
-		}));
+			}));
+		} else {
+			_expired = true;
+		}
 	}
 }
 
@@ -1995,6 +2003,10 @@ FullStoryId MediaStory::storyId() const {
 	return _storyId;
 }
 
+bool MediaStory::storyExpired() const {
+	return _expired;
+}
+
 TextWithEntities MediaStory::notificationText() const {
 	const auto stories = &parent()->history()->owner().stories();
 	const auto maybeStory = stories->lookup(_storyId);
@@ -2015,6 +2027,10 @@ TextForMimeData MediaStory::clipboardText() const {
 		parent()->clipboardText());
 }
 
+bool MediaStory::dropForwardedInfo() const {
+	return true;
+}
+
 bool MediaStory::updateInlineResultMedia(const MTPMessageMedia &media) {
 	return false;
 }
@@ -2030,26 +2046,33 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
 	const auto spoiler = false;
 	const auto stories = &parent()->history()->owner().stories();
 	const auto maybeStory = stories->lookup(_storyId);
-	if (const auto story = maybeStory ? maybeStory->get() : nullptr) {
-		if (const auto photo = story->photo()) {
-			return std::make_unique<HistoryView::Photo>(
-				message,
-				realParent,
-				photo,
-				spoiler);
-		} else {
-			return std::make_unique<HistoryView::Gif>(
-				message,
-				realParent,
-				story->document(),
-				spoiler);
+	if (!maybeStory) {
+		if (maybeStory.error() == Data::NoStory::Deleted) {
+			_expired = true;
+			return nullptr;
 		}
+		_expired = false;
+		return std::make_unique<HistoryView::Photo>(
+			message,
+			realParent,
+			realParent->history()->owner().photo(kLoadingStoryPhotoId),
+			spoiler);
+	}
+	_expired = false;
+	const auto story = *maybeStory;
+	if (const auto photo = story->photo()) {
+		return std::make_unique<HistoryView::Photo>(
+			message,
+			realParent,
+			photo,
+			spoiler);
+	} else {
+		return std::make_unique<HistoryView::Gif>(
+			message,
+			realParent,
+			story->document(),
+			spoiler);
 	}
-	return std::make_unique<HistoryView::Photo>(
-		message,
-		realParent,
-		realParent->history()->owner().photo(kLoadingStoryPhotoId),
-		spoiler);
 }
 
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 614d0333a..0ba84e928 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -112,6 +112,7 @@ public:
 	virtual PollData *poll() const;
 	virtual const WallPaper *paper() const;
 	virtual FullStoryId storyId() const;
+	virtual bool storyExpired() const;
 
 	virtual bool uploading() const;
 	virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;
@@ -571,11 +572,13 @@ public:
 
 	std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
 
-	[[nodiscard]] FullStoryId storyId() const override;
+	FullStoryId storyId() const override;
+	bool storyExpired() const override;
 
 	TextWithEntities notificationText() const override;
 	QString pinnedTextSubstring() const override;
 	TextForMimeData clipboardText() const override;
+	bool dropForwardedInfo() const override;
 
 	bool updateInlineResultMedia(const MTPMessageMedia &media) override;
 	bool updateSentMedia(const MTPMessageMedia &media) override;
@@ -586,6 +589,7 @@ public:
 
 private:
 	const FullStoryId _storyId;
+	bool _expired = false;
 
 };
 
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 0d2e64bfa..3952f9712 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -742,11 +742,12 @@ void Stories::sendResolveRequests() {
 	auto leftToSend = kMaxResolveTogether;
 	auto byPeer = base::flat_map<PeerId, QVector<MTPint>>();
 	for (auto i = begin(_resolvePending); i != end(_resolvePending);) {
-		auto &[peerId, ids] = *i;
+		const auto peerId = i->first;
+		auto &ids = i->second;
 		auto &sent = _resolveSent[peerId];
 		if (ids.size() <= leftToSend) {
 			sent = base::take(ids);
-			i = _resolvePending.erase(i);
+			i = _resolvePending.erase(i); // Invalidates `ids`.
 			leftToSend -= int(sent.size());
 		} else {
 			sent = {
diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp
index ce9a8edf2..05289283e 100644
--- a/Telegram/SourceFiles/history/history_item_components.cpp
+++ b/Telegram/SourceFiles/history/history_item_components.cpp
@@ -192,7 +192,13 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
 	} else {
 		phrase = name;
 	}
-	if (via && psaType.isEmpty()) {
+	if (story) {
+		phrase = tr::lng_forwarded_story(
+			tr::now,
+			lt_user,
+			Ui::Text::Link(phrase.text, QString()), // Link 1.
+			Ui::Text::WithEntities);
+	} else if (via && psaType.isEmpty()) {
 		if (fromChannel) {
 			phrase = tr::lng_forwarded_channel_via(
 				tr::now,
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index 4d734bfcb..763978823 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -839,14 +839,15 @@ auto Element::contextDependentServiceText() -> TextWithLinks {
 void Element::validateText() {
 	const auto item = data();
 	const auto &text = item->_text;
-	if (_text.isEmpty() == text.empty()) {
-		return;
-	}
-	const auto context = Core::MarkedTextContext{
-		.session = &history()->session(),
-		.customEmojiRepaint = [=] { customEmojiRepaint(); },
-	};
-	if (_flags & Flag::ServiceMessage) {
+	const auto media = item->media();
+	if (media && media->storyExpired()) {
+		_media = nullptr;
+		if (_text.isEmpty()) {
+			setTextWithLinks(
+				Ui::Text::Italic(u"This story has expired"_q));
+		}
+	} else if (_text.isEmpty() == text.empty()) {
+	} else if (_flags & Flag::ServiceMessage) {
 		const auto contextDependentText = contextDependentServiceText();
 		const auto &markedText = contextDependentText.text.empty()
 			? text
@@ -854,28 +855,33 @@ void Element::validateText() {
 		const auto &customLinks = contextDependentText.text.empty()
 			? item->customTextLinks()
 			: contextDependentText.links;
-		_text.setMarkedText(
-			st::serviceTextStyle,
-			markedText,
-			Ui::ItemTextServiceOptions(),
-			context);
+		setTextWithLinks(markedText, customLinks);
+	} else {
+		setTextWithLinks(item->translatedTextWithLocalEntities());
+	}
+}
+
+void Element::setTextWithLinks(
+		const TextWithEntities &text,
+		const std::vector<ClickHandlerPtr> &links) {
+	const auto context = Core::MarkedTextContext{
+		.session = &history()->session(),
+		.customEmojiRepaint = [=] { customEmojiRepaint(); },
+	};
+	if (_flags & Flag::ServiceMessage) {
+		const auto &options = Ui::ItemTextServiceOptions();
+		_text.setMarkedText(st::serviceTextStyle, text, options, context);
 		auto linkIndex = 0;
-		for (const auto &link : customLinks) {
+		for (const auto &link : links) {
 			// Link indices start with 1.
 			_text.setLink(++linkIndex, link);
 		}
 	} else {
+		const auto item = data();
+		const auto &options = Ui::ItemTextOptions(item);
 		clearSpecialOnlyEmoji();
-		const auto context = Core::MarkedTextContext{
-			.session = &history()->session(),
-			.customEmojiRepaint = [=] { customEmojiRepaint(); },
-		};
-		_text.setMarkedText(
-			st::messageTextStyle,
-			item->translatedTextWithLocalEntities(),
-			Ui::ItemTextOptions(item),
-			context);
-		if (!text.empty() && _text.isEmpty()) {
+		_text.setMarkedText(st::messageTextStyle, text, options, context);
+		if (!item->_text.empty() && _text.isEmpty()){
 			// If server has allowed some text that we've trim-ed entirely,
 			// just replace it with something so that UI won't look buggy.
 			_text.setMarkedText(
diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h
index a2b9cb049..3faaa355b 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.h
+++ b/Telegram/SourceFiles/history/view/history_view_element.h
@@ -536,6 +536,9 @@ private:
 	virtual QSize performCountCurrentSize(int newWidth) = 0;
 
 	void refreshMedia(Element *replacing);
+	void setTextWithLinks(
+		const TextWithEntities &text,
+		const std::vector<ClickHandlerPtr> &links = {});
 
 	struct TextWithLinks {
 		TextWithEntities text;
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index e2892d105..ad06f53c4 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -1777,9 +1777,8 @@ void Message::unloadHeavyPart() {
 bool Message::showForwardsFromSender(
 		not_null<HistoryMessageForwarded*> forwarded) const {
 	const auto peer = data()->history()->peer;
-	return peer->isSelf()
-		|| peer->isRepliesChat()
-		|| forwarded->imported;
+	return !forwarded->story
+		&& (peer->isSelf() || peer->isRepliesChat() || forwarded->imported);
 }
 
 bool Message::hasFromPhoto() const {
@@ -2877,7 +2876,9 @@ bool Message::displayFromName() const {
 bool Message::displayForwardedFrom() const {
 	const auto item = data();
 	if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
-		if (showForwardsFromSender(forwarded)) {
+		if (forwarded->story) {
+			return true;
+		} else if (showForwardsFromSender(forwarded)) {
 			return false;
 		}
 		if (const auto sender = item->discussionPostOriginalSender()) {
@@ -3685,6 +3686,9 @@ bool Message::needInfoDisplay() const {
 
 bool Message::hasVisibleText() const {
 	if (data()->emptyText()) {
+		if (const auto media = data()->media()) {
+			return media->storyExpired();
+		}
 		return false;
 	}
 	const auto media = this->media();
@@ -3713,6 +3717,9 @@ void Message::refreshInfoSkipBlock() {
 	const auto media = this->media();
 	const auto hasTextSkipBlock = [&] {
 		if (item->_text.empty()) {
+			if (const auto media = data()->media()) {
+				return media->storyExpired();
+			}
 			return false;
 		} else if (item->Has<HistoryMessageLogEntryOriginal>()) {
 			return false;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
index efa945cc4..960a4cd16 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
@@ -72,7 +72,7 @@ Photo::Photo(
 , _spoiler(spoiler ? std::make_unique<MediaSpoiler>() : nullptr) {
 	if (const auto media = realParent->media()) {
 		if (media->storyId()) {
-			_story = true;
+			_story = 1;
 		}
 	}
 	_caption = createCaption(realParent);

From 881867186a3e0e714c9a780deb7688a11adb5386 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 13 Jun 2023 19:51:08 +0400
Subject: [PATCH 077/259] Load more saved / archive in the viewer.

---
 .../data/data_document_resolver.cpp           |   5 +-
 Telegram/SourceFiles/data/data_stories.cpp    |   3 +-
 .../SourceFiles/data/data_stories_ids.cpp     |   4 +-
 .../admin_log/history_admin_log_inner.cpp     |   6 +-
 .../history/history_inner_widget.cpp          |   6 +-
 .../SourceFiles/history/history_widget.cpp    |   4 +-
 .../history_view_compose_controls.cpp         |   4 +-
 .../view/history_view_pinned_section.cpp      |   4 +-
 .../view/history_view_replies_section.cpp     |   7 +-
 .../view/history_view_scheduled_section.cpp   |   4 +-
 .../info/media/info_media_list_widget.cpp     |  22 +-
 .../stories/media_stories_controller.cpp      | 263 ++++++++++++++----
 .../media/stories/media_stories_controller.h  |  21 ++
 .../window/window_session_controller.cpp      |  41 +--
 .../window/window_session_controller.h        |  16 +-
 15 files changed, 301 insertions(+), 109 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp
index 2a03aaa7c..2839eeb76 100644
--- a/Telegram/SourceFiles/data/data_document_resolver.cpp
+++ b/Telegram/SourceFiles/data/data_document_resolver.cpp
@@ -254,7 +254,10 @@ void ResolveDocument(
 			&& !document->filepath().isEmpty()) {
 			File::Launch(document->location(false).fname);
 		} else if (controller) {
-			controller->openDocument(document, msgId, topicRootId, true);
+			controller->openDocument(
+				document,
+				true,
+				{ msgId, topicRootId });
 		}
 	};
 
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 3952f9712..dedc741f9 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -1109,8 +1109,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 		}
 	}
 	const auto i = _all.find(id.peer);
-	Assert(i != end(_all));
-	if (i->second.readTill >= id.story) {
+	if (i == end(_all) || i->second.readTill >= id.story) {
 		return;
 	} else if (!_markReadPending.contains(id.peer)) {
 		sendMarkAsReadRequests();
diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp
index 7506abea5..31604f2eb 100644
--- a/Telegram/SourceFiles/data/data_stories_ids.cpp
+++ b/Telegram/SourceFiles/data/data_stories_ids.cpp
@@ -44,7 +44,7 @@ rpl::producer<StoriesIdsSlice> SavedStoriesIds(
 			const auto hasBefore = int(around - begin(saved->list));
 			const auto hasAfter = int(end(saved->list) - around);
 			if (hasAfter < limit) {
-				stories->savedLoadMore(peer->id);
+				//stories->savedLoadMore(peer->id);
 			}
 			const auto takeBefore = std::min(hasBefore, limit);
 			const auto takeAfter = std::min(hasAfter, limit);
@@ -116,7 +116,7 @@ rpl::producer<StoriesIdsSlice> ArchiveStoriesIds(
 			const auto hasBefore = int(i - begin(archive.list));
 			const auto hasAfter = int(end(archive.list) - i);
 			if (hasAfter < limit) {
-				stories->archiveLoadMore();
+				//stories->archiveLoadMore();
 			}
 			const auto takeBefore = std::min(hasBefore, limit);
 			const auto takeAfter = std::min(hasAfter, limit);
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
index ec8f4c593..f6e5c92d5 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
@@ -610,14 +610,14 @@ void InnerWidget::elementShowPollResults(
 void InnerWidget::elementOpenPhoto(
 		not_null<PhotoData*> photo,
 		FullMsgId context) {
-	_controller->openPhoto(photo, context, MsgId(0));
+	_controller->openPhoto(photo, { context });
 }
 
 void InnerWidget::elementOpenDocument(
 		not_null<DocumentData*> document,
 		FullMsgId context,
 		bool showInMediaView) {
-	_controller->openDocument(document, context, MsgId(0), showInMediaView);
+	_controller->openDocument(document, showInMediaView, { context });
 }
 
 void InnerWidget::elementCancelUpload(const FullMsgId &context) {
@@ -1380,7 +1380,7 @@ void InnerWidget::openContextGif(FullMsgId itemId) {
 	if (const auto item = session().data().message(itemId)) {
 		if (const auto media = item->media()) {
 			if (const auto document = media->document()) {
-				_controller->openDocument(document, itemId, MsgId(), true);
+				_controller->openDocument(document, true, { itemId });
 			}
 		}
 	}
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 8d0d8b980..36a1a29e6 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -2779,7 +2779,7 @@ void HistoryInner::openContextGif(FullMsgId itemId) {
 	if (const auto item = session().data().message(itemId)) {
 		if (const auto media = item->media()) {
 			if (const auto document = media->document()) {
-				_controller->openDocument(document, itemId, MsgId(), true);
+				_controller->openDocument(document, true, { itemId });
 			}
 		}
 	}
@@ -3376,14 +3376,14 @@ void HistoryInner::elementShowPollResults(
 void HistoryInner::elementOpenPhoto(
 		not_null<PhotoData*> photo,
 		FullMsgId context) {
-	_controller->openPhoto(photo, context, MsgId(0));
+	_controller->openPhoto(photo, { context });
 }
 
 void HistoryInner::elementOpenDocument(
 		not_null<DocumentData*> document,
 		FullMsgId context,
 		bool showInMediaView) {
-	_controller->openDocument(document, context, MsgId(0), showInMediaView);
+	_controller->openDocument(document, showInMediaView, { context });
 }
 
 void HistoryInner::elementCancelUpload(const FullMsgId &context) {
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 61c7c7237..8e50593be 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -1467,9 +1467,9 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) {
 				if (result.open) {
 					const auto request = result.result->openRequest();
 					if (const auto photo = request.photo()) {
-						controller()->openPhoto(photo, {}, {});
+						controller()->openPhoto(photo, {});
 					} else if (const auto document = request.document()) {
-						controller()->openDocument(document, {}, {});
+						controller()->openDocument(document, false, {});
 					}
 				} else {
 					sendInlineResult(result);
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 c53650919..2b5523b28 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -3059,9 +3059,9 @@ void ComposeControls::applyInlineBotQuery(
 				if (result.open) {
 					const auto request = result.result->openRequest();
 					if (const auto photo = request.photo()) {
-						_regularWindow->openPhoto(photo, {}, {});
+						_regularWindow->openPhoto(photo, {});
 					} else if (const auto document = request.document()) {
-						_regularWindow->openDocument(document, {}, {});
+						_regularWindow->openDocument(document, false, {});
 					}
 				} else {
 					_inlineResultChosen.fire_copy(result);
diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
index 2a5cd65be..263d057ff 100644
--- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
@@ -643,14 +643,14 @@ void PinnedWidget::listShowPremiumToast(not_null<DocumentData*> document) {
 void PinnedWidget::listOpenPhoto(
 		not_null<PhotoData*> photo,
 		FullMsgId context) {
-	controller()->openPhoto(photo, context, MsgId());
+	controller()->openPhoto(photo, { context });
 }
 
 void PinnedWidget::listOpenDocument(
 		not_null<DocumentData*> document,
 		FullMsgId context,
 		bool showInMediaView) {
-	controller()->openDocument(document, context, MsgId(), showInMediaView);
+	controller()->openDocument(document, showInMediaView, { context });
 }
 
 void PinnedWidget::listPaintEmpty(
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index 4813dcc5b..65c9e503a 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -2553,14 +2553,17 @@ void RepliesWidget::listShowPremiumToast(not_null<DocumentData*> document) {
 void RepliesWidget::listOpenPhoto(
 		not_null<PhotoData*> photo,
 		FullMsgId context) {
-	controller()->openPhoto(photo, context, _rootId);
+	controller()->openPhoto(photo, { context, _rootId });
 }
 
 void RepliesWidget::listOpenDocument(
 		not_null<DocumentData*> document,
 		FullMsgId context,
 		bool showInMediaView) {
-	controller()->openDocument(document, context, _rootId, showInMediaView);
+	controller()->openDocument(
+		document,
+		showInMediaView,
+		{ context, _rootId });
 }
 
 void RepliesWidget::listPaintEmpty(
diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
index c8966ad9c..ca5e7b8bd 100644
--- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
@@ -1290,14 +1290,14 @@ void ScheduledWidget::listShowPremiumToast(
 void ScheduledWidget::listOpenPhoto(
 		not_null<PhotoData*> photo,
 		FullMsgId context) {
-	controller()->openPhoto(photo, context, MsgId());
+	controller()->openPhoto(photo, { context });
 }
 
 void ScheduledWidget::listOpenDocument(
 		not_null<DocumentData*> document,
 		FullMsgId context,
 		bool showInMediaView) {
-	controller()->openDocument(document, context, MsgId(), showInMediaView);
+	controller()->openDocument(document, showInMediaView, { context });
 }
 
 void ScheduledWidget::listPaintEmpty(
diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
index 9909cb23a..7940bca59 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_peer_values.h"
 #include "data/data_document.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_file_click_handler.h"
 #include "data/data_file_origin.h"
 #include "data/data_download_manager.h"
@@ -476,18 +477,31 @@ bool ListWidget::tooltipWindowActive() const {
 }
 
 void ListWidget::openPhoto(not_null<PhotoData*> photo, FullMsgId id) {
-	_controller->parentController()->openPhoto(photo, id, topicRootId());
+	using namespace Data;
+
+	const auto tab = _controller->storiesTab();
+	const auto context = (tab == Stories::Tab::Archive)
+		? Data::StoriesContext{ Data::StoriesContextArchive() }
+		: Data::StoriesContext{ Data::StoriesContextSaved() };
+	_controller->parentController()->openPhoto(
+		photo,
+		{ id, topicRootId() },
+		_controller->storiesPeer() ? &context : nullptr);
 }
 
 void ListWidget::openDocument(
 		not_null<DocumentData*> document,
 		FullMsgId id,
 		bool showInMediaView) {
+	const auto tab = _controller->storiesTab();
+	const auto context = (tab == Stories::Tab::Archive)
+		? Data::StoriesContext{ Data::StoriesContextArchive() }
+		: Data::StoriesContext{ Data::StoriesContextSaved() };
 	_controller->parentController()->openDocument(
 		document,
-		id,
-		topicRootId(),
-		showInMediaView);
+		showInMediaView,
+		{ id, topicRootId() },
+		_controller->storiesPeer() ? &context : nullptr);
 }
 
 void ListWidget::trackSession(not_null<Main::Session*> session) {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 3e6828ddf..7a187bcf8 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -48,6 +48,7 @@ constexpr auto kSiblingOutsidePart = 0.24;
 constexpr auto kSiblingUserpicSize = 0.3;
 constexpr auto kInnerHeightMultiplier = 1.6;
 constexpr auto kPreloadUsersCount = 3;
+constexpr auto kPreloadStoriesCount = 5;
 constexpr auto kMarkAsReadAfterSeconds = 1;
 constexpr auto kMarkAsReadAfterProgress = 0.2;
 
@@ -432,48 +433,142 @@ auto Controller::cachedReactionIconFactory() const
 	return _delegate->storiesCachedReactionIconFactory();
 }
 
-void Controller::show(
-		not_null<Data::Story*> story,
-		Data::StoriesContext context) {
+void Controller::rebuildFromContext(
+		not_null<UserData*> user,
+		FullStoryId storyId) {
 	using namespace Data;
 
-	auto &stories = story->owner().stories();
-	const auto storyId = story->fullId();
+	auto &stories = user->owner().stories();
+	auto list = std::optional<StoriesList>();
+	auto source = (const StoriesSource*)nullptr;
+	const auto peerId = storyId.peer;
 	const auto id = storyId.story;
-	auto source = stories.source(storyId.peer);
-	auto single = StoriesSource{ story->peer()->asUser() };
-	v::match(context.data, [&](StoriesContextSingle) {
-		source = nullptr;
+	v::match(_context.data, [&](StoriesContextSingle) {
 		hideSiblings();
 	}, [&](StoriesContextPeer) {
+		source = stories.source(peerId);
 		hideSiblings();
 	}, [&](StoriesContextSaved) {
+		if (stories.savedCountKnown(peerId)) {
+			if (const auto saved = stories.saved(peerId)) {
+				const auto &ids = saved->list;
+				const auto i = ids.find(id);
+				if (i != end(ids)) {
+					list = StoriesList{
+						.user = user,
+						.ids = *saved,
+						.total = stories.savedCount(peerId),
+					};
+					_index = int(i - begin(ids));
+					if (ids.size() < list->total
+						&& (end(ids) - i) < kPreloadStoriesCount) {
+						stories.savedLoadMore(peerId);
+					}
+				}
+			}
+		}
 		hideSiblings();
 	}, [&](StoriesContextArchive) {
+		Expects(user->isSelf());
+
+		if (stories.archiveCountKnown()) {
+			const auto &archive = stories.archive();
+			const auto &ids = archive.list;
+			const auto i = ids.find(id);
+			if (i != end(ids)) {
+				list = StoriesList{
+					.user = user,
+					.ids = archive,
+					.total = stories.archiveCount(),
+				};
+				_index = int(i - begin(ids));
+				if (ids.size() < list->total
+					&& (end(ids) - i) < kPreloadStoriesCount) {
+					stories.archiveLoadMore();
+				}
+			}
+		}
 		hideSiblings();
 	}, [&](StorySourcesList list) {
+		source = stories.source(peerId);
 		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);
+		if (i != end(sources)) {
+			showSiblings(&user->session(), sources, (i - begin(sources)));
+			if (int(sources.end() - i) < kPreloadUsersCount) {
+				stories.loadMore(list);
+			}
 		}
 	});
-	const auto idDates = story->idDates();
-	_index = source ? (source->ids.find(idDates) - begin(source->ids)) : 0;
-	if (!source || _index == source->ids.size()) {
-		source = &single;
-		single.ids.emplace(idDates);
-		_index = 0;
+	if (list) {
+		_source = std::nullopt;
+		if (_list != list) {
+			_list = std::move(list);
+		}
+	} else {
+		if (source) {
+			const auto i = source->ids.lower_bound(StoryIdDates{ id });
+			if (i != end(source->ids) && i->id == id) {
+				_index = int(i - begin(source->ids));
+			} else {
+				source = nullptr;
+			}
+		}
+		if (!source) {
+			_source = std::nullopt;
+			_list = StoriesList{
+				.user = user,
+				.ids = { { id } },
+				.total = 1,
+			};
+			_index = 0;
+		} else {
+			_list = std::nullopt;
+			if (_source != *source) {
+				_source = *source;
+			}
+		}
 	}
+	_slider->show({ .index = _index, .total = shownCount() });
+}
+
+void Controller::checkMoveByDelta() {
+	const auto index = _index + _waitingForDelta;
+	if (_waitingForDelta && shown() && index >= 0 && index < shownCount()) {
+		subjumpTo(index);
+	}
+}
+
+void Controller::show(
+		not_null<Data::Story*> story,
+		Data::StoriesContext context) {
+	auto &stories = story->owner().stories();
+	const auto storyId = story->fullId();
+	const auto user = story->peer()->asUser();
+	_context = context;
+	_waitingForId = {};
+	_waitingForDelta = 0;
+
+	rebuildFromContext(user, storyId);
+	_contextLifetime.destroy();
+	v::match(_context.data, [&](Data::StoriesContextSaved) {
+		stories.savedChanged() | rpl::filter(
+			rpl::mappers::_1 == storyId.peer
+		) | rpl::start_with_next([=] {
+			rebuildFromContext(user, storyId);
+			checkMoveByDelta();
+		}, _contextLifetime);
+	}, [&](Data::StoriesContextArchive) {
+		stories.archiveChanged(
+		) | rpl::start_with_next([=] {
+			rebuildFromContext(user, storyId);
+			checkMoveByDelta();
+		}, _contextLifetime);
+	}, [](const auto &) {});
+
 	const auto guard = gsl::finally([&] {
 		_paused = false;
 		_started = false;
@@ -483,11 +578,7 @@ void Controller::show(
 			_photoPlayback = nullptr;
 		}
 	});
-	if (_source != *source) {
-		_source = *source;
-	}
-	_context = context;
-	_waitingForId = {};
+
 	if (_shown == storyId) {
 		return;
 	}
@@ -501,13 +592,12 @@ 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 = user, .date = story->date() });
+	_replyArea->show({ .user = user, .id = story->id() });
 	_recentViews->show({
 		.list = story->recentViewers(),
 		.total = story->views(),
-		.valid = source->user->isSelf(),
+		.valid = user->isSelf(),
 	});
 
 	const auto session = &story->session();
@@ -531,7 +621,7 @@ void Controller::show(
 	stories.loadAround(storyId, context);
 
 	updatePlayingAllowed();
-	source->user->updateFull();
+	user->updateFull();
 }
 
 void Controller::updatePlayingAllowed() {
@@ -634,23 +724,23 @@ void Controller::maybeMarkAsRead(const Player::TrackState &state) {
 }
 
 void Controller::markAsRead() {
-	Expects(_source.has_value());
+	Expects(shown());
 
 	if (_viewed) {
 		return;
 	}
 	_viewed = true;
-	_source->user->owner().stories().markAsRead(_shown, _started);
+	shownUser()->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 >= int(_source->ids.size())) {
+	} else if (index >= shownCount()) {
 		return _siblingRight && _siblingRight->shownId().valid();
 	}
-	return index >= 0 && index < int(_source->ids.size());
+	return index >= 0 && index < shownCount();
 }
 
 bool Controller::subjumpFor(int delta) {
@@ -661,12 +751,12 @@ bool Controller::subjumpFor(int delta) {
 	if (index < 0) {
 		if (_siblingLeft && _siblingLeft->shownId().valid()) {
 			return jumpFor(-1);
-		} else if (!_source || _source->ids.empty()) {
+		} else if (!shown() || !shownCount()) {
 			return false;
 		}
 		subjumpTo(0);
 		return true;
-	} else if (index >= int(_source->ids.size())) {
+	} else if (index >= shownCount()) {
 		return _siblingRight
 			&& _siblingRight->shownId().valid()
 			&& jumpFor(1);
@@ -677,27 +767,37 @@ bool Controller::subjumpFor(int delta) {
 }
 
 void Controller::subjumpTo(int index) {
-	Expects(_source.has_value());
-	Expects(index >= 0 && index < _source->ids.size());
+	Expects(shown());
+	Expects(index >= 0 && index < shownCount());
 
+	const auto user = shownUser();
 	const auto id = FullStoryId{
-		.peer = _source->user->id,
-		.story = (begin(_source->ids) + index)->id,
+		.peer = user->id,
+		.story = shownId(index),
 	};
-	auto &stories = _source->user->owner().stories();
-	if (stories.lookup(id)) {
-		_delegate->storiesJumpTo(&_source->user->session(), id, _context);
+	auto &stories = user->owner().stories();
+	if (!id.story) {
+		const auto delta = index - _index;
+		if (_waitingForDelta != delta) {
+			_waitingForDelta = delta;
+			_waitingForId = {};
+			loadMoreToList();
+		}
+	} else if (stories.lookup(id)) {
+		_delegate->storiesJumpTo(&user->session(), id, _context);
 	} else if (_waitingForId != id) {
 		_waitingForId = id;
+		_waitingForDelta = 0;
 		stories.loadAround(id, _context);
 	}
 }
 
 void Controller::checkWaitingFor() {
 	Expects(_waitingForId.valid());
-	Expects(_source.has_value());
+	Expects(shown());
 
-	auto &stories = _source->user->owner().stories();
+	const auto user = shownUser();
+	auto &stories = user->owner().stories();
 	const auto maybe = stories.lookup(_waitingForId);
 	if (!maybe) {
 		if (maybe.error() == Data::NoStory::Deleted) {
@@ -706,7 +806,7 @@ void Controller::checkWaitingFor() {
 		return;
 	}
 	_delegate->storiesJumpTo(
-		&_source->user->session(),
+		&user->session(),
 		base::take(_waitingForId),
 		_context);
 }
@@ -721,7 +821,7 @@ bool Controller::jumpFor(int delta) {
 			return true;
 		}
 	} else if (delta == 1) {
-		if (_source && _index + 1 >= int(_source->ids.size())) {
+		if (shown() && _index + 1 >= shownCount()) {
 			markAsRead();
 		}
 		if (const auto right = _siblingRight.get()) {
@@ -817,7 +917,8 @@ Fn<void(std::vector<Data::StoryView>)> Controller::viewsGotMoreCallback() {
 	return crl::guard(&_viewsLoadGuard, [=](
 			const std::vector<Data::StoryView> &result) {
 		if (_viewsSlice.list.empty()) {
-			auto &stories = _source->user->owner().stories();
+			const auto user = shownUser();
+			auto &stories = user->owner().stories();
 			if (const auto maybeStory = stories.lookup(_shown)) {
 				_viewsSlice = {
 					.list = result,
@@ -838,12 +939,57 @@ Fn<void(std::vector<Data::StoryView>)> Controller::viewsGotMoreCallback() {
 	});
 }
 
-void Controller::refreshViewsFromData() {
-	Expects(_source.has_value());
+bool Controller::shown() const {
+	return _source || _list;
+}
 
-	auto &stories = _source->user->owner().stories();
+UserData *Controller::shownUser() const {
+	return _source
+		? _source->user.get()
+		: _list
+		? _list->user.get()
+		: nullptr;
+}
+
+int Controller::shownCount() const {
+	return _source ? int(_source->ids.size()) : _list ? _list->total : 0;
+}
+
+StoryId Controller::shownId(int index) const {
+	Expects(index >= 0 && index < shownCount());
+
+	return _source
+		? (_source->ids.begin() + index)->id
+		: (index < int(_list->ids.list.size()))
+		? *(_list->ids.list.begin() + index)
+		: StoryId();
+}
+
+void Controller::loadMoreToList() {
+	Expects(shown());
+
+	using namespace Data;
+
+	const auto user = shownUser();
+	const auto peerId = _shown.peer;
+	auto &stories = user->owner().stories();
+	v::match(_context.data, [&](StoriesContextSaved) {
+		stories.savedLoadMore(peerId);
+	}, [&](StoriesContextArchive) {
+		Expects(user->isSelf());
+
+		stories.archiveLoadMore();
+	}, [](const auto &) {
+	});
+}
+
+void Controller::refreshViewsFromData() {
+	Expects(shown());
+
+	const auto user = shownUser();
+	auto &stories = user->owner().stories();
 	const auto maybeStory = stories.lookup(_shown);
-	if (!maybeStory || !_source->user->isSelf()) {
+	if (!maybeStory || !user->isSelf()) {
 		_viewsSlice = {};
 		return;
 	}
@@ -861,11 +1007,12 @@ void Controller::refreshViewsFromData() {
 }
 
 bool Controller::sliceViewsTo(PeerId offset) {
-	Expects(_source.has_value());
+	Expects(shown());
 
-	auto &stories = _source->user->owner().stories();
+	const auto user = shownUser();
+	auto &stories = user->owner().stories();
 	const auto maybeStory = stories.lookup(_shown);
-	if (!maybeStory || !_source->user->isSelf()) {
+	if (!maybeStory || !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 5c83202c8..277140db0 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -135,6 +135,15 @@ public:
 	[[nodiscard]] rpl::lifetime &lifetime();
 
 private:
+	struct StoriesList {
+		not_null<UserData*> user;
+		Data::StoriesIds ids;
+		int total = 0;
+
+		friend inline bool operator==(
+			const StoriesList &,
+			const StoriesList &) = default;
+	};
 	class PhotoPlayback;
 
 	void initLayout();
@@ -166,6 +175,14 @@ private:
 	[[nodiscard]] auto viewsGotMoreCallback()
 		-> Fn<void(std::vector<Data::StoryView>)>;
 
+	[[nodiscard]] bool shown() const;
+	[[nodiscard]] UserData *shownUser() const;
+	[[nodiscard]] int shownCount() const;
+	[[nodiscard]] StoryId shownId(int index) const;
+	void rebuildFromContext(not_null<UserData*> user, FullStoryId storyId);
+	void checkMoveByDelta();
+	void loadMoreToList();
+
 	const not_null<Delegate*> _delegate;
 
 	rpl::variable<std::optional<Layout>> _layout;
@@ -194,7 +211,9 @@ private:
 	TextWithEntities _captionText;
 	Data::StoriesContext _context;
 	std::optional<Data::StoriesSource> _source;
+	std::optional<StoriesList> _list;
 	FullStoryId _waitingForId;
+	int _waitingForDelta = 0;
 	int _index = 0;
 	bool _started = false;
 	bool _viewed = false;
@@ -211,6 +230,8 @@ private:
 	Main::Session *_session = nullptr;
 	rpl::lifetime _sessionLifetime;
 
+	rpl::lifetime _contextLifetime;
+
 	rpl::lifetime _lifetime;
 
 };
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index 2d447621c..c21ef8437 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -2153,14 +2153,14 @@ void SessionController::hideLayer(anim::type animated) {
 
 void SessionController::openPhoto(
 		not_null<PhotoData*> photo,
-		FullMsgId contextId,
-		MsgId topicRootId) {
-	const auto item = session().data().message(contextId);
-	if (openSharedStory(item) || openFakeItemStory(contextId)) {
+		MessageContext message,
+		const Data::StoriesContext *stories) {
+	const auto item = session().data().message(message.id);
+	if (openSharedStory(item) || openFakeItemStory(message.id, stories)) {
 		return;
 	}
 	_window->openInMediaView(
-		Media::View::OpenRequest(this, photo, item, topicRootId));
+		Media::View::OpenRequest(this, photo, item, message.topicRootId));
 }
 
 void SessionController::openPhoto(
@@ -2171,18 +2171,21 @@ void SessionController::openPhoto(
 
 void SessionController::openDocument(
 		not_null<DocumentData*> document,
-		FullMsgId contextId,
-		MsgId topicRootId,
-		bool showInMediaView) {
-	const auto item = session().data().message(contextId);
-	if (openSharedStory(item) || openFakeItemStory(contextId)) {
+		bool showInMediaView,
+		MessageContext message,
+		const Data::StoriesContext *stories) {
+	const auto item = session().data().message(message.id);
+	if (openSharedStory(item) || openFakeItemStory(message.id, stories)) {
 		return;
 	} else if (showInMediaView) {
-		_window->openInMediaView(
-			Media::View::OpenRequest(this, document, item, topicRootId));
+		_window->openInMediaView(Media::View::OpenRequest(
+			this,
+			document,
+			item,
+			message.topicRootId));
 		return;
 	}
-	Data::ResolveDocument(this, document, item, topicRootId);
+	Data::ResolveDocument(this, document, item, message.topicRootId);
 }
 
 bool SessionController::openSharedStory(HistoryItem *item) {
@@ -2203,7 +2206,7 @@ bool SessionController::openSharedStory(HistoryItem *item) {
 
 bool SessionController::openFakeItemStory(
 		FullMsgId fakeItemId,
-		bool forceArchiveContext) {
+		const Data::StoriesContext *stories) {
 	if (!peerIsUser(fakeItemId.peer)
 		|| !IsStoryMsgId(fakeItemId.msg)) {
 		return false;
@@ -2215,13 +2218,11 @@ bool SessionController::openFakeItemStory(
 	if (maybeStory) {
 		using namespace Data;
 		const auto story = *maybeStory;
-		const auto context = !story->expired()
-			? StoriesContext{ StoriesContextPeer() }
-			: (story->pinned() && !forceArchiveContext)
-			? StoriesContext{ StoriesContextSaved() }
-			: StoriesContext{ StoriesContextArchive() };
+		const auto context = stories
+			? *stories
+			: StoriesContext{ StoriesContextSingle() };
 		_window->openInMediaView(
-			::Media::View::OpenRequest(this, *maybeStory, context));
+			::Media::View::OpenRequest(this, story, context));
 	}
 	return true;
 }
diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h
index 845cdb223..4903c6be9 100644
--- a/Telegram/SourceFiles/window/window_session_controller.h
+++ b/Telegram/SourceFiles/window/window_session_controller.h
@@ -482,20 +482,24 @@ public:
 	void showPassportForm(const Passport::FormRequest &request);
 	void clearPassportForm();
 
+	struct MessageContext {
+		FullMsgId id;
+		MsgId topicRootId;
+	};
 	void openPhoto(
 		not_null<PhotoData*> photo,
-		FullMsgId contextId,
-		MsgId topicRootId);
+		MessageContext message,
+		const Data::StoriesContext *stories = nullptr);
 	void openPhoto(not_null<PhotoData*> photo, not_null<PeerData*> peer);
 	void openDocument(
 		not_null<DocumentData*> document,
-		FullMsgId contextId,
-		MsgId topicRootId,
-		bool showInMediaView = false);
+		bool showInMediaView,
+		MessageContext message,
+		const Data::StoriesContext *stories = nullptr);
 	bool openSharedStory(HistoryItem *item);
 	bool openFakeItemStory(
 		FullMsgId fakeItemId,
-		bool forceArchiveContext = false);
+		const Data::StoriesContext *stories = nullptr);
 
 	void showChooseReportMessages(
 		not_null<PeerData*> peer,

From cdb5f4dc1ed9970bfc778ca10da3db361a23f486 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 13 Jun 2023 22:07:29 +0400
Subject: [PATCH 078/259] Update API scheme on layer 160.

---
 Telegram/SourceFiles/data/data_file_origin.cpp | 6 +++++-
 Telegram/SourceFiles/data/data_session.cpp     | 2 ++
 Telegram/SourceFiles/data/data_stories.cpp     | 4 ++--
 Telegram/SourceFiles/mtproto/scheme/api.tl     | 4 +++-
 4 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp
index ead9b1729..d37663208 100644
--- a/Telegram/SourceFiles/data/data_file_origin.cpp
+++ b/Telegram/SourceFiles/data/data_file_origin.cpp
@@ -53,7 +53,11 @@ struct FileReferenceAccumulator {
 		push(data.data().vdocument());
 	}
 	void push(const MTPWebPageAttribute &data) {
-		push(data.data().vdocuments());
+		data.match([&](const MTPDwebPageAttributeStory &data) {
+			push(data.vstory());
+		}, [&](const MTPDwebPageAttributeTheme &data) {
+			push(data.vdocuments());
+		});
 	}
 	void push(const MTPWebPage &data) {
 		data.match([&](const MTPDwebPage &data) {
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 952f1e0fe..ebc7ef8ee 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -3311,6 +3311,8 @@ void Session::webpageApplyFields(
 				const auto result = attribute.match([&](
 						const MTPDwebPageAttributeTheme &data) {
 					return lookupInAttribute(data);
+				}, [&](const MTPDwebPageAttributeStory &data) {
+					return (DocumentData*)nullptr;
 				});
 				if (result) {
 					return result;
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index dedc741f9..d36404bba 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -43,8 +43,8 @@ constexpr auto kSavedPerPage = 100;
 using UpdateFlag = StoryUpdate::Flag;
 
 [[nodiscard]] std::optional<StoryMedia> ParseMedia(
-	not_null<Session*> owner,
-	const MTPMessageMedia &media) {
+		not_null<Session*> owner,
+		const MTPMessageMedia &media) {
 	return media.match([&](const MTPDmessageMediaPhoto &data)
 		-> std::optional<StoryMedia> {
 		if (const auto photo = data.vphoto()) {
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index a916847ce..e3bcb489f 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -1195,6 +1195,7 @@ inputThemeSettings#8fde504f flags:# message_colors_animated:flags.2?true base_th
 themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector<int> wallpaper:flags.1?WallPaper = ThemeSettings;
 
 webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector<Document> settings:flags.1?ThemeSettings = WebPageAttribute;
+webPageAttributeStory#939a4671 flags:# user_id:long id:int story:flags.0?StoryItem = WebPageAttribute;
 
 messages.votesList#4899484e flags:# count:int votes:Vector<MessagePeerVote> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = messages.VotesList;
 
@@ -1529,7 +1530,7 @@ storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector<long>
 
 storyItemDeleted#51e6ee4f id:int = 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<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
+storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
 
 userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = UserStories;
 
@@ -2095,6 +2096,7 @@ stories.getUserStories#96d528e0 user_id:InputUser = stories.UserStories;
 stories.getPinnedStories#b471137 user_id:InputUser offset_id:int limit:int = stories.Stories;
 stories.getStoriesArchive#1f5bc5d2 offset_id:int limit:int = stories.Stories;
 stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector<int> = stories.Stories;
+stories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool;
 stories.readStories#edc5105b user_id:InputUser max_id:int = Vector<int>;
 stories.incrementStoryViews#22126127 user_id:InputUser id:Vector<int> = Bool;
 stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList;

From 41eac3692ccb3face37a886b49be92449f479c62 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 14 Jun 2023 21:31:23 +0400
Subject: [PATCH 079/259] Correctly show views count without viewers.

---
 Telegram/SourceFiles/data/data_stories.cpp    | 24 ++++++-----
 .../stories/media_stories_recent_views.cpp    | 42 ++++++++++++-------
 .../stories/media_stories_recent_views.h      |  2 +
 .../ui/chat/group_call_userpics.cpp           | 16 ++++---
 .../SourceFiles/ui/chat/group_call_userpics.h |  1 +
 5 files changed, 56 insertions(+), 29 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index d36404bba..12f4b33a7 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -313,15 +313,17 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 			&owner().session(),
 			data.ventities().value_or_empty()),
 	};
-	auto views = 0;
+	auto views = -1;
 	auto recent = std::vector<not_null<PeerData*>>();
-	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)));
+	if (!data.is_min()) {
+		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)));
+				}
 			}
 		}
 	}
@@ -331,7 +333,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 		|| (_isPublic != isPublic)
 		|| (_closeFriends != closeFriends)
 		|| (_caption != caption)
-		|| (_views != views)
+		|| (views >= 0 && _views != views)
 		|| (_recentViewers != recent);
 	if (!changed) {
 		return false;
@@ -341,7 +343,9 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 	_isPublic = isPublic;
 	_closeFriends = closeFriends;
 	_caption = std::move(caption);
-	_views = views;
+	if (views >= 0) {
+		_views = views;
+	}
 	_recentViewers = std::move(recent);
 	return true;
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
index 98f8ca9a8..cb6077bf2 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
@@ -150,15 +150,41 @@ void RecentViews::show(RecentViewsData data) {
 	if (usersChanged) {
 		updateUserpics();
 	}
+	refreshClickHandler();
+}
+
+void RecentViews::refreshClickHandler() {
+	const auto nowEmpty = _data.list.empty();
+	const auto wasEmpty = !_clickHandlerLifetime;
+	const auto raw = _widget.get();
+	if (wasEmpty == nowEmpty) {
+		return;
+	} else if (nowEmpty) {
+		_clickHandlerLifetime.destroy();
+	} else {
+		_clickHandlerLifetime = raw->events(
+		) | rpl::filter([=](not_null<QEvent*> e) {
+			return (_data.total > 0)
+				&& (e->type() == QEvent::MouseButtonPress)
+				&& (static_cast<QMouseEvent*>(e.get())->button()
+					== Qt::LeftButton);
+		}) | rpl::start_with_next([=] {
+			showMenu();
+		});
+	}
+	raw->setCursor(_clickHandlerLifetime
+		? style::cur_pointer
+		: style::cur_default);
 }
 
 void RecentViews::updateUserpics() {
 	_userpicsLifetime = ContentByUsers(
 		_data.list
 	) | rpl::start_with_next([=](
-		const std::vector<Ui::GroupCallUser> &list) {
+			const std::vector<Ui::GroupCallUser> &list) {
 		_userpics->update(list, true);
 	});
+	_userpics->finishAnimating();
 }
 
 void RecentViews::setupUserpics() {
@@ -201,18 +227,6 @@ void RecentViews::setupWidget() {
 			_textPosition.y(),
 			raw->width() - _userpicsWidth - st::storiesRecentViewsSkip);
 	}, raw->lifetime());
-
-	raw->events(
-	) | rpl::filter([=](not_null<QEvent*> e) {
-		return (_data.total > 0)
-			&& (e->type() == QEvent::MouseButtonPress)
-			&& (static_cast<QMouseEvent*>(e.get())->button()
-				== Qt::LeftButton);
-	}) | rpl::start_with_next([=] {
-		showMenu();
-	}, raw->lifetime());
-
-	raw->setCursor(style::cur_pointer);
 }
 
 void RecentViews::updatePartsGeometry() {
@@ -242,7 +256,7 @@ void RecentViews::updateText() {
 }
 
 void RecentViews::showMenu() {
-	if (_menu) {
+	if (_menu || _data.list.empty()) {
 		return;
 	}
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h
index 79b4ee3fc..2ecddf4ac 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h
@@ -70,6 +70,7 @@ private:
 	void addMenuRowPlaceholder();
 	void rebuildMenuTail();
 	void subscribeToMenuUserpicsLoading(not_null<Main::Session*> session);
+	void refreshClickHandler();
 
 	const not_null<Controller*> _controller;
 
@@ -88,6 +89,7 @@ private:
 	rpl::variable<bool> _shortAnimationPlaying;
 	bool _waitingUserpicsCheck = false;
 	rpl::lifetime _waitingForUserpicsLifetime;
+	rpl::lifetime _clickHandlerLifetime;
 
 	QRect _outer;
 	QPoint _userpicsPosition;
diff --git a/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
index b5aadd41e..3c2ad72b9 100644
--- a/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
+++ b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
@@ -348,11 +348,17 @@ void GroupCallUserpics::update(
 		_speakingAnimation.start();
 	}
 
-	if (!visible) {
-		for (auto &userpic : _list) {
-			userpic.shownAnimation.stop();
-			userpic.leftAnimation.stop();
-		}
+	if (visible) {
+		recountAndRepaint();
+	} else {
+		finishAnimating();
+	}
+}
+
+void GroupCallUserpics::finishAnimating() {
+	for (auto &userpic : _list) {
+		userpic.shownAnimation.stop();
+		userpic.leftAnimation.stop();
 	}
 	recountAndRepaint();
 }
diff --git a/Telegram/SourceFiles/ui/chat/group_call_userpics.h b/Telegram/SourceFiles/ui/chat/group_call_userpics.h
index 326992d3d..d9be41fd4 100644
--- a/Telegram/SourceFiles/ui/chat/group_call_userpics.h
+++ b/Telegram/SourceFiles/ui/chat/group_call_userpics.h
@@ -34,6 +34,7 @@ public:
 		const std::vector<GroupCallUser> &users,
 		bool visible);
 	void paint(QPainter &p, int x, int y, int size);
+	void finishAnimating();
 
 	[[nodiscard]] int maxWidth() const;
 	[[nodiscard]] rpl::producer<int> widthValue() const;

From ff835ec76ce2d0d60ef02ff74f6a95866c6cae08 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 15 Jun 2023 16:26:49 +0400
Subject: [PATCH 080/259] Show animation on story reaction sending.

---
 .../history_view_reactions_selector.cpp       | 19 ++++++++
 .../history_view_reactions_selector.h         |  1 +
 .../stories/media_stories_controller.cpp      | 46 +++++++++++++++++--
 .../media/stories/media_stories_controller.h  |  8 ++++
 .../media/stories/media_stories_reactions.cpp |  2 +-
 .../media/stories/media_stories_reactions.h   |  6 ++-
 .../ui/effects/reaction_fly_animation.cpp     | 24 +++++++++-
 .../ui/effects/reaction_fly_animation.h       |  2 +
 8 files changed, 100 insertions(+), 8 deletions(-)

diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
index fa0dec3f9..4aa2370a8 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/painter.h"
 #include "history/history_item.h"
 #include "data/data_document.h"
+#include "data/data_document_media.h"
 #include "data/data_session.h"
 #include "data/stickers/data_custom_emoji.h"
 #include "main/main_session.h"
@@ -667,12 +668,30 @@ ChosenReaction Selector::lookupChosen(const Data::ReactionId &id) const {
 	return result;
 }
 
+void Selector::preloadAllRecentsAnimations() {
+	const auto preload = [&](DocumentData *document) {
+		const auto view = document
+			? document->activeMediaView()
+			: nullptr;
+		if (view) {
+			view->checkStickerLarge();
+		}
+	};
+	for (const auto &reaction : _reactions.recent) {
+		if (!reaction.id.custom()) {
+			preload(reaction.centerIcon);
+		}
+		preload(reaction.aroundAnimation);
+	}
+}
+
 void Selector::expand() {
 	if (_expandScheduled) {
 		return;
 	}
 	_expandScheduled = true;
 	_willExpand.fire({});
+	preloadAllRecentsAnimations();
 	const auto parent = parentWidget()->geometry();
 	const auto extents = extentsForShadow();
 	const auto heightLimit = _reactions.customAllowed
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
index 648fe4adb..b47dfa2d8 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h
@@ -131,6 +131,7 @@ private:
 	void createList();
 	void finishExpand();
 	ChosenReaction lookupChosen(const Data::ReactionId &id) const;
+	void preloadAllRecentsAnimations();
 
 	const style::EmojiPan &_st;
 	const std::shared_ptr<ChatHelpers::Show> _show;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 7a187bcf8..1a519058b 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -11,11 +11,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/power_save_blocker.h"
 #include "base/qt_signal_producer.h"
 #include "chat_helpers/compose/compose_show.h"
+#include "data/stickers/data_custom_emoji.h"
 #include "data/data_changes.h"
 #include "data/data_file_origin.h"
+#include "data/data_message_reactions.h"
 #include "data/data_session.h"
 #include "data/data_stories.h"
 #include "data/data_user.h"
+#include "history/view/reactions/history_view_reactions_strip.h"
 #include "main/main_session.h"
 #include "media/stories/media_stories_caption_full_view.h"
 #include "media/stories/media_stories_delegate.h"
@@ -28,8 +31,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/stories/media_stories_share.h"
 #include "media/stories/media_stories_view.h"
 #include "media/audio/media_audio.h"
-#include "ui/rp_widget.h"
+#include "ui/effects/emoji_fly_animation.h"
+#include "ui/effects/message_sending_animation_common.h"
+#include "ui/effects/reaction_fly_animation.h"
 #include "ui/layers/box_content.h"
+#include "ui/rp_widget.h"
 #include "styles/style_media_view.h"
 #include "styles/style_widgets.h"
 #include "styles/style_boxes.h" // UserpicButton
@@ -171,8 +177,14 @@ Controller::Controller(not_null<Delegate*> delegate)
 	}, _lifetime);
 
 	_reactions->chosen(
-	) | rpl::start_with_next([=](const Data::ReactionId &id) {
-		_replyArea->sendReaction(id);
+	) | rpl::start_with_next([=](HistoryView::Reactions::ChosenReaction id) {
+		startReactionAnimation(id.id, {
+			.type = Ui::MessageSendingAnimationFrom::Type::Emoji,
+			.globalStartGeometry = id.globalGeometry,
+			.frame = id.icon,
+		});
+		_replyArea->sendReaction(id.id);
+		unfocusReply();
 	}, _lifetime);
 
 	_delegate->storiesLayerShown(
@@ -1062,4 +1074,32 @@ void Controller::updatePowerSaveBlocker(const Player::TrackState &state) {
 		[=] { return _wrap->window()->windowHandle(); });
 }
 
+void Controller::startReactionAnimation(
+		Data::ReactionId id,
+		Ui::MessageSendingAnimationFrom from) {
+	Expects(shown());
+
+	auto args = Ui::ReactionFlyAnimationArgs{
+		.id = id,
+		.flyIcon = from.frame,
+		.flyFrom = _wrap->mapFromGlobal(from.globalStartGeometry),
+		.scaleOutDuration = st::fadeWrapDuration * 2,
+	};
+	_reactionAnimation = std::make_unique<Ui::EmojiFlyAnimation>(
+		_wrap,
+		&shownUser()->owner().reactions(),
+		std::move(args),
+		[=] { _reactionAnimation->repaint(); },
+		Data::CustomEmojiSizeTag::Isolated);
+	const auto layer = _reactionAnimation->layer();
+	_wrap->paintRequest() | rpl::start_with_next([=] {
+		if (!_reactionAnimation->paintBadgeFrame(_wrap.get())) {
+			InvokeQueued(layer, [=] {
+				_reactionAnimation = nullptr;
+				_wrap->update();
+			});
+		}
+	}, layer->lifetime());
+}
+
 } // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 277140db0..74dde653b 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 FileOrigin;
+struct ReactionId;
 } // namespace Data
 
 namespace HistoryView::Reactions {
@@ -29,6 +30,8 @@ class CachedIconFactory;
 
 namespace Ui {
 class RpWidget;
+struct MessageSendingAnimationFrom;
+class EmojiFlyAnimation;
 } // namespace Ui
 
 namespace Main {
@@ -183,6 +186,10 @@ private:
 	void checkMoveByDelta();
 	void loadMoreToList();
 
+	void startReactionAnimation(
+		Data::ReactionId id,
+		Ui::MessageSendingAnimationFrom from);
+
 	const not_null<Delegate*> _delegate;
 
 	rpl::variable<std::optional<Layout>> _layout;
@@ -226,6 +233,7 @@ private:
 	std::unique_ptr<Sibling> _siblingRight;
 
 	std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
+	std::unique_ptr<Ui::EmojiFlyAnimation> _reactionAnimation;
 
 	Main::Session *_session = nullptr;
 	rpl::lifetime _sessionLifetime;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
index d77e8274e..fb94bc026 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -143,7 +143,7 @@ void Reactions::create() {
 	_selector->chosen(
 	) | rpl::start_with_next([=](
 			HistoryView::Reactions::ChosenReaction reaction) {
-		_chosen.fire_copy(reaction.id);
+		_chosen.fire_copy(reaction);
 		hide();
 	}, _selector->lifetime());
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
index 1be44b34a..ca2200d7e 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
@@ -15,6 +15,7 @@ struct ReactionId;
 
 namespace HistoryView::Reactions {
 class Selector;
+struct ChosenReaction;
 } // namespace HistoryView::Reactions
 
 namespace Ui {
@@ -30,10 +31,11 @@ public:
 	explicit Reactions(not_null<Controller*> controller);
 	~Reactions();
 
+	using Chosen = HistoryView::Reactions::ChosenReaction;
 	[[nodiscard]] rpl::producer<bool> expandedValue() const {
 		return _expanded.value();
 	}
-	[[nodiscard]] rpl::producer<Data::ReactionId> chosen() const {
+	[[nodiscard]] rpl::producer<Chosen> chosen() const {
 		return _chosen.events();
 	}
 
@@ -54,7 +56,7 @@ private:
 	std::unique_ptr<Ui::RpWidget> _parent;
 	std::unique_ptr<HistoryView::Reactions::Selector> _selector;
 	std::vector<std::unique_ptr<Hiding>> _hiding;
-	rpl::event_stream<Data::ReactionId> _chosen;
+	rpl::event_stream<Chosen> _chosen;
 	Ui::Animations::Simple _showing;
 	rpl::variable<float64> _shownValue;
 	rpl::variable<bool> _expanded;
diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp
index 44c7686c6..361eb3b7b 100644
--- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp
+++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp
@@ -67,7 +67,8 @@ ReactionFlyAnimation::ReactionFlyAnimation(
 	Data::CustomEmojiSizeTag customSizeTag)
 : _owner(owner)
 , _repaint(std::move(repaint))
-, _flyFrom(args.flyFrom) {
+, _flyFrom(args.flyFrom)
+, _scaleOutDuration(args.scaleOutDuration) {
 	const auto &list = owner->list(::Data::Reactions::Type::All);
 	auto centerIcon = (DocumentData*)nullptr;
 	auto aroundAnimation = (DocumentData*)nullptr;
@@ -135,7 +136,26 @@ QRect ReactionFlyAnimation::paintGetArea(
 		const QColor &colored,
 		QRect clip,
 		crl::time now) const {
-	if (_flyIcon.isNull()) {
+	const auto scale = [&] {
+		const auto rate = _effect ? _effect->frameRate() : 0.;
+		if (!_scaleOutDuration || !rate) {
+			return 1.;
+		}
+		const auto left = _effect->framesCount() - _effect->frameIndex();
+		const auto duration = left * 1000. / rate;
+		return (duration < _scaleOutDuration)
+			? (duration / double(_scaleOutDuration))
+			: 1.;
+	}();
+	if (scale < 1.) {
+		const auto delta = ((1. - scale) / 2.) * target.size();
+		target = QRect(
+			target.topLeft() + QPoint(delta.width(), delta.height()),
+			target.size() * scale);
+	}
+	if (!_valid) {
+		return QRect();
+	} else if (_flyIcon.isNull()) {
 		const auto wide = QRect(
 			target.topLeft() - QPoint(target.width(), target.height()) / 2,
 			target.size() * 2);
diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h
index 6092d46c1..595e11e86 100644
--- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h
+++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h
@@ -27,6 +27,7 @@ struct ReactionFlyAnimationArgs {
 	::Data::ReactionId id;
 	QImage flyIcon;
 	QRect flyFrom;
+	crl::time scaleOutDuration = 0;
 
 	[[nodiscard]] ReactionFlyAnimationArgs translated(QPoint point) const;
 };
@@ -102,6 +103,7 @@ private:
 	QRect _flyFrom;
 	float64 _centerSizeMultiplier = 0.;
 	int _customSize = 0;
+	crl::time _scaleOutDuration = 0;
 	bool _valid = false;
 
 	mutable Parabolic _cached;

From ad5b96b049d103ae682366053fa4f213de584006 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 15 Jun 2023 20:28:53 +0400
Subject: [PATCH 081/259] Show toasts on reply / archive / unarchive.

---
 Telegram/Resources/langs/lang.strings         |  3 +++
 .../boxes/peer_list_controllers.cpp           |  3 ++-
 Telegram/SourceFiles/data/data_stories.cpp    | 26 +++++++++++++------
 Telegram/SourceFiles/data/data_stories.h      |  9 ++++++-
 .../dialogs/dialogs_inner_widget.cpp          |  3 ++-
 .../media/stories/media_stories_reply.cpp     | 15 ++++++++---
 .../media/stories/media_stories_reply.h       |  7 +++--
 7 files changed, 49 insertions(+), 17 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 9a0a660b4..982160f60 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3811,6 +3811,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_my_title" = "My Stories";
 "lng_stories_archive_button" = "Archive";
 "lng_stories_archive_title" = "Stories Archive";
+"lng_stories_reply_sent" = "Message Sent";
+"lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list.";
+"lng_stories_shown_in_chats" = "Those stories are now shown in your Chats list.";
 
 "lng_stories_link_invalid" = "This link is broken or has expired.";
 
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 86150e820..9e6e2e146 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -108,7 +108,8 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 		) | rpl::start_with_next([=](Stories::ToggleShownRequest request) {
 			sessionController->session().data().stories().toggleHidden(
 				PeerId(int64(request.id)),
-				!request.shown);
+				!request.shown,
+				sessionController->uiShow());
 		}, raw->lifetime());
 
 		raw->loadMoreRequests(
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 12f4b33a7..a0560e565 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -16,16 +16,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #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 "ui/text/text_utilities.h"
-
-// #TODO stories testing
 #include "data/data_user.h"
+#include "data/data_session.h"
+#include "data/data_thread.h"
 #include "history/history.h"
 #include "history/history_item.h"
-#include "storage/storage_shared_media.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "ui/layers/show.h"
+#include "ui/text/text_utilities.h"
 
 namespace Data {
 namespace {
@@ -1140,7 +1139,10 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 	_markReadTimer.callOnce(kMarkAsReadDelay);
 }
 
-void Stories::toggleHidden(PeerId peerId, bool hidden) {
+void Stories::toggleHidden(
+		PeerId peerId,
+		bool hidden,
+		std::shared_ptr<Ui::Show> show) {
 	const auto user = _owner->peer(peerId)->asUser();
 	Assert(user != nullptr);
 	if (user->hasStoriesHidden() != hidden) {
@@ -1153,6 +1155,14 @@ void Stories::toggleHidden(PeerId peerId, bool hidden) {
 		)).send();
 	}
 
+	const auto guard = gsl::finally([&] {
+		if (show) {
+			show->showToast(hidden
+				? tr::lng_stories_hidden_to_contacts(tr::now)
+				: tr::lng_stories_shown_in_chats(tr::now));
+		}
+	});
+
 	const auto i = _all.find(peerId);
 	if (i == end(_all)) {
 		return;
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 8fc341076..74cffd64b 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -21,6 +21,10 @@ namespace Main {
 class Session;
 } // namespace Main
 
+namespace Ui {
+class Show;
+} // namespace Ui
+
 namespace Data {
 
 class Session;
@@ -251,7 +255,10 @@ public:
 	[[nodiscard]] bool isQuitPrevent();
 	void markAsRead(FullStoryId id, bool viewed);
 
-	void toggleHidden(PeerId peerId, bool hidden);
+	void toggleHidden(
+		PeerId peerId,
+		bool hidden,
+		std::shared_ptr<Ui::Show> show);
 
 	static constexpr auto kViewsPerPage = 50;
 	void loadViewsSlice(
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index cb559842e..22ac09842 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -353,7 +353,8 @@ InnerWidget::InnerWidget(
 	) | rpl::start_with_next([=](Stories::ToggleShownRequest request) {
 		session().data().stories().toggleHidden(
 			PeerId(int64(request.id)),
-			!request.shown);
+			!request.shown,
+			_controller->uiShow());
 	}, lifetime());
 
 	_stories->loadMoreRequests(
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index becd18b49..9d7f24715 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -122,7 +122,7 @@ void ReplyArea::sendReaction(const Data::ReactionId &id) {
 		}
 	}
 	if (!message.textWithTags.empty()) {
-		send(std::move(message), {});
+		send(std::move(message), {}, true);
 	}
 }
 
@@ -136,7 +136,10 @@ void ReplyArea::send(Api::SendOptions options) {
 	send(std::move(message), options);
 }
 
-void ReplyArea::send(Api::MessageToSend message, Api::SendOptions options) {
+void ReplyArea::send(
+		Api::MessageToSend message,
+		Api::SendOptions options,
+		bool skipToast) {
 	const auto error = GetErrorTextForSending(
 		_data.user,
 		{
@@ -151,7 +154,7 @@ void ReplyArea::send(Api::MessageToSend message, Api::SendOptions options) {
 	session().api().sendMessage(std::move(message));
 
 	_controls->clear();
-	finishSending();
+	finishSending(skipToast);
 }
 
 void ReplyArea::sendVoice(VoiceToSend &&data) {
@@ -256,9 +259,13 @@ void ReplyArea::sendInlineResult(
 	finishSending();
 }
 
-void ReplyArea::finishSending() {
+void ReplyArea::finishSending(bool skipToast) {
 	_controls->hidePanelsAnimated();
 	_controller->wrap()->setFocus();
+	if (!skipToast) {
+		_controller->uiShow()->showToast(
+			tr::lng_stories_reply_sent(tr::now));
+	}
 }
 
 void ReplyArea::uploadFile(
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h
index 4dcd84013..5853ffc0e 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h
@@ -73,7 +73,10 @@ private:
 	[[nodiscard]] Main::Session &session() const;
 	[[nodiscard]] not_null<History*> history() const;
 
-	void send(Api::MessageToSend message, Api::SendOptions options);
+	void send(
+		Api::MessageToSend message,
+		Api::SendOptions options,
+		bool skipToast = false);
 
 	void uploadFile(const QByteArray &fileContent, SendMediaType type);
 	bool confirmSendingFiles(
@@ -98,7 +101,7 @@ private:
 		TextWithTags &&caption,
 		Api::SendOptions options,
 		bool ctrlShiftEnter);
-	void finishSending();
+	void finishSending(bool skipToast = false);
 
 	void sendExistingDocument(not_null<DocumentData*> document);
 	bool sendExistingDocument(

From a2bf0fc51197f56336d5eb988176478e6e769739 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 16 Jun 2023 13:34:08 +0400
Subject: [PATCH 082/259] Show unsupported stories with an Update button.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 .../SourceFiles/data/data_media_types.cpp     |  7 ++-
 Telegram/SourceFiles/data/data_media_types.h  |  5 ++
 Telegram/SourceFiles/data/data_stories.cpp    | 13 +++-
 Telegram/SourceFiles/data/data_stories.h      |  6 +-
 Telegram/SourceFiles/history/history_item.cpp |  8 ++-
 .../info/stories/info_stories_provider.cpp    |  6 ++
 .../stories/media_stories_controller.cpp      | 61 ++++++++++++++++++-
 .../media/stories/media_stories_controller.h  |  2 +
 .../media/stories/media_stories_sibling.cpp   | 19 ++++--
 .../media/stories/media_stories_sibling.h     |  1 +
 .../SourceFiles/media/view/media_view.style   | 11 ++++
 .../media/view/media_view_overlay_widget.cpp  | 18 +++---
 13 files changed, 138 insertions(+), 20 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 982160f60..ad8e9037d 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3807,6 +3807,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_views#one" = "{count} view";
 "lng_stories_views#other" = "{count} views";
 "lng_stories_no_views" = "No views";
+"lng_stories_unsupported" = "This story is not supported\nby your version of Telegram.";
 
 "lng_stories_my_title" = "My Stories";
 "lng_stories_archive_button" = "Archive";
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index cd9d863dc..8cdaebc25 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -2039,6 +2039,11 @@ bool MediaStory::updateSentMedia(const MTPMessageMedia &media) {
 	return false;
 }
 
+not_null<PhotoData*> MediaStory::LoadingStoryPhoto(
+		not_null<Session*> owner) {
+	return owner->photo(kLoadingStoryPhotoId);
+}
+
 std::unique_ptr<HistoryView::Media> MediaStory::createView(
 		not_null<HistoryView::Element*> message,
 		not_null<HistoryItem*> realParent,
@@ -2055,7 +2060,7 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
 		return std::make_unique<HistoryView::Photo>(
 			message,
 			realParent,
-			realParent->history()->owner().photo(kLoadingStoryPhotoId),
+			LoadingStoryPhoto(&realParent->history()->owner()),
 			spoiler);
 	}
 	_expired = false;
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 0ba84e928..a77e26e46 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -38,6 +38,7 @@ namespace Data {
 
 class CloudImage;
 class WallPaper;
+class Session;
 
 enum class CallFinishReason : char {
 	Missed,
@@ -59,6 +60,7 @@ struct Call {
 	int duration = 0;
 	FinishReason finishReason = FinishReason::Missed;
 	bool video = false;
+
 };
 
 struct ExtendedPreview {
@@ -587,6 +589,9 @@ public:
 		not_null<HistoryItem*> realParent,
 		HistoryView::Element *replacing = nullptr) override;
 
+	[[nodiscard]] static not_null<PhotoData*> LoadingStoryPhoto(
+		not_null<Session*> owner);
+
 private:
 	const FullStoryId _storyId;
 	bool _expired = false;
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index a0560e565..0cf32566d 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -63,6 +63,8 @@ using UpdateFlag = StoryUpdate::Flag;
 			}
 		}
 		return {};
+	}, [&](const MTPDmessageMediaUnsupported &data) {
+		return std::make_optional(StoryMedia{ v::null });
 	}, [](const auto &) { return std::optional<StoryMedia>(); });
 }
 
@@ -135,6 +137,10 @@ bool Story::expired(TimeId now) const {
 	return _expires <= (now ? now : base::unixtime::now());
 }
 
+bool Story::unsupported() const {
+	return v::is_null(_media.data);
+}
+
 const StoryMedia &Story::media() const {
 	return _media;
 }
@@ -154,6 +160,8 @@ bool Story::hasReplyPreview() const {
 		return !photo->isNull();
 	}, [](not_null<DocumentData*> document) {
 		return document->hasThumbnail();
+	}, [](v::null_t) {
+		return false;
 	});
 }
 
@@ -168,6 +176,8 @@ Image *Story::replyPreview() const {
 			Data::FileOriginStory(_peer->id, _id),
 			_peer,
 			false);
+	}, [](v::null_t) {
+		return (Image*)nullptr;
 	});
 }
 
@@ -246,7 +256,8 @@ void Story::setCaption(TextWithEntities &&caption) {
 }
 
 const TextWithEntities &Story::caption() const {
-	return _caption;
+	static const auto empty = TextWithEntities();
+	return unsupported() ? empty : _caption;
 }
 
 void Story::setViewsData(
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 74cffd64b..c136bff5c 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -55,7 +55,10 @@ struct StoriesIds {
 };
 
 struct StoryMedia {
-	std::variant<not_null<PhotoData*>, not_null<DocumentData*>> data;
+	std::variant<
+		v::null_t,
+		not_null<PhotoData*>,
+		not_null<DocumentData*>> data;
 
 	friend inline bool operator==(StoryMedia, StoryMedia) = default;
 };
@@ -86,6 +89,7 @@ public:
 	[[nodiscard]] FullStoryId fullId() const;
 	[[nodiscard]] TimeId date() const;
 	[[nodiscard]] TimeId expires() const;
+	[[nodiscard]] bool unsupported() const;
 	[[nodiscard]] bool expired(TimeId now = 0) const;
 	[[nodiscard]] const StoryMedia &media() const;
 	[[nodiscard]] PhotoData *photo() const;
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 2e73286a8..3fb9ab309 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -685,7 +685,10 @@ HistoryItem::HistoryItem(
 : id(StoryIdToMsgId(story->id()))
 , _history(history)
 , _from(history->peer)
-, _flags(MessageFlag::Local | MessageFlag::Outgoing | MessageFlag::FakeHistoryItem | MessageFlag::StoryItem)
+, _flags(MessageFlag::Local
+	| MessageFlag::Outgoing
+	| MessageFlag::FakeHistoryItem
+	| MessageFlag::StoryItem)
 , _date(story->date()) {
 	setStoryFields(story);
 }
@@ -1522,8 +1525,7 @@ void HistoryItem::setStoryFields(not_null<Data::Story*> story) {
 	const auto spoiler = false;
 	if (const auto photo = story->photo()) {
 		_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);
-	} else {
-		const auto document = story->document();
+	} else if (const auto document = story->document()) {
 		_media = std::make_unique<Data::MediaFile>(
 			this,
 			document,
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
index bd5d88f93..388602347 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
@@ -302,6 +302,12 @@ std::unique_ptr<BaseLayout> Provider::createLayout(
 		return std::make_unique<Photo>(delegate, item, photo, spoiler);
 	} else if (const auto file = getFile()) {
 		return std::make_unique<Video>(delegate, item, file, spoiler);
+	} else {
+		return std::make_unique<Photo>(
+			delegate,
+			item,
+			Data::MediaStory::LoadingStoryPhoto(&item->history()->owner()),
+			spoiler);
 	}
 	return nullptr;
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 1a519058b..f6f70098c 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/power_save_blocker.h"
 #include "base/qt_signal_producer.h"
 #include "chat_helpers/compose/compose_show.h"
+#include "core/update_checker.h"
 #include "data/stickers/data_custom_emoji.h"
 #include "data/data_changes.h"
 #include "data/data_file_origin.h"
@@ -19,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_stories.h"
 #include "data/data_user.h"
 #include "history/view/reactions/history_view_reactions_strip.h"
+#include "lang/lang_keys.h"
 #include "main/main_session.h"
 #include "media/stories/media_stories_caption_full_view.h"
 #include "media/stories/media_stories_delegate.h"
@@ -35,6 +37,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/message_sending_animation_common.h"
 #include "ui/effects/reaction_fly_animation.h"
 #include "ui/layers/box_content.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/labels.h"
 #include "ui/rp_widget.h"
 #include "styles/style_media_view.h"
 #include "styles/style_widgets.h"
@@ -78,6 +82,19 @@ private:
 
 };
 
+class Controller::Unsupported final {
+public:
+	explicit Unsupported(not_null<Controller*> controller);
+
+private:
+	void setup();
+
+	const not_null<Controller*> _controller;
+	std::unique_ptr<Ui::FlatLabel> _text;
+	std::unique_ptr<Ui::RoundButton> _button;
+
+};
+
 Controller::PhotoPlayback::PhotoPlayback(not_null<Controller*> controller)
 : _controller(controller)
 , _timer([=] { callback(); })
@@ -129,6 +146,42 @@ void Controller::PhotoPlayback::callback() {
 	});
 }
 
+Controller::Unsupported::Unsupported(not_null<Controller*> controller)
+: _controller(controller) {
+	setup();
+}
+
+void Controller::Unsupported::setup() {
+	const auto wrap = _controller->wrap();
+
+	_text = std::make_unique<Ui::FlatLabel>(
+		wrap,
+		tr::lng_stories_unsupported(),
+		st::storiesUnsupportedLabel);
+	_text->show();
+
+	_button = std::make_unique<Ui::RoundButton>(
+		wrap,
+		tr::lng_update_telegram(),
+		st::storiesUnsupportedUpdate);
+	_button->show();
+
+	rpl::combine(
+		wrap->sizeValue(),
+		_text->sizeValue(),
+		_button->sizeValue()
+	) | rpl::start_with_next([=](QSize wrap, QSize text, QSize button) {
+		_text->move((wrap.width() - text.width()) / 2, wrap.height() / 3);
+		_button->move(
+			(wrap.width() - button.width()) / 2,
+			2 * wrap.height() / 3);
+	}, _button->lifetime());
+
+	_button->setClickedCallback([=] {
+		Core::UpdateApplication();
+	});
+}
+
 Controller::Controller(not_null<Delegate*> delegate)
 : _delegate(delegate)
 , _wrap(_delegate->storiesWrap())
@@ -584,13 +637,19 @@ void Controller::show(
 	const auto guard = gsl::finally([&] {
 		_paused = false;
 		_started = false;
-		if (story->photo()) {
+		if (!story->document()) {
 			_photoPlayback = std::make_unique<PhotoPlayback>(this);
 		} else {
 			_photoPlayback = nullptr;
 		}
 	});
 
+	if (!story->unsupported()) {
+		_unsupported = nullptr;
+	} else {
+		_unsupported = std::make_unique<Unsupported>(this);
+	}
+
 	if (_shown == storyId) {
 		return;
 	}
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 74dde653b..96d102073 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -148,6 +148,7 @@ private:
 			const StoriesList &) = default;
 	};
 	class PhotoPlayback;
+	class Unsupported;
 
 	void initLayout();
 	void updatePhotoPlayback(const Player::TrackState &state);
@@ -200,6 +201,7 @@ private:
 	const std::unique_ptr<ReplyArea> _replyArea;
 	const std::unique_ptr<Reactions> _reactions;
 	const std::unique_ptr<RecentViews> _recentViews;
+	std::unique_ptr<Unsupported> _unsupported;
 	std::unique_ptr<PhotoPlayback> _photoPlayback;
 	std::unique_ptr<CaptionFullView> _captionFullView;
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
index 375bf1f23..c95674a0b 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
@@ -242,11 +242,7 @@ 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);
-
+			setBlackThumbnail();
 			if (maybeStory.error() == Data::NoStory::Unknown) {
 				_peer->owner().stories().resolve(_id, crl::guard(this, [=] {
 					checkStory();
@@ -266,11 +262,24 @@ void Sibling::checkStory() {
 		_loader = std::make_unique<LoaderVideo>(document, origin, [=] {
 			check();
 		});
+	}, [&](v::null_t) {
+		_loader = nullptr;
 	});
+	if (!_loader) {
+		setBlackThumbnail();
+		return;
+	}
 	_blurred = _loader->blurred();
 	check();
 }
 
+void Sibling::setBlackThumbnail() {
+	_blurred = QImage(
+		st::storiesMaxSize,
+		QImage::Format_ARGB32_Premultiplied);
+	_blurred.fill(Qt::black);
+}
+
 FullStoryId Sibling::shownId() const {
 	return _id;
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.h b/Telegram/SourceFiles/media/stories/media_stories_sibling.h
index ed4b8789c..28cfc9cbc 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_sibling.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h
@@ -45,6 +45,7 @@ private:
 	void checkStory();
 	void check();
 
+	void setBlackThumbnail();
 	[[nodiscard]] QImage userpicImage(const SiblingLayout &layout);
 	[[nodiscard]] QImage nameImage(const SiblingLayout &layout);
 	[[nodiscard]] QPoint namePosition(
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index e6e1805ea..fc94f4dba 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -782,3 +782,14 @@ storiesReactionsPan: EmojiPan(storiesEmojiPan) {
 storiesReactionsWidth: 210px;
 storiesReactionsBottomSkip: 29px;
 storiesReactionsAddedTop: 200px;
+
+storiesUnsupportedLabel: FlatLabel(defaultFlatLabel) {
+	textFg: mediaviewControlFg;
+	style: TextStyle(defaultTextStyle) {
+		font: font(15px semibold);
+		linkFont: font(15px semibold);
+		linkFontOver: font(15px semibold underline);
+	}
+	align: align(top);
+}
+storiesUnsupportedUpdate: themePreviewApplyButton;
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 42a5117b6..59be5c1be 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -3111,8 +3111,8 @@ void OverlayWidget::show(OpenRequest request) {
 		displayPhoto(photo);
 		preloadData(0);
 		activateControls();
-	} else if (document) {
-		setSession(&document->session());
+	} else if (story || document) {
+		setSession(document ? &document->session() : &story->session());
 
 		if (story) {
 			setContext(StoriesContext{
@@ -4093,6 +4093,8 @@ void OverlayWidget::storiesJumpTo(
 		displayPhoto(photo, anim::activation::background);
 	}, [&](not_null<DocumentData*> document) {
 		displayDocument(document, anim::activation::background);
+	}, [&](v::null_t) {
+		displayDocument(nullptr, anim::activation::background);
 	});
 }
 
@@ -4325,12 +4327,12 @@ void OverlayWidget::paint(not_null<Renderer*> renderer) {
 				paint(right, kRightSiblingTextureIndex);
 			}
 		}
-	} else {
-		if (_themePreviewShown) {
-			renderer->paintThemePreview(_themePreviewRect);
-		} else if (documentBubbleShown() && !_docRect.isEmpty()) {
-			renderer->paintDocumentBubble(_docRect, _docIconRect);
-		}
+	} else if (_stories) {
+		// Unsupported story.
+	} else if (_themePreviewShown) {
+		renderer->paintThemePreview(_themePreviewRect);
+	} else if (documentBubbleShown() && !_docRect.isEmpty()) {
+		renderer->paintDocumentBubble(_docRect, _docIconRect);
 	}
 	if (isSaveMsgShown()) {
 		renderer->paintSaveMsg(_saveMsg);

From e41fc695134977b16649325e305feddb2c8391a9 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 16 Jun 2023 15:56:44 +0400
Subject: [PATCH 083/259] Show nice short info box from viewers dropdown.

---
 Telegram/SourceFiles/boxes/boxes.style        | 28 +++++++++++++++++++
 .../boxes/peers/peer_short_info_box.cpp       | 16 +++++++----
 .../boxes/peers/peer_short_info_box.h         |  5 +++-
 .../boxes/peers/prepare_short_info_box.cpp    | 12 +++++---
 .../boxes/peers/prepare_short_info_box.h      |  7 +++--
 Telegram/SourceFiles/info/info.style          | 16 -----------
 .../info/profile/info_profile_actions.cpp     |  1 +
 .../info/profile/info_profile_text.cpp        |  3 +-
 .../info/profile/info_profile_text.h          |  1 +
 .../stories/media_stories_recent_views.cpp    | 28 +++++++++++++++++--
 .../SourceFiles/media/view/media_view.style   | 18 ++++++++++++
 11 files changed, 103 insertions(+), 32 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index fb0a19ef5..4dba7e723 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -24,6 +24,11 @@ UserpicButton {
 	uploadIcon: icon;
 	uploadIconPosition: point;
 }
+ShortInfoBox {
+	label: FlatLabel;
+	labeled: FlatLabel;
+	labeledOneLine: FlatLabel;
+}
 
 countryRowHeight: 36px;
 countryRowNameFont: semiboldFont;
@@ -961,3 +966,26 @@ ringtonesBoxSkip: 7px;
 gradientButtonGlareDuration: 700;
 gradientButtonGlareTimeout: 2000;
 gradientButtonGlareWidth: 100px;
+
+infoLabeledOneLine: FlatLabel(defaultFlatLabel) {
+	maxHeight: 20px;
+	style: TextStyle(defaultTextStyle) {
+		lineHeight: 19px;
+	}
+	margin: margins(5px, 5px, 5px, 5px);
+}
+infoLabelSkip: 2px;
+infoLabeled: FlatLabel(infoLabeledOneLine) {
+	minWidth: 180px;
+	maxHeight: 0px;
+	margin: margins(5px, 5px, 5px, 5px);
+}
+infoLabel: FlatLabel(infoLabeled) {
+	textFg: windowSubTextFg;
+}
+
+shortInfoBox: ShortInfoBox {
+	label: infoLabel;
+	labeled: infoLabeled;
+	labeledOneLine: infoLabeledOneLine;
+}
diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
index 619542edb..1bf236db1 100644
--- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/streaming/media_streaming_player.h"
 #include "base/event_filter.h"
 #include "lang/lang_keys.h"
+#include "styles/style_boxes.h"
 #include "styles/style_layers.h"
 #include "styles/style_info.h"
 
@@ -614,8 +615,10 @@ PeerShortInfoBox::PeerShortInfoBox(
 	rpl::producer<PeerShortInfoFields> fields,
 	rpl::producer<QString> status,
 	rpl::producer<PeerShortInfoUserpic> userpic,
-	Fn<bool()> videoPaused)
-: _type(type)
+	Fn<bool()> videoPaused,
+	const style::ShortInfoBox *stOverride)
+: _st(stOverride ? *stOverride : st::shortInfoBox)
+, _type(type)
 , _fields(std::move(fields))
 , _topRoundBackground(this)
 , _scroll(this, st::shortInfoScroll)
@@ -691,11 +694,12 @@ void PeerShortInfoBox::prepareRows() {
 	auto addInfoLineGeneric = [&](
 			rpl::producer<QString> &&label,
 			rpl::producer<TextWithEntities> &&text,
-			const style::FlatLabel &textSt = st::infoLabeled) {
+			const style::FlatLabel &textSt) {
 		auto line = CreateTextWithLabel(
 			_rows,
 			rpl::duplicate(label) | Ui::Text::ToWithEntities(),
 			rpl::duplicate(text),
+			_st.label,
 			textSt,
 			st::shortInfoLabeledPadding);
 		_rows->add(object_ptr<Ui::OverrideMargins>(
@@ -715,7 +719,7 @@ void PeerShortInfoBox::prepareRows() {
 	auto addInfoLine = [&](
 			rpl::producer<QString> &&label,
 			rpl::producer<TextWithEntities> &&text,
-			const style::FlatLabel &textSt = st::infoLabeled) {
+			const style::FlatLabel &textSt) {
 		return addInfoLineGeneric(
 			std::move(label),
 			std::move(text),
@@ -728,7 +732,7 @@ void PeerShortInfoBox::prepareRows() {
 		auto result = addInfoLine(
 			std::move(label),
 			std::move(text),
-			st::infoLabeledOneLine);
+			_st.labeledOneLine);
 		result->setDoubleClickSelectsParagraph(true);
 		result->setContextCopyText(contextCopyText);
 		return result;
@@ -744,7 +748,7 @@ void PeerShortInfoBox::prepareRows() {
 	auto label = _fields.current().isBio
 		? tr::lng_info_bio_label()
 		: tr::lng_info_about_label();
-	addInfoLine(std::move(label), aboutValue());
+	addInfoLine(std::move(label), aboutValue(), _st.labeled);
 	addInfoOneLine(
 		tr::lng_info_username_label(),
 		usernameValue() | Ui::Text::ToWithEntities(),
diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
index fc29b7062..07e60132a 100644
--- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
+++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace style {
 struct ShortInfoCover;
+struct ShortInfoBox;
 } // namespace style
 
 namespace Media::Streaming {
@@ -144,7 +145,8 @@ public:
 		rpl::producer<PeerShortInfoFields> fields,
 		rpl::producer<QString> status,
 		rpl::producer<PeerShortInfoUserpic> userpic,
-		Fn<bool()> videoPaused);
+		Fn<bool()> videoPaused,
+		const style::ShortInfoBox *stOverride);
 	~PeerShortInfoBox();
 
 	[[nodiscard]] rpl::producer<> openRequests() const;
@@ -166,6 +168,7 @@ private:
 	[[nodiscard]] rpl::producer<QString> usernameValue() const;
 	[[nodiscard]] rpl::producer<TextWithEntities> aboutValue() const;
 
+	const style::ShortInfoBox &_st;
 	const PeerShortInfoType _type = PeerShortInfoType::User;
 
 	rpl::variable<PeerShortInfoFields> _fields;
diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
index a39b7eb47..8baf378b5 100644
--- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
@@ -420,7 +420,8 @@ bool ProcessCurrent(
 object_ptr<Ui::BoxContent> PrepareShortInfoBox(
 		not_null<PeerData*> peer,
 		Fn<void()> open,
-		Fn<bool()> videoPaused) {
+		Fn<bool()> videoPaused,
+		const style::ShortInfoBox *stOverride) {
 	const auto type = peer->isUser()
 		? PeerShortInfoType::User
 		: peer->isBroadcast()
@@ -432,7 +433,8 @@ object_ptr<Ui::BoxContent> PrepareShortInfoBox(
 		FieldsValue(peer),
 		StatusValue(peer),
 		std::move(userpic.value),
-		std::move(videoPaused));
+		std::move(videoPaused),
+		stOverride);
 
 	result->openRequests(
 	) | rpl::start_with_next(open, result->lifetime());
@@ -445,7 +447,8 @@ object_ptr<Ui::BoxContent> PrepareShortInfoBox(
 
 object_ptr<Ui::BoxContent> PrepareShortInfoBox(
 		not_null<PeerData*> peer,
-		not_null<Window::SessionNavigation*> navigation) {
+		not_null<Window::SessionNavigation*> navigation,
+		const style::ShortInfoBox *stOverride) {
 	const auto open = [=] { navigation->showPeerHistory(peer); };
 	const auto videoIsPaused = [=] {
 		return navigation->parentController()->isGifPausedAtLeastFor(
@@ -454,7 +457,8 @@ object_ptr<Ui::BoxContent> PrepareShortInfoBox(
 	return PrepareShortInfoBox(
 		peer,
 		open,
-		videoIsPaused);
+		videoIsPaused,
+		stOverride);
 }
 
 rpl::producer<QString> PrepareShortInfoStatus(not_null<PeerData*> peer) {
diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h
index f50a23bf7..327ce373b 100644
--- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h
+++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h
@@ -13,6 +13,7 @@ class PeerData;
 
 namespace style {
 struct ShortInfoCover;
+struct ShortInfoBox;
 } // namespace style
 
 namespace Ui {
@@ -33,11 +34,13 @@ struct PreparedShortInfoUserpic {
 [[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(
 	not_null<PeerData*> peer,
 	Fn<void()> open,
-	Fn<bool()> videoPaused);
+	Fn<bool()> videoPaused,
+	const style::ShortInfoBox *stOverride = nullptr);
 
 [[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(
 	not_null<PeerData*> peer,
-	not_null<Window::SessionNavigation*> navigation);
+	not_null<Window::SessionNavigation*> navigation,
+	const style::ShortInfoBox *stOverride = nullptr);
 
 [[nodiscard]] rpl::producer<QString> PrepareShortInfoStatus(
 	not_null<PeerData*> peer);
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index d4c146ce7..a106a9586 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -389,22 +389,6 @@ infoNotificationsIconPosition: point(20px, 5px);
 infoSharedMediaButtonIconPosition: point(20px, 3px);
 infoGroupMembersIconPosition: point(20px, 10px);
 infoChannelMembersIconPosition: point(20px, 19px);
-infoLabeledOneLine: FlatLabel(defaultFlatLabel) {
-	maxHeight: 20px;
-	style: TextStyle(defaultTextStyle) {
-		lineHeight: 19px;
-	}
-	margin: margins(5px, 5px, 5px, 5px);
-}
-infoLabelSkip: 2px;
-infoLabeled: FlatLabel(infoLabeledOneLine) {
-	minWidth: 180px;
-	maxHeight: 0px;
-	margin: margins(5px, 5px, 5px, 5px);
-}
-infoLabel: FlatLabel(infoLabeled) {
-	textFg: windowSubTextFg;
-}
 
 infoBlockHeaderLabel: FlatLabel(infoProfileStatus) {
 	textFg: windowBoldFg;
diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
index 82e0e5720..ad6b8519f 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
@@ -350,6 +350,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
 			result,
 			v::text::take_marked(std::move(label)),
 			std::move(text),
+			st::infoLabel,
 			textSt,
 			padding);
 		tracker.track(result->add(std::move(line.wrap)));
diff --git a/Telegram/SourceFiles/info/profile/info_profile_text.cpp b/Telegram/SourceFiles/info/profile/info_profile_text.cpp
index d6655dc7e..b09e35484 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_text.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_text.cpp
@@ -22,6 +22,7 @@ TextWithLabel CreateTextWithLabel(
 		QWidget *parent,
 		rpl::producer<TextWithEntities> &&label,
 		rpl::producer<TextWithEntities> &&text,
+		const style::FlatLabel &labelSt,
 		const style::FlatLabel &textSt,
 		const style::margins &padding) {
 	auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
@@ -58,7 +59,7 @@ TextWithLabel CreateTextWithLabel(
 		) | rpl::after_next([=] {
 			layout->resizeToWidth(layout->widthNoMargins());
 		}),
-		st::infoLabel));
+		labelSt));
 	result->finishAnimating();
 	return { std::move(result), labeled, subtext };
 }
diff --git a/Telegram/SourceFiles/info/profile/info_profile_text.h b/Telegram/SourceFiles/info/profile/info_profile_text.h
index dbf8b8384..50c97681a 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_text.h
+++ b/Telegram/SourceFiles/info/profile/info_profile_text.h
@@ -35,6 +35,7 @@ TextWithLabel CreateTextWithLabel(
 	QWidget *parent,
 	rpl::producer<TextWithEntities> &&label,
 	rpl::producer<TextWithEntities> &&text,
+	const style::FlatLabel &labelSt,
 	const style::FlatLabel &textSt,
 	const style::margins &padding);
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
index cb6077bf2..306e1a1f0 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
@@ -8,17 +8,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/stories/media_stories_recent_views.h"
 
 #include "api/api_who_reacted.h" // FormatReadDate.
+#include "boxes/peers/prepare_short_info_box.h"
+#include "core/application.h"
 #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/layers/box_content.h"
+#include "ui/widgets/popup_menu.h"
 #include "ui/painter.h"
 #include "ui/rp_widget.h"
 #include "ui/userpic_view.h"
+#include "window/window_controller.h"
+#include "window/window_session_controller.h"
 #include "styles/style_chat_helpers.h"
 #include "styles/style_media_view.h"
 
@@ -324,8 +329,27 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
 
 	const auto peer = entry.peer;
 	const auto date = Api::FormatReadDate(entry.date, now);
+	const auto show = _controller->uiShow();
 	const auto prepare = [&](Ui::PeerUserpicView &view) {
 		const auto size = st::storiesWhoViewed.photoSize;
+		auto callback = [=] {
+			const auto open = [=] {
+				if (const auto window = Core::App().windowFor(peer)) {
+					window->invokeForSessionController(
+						&peer->session().account(),
+						peer,
+						[&](not_null<Window::SessionController*> controller) {
+							Core::App().hideMediaView();
+							controller->showPeerHistory(peer);
+						});
+				}
+			};
+			show->show(PrepareShortInfoBox(
+				peer,
+				open,
+				[] { return false; },
+				&st::storiesShortInfoBox));
+		};
 		auto userpic = peer->generateUserpicImage(
 			view,
 			size * style::DevicePixelRatio());
@@ -334,7 +358,7 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
 			.text = peer->name(),
 			.date = date,
 			.userpic = std::move(userpic),
-			.callback = [] {},
+			.callback = std::move(callback),
 		};
 	};
 	if (_menuPlaceholderCount > 0) {
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index fc94f4dba..bf2f7624e 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -793,3 +793,21 @@ storiesUnsupportedLabel: FlatLabel(defaultFlatLabel) {
 	align: align(top);
 }
 storiesUnsupportedUpdate: themePreviewApplyButton;
+storiesShortInfoBox: ShortInfoBox(shortInfoBox) {
+	label: FlatLabel(infoLabel) {
+		textFg: storiesComposeGrayText;
+		palette: TextPalette(mediaviewTextPalette) {
+			linkFg: mediaviewTextLinkFg;
+			monoFg: storiesComposeGrayText;
+			spoilerFg: storiesComposeGrayText;
+		}
+	}
+	labeled: FlatLabel(infoLabeled) {
+		textFg: mediaviewCaptionFg;
+		palette: mediaviewTextPalette;
+	}
+	labeledOneLine: FlatLabel(infoLabeledOneLine) {
+		textFg: mediaviewCaptionFg;
+		palette: mediaviewTextPalette;
+	}
+}

From fc0902adf09675ee598d98e898fca213fb2e6130 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 16 Jun 2023 16:11:27 +0400
Subject: [PATCH 084/259] Force focus on parent widget on story show.

---
 Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 59be5c1be..bb8d12a5b 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -3063,6 +3063,7 @@ bool OverlayWidget::takeFocusFrom(not_null<QWidget*> window) const {
 void OverlayWidget::activate() {
 	_window->raise();
 	_window->activateWindow();
+	setFocus();
 	QApplication::setActiveWindow(_window);
 	setFocus();
 }

From 08c4f1f67a1912d08b90ab25490e11f217481a8e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 16 Jun 2023 16:44:32 +0400
Subject: [PATCH 085/259] Force streaming bit for stories videos.

---
 Telegram/SourceFiles/data/data_document.cpp | 18 ++++++++++++-
 Telegram/SourceFiles/data/data_document.h   | 28 ++++++++++++---------
 Telegram/SourceFiles/data/data_stories.cpp  |  1 +
 3 files changed, 34 insertions(+), 13 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 7af141841..473b7027d 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -443,7 +443,10 @@ void DocumentData::setattributes(
 		_additional = std::make_unique<StickerData>();
 		sticker()->type = StickerType::Webm;
 	}
-	if (isAudioFile() || isAnimation() || isVoiceMessage()) {
+	if (isAudioFile()
+		|| isAnimation()
+		|| isVoiceMessage()
+		|| storyMedia()) {
 		setMaybeSupportsStreaming(true);
 	}
 }
@@ -1588,6 +1591,19 @@ void DocumentData::setRemoteLocation(
 	}
 }
 
+void DocumentData::setStoryMedia(bool value) {
+	if (value) {
+		_flags |= Flag::StoryDocument;
+		setMaybeSupportsStreaming(true);
+	} else {
+		_flags &= ~Flag::StoryDocument;
+	}
+}
+
+bool DocumentData::storyMedia() const {
+	return (_flags & Flag::StoryDocument);
+}
+
 void DocumentData::setContentUrl(const QString &url) {
 	_url = url;
 }
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index b4bc0cad3..617eb331c 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -232,6 +232,9 @@ public:
 
 	[[nodiscard]] Storage::Cache::Key bigFileBaseCacheKey() const;
 
+	void setStoryMedia(bool value);
+	[[nodiscard]] bool storyMedia() const;
+
 	void setRemoteLocation(
 		int32 dc,
 		uint64 access,
@@ -283,18 +286,19 @@ public:
 
 private:
 	enum class Flag : ushort {
-		StreamingMaybeYes = 0x001,
-		StreamingMaybeNo = 0x002,
-		StreamingPlaybackFailed = 0x004,
-		ImageType = 0x008,
-		DownloadCancelled = 0x010,
-		LoadedInMediaCache = 0x020,
-		HasAttachedStickers = 0x040,
-		InlineThumbnailIsPath = 0x080,
-		ForceToCache = 0x100,
-		PremiumSticker = 0x200,
-		PossibleCoverThumbnail = 0x400,
-		UseTextColor = 0x800,
+		StreamingMaybeYes = 0x0001,
+		StreamingMaybeNo = 0x0002,
+		StreamingPlaybackFailed = 0x0004,
+		ImageType = 0x0008,
+		DownloadCancelled = 0x0010,
+		LoadedInMediaCache = 0x0020,
+		HasAttachedStickers = 0x0040,
+		InlineThumbnailIsPath = 0x0080,
+		ForceToCache = 0x0100,
+		PremiumSticker = 0x0200,
+		PossibleCoverThumbnail = 0x0400,
+		UseTextColor = 0x0800,
+		StoryDocument = 0x1000,
 	};
 	using Flags = base::flags<Flag>;
 	friend constexpr bool is_flag_type(Flag) { return true; };
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 0cf32566d..260e4d766 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -59,6 +59,7 @@ using UpdateFlag = StoryUpdate::Flag;
 			const auto result = owner->processDocument(*document);
 			if (!result->isNull()
 				&& (result->isGifv() || result->isVideoFile())) {
+				result->setStoryMedia(true);
 				return StoryMedia{ result };
 			}
 		}

From 2a1631247d601e5cb685925ac2c29623021b8b33 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 16 Jun 2023 21:23:40 +0400
Subject: [PATCH 086/259] Add stories to data export.

---
 Telegram/Resources/export_html/css/style.css  |  15 +
 .../export_html/images/section_stories.png    | Bin 0 -> 605 bytes
 .../export_html/images/section_stories@2x.png | Bin 0 -> 1120 bytes
 Telegram/Resources/langs/lang.strings         |   2 +
 Telegram/Resources/qrc/telegram/export.qrc    |   2 +
 .../export/data/export_data_types.cpp         | 103 +++++++
 .../export/data/export_data_types.h           |  29 ++
 .../SourceFiles/export/export_api_wrap.cpp    | 271 +++++++++++++++++-
 Telegram/SourceFiles/export/export_api_wrap.h |  29 +-
 .../SourceFiles/export/export_controller.cpp  |  55 +++-
 .../SourceFiles/export/export_controller.h    |  12 +-
 Telegram/SourceFiles/export/export_settings.h |   8 +-
 .../export/output/export_output_abstract.h    |   8 +
 .../export/output/export_output_html.cpp      | 210 +++++++++++++-
 .../export/output/export_output_html.h        |  10 +
 .../export/output/export_output_json.cpp      |  71 +++++
 .../export/output/export_output_json.h        |   4 +
 .../export/view/export_view_content.cpp       |   7 +
 .../export/view/export_view_settings.cpp      |   5 +
 19 files changed, 812 insertions(+), 29 deletions(-)
 create mode 100644 Telegram/Resources/export_html/images/section_stories.png
 create mode 100644 Telegram/Resources/export_html/images/section_stories@2x.png

diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css
index 456d59f46..79b680cc2 100644
--- a/Telegram/Resources/export_html/css/style.css
+++ b/Telegram/Resources/export_html/css/style.css
@@ -111,6 +111,11 @@ pre {
     border-radius: 50%;
     overflow: hidden;
 }
+.story {
+    display: block;
+    border-radius: 4px;
+    overflow: hidden;
+}
 .userpic .initials {
     display: block;
     color: #fff;
@@ -194,6 +199,10 @@ a.block_link:hover {
     text-decoration: none !important;
     background-color: #f5f7f8;
 }
+a.expanded {
+    padding: 2px 8px;
+    margin: -2px -8px;
+}
 .sections {
     padding: 11px 0;
 }
@@ -428,6 +437,9 @@ div.toast_shown {
 .section.sessions {
     background-image: url(../images/section_sessions.png);
 }
+.section.stories {
+    background-image: url(../images/section_stories.png);
+}
 .section.web {
     background-image: url(../images/section_web.png);
 }
@@ -489,6 +501,9 @@ div.toast_shown {
 .section.sessions {
     background-image: url(../images/section_sessions@2x.png);
 }
+.section.stories {
+    background-image: url(../images/section_stories@2x.png);
+}
 .section.web {
     background-image: url(../images/section_web@2x.png);
 }
diff --git a/Telegram/Resources/export_html/images/section_stories.png b/Telegram/Resources/export_html/images/section_stories.png
new file mode 100644
index 0000000000000000000000000000000000000000..650c69c91f7966a5e18dedee4cb00a8354291b3f
GIT binary patch
literal 605
zcmV-j0;2tiP)<h;3K|Lk000e1NJLTq000;O000>X1^@s6D=Y3@00009a7bBm000Gv
z000Gv0c~iV`Tzg`0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0rE*iK~#7F?N!T7
z!$1(7v12~BbO0rU4sZpK3KA+{FF*uHED{P999SwKR1l~DIsiIw3BE;&jfdH>6D1^z
z9Znp0l4EBwo;SPu<^|v%6~L+l)jQt>q7FZ{0E4y!DShD27$|=>H`h<UB2YcOT^8D%
z2EDuu<R?t{GLsrJacCkP*CtR$r!DY2E<#OCNjU2spfQ*fk>7n@5#a@Y-R3I+pF-2`
zzgH3zoo8=S9N`f*0%f#LmNuev6V@}$xrai6!Or)UG#1{X)GnVpr+X9i75o5WgjlKL
zGuuq3+|qD1$D8Zu_<X-yatxbm8tP&kDA`IrFA;@{0Fd^|`=x7Jz_k)NCks)WgP`c%
zM&Ds&WP_qUAHI_SKMw$LsT!(aqKM}L+gzjVCV&lq{Idzag~v*=HReLIpmYf64MnOe
zLLKRr3}B%o2LZxI@sM?4RruppStE8601o6vbf~nhy;YK1Of{FtpPd9Ix_afCy_zse
z@1QP5>BKT(b3Jc<i<4}XL=9^dU3GGnz-Hi_3F&+^wvBWP&&tAua!X&FGdt^+bZKW}
zB3}_fKJKs})YK63JT$j8%x+53>#^ziFEANC$;J?J-ae$^Ozf3KaUgTB1l4~y)}K6L
rkHRfmNP^U0ITLq_kL9`j;}7Et0Ts%$t7y#m00000NkvXXu0mjfil6t<

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/export_html/images/section_stories@2x.png b/Telegram/Resources/export_html/images/section_stories@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..429245138d9d9aa5af1375b66d1c69d418e7b93f
GIT binary patch
literal 1120
zcmV-m1fTnfP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00009a7bBm000XU
z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH1NBKnK~#7F?O9Du
z+dve4lMpI)^aM>$K-jS;!knOR0^kI=i$WBTa3n|_b&<&llANIF32LgUV%KnjnjOpT
z5G3*RjbkV?_KfYsNn6REgmA_)Z{FX$dG8q@M~)mh{w4@y1hZ<D!5i)%#WezK&}uf?
z!uzAz84ywi!Y%=p9co)HWPq6kFsrqFU}O&;4UsabQ~TWV=NzB3_&;gs0EKB&yU~Ri
zumn@WtezUcSO*xC6;rnUJQAG4<o6X6yo17_WmtV$K~f7~TGlGu4bPByQ$N$|Jv|cd
zGcvoQm{<-3S%Z~Qom$o}NP$!?<oh`regdw&NNU6Zk0G<np&uIqacvdPNO1=W@D`0>
zD7J8cl(y5HkHi85%zpuhav9b}EmLaSE+l|23=}OCWG~`%Dj^aH5V*|`2s7`Z#&>0H
zCxtHvD@G}0fbO)N#GML|1l;H4O$hXGll5tt=kP6dUs^|Eed$#t*fSQf76`&*M_SgW
z&GTKz5a!WI7r7B#iV$?Ab4Rx>-U5usns8xd=>W3EL&@}M3wyIp+%l~IzV<_|Gu}=c
z>XLv`q>1NA8ZHMsD519iBZtCo0^OvMW*SV+6KP94?CI7h1@OaWDBh3jFmI3%l}3z!
zmq=(V@L-69i8D{_KO)$}5e9jZsDMTQVu(cWhFugGh<g=aO*K^~fGf@%_hC_BB}e$}
zdDp6`Mb(R|Z(p5_H(M`JkXy_ao7Dc`z=Ck~-MQV^+`K`zUB$m1Z8mJk@sC2AN(5z~
zfiBdWwQ@>}NeY9hH=ae>ym+UYrxXBl6BFOIo(76+oi?UV+|nRO-G4p}JS$ISsH0Fp
zy2M6OSe++Aai6_XO;rovfv9c5&AA|i_Trt4XlN`Diu>9tJu3uz7~-hssOBpLh`iF-
z@DSz=7Aj;0x)esAjbikXMohC-tH7KPXn!9>9@@G!@fF$e36BQlXG5-EN@iRWDC%w=
zLSO6F#aCp6$kkUFH)=uFxEpmKR6McR^gUTu;`kJEbSC*f>%+?O!K{{;X#N2eLiQ#4
zZOnnpxb?}p68B3MfD~2%J8fw4St679zE+vIjm$iE(`73OjTDV7We<k~I||qqOxTPH
zZdz{9C=I)Q-JP&_O4(Y}3c=y{#Ymylmxb}87r)IXmz1Gcj~(mrZkoz3b$i`3(Pv+b
ztnitTDYJve1Tea{Bm=`Gx=q?MmAX6aw?DWX)!HEF8I>a^GT`}4ck-<5EVw+<QTxJy
zDKPc-fk3<6iFONU4OR&;_6R~Qu}NhJZ+RyhTkx}{=^1`v;vMILfh6(62Dy>bUq3zP
m#kmL9QD-DajvP7uPy7aUhMV9W!l4@g0000<MNUMnLSTX~%k$6x

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index ad8e9037d..bbd952df0 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3409,6 +3409,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_export_option_info_about" = "Your chosen screen name, username, phone number and profile pictures.";
 "lng_export_option_contacts" = "Contacts list";
 "lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices.";
+"lng_export_option_stories" = "Stories archive";
+"lng_export_option_stories_about" = "All stories you posted from Telegram mobile apps.";
 "lng_export_option_sessions" = "Active sessions";
 "lng_export_option_sessions_about" = "We store this to display your connected devices in Settings > Privacy & Security > Active Sessions.";
 "lng_export_header_other" = "Other";
diff --git a/Telegram/Resources/qrc/telegram/export.qrc b/Telegram/Resources/qrc/telegram/export.qrc
index 06ecc7eb7..290d24a30 100644
--- a/Telegram/Resources/qrc/telegram/export.qrc
+++ b/Telegram/Resources/qrc/telegram/export.qrc
@@ -37,6 +37,8 @@
     <file alias="images/section_photos@2x.png">../../export_html/images/section_photos@2x.png</file>
     <file alias="images/section_sessions.png">../../export_html/images/section_sessions.png</file>
     <file alias="images/section_sessions@2x.png">../../export_html/images/section_sessions@2x.png</file>
+    <file alias="images/section_stories.png">../../export_html/images/section_stories.png</file>
+    <file alias="images/section_stories@2x.png">../../export_html/images/section_stories@2x.png</file>
     <file alias="images/section_web.png">../../export_html/images/section_web.png</file>
     <file alias="images/section_web@2x.png">../../export_html/images/section_web@2x.png</file>
     <file alias="js/script.js">../../export_html/js/script.js</file>
diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp
index e2840acc4..e2bc9fde2 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.cpp
+++ b/Telegram/SourceFiles/export/data/export_data_types.cpp
@@ -42,6 +42,16 @@ QString PreparePhotoFileName(int index, TimeId date) {
 		+ ".jpg";
 }
 
+QString PrepareStoryFileName(
+		int index,
+		TimeId date,
+		const Utf8String &extension) {
+	return "story_"
+		+ QString::number(index)
+		+ PrepareFileNameDatePart(date)
+		+ extension;
+}
+
 } // namespace
 
 int PeerColorIndex(BareId bareId) {
@@ -584,6 +594,99 @@ UserpicsSlice ParseUserpicsSlice(
 	return result;
 }
 
+File &Story::file() {
+	return media.file();
+}
+
+const File &Story::file() const {
+	return media.file();
+}
+
+Image &Story::thumb() {
+	return media.thumb();
+}
+
+const Image &Story::thumb() const {
+	return media.thumb();
+}
+
+StoriesSlice ParseStoriesSlice(
+		const MTPVector<MTPStoryItem> &data,
+		int baseIndex) {
+	const auto &list = data.v;
+	auto result = StoriesSlice();
+	result.list.reserve(list.size());
+	for (const auto &story : list) {
+		result.lastId = story.match([](const auto &data) {
+			return data.vid().v;
+		});
+		++result.skipped;
+		story.match([&](const MTPDstoryItem &data) {
+			const auto date = data.vdate().v;
+			const auto expires = data.vexpire_date().v;
+			auto media = Media();
+			data.vmedia().match([&](const MTPDmessageMediaPhoto &data) {
+				const auto suggestedPath = "stories/"
+					+ PrepareStoryFileName(
+						++baseIndex,
+						date,
+						".jpg"_q);
+				const auto photo = data.vphoto();
+				auto content = photo
+					? ParsePhoto(*photo, suggestedPath)
+					: Photo();
+				media.content = content;
+			}, [&](const MTPDmessageMediaDocument &data) {
+				const auto document = data.vdocument();
+				auto fake = ParseMediaContext();
+				auto content = document
+					? ParseDocument(fake, *document, "stories", date)
+					: Document();
+				const auto extension = (content.mime == "image/jpeg")
+					? ".jpg"_q
+					: (content.mime == "image/png")
+					? ".png"_q
+					: [&] {
+						const auto mimeType = Core::MimeTypeForName(
+							content.mime);
+						QStringList patterns = mimeType.globPatterns();
+						if (!patterns.isEmpty()) {
+							return patterns.front().replace(
+								'*',
+								QString()).toUtf8();
+						}
+						return QByteArray();
+					}();
+				const auto path = content.file.suggestedPath = "stories/"
+					+ PrepareStoryFileName(
+						++baseIndex,
+						date,
+						extension);
+				content.thumb.file.suggestedPath = path + "_thumb.jpg";
+				media.content = content;
+			}, [&](const auto &data) {
+				media.content = UnsupportedMedia();
+			});
+			if (!v::is<UnsupportedMedia>(media.content)) {
+				result.list.push_back(Story{
+					.id = data.vid().v,
+					.date = date,
+					.expires = data.vexpire_date().v,
+					.media = std::move(media),
+					.pinned = data.is_pinned(),
+					.caption = (data.vcaption()
+						? ParseText(
+							*data.vcaption(),
+							data.ventities().value_or_empty())
+						: std::vector<TextPart>()),
+				});
+				--result.skipped;
+			}
+		}, [](const auto &) {});
+	}
+	return result;
+}
+
 std::pair<QString, QSize> WriteImageThumb(
 		const QString &basePath,
 		const QString &largePath,
diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h
index d383fdb94..27fb14ea1 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.h
+++ b/Telegram/SourceFiles/export/data/export_data_types.h
@@ -48,6 +48,10 @@ struct UserpicsInfo {
 	int count = 0;
 };
 
+struct StoriesInfo {
+	int count = 0;
+};
+
 struct FileLocation {
 	int dcId = 0;
 	MTPInputFileLocation data;
@@ -663,9 +667,34 @@ struct FileOrigin {
 	int split = 0;
 	MTPInputPeer peer;
 	int32 messageId = 0;
+	int32 storyId = 0;
 	uint64 customEmojiId = 0;
 };
 
+struct Story {
+	int32 id = 0;
+	TimeId date = 0;
+	TimeId expires = 0;
+	Media media;
+	bool pinned = false;
+	std::vector<TextPart> caption;
+
+	File &file();
+	const File &file() const;
+	Image &thumb();
+	const Image &thumb() const;
+};
+
+struct StoriesSlice {
+	std::vector<Story> list;
+	int32 lastId = 0;
+	int skipped = 0;
+};
+
+StoriesSlice ParseStoriesSlice(
+	const MTPVector<MTPStoryItem> &data,
+	int baseIndex);
+
 Message ParseMessage(
 	ParseMediaContext &context,
 	const MTPMessage &data,
diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp
index 3e31914d4..1ff8d6774 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.cpp
+++ b/Telegram/SourceFiles/export/export_api_wrap.cpp
@@ -30,6 +30,7 @@ constexpr auto kTopPeerSliceLimit = 100;
 constexpr auto kFileMaxSize = 4000 * int64(1024 * 1024);
 constexpr auto kLocationCacheSize = 100'000;
 constexpr auto kMaxEmojiPerRequest = 100;
+constexpr auto kStoriesSliceLimit = 100;
 
 struct LocationKey {
 	uint64 type;
@@ -109,6 +110,7 @@ struct ApiWrap::StartProcess {
 
 	enum class Step {
 		UserpicsCount,
+		StoriesCount,
 		SplitRanges,
 		DialogsCount,
 		LeftChannelsCount,
@@ -139,6 +141,19 @@ struct ApiWrap::UserpicsProcess {
 	int fileIndex = 0;
 };
 
+struct ApiWrap::StoriesProcess {
+	FnMut<bool(Data::StoriesInfo&&)> start;
+	Fn<bool(DownloadProgress)> fileProgress;
+	Fn<bool(Data::StoriesSlice&&)> handleSlice;
+	FnMut<void()> finish;
+
+	int processed = 0;
+	std::optional<Data::StoriesSlice> slice;
+	int offsetId = 0;
+	bool lastSlice = false;
+	int fileIndex = 0;
+};
+
 struct ApiWrap::OtherDataProcess {
 	Data::File file;
 	FnMut<void(Data::File&&)> done;
@@ -417,6 +432,9 @@ void ApiWrap::startExport(
 	if (_settings->types & Settings::Type::Userpics) {
 		_startProcess->steps.push_back(Step::UserpicsCount);
 	}
+	if (_settings->types & Settings::Type::Stories) {
+		_startProcess->steps.push_back(Step::StoriesCount);
+	}
 	if (_settings->types & Settings::Type::AnyChatsMask) {
 		_startProcess->steps.push_back(Step::SplitRanges);
 	}
@@ -447,6 +465,8 @@ void ApiWrap::sendNextStartRequest() {
 	switch (step) {
 	case Step::UserpicsCount:
 		return requestUserpicsCount();
+	case Step::StoriesCount:
+		return requestStoriesCount();
 	case Step::SplitRanges:
 		return requestSplitRanges();
 	case Step::DialogsCount:
@@ -480,6 +500,22 @@ void ApiWrap::requestUserpicsCount() {
 	}).send();
 }
 
+void ApiWrap::requestStoriesCount() {
+	Expects(_startProcess != nullptr);
+
+	mainRequest(MTPstories_GetStoriesArchive(
+		MTP_int(0), // offset_id
+		MTP_int(0) // limit
+	)).done([=](const MTPstories_Stories &result) {
+		Expects(_settings != nullptr);
+		Expects(_startProcess != nullptr);
+
+		_startProcess->info.storiesCount = result.data().vcount().v;
+
+		sendNextStartRequest();
+	}).send();
+}
+
 void ApiWrap::requestSplitRanges() {
 	Expects(_startProcess != nullptr);
 
@@ -616,7 +652,8 @@ void ApiWrap::startMainSession(FnMut<void()> done) {
 	using Type = Settings::Type;
 	const auto sizeLimit = _settings->media.sizeLimit;
 	const auto hasFiles = ((_settings->media.types != 0) && (sizeLimit > 0))
-		|| (_settings->types & Type::Userpics);
+		|| (_settings->types & Type::Userpics)
+		|| (_settings->types & Type::Stories);
 
 	using Flag = MTPaccount_InitTakeoutSession::Flag;
 	const auto flags = Flag(0)
@@ -856,6 +893,171 @@ void ApiWrap::finishUserpics() {
 	base::take(_userpicsProcess)->finish();
 }
 
+void ApiWrap::requestStories(
+		FnMut<bool(Data::StoriesInfo&&)> start,
+		Fn<bool(DownloadProgress)> progress,
+		Fn<bool(Data::StoriesSlice&&)> slice,
+		FnMut<void()> finish) {
+	Expects(_storiesProcess == nullptr);
+
+	_storiesProcess = std::make_unique<StoriesProcess>();
+	_storiesProcess->start = std::move(start);
+	_storiesProcess->fileProgress = std::move(progress);
+	_storiesProcess->handleSlice = std::move(slice);
+	_storiesProcess->finish = std::move(finish);
+
+	mainRequest(MTPstories_GetStoriesArchive(
+		MTP_int(_storiesProcess->offsetId),
+		MTP_int(kStoriesSliceLimit)
+	)).done([=](const MTPstories_Stories &result) mutable {
+		Expects(_storiesProcess != nullptr);
+
+		auto startInfo = Data::StoriesInfo{ result.data().vcount().v };
+		if (!_storiesProcess->start(std::move(startInfo))) {
+			return;
+		}
+
+		handleStoriesSlice(result);
+	}).send();
+}
+
+void ApiWrap::handleStoriesSlice(const MTPstories_Stories &result) {
+	Expects(_storiesProcess != nullptr);
+
+	loadStoriesFiles(Data::ParseStoriesSlice(
+		result.data().vstories(),
+		_storiesProcess->processed));
+}
+
+void ApiWrap::loadStoriesFiles(Data::StoriesSlice &&slice) {
+	Expects(_storiesProcess != nullptr);
+	Expects(!_storiesProcess->slice.has_value());
+
+	if (!slice.lastId) {
+		_storiesProcess->lastSlice = true;
+	}
+	_storiesProcess->slice = std::move(slice);
+	_storiesProcess->fileIndex = 0;
+	loadNextStory();
+}
+
+void ApiWrap::loadNextStory() {
+	Expects(_storiesProcess != nullptr);
+	Expects(_storiesProcess->slice.has_value());
+
+	for (auto &list = _storiesProcess->slice->list
+		; _storiesProcess->fileIndex < list.size()
+		; ++_storiesProcess->fileIndex) {
+		auto &story = list[_storiesProcess->fileIndex];
+		const auto origin = Data::FileOrigin{ .storyId = story.id };
+		const auto ready = processFileLoad(
+			story.file(),
+			origin,
+			[=](FileProgress value) { return loadStoryProgress(value); },
+			[=](const QString &path) { loadStoryDone(path); });
+		if (!ready) {
+			return;
+		}
+		const auto thumbProgress = [=](FileProgress value) {
+			return loadStoryThumbProgress(value);
+		};
+		const auto thumbReady = processFileLoad(
+			story.thumb().file,
+			origin,
+			thumbProgress,
+			[=](const QString &path) { loadStoryThumbDone(path); },
+			nullptr,
+			&story);
+		if (!thumbReady) {
+			return;
+		}
+	}
+	finishStoriesSlice();
+}
+
+void ApiWrap::finishStoriesSlice() {
+	Expects(_storiesProcess != nullptr);
+	Expects(_storiesProcess->slice.has_value());
+
+	auto slice = *base::take(_storiesProcess->slice);
+	if (slice.lastId) {
+		_storiesProcess->processed += slice.list.size();
+		_storiesProcess->offsetId = slice.lastId;
+		if (!_storiesProcess->handleSlice(std::move(slice))) {
+			return;
+		}
+	}
+	if (_storiesProcess->lastSlice) {
+		finishStories();
+		return;
+	}
+
+	mainRequest(MTPstories_GetStoriesArchive(
+		MTP_int(_storiesProcess->offsetId),
+		MTP_int(kStoriesSliceLimit)
+	)).done([=](const MTPstories_Stories &result) {
+		handleStoriesSlice(result);
+	}).send();
+}
+
+bool ApiWrap::loadStoryProgress(FileProgress progress) {
+	Expects(_fileProcess != nullptr);
+	Expects(_storiesProcess != nullptr);
+	Expects(_storiesProcess->slice.has_value());
+	Expects((_storiesProcess->fileIndex >= 0)
+		&& (_storiesProcess->fileIndex
+			< _storiesProcess->slice->list.size()));
+
+	return _storiesProcess->fileProgress(DownloadProgress{
+		_fileProcess->randomId,
+		_fileProcess->relativePath,
+		_storiesProcess->fileIndex,
+		progress.ready,
+		progress.total });
+}
+
+void ApiWrap::loadStoryDone(const QString &relativePath) {
+	Expects(_storiesProcess != nullptr);
+	Expects(_storiesProcess->slice.has_value());
+	Expects((_storiesProcess->fileIndex >= 0)
+		&& (_storiesProcess->fileIndex
+			< _storiesProcess->slice->list.size()));
+
+	const auto index = _storiesProcess->fileIndex;
+	auto &file = _storiesProcess->slice->list[index].file();
+	file.relativePath = relativePath;
+	if (relativePath.isEmpty()) {
+		file.skipReason = Data::File::SkipReason::Unavailable;
+	}
+	loadNextStory();
+}
+
+bool ApiWrap::loadStoryThumbProgress(FileProgress progress) {
+	return loadStoryProgress(progress);
+}
+
+void ApiWrap::loadStoryThumbDone(const QString &relativePath) {
+	Expects(_storiesProcess != nullptr);
+	Expects(_storiesProcess->slice.has_value());
+	Expects((_storiesProcess->fileIndex >= 0)
+		&& (_storiesProcess->fileIndex
+			< _storiesProcess->slice->list.size()));
+
+	const auto index = _storiesProcess->fileIndex;
+	auto &file = _storiesProcess->slice->list[index].thumb().file;
+	file.relativePath = relativePath;
+	if (relativePath.isEmpty()) {
+		file.skipReason = Data::File::SkipReason::Unavailable;
+	}
+	loadNextStory();
+}
+
+void ApiWrap::finishStories() {
+	Expects(_storiesProcess != nullptr);
+
+	base::take(_storiesProcess)->finish();
+}
+
 void ApiWrap::requestContacts(FnMut<void(Data::ContactsList&&)> done) {
 	Expects(_contactsProcess == nullptr);
 
@@ -1753,7 +1955,8 @@ bool ApiWrap::processFileLoad(
 		const Data::FileOrigin &origin,
 		Fn<bool(FileProgress)> progress,
 		FnMut<void(QString)> done,
-		Data::Message *message) {
+		Data::Message *message,
+		Data::Story *story) {
 	using SkipReason = Data::File::SkipReason;
 
 	if (!file.relativePath.isEmpty()
@@ -1767,7 +1970,12 @@ bool ApiWrap::processFileLoad(
 	}
 
 	using Type = MediaSettings::Type;
-	const auto type = message ? v::match(message->media.content, [&](
+	const auto media = message
+		? &message->media
+		: story
+		? &story->media
+		: nullptr;
+	const auto type = media ? v::match(media->content, [&](
 			const Data::Document &data) {
 		if (data.isSticker) {
 			return Type::Sticker;
@@ -1786,14 +1994,18 @@ bool ApiWrap::processFileLoad(
 		return Type::Photo;
 	}) : Type(0);
 
-	const auto limit = _settings->media.sizeLimit;
+	const auto fullSize = message
+		? message->file().size
+		: story
+		? story->file().size
+		: file.size;
 	if (message && Data::SkipMessageByDate(*message, *_settings)) {
 		file.skipReason = SkipReason::DateLimits;
 		return true;
-	} else if ((_settings->media.types & type) != type) {
+	} else if (!story && (_settings->media.types & type) != type) {
 		file.skipReason = SkipReason::FileType;
 		return true;
-	} else if ((message ? message->file().size : file.size) >= limit) {
+	} else if (!story && fullSize >= _settings->media.sizeLimit) {
 		// Don't load thumbs for large files that we skip.
 		file.skipReason = SkipReason::FileSize;
 		return true;
@@ -1972,7 +2184,20 @@ void ApiWrap::filePartRefreshReference(int64 offset) {
 	Expects(_fileProcess->requestId == 0);
 
 	const auto &origin = _fileProcess->origin;
-	if (!origin.messageId) {
+	if (origin.storyId) {
+		_fileProcess->requestId = mainRequest(MTPstories_GetStoriesByID(
+			MTP_inputUserSelf(),
+			MTP_vector<MTPint>(1, MTP_int(origin.storyId))
+		)).fail([=](const MTP::Error &error) {
+			_fileProcess->requestId = 0;
+			filePartUnavailable();
+			return true;
+		}).done([=](const MTPstories_Stories &result) {
+			_fileProcess->requestId = 0;
+			filePartExtractReference(offset, result);
+		}).send();
+		return;
+	} else if (!origin.messageId) {
 		error("FILE_REFERENCE error for non-message file.");
 		return;
 	}
@@ -2061,6 +2286,38 @@ void ApiWrap::filePartExtractReference(
 	});
 }
 
+void ApiWrap::filePartExtractReference(
+		int64 offset,
+		const MTPstories_Stories &result) {
+	Expects(_fileProcess != nullptr);
+	Expects(_fileProcess->requestId == 0);
+
+	const auto stories = Data::ParseStoriesSlice(
+		result.data().vstories(),
+		0);
+	for (const auto &story : stories.list) {
+		if (story.id == _fileProcess->origin.storyId) {
+			const auto refresh1 = Data::RefreshFileReference(
+				_fileProcess->location,
+				story.file().location);
+			const auto refresh2 = Data::RefreshFileReference(
+				_fileProcess->location,
+				story.thumb().file.location);
+			if (refresh1 || refresh2) {
+				_fileProcess->requestId = fileRequest(
+					_fileProcess->location,
+					offset
+				).done([=](const MTPupload_File &result) {
+					_fileProcess->requestId = 0;
+					filePartDone(offset, result);
+				}).send();
+				return;
+			}
+		}
+	}
+	filePartUnavailable();
+}
+
 void ApiWrap::filePartUnavailable() {
 	Expects(_fileProcess != nullptr);
 	Expects(!_fileProcess->requests.empty());
diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h
index 6384457d7..4723cd882 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.h
+++ b/Telegram/SourceFiles/export/export_api_wrap.h
@@ -19,12 +19,15 @@ struct FileLocation;
 struct PersonalInfo;
 struct UserpicsInfo;
 struct UserpicsSlice;
+struct StoriesInfo;
+struct StoriesSlice;
 struct ContactsList;
 struct SessionsList;
 struct DialogsInfo;
 struct DialogInfo;
 struct MessagesSlice;
 struct Message;
+struct Story;
 struct FileOrigin;
 } // namespace Data
 
@@ -44,6 +47,7 @@ public:
 
 	struct StartInfo {
 		int userpicsCount = 0;
+		int storiesCount = 0;
 		int dialogsCount = 0;
 	};
 	void startExport(
@@ -74,6 +78,12 @@ public:
 		Fn<bool(Data::UserpicsSlice&&)> slice,
 		FnMut<void()> finish);
 
+	void requestStories(
+		FnMut<bool(Data::StoriesInfo&&)> start,
+		Fn<bool(DownloadProgress)> progress,
+		Fn<bool(Data::StoriesSlice&&)> slice,
+		FnMut<void()> finish);
+
 	void requestContacts(FnMut<void(Data::ContactsList&&)> done);
 
 	void requestSessions(FnMut<void(Data::SessionsList&&)> done);
@@ -96,6 +106,7 @@ private:
 	struct StartProcess;
 	struct ContactsProcess;
 	struct UserpicsProcess;
+	struct StoriesProcess;
 	struct OtherDataProcess;
 	struct FileProcess;
 	struct FileProgress;
@@ -107,6 +118,7 @@ private:
 	void startMainSession(FnMut<void()> done);
 	void sendNextStartRequest();
 	void requestUserpicsCount();
+	void requestStoriesCount();
 	void requestSplitRanges();
 	void requestDialogsCount();
 	void requestLeftChannelsCount();
@@ -122,6 +134,16 @@ private:
 	void finishUserpicsSlice();
 	void finishUserpics();
 
+	void handleStoriesSlice(const MTPstories_Stories &result);
+	void loadStoriesFiles(Data::StoriesSlice &&slice);
+	void loadNextStory();
+	bool loadStoryProgress(FileProgress value);
+	void loadStoryDone(const QString &relativePath);
+	bool loadStoryThumbProgress(FileProgress value);
+	void loadStoryThumbDone(const QString &relativePath);
+	void finishStoriesSlice();
+	void finishStories();
+
 	void otherDataDone(const QString &relativePath);
 
 	bool useOnlyLastSplit() const;
@@ -179,7 +201,8 @@ private:
 		const Data::FileOrigin &origin,
 		Fn<bool(FileProgress)> progress,
 		FnMut<void(QString)> done,
-		Data::Message *message = nullptr);
+		Data::Message *message = nullptr,
+		Data::Story *story = nullptr);
 	std::unique_ptr<FileProcess> prepareFileProcess(
 		const Data::File &file,
 		const Data::FileOrigin &origin) const;
@@ -198,6 +221,9 @@ private:
 	void filePartExtractReference(
 		int64 offset,
 		const MTPmessages_Messages &result);
+	void filePartExtractReference(
+		int64 offset,
+		const MTPstories_Stories &result);
 
 	template <typename Request>
 	class RequestBuilder;
@@ -228,6 +254,7 @@ private:
 	std::unique_ptr<LoadedFileCache> _fileCache;
 	std::unique_ptr<ContactsProcess> _contactsProcess;
 	std::unique_ptr<UserpicsProcess> _userpicsProcess;
+	std::unique_ptr<StoriesProcess> _storiesProcess;
 	std::unique_ptr<OtherDataProcess> _otherDataProcess;
 	std::unique_ptr<FileProcess> _fileProcess;
 	std::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;
diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp
index 516c75cf7..b3d543f4c 100644
--- a/Telegram/SourceFiles/export/export_controller.cpp
+++ b/Telegram/SourceFiles/export/export_controller.cpp
@@ -75,6 +75,7 @@ private:
 	void collectDialogsList();
 	void exportPersonalInfo();
 	void exportUserpics();
+	void exportStories();
 	void exportContacts();
 	void exportSessions();
 	void exportOtherData();
@@ -89,6 +90,7 @@ private:
 	ProcessingState stateDialogsList(int processed) const;
 	ProcessingState statePersonalInfo() const;
 	ProcessingState stateUserpics(const DownloadProgress &progress) const;
+	ProcessingState stateStories(const DownloadProgress &progress) const;
 	ProcessingState stateContacts() const;
 	ProcessingState stateSessions() const;
 	ProcessingState stateOtherData() const;
@@ -114,6 +116,9 @@ private:
 	int _userpicsWritten = 0;
 	int _userpicsCount = 0;
 
+	int _storiesWritten = 0;
+	int _storiesCount = 0;
+
 	// rpl::variable<State> fails to compile in MSVC :(
 	State _state;
 	rpl::event_stream<State> _stateChanges;
@@ -273,6 +278,9 @@ void ControllerObject::fillExportSteps() {
 	if (_settings.types & Type::Userpics) {
 		_steps.push_back(Step::Userpics);
 	}
+	if (_settings.types & Type::Stories) {
+		_steps.push_back(Step::Stories);
+	}
 	if (_settings.types & Type::Contacts) {
 		_steps.push_back(Step::Contacts);
 	}
@@ -306,6 +314,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
 	if (_settings.types & Settings::Type::Userpics) {
 		push(Step::Userpics, 1);
 	}
+	if (_settings.types & Settings::Type::Stories) {
+		push(Step::Stories, 1);
+	}
 	if (_settings.types & Settings::Type::Contacts) {
 		push(Step::Contacts, 1);
 	}
@@ -344,6 +355,7 @@ void ControllerObject::exportNext() {
 	case Step::DialogsList: return collectDialogsList();
 	case Step::PersonalInfo: return exportPersonalInfo();
 	case Step::Userpics: return exportUserpics();
+	case Step::Stories: return exportStories();
 	case Step::Contacts: return exportContacts();
 	case Step::Sessions: return exportSessions();
 	case Step::OtherData: return exportOtherData();
@@ -416,6 +428,32 @@ void ControllerObject::exportUserpics() {
 	});
 }
 
+void ControllerObject::exportStories() {
+	_api.requestStories([=](Data::StoriesInfo &&start) {
+		if (ioCatchError(_writer->writeStoriesStart(start))) {
+			return false;
+		}
+		_storiesWritten = 0;
+		_storiesCount = start.count;
+		return true;
+	}, [=](DownloadProgress progress) {
+		setState(stateStories(progress));
+		return true;
+	}, [=](Data::StoriesSlice &&slice) {
+		if (ioCatchError(_writer->writeStoriesSlice(slice))) {
+			return false;
+		}
+		_storiesWritten += slice.list.size();
+		setState(stateStories(DownloadProgress()));
+		return true;
+	}, [=] {
+		if (ioCatchError(_writer->writeStoriesEnd())) {
+			return;
+		}
+		exportNext();
+	});
+}
+
 void ControllerObject::exportContacts() {
 	setState(stateContacts());
 	_api.requestContacts([=](Data::ContactsList &&result) {
@@ -533,7 +571,21 @@ ProcessingState ControllerObject::stateUserpics(
 	return prepareState(Step::Userpics, [&](ProcessingState &result) {
 		result.entityIndex = _userpicsWritten + progress.itemIndex;
 		result.entityCount = std::max(_userpicsCount, result.entityIndex);
-		result.bytesType = ProcessingState::FileType::Photo;
+		result.bytesRandomId = progress.randomId;
+		if (!progress.path.isEmpty()) {
+			const auto last = progress.path.lastIndexOf('/');
+			result.bytesName = progress.path.mid(last + 1);
+		}
+		result.bytesLoaded = progress.ready;
+		result.bytesCount = progress.total;
+	});
+}
+
+ProcessingState ControllerObject::stateStories(
+		const DownloadProgress &progress) const {
+	return prepareState(Step::Stories, [&](ProcessingState &result) {
+		result.entityIndex = _storiesWritten + progress.itemIndex;
+		result.entityCount = std::max(_storiesCount, result.entityIndex);
 		result.bytesRandomId = progress.randomId;
 		if (!progress.path.isEmpty()) {
 			const auto last = progress.path.lastIndexOf('/');
@@ -586,7 +638,6 @@ void ControllerObject::fillMessagesState(
 		: ProcessingState::EntityType::Chat;
 	result.itemIndex = _messagesWritten + progress.itemIndex;
 	result.itemCount = std::max(_messagesCount, result.itemIndex);
-	result.bytesType = ProcessingState::FileType::File; // TODO
 	result.bytesRandomId = progress.randomId;
 	if (!progress.path.isEmpty()) {
 		const auto last = progress.path.lastIndexOf('/');
diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h
index e9b08acdc..fae4b54a1 100644
--- a/Telegram/SourceFiles/export/export_controller.h
+++ b/Telegram/SourceFiles/export/export_controller.h
@@ -38,21 +38,12 @@ struct ProcessingState {
 		DialogsList,
 		PersonalInfo,
 		Userpics,
+		Stories,
 		Contacts,
 		Sessions,
 		OtherData,
 		Dialogs,
 	};
-	enum class FileType {
-		None,
-		Photo,
-		Video,
-		VoiceMessage,
-		VideoMessage,
-		Sticker,
-		GIF,
-		File,
-	};
 	enum class EntityType {
 		Chat,
 		SavedMessages,
@@ -75,7 +66,6 @@ struct ProcessingState {
 	int itemCount = 0;
 
 	uint64 bytesRandomId = 0;
-	FileType bytesType = FileType::None;
 	QString bytesName;
 	int64 bytesLoaded = 0;
 	int64 bytesCount = 0;
diff --git a/Telegram/SourceFiles/export/export_settings.h b/Telegram/SourceFiles/export/export_settings.h
index beac47760..be2e1b782 100644
--- a/Telegram/SourceFiles/export/export_settings.h
+++ b/Telegram/SourceFiles/export/export_settings.h
@@ -57,13 +57,18 @@ struct Settings {
 		PublicGroups        = 0x100,
 		PrivateChannels     = 0x200,
 		PublicChannels      = 0x400,
+		Stories             = 0x800,
 
 		GroupsMask          = PrivateGroups | PublicGroups,
 		ChannelsMask        = PrivateChannels | PublicChannels,
 		GroupsChannelsMask  = GroupsMask | ChannelsMask,
 		NonChannelChatsMask = PersonalChats | BotChats | PrivateGroups,
 		AnyChatsMask        = PersonalChats | BotChats | GroupsChannelsMask,
-		NonChatsMask        = PersonalInfo | Userpics | Contacts | Sessions,
+		NonChatsMask        = (PersonalInfo
+			| Userpics
+			| Contacts
+			| Stories
+			| Sessions),
 		AllMask             = NonChatsMask | OtherData | AnyChatsMask,
 	};
 	using Types = base::flags<Type>;
@@ -91,6 +96,7 @@ struct Settings {
 		return Type::PersonalInfo
 			| Type::Userpics
 			| Type::Contacts
+			| Type::Stories
 			| Type::PersonalChats
 			| Type::PrivateGroups;
 	}
diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.h b/Telegram/SourceFiles/export/output/export_output_abstract.h
index b98f65422..7a2bb65ea 100644
--- a/Telegram/SourceFiles/export/output/export_output_abstract.h
+++ b/Telegram/SourceFiles/export/output/export_output_abstract.h
@@ -14,6 +14,8 @@ namespace Data {
 struct PersonalInfo;
 struct UserpicsInfo;
 struct UserpicsSlice;
+struct StoriesInfo;
+struct StoriesSlice;
 struct ContactsList;
 struct SessionsList;
 struct DialogsInfo;
@@ -55,6 +57,12 @@ public:
 		const Data::UserpicsSlice &data) = 0;
 	[[nodiscard]] virtual Result writeUserpicsEnd() = 0;
 
+	[[nodiscard]] virtual Result writeStoriesStart(
+		const Data::StoriesInfo &data) = 0;
+	[[nodiscard]] virtual Result writeStoriesSlice(
+		const Data::StoriesSlice &data) = 0;
+	[[nodiscard]] virtual Result writeStoriesEnd() = 0;
+
 	[[nodiscard]] virtual Result writeContactsList(
 		const Data::ContactsList &data) = 0;
 
diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp
index 00c4155ad..efa4a7599 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html.cpp
@@ -35,11 +35,23 @@ constexpr auto kStickerMaxWidth = 384;
 constexpr auto kStickerMaxHeight = 384;
 constexpr auto kStickerMinWidth = 80;
 constexpr auto kStickerMinHeight = 80;
+constexpr auto kStoryThumbWidth = 45;
+constexpr auto kStoryThumbHeight = 80;
+
+constexpr auto kChatsPriority = 0;
+constexpr auto kContactsPriority = 2;
+constexpr auto kFrequentContactsPriority = 3;
+constexpr auto kUserpicsPriority = 4;
+constexpr auto kStoriesPriority = 5;
+constexpr auto kSessionsPriority = 6;
+constexpr auto kWebSessionsPriority = 7;
+constexpr auto kOtherPriority = 8;
 
 const auto kLineBreak = QByteArrayLiteral("<br>");
 
 using Context = details::HtmlContext;
 using UserpicData = details::UserpicData;
+using StoryData = details::StoryData;
 using PeersMap = details::PeersMap;
 using MediaData = details::MediaData;
 
@@ -347,6 +359,11 @@ struct UserpicData {
 	QByteArray lastName;
 };
 
+struct StoryData {
+	QString imageLink;
+	QString largeLink;
+};
+
 class PeersMap {
 public:
 	using Peer = Data::Peer;
@@ -503,6 +520,14 @@ public:
 		const QByteArray &details,
 		const QByteArray &info,
 		const QString &link = QString());
+	[[nodiscard]] QByteArray pushStoriesListEntry(
+		const StoryData &story,
+		const QByteArray &name,
+		const QByteArrayList &details,
+		const QByteArray &info,
+		const std::vector<Data::TextPart> &caption,
+		const QString &internalLinksDomain,
+		const QString &link = QString());
 	[[nodiscard]] QByteArray pushSessionListEntry(
 		int apiId,
 		const QByteArray &name,
@@ -750,6 +775,75 @@ QByteArray HtmlWriter::Wrap::pushListEntry(
 		info);
 }
 
+QByteArray HtmlWriter::Wrap::pushStoriesListEntry(
+		const StoryData &story,
+		const QByteArray &name,
+		const QByteArrayList &details,
+		const QByteArray &info,
+		const std::vector<Data::TextPart> &caption,
+		const QString &internalLinksDomain,
+		const QString &link) {
+	auto result = pushDiv("entry clearfix");
+	if (!link.isEmpty()) {
+		result.append(pushTag("a", {
+			{ "class", "pull_left userpic_wrap" },
+			{ "href", relativePath(link).toUtf8() + "#allow_back" },
+		}));
+	} else {
+		result.append(pushDiv("pull_left userpic_wrap"));
+	}
+	if (!story.imageLink.isEmpty()) {
+		const auto sizeStyle = "width: "
+			+ Data::NumberToString(kStoryThumbWidth)
+			+ "px; height: "
+			+ Data::NumberToString(kStoryThumbHeight)
+			+ "px";
+		result.append(pushTag("img", {
+			{ "class", "story" },
+			{ "style", sizeStyle },
+			{ "src", relativePath(story.imageLink).toUtf8() },
+			{ "empty", "" }
+		}));
+	}
+	result.append(popTag());
+	result.append(pushDiv("body"));
+	if (!info.isEmpty()) {
+		result.append(pushDiv("pull_right info details"));
+		result.append(SerializeString(info));
+		result.append(popTag());
+	}
+	if (!name.isEmpty()) {
+		if (!link.isEmpty()) {
+			result.append(pushTag("a", {
+				{ "class", "block_link expanded" },
+				{ "href", relativePath(link).toUtf8() + "#allow_back" },
+			}));
+		}
+		result.append(pushDiv("name bold"));
+		result.append(SerializeString(name));
+		result.append(popTag());
+		if (!link.isEmpty()) {
+			result.append(popTag());
+		}
+	}
+	const auto text = caption.empty()
+		? QByteArray()
+		: FormatText(caption, internalLinksDomain, _base);
+	if (!text.isEmpty()) {
+		result.append(pushDiv("text"));
+		result.append(text);
+		result.append(popTag());
+	}
+	for (const auto &detail : details) {
+		result.append(pushDiv("details_entry details"));
+		result.append(SerializeString(detail));
+		result.append(popTag());
+	}
+	result.append(popTag());
+	result.append(popTag());
+	return result;
+}
+
 QByteArray HtmlWriter::Wrap::pushSessionListEntry(
 		int apiId,
 		const QByteArray &name,
@@ -1980,6 +2074,7 @@ Result HtmlWriter::start(
 		"images/section_other.png",
 		"images/section_photos.png",
 		"images/section_sessions.png",
+		"images/section_stories.png",
 		"images/section_web.png",
 		"js/script.js",
 	};
@@ -2176,13 +2271,114 @@ QString HtmlWriter::userpicsFilePath() const {
 
 void HtmlWriter::pushUserpicsSection() {
 	pushSection(
-		4,
+		kUserpicsPriority,
 		"Profile pictures",
 		"photos",
 		_userpicsCount,
 		userpicsFilePath());
 }
 
+Result HtmlWriter::writeStoriesStart(const Data::StoriesInfo &data) {
+	Expects(_summary != nullptr);
+	Expects(_stories == nullptr);
+
+	_storiesCount = data.count;
+	if (!_storiesCount) {
+		return Result::Success();
+	}
+	_stories = fileWithRelativePath(storiesFilePath());
+
+	auto block = _stories->pushHeader(
+		"Stories archive",
+		mainFileRelativePath());
+	block.append(_stories->pushDiv("page_body list_page"));
+	block.append(_stories->pushDiv("entry_list"));
+	if (const auto result = _stories->writeBlock(block); !result) {
+		return result;
+	}
+	return Result::Success();
+}
+
+Result HtmlWriter::writeStoriesSlice(const Data::StoriesSlice &data) {
+	Expects(_stories != nullptr);
+
+	_storiesCount -= data.skipped;
+	if (data.list.empty()) {
+		return Result::Success();
+	}
+	auto block = QByteArray();
+	for (const auto &story : data.list) {
+		auto data = StoryData{};
+		using SkipReason = Data::File::SkipReason;
+		const auto &file = story.file();
+		Assert(!file.relativePath.isEmpty()
+			|| file.skipReason != SkipReason::None);
+		auto status = QByteArrayList();
+		if (story.pinned) {
+			status.append("Saved to Profile");
+		}
+		if (story.expires > 0) {
+			status.append("Expiring: " + Data::FormatDateTime(story.expires));
+		}
+		status.append([&]() -> Data::Utf8String {
+			switch (file.skipReason) {
+			case SkipReason::Unavailable:
+				return "(Story unavailable, please try again later)";
+			case SkipReason::FileSize:
+				return "(Story exceeds maximum size. "
+					"Change data exporting settings to download.)";
+			case SkipReason::FileType:
+				return "(Story not included. "
+					"Change data exporting settings to download.)";
+			case SkipReason::None: return Data::FormatFileSize(file.size);
+			}
+			Unexpected("Skip reason while writing story path.");
+		}());
+		const auto &path = story.file().relativePath;
+		const auto &image = story.thumb().file.relativePath.isEmpty()
+			? story.file().relativePath
+			: story.thumb().file.relativePath;
+		data.imageLink = Data::WriteImageThumb(
+			_settings.path,
+			image,
+			kStoryThumbWidth * 2,
+			kStoryThumbHeight * 2);
+		const auto info = (story.date > 0)
+			? Data::FormatDateTime(story.date)
+			: QByteArray();
+		block.append(_stories->pushStoriesListEntry(
+			data,
+			(path.isEmpty() ? QString("Story unavailable") : path).toUtf8(),
+			status,
+			info,
+			story.caption,
+			_environment.internalLinksDomain,
+			path));
+	}
+	return _stories->writeBlock(block);
+}
+
+Result HtmlWriter::writeStoriesEnd() {
+	pushStoriesSection();
+	if (_stories) {
+		return base::take(_stories)->close();
+	}
+	return Result::Success();
+}
+
+QString HtmlWriter::storiesFilePath() const {
+	return "lists/stories.html";
+}
+
+void HtmlWriter::pushStoriesSection() {
+	pushSection(
+		kStoriesPriority,
+		"Stories archive",
+		"stories",
+		_storiesCount,
+		storiesFilePath());
+}
+
 Result HtmlWriter::writeContactsList(const Data::ContactsList &data) {
 	Expects(_summary != nullptr);
 
@@ -2228,7 +2424,7 @@ Result HtmlWriter::writeSavedContacts(const Data::ContactsList &data) {
 	}
 
 	pushSection(
-		2,
+		kContactsPriority,
 		"Contacts",
 		"contacts",
 		data.list.size(),
@@ -2294,7 +2490,7 @@ Result HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) {
 	}
 
 	pushSection(
-		3,
+		kFrequentContactsPriority,
 		"Frequent contacts",
 		"frequent",
 		size,
@@ -2360,7 +2556,7 @@ Result HtmlWriter::writeSessions(const Data::SessionsList &data) {
 	}
 
 	pushSection(
-		5,
+		kSessionsPriority,
 		"Sessions",
 		"sessions",
 		data.list.size(),
@@ -2406,7 +2602,7 @@ Result HtmlWriter::writeWebSessions(const Data::SessionsList &data) {
 	}
 
 	pushSection(
-		6,
+		kWebSessionsPriority,
 		"Web sessions",
 		"web",
 		data.webList.size(),
@@ -2418,7 +2614,7 @@ Result HtmlWriter::writeOtherData(const Data::File &data) {
 	Expects(_summary != nullptr);
 
 	pushSection(
-		7,
+		kOtherPriority,
 		"Other data",
 		"other",
 		1,
@@ -2447,7 +2643,7 @@ Result HtmlWriter::writeDialogsStart(const Data::DialogsInfo &data) {
 	}
 
 	pushSection(
-		0,
+		kChatsPriority,
 		"Chats",
 		"chats",
 		data.chats.size() + data.left.size(),
diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h
index a2235d130..c1dee2ffd 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.h
+++ b/Telegram/SourceFiles/export/output/export_output_html.h
@@ -35,6 +35,7 @@ private:
 };
 
 struct UserpicData;
+struct StoryData;
 class PeersMap;
 struct MediaData;
 
@@ -59,6 +60,10 @@ public:
 	Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
 	Result writeUserpicsEnd() override;
 
+	Result writeStoriesStart(const Data::StoriesInfo &data) override;
+	Result writeStoriesSlice(const Data::StoriesSlice &data) override;
+	Result writeStoriesEnd() override;
+
 	Result writeContactsList(const Data::ContactsList &data) override;
 
 	Result writeSessionsList(const Data::SessionsList &data) override;
@@ -125,8 +130,10 @@ private:
 		const Data::PersonalInfo &data,
 		const QString &userpicPath);
 	void pushUserpicsSection();
+	void pushStoriesSection();
 
 	[[nodiscard]] QString userpicsFilePath() const;
+	[[nodiscard]] QString storiesFilePath() const;
 
 	[[nodiscard]] QByteArray wrapMessageLink(
 		int messageId,
@@ -149,6 +156,9 @@ private:
 	int _userpicsCount = 0;
 	std::unique_ptr<Wrap> _userpics;
 
+	int _storiesCount = 0;
+	std::unique_ptr<Wrap> _stories;
+
 	QString _dialogsRelativePath;
 	Data::DialogInfo _dialog;
 	DialogsMode _dialogsMode = DialogsMode::None;
diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp
index 1c6ba2c87..9dfd21620 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_json.cpp
@@ -887,6 +887,77 @@ Result JsonWriter::writeUserpicsEnd() {
 	return _output->writeBlock(popNesting());
 }
 
+Result JsonWriter::writeStoriesStart(const Data::StoriesInfo &data) {
+	Expects(_output != nullptr);
+
+	auto block = prepareObjectItemStart("stories");
+	return _output->writeBlock(block + pushNesting(Context::kArray));
+}
+
+Result JsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) {
+	Expects(_output != nullptr);
+
+	if (data.list.empty()) {
+		return Result::Success();
+	}
+
+	auto block = QByteArray();
+	for (const auto &story : data.list) {
+		using SkipReason = Data::File::SkipReason;
+		const auto &file = story.file();
+		Assert(!file.relativePath.isEmpty()
+			|| file.skipReason != SkipReason::None);
+		const auto path = [&]() -> Data::Utf8String {
+			switch (file.skipReason) {
+			case SkipReason::Unavailable:
+				return "(Photo unavailable, please try again later)";
+			case SkipReason::FileSize:
+				return "(Photo exceeds maximum size. "
+					"Change data exporting settings to download.)";
+			case SkipReason::FileType:
+				return "(Photo not included. "
+					"Change data exporting settings to download.)";
+			case SkipReason::None: return FormatFilePath(file);
+			}
+			Unexpected("Skip reason while writing story path.");
+		}();
+		block.append(prepareArrayItemStart());
+		block.append(SerializeObject(_context, {
+			{
+				"date",
+				story.date ? SerializeDate(story.date) : QByteArray()
+			},
+			{
+				"date_unixtime",
+				story.date ? SerializeDateRaw(story.date) : QByteArray()
+			},
+			{
+				"expires",
+				story.expires ? SerializeDate(story.expires) : QByteArray()
+			},
+			{
+				"expires_unixtime",
+				story.expires ? SerializeDateRaw(story.expires) : QByteArray()
+			},
+			{
+				"pinned",
+				story.pinned ? "true" : "false"
+			},
+			{
+				"media",
+				SerializeString(path)
+			},
+		}));
+	}
+	return _output->writeBlock(block);
+}
+
+Result JsonWriter::writeStoriesEnd() {
+	Expects(_output != nullptr);
+
+	return _output->writeBlock(popNesting());
+}
+
 Result JsonWriter::writeContactsList(const Data::ContactsList &data) {
 	Expects(_output != nullptr);
 
diff --git a/Telegram/SourceFiles/export/output/export_output_json.h b/Telegram/SourceFiles/export/output/export_output_json.h
index 49f7035b0..879918fce 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.h
+++ b/Telegram/SourceFiles/export/output/export_output_json.h
@@ -44,6 +44,10 @@ public:
 	Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
 	Result writeUserpicsEnd() override;
 
+	Result writeStoriesStart(const Data::StoriesInfo &data) override;
+	Result writeStoriesSlice(const Data::StoriesSlice &data) override;
+	Result writeStoriesEnd() override;
+
 	Result writeContactsList(const Data::ContactsList &data) override;
 
 	Result writeSessionsList(const Data::SessionsList &data) override;
diff --git a/Telegram/SourceFiles/export/view/export_view_content.cpp b/Telegram/SourceFiles/export/view/export_view_content.cpp
index fac8b3d76..9813321ef 100644
--- a/Telegram/SourceFiles/export/view/export_view_content.cpp
+++ b/Telegram/SourceFiles/export/view/export_view_content.cpp
@@ -89,6 +89,13 @@ Content ContentFromState(
 	case Step::Contacts:
 		pushMain(tr::lng_export_option_contacts(tr::now));
 		break;
+	case Step::Stories:
+		pushMain(tr::lng_export_option_stories(tr::now));
+		pushBytes(
+			"story" + QString::number(state.entityIndex),
+			state.bytesName,
+			state.bytesRandomId);
+		break;
 	case Step::Sessions:
 		pushMain(tr::lng_export_option_sessions(tr::now));
 		break;
diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp
index d01d4eb53..94e00b43a 100644
--- a/Telegram/SourceFiles/export/view/export_view_settings.cpp
+++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp
@@ -173,6 +173,11 @@ void SettingsWidget::setupFullExportOptions(
 		tr::lng_export_option_contacts(tr::now),
 		Type::Contacts,
 		tr::lng_export_option_contacts_about(tr::now));
+	addOptionWithAbout(
+		container,
+		tr::lng_export_option_stories(tr::now),
+		Type::Stories,
+		tr::lng_export_option_stories_about(tr::now));
 	addHeader(container, tr::lng_export_header_chats(tr::now));
 	addOption(
 		container,

From 119ee6044aea3cae32732ad655c9c6587460a2ab Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 16 Jun 2023 22:03:34 +0400
Subject: [PATCH 087/259] Update stories icons in some places.

---
 .../icons/info/info_media_stories.png         | Bin 0 -> 729 bytes
 .../icons/info/info_media_stories@2x.png      | Bin 0 -> 1463 bytes
 .../icons/info/info_media_stories@3x.png      | Bin 0 -> 2222 bytes
 .../icons/mediaview/viewer_share.png          | Bin 0 -> 671 bytes
 .../icons/mediaview/viewer_share@2x.png       | Bin 0 -> 1278 bytes
 .../icons/mediaview/viewer_share@3x.png       | Bin 0 -> 1959 bytes
 Telegram/Resources/icons/settings/stories.png | Bin 692 -> 667 bytes
 .../Resources/icons/settings/stories@2x.png   | Bin 1311 -> 1276 bytes
 .../Resources/icons/settings/stories@3x.png   | Bin 1923 -> 1848 bytes
 Telegram/Resources/langs/lang.strings         |   2 +
 Telegram/SourceFiles/info/info.style          |   1 +
 .../info/media/info_media_buttons.h           |   2 +-
 .../profile/info_profile_inner_widget.cpp     |   2 +-
 .../SourceFiles/media/view/media_view.style   |   2 +-
 .../SourceFiles/window/window_main_menu.cpp   |  39 +++++++++---------
 15 files changed, 26 insertions(+), 22 deletions(-)
 create mode 100644 Telegram/Resources/icons/info/info_media_stories.png
 create mode 100644 Telegram/Resources/icons/info/info_media_stories@2x.png
 create mode 100644 Telegram/Resources/icons/info/info_media_stories@3x.png
 create mode 100644 Telegram/Resources/icons/mediaview/viewer_share.png
 create mode 100644 Telegram/Resources/icons/mediaview/viewer_share@2x.png
 create mode 100644 Telegram/Resources/icons/mediaview/viewer_share@3x.png

diff --git a/Telegram/Resources/icons/info/info_media_stories.png b/Telegram/Resources/icons/info/info_media_stories.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b4cb99beb7eadffe7dc268a251206ffc9ee313d
GIT binary patch
literal 729
zcmV;~0w(>5P)<h;3K|Lk000e1NJLTq001BW001Be0ssI2{21+{00009a7bBm000XU
z000XU0RWnu7ytkQi%CR5R7i>KQq4;$aTL8XPa2ZKMIxzKh@fs1LL?Y5MZ(+(inCBc
za4iYC4<-EzLZXE(X5mT#-Kj+s(kciIaiJ3~gh2;15XBsYx9i!Muc-%b<yoG4?m6f8
z0kFaf{{<?QDiVp1Bv~qz%w{tHY;SKX6be4O=-<a0jV71NHJeSZ*W2&+i^XEI*#v-K
zFi25UDwW#U*!X#If*?ktQ8t@ZtJRzlf*>A`=lc3uqtX1(-RJYYy}bnjfiIO(sdRUD
z_j~Soy?!(r`Tc%A;#e$3QPleS`ZofK#Ue?P9CB}OZ)ay`QM|Ra#W2k2=_y}#gTXMF
zOgLVCzkf2Buq+#k#SjGHtXQqqqoX6f0cU4trBaDQk|cS4er~Z?7>2pJy5j$NKKZ6N
zj^EteaCIokvh3a6ozZAisZ<XS4@#wSUJwq4O(qks0GUkIZnu{;YPDK8jyD>OLZKiK
z2;O;{&8FAuc?Adr0+wZ$1pvTsIILEy5{YE~F^0q8eBK|oMT(|rnM}6KT__Z$(`mci
z?smK9dqktr*Vk8GC#X~^pWY^dAPR+|TrTT$I-}9Zd8c~4PSf<VS|4zDcsLjgL?Y2V
zd2(_>(=@{{`FwtJbCa`jaBvU`g?PCG0E(i|&(DcOf>Yn)@%Vf`6y+Wo!?2f^mt-=@
zHvj+}4##*r{`8_2K@dd0-xrI;-v|JJcs$<icK7%9zf@uvCYQ^XO@A#L+}_@b#o~*L
zi`i`U`1m-TPB{XdPM1t3F${ZpdKwG{{HuR}%jKdds?+IQUS1v_AKPp;48s7xY&Oqk
zv-|sdtycR(>vycJtq}x~$z*D^TCdl0I-LL@6bf-1|5xi3R^W%<hhXpD#S@Ay00000
LNkvXXu0mjf$Gl0S

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_media_stories@2x.png b/Telegram/Resources/icons/info/info_media_stories@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc0a2f6e32f22765f563299d97b6962f1bbd0091
GIT binary patch
literal 1463
zcmV;o1xWgdP)<h;3K|Lk000e1NJLTq002M$002M;0ssI2B@5<>00009a7bBm000XU
z000XU0RWnu7ytkTX-PyuRA_<inO#UzZyd)Toz2Y|o3H3}T1hSp$3(=`1W~aUw8cO#
zBDzp9Og0#SFW?Kc1*YOc1wpaIAfz_Qu99N0qKkPUl-)Gu8^RpS(8RsC)aQTk`*See
zjz@jeKm5<<>UjK~@ALhgo#*8|2O$;~78Vv37MA}nLBt&+A|g^#Qxg&r#A0!DbhOcE
zoSvR0-U@k=l9JSFwaH}K+}y-FuvjcRWwo`nb#-+Df#9@q`r*Qb3!|f>cn=#J8#bG*
zy1JTD=H}+Yzno5IQ&ZE~vu95+HbMxAL?XRjzp=3av9`9hK7anK(P-pyd31DiP|)t@
zGbSd+YPC`o{`&PRGc)s4k|TtWR4S#?xZQ4@P8Smsv+q?r9#5rGEiNv?g-@P5If1Bt
zC@wBuSy_P)FJ8PzNJu!)<y<bes;Ww_*T==h9aBO&Zr!@|=g%LEvB%@NbLY+}BsdC}
zFJE3;Tf-PT9F8kju8^{=si|pbXgG7`3@JnZL`Fv1Y&MLs%jJ^G<)m!SpFa;zV{2<G
zDNp~rdi4rgs;H<SXv^Vn78VvT#$K;CH8qu>^Iv3SWNdG5V~o4IyNR2Zm6gG*F_}!n
z&HrU$Vgd$lp-@P#%+Jq%`t+$*tCdQn%>9i<Bh-+Wmv^+J{>aPA1NVmyAJQu|8V&qV
z7-OH$S65fZunDnPJTo)1y1JT@l5*s{d+6!ufh7Qs$D@%&Mn=NK>~uQU*Vmzpfq?;r
z!wZkcqxU~Nb8~YrXVJ?F3JM^!OeQ;b?%cO;-{8+LU%te~#vV&DCdgzm$Z`AjZF=SP
z>(@akkw_3i0)fC_Fu<9Gg$05m@4-;1RIs|-cNx5^0EAFbP*7W28}tg7%T-=pPVd0w
za#bqTjT<+}L<id2+cCzov$Oju_Z9#sl}f5hXtmnC3ss7WiVBzz7<PtWCZhQGc!ZGM
zZf7F)%h=dhZf@@U{5(Raxw*NouP=}ix=LthD9HlEVlhH!X=#axI1t~zf6vOwGMmi^
zp$88hBqb&Jm3h5hIN)$NOl%nyz~}Q3LLQHYQPRLHFE77+`xcI)qN4n2j->#&_eWg2
zb2yx?t}eY^j}Wq2t%2V3mzS5}0AtL=HiU@?3V;XcaEL^r(b3Us*RCOitX6AjY3b(X
zreEc!PoJJYf4;uH?sPg2-P~>(8yhjkD=RDeD*cW^mo8oU`ST~7fB*iyNF*Ybj3Lxg
zF(V^`Ub(k`n>TN+uCBt#uC6YIPc~ure7@W5#u&G?wb3i70+N%H5kmF#^;=t8Fs|Rb
zcke{P24H-A9Ai8+HAOE=OG~5P^vA}=;Mi`rXJ=<KX(cu`wxpyaIGEAUaxf|@D<RF*
zt5@ljX0sXZz+$m5?EPV3VbE3fU9lgAu&^+@-HtK-`t>WlQYaLTjEwyL{oCPi=yW>f
z%i~9n9zo4&wfe|;{c->PeP}?TP!KmyPELkyHa9nST(1NnAtCS+FD)${bSG<HhK7dV
z?kJT?vd(`|R8$0;&y$mrWbf054<9hbA3uI1=;<GgMgzC%_3PJME|-+8L?WrGstOMe
zCuK;-@bED8qR3=J_diEyXehPAH9I?-laq4@6RA|%+1a`C>%mFW($Ye0pS*eVhWTDd
zVq)Up;2`v+?d|Q;{LG<LDjg07)Zp{^3<ks9yLai=YD%Tj<MB|_+{>3QPbzEx!o$NK
zKYr|Txu{G&pU+~k7>&l>-roB9dRP=uynFYKnxP8|3r{QdZsOzPwOZ|uA3yLOcFo-B
z>FNFb{R9Vl!k<|$Uc9JKDALl>#9}dgW_|YTSw}|)*;ZIsSXfwCSXfwqe*p0_SSd{x
Rf9wDN002ovPDHLkV1nzm&LIE*

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_media_stories@3x.png b/Telegram/Resources/icons/info/info_media_stories@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..df8a23bbdd4cd4568064dabeffa7897787d3fef6
GIT binary patch
literal 2222
zcmV;f2vPTmP)<h;3K|Lk000e1NJLTq003YB003YJ0ssI2ZTjGE00009a7bBm000XU
z000XU0RWnu7ytkWU`a$lRCt{2n|(-AT^z^noVqsK%%$XevJj!tqNWH7iy9$;_8w`G
z2`MowH3Ey$gbFHYLhq4akdRYREQ_L8X@M<Erim^m<Xhs~(#@&cboV}g+=s(`HmBRY
z+r7qfK7ZX^_WPah`R=})^E+n<0)apv5C{YUfj}S-2m}IwKp@V-Vp;&=<Kq(*6%`N=
zz~}Qh9F9t*(&=<ei(;CXm>8K%R$X1K(P-x8<}d=o!^1&AK}-u|h_!3iUcP*}ySp1(
zl$4Ydo51Ac<hHi9rlzKhj0_Hk!-#m#5*HU&RaIrRS{Jtl!?1yYfz_*5+l1?MI`D<j
z(b2-fLO(x0dX#(o85ABKE|<#>9XiBf*|uf1TGeWGS67!tqZu9^o|>Ba^XJdkuU`!Y
z1MzEYHe0XP+c+{bG*nnvSYBR^VVI}8;3-%vR(^i|)YO#CHKNz+%gV}9Qc?s0fs?6@
z9Xn<)80;@Mxm@niE_e_gk5^MuW7D6$zP`-NOfHv8BB4Ma$j!~wYPH0KLqkJuS560E
zVPW6De<${2a&oeupkT#{6%;eG+3f7>?BU^IJnge*&)g!IR^sF1hlYlLH7JVSxpT+<
zxl08;pP!wbot2eE-Eq3vxpU{t%nWd6dV2cc!GlbTS~iG8BI5gaaBy(TmMu(+S~jd)
zxw5091Gv-D(h?F9LZ<++Slrp!IWRDA`0!ym+jR;Si&a-w2i(zWwE+PEG;3&VY{Xli
zot@pVVFS&rFF|o}F>uFVFsxs{o_-DQ-n|1F)M~Y_uP^-?7A7_}7ECx%6irP{Wkg0&
zQWA=yz@y^gVn$>v0Jyd>3@a)sVo*t0SsC#K8WtADpqPKzzkff_)YH@B<Kx4i7@<%&
zIywsUl$Vz?DCS>UT3UdhqeqXr8ZMDYDl03yySvqD_5J(zLqkI;>N$S=IM8D@o0<K=
z5UErO)U>v?+RxV)=fZ^xHU$u)(Rk#@5sIqbym^CX>+S8On2IV@RaKyGyLaz)DOMtp
zI4Tcay?WJEuZaW(20ncFu(Gl;Ha3=Y5;x%Q?>{>`i+AbUw{Nb7KY#ulIMUwU{`>cD
zqDS@h^?W|x6SC<eBO?R!CpS0O)o@UFZEtT!5X8^V&!#-m(a|9ii98^jE-ET2@SdP3
zx@OH9SHs80$MNu*ni~8Ji^aNf<qB~uqtoej?b_vT;dC)LIEZ&ctyYr=w>Jpqz<~os
zqY-#CH#c|Y%o+D6rjw|sDA0$}(ozx`92tZlNL*apuV252w_90R8HuR_m&;8}P2IF<
z6A5`#Nls1%>eJKHNn}{WAOt}I0|P&N_(1gN^XJbl-N-B!tGT%uk7ZzffA;KIAYCjL
zlgO~RK?s6)dwV~4@PO!1e}BL81*UcD)`7r=h6WM}alFWdMMXt{W35(8y7@(zpP!e>
zWT#J`o|~J)&qPE-eERe$K0e-=aFfXdj)Q}PNy{K<5LmIXTCIA$o^<m|^7!#%sZ=^P
zHim!X@p!ehwcg&|O9qaQkAq_>YuO|X0;4#i(MY;&CuwSGijR+PYiq+lhKGksB$6eg
z%x3e<%nW|)@9$5#b&>{wcZ$VgA(z3W$wXgY-@SYHz(*&`m25T}Kd@S@E@>k|(jai*
zkX|FlTv;;C%gcN8=n)9iXf)15fvG%rijjb-K|CH0{7{~N!{L;dmzS27f>!WNGtR_<
zTWn%tg0ujV1`+o-Q7_^+A~-ns<HwIDPo4y4luBh*R+f|DH*elV5X5S=mXwruLf&E!
zXR73;OG}P}#9}d+&EVX<d)Jxw7m>)wNGk7m@+2lE0^K<|IV3VHZcuu9I&p>8Y&M@d
zb&9fT8W9Kt^Yin#dWAwkB7-A?yu7?_+_*u!a7IQ(wr$(yCgC&!s?j4OBP7BXG)N#2
zG&D32-Dz!YjfjYFpK2Ppb?X-BMq*;3tKnl~V|Z*`T^)WVIy$<mtBdH4LZRUCc<vTX
z7fDG;peMvpoRfeHaD04RB9Wx0r%y~wfP^TDUb=LNiac`LwA=}c#p><t1)X$t#8^^N
zf;qroFi=^Z<#M@7r4qw1rBcbTP1mkn1HCFNEOaT>W|0rj)6+w7>j812Kt@Iel~hy-
z3kx%w&A1f3ULP13=u)^yBqHvZK~c1#qCzMXQjsJS3iWzD$Z5ajnr@yvc>>gssJ*h;
z?4+cmf`WqV?CgyjH&WLlm&<{k&dyFRFOpLQw;>XVz(r&-nTUln`bkJg0HZUEkxJ`=
zT)%!DX!`Qy3sZj;V6)jje*6GRR4Nsf<ym*a<MF`j48yRePoFX(WBc~)K!e$AcFPUx
zHWCsNOePc1mzS4EzlP}OXmEcqyG?0fPMkOa44R*xPfkvzU&G$Ld+Y1#FJ8RJ{IhvM
zZr{EQJhE6Uf7$$-2H0%&+qZ9tqdvJ@zD$2irdYLVmCcIit5>h6EGjr6GBUEDpdc+R
z&Fz^zj*pKIarB5`*vQC;OeS-+80ZKNhf`Ek1j=b7cHS%t^7Hd;HjrsFntl8BQOU{W
za%D1^&Ax`(+FCbBrky=|_DoJrV*jPNxj8d4lf>?^w6rw)$t{YafA^ndLPJ9p3I%4z
z^z`)0moM}3^0sc>>P$&2C@9EcvDhT(=;+w7V~6KE;d!J|DcFf(4~nAw{rxJH>izro
z)z#GtK6fJ`B5WRN=gysT*F$Z&AeBmAzkW@eOgduo=FRru#l^+5v$H>c{*=jN%vTFN
zM@UG>`Sa%+8yi6lz#c~nZH!Dl8Jq9&csz+j5*r&E6B8319W4|J`Fy^wukVW&FLH8n
wu)p`u-Vg``0)apv5C{YUfj}S-2;_gvKVcGYWmoR)@c;k-07*qoM6N<$g6p#^OaK4?

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/viewer_share.png b/Telegram/Resources/icons/mediaview/viewer_share.png
new file mode 100644
index 0000000000000000000000000000000000000000..14861b1ee6630af80b47dfe759dbfc03204b209c
GIT binary patch
literal 671
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfr-=8#WBP}
z@NMwP)0v49&$TsE7rN;1NGhcyo|tgRD`!nkgogICf2?65sZaGcsB<sXko5BO5EDt&
zFiMl#|NG9961yvJf4}>5Z?on3-+$UZ?>ql<uf!SqKNm8#WSB^CwQ7i*itkmF>yOu)
z{`%{!*IzAU&Yj<T>us6Y?5z<xzyH=*$Z)YV$yYT7s!e9xcroKnj@jdn5lRzl4tJkT
ztG1b!V6Y=<?K?MxYgt=2%sHE8H1o~*IHidibN%Y=<{f@G!ADJSrqjY1=bxW`8npPL
z!ddgylPOJ+4Hhzdhrf5aJks{mn(C)E`D@kM>#yT4-k04N!6SAzZ~F1ad(&-pIV~(O
zk;>g3tvCJmi-q@o*3@mkz4v&)mg<$4Uq1K}xBk22#M4h1e*FEH{^0H`7p1AEQ#q!4
zEv-7HxO@BU-lIvj%=+0zGh^0<HNGj^9q+)iEJEko@4wa#3IYk|J6#rqt=1Lm4$*4m
zSP_!7_+rD}9Wi=KelBg;AD|(!;OO(uv(Ku@2yY74nwqf4qwhsY)#|HF(vJ%)_H|5T
zmFs6u)e%cJn%R7i;fUXI;qIeMjGkuA2Vay}J^y?&ZL{K`=S+RPZjBBMiz=+<@~|-<
z+9B<DK~e1Z<BM5al`hO?YCn9?c9!4rgy)}sUf?;w(!;61vFc`yoAdkZ-FM^GUss-Z
x;`nj)Sqnq7w5NLMO%Gmu^~JyczitcvVC~iAGhFpOwh)v!JYD@<);T3K0RRIz8_ECx

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/viewer_share@2x.png b/Telegram/Resources/icons/mediaview/viewer_share@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c6e9341e6004f3e02f502ecabb4a42e54029c4e
GIT binary patch
literal 1278
zcmV<a1OfYrP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGXh}ptR9Fe^SXn4`T@?PzLxyBX
zAwy(}gc8YAQgVSrq=ZNkigH2W#)aGnAwxup5M?Z(loFy0DVahfQ|9^qpT1qI(|Nbo
ze(_)Y^}ZKx`>eg5XFX?~z1Lpn{Q0B)sWYI?z<-+ojo+fTwzduo4Aj)roSvS(y1M!;
z+Hdk?Wo11+KK}FfcXxM}oSghkqF;ge`}@DYzcV%TUs+lCE9BoHsjaPLI<Bv;@9*zP
zQ%z0Hcj$hO85tR2A}T5>JUu-R4h~3DLqmgxhHA@H^XG!Jw6qw>+uPg5#zsg;$nNeg
zv*hOHwzjrnrHe;V!BvnH8G@#*tu5xCk&)5W)upYiEeQX8K?r<&d>k4Yy0*5)M5Lso
z^!E1Z=;(-O`jH?bBO@m#Cu3t{#YksoXW`-D%gf7bDn34bXlO`ZUtiJlrxsgV+nk)7
z(b3V{+gn<^Y*1EKCSx}>HJzWIC%37oDMLd;na}qxeSCaoW@d<8aiAy&!H`8MYHn_h
zn^DzdnVXw8H8m+#Kc?yV`T6AJ1nZ`tpn%J3U|=vYF+n%5u&`ieX2$gtGCDXo?CtFl
z9R>*B*47pm7w6*Qf`=l)qNk@fJUmP!D=RA&78W8vxsRix<Np3W5n-H=kf3U<W2D&m
z*lR?EiiPu79#8o-o|Lrg;wV7!l|z)zqJ(yKc9N#`^>x+uU3+^wfw>D-d3f$DvfJF;
zOtjV2)m%4@5!LqN<AWhpfN_{OCepOCv%{I@nowt%n(ggvEJ?1<w~c6+82RPpr4UI_
zP!MG`GBQ#KBkU9w7Dh#NY-~&jhpNK_EiEkx0fe1!dk+r}j2um-hlj`4P&^<s#SD|5
zpZ_&L*dsbR`swM3F~7dPCM6{aK_em}2xDn!DTEPlij9p$O=rw6FE2Q51wemYii(OD
z?6c=(Z*Pw-r@p>^c6JtPAGHc=6a8y(aq(v{5)%`V5=M(8#l*yX=E<>lcXu;nOG}Gv
z+Re>vU|;~9A#;%Ze^#qfQ&S0o1r!w(C8Pb~5=EUwLIWywO;1n9D~?Qr^pYzmGc%J;
zWx&xSNjZhawC+$F835(Av9VDa8|Q0(e}8FdX<AwuPD(bVC<wPi)%E7)CO9})NP*M|
zTZ{tZ6@pFG)zy&^B_umLTiTC~kB@T&6&4l}3fndyAV3;Obc}69EOvHwfFTWJ+1%W8
zb#;|-aRrr>lo0CT;=<S0SB4V3bar+oA`=r6FE20J9avj9{}qW81(lbV6A7AYZ*Ol!
zl&A#?h)Y*jmpbCp(^DH88!n4XP*qhGF<_~=ySsByqDJI^==%ElXsqP>K~GN)b3+e5
zI5<d<!^1<gl%j!^_^qz45-A%@PEIQE&&$h``QsA3CAv)|C>5q~bkO++>Z7Bh&mI;d
z#g1ECT$F0UgT~vE3Lzz~{4W81ety{7R#sMcL%_@V`T4m7!Wp6y#esvmg$;!dRvjH3
oD9s!{wMm@;bq3TK_#b58Uv8BPryNFOzW@LL07*qoM6N<$f{i9H?f?J)

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/viewer_share@3x.png b/Telegram/Resources/icons/mediaview/viewer_share@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..43310c8956492e9536765475086057a19275159a
GIT binary patch
literal 1959
zcmV;Y2Uz%tP)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS?5lKWrRA>e5T74{5Z5R)U5XDN3
zn5=w6Qbuc1=3^>qOKFPMW|qyCW|DuhTGp0kv-*eiIW;!2MNJwtq)A~F+FOe$q-2Sr
zlDzM4dtPVP?(;n7xt-^D^vCf>UFW{9`*(fZ=Un%7UH3C@p8BVHK=pv?0o4Pl2UHLI
zbsjJ?zD5fdE;KVU`}60I@kP&yM00cV)2C0jx3^DDPEJitb#`_hJ$iKh{Q0vY%xIuX
zmo9B?ZvI#F_vFbFM@L7asiC}}n>TNYjQW3|ySsbUs#VI1Gy>wfb?bClr5W$*>+|sN
zFoGrui%3pRriOKOb$$5ofsTOw`0=BUkB`EFXB#drFOQlL5fQO?@#6aWdO8StXlQ7|
zh7C$7W2zL81P<aMogEt+`}*~3R8-Xc`}gU%gM-7vhYvSx+C&Fs)5b7Nt8zbn{MfyF
zcV%TIowc*GtEs8kx^*iZlt<5*VNmziuU~uj?kz1XrK;Q5*xb8!FETQc4$7eC(lDs|
z`}gky2M!b!6;aiZnqVhmVq)mv-$-A$aDm2&&6_tfTAG-c6c!dz9?%mL6Z`h<V>th=
zN4<t=)XmS&XKX|Am6(_)&lu%XH3YXUTekT6`ntQjBc(tx<>chVqujA$#}HAnva$qb
z<jU8tU$?ZhymaZ3FlcN7sY6aqPDe)v5?sdd%%k{Kq`GEjXEXc&M7m6Qj6`qWzWwpz
z$BZ@-kJ5%g>5PmF@$wTVP8i7^h}pPtV^dR;SPAJ7y@mlfH8oYd^QTUonswWdOq@A$
z2Ekg|2F)zk1_lQHnf~+l_vg_sDJcoXD1)Dyo6ECSCeM;3OX}+C7?Hx``1tspJ9my8
zIf4*^56|A-9?@K$Bph)Fi~_QTxOnlRY(ObBR<2z6>eVYo7#?x#T)upH=gyrtUZvL0
za4t4B7RZb^1W8j<MN<pxl~${e&U$)!&QNDwb~p>i0Am{}R~!bs5Yl|;Pds|`h!ODq
z{rg?Jc1bTZd^S8h95n$Wy12MlnRhZXGn0Wv$>`$ZVz}Ns2>4=0M@Jb26gUH1U0oTU
zy?y%@-y4rkO7R2*1q}}mQ`=xFssV%W5$Gu5yLa!Lot+Irl(l&M`t>NZsU`*$Z+rIa
zp;(~tLCaE1fkik-s4{73X);0af#O@DSmovAGO?6ccJ=C2s?5QI2W6sSuPM;CZ{OzR
zCDdqt{``5W3}`PeFPVm2xpIYKojiF`CXivv@ZvCa9khD-^r=i#q!Bd8B6*vW?SfU}
z68ro2@0@9;7UPkv(e0|Ot<}Ob%R|VXu3fuE)c}oL1Pwg-lB=q!DB__*hvZ|)T8X>~
z83xq>G)~m8urOJ8(~IbV2slGe!)aK~|4|&>x^;`O3ehVhBt$QQRL{fB%?-V10Rs0x
zc&&v;U4)gD6>cHWtj1xweED))TU)e2@h`?Tsa<q*bW2N%uI0kGg@r{~SsBxW8yOiv
z!H()yV3R{I=Q3LG5-3QK_wV1|*w`qppct3)RM2SPpl!-5{PN{XKtO<m4!l#Sktwu9
zC+zUy!@a$|lv#q-YZ&(S!Gi}3!spMQ{rvoRRhQ<wd-pCy*Lux^ceSFTg0ktQr43uO
zXi-B$0|SCfrnPI=N^3iVS=3sB0EAcZeKCX~#}{}w2?@jSHHy1K%3llq{P|PR6%9ki
zCBebL_>=_(fj}TFDJeOA{5YyZv`Ve5t<j|rW<lsO4BZg~70L-3Wds_pjA(-%7c5vn
zWuS9N;c(>Rj*BvYMj%d4PZu8vOe&<+Ff@#xKYz{uBKbxgNaZR(LlPB(lUe{>vu4ew
zPoL;4XsCs8frk=$4a2E`Uq>*2uV26B?2S`d0<E*xgV$YP0)dQC^QgI-TefT&oxO46
z25utXym><hIcaE*Yg=z`Z_1CB5vq#*{(d?*BU-0nI8rGPJ}1rN*|TSvK1V@80a7h|
zkd&X3#+!y1$ZYk?moK+%+lENU38Ixvr(s%mEHE%IB_)Ld;XV@WXgb1ABPzfu=0_f_
zt*xP<p*XepakO&kG>qv;qD_r_6tx2#fA;JdPHi;VwL(h|qDhQ5klB3vPzTv9G7Rap
z2F<Qsy_$Z_Xo;RXcaDxAb>b|CaJ@tqV$b8_<7s-WnZ@qmbT&w7p2Z0F;@&dV4;PR;
zIN*tki=zv*(#WB3;&B2R$|f#a1)X|&dicG&V#NxS1;Sb}QC(e)tk+O=`PU<4(Y}D7
z5E=wL6%iGLD@kEFLm>R1LZQsYpix=Tj1f8w4i2IsnyGAr99%BrhaJ=^TsWa^t|`)H
z3aFP~|IHOH%M%h3a9NHDx~;A4_U+rknWh(=VZe>djvYHtfFkg`c<}-$CEUdDsE?`#
tR1c^gP(7e}K=pv?0o4Pl2WHy?{{fbxC7#M=G^_vs002ovPDHLkV1lB_m306B

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/settings/stories.png b/Telegram/Resources/icons/settings/stories.png
index 5cc794ddc38ecf373529878ce1bea92f69f52c2d..c41ac06dd731404239fd352eb4b28930076c0edf 100644
GIT binary patch
delta 549
zcmV+=0^0qw1)Bwsfqw-_L_t(I5#>}psQF<O_ALe_3>L|#l*vX>OiC$>K`aIq%BYM!
zb|yv{EWQ?{SPWuhVIn1A;3nVx|L6YP-uw2uzuT?5-ZSWVpXZ$Od(U~#!Q=fsGnq`O
zRI1Tv3<d+Q*ZZ3W7z~D5t@e+x`~ALLF8zN0D^WU~P9~EAfq&rVymq^Nx7#5}y<T^@
zToQ?djv0+ckebb=LZP5}HbyybjCnjBw#J;*YIV2UMXpk*#Od%(3WdV)cznCv91aI@
z`dWopEKVd6R;!ixut6XYz^8J#91(On9m;IAS}K)_kUxWVyN&D6F>bdTsbTZ^++wk?
zQlQMQ_vL}QTz@XTUXS)BiI59wBoZMinteW>!{IPtJ|0q|(cpn*vl$Us0BSy;kHumy
zUa3?{rBXx@w~s_3kq8u~h6tR1&*#7R$KwG&m-ZeHTd&v8=TooOGbfIp>-8Fs#|wqR
zOWSNV+wFF>T5-7c`~BnbD3{BLMta2AY}TxjC?4uZQ-3idgu<@i%a~*ODT(?mkjZ3N
ztHa@7G#ZH$X$oNgLZOh&W}_K4b~>Ht_hPXyo6T&EIpd|u<uWdgt>$$2B!j^q`g%H@
zve_&q`%_?0wOU1%;c%$cYFQG;5qmV5Ob`IWWOX_n{3X2nGMP-VSfn3>Bc7(Pz&@X^
nQmIU*(;oePAB{$7^{?UoV3P6g1z1t(00000NkvXXu0mjf*}f3t

delta 574
zcmV-E0>S;81+)c_fqx!JL_t(I5#>|AtFd7i_FKrWEC!n}2$NEVlQNJLgA^8JFer;M
z9A}`MGAf3Hzknzyg@q)GnB-SV3>E{DQXJpi@%6m#ao+d*0pIsb*L_{r^SsY}-Oo#*
z{IiQr))|#b)$8>>pU>@fGZ>8dd`{;4jdwU4lgZ?Mzb_VxMt`H>a=E}wqtWX1y5H~r
zZ70&;<C#q6cDoe{1*KAncmQz6kB<*Hy;v;bd19Q+W)Fvh#bP0rDHN;Knn)zb)W5vb
z>4dxdathiHZHmQWRDJ_+^Vw{c&*#6fB!$=O-R*X4Hk&{<7!1(ATrPiT3WZ`c8f`Y4
za5x+Y1Xioncz-<BYPAnlrBZ<zW<qe#>+9=uIz>XYS_SLV>U28D(C_!(-ri8jKu9K&
zEtgAF5=MY0UtV4yP~@i}1cSd`uc$1QN(dwHL_(}c!azJ8hfq*Wr_<pn;#=$WdiZqP
z?e_coJMv?|Y&L&>eoCcMMDcjMZnyh5Yd9R@`+^+6?0@likQoC^CUd*pCX-2wqENV8
zZlzLbHk(4B5YO9ew$te(5D4%jhJ(T2`Fz&tbQnc~ARUQBLLz?smbF@Ky<V%;YBG~X
zqlv|0WGcq<`FzOucsxcT5e|og3127_3SBOj!C(Nt$dg1+!n~r<=zhP46ySpCblPY%
zK76fKD>s!&u~@91+lU^*mrEoP+3j|d$pix`m&>_a4#xL$|Np@J0fF@7CM|i5&Hw-a
M07*qoM6N<$f?e4iqyPW_

diff --git a/Telegram/Resources/icons/settings/stories@2x.png b/Telegram/Resources/icons/settings/stories@2x.png
index eafc4902550e5b57acdfaa4733ff903a722dad2e..82e24fc672e249cebea379fe1035fe67bc2afa9d 100644
GIT binary patch
delta 1163
zcmV;61a$kK3j7I>fq!O6L_t(o3GG-(C~jdG{!N+Z`63ykkjP3XD^l!?S&@{Xh!sf|
zh_Wy&SP4-q5M?2vL_&!|BpdUPF*4uhI(h$dzjJ)fIX8Fpp9Syte(&>q&-teJJ-<$8
zcnmXOn1P?20VB=y!NI{%QBl7>9w#TKv9Yn?;bF~e+JO-f5q}L04I3L9?|(n9ude|C
z0or*rApQOQ`}_Of-roLJqHl$UhKgxjTwE$EE3>k)jEuCegP2fn8yg$f*VjKiJ?RJk
zv~F*2x3{;ar>6@F3iN;vYj$>4h?<(3^6~LudbKz0?d>NfCO!rH`445|<Ky}H`5qn~
z9NvqI3qoC9UVkPgCUSz6c6obyZ*6T6FV<^dU;t~U1YcBCgpEbmhlhvc<YXnlFJr8%
ztX5W5i1Fy?C^k0s%V@cd`1ts}y**-he0;>2kPB4c!%iF=93-x#r6n&fF9kR@#KFN~
zVPS!IN=i!D2!-v!!a`!fA-A)$QwWrX+1S`jPELaD?0@Xc&(BX9@Vy5V$H&KlAnZ-f
z1!HS#`#qgbhjSVp9<KeZYHMq0da|>#WruWkcVAy$$2P~Q<|vlMs&kr~o8R2r2!gPw
zzq;1y>gq`J)zwvMYO1<nlt-teq|iv5gWB2INntQBRY)l&CML=UQX1LS)g_E{etymw
zfDt4>aDQcEDfuJeI?c??L{jF5>3a_j4g~h@?ryzTV#x{u5OB_Q<>=^ma&m$&oH$EM
zO9`vam~mNHSdjLanHi>6NfU4Y%gV}RgCip&-Q3*Jh14sX;8;PV2AKW*eOc0Pou;Oy
zEiElQJw4I`f$dLlYinz=$zTO}dU{gd;o%|K8h?!3+}wnO1O|xpB99_LSTX`2^^mR2
zz+Pd1VvpFSFm{H-vm%}?0w_07&7mY>;<2g8Bm2UvAf$8xi1!mYl^8ECFXiRsIXOAf
zU^6o_a^BzHlg%^k?(Q&|cp4iUIdR&KJ(Ako+#JYvc6N{wIQ;r9d_R2XdEeaJ)O#hC
ztbZUS7b7%<goG$ngK<%t3WD&LsoTY>7(qxKm{HEnF2<^}adA=U+1uOu6}Y0Jf^?%~
z{=wu51w%ta!eDrB+^w>jpnGj?4Po>1^FLdDVECY*py%gjLDS;mB4@pwot<UnN}c$+
z<?u-3Sx-kt2dxN_0Nbsy-Q3&^w$szouz#>HmC(;{++aAFq^YT?Nt&2GV|o99YHx2R
z9w_5Q^ZRf(kqe1ybaWK8HXp+DVnIhnMu-7MdU`q&qpq2hlqCLw-re2J$jDGQRrmx$
zy&zUoQ&XdEc-iQ<xHzo6n0I-38F$sUl_B#1UR6~k%df@h>+8F^x+>NL8+R2BV1Hj<
zAGTL%X=z|!AVZChZUKV#L7mR@{%aFSvADQcs6H8GLi76i+SS!nJPc~Wwzjs`*4BTl
z8pNEejnBgB>T2XTSxqr9F)ToRTjNhj_&qNzEiF1a8YK!QKsZf#d3g}3UV{N)m;u8K
d{MQ-y4YQ1YdLI4Z>i_@%07*qoL<FuvV1f%4K^g!6

delta 1198
zcmV;f1X26^37-m(fq#ifL_t(o3GG-}D0NX3zUH~eJeDCvghC$7WhfpfNghb#LGmJA
zC?1sXVt7zq$dD4Ekoke6B#|L94<TbAnf+h?{yUDd_c`vl_v(MS_u;nB+TXYK+H0@9
z);dR_(EhX@(0br^J)ozidR<*z2L}glZ|~2~&xeNxHMOZo*MHa7kByCOY;4@w*?D_=
zgDLpEyu5gMd8w#TWtx$ZQE_qc<>e(YLh$eB=f~HanwnZuQ{(IF%NJ^LoRN`naB#pk
zVr^}$rKP2^vNAtEU$C&TvGK>p2f(kdul4oyCMG7DGzDkW)YQb8a(;eZT3YJq=_#=W
zD06dj9A$ZVS$|^(YH4XXF)=|@BWx=wD$LBxBuwF^0K+zjpFp3VpXa7jA7G=Clas`n
z{r!D^e}B2UqNAht_V%D*YHCU@mx1R_3j4FQwPhfb5E&X8#>B*EAdE6IGdbVb*w`qs
z^lw6NO^%L^U=ZR)g_9C94S^LA5rOm}1`w5(l#~$nlz)|#iGt)$^z`&V?a<JWJStB8
z=H>>>)6-K^Q&V|lN%YLj4B2O3U?3By%ao!rSXo(Ns;;i?@$pfBLH#N!Dne@6-rgP@
z9E^;NBmk_@-QB&lwY9mqSzcbwjN-AeF#^uZ%M+-TdTefPCi`%6bE9z$4Gm<L`$uxh
z&CTWN8h?@Z_xHne0Uha-%pw;ErlcN^kB>oHT3Vt(QBhGuqcZ<lSy{A(#l=O!b#-;6
zXN5Ze0<Ly(alv4Ac9te2BqY#6F$oh~U0n@4((LW+Elt#?MUIY+M2Lu{XFWVTXrY*d
z3GVCb^YQUHIXPkWPYi>coSY;KLJuUiJT2ed-G8NpkRa-ratIkm#$~PN=VzLRd-VSP
zPRp5+j*bqdTtdh&=<Mu_5V*7B^ViqcQQ-IzvGKsb0C&j+7#taz2!T-a5m0gq(C;xp
z78VvbIO$t<b#=u|8BmBw2N&D6v$JFBX9(}_@4*=v8DY%*6(L%oQOqVpYiMYw7<YVp
zoPQ8ylt_3RoTsNJb8~Ysy#WCMxVN-Th`jjtcrg|V0Ge8`z@^is7)z0plS5WYOG^_4
zDHK6LK^GSne49{b5)%_8DB0QB1mD}+D*+Quq5C0q0DZA=?)x*zC}fShyF0F~eMfyg
zwYRquaB_0;msFWiTU#4prlh1u<K5lehku8MQO1LVgQby}t*xzv+Le_RJw2K4sJJ7D
zA&^nMB_ioZ3j_c(>1fbo+<879y$5j;Vq8Z@hd=dy#;CGfWrnNOF#_etz`#Hq4ga{#
z&dyxE>Fn%8qb*k{?s->N7v9cRS64OT=8%vOq+N1$3kwUl>dM;`7Z-=BPPj<6N`Jm#
z{;VH)8L#m~FZK#o;rq*vw8Fwd?zQ#$`WoLJex%3%pnjpw6Pw6Cj-aZlDk&)mt+c(p
zJ*{hFV}l?^M@PAqhK8vn2@4D3UK{wvVT=~X+S;10Jv}{rdV0E`pnxy@l`-0Ce62b>
zJQUaz9v-g#dTM1Ce4#>k;W+TarzfM<)>fFPzD-&US`TPF@T(s912A7pPFoq;2mk;8
M07*qoM6N<$g1MVcUH||9

diff --git a/Telegram/Resources/icons/settings/stories@3x.png b/Telegram/Resources/icons/settings/stories@3x.png
index a1d6678e0911991ab32a1ebfd118d9799ee0efd5..23c70a8ccc06db02b2d3a72756094e83b2f5dcb0 100644
GIT binary patch
delta 1739
zcmV;+1~mDD54aAHfq$Y&L_t(&1?^f}NL5V~_L6zSyJ(qr%u*4A3?WVO!3QxfAc`P@
z_Rve1AwG!EL!@aLiXb71hN1|Hs73mq4<d$GXliN~&?vk{N-7$f+W-CS-|aN}>^b|K
z&KwW?`@Ebrv)8P(zBPN+tXZ?o%+#Oi0o4Pl2UHKJ9#B1Ca(^B$=Vy_PjZIKcP;hXt
zi;Ihcg98-x_xI1t%<$95QL?qQby8ANb#?XIw{H&)4u1Xl`}y-{N=gbx^*ls}hKAPG
z)~>Iw|A~cey?XVEhjuP9&z(Eh(9p2IzfXhE)SjH2lrzH4&Mr1KHZU+yj=^~S&d$!|
z<>fm&JDMP9?ti;^^QK1oA|fKbe*G$pX=!P3a&pqhW{l40=;)P|70u=!^<tSVFE5Xc
zjXi(<yr!lmAt6D|vX?Jk9u4~U|I3#zadB~S%pCh~+_<s3yGxov(o<7Y_wV12jEuCj
zv{Yz>ba5UY9+sAtDjaSAHrQHKRYk%O=^sCSJbd_2zkfy2-Q6w9AqfN|b90V+(tjQS
zs-B)6Qn5fIav*+qdwc8cxie?Zyng*!Dj7XZO-;&YUQYumD=VeC-@SVm9v-fzL}Hkg
zmDPg>52U*vG(LIqgkUr@iFGAK)!W-^*a(K0w6L&1DZ+a5^z<||px^RhVq&DxqN1Y0
z!otGf`hT!WKYjW{VLN~RyulTjVNO<8S1C=8A3rv{rXz`1t{*;p5H*gDjvhTR=zm#R
znW#e`Uc7k0W3EW6kvbzJVh^mZujld}B4eU*i`m&(M@L7}2ss)_y|1sYG2*qH1&sd2
z#zu0w5Ve%Vz|*HsW1tY?km*N7MKM&Iz@xCRFn=ok!dxYG+`D&=bO!>@ty{O03^WoC
zHW1Q2ypsxdVPx^-K!I*;ZMC<zH<IZLHF%xY*49Y-kjpdp$wx~|OXNV2o}Qk*aNz>U
zc>;7|Vxnk}K;Wpa&<;6Zkh8hDnVz1mP!1P1DuIHtm8)R@6MO{Wr%s(xE)blEu~?``
zyMK_K=roXKo|u>*XH2I<gjQgXJUl$?<>f_dm1!fB#GWSDgA6MxD@%q^TQ>xu_Cf-?
zeEG7rLC4RP0E2KOGzt%tsi~=?G4J2MKUSs_b@AdwQV?iVpp;8c2n=Fz<S8mD(z=OL
zDL6JEwlT6PEEOkk!o^<1mZjfQoVAd$b$@kru^k5kNcl+U*h^Fv7z#C{Aj|;SG&qfV
zBf<$oEb1%NQ!Ey$hiu9M6Kc02D{o*>l%1U|s*@5}jZ~+ez_2@$g0{D}$tDk)z09Of
zu|64Q$j;c-))qPJ?AfzqlLy`0+{_S*Qw;T#9Y2%;Q@-I)>J>JhJ$qJISjb?O0)Im|
zW$?=aTU=ZuhoOojeHlmwy=geUxVRYG6smU&A<~%1o-tWbZf<VWKBm*#sBj-szCokr
zfrLXe355e$)3rVji|K;6oSYo3LqrB8z;sHfAo=dyy8^b2jg4#9u8F#ICOSJiMGt|%
z9;#e~R$wS6v*Wc!U4DLkNWuQL;D3iyngoW3Z)<C-QJ=Jv0qo?DEdc=m<ZBI(X=!Ov
z3>tgnfSJTW6BZ8*4Uv=f=FJ<0RX5x<lLH0XR7wK?mYbVP*~d<N#Mvt;DIo`%Trme3
z^v91MNfSSR{w&>6L_8vaj12cL!ijEteB9*9Z!i?=iUbB)`B56_0_v?IT7Q!xMn*;`
zY#A9DMz9jEeOp@_E)zXGJh-U4di5#=3<btZmo9Np_FE{@x4ODIQ7H-m_@;o1I@}M4
zY6JqeSX{1l%*+sYsk%BeCMPFz0fsDLaBz^qhU0pCe7s@x;X@^yUnoUr;)5T<Dms>i
zw0LfAj#7!U!|mI*kD30C!hh#I_~L+4fhJO3>frl#dL*N9M}dS$su-uKkdP1wyKWsw
zvZ?zjLDBsDJW@p6Wa+_u<;oT5?ItY1)ZvCs@8Ev@`gN(l&;wMsVbjA-eTG02ax0B?
zz~$}j?I_byMJ}yskZnl?A{hGk_-Gw#NCpJoj*bqgDPj+Dj)8#zY=1%d`T5xNa5v93
z5g#OpV+G>r)2AnM=Y*~1&Ye5bPlYJ^$R;k${QdnkIv6|0w{PDB=I`IXXJ%$<WHVkT
zlBP$G9-&-80$|XL(<<sk1qB6gmQFPYzEJU+;mz6G+hafy9wDXXYYGqP?(Xh+d3pFi
zkGiHI(ISK4p;{`F%S4A|D32q(L^6q6Su7q`S6B1^USD4y-ZbolQajWh)dQ*rR1c^g
hP(7e};Q!!(e*ghRL<D8R&zk@M002ovPDHLkV1g%1Knwr?

delta 1815
zcmV+y2k7{?4ucPnfq(5uL_t(&1?^f}NL5`B&Q!Xnm1d$on5lWevL5n53qb^h67%+;
zrDj<ahKO|20xcp4d?`V^Lu&QlLs5ok7EzL=qTMfEvQSjAqSC~B{U86qX>s;GYhV6-
z4*sxxan4#Z-^{GN*6f*Cd&v_0PhSCj1@slrS3q9@eFgLtuz%DFSeZ%DiWMtDLqoT2
z-Rke}@9piqdi81t2Zx_Of1W&f^1*`#W>RWK*fnd`96EI9{{8z?Q&WG${zgVd%&6WR
z&@L`6#l^*wlapfZ{2lx8<A;@%$To3wbUb?W=$<`$Y;A4LVV)XHJ3G7l{QU9p@xSJq
zpuT<k2GfN)Y=3NQ-n@BZ5bEyk-nnz9kjK&^si~>hU80V>eEIUig$w)l?~jU#+Pr!5
z+O=y<85a~3WQ1_*)-7jeXH)#<9k;f&zI^$zkwN(R<jIqqoSY3CHpo|xxbpSuS0kvw
z!NHJ_5cwe5a3V<Dy?fWllvl4_?b@|Voq{b}wsduM8GixJ%*-SvCaMEeBjw}cgOk7-
zF+4nsEw^;(QZ;Z3G7v{Z{qp4ti`>`Ow=i$!{|H~by}hjNr%#`{y1Htm*4^E`rKN@N
zu?w_9=HVcGH8(etRuHwdwab?;=gBCagO>B>&reKD3=9l}g@q{xP%cqaRK$99?%X-$
z7;+^<Uw?OUg%$Cor>C=CRaaLl5?`dut5&UIk2PZYvSrH_sjZ64!ltLkH8eD&VYHBG
zhKGm8v17;L;^MU2qHb<(ILhSM-o1NeTB8*kvKHDeXU?3_3U*0pX(@U2?AbFd*G!&`
zyLay<1HOO%zH;SCd2(DX<ZQ?Q2sbh^lFM$ENq;Xduiw9alMNXe85*d`%E}_dhYuf0
zB6@myqJDY%_AQPgV$6vXCy)xVQYcIiU{P*@p})WX+O=!jwryh>MCp6??vWWC9UY=P
z$_86oTM6;Vkt2NJgoFfPNrugH`0!zaARahgy98(;@H8Y(PJDbkD}`gvw?twVQGRZ2
zj(^l6-{A;s-@bi*ex6m&(kNk4Qc~Dm&CY@*=S5ZEIkP3-zJ2>d+2jsJM@N(Twzf9D
z*z4D?iCF~!yHPZ7RMgkk6YSNiS2>)RlE}x|-49$27Fsuffq@(b$tin#`{d+g4qIbm
zBh7NkRs)ZOV}5>qg!S&-JEDaIy9B~nnSUd}!NDBP>({S;`t%75IFn^%WgJdS+3=?~
zZrn%=D7uIi5|T?O6cLU%IXMXjX#_zCO@#39@b~ZE_w@A8xF{jv7^*K4<2#1ho;Xz#
z==ShsaSm_azRl+`%t~xU?0Z7tw**mc5~pfH6A)S2@$?OW2w|EpfIJHafTETPVt;yi
z+H$!y`!bb0MF$TaRN)VaAYb9HotCrI$Ao(H2Kmq^>D$}e$xuba=g*(lty{;($1Iw&
zs9ZFNz)ej}WFPvTl8O;U+5CWH`2@nZoW32JD@Z{NP?E`=gsE(kLuO8mWQCJhiF$bY
z^l1q@{+&jTk;G9w$q_NhgM}e1$$u<{P%<+!NlKC^FE1xSK2cIqLTG4~@tGxN0|EjF
z3lS2hNJ3y?`uOqVB*-Dqy_4T&#e12cWIp5tbHx6$cMcH|5n?RjW2a7?B0EG0RARUQ
z5EmAVZrZeo1<|0b&-Mjy<OCXd11M72%9pjQs;Wx<vx1IGmoAa`=;)~Yy?<lUK2%wR
zh`Uy$w>t+89H5GhaBxkdsBwY^i)@R;3kwTPnyYAnEwr$5ie$lz;ul#3nRWK;S)~Z*
zy^%Nw+-oaE5a*AHi6LB=sOav?fVgAFj>nH5<0>#MElpepcNCkpv$NA+!;Kp^xV#D{
z*&C3$x;ll@8nENLgY4{V+<!A^+<``cOb-nNqJjp-E)odr0rG8jc2-g7UnDMwn}K-v
z@#9DG4Pgpbo8qkhyHQl;Y>tbLC%;Rf+ClV%@ZrFtM~^Jmct+L0zH`wY<+qkp_?DEE
z^y}9z!`6`O$*I6MDUzF;OB>QaT)cQulM7q)XHjk%fwP@5lQwBJfq!oV*mo{WL9LB<
zl%g!j$QXRI!{((1_wZSkq8!BtQ_WALTFBXqR`<n=7qPLiDqc0{-5WvS=anm0G}#D>
z-CMtY{qyI~g$9j{jo~GZC%|u6v-&=K_`u>|JK&o!mci0AvTT%<=vc635sff|DO9@+
z4Goo*mH1fEl&d#y-hU)1eDmY$>uYIGMD?Nlh)*LZl#z)-0;!oO0=^2YsHnJe=Z;0c
zF%mUJIEb!bK|ujN5)%4DBS=~POMnq7wde@m+Ie|-*im#NPL9>o)L6z8D|4&CJp;Z8
z#?iudMV*QpS~zKLr8>U80{RN*E1<7{z5@CREY2192cbRb@(>NhGob(g002ovPDHLk
FV1nv&i-Z6G

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index bbd952df0..fc537b734 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1123,6 +1123,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_profile_sure_kick_channel" = "Remove {user} from the channel?";
 "lng_profile_sure_remove_admin" = "Remove {user} from admins?";
 "lng_profile_loading" = "Loading...";
+"lng_profile_stories#one" = "{count} story";
+"lng_profile_stories#other" = "{count} stories";
 "lng_profile_photos#one" = "{count} photo";
 "lng_profile_photos#other" = "{count} photos";
 "lng_profile_gifs#one" = "{count} GIF";
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index a106a9586..62febc455 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -367,6 +367,7 @@ infoIconMediaAudio: icon {{ "info/info_media_audio", infoIconFg }};
 infoIconMediaLink: icon {{ "info/info_media_link", infoIconFg }};
 infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }};
 infoIconMediaVoice: icon {{ "info/info_media_voice", infoIconFg }};
+infoIconMediaStories: icon {{ "info/info_media_stories", infoIconFg }};
 
 infoRoundedIconRequests: icon {{ "info/edit/group_manage_join_requests", settingsIconFg }};
 infoRoundedIconRecentActions: icon {{ "info/edit/group_manage_actions", settingsIconFg }};
diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h
index 8ba60b121..af1facc58 100644
--- a/Telegram/SourceFiles/info/media/info_media_buttons.h
+++ b/Telegram/SourceFiles/info/media/info_media_buttons.h
@@ -142,7 +142,7 @@ inline auto AddStoriesButton(
 		parent,
 		std::move(count),
 		[](int count) {
-			return tr::lng_stories_row_count(tr::now, lt_count, count);
+			return tr::lng_profile_stories(tr::now, lt_count, count);
 		},
 		tracker)->entity();
 	result->addClickHandler([=] {
diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
index 3b85f8989..642b4d422 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
@@ -201,7 +201,7 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(
 	};
 
 	if (auto user = _peer->asUser()) {
-		addStoriesButton(user, st::infoIconMediaGroup);
+		addStoriesButton(user, st::infoIconMediaStories);
 	}
 	addMediaButton(MediaType::Photo, st::infoIconMediaPhoto);
 	addMediaButton(MediaType::Video, st::infoIconMediaVideo);
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index bf2f7624e..4137dbce9 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -109,7 +109,7 @@ mediaviewRight: icon {
 	{ "mediaview/next", mediaviewControlFg }
 };
 mediaviewSave: icon {{ "mediaview/download", mediaviewControlFg }};
-mediaviewShare: icon {{ "mediaview/stories_next", mediaviewControlFg }};
+mediaviewShare: icon {{ "mediaview/viewer_share", mediaviewControlFg }};
 mediaviewRotate: icon {{ "mediaview/rotate", mediaviewControlFg }};
 mediaviewMore: icon {{ "title_menu_dots", mediaviewControlFg }};
 
diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp
index 816f918cf..13f4e45bf 100644
--- a/Telegram/SourceFiles/window/window_main_menu.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu.cpp
@@ -744,24 +744,6 @@ void MainMenu::setupMenu() {
 		)->setClickedCallback([=] {
 			controller->showNewChannel();
 		});
-		addAction(
-			tr::lng_menu_contacts(),
-			{ &st::settingsIconUser, kIconRed }
-		)->setClickedCallback([=] {
-			controller->show(PrepareContactsBox(controller));
-		});
-		addAction(
-			tr::lng_menu_calls(),
-			{ &st::settingsIconCalls, kIconGreen }
-		)->setClickedCallback([=] {
-			ShowCallsBox(controller);
-		});
-		addAction(
-			tr::lng_saved_messages(),
-			{ &st::settingsIconSavedMessages, kIconLightBlue }
-		)->setClickedCallback([=] {
-			controller->showPeerHistory(controller->session().user());
-		});
 
 		const auto wrap = _menu->add(
 			object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
@@ -772,7 +754,7 @@ void MainMenu::setupMenu() {
 					st::mainMenuButton,
 					IconDescriptor{
 						&st::settingsIconStories,
-						kIconLightOrange
+						kIconDarkBlue
 					})));
 		const auto stories = &controller->session().data().stories();
 		if (stories->archiveCount() > 0) {
@@ -791,6 +773,25 @@ void MainMenu::setupMenu() {
 			controller->showSection(
 				Info::Stories::Make(controller->session().user()));
 		});
+
+		addAction(
+			tr::lng_menu_contacts(),
+			{ &st::settingsIconUser, kIconRed }
+		)->setClickedCallback([=] {
+			controller->show(PrepareContactsBox(controller));
+		});
+		addAction(
+			tr::lng_menu_calls(),
+			{ &st::settingsIconCalls, kIconGreen }
+		)->setClickedCallback([=] {
+			ShowCallsBox(controller);
+		});
+		addAction(
+			tr::lng_saved_messages(),
+			{ &st::settingsIconSavedMessages, kIconLightBlue }
+		)->setClickedCallback([=] {
+			controller->showPeerHistory(controller->session().user());
+		});
 	} else {
 		addAction(
 			tr::lng_profile_add_contact(),

From e98770d418e52b6518a363961de2192b6166a872 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 19 Jun 2023 21:00:34 +0400
Subject: [PATCH 088/259] Improve saved / archive stories design.

---
 .../icons/info/info_stories_archive.png       | Bin 0 -> 727 bytes
 .../icons/info/info_stories_archive@2x.png    | Bin 0 -> 1450 bytes
 .../icons/info/info_stories_archive@3x.png    | Bin 0 -> 2221 bytes
 .../icons/info/info_stories_recent.png        | Bin 0 -> 606 bytes
 .../icons/info/info_stories_recent@2x.png     | Bin 0 -> 1131 bytes
 .../icons/info/info_stories_recent@3x.png     | Bin 0 -> 1719 bytes
 Telegram/Resources/langs/lang.strings         |  10 +-
 .../boxes/peer_list_controllers.cpp           |   1 +
 Telegram/SourceFiles/data/data_stories.cpp    |   1 +
 Telegram/SourceFiles/dialogs/dialogs.style    |  19 ++
 .../dialogs/dialogs_inner_widget.cpp          |   1 +
 .../dialogs/ui/dialogs_stories_content.cpp    | 292 +++++++++++++++++-
 .../dialogs/ui/dialogs_stories_content.h      |   2 +
 .../dialogs/ui/dialogs_stories_list.cpp       | 208 +++++++------
 .../dialogs/ui/dialogs_stories_list.h         |  40 ++-
 Telegram/SourceFiles/history/history_item.cpp |   2 +
 Telegram/SourceFiles/info/info.style          |   2 +
 Telegram/SourceFiles/info/info_top_bar.cpp    |   2 +
 .../info/media/info_media_buttons.h           |   2 +-
 .../info/media/info_media_list_section.cpp    |   4 +-
 .../info/media/info_media_list_section.h      |   1 +
 .../info/media/info_media_provider.cpp        |  18 +-
 .../stories/info_stories_inner_widget.cpp     | 123 ++++++--
 .../info/stories/info_stories_inner_widget.h  |   2 +-
 .../info/stories/info_stories_provider.cpp    |  19 +-
 .../SourceFiles/overview/overview_layout.cpp  |  92 +++---
 .../SourceFiles/overview/overview_layout.h    |  11 +-
 .../window/window_session_controller.cpp      |   6 +-
 .../window/window_session_controller.h        |   4 +-
 29 files changed, 660 insertions(+), 202 deletions(-)
 create mode 100644 Telegram/Resources/icons/info/info_stories_archive.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_archive@2x.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_archive@3x.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_recent.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_recent@2x.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_recent@3x.png

diff --git a/Telegram/Resources/icons/info/info_stories_archive.png b/Telegram/Resources/icons/info/info_stories_archive.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b4b79ce4365cb35163cb2cb7eb37c13479bc395
GIT binary patch
literal 727
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={W7>k44ofy`glX(f`xTHpSruq6Z
zXaU(A42<oW3@jieKr98s3=GT*7#Wy>G$TlC0TWzSVF5FO4N|zKE$K4@1Cyhti(`lf
z@7vJ*uU96D{QouellH=vNfId+!~?~CN~<s%CQV>W-7+I(2e&5k?ACxVql6$YFR>QR
zX2(<c-%IXSRjvQNH!nW^yWhNrZ;S1o+dQ{<UwO{*{PE+}dyPNT8#^q&-0Gw_)vLDf
z#`*K-L$pli`sq(U{kF_{uHWH@1`|Crrk;B5mb5WqXUw}AyKpVh{^N_UzZT_cJ(=?9
z_upQ(#}zhelRep*Kc+}DI$U`Dm94qaZ@F@5;?}5#+h?6sn|)Tzw8G_5#+ItR_wu#_
z&0w~k=yB!!_tQ^}TAeBr+!lSR+I#wGkdBzLtjnedog*zLo_;!;_PA*0gs1lzWzy~D
zvo|}YaPd6J+JE0)O?YiscLk5j;)@?EYTWIF&Zd2SSa3j{y^i&j`_iD)%{SkcWpBRu
z;rGuPyD8HyW*nK^U!WqCxizYSyVHe<@d0;=f<VKEjS)Qazxyp0=4$;AZQ!FGbiiZj
zrK3rXtC)=D`Z>#sbRYd%HTPtS!RDJeYLicPM8B&xnt%SV=i!GNF27`9P%7j%C=gzK
zl`EcUNt%jE`{9*WvmQu^F$fqc*K{qIcA$D|lxx(6pTS)oNe=9Per#n>Fy(si`R4<s
zM;|TzG3~24c|xD*0pBK`sk?5!mEmK5WccB!(catrogah)VzajX^84+xSW&!eH_!n}
z6FKI|i$369_CLeqRoQN--nJJ~GxB+@&nWjF*B0S2Sm(WT((Six?GL9umYRDj!N9@!
zV9LI@^#S|V*AxWs@cdb={#ox#bWQq$?>v84qD|Nya#>xp0VOI=S3j3^P6<r_xzsZm

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_archive@2x.png b/Telegram/Resources/icons/info/info_stories_archive@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..831363fa5c51083ffa552c679471de10d18f94af
GIT binary patch
literal 1450
zcmV;b1y%ZqP)<h;3K|Lk000e1NJLTq002M$002M;0ssI2B@5<>00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuB6iGxuRA>e5Sy?D{T@?Pz^N@Kc
zB}$Qb$XE&&?xYMCawQjplq(`BQ(uV^Wr&C(Tp)Lb#4T5f%$etT9=`v{s?+<Q_niH{
zj`Q;U{pVbq_E~$awbxpE?Y*D1&#zy~qeMW7fD!>E0!jpw2q+OyBJiJ$fPqdex3#tP
z^z^j0wtjqke0_b@=~(h8H#av=NlEGI>RMY{yT8Bxqw-u_Tr4gw*45QTM@Q@H>&r8q
zj9NTAJi5EPc}A<)|F7oJ(NRT3g@uL1*A3A7x(eh;X=!OeL4m2MDQVl@-X0$xUtL|@
z-`_ttH~`(%)z!_-EjTzhIXM{_MQm`Rs;a83t?ehX%h%U;eSMw6i$rf|Xb21pWCX>?
z$S5f(X=G%Cn~~}1X$J=fhKLTk5YDTsD>9|0rw3W2{a}%ik+ZY2B)hq}84wVly{tT(
zupUzn3y}9285t6k>gnlKR#v{hzeCCO^|imhzXT-`MSOgGE-x=JFj71$EKH)753$tL
z)VsSosM^}va&mI|iYjeou?m-$moenS!^0P?ry=13&BNT>92SYTc1Y%{si}eWc(Ssx
zBrD+wmX(!}&Z43sUYE?D*br&P`}_MDszXCVtE;PPYiomof*5S7T~kw2@Zt3I)W*g}
zl`eC;rKJVKJU>5sd3lMc&d$!J^$pIqx3}{0axqR-I(()?g4?QeIol>CCbze@Fbi&o
z=`}Poq`eiZoEAFx2L}g%GQ>nUI%XBjm;?g@-GDS~ofrU5cz8HBDzD+^=SMsV2?@^5
z&RCc92_U5Lpoou;CuTHwYUTNCZEeK>00`K^#WW%l$OtsRV{B~P(a}Ma<KyGFxHvIE
z&=GiqAIwcnO$;{aU9@|)x3@D?@Ejn{!otFrmls%u#gm(x%Mb@51l!Wm5`#y27cGF|
z;$nshK?lH(j5<3zqp4_aZpKm(R8mq>0v;U9g1zd*PvCMZD=PxPIDOR>X{1a}PDVsT
zP-ye>^ZWYx1VxaapkpjcOG`lp9`O?n-~=FHX%0XZxHT#&YIJlI?ez3?S|mJ!$N>-m
zzj<APpBfy<%*;gA(!8&%tdNSCnHi#>3k-sK#fUdIH`r3CR~8hl!GVp9ji;w4F>BC;
zpo;Yh(g7KIdwbD!czB2%R-K0nJU12UJfEWg3+(RhQpi=`APT|+u|<r7Y+zu3H0<o`
zpgI$@<FkucPzj&t?(U9bNY#3Lg2?Cl_!e{kN7%x`0%@3^pT_|wC<~&lwo&W}V`F1v
z0UC@gLfwJb*jSXmB-`29iK9)-dhQ@=Y-|+ckxqy9-yaVfsDy-skgZfI<mBXFsel4_
zrx7aa97<^8y`!TeIWROdghH9x=jZ2%iHRD;bq?k<T0jgio$RJJ4lf4w=5!+WbPlJv
z!PpHmGc(|AL><Ts*^FbA%}>4GRM2Q+iUgkTD=8%yr%~j)i+au=tkL1&VU1-9$1m9U
zfom1A!`|MW3g45H6WkUG8pkI7<i!?QmY0`DP!E+m7VD?3rGEZuk$i}WiP_uRqwNyq
zPJMknF6|hBm;G{Aw6tXy!RrFB!BrQoQn(@u7<OV}0`(^#CqM-5GJrAh{EgS^oHO3u
z-oVLJ_^O^_f^GW2!2nd8+V5`QQY|el4ZoOhV@?o=bAWy^A;;wX#Uw)tF@j`gXXoPL
zf*&8KLU9w!;8ePl2q+OyBA`S-iGUITB?3wW{*Mv(2VDWuz*png9smFU07*qoM6N<$
Ef+_Hj#sB~S

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_archive@3x.png b/Telegram/Resources/icons/info/info_stories_archive@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..e02e85c6cf21b8384a02b72f8008303628c7aa91
GIT binary patch
literal 2221
zcmai0X*kqv7oS1JFw8IvV;g%hmZ9uKNMjjImQ?oak$uZ!eae=7YrK<PAsIAfXHtnl
z_EMoRgzVX~J^wzu@B8V!-Vf(K=iKL9_jTRpcc0(66R+Y8p%4@V1Oh>ESM;v|+To8d
zGXSgi?<pTZfdj4?UILN(1r`BE-^Ci|YHA9S25e>!INk%q^d|)n6d)iFV-Xm{2x#!1
zy&{JHL>-D4|7-tAtW>hc14kfn`nndO;1$yK1cWK9Yukhv@IG3tl(^lI;Q!N)SrTDD
zfMd_}$xA1(5H@u62UWy);gnc777h8q6ld0X$VZ^A#itl3QCdcnR5ZfFceLD1&4Mhq
zqckim$7XxoPB08S;D^x$uSFf;wanH`|I39z8TLyF^vWl`({Eng8Iv=AYLdt)QFL;2
zv^%hNaiAUNaz#SNl%%}!tu4Ia?&Qsv?qKy(5QHzs_S<-l2A}WRQOAe0EGe^m_3hP3
z4HYxHJC$~AzgxpL?|y6DFC3t*eW$KXwuHQn2A^W(>AO=@u~8eFD{p^1*NZIdR{gT}
ztxdyyu;_5Vlz2>70OgAsB$+%`%}6vS+o*M=oh*&j$Xq8+H2Tr|^EKq{+Yk3P#FQT7
zS6EbDd)^WU<*&apMmEZkPPNmtYYz0-S^gr0f9y*&qU7T9)wS+zbz`AK!-L&5VI^0S
zxt<)ZwD`Q?_K1Tg#*Y|E^qAAmunSaNExzQ$()4>J0WQ`adGw59`2$Hs$*mt_*BT0H
z;lJSd(tBK4GGA`J<L@&-gaa+xX{)HH_}OnOBYLzaN2WJdmZaBvv*B*U)&jv+k(Cm4
za;%2n_XtrI#0x~QxeXLl-=MfATy}PKZNIl|WT8;~W1vvD<Zw3WYik&V$X@5t%Y$aM
zm1x-miXUh|f1ko?YrMx$ZjvW@`Q&gv;M4OP(@T3|8Esx;WZRNdF4(~4K(Eby)Ifn&
z)Zv~8Pnv+5;ulx{KJSS}!g=5V&30|~XjZ7Jk<~X})tY(Pw%rTT(baC5zD0*6+D!y%
zo-0%ob^P8ORAn7D+nqt2w#;PfIMw-<XXPPM9-mn<<#g+nr-V=sfNenJ<AV#Q<P)c?
z9Jdxn$~`|m+s{e2Io6A3r*Wu;t~=<t^u4GwFP|CHj65WUMgYjD<%I;y_jB_rdw+Fy
z*F$*Owghwc%GtMj@#0>X+cf&>s!PYBt1hRYI+EpXzFYtvO}0QkL{Yk9_Wt*Wf2AZ-
zuGiSbL6p5dquT;!Iv*nQHA0jTGY$X<*$H2p0?O&$o<eG^&by9zyePtRYpfshx_$p+
zwat}r6+Zs+#m|e+=BL2se{U}tv0v(WBws@{g=)F=3-Enj7%GLPpnD$Kjg*^}UnePh
zJI7R9U;0$dnl2>9%R}RmG*NgdmFQJ&R%mq^o2EH}bTXty9y#+4KFc2+XbPAw6P(%G
zps!*wnG<nSXOr#&)#;i|Bxghra!+fJn`Vfhq5O&u@SaI(6cs@GN9J_4<@>M!+31>f
z^?<lzSh6HZf@gWWL7oU}L;6yN@Fe9FRs5^5O7r;Eh~VJ*{#@cWid&kKL;3AFNmu;3
zGE+24<t_%&7vjFinPyFKg|egottNe1AQosPdFxuV5ViE^bKULX%&fQCK=OHa^iy90
zvA}+oRzuEVCMzREJ2&`Us-OlDQSjyo^09cm);=A3ACz9!@r@c~R)#V&uiXjXT@6|o
zjCn^16jt>`6ACb%9wD&V6BZ2%F}nnM_5N*U5N-m~+hNnjB1L!Jy;1L#r$E<}vuV64
zM29<ZAjLOAHOKWp70$FwBj{LX!S7h?lorexDyW{_0+Vp;P8TUCxndzBR5wU@Na_&Q
zIYcZg&N!Vdx!sj&Bqvoz=SJEoQYV^^>T3^dT0;rjNf^s1j-uwErJuK6%roPnUwVBR
z&iZg4tl!NOxHxQpT?+SYT~+jRmhPmpQSwK|u%?`vWZM?ynAOnKk{07ENz*N7UepF?
z!Yh@B9qMvxRAi|PmnGhv9M8^HtA+{IL@{LN<VbiZjEaQ)nx(N5R=WV|Nc66<s`ghB
zZ+K9L_330d(;>sFDXRcZXyc2zIUAq=klMJpA{*xN;)UR4>nu5u&DthaQ6CprO56e7
zDVlj>i%sQ?&t$VpJLAkXh)x;wl2B{0s&oX|u1z{vXZ=d4<1P~@gCH(jX{-%2&EoJA
zsKi?yt6EPoR#~cC)d1xw4r^!M4h3!eZ601;N^$Ch3_H}xCVYHWuAzG1RHy&>a;hLT
z_ac?H0}vMk!daQ+lQqlYv%wY*<^K_KzDdsUM+hjX5Pb_Isu5Gz6nS*dBgR4F&Uk&b
z{d5y{?q>*F5*6S$vqJ5U@F5RQ9m^^!C|XTLa)psiFNjb3uSWKy!I&yt|Fu_U4#@%%
z?%9XFA-vWom-JZwv&eWjx1Xetf}d5Pc0}X3$em?A!{S*#&Ru`%+|N9Bs|OB+7o*7b
zRBeVyuP6^=6hgji{Sl7z3n?g;X^1d4NN>cIGf0CIdQ^vL%a0~r>@E!Hebtj1u3<9M
z)p=@*=i)>#&BbjCwZlxtMDeF-ACkq?j4f+x+()=7x8xXIL$fUxBrHwNlAT=ph%xf>
zYJ2b4!iv*vQqpVPhe`~t@s;vSZq5(jluyf$`ppYPyD@0b`2)5unF!sxz6U!ijYHfr
zR)~Hg6A3wXVw{V?(Yi43u;C&dC>(n~JOWE?6rAZc7k2n1UvS=S#C}UHyN|Ub08%0#
ze4^@_OS_#dJes&M1TQl`H;g6~yU|U3aWttsxNIG9eN#lh@PeZ>o{JlX&3cAu7>yhy
z8AS(<R(yKBBqk?PWh$ETLSZ#^lw3x+@%8_V;C~P3Fl5XL81?>)c=Erv5C41>a0YmN
J@+F7Ze*lqM@|*ww

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_recent.png b/Telegram/Resources/icons/info/info_stories_recent.png
new file mode 100644
index 0000000000000000000000000000000000000000..341ee2a06ce04c9b9a159b216e977e5653984211
GIT binary patch
literal 606
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={W7>k44ofy`glX(f`xTHpSruq6Z
zXaU(A42<oW3@jieKr98s3=GT*7#Wy>G$TlC0TWzSVF5FO4N|zKE$K5**$Gb<#}E(R
zx1sy{8672__H}4*a2@G)S-kj$%chV?xh@MYb-Sou6#C!d=DbvJE!)&2(Jex~K_ZKS
z12|4Ti^~(98GV19;=5U!k3Kv5`(91{x7joEeV2XzyzapLumf3JXPtd^z4}?v&bs~k
zZ@(3BU3vYrL5JJon>pWD{U&<2OndtH<BgXkpMKUTO+4VPuwKQp{V=0q`{9p!d~M{8
zuVQa*y#Idx*+-v$0u8#Gx1YDrP_93HZ`}6VZ+E-qn@HttpRKK_?eywrP2R?c2%&Ww
zBYILB(l<x0zItgwdE3E+4@End1Znp2nM(4tO9hFzh6FjswabSeVS4jCYwMv)5kfyc
z|GqfkM?lf!5XsyLe#_M-cdAW1DA4XJz!Kyj&BD}pHqF^j;iS{T0IjJfF7(@~i0V|@
z&EI(GV+D`U?2xr#g_jx%86zdTGn?O*N%OTw&h6TLSFho0+F@z7-+$wtR_~p6`e~5i
z_1!@$b*6jky)0JRo9vZ#*l@1j@{|)DD*95Ei5nwKX8RhYxK!<pn-|u2uifrNiB)gW
z%cDKBV>Lvyrk}1VuB_Z!7ONxXzx;BB$*$X6JLlxpTbxWWntZarx6b2B^!_vLVjso+
c@8_yvl)trP%Io6H%b>XRboFyt=akR{0PKbPApigX

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_recent@2x.png b/Telegram/Resources/icons/info/info_stories_recent@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..ecb3fc72d91d3cbbc9ec444f1b5151960118bb19
GIT binary patch
literal 1131
zcmV-x1eE)UP)<h;3K|Lk000e1NJLTq002M$002M;0ssI2B@5<>00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAu9)k#D_RA>e5S<5SYQ5^RkGlVco
zW2Go1k)&UFEU=kKR*FcJ1q&3}%*sMm{s${7C9jCZM5cy96q%Yl@|X$bk$xWG_xU+>
z;|}+J&)j>>sB;##d(ZiNU!QxP-xCobieNx6AQ%t~2nGZLf&syRU?7lz$jC^m)fy8M
z6WA2KH90vsgM))kr}N<8;PLVC?d{F$^~S};g|ElVGmzcX)U>_54Uc8z`uaLCF;NO&
zWMstcb^{C%sWQYbWo2dXN%H&Y>1lp`et3AeySuxgp&>mz{iA<}hldQgwzgJYUjDHu
zH17_F<Ms8G`LwmQ)zZ?El9Hm}N^x=V=H@2Dzr4H@6ci|cjcr2D9vd5DuAHBrx3{-P
zMMY6e0@l{nc6@w%b8~|+LlMy*gC0CJHO0JNU0s#+6QVtf#X>~t4;va9V&0FBkDE*;
zjRwqSv(0AHsDwkjzP_II`=zC&=;&w;$#0<R>+2Z$cDwzXc3rE%*6Haf=*LW-mX@ZA
zZUg{@9*-wBHdYsfN=gO>23Y5*sHjlFy|>lX)s2phCM6{)BLQPNdwYA8ky=}mlasHm
zu3&6#ZjPc0vlGA_9UZD*^hO{rE-qA&ngDQxQB_n_L{W5qe-C_LU!N*CFE8)%@)E0?
zDpCgE+iBrnLmn#;icFEl&%M39%*;$IZM=Fo0*H@~udc3!f$8aKUNaQ@^Yb$WCc}`{
zP9{@YTAGlMfcKf18JU(4esTnmm6gRD-QC?a$YZRJ`}_MD85wl_908z@Lhs$(9fl^|
z02i{evokk0x3I8~F2x-HW=6VpUL-rdaUB&n1~IQU5*|w)wjR<aUNB=$OiVmHJbbLg
zJLX(GxdM=BQ2Ole?}vfg+gl~j=Qf|u$JUL&Cr+N9vH!o0j*ft=si_HU(xIlgxtaOF
z<|2y5*49?P-~TtDoSd9|xf8)cIXgRx^`4@f8&g?X$pR=TDd842On_q$<KWBYpW515
z!~8K=@#N$re!|AahQW3WQ_s7ZqdStFosDb~<_Wq;Fi76*G~LF^$_oDN&dv^9BuFIJ
zfre;9n%}y*x`-md!*oAV;#7Thb_V*9R?_8!tsa|ns9#)M)NT@6H=O9PR3pQuZd)&e
zzG;;|??o@)NP1+6Eu_&&VqpaeI<<&ISWiz6((Vfj3!#sQH@Vx`*vNKl7`51whQ;*<
zJ-wcvpAWxTK^}#e+W3jTgdst>;7CnPb-7&2%ga?&RX^cY5H_SMCR}9vrYoi|L&b0T
xeV2S+Mj|Q&1A+m;fM7r{AQ%t~2nGyd;2#KPV-@s>`&<A3002ovPDHLkV1kc0`MdxC

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_recent@3x.png b/Telegram/Resources/icons/info/info_stories_recent@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..bacf1a3c1e7d87b063979055f1a360e1dc3acea8
GIT binary patch
literal 1719
zcmaKtd05iv7RP_6Bso{Xg>Y@eYtJ|^E}=E0ky)6cX=!C+C0diFCK_s~U}!G5WHm~f
zTP{r82^TcV4HaXPvfRtk$HJ>lTA3ju;(|8#PxIgW@jajOp7)$T&ikD6ygA2wJ#=+U
zbpQa+#e3rX)ePAbL_=+fs}nJ5fa3f;4g%~x^AG9+F7y|Cn2!&DQu`19lobJJZKkMY
zsulofR)GLbH9?!PDvfWqAyu0H_?w9=$6#Lo*n-64+)gBdK9_w$hXxx<vPdS=0d~l#
z?XhS-dTcM#I#*=oZ)BhS^5unTDC`;febb%npY3`xZ2qjkO!+!mwT$;%b9kYAz5P?m
zOgX7<l=AtJl5~1Ty!X<V^*Y7A{+YVYrAH&=rP@<-LWqM#9`N5M|B9!FCYts|K8_^C
z$H!l}qF7u^PEPLNa)(ecDviFrzNDn2%Kg_$i;MSQv0%h}9EDOwr~j(jaGj|xEIjGv
z#+S)tLZQ&m@Rf99gR1xl0)YYp1Dl(hKi<uo&SSA6NTh{#?<!ExrYYqYY3;JPiAhcc
zm`|tExm;%og#y2hNGUETz__|L&?dVKU8$<s&!34=QFScVkM{OQeSHIz&0KD2uu>*#
zY-~IhH0NGHij7VCnyhS<kB^T_ZhxPio_=MaM=Jd~JL}2J=kxi96bX3CmguM`nMHe7
zR}7J;SXda${4Tg#EM_v9;8!d4ENPw!MI-ngIU@Rvubla7rXPt!I$ix;qEIOEcYAw#
zN1Q&+iT3l;|G6%<FXW}?(W7^JEURK-VkF_~t4-A+lHjbjkI%Z|6Jn+ER6u}ArOMcK
zPBg^h@l-CCE?m&^<LIxUU+gk7D{BbuD&RRfID|w;+tqFL8-nP;@)P3Yjmg<shFk9`
z6j!vhwf#19*pDAQ5`GZBv2Nu=!C|@v&Ikmey1JUfdarF55fQ=p0RLZjSQriH>FtFF
ztl-B)qG^`R1On>AFvY%xRYs4Fj*3L~!92+2dA!jS;_zZwS=s99>K}33jq{AGfpTMj
zz#X~cp|@mfYpcJ;U@&mAe=0Q+5)!IyMG}dj=0HwH!0U!&Ds}%(c6<Aoa!yZ=*HHGY
zI48I}7K^<Pc5!hjto7PHN$Lxvo!n01@yX-^1OlPGy}j{%eZ3Ff9L0UOJ24?a0xB&p
zKhajRhQwOV3ew73T3Y74n!HLQ3kOiClA4kdlOJe>y7h80yp_j$H$HynSf*C0fq_A`
zTX7_vLo%`kjLgiQKYTMbCg4<)E+Yv88l1*POGm}ShdYPZoJ{oya5xw!6iRjs2c4Vu
z6$I>ao7H8+AZ={IuU_5u{PythuxR&S?~n$0ZOL}<rp0&u0gbZxw=Rp9j8ntuRA%Nj
zSjy=50RUO|LkiR**F4uoB`wPCd2l{8b@|qS=Bi<r%xqT%CJp=3*v7EI+}9AXyz*`<
z9TNELB(<d6=tLDbo=WvyY-?-F$Ap67?#`SB_RQIz|1!H5wBIOWxj*MaV#U%nIt(VT
z3CinEXVuhrrBC-)x_&DcDRVj6$cyJ9BfSuxcJ(hePH{UsZ(bcJeP8B>2zuP|M!hA2
zGi{d-iLCUbxVhiOVsU6#m{t5hBfGV$OFBCGtl@!ww`JxrNEuhkyEY|_MxB>iu-R;Z
zUh;A$e!qe|f}`Bv@52ZWP=CSUGAeY7+LN{dH*eiCsCC3(JWJpku-_v2FN&{wai+bX
zkcg_FprFKDlkt0Y&lp&&WdV!1SFh@nbu0J<R6T@k7fpC#;u>URH39se6jAEJ!a|q5
z=@b3+ZsbkFqM{<z+l!h$*4-NL6{vd*_J%Mc-h*N4GBIg7NU>!WAY{g!9Uar^*S?*T
zGuU3sfZG&oq!Hmm*GfxEac1@&?(PN;8~`LmCr?Hs5^b|8ot%yEeBtyqLkr#GXH9S4
zZ+Kp?r^(UX?#9MYYQEpuGiPqFYu@S|DjM*xO7izNU|fjyJWQ}!FYOQjJCfl@XJ=FT
zPx8l;I*CuvbI49Io)ESo`+xfCT4--?@8bIiqT7+~`y+>6fnkSsJ2_=!Wz_~BZ0->V
z1Y=`k>eke2B$s0dEywkR7<AajX={Ha8ik_KXirUkwFx<hUd?naIHT>fW5*7;^&e<W
zi|ggtr!dlvr1aBIUy^s`>tTcP8tf;TgpzK{`UCm2>m4WN7QZ()pLqLYz5Gb3eu$Pa
g=<vV8!C9jV<o%%M-pKG)-OWz}@9v9Z9}G$R8#f*$IRF3v

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index fc537b734..328cc231a 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1123,8 +1123,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_profile_sure_kick_channel" = "Remove {user} from the channel?";
 "lng_profile_sure_remove_admin" = "Remove {user} from admins?";
 "lng_profile_loading" = "Loading...";
-"lng_profile_stories#one" = "{count} story";
-"lng_profile_stories#other" = "{count} stories";
+"lng_profile_saved_stories#one" = "{count} saved story";
+"lng_profile_saved_stories#other" = "{count} saved stories";
 "lng_profile_photos#one" = "{count} photo";
 "lng_profile_photos#other" = "{count} photos";
 "lng_profile_gifs#one" = "{count} GIF";
@@ -3812,9 +3812,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_views#other" = "{count} views";
 "lng_stories_no_views" = "No views";
 "lng_stories_unsupported" = "This story is not supported\nby your version of Telegram.";
+"lng_stories_cant_reply" = "You can't reply to this story.";
 
-"lng_stories_my_title" = "My Stories";
-"lng_stories_archive_button" = "Archive";
+"lng_stories_my_title" = "Saved Stories";
+"lng_stories_archive_button" = "Stories Archive";
+"lng_stories_recent_button" = "Recent Stories";
 "lng_stories_archive_title" = "Stories Archive";
 "lng_stories_reply_sent" = "Message Sent";
 "lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list.";
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 9e6e2e146..834592de6 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -82,6 +82,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 
 		auto stories = object_ptr<Stories::List>(
 			box,
+			st::dialogsStoriesList,
 			Stories::ContentForSession(
 				&sessionController->session(),
 				Data::StorySourcesList::All),
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 260e4d766..f8a51b93e 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -423,6 +423,7 @@ void Stories::apply(const MTPDupdateStory &data) {
 	if (!user->hasStoriesHidden()) {
 		refreshInList(StorySourcesList::NotHidden);
 	}
+	_sourceChanged.fire_copy(peerId);
 }
 
 void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {
diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index 47a025611..30b36538d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -500,6 +500,12 @@ DialogsStories {
 	nameTop: pixels;
 	nameStyle: TextStyle;
 }
+DialogsStoriesList {
+	small: DialogsStories;
+	full: DialogsStories;
+	bg: color;
+	readOpacity: double;
+}
 
 dialogsStories: DialogsStories {
 	left: 4px;
@@ -532,3 +538,16 @@ dialogsStoriesFull: DialogsStories {
 		linkFontOver: font(11px);
 	}
 }
+
+dialogsStoriesList: DialogsStoriesList {
+	small: dialogsStories;
+	full: dialogsStoriesFull;
+	bg: dialogsBg;
+	readOpacity: 0.6;
+}
+dialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) {
+	bg: transparent;
+}
+dialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) {
+	readOpacity: 1.;
+}
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 22ac09842..a63e159c7 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -142,6 +142,7 @@ InnerWidget::InnerWidget(
 , _controller(controller)
 , _stories(std::make_unique<Stories::List>(
 	this,
+	st::dialogsStoriesList,
 	Stories::ContentForSession(
 		&controller->session(),
 		Data::StorySourcesList::NotHidden),
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index 332dec14a..24a97ae70 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -8,6 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/ui/dialogs_stories_content.h"
 
 #include "data/data_changes.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 "data/data_stories.h"
 #include "data/data_user.h"
@@ -19,7 +24,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Dialogs::Stories {
 namespace {
 
-class PeerUserpic final : public Userpic {
+constexpr auto kShownLastCount = 3;
+
+class PeerUserpic final : public Thumbnail {
 public:
 	explicit PeerUserpic(not_null<PeerData*> peer);
 
@@ -48,6 +55,70 @@ private:
 
 };
 
+class StoryThumbnail : public Thumbnail {
+public:
+	explicit StoryThumbnail(FullStoryId id);
+	virtual ~StoryThumbnail() = default;
+
+	QImage image(int size) override;
+	void subscribeToUpdates(Fn<void()> callback) override;
+
+protected:
+	struct Thumb {
+		Image *image = nullptr;
+		bool blurred = false;
+	};
+	[[nodiscard]] virtual Main::Session &session() = 0;
+	[[nodiscard]] virtual Thumb loaded(FullStoryId id) = 0;
+	virtual void clear() = 0;
+
+private:
+	const FullStoryId _id;
+	QImage _full;
+	rpl::lifetime _subscription;
+	QImage _prepared;
+	bool _blurred = false;
+
+};
+
+class PhotoThumbnail final : public StoryThumbnail {
+public:
+	PhotoThumbnail(not_null<PhotoData*> photo, FullStoryId id);
+
+private:
+	Main::Session &session() override;
+	Thumb loaded(FullStoryId id) override;
+	void clear() override;
+
+	const not_null<PhotoData*> _photo;
+	std::shared_ptr<Data::PhotoMedia> _media;
+
+};
+
+class VideoThumbnail final : public StoryThumbnail {
+public:
+	VideoThumbnail(not_null<DocumentData*> video, FullStoryId id);
+
+private:
+	Main::Session &session() override;
+	Thumb loaded(FullStoryId id) override;
+	void clear() override;
+
+	const not_null<DocumentData*> _video;
+	std::shared_ptr<Data::DocumentMedia> _media;
+
+};
+
+class EmptyThumbnail final : public Thumbnail {
+public:
+	QImage image(int size) override;
+	void subscribeToUpdates(Fn<void()> callback) override;
+
+private:
+	QImage _cached;
+
+};
+
 class State final {
 public:
 	State(not_null<Data::Stories*> data, Data::StorySourcesList list);
@@ -57,7 +128,9 @@ public:
 private:
 	const not_null<Data::Stories*> _data;
 	const Data::StorySourcesList _list;
-	base::flat_map<not_null<UserData*>, std::shared_ptr<Userpic>> _userpics;
+	base::flat_map<
+		not_null<UserData*>,
+		std::shared_ptr<Thumbnail>> _userpics;
 
 };
 
@@ -122,6 +195,127 @@ void PeerUserpic::processNewPhoto() {
 	}, _subscribed->downloadLifetime);
 }
 
+StoryThumbnail::StoryThumbnail(FullStoryId id)
+: _id(id) {
+}
+
+QImage StoryThumbnail::image(int size) {
+	const auto ratio = style::DevicePixelRatio();
+	if (_prepared.width() != size * ratio) {
+		if (_full.isNull()) {
+			_prepared = QImage(
+				QSize(size, size) * ratio,
+				QImage::Format_ARGB32_Premultiplied);
+			_prepared.fill(Qt::black);
+		} else {
+			const auto width = _full.width();
+			const auto skip = std::max((_full.height() - width) / 2, 0);
+			_prepared = _full.copy(0, skip, width, width).scaled(
+				QSize(size, size) * ratio,
+				Qt::IgnoreAspectRatio,
+				Qt::SmoothTransformation);
+		}
+		_prepared = Images::Circle(std::move(_prepared));
+	}
+	return _prepared;
+}
+
+void StoryThumbnail::subscribeToUpdates(Fn<void()> callback) {
+	_subscription.destroy();
+	if (!callback) {
+		clear();
+		return;
+	} else if (!_full.isNull() && !_blurred) {
+		return;
+	}
+	const auto thumbnail = loaded(_id);
+	if (const auto image = thumbnail.image) {
+		_full = image->original();
+	}
+	_blurred = thumbnail.blurred;
+	if (!_blurred) {
+		_prepared = QImage();
+	} else {
+		_subscription = session().downloaderTaskFinished(
+		) | rpl::filter([=] {
+			const auto thumbnail = loaded(_id);
+			if (!thumbnail.blurred) {
+				_full = thumbnail.image->original();
+				_prepared = QImage();
+				_blurred = false;
+				return true;
+			}
+			return false;
+		}) | rpl::take(1) | rpl::start_with_next(callback);
+	}
+}
+
+PhotoThumbnail::PhotoThumbnail(not_null<PhotoData*> photo, FullStoryId id)
+: StoryThumbnail(id)
+, _photo(photo) {
+}
+
+Main::Session &PhotoThumbnail::session() {
+	return _photo->session();
+}
+
+StoryThumbnail::Thumb PhotoThumbnail::loaded(FullStoryId id) {
+	if (!_media) {
+		_media = _photo->createMediaView();
+		_media->wanted(
+			Data::PhotoSize::Small,
+			Data::FileOriginStory(id.peer, id.story));
+	}
+	if (const auto small = _media->image(Data::PhotoSize::Small)) {
+		return { .image = small };
+	}
+	return { .image = _media->thumbnailInline(), .blurred = true };
+}
+
+void PhotoThumbnail::clear() {
+	_media = nullptr;
+}
+
+VideoThumbnail::VideoThumbnail(
+	not_null<DocumentData*> video,
+	FullStoryId id)
+: StoryThumbnail(id)
+, _video(video) {
+}
+
+Main::Session &VideoThumbnail::session() {
+	return _video->session();
+}
+
+StoryThumbnail::Thumb VideoThumbnail::loaded(FullStoryId id) {
+	if (!_media) {
+		_media = _video->createMediaView();
+		_media->thumbnailWanted(Data::FileOriginStory(id.peer, id.story));
+	}
+	if (const auto small = _media->thumbnail()) {
+		return { .image = small };
+	}
+	return { .image = _media->thumbnailInline(), .blurred = true };
+}
+
+void VideoThumbnail::clear() {
+	_media = nullptr;
+}
+
+QImage EmptyThumbnail::image(int size) {
+	const auto ratio = style::DevicePixelRatio();
+	if (_cached.width() != size * ratio) {
+		_cached = QImage(
+			QSize(size, size) * ratio,
+			QImage::Format_ARGB32_Premultiplied);
+		_cached.fill(Qt::black);
+	}
+	return _cached;
+}
+
+void EmptyThumbnail::subscribeToUpdates(Fn<void()> callback) {
+}
+
 State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
 : _data(data)
 , _list(list) {
@@ -130,12 +324,12 @@ State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
 Content State::next() {
 	auto result = Content{ .full = (_list == Data::StorySourcesList::All) };
 	const auto &sources = _data->sources(_list);
-	result.users.reserve(sources.size());
+	result.elements.reserve(sources.size());
 	for (const auto &info : sources) {
 		const auto source = _data->source(info.id);
 		Assert(source != nullptr);
 
-		auto userpic = std::shared_ptr<Userpic>();
+		auto userpic = std::shared_ptr<Thumbnail>();
 		const auto user = source->user;
 		if (const auto i = _userpics.find(user); i != end(_userpics)) {
 			userpic = i->second;
@@ -143,15 +337,15 @@ Content State::next() {
 			userpic = std::make_shared<PeerUserpic>(user);
 			_userpics.emplace(user, userpic);
 		}
-		result.users.push_back({
+		result.elements.push_back({
 			.id = uint64(user->id.value),
 			.name = (user->isSelf()
 				? tr::lng_stories_my_name(tr::now)
 				: user->shortName()),
-			.userpic = std::move(userpic),
+			.thumbnail = std::move(userpic),
 			.unread = info.unread,
 			.hidden = info.hidden,
-			.self = user->isSelf(),
+			.skipSmall = user->isSelf(),
 		});
 	}
 	return result;
@@ -177,4 +371,88 @@ rpl::producer<Content> ContentForSession(
 	};
 }
 
+[[nodiscard]] std::shared_ptr<Thumbnail> PrepareThumbnail(
+		not_null<Data::Story*> story) {
+	using Result = std::shared_ptr<Thumbnail>;
+	const auto id = story->fullId();
+	return v::match(story->media().data, [](v::null_t) -> Result {
+		return std::make_shared<EmptyThumbnail>();
+	}, [&](not_null<PhotoData*> photo) -> Result {
+		return std::make_shared<PhotoThumbnail>(photo, id);
+	}, [&](not_null<DocumentData*> video) -> Result {
+		return std::make_shared<VideoThumbnail>(video, id);
+	});
+}
+
+rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
+	using namespace rpl::mappers;
+
+	const auto stories = &peer->owner().stories();
+	const auto peerId = peer->id;
+
+	return rpl::single(
+		peerId
+	) | rpl::then(
+		stories->sourceChanged() | rpl::filter(_1 == peerId)
+	) | rpl::map([=] {
+		auto ids = std::vector<StoryId>();
+		auto readTill = StoryId();
+		if (const auto source = stories->source(peerId)) {
+			readTill = source->readTill;
+			ids = ranges::views::all(source->ids)
+				| ranges::views::reverse
+				| ranges::views::take(kShownLastCount)
+				| ranges::views::transform(&Data::StoryIdDates::id)
+				| ranges::to_vector;
+		}
+		return rpl::make_producer<Content>([=](auto consumer) {
+			auto lifetime = rpl::lifetime();
+
+			struct State {
+				Fn<void()> check;
+				base::has_weak_ptr guard;
+				bool pushed = false;
+			};
+			const auto state = lifetime.make_state<State>();
+			state->check = [=] {
+				if (state->pushed) {
+					return;
+				}
+				auto resolving = false;
+				auto result = Content();
+				for (const auto id : ids) {
+					const auto storyId = FullStoryId{ peerId, id };
+					const auto maybe = stories->lookup(storyId);
+					if (maybe) {
+						if (!resolving) {
+							result.elements.reserve(ids.size());
+							result.elements.push_back({
+								.id = uint64(id),
+								.thumbnail = PrepareThumbnail(*maybe),
+								.unread = (id > readTill),
+							});
+						}
+					} else if (maybe.error() == Data::NoStory::Unknown) {
+						resolving = true;
+						stories->resolve(
+							storyId,
+							crl::guard(&state->guard, state->check));
+					}
+				}
+				if (resolving) {
+					return;
+				}
+				state->pushed = true;
+				consumer.put_next(std::move(result));
+				consumer.put_done();
+			};
+			rpl::single(peerId) | rpl::then(
+				stories->itemsChanged() | rpl::filter(_1 == peerId)
+			) | rpl::start_with_next(state->check, lifetime);
+
+			return lifetime;
+		});
+	}) | rpl::flatten_latest();
+}
+
 } // namespace Dialogs::Stories
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
index 1feb6a77a..8b25c8313 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
@@ -23,4 +23,6 @@ struct Content;
 	not_null<Main::Session*> session,
 	Data::StorySourcesList list);
 
+[[nodiscard]] rpl::producer<Content> LastForPeer(not_null<PeerData*> peer);
+
 } // namespace Dialogs::Stories
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index c53282395..c022e025c 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -17,13 +17,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Dialogs::Stories {
 namespace {
 
-constexpr auto kSmallUserpicsShown = 3;
-constexpr auto kSmallReadOpacity = 0.6;
+constexpr auto kSmallThumbsShown = 3;
 constexpr auto kSummaryExpandLeft = 1.5;
 constexpr auto kPreloadPages = 2;
 
-[[nodiscard]] int AvailableNameWidth() {
-	const auto &full = st::dialogsStoriesFull;
+[[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) {
+	const auto &full = st.full;
 	const auto &font = full.nameStyle.font;
 	const auto skip = font->spacew;
 	return full.photoLeft * 2 + full.photo - 2 * skip;
@@ -35,7 +34,7 @@ struct List::Layout {
 	int itemsCount = 0;
 	int shownHeight = 0;
 	float64 ratio = 0.;
-	float64 userpicLeft = 0.;
+	float64 thumbnailLeft = 0.;
 	float64 photoLeft = 0.;
 	float64 left = 0.;
 	float64 single = 0.;
@@ -52,9 +51,11 @@ struct List::Layout {
 
 List::List(
 	not_null<QWidget*> parent,
+	const style::DialogsStoriesList &st,
 	rpl::producer<Content> content,
 	Fn<int()> shownHeight)
 : RpWidget(parent)
+, _st(st)
 , _shownHeight(shownHeight) {
 	setCursor(style::cur_default);
 
@@ -64,45 +65,46 @@ List::List(
 
 	_shownAnimation.stop();
 	setMouseTracking(true);
-	resize(0, _data.empty() ? 0 : st::dialogsStoriesFull.height);
+	resize(0, _data.empty() ? 0 : st.full.height);
 }
 
 void List::showContent(Content &&content) {
 	if (_content == content) {
 		return;
 	}
-	if (content.users.empty()) {
+	if (content.elements.empty()) {
 		_hidingData = base::take(_data);
 		if (!_hidingData.empty()) {
 			toggleAnimated(false);
 		}
 		return;
 	}
-	const auto hidden = _content.users.empty();
+	const auto hidden = _content.elements.empty();
 	_content = std::move(content);
 	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;
+	_data.items.reserve(_content.elements.size());
+	for (const auto &element : _content.elements) {
+		const auto id = element.id;
+		const auto i = ranges::find(items, id, [](const Item &item) {
+			return item.element.id;
 		});
 		if (i != end(items)) {
 			_data.items.push_back(std::move(*i));
 			auto &item = _data.items.back();
-			if (item.user.userpic != user.userpic) {
-				item.user.userpic = user.userpic;
+			if (item.element.thumbnail != element.thumbnail) {
+				item.element.thumbnail = element.thumbnail;
 				item.subscribed = false;
 			}
-			if (item.user.name != user.name) {
-				item.user.name = user.name;
+			if (item.element.name != element.name) {
+				item.element.name = element.name;
 				item.nameCache = QImage();
 			}
-			item.user.unread = user.unread;
-			item.user.hidden = user.hidden;
+			item.element.unread = element.unread;
+			item.element.hidden = element.hidden;
 		} else {
-			_data.items.emplace_back(Item{ .user = user });
+			_data.items.emplace_back(Item{ .element = element });
 		}
 	}
 	updateScrollMax();
@@ -115,23 +117,25 @@ void List::showContent(Content &&content) {
 
 List::Summaries List::ComposeSummaries(Data &data) {
 	const auto total = int(data.items.size());
-	const auto skip = (total > 1 && data.items[0].user.self) ? 1 : 0;
+	const auto skip = (total > 1 && data.items[0].element.skipSmall)
+		? 1
+		: 0;
 	auto unreadInFirst = 0;
 	auto unreadTotal = 0;
 	for (auto i = skip; i != total; ++i) {
-		if (data.items[i].user.unread) {
+		if (data.items[i].element.unread) {
 			++unreadTotal;
-			if (i < skip + kSmallUserpicsShown) {
+			if (i < skip + kSmallThumbsShown) {
 				++unreadInFirst;
 			}
 		}
 	}
-	auto result = Summaries{ .skipSelf = (skip > 0) };
+	auto result = Summaries{ .skipOne = (skip > 0) };
 	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;
+			to = data.items[index].element.name;
 		} else {
 			to = (last
 				? tr::lng_stories_row_unread_and_last
@@ -140,19 +144,19 @@ List::Summaries List::ComposeSummaries(Data &data) {
 					lt_accumulated,
 					to,
 					lt_user,
-					data.items[index].user.name);
+					data.items[index].element.name);
 		}
 	};
 	if (!total) {
 		return result;
-	} else if (total <= skip + kSmallUserpicsShown) {
+	} else if (total <= skip + kSmallThumbsShown) {
 		for (auto i = skip; i != total; ++i) {
 			append(result.allNames.string, i, i == total - 1);
 		}
 	}
 	if (unreadInFirst > 0 && unreadInFirst == unreadTotal) {
 		for (auto i = skip; i != total; ++i) {
-			if (data.items[i].user.unread) {
+			if (data.items[i].element.unread) {
 				append(result.unreadNames.string, i, !--unreadTotal);
 			}
 		}
@@ -166,20 +170,22 @@ bool List::StringsEqual(const Summaries &a, const Summaries &b) {
 		&& (a.unreadNames.string == b.unreadNames.string);
 }
 
-void List::Populate(Summary &summary) {
+void List::Populate(
+		const style::DialogsStories &st,
+		Summary &summary) {
 	if (summary.empty()) {
 		return;
 	}
 	summary.cache = QImage();
-	summary.text = Ui::Text::String(
-		st::dialogsStories.nameStyle,
-		summary.string);
+	summary.text = Ui::Text::String(st.nameStyle, summary.string);
 }
 
-void List::Populate(Summaries &summaries) {
-	Populate(summaries.total);
-	Populate(summaries.allNames);
-	Populate(summaries.unreadNames);
+void List::Populate(
+		const style::DialogsStories &st,
+		Summaries &summaries) {
+	Populate(st, summaries.total);
+	Populate(st, summaries.allNames);
+	Populate(st, summaries.unreadNames);
 }
 
 void List::updateSummary(Data &data) {
@@ -188,7 +194,7 @@ void List::updateSummary(Data &data) {
 		return;
 	}
 	data.summaries = std::move(summaries);
-	Populate(data.summaries);
+	Populate(_st.small, data.summaries);
 }
 
 void List::toggleAnimated(bool shown) {
@@ -203,14 +209,14 @@ void List::updateHeight() {
 	const auto shown = _shownAnimation.value(_data.empty() ? 0. : 1.);
 	resize(
 		width(),
-		anim::interpolate(0, st::dialogsStoriesFull.height, shown));
+		anim::interpolate(0, _st.full.height, shown));
 	if (_data.empty() && shown == 0.) {
 		_hidingData = {};
 	}
 }
 
 void List::updateScrollMax() {
-	const auto &full = st::dialogsStoriesFull;
+	const auto &full = _st.full;
 	const auto singleFull = full.photoLeft * 2 + full.photo;
 	const auto widthFull = full.left + int(_data.items.size()) * singleFull;
 	_scrollLeftMax = std::max(widthFull - width(), 0);
@@ -252,8 +258,8 @@ void List::resizeEvent(QResizeEvent *e) {
 }
 
 List::Layout List::computeLayout() const {
-	const auto &st = st::dialogsStories;
-	const auto &full = st::dialogsStoriesFull;
+	const auto &st = _st.small;
+	const auto &full = _st.full;
 	const auto shownHeight = std::max(_shownHeight(), st.height);
 	const auto ratio = float64(shownHeight - st.height)
 		/ (full.height - st.height);
@@ -267,11 +273,12 @@ List::Layout List::computeLayout() const {
 		+ st::defaultDialogRow.photoSize
 		+ st::defaultDialogRow.padding.left();
 	const auto narrow = (width() <= narrowWidth);
-	const auto smallSkip = (itemsCount > 1 && rendering.items[0].user.self)
+	const auto smallSkip = (itemsCount > 1
+		&& rendering.items[0].element.skipSmall)
 		? 1
 		: 0;
 	const auto smallCount = std::min(
-		kSmallUserpicsShown,
+		kSmallThumbsShown,
 		itemsCount - smallSkip);
 	const auto smallWidth = st.photo + (smallCount - 1) * st.shift;
 	const auto leftSmall = (narrow
@@ -288,17 +295,17 @@ List::Layout List::computeLayout() const {
 	const auto startIndexSmall = std::min(startIndexFull, smallSkip);
 	const auto endIndexSmall = smallSkip + smallCount;
 	const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift);
-	const auto userpicLeftFull = cellLeftFull + full.photoLeft;
-	const auto userpicLeftSmall = cellLeftSmall + st.photoLeft;
-	const auto userpicLeft = lerp(userpicLeftSmall, userpicLeftFull);
+	const auto thumbnailLeftFull = cellLeftFull + full.photoLeft;
+	const auto thumbnailLeftSmall = cellLeftSmall + st.photoLeft;
+	const auto thumbnailLeft = lerp(thumbnailLeftSmall, thumbnailLeftFull);
 	const auto photoLeft = lerp(st.photoLeft, full.photoLeft);
 	return Layout{
 		.itemsCount = itemsCount,
 		.shownHeight = shownHeight,
 		.ratio = ratio,
-		.userpicLeft = userpicLeft,
+		.thumbnailLeft = thumbnailLeft,
 		.photoLeft = photoLeft,
-		.left = userpicLeft - photoLeft,
+		.left = thumbnailLeft - photoLeft,
 		.single = lerp(st.shift, singleFull),
 		.smallSkip = smallSkip,
 		.leftFull = leftFull,
@@ -313,8 +320,8 @@ List::Layout List::computeLayout() const {
 }
 
 void List::paintEvent(QPaintEvent *e) {
-	const auto &st = st::dialogsStories;
-	const auto &full = st::dialogsStoriesFull;
+	const auto &st = _st.small;
+	const auto &full = _st.full;
 	const auto layout = computeLayout();
 	const auto ratio = layout.ratio;
 	const auto lerp = [&](float64 a, float64 b) {
@@ -331,14 +338,14 @@ void List::paintEvent(QPaintEvent *e) {
 		+ (photoTop + (photo / 2.));
 	const auto nameScale = layout.shownHeight / float64(full.height);
 	const auto nameTop = nameScale * full.nameTop;
-	const auto nameWidth = nameScale * AvailableNameWidth();
+	const auto nameWidth = nameScale * AvailableNameWidth(_st);
 	const auto nameHeight = nameScale * full.nameStyle.font->height;
 	const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;
-	const auto readUserpicOpacity = lerp(kSmallReadOpacity, 1.);
-	const auto readUserpicAppearingOpacity = lerp(kSmallReadOpacity, 0.);
+	const auto readUserpicOpacity = lerp(_st.readOpacity, 1.);
+	const auto readUserpicAppearingOpacity = lerp(_st.readOpacity, 0.);
 
 	auto p = QPainter(this);
-	p.fillRect(e->rect(), st::dialogsBg);
+	p.fillRect(e->rect(), _st.bg);
 	p.translate(0, height() - layout.shownHeight);
 
 	const auto drawSmall = (ratio < 1.);
@@ -375,8 +382,8 @@ void List::paintEvent(QPaintEvent *e) {
 		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);
+		return (single.itemSmall && single.itemSmall->element.unread)
+			|| (single.itemFull && single.itemFull->element.unread);
 	};
 	const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
 		auto nextGradientPainted = false;
@@ -398,7 +405,9 @@ void List::paintEvent(QPaintEvent *e) {
 				}
 				if (i > first && hasUnread(current) && next) {
 					if (current.itemSmall || !next.itemSmall) {
-						if (i - 1 == first && first > 0 && !skippedPainted) {
+						if (i - 1 == first
+							&& first > 0
+							&& !skippedPainted) {
 							if (const auto skipped = lookup(i - 2)) {
 								skippedPainted = true;
 								paintGradient(skipped);
@@ -425,11 +434,15 @@ void List::paintEvent(QPaintEvent *e) {
 
 		// Unread gradient.
 		const auto x = single.x;
-		const auto userpic = QRectF(x + layout.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;
-		const auto fullUnread = itemFull && itemFull->user.unread;
+		const auto smallUnread = small && small->element.unread;
+		const auto fullUnread = itemFull && itemFull->element.unread;
 		const auto unreadOpacity = (smallUnread && fullUnread)
 			? 1.
 			: smallUnread
@@ -458,11 +471,15 @@ void List::paintEvent(QPaintEvent *e) {
 		Expects(single.itemSmall || single.itemFull);
 
 		const auto x = single.x;
-		const auto userpic = QRectF(x + layout.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;
-		const auto fullUnread = itemFull && itemFull->user.unread;
+		const auto smallUnread = small && small->element.unread;
+		const auto fullUnread = itemFull && itemFull->element.unread;
 
 		// White circle with possible read gray line.
 		const auto hasReadLine = (itemFull && !fullUnread);
@@ -483,25 +500,27 @@ void List::paintEvent(QPaintEvent *e) {
 		// Userpic.
 		if (itemFull == small) {
 			p.setOpacity(smallUnread ? 1. : readUserpicOpacity);
-			validateUserpic(itemFull);
+			validateThumbnail(itemFull);
 			const auto size = full.photo;
-			p.drawImage(userpic, itemFull->user.userpic->image(size));
+			p.drawImage(userpic, itemFull->element.thumbnail->image(size));
 		} else {
 			if (small) {
 				p.setOpacity(smallUnread
 					? (itemFull ? 1. : (1. - ratio))
 					: (itemFull
-						? kSmallReadOpacity
+						? _st.readOpacity
 						: readUserpicAppearingOpacity));
-				validateUserpic(small);
+				validateThumbnail(small);
 				const auto size = (ratio > 0.) ? full.photo : st.photo;
-				p.drawImage(userpic, small->user.userpic->image(size));
+				p.drawImage(userpic, small->element.thumbnail->image(size));
 			}
 			if (itemFull) {
 				p.setOpacity(ratio);
-				validateUserpic(itemFull);
+				validateThumbnail(itemFull);
 				const auto size = full.photo;
-				p.drawImage(userpic, itemFull->user.userpic->image(size));
+				p.drawImage(
+					userpic,
+					itemFull->element.thumbnail->image(size));
 			}
 		}
 		p.setOpacity(1.);
@@ -510,11 +529,11 @@ void List::paintEvent(QPaintEvent *e) {
 	paintSummary(p, rendering, summaryTop, ratio);
 }
 
-void List::validateUserpic(not_null<Item*> item) {
+void List::validateThumbnail(not_null<Item*> item) {
 	if (!item->subscribed) {
 		item->subscribed = true;
-		//const auto id = item.user.id;
-		item->user.userpic->subscribeToUpdates([=] {
+		//const auto id = item.element.id;
+		item->element.thumbnail->subscribeToUpdates([=] {
 			update();
 		});
 	}
@@ -525,10 +544,10 @@ void List::validateName(not_null<Item*> item) {
 	if (!item->nameCache.isNull() && item->nameCacheColor == color->c) {
 		return;
 	}
-	const auto &full = st::dialogsStoriesFull;
+	const auto &full = _st.full;
 	const auto &font = full.nameStyle.font;
-	const auto available = AvailableNameWidth();
-	const auto text = Ui::Text::String(full.nameStyle, item->user.name);
+	const auto available = AvailableNameWidth(_st);
+	const auto text = Ui::Text::String(full.nameStyle, item->element.name);
 	const auto ratio = style::DevicePixelRatio();
 	item->nameCacheColor = color->c;
 	item->nameCache = QImage(
@@ -542,13 +561,13 @@ void List::validateName(not_null<Item*> item) {
 }
 
 List::Summary &List::ChooseSummary(
+		const style::DialogsStories &st,
 		Summaries &summaries,
 		int totalItems,
 		int fullWidth) {
-	const auto &st = st::dialogsStories;
 	const auto used = std::min(
-		totalItems - (summaries.skipSelf ? 1 : 0),
-		kSmallUserpicsShown);
+		totalItems - (summaries.skipOne ? 1 : 0),
+		kSmallThumbsShown);
 	const auto taken = st.left
 		+ st.photoLeft
 		+ st.photo
@@ -572,13 +591,14 @@ List::Summary &List::ChooseSummary(
 	return summaries.total;
 }
 
-void List::PrerenderSummary(Summary &summary) {
+void List::PrerenderSummary(
+		const style::DialogsStories &st,
+		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(
@@ -597,16 +617,20 @@ void List::paintSummary(
 		float64 summaryTop,
 		float64 hidden) {
 	const auto total = int(data.items.size());
-	auto &summary = ChooseSummary(data.summaries, total, width());
-	PrerenderSummary(summary);
+	auto &summary = ChooseSummary(
+		_st.small,
+		data.summaries,
+		total,
+		width());
+	PrerenderSummary(_st.small, 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 &st = _st.small;
+	const auto &full = _st.full;
 	const auto used = std::min(
-		total - (data.summaries.skipSelf ? 1 : 0),
-		kSmallUserpicsShown);
+		total - (data.summaries.skipOne ? 1 : 0),
+		kSmallThumbsShown);
 	const auto fullLeft = st.left
 		+ st.photoLeft
 		+ st.photo
@@ -671,7 +695,7 @@ void List::mouseMoveEvent(QMouseEvent *e) {
 	if (!_dragging && _mouseDownPosition) {
 		if ((_lastMousePosition - *_mouseDownPosition).manhattanLength()
 			>= QApplication::startDragDistance()) {
-			if (_shownHeight() < st::dialogsStoriesFull.height) {
+			if (_shownHeight() < _st.full.height) {
 				_expandRequests.fire({});
 			}
 			_dragging = true;
@@ -718,7 +742,7 @@ void List::mouseReleaseEvent(QMouseEvent *e) {
 		if (_selected < 0) {
 			_expandRequests.fire({});
 		} else if (_selected < _data.items.size()) {
-			_clicks.fire_copy(_data.items[_selected].user.id);
+			_clicks.fire_copy(_data.items[_selected].element.id);
 		}
 	}
 }
@@ -737,8 +761,8 @@ void List::contextMenuEvent(QContextMenuEvent *e) {
 	auto &item = _data.items[_selected];
 	_menu = base::make_unique_q<Ui::PopupMenu>(this);
 
-	const auto id = item.user.id;
-	const auto hidden = item.user.hidden;
+	const auto id = item.element.id;
+	const auto hidden = item.element.hidden;
 	_menu->addAction(tr::lng_context_view_profile(tr::now), [=] {
 		_showProfileRequests.fire_copy(id);
 	});
@@ -781,8 +805,8 @@ void List::updateSelected() {
 	if (_pressed >= 0) {
 		return;
 	}
-	const auto &st = st::dialogsStories;
-	const auto &full = st::dialogsStoriesFull;
+	const auto &st = _st.small;
+	const auto &full = _st.full;
 	const auto p = mapFromGlobal(_lastMousePosition);
 	const auto layout = computeLayout();
 	const auto firstRightFull = layout.leftFull
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 72fd67a9a..f81f90c05 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -13,31 +13,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 class QPainter;
 
+namespace style {
+struct DialogsStories;
+struct DialogsStoriesList;
+} // namespace style
+
 namespace Ui {
 class PopupMenu;
 } // namespace Ui
 
 namespace Dialogs::Stories {
 
-class Userpic {
+class Thumbnail {
 public:
 	[[nodiscard]] virtual QImage image(int size) = 0;
 	virtual void subscribeToUpdates(Fn<void()> callback) = 0;
 };
 
-struct User {
+struct Element {
 	uint64 id = 0;
 	QString name;
-	std::shared_ptr<Userpic> userpic;
+	std::shared_ptr<Thumbnail> thumbnail;
 	bool unread = false;
 	bool hidden = false;
-	bool self = false;
+	bool skipSmall = false;
 
-	friend inline bool operator==(const User &a, const User &b) = default;
+	friend inline bool operator==(
+		const Element &a,
+		const Element &b) = default;
 };
 
 struct Content {
-	std::vector<User> users;
+	std::vector<Element> elements;
 	bool full = false;
 
 	friend inline bool operator==(
@@ -54,6 +61,7 @@ class List final : public Ui::RpWidget {
 public:
 	List(
 		not_null<QWidget*> parent,
+		const style::DialogsStoriesList &st,
 		rpl::producer<Content> content,
 		Fn<int()> shownHeight);
 
@@ -67,7 +75,7 @@ public:
 private:
 	struct Layout;
 	struct Item {
-		User user;
+		Element element;
 		QImage nameCache;
 		QColor nameCacheColor;
 		bool subscribed = false;
@@ -88,7 +96,7 @@ private:
 		Summary total;
 		Summary allNames;
 		Summary unreadNames;
-		bool skipSelf = false;
+		bool skipOne = false;
 	};
 	struct Data {
 		std::vector<Item> items;
@@ -103,13 +111,20 @@ private:
 	[[nodiscard]] static bool StringsEqual(
 		const Summaries &a,
 		const Summaries &b);
-	static void Populate(Summary &summary);
-	static void Populate(Summaries &summaries);
+	static void Populate(
+		const style::DialogsStories &st,
+		Summary &summary);
+	static void Populate(
+		const style::DialogsStories &st,
+		Summaries &summaries);
 	[[nodiscard]] static Summary &ChooseSummary(
+		const style::DialogsStories &st,
 		Summaries &summaries,
 		int totalItems,
 		int fullWidth);
-	static void PrerenderSummary(Summary &summary);
+	static void PrerenderSummary(
+		const style::DialogsStories &st,
+		Summary &summary);
 
 	void showContent(Content &&content);
 	void enterEventHook(QEnterEvent *e) override;
@@ -121,7 +136,7 @@ private:
 	void mouseReleaseEvent(QMouseEvent *e) override;
 	void contextMenuEvent(QContextMenuEvent *e) override;
 
-	void validateUserpic(not_null<Item*> item);
+	void validateThumbnail(not_null<Item*> item);
 	void validateName(not_null<Item*> item);
 	void updateScrollMax();
 	void updateSummary(Data &data);
@@ -140,6 +155,7 @@ private:
 
 	[[nodiscard]] Layout computeLayout() const;
 
+	const style::DialogsStoriesList &_st;
 	Content _content;
 	Data _data;
 	Data _hidingData;
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 3fb9ab309..e2bee8c06 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -1979,6 +1979,8 @@ bool HistoryItem::forbidsSaving() const {
 bool HistoryItem::canDelete() const {
 	if (isSponsored()) {
 		return false;
+	} else if (IsStoryMsgId(id)) {
+		return false && _history->peer->isSelf(); // #TODO stories
 	} else if (isService() && !isRegular()) {
 		return false;
 	} else if (topicRootId() == id) {
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index 62febc455..f2a9482cb 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -368,6 +368,8 @@ infoIconMediaLink: icon {{ "info/info_media_link", infoIconFg }};
 infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }};
 infoIconMediaVoice: icon {{ "info/info_media_voice", infoIconFg }};
 infoIconMediaStories: icon {{ "info/info_media_stories", infoIconFg }};
+infoIconMediaStoriesArchive: icon {{ "info/info_stories_archive", infoIconFg }};
+infoIconMediaStoriesRecent: icon {{ "info/info_stories_recent", infoIconFg }};
 
 infoRoundedIconRequests: icon {{ "info/edit/group_manage_join_requests", settingsIconFg }};
 infoRoundedIconRecentActions: icon {{ "info/edit/group_manage_actions", settingsIconFg }};
diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp
index 37dfe5529..a9e114515 100644
--- a/Telegram/SourceFiles/info/info_top_bar.cpp
+++ b/Telegram/SourceFiles/info/info_top_bar.cpp
@@ -534,6 +534,8 @@ Ui::StringWithNumbers TopBar::generateSelectedText() const {
 		case Type::MusicFile: return tr::lng_media_selected_song;
 		case Type::Link: return tr::lng_media_selected_link;
 		case Type::RoundVoiceFile: return tr::lng_media_selected_audio;
+			// #TODO stories
+		case Type::PhotoVideo: return tr::lng_media_selected_photo;
 		}
 		Unexpected("Type in TopBar::generateSelectedText()");
 	}();
diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h
index af1facc58..9d3693280 100644
--- a/Telegram/SourceFiles/info/media/info_media_buttons.h
+++ b/Telegram/SourceFiles/info/media/info_media_buttons.h
@@ -142,7 +142,7 @@ inline auto AddStoriesButton(
 		parent,
 		std::move(count),
 		[](int count) {
-			return tr::lng_profile_stories(tr::now, lt_count, count);
+			return tr::lng_profile_saved_stories(tr::now, lt_count, count);
 		},
 		tracker)->entity();
 	result->addClickHandler([=] {
diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp
index 970b63d02..9fabb28ad 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp
@@ -347,7 +347,7 @@ void ListSection::resizeToWidth(int newWidth) {
 		_itemWidth = ((newWidth - _itemsLeft) / _itemsInRow)
 			- st::infoMediaSkip;
 		for (auto &item : _items) {
-			item->resizeGetHeight(_itemWidth);
+			_itemHeight = item->resizeGetHeight(_itemWidth);
 		}
 	} break;
 
@@ -378,7 +378,7 @@ int ListSection::recountHeight() {
 	case Type::Video:
 	case Type::PhotoVideo: // #TODO stories
 	case Type::RoundFile: {
-		auto itemHeight = _itemWidth + st::infoMediaSkip;
+		auto itemHeight = _itemHeight + st::infoMediaSkip;
 		auto index = 0;
 		result += _itemsTop;
 		for (auto &item : _items) {
diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.h b/Telegram/SourceFiles/info/media/info_media_list_section.h
index 77666a31a..358712a75 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_section.h
+++ b/Telegram/SourceFiles/info/media/info_media_list_section.h
@@ -82,6 +82,7 @@ private:
 	int _itemsLeft = 0;
 	int _itemsTop = 0;
 	int _itemWidth = 0;
+	int _itemHeight = 0;
 	int _itemsInRow = 1;
 	mutable int _rowsCount = 0;
 	int _top = 0;
diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp
index f2d8dcde7..3e75975fa 100644
--- a/Telegram/SourceFiles/info/media/info_media_provider.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp
@@ -421,19 +421,21 @@ std::unique_ptr<BaseLayout> Provider::createLayout(
 		}
 		return nullptr;
 	};
-	const auto spoiler = [&] {
-		if (const auto media = item->media()) {
-			return media->hasSpoiler();
-		}
-		return false;
-	};
 
 	const auto &songSt = st::overviewFileLayout;
 	using namespace Overview::Layout;
+	const auto options = [&] {
+		const auto media = item->media();
+		return MediaOptions{ .spoiler = media && media->hasSpoiler() };
+	};
 	switch (type) {
 	case Type::Photo:
 		if (const auto photo = getPhoto()) {
-			return std::make_unique<Photo>(delegate, item, photo, spoiler());
+			return std::make_unique<Photo>(
+				delegate,
+				item,
+				photo,
+				options());
 		}
 		return nullptr;
 	case Type::GIF:
@@ -443,7 +445,7 @@ std::unique_ptr<BaseLayout> Provider::createLayout(
 		return nullptr;
 	case Type::Video:
 		if (const auto file = getFile()) {
-			return std::make_unique<Video>(delegate, item, file, spoiler());
+			return std::make_unique<Video>(delegate, item, file, options());
 		}
 		return nullptr;
 	case Type::File:
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index fe32be04d..068da1c33 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -8,17 +8,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "info/stories/info_stories_inner_widget.h"
 
 #include "data/data_peer.h"
+#include "data/data_session.h"
+#include "data/data_stories.h"
+#include "data/data_user.h"
+#include "dialogs/ui/dialogs_stories_content.h"
+#include "dialogs/ui/dialogs_stories_list.h"
 #include "info/media/info_media_list_widget.h"
 #include "info/profile/info_profile_icon.h"
 #include "info/stories/info_stories_widget.h"
 #include "info/info_controller.h"
 #include "info/info_memento.h"
 #include "lang/lang_keys.h"
+#include "main/main_session.h"
 #include "settings/settings_common.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
+#include "ui/wrap/slide_wrap.h"
 #include "ui/wrap/vertical_layout.h"
+#include "styles/style_dialogs.h"
 #include "styles/style_info.h"
+#include "styles/style_settings.h"
 
 namespace Info::Stories {
 
@@ -99,38 +108,112 @@ void InnerWidget::setupArchive() {
 		&& _isStackBottom) {
 		createArchiveButton();
 	} else {
-		_archive.destroy();
+		_buttons.destroy();
 		refreshHeight();
 	}
 }
 
 void InnerWidget::createArchiveButton() {
-	_archive.create(this);
-	_archive->show();
+	_buttons.create(this);
+	_buttons->show();
 
-	const auto button = ::Settings::AddButton(
-		_archive,
+	const auto stories = &_controller->session().data().stories();
+	const auto self = _controller->session().user();
+	const auto archive = ::Settings::AddButton(
+		_buttons,
 		tr::lng_stories_archive_button(),
 		st::infoSharedMediaButton);
-	button->addClickHandler([=] {
+	archive->addClickHandler([=] {
 		_controller->showSection(Info::Stories::Make(
 			_controller->key().storiesPeer(),
 			Stories::Tab::Archive));
 	});
+	auto count = rpl::single(
+		rpl::empty
+	) | rpl::then(stories->archiveChanged()) | rpl::map([=] {
+		const auto value = stories->archiveCount();
+		return (value > 0) ? QString::number(value) : QString();
+	});
+	::Settings::CreateRightLabel(
+		archive,
+		std::move(count),
+		st::infoSharedMediaButton,
+		tr::lng_stories_archive_button());
 	object_ptr<Profile::FloatingIcon>(
-		button,
-		st::infoIconMediaGroup,
+		archive,
+		st::infoIconMediaStoriesArchive,
 		st::infoSharedMediaButtonIconPosition)->show();
-	_archive->add(object_ptr<Ui::FixedHeightWidget>(
-		_archive,
-		st::infoProfileSkip));
-	_archive->add(object_ptr<Ui::BoxContentDivider>(_archive));
 
-	_archive->resizeToWidth(width());
-	_archive->heightValue(
+	const auto recentWrap = _buttons->add(
+		object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
+			_buttons,
+			::Settings::CreateButton(
+				_buttons,
+				tr::lng_stories_recent_button(),
+				st::infoSharedMediaButton)));
+
+	using namespace Dialogs::Stories;
+	auto last = LastForPeer(
+		self
+	) | rpl::map([=](Content &&content) {
+		for (auto &element : content.elements) {
+			element.unread = false;
+		}
+		return std::move(content);
+	}) | rpl::start_spawning(recentWrap->lifetime());
+	const auto recent = recentWrap->entity();
+	const auto thumbs = Ui::CreateChild<List>(
+		recent,
+		st::dialogsStoriesListMine,
+		rpl::duplicate(last) | rpl::filter([](const Content &content) {
+			return !content.elements.empty();
+		}),
+		[] { return st::dialogsStories.height; });
+	rpl::combine(
+		recent->sizeValue(),
+		rpl::duplicate(last)
+	) | rpl::start_with_next([=](QSize size, const Content &content) {
+		if (content.elements.empty()) {
+			return;
+		}
+		const auto width = st::defaultDialogRow.padding.left()
+			+ st::defaultDialogRow.photoSize
+			+ st::defaultDialogRow.padding.left();
+		const auto &small = st::dialogsStories;
+		const auto count = int(content.elements.size());
+		const auto smallWidth = small.photo + (count - 1) * small.shift;
+		const auto real = smallWidth;
+		const auto top = st::dialogsStories.height
+			- st::dialogsStoriesFull.height
+			+ (size.height() - st::dialogsStories.height) / 2;
+		const auto right = st::settingsButtonRightSkip - (width - real) / 2;
+		thumbs->resizeToWidth(width);
+		thumbs->moveToRight(right, top);
+	}, thumbs->lifetime());
+	thumbs->setAttribute(Qt::WA_TransparentForMouseEvents);
+	recent->addClickHandler([=] {
+		_controller->parentController()->openPeerStories(self->id);
+	});
+	object_ptr<Profile::FloatingIcon>(
+		recent,
+		st::infoIconMediaStoriesRecent,
+		st::infoSharedMediaButtonIconPosition)->show();
+	recentWrap->toggleOn(rpl::duplicate(
+		last
+	) | rpl::map([](const Content &content) {
+		return !content.elements.empty();
+	}));
+
+	_buttons->add(object_ptr<Ui::FixedHeightWidget>(
+		_buttons,
+		st::infoProfileSkip));
+	_buttons->add(object_ptr<Ui::BoxContentDivider>(_buttons));
+
+	_buttons->resizeToWidth(width());
+	_buttons->heightValue(
 	) | rpl::start_with_next([=] {
 		refreshHeight();
-	}, _archive->lifetime());
+	}, _buttons->lifetime());
 }
 
 void InnerWidget::visibleTopBottomUpdated(
@@ -194,8 +277,8 @@ int InnerWidget::resizeGetHeight(int newWidth) {
 	_inResize = true;
 	auto guard = gsl::finally([this] { _inResize = false; });
 
-	if (_archive) {
-		_archive->resizeToWidth(newWidth);
+	if (_buttons) {
+		_buttons->resizeToWidth(newWidth);
 	}
 	_list->resizeToWidth(newWidth);
 	_empty->resizeToWidth(newWidth);
@@ -211,9 +294,9 @@ void InnerWidget::refreshHeight() {
 
 int InnerWidget::recountHeight() {
 	auto top = 0;
-	if (_archive) {
-		_archive->moveToLeft(0, top);
-		top += _archive->heightNoMargins() - st::lineWidth;
+	if (_buttons) {
+		_buttons->moveToLeft(0, top);
+		top += _buttons->heightNoMargins() - st::lineWidth;
 	}
 	auto listHeight = 0;
 	if (_list) {
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
index e683013fe..92814b72e 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
@@ -71,7 +71,7 @@ private:
 
 	const not_null<Controller*> _controller;
 
-	object_ptr<Ui::VerticalLayout> _archive = { nullptr };
+	object_ptr<Ui::VerticalLayout> _buttons = { nullptr };
 	object_ptr<Media::ListWidget> _list = { nullptr };
 	object_ptr<EmptyWidget> _empty;
 
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
index 388602347..2c6621fff 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
@@ -61,7 +61,7 @@ Type Provider::type() {
 }
 
 bool Provider::hasSelectRestriction() {
-	return false;
+	return true; // #TODO stories
 }
 
 rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
@@ -292,22 +292,18 @@ std::unique_ptr<BaseLayout> Provider::createLayout(
 		}
 		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;
+	const auto options = MediaOptions{ .story = true };
 	if (const auto photo = getPhoto()) {
-		return std::make_unique<Photo>(delegate, item, photo, spoiler);
+		return std::make_unique<Photo>(delegate, item, photo, options);
 	} else if (const auto file = getFile()) {
-		return std::make_unique<Video>(delegate, item, file, spoiler);
+		return std::make_unique<Video>(delegate, item, file, options);
 	} else {
 		return std::make_unique<Photo>(
 			delegate,
 			item,
 			Data::MediaStory::LoadingStoryPhoto(&item->history()->owner()),
-			spoiler);
+			options);
 	}
 	return nullptr;
 }
@@ -316,9 +312,8 @@ ListItemSelectionData Provider::computeSelectionData(
 		not_null<const HistoryItem*> item,
 		TextSelection selection) {
 	auto result = ListItemSelectionData(selection);
-	result.canDelete = true;
-	result.canForward = item->allowsForward()
-		&& (&item->history()->session() == &_controller->session());
+	result.canDelete = item->canDelete();
+	result.canForward = item->allowsForward();
 	return result;
 }
 
diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp
index 599dd8f1b..603963621 100644
--- a/Telegram/SourceFiles/overview/overview_layout.cpp
+++ b/Telegram/SourceFiles/overview/overview_layout.cpp
@@ -65,12 +65,42 @@ TextParseOptions _documentNameOptions = {
 };
 
 constexpr auto kMaxInlineArea = 1280 * 720;
+constexpr auto kStoryRatio = 1.46;
 
 [[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) {
 	const auto dimensions = document->dimensions;
 	return dimensions.width() * dimensions.height() <= kMaxInlineArea;
 }
 
+[[nodiscard]] QImage CropMediaFrame(QImage image, int width, int height) {
+	const auto ratio = style::DevicePixelRatio();
+	width *= ratio;
+	height *= ratio;
+	const auto finalize = [&](QImage result) {
+		result = result.scaled(
+			width,
+			height,
+			Qt::IgnoreAspectRatio,
+			Qt::SmoothTransformation);
+		result.setDevicePixelRatio(ratio);
+		return result;
+	};
+	if (image.width() * height == image.height() * width) {
+		if (image.width() != width) {
+			return finalize(std::move(image));
+		}
+		image.setDevicePixelRatio(ratio);
+		return image;
+	} else if (image.width() * height > image.height() * width) {
+		const auto use = (image.height() * width) / height;
+		const auto skip = (image.width() - use) / 2;
+		return finalize(image.copy(skip, 0, use, image.height()));
+	} else {
+		const auto use = (image.width() * height) / width;
+		const auto skip = (image.height() - use) / 2;
+		return finalize(image.copy(0, skip, image.width(), use));
+	}
+}
 
 } // namespace
 
@@ -298,7 +328,7 @@ Photo::Photo(
 	not_null<Delegate*> delegate,
 	not_null<HistoryItem*> parent,
 	not_null<PhotoData*> photo,
-	bool spoiler)
+	MediaOptions options)
 : ItemBase(delegate, parent)
 , _data(photo)
 , _link(std::make_shared<PhotoOpenClickHandler>(
@@ -308,9 +338,10 @@ Photo::Photo(
 		delegate->openPhoto(photo, id);
 	}),
 	parent->fullId()))
-, _spoiler(spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] {
+, _spoiler(options.spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] {
 	delegate->repaintItem(this);
-}) : nullptr) {
+}) : nullptr)
+, _story(options.story) {
 	if (_data->inlineThumbnailBytes().isEmpty()
 		&& (_data->hasExact(Data::PhotoSize::Small)
 			|| _data->hasExact(Data::PhotoSize::Thumbnail))) {
@@ -320,14 +351,14 @@ Photo::Photo(
 
 void Photo::initDimensions() {
 	_maxw = 2 * st::overviewPhotoMinSize;
-	_minh = _maxw;
+	_minh = _story ? qRound(_maxw * kStoryRatio) : _maxw;
 }
 
 int32 Photo::resizeGetHeight(int32 width) {
 	width = qMin(width, _maxw);
-	if (width != _width || width != _height) {
-		_width = qMin(width, _maxw);
-		_height = _width;
+	if (_width != width) {
+		_width = width;
+		_height = _story ? qRound(_width * kStoryRatio) : _width;
 	}
 	return _height;
 }
@@ -382,21 +413,14 @@ void Photo::paint(Painter &p, const QRect &clip, TextSelection selection, const
 }
 
 void Photo::setPixFrom(not_null<Image*> image) {
-	const auto size = _width * cIntRetinaFactor();
+	Expects(_width > 0 && _height > 0);
+
 	auto img = image->original();
 	if (!_goodLoaded) {
 		img = Images::Blur(std::move(img));
 	}
-	if (img.width() == img.height()) {
-		if (img.width() != size) {
-			img = img.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
-		}
-	} else if (img.width() > img.height()) {
-		img = img.copy((img.width() - img.height()) / 2, 0, img.height(), img.height()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
-	} else {
-		img = img.copy(0, (img.height() - img.width()) / 2, img.width(), img.width()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
-	}
-	img.setDevicePixelRatio(cRetinaFactor());
+	_pix = Ui::PixmapFromImage(
+		CropMediaFrame(std::move(img), _width, _height));
 
 	// In case we have inline thumbnail we can unload all images and we still
 	// won't get a blank image in the media viewer when the photo is opened.
@@ -404,8 +428,6 @@ void Photo::setPixFrom(not_null<Image*> image) {
 		_dataMedia = nullptr;
 		delegate()->unregisterHeavyItem(this);
 	}
-
-	_pix = Ui::PixmapFromImage(std::move(img));
 }
 
 void Photo::ensureDataMediaCreated() const {
@@ -445,13 +467,14 @@ Video::Video(
 	not_null<Delegate*> delegate,
 	not_null<HistoryItem*> parent,
 	not_null<DocumentData*> video,
-	bool spoiler)
+	MediaOptions options)
 : RadialProgressItem(delegate, parent)
 , _data(video)
 , _duration(Ui::FormatDurationText(_data->duration() / 1000))
-, _spoiler(spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] {
+, _spoiler(options.spoiler ? std::make_unique<Ui::SpoilerAnimation>([=] {
 	delegate->repaintItem(this);
-}) : nullptr) {
+}) : nullptr)
+, _story(options.story) {
 	setDocumentLinks(_data);
 	_data->loadThumbnail(parent->fullId());
 }
@@ -460,12 +483,15 @@ Video::~Video() = default;
 
 void Video::initDimensions() {
 	_maxw = 2 * st::overviewPhotoMinSize;
-	_minh = _maxw;
+	_minh = _story ? qRound(_maxw * kStoryRatio) : _maxw;
 }
 
 int32 Video::resizeGetHeight(int32 width) {
-	_width = qMin(width, _maxw);
-	_height = _width;
+	width = qMin(width, _maxw);
+	if (_width != width) {
+		_width = width;
+		_height = _story ? qRound(_width * kStoryRatio) : _width;
+	}
 	return _height;
 }
 
@@ -497,18 +523,8 @@ void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const
 			: thumbnail
 			? thumbnail->original()
 			: Images::Blur(blurred->original());
-		if (img.width() == img.height()) {
-			if (img.width() != size) {
-				img = img.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
-			}
-		} else if (img.width() > img.height()) {
-			img = img.copy((img.width() - img.height()) / 2, 0, img.height(), img.height()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
-		} else {
-			img = img.copy(0, (img.height() - img.width()) / 2, img.width(), img.width()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
-		}
-		img.setDevicePixelRatio(cRetinaFactor());
-
-		_pix = Ui::PixmapFromImage(std::move(img));
+		_pix = Ui::PixmapFromImage(
+			CropMediaFrame(std::move(img), _width, _height));
 		_pixBlurred = !(thumbnail || good);
 	}
 
diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h
index 53dc9bdb0..e566de4a5 100644
--- a/Telegram/SourceFiles/overview/overview_layout.h
+++ b/Telegram/SourceFiles/overview/overview_layout.h
@@ -183,13 +183,18 @@ struct Info : public RuntimeComponent<Info, LayoutItemBase> {
 	int top = 0;
 };
 
+struct MediaOptions {
+	bool spoiler = false;
+	bool story = false;
+};
+
 class Photo final : public ItemBase {
 public:
 	Photo(
 		not_null<Delegate*> delegate,
 		not_null<HistoryItem*> parent,
 		not_null<PhotoData*> photo,
-		bool spoiler);
+		MediaOptions options);
 
 	void initDimensions() override;
 	int32 resizeGetHeight(int32 width) override;
@@ -212,6 +217,7 @@ private:
 
 	QPixmap _pix;
 	bool _goodLoaded = false;
+	bool _story = false;
 
 };
 
@@ -279,7 +285,7 @@ public:
 		not_null<Delegate*> delegate,
 		not_null<HistoryItem*> parent,
 		not_null<DocumentData*> video,
-		bool spoiler);
+		MediaOptions options);
 	~Video();
 
 	void initDimensions() override;
@@ -311,6 +317,7 @@ private:
 
 	QPixmap _pix;
 	bool _pixBlurred = true;
+	bool _story = false;
 
 };
 
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index c21ef8437..394ca2358 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -2530,7 +2530,7 @@ void SessionController::openPeerStory(
 
 void SessionController::openPeerStories(
 		PeerId peerId,
-		Data::StorySourcesList list) {
+		std::optional<Data::StorySourcesList> list) {
 	using namespace Media::View;
 	using namespace Data;
 
@@ -2541,7 +2541,9 @@ void SessionController::openPeerStories(
 		openPeerStory(
 			source->user,
 			j != source->ids.end() ? j->id : source->ids.front().id,
-			{ list });
+			(list
+				? StoriesContext{ *list }
+				: StoriesContext{ StoriesContextPeer() }));
 	}
 }
 
diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h
index 4903c6be9..7d9567427 100644
--- a/Telegram/SourceFiles/window/window_session_controller.h
+++ b/Telegram/SourceFiles/window/window_session_controller.h
@@ -582,7 +582,9 @@ public:
 		not_null<PeerData*> peer,
 		StoryId storyId,
 		Data::StoriesContext context);
-	void openPeerStories(PeerId peerId, Data::StorySourcesList list);
+	void openPeerStories(
+		PeerId peerId,
+		std::optional<Data::StorySourcesList> list = std::nullopt);
 
 	struct PaintContextArgs {
 		not_null<Ui::ChatTheme*> theme;

From 738e20252e880ddd2323b23304122cc75a389e7c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 20 Jun 2023 18:24:40 +0400
Subject: [PATCH 089/259] Fix a crash in saved stories layer management.

---
 Telegram/SourceFiles/info/info_wrap_widget.cpp | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp
index 0c5ba45ed..3ff8fe7be 100644
--- a/Telegram/SourceFiles/info/info_wrap_widget.cpp
+++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp
@@ -244,6 +244,12 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const {
 		return Dialogs::RowDescriptor(
 			peer->owner().history(peer),
 			FullMsgId());
+	} else if (const auto storiesPeer = key().storiesPeer()) {
+		return (key().storiesTab() == Stories::Tab::Saved)
+			? Dialogs::RowDescriptor(
+				storiesPeer->owner().history(storiesPeer),
+				FullMsgId())
+			: Dialogs::RowDescriptor();
 	} else if (key().settingsSelf() || key().isDownloads() || key().poll()) {
 		return Dialogs::RowDescriptor();
 	}

From d2dd63e90af13948b722f714bd9f9b828ea47faf Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 20 Jun 2023 19:31:40 +0400
Subject: [PATCH 090/259] Show active stories in profile top bar.

---
 Telegram/SourceFiles/dialogs/dialogs.style    |  3 +
 .../dialogs/ui/dialogs_stories_list.cpp       |  2 +-
 .../SourceFiles/info/info_content_widget.cpp  |  5 +
 .../SourceFiles/info/info_content_widget.h    |  6 ++
 Telegram/SourceFiles/info/info_top_bar.cpp    | 94 ++++++++++++++++++-
 Telegram/SourceFiles/info/info_top_bar.h      | 24 ++++-
 .../SourceFiles/info/info_wrap_widget.cpp     |  6 ++
 .../info/profile/info_profile_widget.cpp      | 10 ++
 .../info/profile/info_profile_widget.h        |  1 +
 9 files changed, 143 insertions(+), 8 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index 30b36538d..8e9f09232 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -505,6 +505,7 @@ DialogsStoriesList {
 	full: DialogsStories;
 	bg: color;
 	readOpacity: double;
+	fullClickable: int;
 }
 
 dialogsStories: DialogsStories {
@@ -544,9 +545,11 @@ dialogsStoriesList: DialogsStoriesList {
 	full: dialogsStoriesFull;
 	bg: dialogsBg;
 	readOpacity: 0.6;
+	fullClickable: 0;
 }
 dialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) {
 	bg: transparent;
+	fullClickable: 1;
 }
 dialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) {
 	readOpacity: 1.;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index c022e025c..b630d9b64 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -846,7 +846,7 @@ void List::updateSelected() {
 				+ lastRightAdd)
 		? (infiniteIndex - 1) // Last small part should still be clickable.
 		: (startIndex + infiniteIndex >= endIndex)
-		? -1
+		? (_st.fullClickable ? (endIndex - 1) : -1)
 		: infiniteIndex;
 	const auto selected = (index < 0
 		|| startIndex + index >= layout.itemsCount)
diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp
index 640c75d67..e96ba3cfc 100644
--- a/Telegram/SourceFiles/info/info_content_widget.cpp
+++ b/Telegram/SourceFiles/info/info_content_widget.cpp
@@ -273,6 +273,11 @@ void ContentWidget::setViewport(
 	}, _scroll->lifetime());
 }
 
+auto ContentWidget::titleStories()
+-> rpl::producer<Dialogs::Stories::Content> {
+	return nullptr;
+}
+
 void ContentWidget::saveChanges(FnMut<void()> done) {
 	done();
 }
diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h
index 1a54f5fdf..6a6f75bbd 100644
--- a/Telegram/SourceFiles/info/info_content_widget.h
+++ b/Telegram/SourceFiles/info/info_content_widget.h
@@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/rp_widget.h"
 #include "info/info_wrap_widget.h"
 
+namespace Dialogs::Stories {
+struct Content;
+} // namespace Dialogs::Stories
+
 namespace Storage {
 enum class SharedMediaType : signed char;
 } // namespace Storage
@@ -88,6 +92,8 @@ public:
 	}
 
 	[[nodiscard]] virtual rpl::producer<QString> title() = 0;
+	[[nodiscard]] virtual auto titleStories()
+		-> rpl::producer<Dialogs::Stories::Content>;
 
 	virtual void saveChanges(FnMut<void()> done);
 
diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp
index a9e114515..309ca167d 100644
--- a/Telegram/SourceFiles/info/info_top_bar.cpp
+++ b/Telegram/SourceFiles/info/info_top_bar.cpp
@@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include <rpl/never.h>
 #include <rpl/merge.h>
+#include "dialogs/ui/dialogs_stories_content.h"
+#include "dialogs/ui/dialogs_stories_list.h"
 #include "lang/lang_keys.h"
 #include "lang/lang_numbers_animation.h"
 #include "info/info_wrap_widget.h"
@@ -30,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_session.h"
 #include "data/data_channel.h"
 #include "data/data_user.h"
+#include "styles/style_dialogs.h"
 #include "styles/style_info.h"
 
 namespace Info {
@@ -88,9 +91,11 @@ void TopBar::setTitle(rpl::producer<QString> &&title) {
 		object_ptr<Ui::FlatLabel>(this, std::move(title), _st.title),
 		st::infoTopBarScale);
 	_title->setDuration(st::infoTopBarDuration);
-	_title->toggle(!selectionMode(), anim::type::instant);
+	_title->toggle(
+		!selectionMode() && !storiesTitle(),
+		anim::type::instant);
 	registerToggleControlCallback(_title.data(), [=] {
-		return !selectionMode() && !searchMode();
+		return !selectionMode() && !storiesTitle() && !searchMode();
 	});
 
 	if (_back) {
@@ -309,12 +314,15 @@ int TopBar::resizeGetHeight(int newWidth) {
 void TopBar::updateControlsGeometry(int newWidth) {
 	updateDefaultControlsGeometry(newWidth);
 	updateSelectionControlsGeometry(newWidth);
+	updateStoriesGeometry(newWidth);
 }
 
 void TopBar::updateDefaultControlsGeometry(int newWidth) {
 	auto right = 0;
 	for (auto &button : _buttons) {
-		if (!button) continue;
+		if (!button) {
+			continue;
+		}
 		button->moveToRight(right, 0, newWidth);
 		right += button->width();
 	}
@@ -362,6 +370,28 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) {
 		newWidth);
 }
 
+void TopBar::updateStoriesGeometry(int newWidth) {
+	if (!_stories) {
+		return;
+	}
+
+	auto right = 0;
+	for (auto &button : _buttons) {
+		if (!button) {
+			continue;
+		}
+		button->moveToRight(right, 0, newWidth);
+		right += button->width();
+	}
+	const auto left = (_back ? _st.back.width : _st.titlePosition.x())
+		- st::dialogsStories.left - st::dialogsStories.photoLeft;
+	const auto top = st::dialogsStories.height
+		- st::dialogsStoriesFull.height
+		+ (_st.height - st::dialogsStories.height) / 2;
+	_stories->resizeToWidth(newWidth - left - right);
+	_stories->moveToLeft(left, top, newWidth);
+}
+
 void TopBar::paintEvent(QPaintEvent *e) {
 	auto p = QPainter(this);
 
@@ -412,6 +442,60 @@ void TopBar::updateControlsVisibility(anim::type animated) {
 	}
 }
 
+void TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) {
+	_storiesLifetime.destroy();
+	if (content) {
+		using namespace Dialogs::Stories;
+
+		auto last = std::move(
+			content
+		) | rpl::start_spawning(_storiesLifetime);
+		delete _stories;
+
+		const auto stories = Ui::CreateChild<Ui::FadeWrap<List>>(
+			this,
+			object_ptr<List>(
+				this,
+				st::dialogsStoriesListInfo,
+				rpl::duplicate(
+					last
+				) | rpl::filter([](const Content &content) {
+					return !content.elements.empty();
+				}),
+				[] { return st::dialogsStories.height; }),
+				st::infoTopBarScale);
+		registerToggleControlCallback(
+			stories,
+			[this] { return _storiesCount > 0; });
+		stories->toggle(false, anim::type::instant);
+		stories->setDuration(st::infoTopBarDuration);
+		_stories = stories;
+		_stories->entity()->clicks(
+		) | rpl::start_to_stream(_storyClicks, _stories->lifetime());
+		if (_back) {
+			_back->raise();
+		}
+
+		rpl::duplicate(
+			last
+		) | rpl::start_with_next([=](const Content &content) {
+			const auto count = int(content.elements.size());
+			if (_storiesCount != count) {
+				const auto was = (_storiesCount > 0);
+				_storiesCount = count;
+				const auto now = (_storiesCount > 0);
+				if (was != now) {
+					updateControlsVisibility(anim::type::normal);
+				}
+				updateControlsGeometry(width());
+			}
+		}, _storiesLifetime);
+	} else {
+		_storiesCount = 0;
+	}
+	updateControlsVisibility(anim::type::instant);
+}
+
 void TopBar::setSelectedItems(SelectedItems &&items) {
 	auto wasSelectionMode = selectionMode();
 	_selectedItems = std::move(items);
@@ -550,6 +634,10 @@ bool TopBar::selectionMode() const {
 	return !_selectedItems.list.empty();
 }
 
+bool TopBar::storiesTitle() const {
+	return _storiesCount > 0;
+}
+
 bool TopBar::searchMode() const {
 	return _searchModeAvailable && _searchModeEnabled;
 }
diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h
index a2ed052c5..b038e213a 100644
--- a/Telegram/SourceFiles/info/info_top_bar.h
+++ b/Telegram/SourceFiles/info/info_top_bar.h
@@ -18,6 +18,11 @@ namespace style {
 struct InfoTopBar;
 } // namespace style
 
+namespace Dialogs::Stories {
+class List;
+struct Content;
+} // namespace Dialogs::Stories
+
 namespace Window {
 class SessionNavigation;
 } // namespace Window
@@ -43,11 +48,15 @@ public:
 		const style::InfoTopBar &st,
 		SelectedItems &&items);
 
-	auto backRequest() const {
+	[[nodiscard]] auto backRequest() const {
 		return _backClicks.events();
 	}
+	[[nodiscard]] auto storyClicks() const {
+		return _storyClicks.events();
+	}
 
 	void setTitle(rpl::producer<QString> &&title);
+	void setStories(rpl::producer<Dialogs::Stories::Content> content);
 	void enableBackButton();
 	void highlight();
 
@@ -95,6 +104,7 @@ private:
 	void updateControlsGeometry(int newWidth);
 	void updateDefaultControlsGeometry(int newWidth);
 	void updateSelectionControlsGeometry(int newWidth);
+	void updateStoriesGeometry(int newWidth);
 	Ui::FadeWrap<Ui::RpWidget> *pushButton(
 		base::unique_qptr<Ui::RpWidget> button);
 	void forceButtonVisibility(
@@ -104,9 +114,10 @@ private:
 	void startHighlightAnimation();
 	void updateControlsVisibility(anim::type animated);
 
-	bool selectionMode() const;
-	bool searchMode() const;
-	Ui::StringWithNumbers generateSelectedText() const;
+	[[nodiscard]] bool selectionMode() const;
+	[[nodiscard]] bool storiesTitle() const;
+	[[nodiscard]] bool searchMode() const;
+	[[nodiscard]] Ui::StringWithNumbers generateSelectedText() const;
 	[[nodiscard]] bool computeCanDelete() const;
 	[[nodiscard]] bool computeCanForward() const;
 	void updateSelectionState();
@@ -147,6 +158,7 @@ private:
 	QPointer<Ui::InputField> _searchField;
 
 	rpl::event_stream<> _backClicks;
+	rpl::event_stream<uint64> _storyClicks;
 
 	SelectedItems _selectedItems;
 	bool _canDelete = false;
@@ -157,6 +169,10 @@ private:
 	QPointer<Ui::FadeWrap<Ui::IconButton>> _delete;
 	rpl::event_stream<SelectionAction> _selectionActionRequests;
 
+	QPointer<Ui::FadeWrap<Dialogs::Stories::List>> _stories;
+	rpl::lifetime _storiesLifetime;
+	int _storiesCount = 0;
+
 	using UpdateCallback = Fn<bool(anim::type)>;
 	std::map<QObject*, UpdateCallback> _updateControlCallbacks;
 
diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp
index 3ff8fe7be..2d8e2669a 100644
--- a/Telegram/SourceFiles/info/info_wrap_widget.cpp
+++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp
@@ -303,6 +303,11 @@ void WrapWidget::createTopBar() {
 			_controller->parentController()->closeThirdSection();
 		});
 	}
+	_topBar->storyClicks() | rpl::start_with_next([=] {
+		if (const auto peer = _controller->key().peer()) {
+			_controller->parentController()->openPeerStories(peer->id);
+		}
+	}, _topBar->lifetime());
 	if (wrapValue == Wrap::Layer) {
 		auto close = _topBar->addButton(
 			base::make_unique_q<Ui::IconButton>(
@@ -579,6 +584,7 @@ void WrapWidget::finishShowContent() {
 	_content->setIsStackBottom(!hasStackHistory());
 	if (_topBar) {
 		_topBar->setTitle(_content->title());
+		_topBar->setStories(_content->titleStories());
 	}
 	_desiredHeights.fire(desiredHeightForContent());
 	_desiredShadowVisibilities.fire(_content->desiredShadowVisibility());
diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp
index f610c2bfe..40b374ef1 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "info/profile/info_profile_widget.h"
 
+#include "dialogs/ui/dialogs_stories_content.h"
 #include "info/profile/info_profile_inner_widget.h"
 #include "info/profile/info_profile_members.h"
 #include "ui/widgets/scroll_area.h"
@@ -105,7 +106,16 @@ rpl::producer<QString> Widget::title() {
 		return tr::lng_info_group_title();
 	}
 	Unexpected("Bad peer type in Info::TitleValue()");
+}
 
+rpl::producer<Dialogs::Stories::Content> Widget::titleStories() {
+	const auto peer = controller()->key().peer();
+	if (const auto user = peer->asUser()) {
+		if (!user->isBot()) {
+			return Dialogs::Stories::LastForPeer(user);
+		}
+	}
+	return nullptr;
 }
 
 bool Widget::showInternal(not_null<ContentMemento*> memento) {
diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.h b/Telegram/SourceFiles/info/profile/info_profile_widget.h
index 51e9f0b06..38758fc00 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_widget.h
+++ b/Telegram/SourceFiles/info/profile/info_profile_widget.h
@@ -60,6 +60,7 @@ public:
 	void setInnerFocus() override;
 
 	rpl::producer<QString> title() override;
+	rpl::producer<Dialogs::Stories::Content> titleStories() override;
 
 private:
 	void saveState(not_null<Memento*> memento);

From 19d0bf142cdaa4b38190168dd7a3d7490b2d1875 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 21 Jun 2023 09:34:59 +0400
Subject: [PATCH 091/259] Improve empty / archived stories section.

---
 .../icons/info/info_media_story_empty.png     | Bin 0 -> 1534 bytes
 .../icons/info/info_media_story_empty@2x.png  | Bin 0 -> 3435 bytes
 .../icons/info/info_media_story_empty@3x.png  | Bin 0 -> 5763 bytes
 Telegram/Resources/langs/lang.strings         |   1 +
 .../dialogs/ui/dialogs_stories_content.cpp    |   3 +-
 .../dialogs/ui/dialogs_stories_list.cpp       |   8 +-
 .../dialogs/ui/dialogs_stories_list.h         |   1 +
 Telegram/SourceFiles/info/info.style          |   9 +++
 .../stories/info_stories_inner_widget.cpp     |  74 ++++++++++++------
 .../info/stories/info_stories_inner_widget.h  |  10 ++-
 10 files changed, 73 insertions(+), 33 deletions(-)
 create mode 100644 Telegram/Resources/icons/info/info_media_story_empty.png
 create mode 100644 Telegram/Resources/icons/info/info_media_story_empty@2x.png
 create mode 100644 Telegram/Resources/icons/info/info_media_story_empty@3x.png

diff --git a/Telegram/Resources/icons/info/info_media_story_empty.png b/Telegram/Resources/icons/info/info_media_story_empty.png
new file mode 100644
index 0000000000000000000000000000000000000000..6196956ba4c9da3760487afaa77a0445271a8be0
GIT binary patch
literal 1534
zcmV<a1p)erP)<h;3K|Lk000e1NJLTq003YB002M;0ssI2YF6c000001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91V4wp41ONa40RR91KmY&$0BG`XN&o-_Xh}ptRCodHTUjV=Ul_lpi;S74
z44E>Ou_T2TQi>PT3lhnb>BR#{@+jU!Azn-=nF_^|p$rivb5drR=i&Z;|JJ>`?m5o3
z&z{e1?+5l->s#OVTfe=A{eA1(KYl2e!hphn!hphn!ho(aU?2zJPEJm4Zf;&)UhnVk
z4-XG=fFv;n0|SGEgoLK1rlqB&ySuwzf3BOG8&gwLiNQ%|USD57H8r)fv-9%u@((Bf
z|Nr>-xW2wVJUr~~?pB94K0bbOa?;Sy;N#<?Ui<HqBqk=VtgKMHTwGjqbaZ57WCR5T
z{rvfprk{Izd!*I<{k?~W2aTkzR)vLyO-@ddPGY1qHa5n^#bK5)><?C``uchdqO-Fz
z7Z(?Xt#n4Ly1M%F^Aqg6yu5%KTU%S!(3x|}+JvYLvE26b^pJsr8*Ea%n>R5r;a#Ul
z3SC`Y7Zw)4&g<)IVPT;VR$E$H9v>g`^74dGF04*RN5_MM1F&;`evaivAY=Xg{V;(Y
zFefKRAQOV9#g;TPGXv;bTU$a*D+;uYjm`4%G7RDUfGX61o3^$#fWErA^7Hc(8ZJ0$
zZf+K63JU_hw6qlLz+*^CN)p6?PAheHcYl6<20>+IWjfsvp}@n306SAtQ$iWgX}uvK
zA=pqrP*hZuR2_+oj0^|}kjnZuc1K4?!A@Ua-#2oCRl^H;dwWA{z`?;mFe7Xg2L}g(
z9pLlz^<}GrYdbSDGkCLrQCV5ZRTuB=tE#GiY<PH>cg<QUY;A1?jK#%8ty+m8OuUMM
zf&wv&X}00%>AAYP3fEt=h!iz&yWp?mf``G-zLAj;+*iCz*4Eb2G+`)4qd^u`#l^)K
z*t4^<%+ciM=c5U^LPJ9}HDoBJsW~s@`rNfdM@K`+#KZ)1Rd^?$gt5R=_bN9G#Z+4H
z`;!NPh|jgLvB4bK+S(c=yuQ9p-K*R%6jNy>yPt@JVzPvXhcib8kDtjJ#86D#E*3BK
zLdi-HO9esLK_o-)x8bp|F`NiEJ3IefEYUAKWU|(B2{!YKo#07HN#R8;4{H!FIQUYc
zr>93$;kAPoom>e*$Qc))SXfx_0*Ip$44<Q;BPfwkn32UI&5;HA=I{sO<KyXNM6I2h
zo5OR<%*^C&)zZ?^+}xbI9v(GtonnBpxw(nI>;f?eD(B|r4h|0P@9#f7Jt27k@d_k(
z?C$RN_V#9FWq~ptdn=797On<>;>-_@7<ok?`_7kt5y7aas4z4%l%{XISY_d?`uqED
zZ*LPuj*pKKrbtgukBN!#_V&gK5*r(vot=#UE?#Gd46+_zg%`BaDA6k1fmcdwG3fd2
z?JdIlG^4}V*tn*q22&5haAoZ6?diK`ZEcNF3HIs5B&a0;vyqVzCbmfu3<eIv@MA<o
z1ad;a5fX*y^^;QrI5G5M64YuHa-E-_$H6p>6R+L@%)Zmp)1jdu^(v|a(I~Q_;cSj7
zl+le=L3<X?<hM<bi$$ibOgBEskA1AHtgzT4#ZN|~Mg<C40;Z>@Rmzp0zZsy8p3BS2
z6;i%tproV(3xSfn@HIQ6w7tC@9Ep?uVrOSpR8%C#qf{D(L`$H;jNaMV!NWj8It?B<
zR*8^;hsOYi7VO~N2TA*KWPu)&&`F$_R#@rag(N2@(<6~@t<Y(}IBvoxF!Jp`Lu5QA
zk~6fZdtw|vML_hUtE&sXSY2J6?sc9yVwI0skU~v9W(f=o#EPp)Pg+_UR$_7u3=C)z
z`$x)Id=7>1JN7A@K_hY`lY<j69)=EmQ1|<oO$=5v+rS4;2nJ$+AWnosB+Y_KjlzJ!
kfWm;nfWm;nfEEn=2l1?1r|GMat^fc407*qoM6N<$g4>C_Pyhe`

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_media_story_empty@2x.png b/Telegram/Resources/icons/info/info_media_story_empty@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3e6997cd164e77196d4c0d1be72b22a29494871
GIT binary patch
literal 3435
zcmb7HX*|?z_n-NhG{(}5F-SAX8repY^)CC8kTvUMLY9zy&o<U(5VwjfF-U~lzROsX
zge*1INR~2U5)G06-2WHP>*slKuJ1Y5Id9JSobTtHlXAmEpA#kk1A#!Ch6Xz3Or$U+
z1`06W@m0Dy6TkuH`dXms5%gE4q2p>}c*ocnbb+ZuLEt105aizwrU)<v1Y#)!gIJgd
z{`apC_>Y%T$nqaO=HEcnAf5mMaZDNNXj+DXHy!VKaT)XYG=9JGk;km!+YgkkgPjoB
zoGjrXYH;FFOZ+tlZKUW0*{TMu`QsZ`OWY04L|JOjKPcouO)5hC*zJP4qCPFpj`Fw9
z{+j=;_WS$y_O+vK8f`cypg<)*UnO9b^i1#w3f78&pM(7OzyoDS`R8RL;BdHrfB+hO
z_%XVRl~Z8xI9>4S>X$EBPoAhD5brLkS;juS<iU->U{I*tU(sD$7}d#YM|-xJD*NW}
zpX>Df9k&&!AwM*rc=rP-HPzVEv@4YU`PbH`<M(?+5@|9ntE=nk`dvvwS6UKC@9Dc-
zrLM?*^W2d`St~QM*xI2giijR5^v~gb^Sw_n80_=%GF>y!J^@Arfrp)WWgTp+t@rFj
z9G+vphV9X&^VLFR9oqtbe0ek99-_T7H)LC^$C=}KCH$xRbe*fFrl!w^IhZIMgBKAK
zTiM<CD9gjDkV__$hlhttN=mY``G*U4*;o{h=*vlULL@^WA)y0zbD0UaJGBOHXJ_Z?
zT3V-HnwOcG$;^fWquPM$3@5kr-8OD)MM+sFd&aGYvc$&6$DcPXc(J$An{>*;!lJjg
zH+4}2i}hU{&iVMyttX24d3jzs0Xi1yfDMU{mv`vEG<<k;R8Tz>Yi;=1-s%Qg*>|zv
zuZV3wor(Vbeny+*gByvv@NBh^>Pfc_0FTSe&7FODQ*LXAUCrfn8b-c#FKS})_AcCg
zuEh_BJNu84xj?a*j*iZ*yO~Fs5D59{==U$z8G_0fgT9>4dg6_}H)J3U^YYNE+b-G4
zrqLtE46JsZD%$;egfxKpBrdH~*9pyzbOM^#vDrd+tK?cFuK_;~t*0q{n@!{&T7A%;
z!!TOutU*U*rL;IO{Ha8HVWXWy;$ZE}(r}~t@F9)v<6z@La*|_YU1I25MxybwkvF?H
z>%_6?vn%vEjU;9li8dgF_s#<;DJh4Kk>l*V(v14#{DQ<&g0d4ucRyB78-gO}u$Hc<
zqoLzBCeN<S<AfzzAAN<3PYzwo5&^~5)L<Da+cWinU(LaRG8%yD&p$^Ih7D6MZ`#L*
zJaaCxuCn8kby#dXIdJw?Cig<a&e~`?k+>sPBrVE*7!<nMy~n#LZ~G!bdIaW0=-8X<
zjQA*<c!$Wc0o_|<?^gTev`h}!T1b1C7ta$%IoRFUUveACQ_&jaM(Cd#tFl*$n&<k(
zp`UE51=9pA5XwyQZ}*Nqk-Np=uZ!xDLDZswV!hPcGXzN97n#?>@-D8Bk$kn+JW!ug
zK)R`ot>@``%9YT2-)S`3-sbFE-1}lF$RcR8;bcm=MOoX0&El=L;LQgC)!bal>+WhH
zEDG%_>+g%N$NKgmAG<5G%9nf7e#*li@+;gS8Iq_~wrKKVYyfS%lUZJME=F;kz3J1w
ztwhc<{JwIyju-aL)rO?(Pqar;H~D$C9luVN|Glqno5zy36+e!h^!g?(4s$^Vx!yQe
z8@y>}mUaK}$Y6#@*dR9Jr>&VHG%-JqNxX|1%=C+kGTC&c1H=8tOAI+c5SwnZfW37J
zq`~d?-sbh~@p1FV&*NbLBZ?mFyR-VvLh?(Ebe;f#i%G<LAFCZx1r+_We@^i<M<4(3
zYSGgQX*k>buE}efJ4qNj-4?!Ev4)$vUEb<i0JBU1by(&A^BrOJL;eMw?(fM7f#meY
zMvn>cdC`9JiFd44hbf?YSLd3%b2SUS=Gz{Xr)5!Kt!|?Q2Tzek#)79TH_ijf5@`gp
z%DaBw=b;WYZ$?e)Z2~qvR873kv+-2ruQ5OwRrbKoR+!|gav&frE*`Ndm%V3Ruqv#N
zmlD&g`^+c0V{|KQbYuC^$;K(CWg|-B`w=yH!2GNGv*!kD6n}r8R}-V3QSy;cm|smU
zU2Rf%%gb~x1%nBco|2QSsMCH^*KJQv=}$eV$f=l;v=9Z7M}hTHK`jDN^ys`yE&ILN
z$v<DR3MvQ`!pT!nU(=Uf$=x=NW>$bw8Fh5P(^7velJOFdn91Z8_fl})OYD+&Kx#(_
zT#GzpxF;PQ>{>`J(WJeSY7yaYGQ}PPy@!UE@~p$Utf&XlCmy1*Jw8@roQu6@o20!H
z;`d>q>}%C+<4u6M_zz0b^y>v;N}fdS<y)~>WvOX0Ix$9CfK3vvy?Pw)HJHI^@MC-=
zLi~jP#F}y|Nl>b!&9Q$$6&ovbQnNN^QQca4qAz~`85TNfn&$z@^kCsh4oSWeOv|nW
z{dGXUi5nAAE!Ar9oPy<I%S@jQm~pPy57BpKtXc6gXs}Ua4+9g*U}fXWgbE~S6#A7k
zTC9dFRuyVsy*|(2uCLXyBuGL(q(BFVza1EFDucg9wu?%RjsEFpMf$w#b7g6XWAh7H
z@|LhyFXj+_j-TL*y;3U_bjYCByNwD@cHEOn@-Ep}=v15C&?JZLtS;a&uc*rdscM1i
zaAIjs{gNu@c^?Pqt1g4^(u%O8FwYew;9^;L;J2-iffBf%+lNY9CX0fq#OxM&iu``A
zzaOYEv{V#X%y<5aTaAU4Lt|20W~iu><GSMZ$M@3JTRaVYXsB<`6<#D#WNC=I2hRnC
z+PsR~|J+Z^xWWB8i6cA)q3ch3@~1!NZlw8Aq{I=?W~xF-bJ=Bc<?Rz@$94v+nTWX#
z$)mw?Xzzg;&*MKwLz<;^b^f1T-AA4`@tSEU=T)(9_DLPRrKt(t8t5CWwrjk`j<_Jg
z4=yH1IXw+xY~w$?GY8)Okg;!C{O)_dhoZPfiDkL?G?XV4!x1R(QtLiVrP|uWglmJ8
znaN%I!&xc@@r1ht0?qiMe(!{4%6=7pN~pho-C7pHiO0U|T<DiU#KPK0z|bqh?iP^6
zQ2RwCpE(KN{Zx%U2AK|fP|Fz-8#Zpr9HxR-BAqTVXB~}AQGZfVq7TD!q8|MLX+Wda
z5`NK<fNYNnFA7DkqKvPzxn6N#8@&Btg-x++Qc-wNoY$cP*qm>_xk&8|;ix8Gkd?io
z8t?n{aMrVw-u#n6ch-aH`3}#$)a@=x;Dqg{IS+7n^v92xC$er1<nghbQvJhR6$<!8
zxVgEr_s)S!LtbdD!*iP@U>uhgDsAf@K7?Zq*I&d1{hbu`i0Zh6f?nprcqunLpj8k&
zCZ4Onp$nR@m%1l~7c&Oy-dZnOEFo0RSQlXNz?=aVir-s)Q#O`JCOYL*M*t6~3b8DA
z*GBSn&waxR*f>ul1aNHxy#G8@-BB{Dr^6ymDj0g+fSoxD*UC~;epIKyu8hXvaB1bG
zCq>IEdf^nXeB0hE{=Tf;9D@<MfCi#nY$9}(w0=ek#10Q%$0hj(1aQtIZ+q}yq|tNy
zPKL`t28GS31izrnbvz!ALZLdi@MSYfAPVPAUt09;H&i74zP2#&8u*g{)g=tm)7Zef
zVx?f;B9&|iX|L~<qfT(-F2pg1Yi7_b0OK(#cX#|PF(&+Ch#>Stv?@3-C7vCUm6mY1
z^Cs%NHkB$Ows@rN=j{;KS#$IB>C+e2>+9+`g6N2W139hxzxN)cfCBh7!;a!!Gmb6H
z&0R1c_8@on7jzk-2Yx6^QvJ9Els@8^tZA+GsH8+Rv}$KQX6$s0s%M$gZ9^TOhvY;;
zG$f|Uj*3HO&^;$wvP{CNPl*fLXICJu1C`Ydt=v(WINel%WkfQGWO;V5ClcW5kCeZL
z1gwN(v`fRn!Va6SUt=yxdOA9mwy8KcpboUS;PzDrMbI;D6|rzeTb4K^N;rlR${&=a
zjqv}mv$OO<?ohOdJ49zYrnI7=fhXgw@0|jg>b2y`0>bpEEv>!{r1jhK^75snCA~qc
z7ZvMH@~Thh3FHSiE?ugH85tQZr;~qp#$W2WOo>Xhf8$%|2vr(0vPF(N2M0I74|Mt<
zkVP)`2oaKYg`<?~?V0!q+O|@h6`Q_W9DxvoeIO^I@NL^0&&tckS4KEzRSO5Q6Bb!N
z)YV;lb=R>3!O1d$g4(5;ed|j>CKsJXp;oNVqMxmJeAW)$UhH09cW*|O$9ThI&Qg3|
z->Q5Q(n`E^_z;`j`WPv~Y0^W0UPArtK1cr6rry~o1yBQ0|9`L6FLawWNoL2ywT@R&
zpwX)~o%9f5nkg@(C9dZ44X(=L+DpB`&pn~o_%Z!;zv0J`+z=rx70Xx`s!$?)Z%00T
xpdsZ5E2cI1SDyagsOS3s<GsMVEGV47F)%8S1qq0%f&Y8x80wnnRBJiK{U0fbGQ$7>

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_media_story_empty@3x.png b/Telegram/Resources/icons/info/info_media_story_empty@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6422809dcbdc0a470834b91e6923a3f08542494
GIT binary patch
literal 5763
zcmcIohgTC#v`<2)h8Cns@4d(;HFS_JQl$t46cD7VbPz%)Qbegykfwk@1VkxP6M|Bu
zOEVxr=`GT$zV*EG{)BhV?#}G)Jv+OzcYftQxp~8oo{ozS1Om|;8|hntKwy5LkE4MA
zN2xIh63~DjSQ+YqYKFMifD?TWdt*;?bC4v^rvZVJyg|@^A;8WB>>v<DF&IPvwBUc&
ziXs1Z7tCKw@qc{~?qA@25X)B(h;_?YU&kg4y!9&5m-RDmj{zF8#Tu^fEi=uZZKd-r
zmOdH5BEg79hgR)cX$zVZ!$scnJMwoVGyjsbebV?IVIbJBWVs}R5-|~S<fGKF!m+KF
zjX;8Tj)FHmU(6I9ZEtR$6l%@RXnauYJQ+M6+WzCYeAG&!iZP0X{(o=#P_9)*jGYm`
zq+}JL!6KDS=8s6UtaFQvjZOBeSJ4OSwh6lR>)Nz8@emQV!t;^hv+Y(n35iPUIWYl&
z5;sEPyy@`3z!j~Swj;!OE>tjEd%WD7nt>aWlhY9JqhlaX?yvG&#)+%Czg0iXD63lg
ziW$Gk%)IJ5T?eM1+TWaE8IBW3nSXz$XK`_Hfez0DH;MUkezG9DVk?Xkf&>Kx4Qm_`
zNra^M7E4Z7G`l>)l4`W~dOBD8>3X%Tt*!V`o@>kt<$!s=ra3l!)iyNrGRwV5tV2XZ
zgqs`b$K$_#SP_8&=eDq*z^{>eUzHPl+^*rTICsWeproGNIx&vD&Y-1pGBnxf_x;m@
zl^Ym&Fcs{_r;TAtOUvJUYt}9<9e&@HIN%$dHS+;n%Km9Ry2NhS18&veUq7ZaZ>kbt
z2xCYJEiruMmDH`8ZV>A>T$8c7x|)$k4XxgXqP&FUMT3PzMMXE$pJZelN%Z04sq>Ws
zcn_-__-WDl{Hm&|Vqz=(IZ~R2b-7^WEEEbt+-P~U5%M!#Ink#52YV4B+=`{RqkE&a
zb2~JA&<G;Rg8TUG<+c0BM{fh#=vLfb4i64)&b5X{XZV!&tNTod7nM@^75rx#%*Go8
zC7V!45fd)4Ls_pPq@|>snga!VH~JOI%FF$Kbj;l}8t!|g9xBP63DH*rn|a|S7d$W#
z9Vi{{ftRQIYvaXn?Sn>jyK>GgAM5L_{JSP9t%ce{XJQhdvmd>)^7ExKOi8f$5YOR4
z9v+@|H*Jb+@3e%Ey)iyJ+Bu@h2<@G6iTt`^JE#ruWad!zCj#>eb%rx!@M@^t`I0s=
z<%9V~^*Y!Mx%s2>v9B+7G!J12S!TI6_NFg|Rq9`)si~=$MIp93!c7x2sSPP-jtzqB
z*`U<c7!?Exo<9#7H@`jR?Z{V$Y=U#Bhitk&z9_pTCeZFbr-pF<)Nta<PKz-BRS)IK
zxyruH`opy0F+MgnSXH=`8JkLr>uU5K7j0Kt8^_c**Waae8N6+V^BS*P;p&u|jwX2B
zDZ*k%YLF&k@IZ}o>poYRMUiH7#7MEulrvK}b%y4txLss;U3PXh@xidh^PX54h7=I?
z8<219F0c2!r5`q59Da|NoYr7}v`uudA3t9ydORPw)4uI3;N%VmbHLtXjcY9(xiS)f
z1-H8<PY}lY+DhkI``CYkJ{q)(zH8{#@^CG9eL}*odzD4Lo00Qs_fy^KXS6#-5h{)^
z)yhkixvjifL$+=dsFb9$MlTM&Py~`7)JPr6#4@0{AGPnjj$!3lXjXG<@Ui`|=qw#=
zVrKU6cIqJ8@mAB1$&DOMdY=0~I@)I-bx}P`v43H{<714>GW2I0=3I$9Ps~gCVvb9w
zOpK7bb7aZFG!+PjFw++PL0%J)6nVIbx3TDE{@sdqc?eVFjA19F)ri*le3R7MIz9Xz
zpttY%)5rJke>Ivsgq9Vfdz#-@<8i?I(KlSWU%V<mLfzU@OaEx|WTmL~tnjZ;4yf(x
z#N=c@9o5u5G9zoH4a!^jp)Y^|yvhsH4YWI{DlBF?ZC|cI{X0%PiRE{<y1K$!q8V%Q
zNInd!j*Y&Y#HF@=Tc};Fi_Cp?|9dn83soWY3c^T@xV7<$f!PGb51wD^T{tF<AT@#`
z4fm%~+2#7_8iRgbztxbHE&$%3#YNA~w?}$xsfBEERJ37t7$#ShAb}=A_t^(sr?0Ix
zme*?tBNf2J*LA+P?BrFFo9y9sh7eid*i}S*$kz0Gg1>*Lb8?}1utprOMnvy>GC43=
z16dQGe#Ho~499h$D5x2Fn`)Kk!wAF_2Q^xKNZjaa!-3g@9=^wcssY0Ln&0oKf%Q7u
z$l^W`pRwA4fjvB-<WTd{#a}FXNenpwuE`HKe<`V2g-GiW4g)P*qjvkm$muJ+o;UBs
zfey`qnafXlmY!4&$5Bzbfxn&}Y?!}$p5UZY?AjSqAL2Jt|2d__A?c~Mon0=GSwcdh
zMcPd58E*2O(^AaV_oiBk{6*jAw`y)ngm$;?bZl(e88q)tTS<hjZ_l*~ai?Z3YyIwK
z33kPO7pCVmX%Rx?$T~HeZBGD5mQ?XD7fu7l{0F1>qIED~9e0_~)+i8blodE4CT<@T
z^xhAerlJFzYl}Xb3uUJ6;-TkNa7PzAs}x-INKtoO>9%`QUf!>PQY7{-bkDbeJLD3!
z(Jz|A_m-dfra7UN3Vyq`Jgjt@Wmnk3u{<_6PcKH23Cs#0-`NrG@p9T%C<6TzrD+=?
z`N@)!m=gdhPf}9g$5H#EhFzSzj%NJI%14_sA9Tx-zlau)O6F*z4!+&$k`pOVRUE72
zX`!#hpVpjQ9Q{~Z`^s<ukA;nnj_RHp?5{cnw6kh65BiA40lf40_h<IN8y)VS=^}0_
zWqnlj5Q?q;qdb<?m{CbbO55ArWm>CkX*tag=)l9UlulJSHa<6w>TSM3e;ucir|xI1
z5P;u&yQ6h^WkuP0tQ+BLB6+P=RoVeaC^ob@2jsFu?waF4qW>jC>B}Zb3*)koVslP2
z0Z_$WvKGJibb;yvvr1{4ZfZ&j6#r4Lw;((FGBP~I<CayWYo<aL3oYWDOqhN94IYxb
zB9Fv@%y|h=m%%QE+s0*X0hE_MGJ}ubGbaXkc$D#!uJOOk;Ku~o+uDAsb0d&TZLGY*
z@)w?t5fw*a4_=IjkfXu?x+lFkNb2kBo0s}b@SFarBUL#OsVKDPbR=%??p`{Q5I8Mb
zU>UqRe6_c(weqG_CA>|kc0z;>pxh6~$6;{?7Frj=KtQsXcF{qhdgzc>VeD1$%K!Lj
zg@iwtUWPTvg^x{&(j5{QwZiz!FxdF&>pS6)3p?YoI66++6X#OoE44@Ph)V&kG^|nu
zHo?aLOhneQS0^nHzeg5ociLKc7wcZ_yhC%>)03|JQ+@r_Go#wOCML#Zhi*5FyNp4t
z0#HgRad8?i%95P2yOs%=CVD@+tG7(F4;6fx4E9ZODJ3f>YVM$e6SNn|BtjR@W&*nl
zO$owU*0#RR*|;obF*P-H<9{is=;VbtC%7$$mhNzW+(IfVk@ZY4)_H|5iX2xlfLw@s
zz%WCzSq+N`5CG3ueD8k3B7-fmrM$jSmgfSGVe%R(;IwBR^b?7jopmfBTz|^#T?WER
z@k2WQoK}yx0(A9ATSP_V!<A(`m+Ki#H>toug`juFq>Yu=!Gvth(s~htR;efz_k(Xx
ze%J%$gfcE9dV0mh=YFN9V`E_vYAT`=ReayasTY~ZbEtvA_t4XM_I|Eor+={f<}(zg
zyk?1)-cA5jDYiLq&QC(bvmMNZv?&$o->Fg-X+3t4ohE-a8~UO+$x@7R7)HF|A~DeO
zmC#6HK;4>2iH9&v!P$y<nDFx=cqNhwvl&F!nYgsHgpyX8aS}FSJuNX!<F=_ANkZj_
z-1YLh^i9C1C@?WeNibH0r>j2}Si&<#nkm5=V5|W;G9rV|6pH!U+uQsnl6NKz1Sz;g
z=h?I+sYKy<DQMvZb?OCkJ%McuBZM~PWzOJH7bQP`MMJ|G_O<|o*yFcS)O8cj>qdZY
zmm=#QUy;Ow$0v%MS>PJz5<kB#r;`26r|{tV)wq1(XxIru;>;iej~c+sDlKvLms7PL
z+2^3xS%+PHExHw1mnVO-|Ct{uQ2n_|)#4JCCvNZUEs@6Eb*hIDQM-XBzaFV|?F8sv
za1_v^Xfzs-nc<uXHx&o@Nqc+4T0+6Xf~)R3m3imqDNvMfET$7BFe;H7(JXSq!s74o
z&x{N{E!qDBM)a^rxHL-Xx5+hv=S1j}oV@3;N&nzBfpol9Q8dea!^cMwLdIi6jd*kQ
zjwN1bJ??007+p3KFEpo}R*HHqAQxucC3;dPo_VB89-^vv4xqI1<#;h;oGXNoNS~GT
zk_SSjr8hD*mXMatHn?^G%l#|k)CfC->u~U9#&$wCxpgPUsnB;GpDg7qsWg>W<#v!$
zQs_=0yP=2#3D<b}oI9!RhdY2;xj5P|=o=p9Q+xRKPg>P~SX7t|OZT2248ny!$Xuat
zLSPe)ku$vIe8~EvD5l1pq5FIY@Tt}sbrO15^9)N8Rg>E#OTnAn`Z7gM`F!6fSfkGp
zga3=CqVI}$tfO>724p0laV<1Y?iLT+9JxMix;*r%708a4FS$P5B@Je1{W1EPv};L1
zfO#WU1>A&5bzZ}TK2BFdHtpLWeJF1)@>U}Kl|)9q?dbl2VLN{NW#EZNTi+{k0x-C2
z?UkWd7t&cXMhkzT<ON|ykA{Ku3iEs_^+FO;k8xW@``<rjqMx8q3vtv;Wi1_5LA(})
zfJ|B$5r_lPASL4E-;jxA>k>hO7zNA;>#tFUCf^zO>T4v3f4#oEtPId_0gh$5kO56H
z{8ru+@h`?AsmJ%>2*9vOFiv-W59v8!h;_vc)-Oy9Zg}NNHM!4=%b<L;G;C<$nl}K6
zx$i#49-(ck?Kn+PS%y!!e~Y=d<mI03u@=SvD06UigtJ|X=$l6!z`yJCXZ>5X4BO>H
z4Yc!QhL}wHUg(eLrq8>@Ej_%EW-&2`MhVD-`sz8diHg?{cY3T?oGag7nIaNvVp)Pd
za<1i(uB%I1*eHt6yM5&TR1NE0GPk?2?Bve+1IHJ<h%kBIi$oO3c@$w>+`emKne0aJ
z04?hTTJ*{H^xe7FpDmHwz?7kvm`KmjlT%w3M#fYBejv1`qwTeKoW^y{q=wncvB}@h
zrA&H88p81-xLLjpgt0BMku8jpb?uObRrdFr_-Z}hl~hsiC#vWualfoW0_wEB`8uV&
zP?ZU`H7oKmqfxF-Y>Am1XrKPfP`Y}~%OLn%-R#v#n`AT0KC;rVq>L~8*YgFsU$*PC
z4>u;wH;iP!!`8XRlcff!fK7?0`cf(;;%T;%9=()v3(@2-_}Q&$1d&x<0%lR9PGtDl
zJ}?;Ir>{NFn@E~(3UHM&*c>Y}-I;IqyD!yxFzIx*zsADH7r>iUZl2H8v5S9u@88$Z
zL8ilM=UP8PmqXsY&3`n(y!x#=kIK~)`AWgldcB_9d50E|5QZBzGJs}d>{03sM-ggX
zKc9l{wCz3JbJTs}0hxYV`lu>*y3sGcAj@k|D7J!%UK0*v@se)L-aRMh-4eZ|{q_yx
zJK?S0g7pKV-#IqSHV3`-a%Y-9kY{Gc%io^b|EC85G&6zu-wnD&z@{wSS)P%U@n@($
zO9J`g0XO~Z5sWvuHyLWg4}&7VJ!6-3qoYyUm(Ly&1nL7!DJC6H+ypTe(%e^snW!LP
zVBEH|L(f9DAgt^I4V*u&FG~z~Jhj0Ruln>g;RPUqi`W`ki@lRfOghWI;@GS7EY=Z1
z<KW&byZvEU(^`t-z8E-<3D@C#BATd3en!e<tLGFh&IO%6=vA|Ga#9am&~8Az4}k|#
zfSc<Ko7I59C@`{u#1Xu2d@yAC=@X}1>>YqDa#d+<7zCstxQFQWH#84bV$P4hq+dxY
zGh<t%f&})9NpTD<ESD4I(yfmheP@Et4!5|jAK&sFFHg5dR}_@9$P@dYl2ArHx9K*s
zG(+@+?B10-7j^^gH_D|K_FNbA=Oc5fK8IkT-sZ%wUzofgCU*9uvz#=Bo%gq+mlK@2
zqbUQ{4h#eU&5J)wNfuf(jDPPuHev#eCPe(FMTg8a5{%u7P1O3rA#0eyYi?$iCd|?C
zolb~pvZMD%c9=74V9y!)K%Wb&&!`r(h$a=g#qwPsan%xy!dFmTeeu+c?sWP^Mc(k^
zB%E{T+#|rQ5lZ5Fv(p-W;`av?hdBhhjInzkr_Z!{Y6vvAzm^UTFGwKWv|58ywii5)
zyZ^KSo=`(i(>3PA0~q{bsRErs+x_KBf4P^Rkz2ecI$xtM`o`-L5xPs4NX|Av!|x+1
zm}2$J2u75JztPASG+x`;jcDf^2xuWtCa{(fR`Q#@%*x7om||a?PrGo$@$<S?+P*hJ
zR@R}uyK%nm>ox|ca|oVup{Dpp9IjtateS4~RL)w;HwD!?0hOAmjhts+^q=LtcO3MS
zF%<)gKo^QGo3cQ^jqxou5hr?SO&q_P7~|9+@-kefRFf01>rt8#4pV&CzGx`9#bfor
zd|W};1aR%<FVVd(x9@8@=|k+8w5M&mN5`dd3cie0aVmPM3{-t-_+*#HJ4s>$`-b#?
z{hD)NqC%l}2&}W$uU}tCSR3;;vsjp#4h#$suoSwF1g=sLUWu>K*!gIKN^&=xxUo|o
ztE#FtH+{P^K7Nxa1w2wRcjWnTQ2SdW!m|I0D>UAJL4utHHy`Gk*%?Se$(zwLzCZUN
zbtQ1j_{61gD7g1L<w=A`+yKfwI`7cfSI`=vXV0G1g={$vsz-7*oYvO;mR3|`5y*en
zlrYhhcvFryhO*{9FQXBT0h*bW^&mL7=JuyjKJ5H>*p`I2IIczWcpks;Z04OdF?44)
ztb<22_{~gaT@;)q6VxcMn7#o3(I1#cbm98C&)^6Ad>s-GQSn6mXs(kp{pT+Zu5p!z
z?#%neRZ-JUk@BtuHlHiO`8M_gU0<pMxhI=gSz5-i3OA3su{oXL%D78S*;!dBXOV;4
z$eCNww@PFY+KVT3<>loa2!sVhNf4yPx3;!6-7R2vq1YA)eHi?E@pGI~!=sJKWOdmI
zl-KO}c~tjQnIb_u{A#B&4ekK$e=o352)2Eb!OeONsPYucYyv3Qn0J{i9HT81b;qd~
z^y0@P)XK_=EQt2&41--VXhlr9(*`rW`~2{>l@*-VJ-#_N?^&!O4YGn$*X*u`2mWrx
z^kcll6`ob~*B>_Cym|imDZ1e=DVDYhwW2Xh-NcT)wYzpmad@5M%DbHGh6MdsIyQKR
zyi1yGmpwU-v4D=C^OB6`2AL&eV7UAC98w;Z!rGAk_YEl%_ZM8)fWF2?s=4@A05&$b
Kp<knmMEwsXaG);$

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 328cc231a..018d19573 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3818,6 +3818,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_archive_button" = "Stories Archive";
 "lng_stories_recent_button" = "Recent Stories";
 "lng_stories_archive_title" = "Stories Archive";
+"lng_stories_archive_about" = "Only you can see archived stories unless you choose to save them to your profile.";
 "lng_stories_reply_sent" = "Message Sent";
 "lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list.";
 "lng_stories_shown_in_chats" = "Those stories are now shown in your Chats list.";
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index 24a97ae70..a0a8a8aaa 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -345,6 +345,7 @@ Content State::next() {
 			.thumbnail = std::move(userpic),
 			.unread = info.unread,
 			.hidden = info.hidden,
+			.profile = true,
 			.skipSmall = user->isSelf(),
 		});
 	}
@@ -419,7 +420,7 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 					return;
 				}
 				auto resolving = false;
-				auto result = Content();
+				auto result = Content{ .full = true };
 				for (const auto id : ids) {
 					const auto storyId = FullStoryId{ peerId, id };
 					const auto maybe = stories->lookup(storyId);
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index b630d9b64..811fbc033 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -763,9 +763,11 @@ void List::contextMenuEvent(QContextMenuEvent *e) {
 
 	const auto id = item.element.id;
 	const auto hidden = item.element.hidden;
-	_menu->addAction(tr::lng_context_view_profile(tr::now), [=] {
-		_showProfileRequests.fire_copy(id);
-	});
+	if (item.element.profile) {
+		_menu->addAction(tr::lng_context_view_profile(tr::now), [=] {
+			_showProfileRequests.fire_copy(id);
+		});
+	}
 	if (!_content.full || hidden) {
 		_menu->addAction(hidden
 			? tr::lng_stories_show_in_chats(tr::now)
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index f81f90c05..a7826d892 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -36,6 +36,7 @@ struct Element {
 	std::shared_ptr<Thumbnail> thumbnail;
 	bool unread = false;
 	bool hidden = false;
+	bool profile = false;
 	bool skipSmall = false;
 
 	friend inline bool operator==(
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index f2a9482cb..1b9d1fa4a 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -576,6 +576,7 @@ infoEmptyAudio: icon {{ "info/info_media_audio_empty", infoEmptyFg }};
 infoEmptyFile: icon {{ "info/info_media_file_empty", infoEmptyFg }};
 infoEmptyVoice: icon {{ "info/info_media_voice_empty", infoEmptyFg }};
 infoEmptyLink: icon {{ "info/info_media_link_empty", infoEmptyFg }};
+infoEmptyStories: icon {{ "info/info_media_story_empty", infoEmptyFg }};
 infoEmptyIconTop: 120px;
 infoEmptyLabelTop: 40px;
 infoEmptyLabelSkip: 20px;
@@ -584,6 +585,14 @@ infoEmptyLabel: FlatLabel(defaultFlatLabel) {
 	textFg: windowSubTextFg;
 }
 
+infoStoriesAboutArchive: FlatLabel(defaultFlatLabel) {
+	minWidth: 245px;
+	align: align(top);
+	textFg: windowSubTextFg;
+	style: defaultTextStyle;
+}
+infoStoriesAboutArchivePadding: margins(22px, 8px, 22px, 16px);
+
 editPeerBottomButtonsLayoutMargins: margins(0px, 7px, 0px, 0px);
 
 editPeerTopButtonsLayoutSkip: 13px;
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index 068da1c33..4f7af1f68 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -59,7 +59,7 @@ void EmptyWidget::setFullHeight(rpl::producer<int> 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 iconHeight = st::infoEmptyStories.height();
 		auto iconTop = iconCenter - iconHeight / 2;
 		_height = iconTop + st::infoEmptyIconTop;
 		resizeToWidth(width());
@@ -81,9 +81,9 @@ int EmptyWidget::resizeGetHeight(int newWidth) {
 void EmptyWidget::paintEvent(QPaintEvent *e) {
 	auto p = QPainter(this);
 
-	const auto iconLeft = (width() - st::infoEmptyFile.width()) / 2;
+	const auto iconLeft = (width() - st::infoEmptyStories.width()) / 2;
 	const auto iconTop = height() - st::infoEmptyIconTop;
-	st::infoEmptyFile.paint(p, iconLeft, iconTop, width());
+	st::infoEmptyStories.paint(p, iconLeft, iconTop, width());
 }
 
 InnerWidget::InnerWidget(
@@ -99,28 +99,31 @@ InnerWidget::InnerWidget(
 	_list = setupList();
 }
 
-void InnerWidget::setupArchive() {
+void InnerWidget::setupTop() {
 	const auto key = _controller->key();
 	const auto peer = key.storiesPeer();
 	if (peer
 		&& peer->isSelf()
 		&& key.storiesTab() == Stories::Tab::Saved
 		&& _isStackBottom) {
-		createArchiveButton();
+		createButtons();
+	} else if (key.storiesTab() == Stories::Tab::Archive) {
+		createAboutArchive();
 	} else {
-		_buttons.destroy();
+		_top.destroy();
 		refreshHeight();
 	}
 }
 
-void InnerWidget::createArchiveButton() {
-	_buttons.create(this);
-	_buttons->show();
+void InnerWidget::createButtons() {
+	_top.create(this);
+	_top->show();
+	_topHeight = _top->heightValue();
 
 	const auto stories = &_controller->session().data().stories();
 	const auto self = _controller->session().user();
 	const auto archive = ::Settings::AddButton(
-		_buttons,
+		_top,
 		tr::lng_stories_archive_button(),
 		st::infoSharedMediaButton);
 	archive->addClickHandler([=] {
@@ -144,11 +147,11 @@ void InnerWidget::createArchiveButton() {
 		st::infoIconMediaStoriesArchive,
 		st::infoSharedMediaButtonIconPosition)->show();
 
-	const auto recentWrap = _buttons->add(
+	const auto recentWrap = _top->add(
 		object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
-			_buttons,
+			_top,
 			::Settings::CreateButton(
-				_buttons,
+				_top,
 				tr::lng_stories_recent_button(),
 				st::infoSharedMediaButton)));
 
@@ -204,16 +207,36 @@ void InnerWidget::createArchiveButton() {
 		return !content.elements.empty();
 	}));
 
-	_buttons->add(object_ptr<Ui::FixedHeightWidget>(
-		_buttons,
+	_top->add(object_ptr<Ui::FixedHeightWidget>(
+		_top,
 		st::infoProfileSkip));
-	_buttons->add(object_ptr<Ui::BoxContentDivider>(_buttons));
+	_top->add(object_ptr<Ui::BoxContentDivider>(_top));
 
-	_buttons->resizeToWidth(width());
-	_buttons->heightValue(
+	_top->resizeToWidth(width());
+	_top->heightValue(
 	) | rpl::start_with_next([=] {
 		refreshHeight();
-	}, _buttons->lifetime());
+	}, _top->lifetime());
+}
+
+void InnerWidget::createAboutArchive() {
+	_top.create(this);
+	_top->show();
+	_topHeight = _top->heightValue();
+
+	_top->add(object_ptr<Ui::DividerLabel>(
+		_top,
+		object_ptr<Ui::FlatLabel>(
+			_top,
+			tr::lng_stories_archive_about(),
+			st::infoStoriesAboutArchive),
+		st::infoStoriesAboutArchivePadding));
+
+	_top->resizeToWidth(width());
+	_top->heightValue(
+	) | rpl::start_with_next([=] {
+		refreshHeight();
+	}, _top->lifetime());
 }
 
 void InnerWidget::visibleTopBottomUpdated(
@@ -277,8 +300,8 @@ int InnerWidget::resizeGetHeight(int newWidth) {
 	_inResize = true;
 	auto guard = gsl::finally([this] { _inResize = false; });
 
-	if (_buttons) {
-		_buttons->resizeToWidth(newWidth);
+	if (_top) {
+		_top->resizeToWidth(newWidth);
 	}
 	_list->resizeToWidth(newWidth);
 	_empty->resizeToWidth(newWidth);
@@ -294,9 +317,9 @@ void InnerWidget::refreshHeight() {
 
 int InnerWidget::recountHeight() {
 	auto top = 0;
-	if (_buttons) {
-		_buttons->moveToLeft(0, top);
-		top += _buttons->heightNoMargins() - st::lineWidth;
+	if (_top) {
+		_top->moveToLeft(0, top);
+		top += _top->heightNoMargins() - st::lineWidth;
 	}
 	auto listHeight = 0;
 	if (_list) {
@@ -321,7 +344,8 @@ void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
 		_listTops.events_starting_with(
 			_list->topValue()
 		) | rpl::flatten_latest(),
-		_1 - _2));
+		_topHeight.value(),
+		_1 - _2 + _3));
 }
 
 rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
index 92814b72e..3e6819af9 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h
@@ -40,7 +40,7 @@ public:
 	bool showInternal(not_null<Memento*> memento);
 	void setIsStackBottom(bool isStackBottom) {
 		_isStackBottom = isStackBottom;
-		setupArchive();
+		setupTop();
 	}
 
 	void saveState(not_null<Memento*> memento);
@@ -64,14 +64,15 @@ private:
 	int recountHeight();
 	void refreshHeight();
 
-	void setupArchive();
-	void createArchiveButton();
+	void setupTop();
+	void createButtons();
+	void createAboutArchive();
 
 	object_ptr<Media::ListWidget> setupList();
 
 	const not_null<Controller*> _controller;
 
-	object_ptr<Ui::VerticalLayout> _buttons = { nullptr };
+	object_ptr<Ui::VerticalLayout> _top = { nullptr };
 	object_ptr<Media::ListWidget> _list = { nullptr };
 	object_ptr<EmptyWidget> _empty;
 
@@ -81,6 +82,7 @@ private:
 	rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
 	rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
 	rpl::event_stream<rpl::producer<int>> _listTops;
+	rpl::variable<int> _topHeight;
 
 };
 

From 8d2fd4bd69e8782a283115eb401e0a0e3076d916 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 21 Jun 2023 10:12:28 +0400
Subject: [PATCH 092/259] Show only hidden stories in Contacts.

---
 .../boxes/peer_list_controllers.cpp           |   6 +-
 Telegram/SourceFiles/data/data_stories.cpp    | 102 +++++++++---------
 Telegram/SourceFiles/data/data_stories.h      |   2 +-
 .../dialogs/ui/dialogs_stories_content.cpp    |   9 +-
 .../dialogs/ui/dialogs_stories_list.cpp       |  18 ++--
 .../dialogs/ui/dialogs_stories_list.h         |  11 +-
 Telegram/SourceFiles/mtproto/scheme/api.tl    |   4 +-
 7 files changed, 79 insertions(+), 73 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 834592de6..1b62876ed 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -85,7 +85,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 			st::dialogsStoriesList,
 			Stories::ContentForSession(
 				&sessionController->session(),
-				Data::StorySourcesList::All),
+				Data::StorySourcesList::Hidden),
 			[=] { return state->stories->height() - box->scrollTop(); });
 		const auto raw = state->stories = stories.data();
 		box->peerListSetAboveWidget(object_ptr<::Ui::PaddingWrap<>>(
@@ -97,7 +97,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 		) | rpl::start_with_next([=](uint64 id) {
 			sessionController->openPeerStories(
 				PeerId(int64(id)),
-				Data::StorySourcesList::All);
+				Data::StorySourcesList::Hidden);
 		}, raw->lifetime());
 
 		raw->showProfileRequests(
@@ -116,7 +116,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 		raw->loadMoreRequests(
 		) | rpl::start_with_next([=] {
 			sessionController->session().data().stories().loadMore(
-				Data::StorySourcesList::All);
+				Data::StorySourcesList::Hidden);
 		}, raw->lifetime());
 	};
 	return Box<PeerListBox>(std::move(controller), std::move(init));
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index f8a51b93e..525a0d200 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -419,8 +419,9 @@ void Stories::apply(const MTPDupdateStory &data) {
 			sort(list);
 		}
 	};
-	refreshInList(StorySourcesList::All);
-	if (!user->hasStoriesHidden()) {
+	if (user->hasStoriesHidden()) {
+		refreshInList(StorySourcesList::Hidden);
+	} else {
 		refreshInList(StorySourcesList::NotHidden);
 	}
 	_sourceChanged.fire_copy(peerId);
@@ -428,7 +429,8 @@ void Stories::apply(const MTPDupdateStory &data) {
 
 void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {
 	if (!data) {
-		applyDeletedFromSources(peer->id, StorySourcesList::All);
+		applyDeletedFromSources(peer->id, StorySourcesList::NotHidden);
+		applyDeletedFromSources(peer->id, StorySourcesList::Hidden);
 		_all.erase(peer->id);
 		_sourceChanged.fire_copy(peer->id);
 	} else {
@@ -449,7 +451,8 @@ void Stories::requestUserStories(not_null<UserData*> user) {
 		parseAndApply(data.vstories());
 	}).fail([=] {
 		_requestingUserStories.remove(user);
-		applyDeletedFromSources(user->id, StorySourcesList::All);
+		applyDeletedFromSources(user->id, StorySourcesList::NotHidden);
+		applyDeletedFromSources(user->id, StorySourcesList::Hidden);
 	}).send();
 }
 
@@ -528,7 +531,8 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 		}
 	}
 	if (result.ids.empty()) {
-		applyDeletedFromSources(peerId, StorySourcesList::All);
+		applyDeletedFromSources(peerId, StorySourcesList::NotHidden);
+		applyDeletedFromSources(peerId, StorySourcesList::Hidden);
 		return;
 	} else if (user->isSelf()) {
 		result.readTill = result.ids.back().id;
@@ -558,14 +562,15 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 		sort(list);
 	};
 	if (result.user->isContact()) {
-		add(StorySourcesList::All);
-		if (result.user->hasStoriesHidden()) {
-			applyDeletedFromSources(peerId, StorySourcesList::NotHidden);
-		} else {
-			add(StorySourcesList::NotHidden);
-		}
+		const auto hidden = result.user->hasStoriesHidden();
+		using List = StorySourcesList;
+		add(hidden ? List::Hidden : List::NotHidden);
+		applyDeletedFromSources(
+			peerId,
+			hidden ? List::NotHidden : List::Hidden);
 	} else {
-		applyDeletedFromSources(peerId, StorySourcesList::All);
+		applyDeletedFromSources(peerId, StorySourcesList::NotHidden);
+		applyDeletedFromSources(peerId, StorySourcesList::Hidden);
 	}
 	_sourceChanged.fire_copy(peerId);
 }
@@ -725,11 +730,11 @@ void Stories::loadMore(StorySourcesList list) {
 	if (_loadMoreRequestId[index] || _sourcesLoaded[index]) {
 		return;
 	}
-	const auto all = (list == StorySourcesList::All);
+	const auto hidden = (list == StorySourcesList::Hidden);
 	const auto api = &_owner->session().api();
 	using Flag = MTPstories_GetAllStories::Flag;
 	_loadMoreRequestId[index] = api->request(MTPstories_GetAllStories(
-		MTP_flags((all ? Flag::f_include_hidden : Flag())
+		MTP_flags((hidden ? Flag::f_hidden : Flag())
 			| (_sourcesStates[index].isEmpty()
 				? Flag(0)
 				: (Flag::f_next | Flag::f_state))),
@@ -917,7 +922,7 @@ void Stories::applyRemovedFromActive(FullStoryId id) {
 			if (i->second.ids.empty()) {
 				_all.erase(i);
 				removeFromList(StorySourcesList::NotHidden);
-				removeFromList(StorySourcesList::All);
+				removeFromList(StorySourcesList::Hidden);
 			}
 			_sourceChanged.fire_copy(id.peer);
 		}
@@ -925,21 +930,15 @@ void Stories::applyRemovedFromActive(FullStoryId id) {
 }
 
 void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) {
-	const auto removeFromList = [&](StorySourcesList from) {
-		auto &sources = _sources[static_cast<int>(from)];
-		const auto i = ranges::find(
-			sources,
-			id,
-			&StoriesSourceInfo::id);
-		if (i != end(sources)) {
-			sources.erase(i);
-		}
-		_sourcesChanged[static_cast<int>(from)].fire({});
-	};
-	removeFromList(StorySourcesList::NotHidden);
-	if (list == StorySourcesList::All) {
-		removeFromList(StorySourcesList::All);
+	auto &sources = _sources[static_cast<int>(list)];
+	const auto i = ranges::find(
+		sources,
+		id,
+		&StoriesSourceInfo::id);
+	if (i != end(sources)) {
+		sources.erase(i);
 	}
+	_sourcesChanged[static_cast<int>(list)].fire({});
 }
 
 void Stories::removeDependencyStory(not_null<Story*> story) {
@@ -1146,8 +1145,8 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 				sort(list);
 			}
 		};
-		refreshInList(StorySourcesList::All);
 		refreshInList(StorySourcesList::NotHidden);
+		refreshInList(StorySourcesList::Hidden);
 	}
 	_markReadTimer.callOnce(kMarkAsReadDelay);
 }
@@ -1181,37 +1180,36 @@ void Stories::toggleHidden(
 		return;
 	}
 	i->second.hidden = hidden;
+	const auto info = i->second.info();
 	const auto main = static_cast<int>(StorySourcesList::NotHidden);
-	const auto all = static_cast<int>(StorySourcesList::All);
+	const auto other = static_cast<int>(StorySourcesList::Hidden);
+	const auto proj = &StoriesSourceInfo::id;
 	if (hidden) {
-		const auto i = ranges::find(
-			_sources[main],
-			peerId,
-			&StoriesSourceInfo::id);
+		const auto i = ranges::find(_sources[main], peerId, proj);
 		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({});
+		const auto j = ranges::find(_sources[other], peerId, proj);
+		if (j == end(_sources[other])) {
+			_sources[other].push_back(info);
+		} else {
+			*j = info;
 		}
+		sort(StorySourcesList::Hidden);
 	} 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);
-			}
+		const auto i = ranges::find(_sources[other], peerId, proj);
+		if (i != end(_sources[other])) {
+			_sources[other].erase(i);
+			_sourcesChanged[other].fire({});
 		}
+		const auto j = ranges::find(_sources[main], peerId, proj);
+		if (j == end(_sources[main])) {
+			_sources[main].push_back(info);
+		} else {
+			*j = info;
+		}
+		sort(StorySourcesList::NotHidden);
 	}
 }
 
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index c136bff5c..d082d05a6 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -172,7 +172,7 @@ enum class NoStory : uchar {
 
 enum class StorySourcesList : uchar {
 	NotHidden,
-	All,
+	Hidden,
 };
 
 struct StoriesContextSingle {
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index a0a8a8aaa..acce7637e 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -322,7 +322,9 @@ State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
 }
 
 Content State::next() {
-	auto result = Content{ .full = (_list == Data::StorySourcesList::All) };
+	auto result = Content{
+		.hidden = (_list == Data::StorySourcesList::Hidden)
+	};
 	const auto &sources = _data->sources(_list);
 	result.elements.reserve(sources.size());
 	for (const auto &info : sources) {
@@ -344,7 +346,8 @@ Content State::next() {
 				: user->shortName()),
 			.thumbnail = std::move(userpic),
 			.unread = info.unread,
-			.hidden = info.hidden,
+			.suggestHide = !info.hidden,
+			.suggestUnhide = info.hidden,
 			.profile = true,
 			.skipSmall = user->isSelf(),
 		});
@@ -420,7 +423,7 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 					return;
 				}
 				auto resolving = false;
-				auto result = Content{ .full = true };
+				auto result = Content{};
 				for (const auto id : ids) {
 					const auto storyId = FullStoryId{ peerId, id };
 					const auto maybe = stories->lookup(storyId);
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 811fbc033..c54803ef6 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -102,7 +102,9 @@ void List::showContent(Content &&content) {
 				item.nameCache = QImage();
 			}
 			item.element.unread = element.unread;
-			item.element.hidden = element.hidden;
+			item.element.suggestHide = element.suggestHide;
+			item.element.suggestUnhide = element.suggestUnhide;
+			item.element.profile = element.profile;
 		} else {
 			_data.items.emplace_back(Item{ .element = element });
 		}
@@ -762,17 +764,19 @@ void List::contextMenuEvent(QContextMenuEvent *e) {
 	_menu = base::make_unique_q<Ui::PopupMenu>(this);
 
 	const auto id = item.element.id;
-	const auto hidden = item.element.hidden;
 	if (item.element.profile) {
 		_menu->addAction(tr::lng_context_view_profile(tr::now), [=] {
 			_showProfileRequests.fire_copy(id);
 		});
 	}
-	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 }); });
+	if (item.element.suggestHide) {
+		_menu->addAction(
+			tr::lng_stories_hide_to_contacts(tr::now),
+			[=] { _toggleShown.fire({ .id = id, .shown = false }); });
+	} else if (item.element.suggestUnhide) {
+		_menu->addAction(
+			tr::lng_stories_show_in_chats(tr::now),
+			[=] { _toggleShown.fire({ .id = id, .shown = true }); });
 	}
 	const auto updateAfterMenuDestroyed = [=] {
 		const auto globalPosition = QCursor::pos();
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index a7826d892..8381a4289 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -34,10 +34,11 @@ struct Element {
 	uint64 id = 0;
 	QString name;
 	std::shared_ptr<Thumbnail> thumbnail;
-	bool unread = false;
-	bool hidden = false;
-	bool profile = false;
-	bool skipSmall = false;
+	bool unread : 1 = false;
+	bool suggestHide : 1 = false;
+	bool suggestUnhide : 1 = false;
+	bool profile : 1 = false;
+	bool skipSmall : 1 = false;
 
 	friend inline bool operator==(
 		const Element &a,
@@ -46,7 +47,7 @@ struct Element {
 
 struct Content {
 	std::vector<Element> elements;
-	bool full = false;
+	bool hidden = false;
 
 	friend inline bool operator==(
 		const Content &a,
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index e3bcb489f..1ccafbc6f 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -129,7 +129,7 @@ messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true tes
 messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
 messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
 messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
-messageMediaStory#cbb20d88 flags:# user_id:long id:int story:flags.0?StoryItem = MessageMedia;
+messageMediaStory#cbb20d88 flags:# via_mention:flags.1?true user_id:long id:int story:flags.0?StoryItem = MessageMedia;
 
 messageActionEmpty#b6aef7b0 = MessageAction;
 messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;
@@ -2091,7 +2091,7 @@ stories.sendStory#424cd47a 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<MessageEntity> privacy_rules:flags.2?Vector<InputPrivacyRule> = Updates;
 stories.deleteStories#b5d501d7 id:Vector<int> = Vector<int>;
 stories.togglePinned#51602944 id:Vector<int> pinned:Bool = Vector<int>;
-stories.getAllStories#eeb0d625 flags:# next:flags.1?true include_hidden:flags.2?true state:flags.0?string = stories.AllStories;
+stories.getAllStories#eeb0d625 flags:# next:flags.1?true 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.getStoriesArchive#1f5bc5d2 offset_id:int limit:int = stories.Stories;

From eb260b91c1943f59423dbc8fe92b782a14893fe3 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 21 Jun 2023 12:29:04 +0400
Subject: [PATCH 093/259] Improve unsupported stories design.

---
 .../history_view_compose_controls.cpp         | 28 +++---
 Telegram/SourceFiles/info/info.style          |  5 +-
 .../info/media/info_media_list_section.cpp    | 11 ++-
 .../stories/media_stories_controller.cpp      | 90 ++++++++++++++++---
 .../media/stories/media_stories_header.cpp    |  6 ++
 .../media/stories/media_stories_header.h      |  1 +
 .../media/stories/media_stories_reply.cpp     |  1 +
 .../media/stories/media_stories_slider.cpp    |  6 ++
 .../media/stories/media_stories_slider.h      |  1 +
 .../SourceFiles/media/view/media_view.style   | 15 +++-
 10 files changed, 126 insertions(+), 38 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 2b5523b28..235091a00 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -2791,24 +2791,24 @@ void ComposeControls::replyToMessage(FullMsgId id) {
 }
 
 void ComposeControls::cancelReplyMessage() {
-	Expects(_history != nullptr);
-
 	const auto wasReply = replyingToMessage();
 	_header->replyToMessage({});
-	const auto key = draftKey(DraftType::Normal);
-	if (const auto localDraft = _history->draft(key)) {
-		if (localDraft->msgId) {
-			if (localDraft->textWithTags.text.isEmpty()) {
-				_history->clearDraft(key);
-			} else {
-				localDraft->msgId = 0;
+	if (_history) {
+		const auto key = draftKey(DraftType::Normal);
+		if (const auto localDraft = _history->draft(key)) {
+			if (localDraft->msgId) {
+				if (localDraft->textWithTags.text.isEmpty()) {
+					_history->clearDraft(key);
+				} else {
+					localDraft->msgId = 0;
+				}
 			}
 		}
-	}
-	if (wasReply) {
-		_saveDraftText = true;
-		_saveDraftStart = crl::now();
-		saveDraft();
+		if (wasReply) {
+			_saveDraftText = true;
+			_saveDraftStart = crl::now();
+			saveDraft();
+		}
 	}
 }
 
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index 1b9d1fa4a..fcd649b38 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -496,7 +496,8 @@ infoMediaHeaderStyle: TextStyle(semiboldTextStyle) {
 }
 infoMediaHeaderHeight: 28px;
 infoMediaHeaderPosition: point(14px, 6px);
-infoMediaSkip: 5px;
+infoMediaSkip: 2px;
+infoMediaLeft: 3px;
 infoMediaMargin: margins(0px, 6px, 0px, 2px);
 infoMediaMinGridSize: 90px;
 
@@ -591,7 +592,7 @@ infoStoriesAboutArchive: FlatLabel(defaultFlatLabel) {
 	textFg: windowSubTextFg;
 	style: defaultTextStyle;
 }
-infoStoriesAboutArchivePadding: margins(22px, 8px, 22px, 16px);
+infoStoriesAboutArchivePadding: margins(22px, 12px, 22px, 12px);
 
 editPeerBottomButtonsLayoutMargins: margins(0px, 7px, 0px, 0px);
 
diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp
index 9fabb28ad..ed70623ce 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp
@@ -340,12 +340,15 @@ void ListSection::resizeToWidth(int newWidth) {
 	case Type::Video:
 	case Type::PhotoVideo: // #TODO stories
 	case Type::RoundFile: {
-		_itemsLeft = st::infoMediaSkip;
+		const auto skip = st::infoMediaSkip;
+		_itemsLeft = st::infoMediaLeft;
 		_itemsTop = st::infoMediaSkip;
-		_itemsInRow = (newWidth - _itemsLeft)
-			/ (st::infoMediaMinGridSize + st::infoMediaSkip);
-		_itemWidth = ((newWidth - _itemsLeft) / _itemsInRow)
+		_itemsInRow = (newWidth - _itemsLeft * 2 + skip)
+			/ (st::infoMediaMinGridSize + skip);
+		_itemWidth = ((newWidth - _itemsLeft * 2 + skip) / _itemsInRow)
 			- st::infoMediaSkip;
+		_itemsLeft = (newWidth - (_itemWidth + skip) * _itemsInRow + skip)
+			/ 2;
 		for (auto &item : _items) {
 			_itemHeight = item->resizeGetHeight(_itemWidth);
 		}
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index f6f70098c..5e71fcad0 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -39,7 +39,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/layers/box_content.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
+#include "ui/round_rect.h"
 #include "ui/rp_widget.h"
+#include "styles/style_chat.h"
+#include "styles/style_chat_helpers.h"
 #include "styles/style_media_view.h"
 #include "styles/style_widgets.h"
 #include "styles/style_boxes.h" // UserpicButton
@@ -84,14 +87,17 @@ private:
 
 class Controller::Unsupported final {
 public:
-	explicit Unsupported(not_null<Controller*> controller);
+	Unsupported(not_null<Controller*> controller, not_null<UserData*> user);
 
 private:
-	void setup();
+	void setup(not_null<UserData*> user);
 
 	const not_null<Controller*> _controller;
+	std::unique_ptr<Ui::RpWidget> _bg;
+	std::unique_ptr<Ui::RpWidget> _reply;
 	std::unique_ptr<Ui::FlatLabel> _text;
 	std::unique_ptr<Ui::RoundButton> _button;
+	Ui::RoundRect _bgRound;
 
 };
 
@@ -146,14 +152,52 @@ void Controller::PhotoPlayback::callback() {
 	});
 }
 
-Controller::Unsupported::Unsupported(not_null<Controller*> controller)
-: _controller(controller) {
-	setup();
+Controller::Unsupported::Unsupported(
+	not_null<Controller*> controller,
+	not_null<UserData*> user)
+: _controller(controller)
+, _bgRound(st::storiesRadius, st::storiesComposeBg) {
+	setup(user);
 }
 
-void Controller::Unsupported::setup() {
+void Controller::Unsupported::setup(not_null<UserData*> user) {
 	const auto wrap = _controller->wrap();
 
+	_bg = std::make_unique<Ui::RpWidget>(wrap);
+	_bg->show();
+	_bg->paintRequest() | rpl::start_with_next([=] {
+		auto p = QPainter(_bg.get());
+		_bgRound.paint(p, _bg->rect());
+	}, _bg->lifetime());
+
+	if (!user->isSelf()) {
+		_reply = std::make_unique<Ui::RpWidget>(wrap);
+		_reply->show();
+		_reply->paintRequest() | rpl::start_with_next([=] {
+			auto p = QPainter(_reply.get());
+			_bgRound.paint(p, _reply->rect());
+
+			p.setPen(st::storiesComposeGrayText);
+			p.setFont(st::normalFont);
+			p.drawText(
+				_reply->rect(),
+				tr::lng_stories_cant_reply(tr::now),
+				style::al_center);
+		}, _reply->lifetime());
+	}
+
+	_controller->layoutValue(
+	) | rpl::start_with_next([=](const Layout &layout) {
+		_bg->setGeometry(layout.content);
+		if (_reply) {
+			const auto height = st::storiesComposeControls.attach.height;
+			const auto position = layout.controlsBottomPosition
+				- QPoint(0, height);
+			_reply->setGeometry(
+				{ position, QSize{ layout.controlsWidth, height } });
+		}
+	}, _bg->lifetime());
+
 	_text = std::make_unique<Ui::FlatLabel>(
 		wrap,
 		tr::lng_stories_unsupported(),
@@ -164,17 +208,29 @@ void Controller::Unsupported::setup() {
 		wrap,
 		tr::lng_update_telegram(),
 		st::storiesUnsupportedUpdate);
+	_button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
 	_button->show();
 
 	rpl::combine(
-		wrap->sizeValue(),
+		_controller->layoutValue(),
 		_text->sizeValue(),
 		_button->sizeValue()
-	) | rpl::start_with_next([=](QSize wrap, QSize text, QSize button) {
-		_text->move((wrap.width() - text.width()) / 2, wrap.height() / 3);
+	) | rpl::start_with_next([=](
+			const Layout &layout,
+			QSize text,
+			QSize button) {
+		const auto wrap = layout.content;
+		const auto totalHeight = st::storiesUnsupportedTop
+			+ text.height()
+			+ st::storiesUnsupportedSkip
+			+ button.height();
+		const auto top = (wrap.height() - totalHeight) / 2;
+		_text->move(
+			wrap.x() + (wrap.width() - text.width()) / 2,
+			wrap.y() + top + st::storiesUnsupportedTop);
 		_button->move(
-			(wrap.width() - button.width()) / 2,
-			2 * wrap.height() / 3);
+			wrap.x() + (wrap.width() - button.width()) / 2,
+			wrap.y() + top + totalHeight - button.height());
 	}, _button->lifetime());
 
 	_button->setClickedCallback([=] {
@@ -644,10 +700,13 @@ void Controller::show(
 		}
 	});
 
-	if (!story->unsupported()) {
+	const auto unsupported = story->unsupported();
+	if (!unsupported) {
 		_unsupported = nullptr;
 	} else {
-		_unsupported = std::make_unique<Unsupported>(this);
+		_unsupported = std::make_unique<Unsupported>(this, user);
+		_header->raise();
+		_slider->raise();
 	}
 
 	if (_shown == storyId) {
@@ -664,7 +723,10 @@ void Controller::show(
 	}
 
 	_header->show({ .user = user, .date = story->date() });
-	_replyArea->show({ .user = user, .id = story->id() });
+	_replyArea->show({
+		.user = unsupported ? nullptr : user,
+		.id = story->id(),
+	});
 	_recentViews->show({
 		.list = story->recentViewers(),
 		.total = story->views(),
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 3c089c9bd..cbd67f5e6 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -131,6 +131,12 @@ void Header::show(HeaderData data) {
 	}
 }
 
+void Header::raise() {
+	if (_widget) {
+		_widget->raise();
+	}
+}
+
 void Header::updateDateText() {
 	if (!_date || !_data || !_data->date) {
 		return;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index 32c310bff..97d62b250 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -33,6 +33,7 @@ public:
 	~Header();
 
 	void show(HeaderData data);
+	void raise();
 
 private:
 	void updateDateText();
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 9d7f24715..2929d52c9 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -74,6 +74,7 @@ ReplyArea::ReplyArea(not_null<Controller*> controller)
 )) {
 	initGeometry();
 	initActions();
+	_controls->hide();
 }
 
 ReplyArea::~ReplyArea() {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp
index f4dc57956..53f39c1ad 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp
@@ -73,6 +73,12 @@ void Slider::show(SliderData data) {
 	}, raw->lifetime());
 }
 
+void Slider::raise() {
+	if (_widget) {
+		_widget->raise();
+	}
+}
+
 void Slider::updatePlayback(const Player::TrackState &state) {
 	_progress->updateState(state);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.h b/Telegram/SourceFiles/media/stories/media_stories_slider.h
index 140df471f..37d1c71ec 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_slider.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_slider.h
@@ -37,6 +37,7 @@ public:
 	~Slider();
 
 	void show(SliderData data);
+	void raise();
 
 	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 4137dbce9..edc5bd517 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -786,13 +786,20 @@ storiesReactionsAddedTop: 200px;
 storiesUnsupportedLabel: FlatLabel(defaultFlatLabel) {
 	textFg: mediaviewControlFg;
 	style: TextStyle(defaultTextStyle) {
-		font: font(15px semibold);
-		linkFont: font(15px semibold);
-		linkFontOver: font(15px semibold underline);
+		font: font(14px);
+		linkFont: font(14px);
+		linkFontOver: font(14px underline);
+		lineHeight: 21px;
 	}
 	align: align(top);
 }
-storiesUnsupportedUpdate: themePreviewApplyButton;
+storiesUnsupportedUpdate: RoundButton(defaultActiveButton) {
+	width: -102px;
+	height: 42px;
+	textTop: 12px;
+}
+storiesUnsupportedTop: 24px;
+storiesUnsupportedSkip: 18px;
 storiesShortInfoBox: ShortInfoBox(shortInfoBox) {
 	label: FlatLabel(infoLabel) {
 		textFg: storiesComposeGrayText;

From 80bec508b620e24290d3d5b756e7bafba86db898 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 21 Jun 2023 12:57:57 +0400
Subject: [PATCH 094/259] Support new stories link t.me/username/s/123.

---
 Telegram/SourceFiles/apiwrap.cpp                 | 2 +-
 Telegram/SourceFiles/core/local_url_handlers.cpp | 8 ++++++--
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 44c7d507e..ea70c516b 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -779,7 +779,7 @@ QString ApiWrap::exportDirectStoryLink(not_null<Data::Story*> story) {
 	const auto fallback = [&] {
 		const auto base = user->username();
 		const auto story = QString::number(storyId.story);
-		const auto query = base + "?story=" + story;
+		const auto query = base + "/s" + story;
 		return session().createInternalLinkFull(query);
 	};
 	const auto i = _unlikelyStoryLinks.find(storyId);
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 2d013e612..df0c92771 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -383,7 +383,8 @@ bool ResolveUsernameOrPhone(
 	if (const auto postId = postParam.toInt()) {
 		post = postId;
 	}
-	const auto storyId = params.value(u"story"_q).toInt();
+	const auto storyParam = params.value(u"story"_q);
+	const auto storyId = storyParam.toInt();
 	const auto appname = params.value(u"appname"_q);
 	const auto appstart = params.value(u"startapp"_q);
 	const auto commentParam = params.value(u"comment"_q);
@@ -423,7 +424,7 @@ bool ResolveUsernameOrPhone(
 		.startToken = startToken,
 		.startAdminRights = adminRights,
 		.startAutoSubmit = myContext.botStartAutoSubmit,
-		.botAppName = appname.isEmpty() ? postParam : appname,
+		.botAppName = (appname.isEmpty() ? postParam : appname),
 		.botAppForceConfirmation = myContext.mayShowConfirmation,
 		.attachBotUsername = params.value(u"attach"_q),
 		.attachBotToggleCommand = (params.contains(u"startattach"_q)
@@ -1029,6 +1030,7 @@ QString TryConvertUrlToLocal(QString url) {
 				"/?$|"
 				"/[a-zA-Z0-9\\.\\_]+/?(\\?|$)|"
 				"/\\d+/?(\\?|$)|"
+				"/s/\\d+/?(\\?|$)|"
 				"/\\d+/\\d+/?(\\?|$)"
 			")"_q, query, matchOptions)) {
 			const auto params = query.mid(usernameMatch->captured(0).size()).toString();
@@ -1038,6 +1040,8 @@ QString TryConvertUrlToLocal(QString url) {
 				added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2));
 			} else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
 				added = u"&post="_q + postMatch->captured(1);
+			} else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
+				added = u"&story="_q + storyMatch->captured(1);
 			} else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
 				added = u"&appname="_q + appNameMatch->captured(1);
 			}

From 4e39144d0f5c74de3af182ae0320bbbe2b5e93b2 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 21 Jun 2023 13:15:59 +0400
Subject: [PATCH 095/259] Resolve unknown story on open.

---
 Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp     | 4 +++-
 Telegram/SourceFiles/window/window_session_controller.cpp | 8 +++++++-
 Telegram/SourceFiles/window/window_session_controller.h   | 2 ++
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index a63e159c7..c65f9a2a0 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -342,7 +342,9 @@ InnerWidget::InnerWidget(
 
 	_stories->clicks(
 	) | rpl::start_with_next([=](uint64 id) {
-		_controller->openPeerStories(PeerId(int64(id)), {});
+		_controller->openPeerStories(
+			PeerId(int64(id)),
+			Data::StorySourcesList::NotHidden);
 	}, lifetime());
 
 	_stories->showProfileRequests(
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index 394ca2358..9758d8545 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -2522,9 +2522,15 @@ void SessionController::openPeerStory(
 	using namespace Media::View;
 	using namespace Data;
 
+	invalidate_weak_ptrs(&_storyOpenGuard);
 	auto &stories = session().data().stories();
-	if (const auto from = stories.lookup({ peer->id, storyId })) {
+	const auto from = stories.lookup({ peer->id, storyId });
+	if (from) {
 		window().openInMediaView(OpenRequest(this, *from, context));
+	} else if (from.error() == Data::NoStory::Unknown) {
+		stories.resolve({ peer->id, storyId }, crl::guard(&_storyOpenGuard, [=] {
+			openPeerStory(peer, storyId, context);
+		}));
 	}
 }
 
diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h
index 7d9567427..509fa2818 100644
--- a/Telegram/SourceFiles/window/window_session_controller.h
+++ b/Telegram/SourceFiles/window/window_session_controller.h
@@ -717,6 +717,8 @@ private:
 	using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;
 	std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;
 
+	base::has_weak_ptr _storyOpenGuard;
+
 	GiftPremiumValidator _giftPremiumValidator;
 
 	QString _premiumRef;

From ebafb55b1b9b93667546dec19ca92c99a3357eaf Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 21 Jun 2023 13:35:10 +0400
Subject: [PATCH 096/259] Cache stories sources order in viewer.

---
 .../stories/media_stories_controller.cpp      | 78 +++++++++++++++++--
 .../media/stories/media_stories_controller.h  | 11 ++-
 2 files changed, 78 insertions(+), 11 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 5e71fcad0..79828f7fc 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -618,7 +618,8 @@ void Controller::rebuildFromContext(
 			storyId.peer,
 			&StoriesSourceInfo::id);
 		if (i != end(sources)) {
-			showSiblings(&user->session(), sources, (i - begin(sources)));
+			rebuildCachedSourcesList(sources, (i - begin(sources)));
+			showSiblings(&user->session());
 			if (int(sources.end() - i) < kPreloadUsersCount) {
 				stories.loadMore(list);
 			}
@@ -780,18 +781,19 @@ void Controller::setPlayingAllowed(bool allowed) {
 	}
 }
 
-void Controller::showSiblings(
-		not_null<Main::Session*> session,
-		const std::vector<Data::StoriesSourceInfo> &sources,
-		int index) {
+void Controller::showSiblings(not_null<Main::Session*> session) {
 	showSibling(
 		_siblingLeft,
 		session,
-		(index > 0) ? sources[index - 1].id : PeerId());
+		(_cachedSourceIndex > 0
+			? _cachedSourcesList[_cachedSourceIndex - 1]
+			: PeerId()));
 	showSibling(
 		_siblingRight,
 		session,
-		(index + 1 < sources.size()) ? sources[index + 1].id : PeerId());
+		(_cachedSourceIndex + 1 < _cachedSourcesList.size()
+			? _cachedSourcesList[_cachedSourceIndex + 1]
+			: PeerId()));
 }
 
 void Controller::hideSiblings() {
@@ -1116,6 +1118,68 @@ void Controller::loadMoreToList() {
 	});
 }
 
+void Controller::rebuildCachedSourcesList(
+		const std::vector<Data::StoriesSourceInfo> &lists,
+		int index) {
+	Expects(index >= 0 && index < lists.size());
+
+	// Remove removed.
+	_cachedSourcesList.erase(ranges::remove_if(_cachedSourcesList, [&](
+			PeerId id) {
+		return !ranges::contains(lists, id, &Data::StoriesSourceInfo::id);
+	}), end(_cachedSourcesList));
+
+	// Find current, full rebuild if can't find.
+	const auto i = ranges::find(_cachedSourcesList, lists[index].id);
+	if (i == end(_cachedSourcesList)) {
+		_cachedSourcesList.clear();
+	} else {
+		_cachedSourceIndex = int(i - begin(_cachedSourcesList));
+	}
+
+	if (_cachedSourcesList.empty()) {
+		// Full rebuild.
+		_cachedSourcesList = lists
+			| ranges::views::transform(&Data::StoriesSourceInfo::id)
+			| ranges::to_vector;
+		_cachedSourceIndex = index;
+	} else if (ranges::equal(
+			lists,
+			_cachedSourcesList,
+			ranges::equal_to(),
+			&Data::StoriesSourceInfo::id)) {
+		// No rebuild needed.
+		_cachedSourceIndex = index;
+	} else {
+		// All that go before the current push to front.
+		for (auto before = index; before > 0;) {
+			--before;
+			const auto peerId = lists[--before].id;
+			if (!ranges::contains(_cachedSourcesList, peerId)) {
+				_cachedSourcesList.insert(
+					begin(_cachedSourcesList),
+					peerId);
+				++_cachedSourceIndex;
+			}
+		}
+		// All that go after the current push to back.
+		for (auto after = index + 1, count = int(lists.size()); after != count; ++after) {
+			const auto peerId = lists[after].id;
+			if (!ranges::contains(_cachedSourcesList, peerId)) {
+				_cachedSourcesList.push_back(peerId);
+			}
+		}
+	}
+
+	Ensures(_cachedSourcesList.size() == lists.size());
+	Ensures(ranges::equal(
+		lists,
+		_cachedSourcesList,
+		ranges::equal_to(),
+		&Data::StoriesSourceInfo::id));
+	Ensures(_cachedSourceIndex >= 0 && _cachedSourceIndex < _cachedSourcesList.size());
+}
+
 void Controller::refreshViewsFromData() {
 	Expects(shown());
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 96d102073..405960f39 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -162,10 +162,7 @@ private:
 	void setPlayingAllowed(bool allowed);
 
 	void hideSiblings();
-	void showSiblings(
-		not_null<Main::Session*> session,
-		const std::vector<Data::StoriesSourceInfo> &lists,
-		int index);
+	void showSiblings(not_null<Main::Session*> session);
 	void showSibling(
 		std::unique_ptr<Sibling> &sibling,
 		not_null<Main::Session*> session,
@@ -186,6 +183,9 @@ private:
 	void rebuildFromContext(not_null<UserData*> user, FullStoryId storyId);
 	void checkMoveByDelta();
 	void loadMoreToList();
+	void rebuildCachedSourcesList(
+		const std::vector<Data::StoriesSourceInfo> &lists,
+		int index);
 
 	void startReactionAnimation(
 		Data::ReactionId id,
@@ -227,6 +227,9 @@ private:
 	bool _started = false;
 	bool _viewed = false;
 
+	std::vector<PeerId> _cachedSourcesList;
+	int _cachedSourceIndex = -1;
+
 	ViewsSlice _viewsSlice;
 	rpl::event_stream<> _moreViewsLoaded;
 	base::has_weak_ptr _viewsLoadGuard;

From e21c06f67ce89b9f302683438745c878b927540d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 22 Jun 2023 19:29:11 +0400
Subject: [PATCH 097/259] Support deleting and reporting stories.

---
 Telegram/Resources/langs/lang.strings         |  6 ++
 Telegram/SourceFiles/api/api_report.cpp       | 26 +++++--
 Telegram/SourceFiles/api/api_report.h         |  6 +-
 Telegram/SourceFiles/boxes/background_box.cpp |  4 +-
 .../SourceFiles/boxes/report_messages_box.cpp | 26 +++++--
 .../chat_helpers/chat_helpers.style           | 37 ++++++++++
 Telegram/SourceFiles/data/data_stories.cpp    | 48 +++++++++++++
 Telegram/SourceFiles/data/data_stories.h      | 13 ++++
 .../SourceFiles/history/history_widget.cpp    |  3 +-
 Telegram/SourceFiles/info/info.style          |  6 --
 .../stories/media_stories_controller.cpp      | 70 +++++++++++++------
 .../media/stories/media_stories_controller.h  |  8 +--
 .../media/stories/media_stories_view.cpp      | 20 +++---
 .../media/stories/media_stories_view.h        |  8 ++-
 .../SourceFiles/media/view/media_view.style   | 53 ++++++++++----
 .../media/view/media_view_overlay_widget.cpp  | 36 +++++++---
 Telegram/SourceFiles/mtproto/scheme/api.tl    |  1 +
 Telegram/SourceFiles/ui/boxes/report_box.cpp  | 44 ++++++------
 Telegram/SourceFiles/ui/boxes/report_box.h    |  7 ++
 19 files changed, 322 insertions(+), 100 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 018d19573..091cf0610 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -757,6 +757,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_background_apply2" = "Enjoy the view.";
 "lng_background_apply_button" = "Apply For This Chat";
 "lng_background_dimming" = "Background dimming";
+"lng_background_sure_reset_default" = "Are you sure you want to reset the wallpaper?";
+"lng_background_reset_default" = "Reset";
 
 "lng_download_path_ask" = "Ask download path for each file";
 "lng_download_path" = "Download path";
@@ -1348,6 +1350,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_report_group_video_title" = "Report group video";
 "lng_report_channel_photo_title" = "Report channel photo";
 "lng_report_channel_video_title" = "Report channel video";
+"lng_report_story" = "Report story";
 "lng_report_please_select_messages" = "Please select messages to report.";
 "lng_report_select_messages" = "Select messages";
 "lng_report_messages_none" = "Select Messages";
@@ -3822,6 +3825,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_reply_sent" = "Message Sent";
 "lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list.";
 "lng_stories_shown_in_chats" = "Those stories are now shown in your Chats list.";
+"lng_stories_delete_one_sure" = "Are you sure you want to delete this story?";
+"lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?";
+"lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?";
 
 "lng_stories_link_invalid" = "This link is broken or has expired.";
 
diff --git a/Telegram/SourceFiles/api/api_report.cpp b/Telegram/SourceFiles/api/api_report.cpp
index 7185b76f5..501870180 100644
--- a/Telegram/SourceFiles/api/api_report.cpp
+++ b/Telegram/SourceFiles/api/api_report.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "apiwrap.h"
 #include "data/data_peer.h"
 #include "data/data_photo.h"
+#include "data/data_user.h"
 #include "lang/lang_keys.h"
 #include "main/main_session.h"
 #include "ui/boxes/report_box.h"
@@ -39,11 +40,15 @@ MTPreportReason ReasonToTL(const Ui::ReportReason &reason) {
 } // namespace
 
 void SendReport(
-		std::shared_ptr<Ui::Show> show,
-		not_null<PeerData*> peer,
-		Ui::ReportReason reason,
-		const QString &comment,
-		std::variant<v::null_t, MessageIdsList, not_null<PhotoData*>> data) {
+	std::shared_ptr<Ui::Show> show,
+	not_null<PeerData*> peer,
+	Ui::ReportReason reason,
+	const QString &comment,
+	std::variant<
+		v::null_t,
+		MessageIdsList,
+		not_null<PhotoData*>,
+		StoryId> data) {
 	auto done = [=] {
 		show->showToast(tr::lng_report_thanks(tr::now));
 	};
@@ -72,6 +77,17 @@ void SendReport(
 			ReasonToTL(reason),
 			MTP_string(comment)
 		)).done(std::move(done)).send();
+	}, [&](StoryId id) {
+		const auto user = peer->asUser();
+		if (!user) {
+			return;
+		}
+		peer->session().api().request(MTPstories_Report(
+			user->inputUser,
+			MTP_vector<MTPint>(1, MTP_int(id)),
+			ReasonToTL(reason),
+			MTP_string(comment)
+		)).done(std::move(done)).send();
 	});
 }
 
diff --git a/Telegram/SourceFiles/api/api_report.h b/Telegram/SourceFiles/api/api_report.h
index 08535c00b..14e9d4ef1 100644
--- a/Telegram/SourceFiles/api/api_report.h
+++ b/Telegram/SourceFiles/api/api_report.h
@@ -22,6 +22,10 @@ void SendReport(
 	not_null<PeerData*> peer,
 	Ui::ReportReason reason,
 	const QString &comment,
-	std::variant<v::null_t, MessageIdsList, not_null<PhotoData*>> data);
+	std::variant<
+		v::null_t,
+		MessageIdsList,
+		not_null<PhotoData*>,
+		StoryId> data);
 
 } // namespace Api
diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp
index e1262b4c9..a7b92aa32 100644
--- a/Telegram/SourceFiles/boxes/background_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_box.cpp
@@ -282,9 +282,9 @@ void BackgroundBox::chosen(const Data::WallPaper &paper) {
 				close();
 			});
 			_controller->show(Ui::MakeConfirmBox({
-				.text = u"Are you sure you want to reset the wallpaper?"_q,
+				.text = tr::lng_background_sure_reset_default(),
 				.confirmed = reset,
-				.confirmText = u"Reset"_q,
+				.confirmText = tr::lng_background_reset_default(),
 			}));
 		} else {
 			closeBox();
diff --git a/Telegram/SourceFiles/boxes/report_messages_box.cpp b/Telegram/SourceFiles/boxes/report_messages_box.cpp
index 291e0617b..5c8d59133 100644
--- a/Telegram/SourceFiles/boxes/report_messages_box.cpp
+++ b/Telegram/SourceFiles/boxes/report_messages_box.cpp
@@ -14,12 +14,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/boxes/report_box.h"
 #include "ui/layers/generic_box.h"
 #include "window/window_session_controller.h"
+#include "styles/style_chat_helpers.h"
 
 namespace {
 
 [[nodiscard]] object_ptr<Ui::BoxContent> Report(
 		not_null<PeerData*> peer,
-		std::variant<v::null_t, MessageIdsList, not_null<PhotoData*>> data) {
+		std::variant<
+			v::null_t,
+			MessageIdsList,
+			not_null<PhotoData*>,
+			StoryId> data,
+		const style::ReportBox *stOverride) {
 	const auto source = v::match(data, [](const MessageIdsList &ids) {
 		return Ui::ReportSource::Message;
 	}, [&](not_null<PhotoData*> photo) {
@@ -34,15 +40,18 @@ namespace {
 			: (photo->hasVideo()
 				? Ui::ReportSource::ChannelVideo
 				: Ui::ReportSource::ChannelPhoto);
+	}, [&](StoryId id) {
+		return Ui::ReportSource::Story;
 	}, [](v::null_t) {
 		Unexpected("Bad source report.");
 		return Ui::ReportSource::Bot;
 	});
+	const auto st = stOverride ? stOverride : &st::defaultReportBox;
 	return Box([=](not_null<Ui::GenericBox*> box) {
 		const auto show = box->uiShow();
-		Ui::ReportReasonBox(box, source, [=](Ui::ReportReason reason) {
+		Ui::ReportReasonBox(box, *st, source, [=](Ui::ReportReason reason) {
 			show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
-				Ui::ReportDetailsBox(box, [=](const QString &text) {
+				Ui::ReportDetailsBox(box, *st, [=](const QString &text) {
 					Api::SendReport(show, peer, reason, text, data);
 					show->hideLayer();
 				});
@@ -56,13 +65,13 @@ namespace {
 object_ptr<Ui::BoxContent> ReportItemsBox(
 		not_null<PeerData*> peer,
 		MessageIdsList ids) {
-	return Report(peer, ids);
+	return Report(peer, ids, nullptr);
 }
 
 object_ptr<Ui::BoxContent> ReportProfilePhotoBox(
 		not_null<PeerData*> peer,
 		not_null<PhotoData*> photo) {
-	return Report(peer, photo);
+	return Report(peer, photo, nullptr);
 }
 
 void ShowReportPeerBox(
@@ -93,17 +102,20 @@ void ShowReportPeerBox(
 		if (reason == Ui::ReportReason::Fake
 			|| reason == Ui::ReportReason::Other) {
 			state->ids = {};
-			state->detailsBox = window->show(Box(Ui::ReportDetailsBox, send));
+			state->detailsBox = window->show(
+				Box(Ui::ReportDetailsBox, st::defaultReportBox, send));
 			return;
 		}
 		window->showChooseReportMessages(peer, reason, [=](
 				MessageIdsList ids) {
 			state->ids = std::move(ids);
-			state->detailsBox = window->show(Box(Ui::ReportDetailsBox, send));
+			state->detailsBox = window->show(
+				Box(Ui::ReportDetailsBox, st::defaultReportBox, send));
 		});
 	};
 	state->reasonBox = window->show(Box(
 		Ui::ReportReasonBox,
+		st::defaultReportBox,
 		(peer->isBroadcast()
 			? Ui::ReportSource::Channel
 			: peer->isUser()
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index e5552ead4..5c72090e0 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -206,6 +206,21 @@ ComposeControls {
 	premium: PremiumLimits;
 }
 
+ReportBox {
+	button: SettingsButton;
+	label: FlatLabel;
+	field: InputField;
+	spam: icon;
+	fake: icon;
+	violence: icon;
+	children: icon;
+	pornography: icon;
+	copyright: icon;
+	drugs: icon;
+	personal: icon;
+	other: icon;
+}
+
 WhoRead {
 	userpics: GroupCallUserpics;
 	photoLeft: pixels;
@@ -1152,3 +1167,25 @@ moreChatsBarClose: IconButton(defaultIconButton) {
 		color: windowBgOver;
 	}
 }
+
+reportReasonTopSkip: 8px;
+reportReasonButton: SettingsButton(defaultSettingsButton) {
+	style: boxTextStyle;
+	padding: margins(62px, 7px, 8px, 7px);
+	iconLeft: 22px;
+}
+
+defaultReportBox: ReportBox {
+	button: reportReasonButton;
+	label: boxLabel;
+	field: newGroupDescription;
+	spam: menuIconDelete;
+	fake: menuIconFake;
+	violence: menuIconViolence;
+	children: menuIconBlock;
+	pornography: menuIconPorn;
+	copyright: menuIconCopyright;
+	drugs: menuIconDrugs;
+	personal: menuIconPersonal;
+	other: menuIconReport;
+}
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 525a0d200..1ebd7a1ed 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 "api/api_report.h"
 #include "base/unixtime.h"
 #include "api/api_text_entities.h"
 #include "apiwrap.h"
@@ -223,6 +224,22 @@ bool Story::closeFriends() const {
 	return _closeFriends;
 }
 
+bool Story::canDownload() const {
+	return _peer->isSelf();
+}
+
+bool Story::canShare() const {
+	return isPublic() && (pinned() || !expired());
+}
+
+bool Story::canDelete() const {
+	return _peer->isSelf();
+}
+
+bool Story::canReport() const {
+	return !_peer->isSelf();
+}
+
 bool Story::hasDirectLink() const {
 	if (!_isPublic || (!_pinned && expired())) {
 		return false;
@@ -1479,7 +1496,38 @@ void Stories::savedLoadMore(PeerId peerId) {
 		saved.total = int(saved.ids.list.size());
 		_savedChanged.fire_copy(peerId);
 	}).send();
+}
 
+void Stories::deleteList(const std::vector<FullStoryId> &ids) {
+	auto list = QVector<MTPint>();
+	list.reserve(ids.size());
+	const auto selfId = session().userPeerId();
+	for (const auto &id : ids) {
+		if (id.peer == selfId) {
+			list.push_back(MTP_int(id.story));
+		}
+	}
+	if (!list.empty()) {
+		const auto api = &_owner->session().api();
+		api->request(MTPstories_DeleteStories(
+			MTP_vector<MTPint>(list)
+		)).done([=](const MTPVector<MTPint> &result) {
+			for (const auto &id : result.v) {
+				applyDeleted({ selfId, id.v });
+			}
+		}).send();
+	}
+}
+
+void Stories::report(
+		std::shared_ptr<Ui::Show> show,
+		FullStoryId id,
+		Ui::ReportReason reason,
+		QString text) {
+	if (const auto maybeStory = lookup(id)) {
+		const auto story = *maybeStory;
+		Api::SendReport(show, story->peer(), reason, text, story->id());
+	}
 }
 
 bool Stories::isQuitPrevent() {
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index d082d05a6..3199ff82f 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -23,6 +23,7 @@ class Session;
 
 namespace Ui {
 class Show;
+enum class ReportReason;
 } // namespace Ui
 
 namespace Data {
@@ -106,6 +107,11 @@ public:
 	void setCloseFriends(bool closeFriends);
 	[[nodiscard]] bool closeFriends() const;
 
+	[[nodiscard]] bool canDownload() const;
+	[[nodiscard]] bool canShare() const;
+	[[nodiscard]] bool canDelete() const;
+	[[nodiscard]] bool canReport() const;
+
 	[[nodiscard]] bool hasDirectLink() const;
 	[[nodiscard]] std::optional<QString> errorTextForForward(
 		not_null<Thread*> to) const;
@@ -284,6 +290,13 @@ public:
 	[[nodiscard]] bool savedLoaded(PeerId peerId) const;
 	void savedLoadMore(PeerId peerId);
 
+	void deleteList(const std::vector<FullStoryId> &ids);
+	void report(
+		std::shared_ptr<Ui::Show> show,
+		FullStoryId id,
+		Ui::ReportReason reason,
+		QString text);
+
 private:
 	struct Saved {
 		StoriesIds ids;
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 8e50593be..e99b55c85 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -4009,7 +4009,8 @@ void HistoryWidget::reportSelectedMessages() {
 	const auto reason = _chooseForReport->reason;
 	const auto weak = Ui::MakeWeak(_list.data());
 	controller()->window().show(Box([=](not_null<Ui::GenericBox*> box) {
-		Ui::ReportDetailsBox(box, [=](const QString &text) {
+		const auto &st = st::defaultReportBox;
+		Ui::ReportDetailsBox(box, st, [=](const QString &text) {
 			if (weak) {
 				clearSelected();
 				controller()->clearChooseReportMessages();
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index fcd649b38..b6958d4fa 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -885,10 +885,4 @@ shortInfoCover: ShortInfoCover {
 	}
 }
 
-reportReasonTopSkip: 8px;
-reportReasonButton: SettingsButton(infoProfileButton) {
-	style: boxTextStyle;
-	padding: margins(62px, 7px, 8px, 7px);
-}
-
 permissionsExpandIcon: icon{{ "info/edit/expand_arrow_small", windowBoldFg }};
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 79828f7fc..bc7d0eaec 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -33,6 +33,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/stories/media_stories_share.h"
 #include "media/stories/media_stories_view.h"
 #include "media/audio/media_audio.h"
+#include "ui/boxes/confirm_box.h"
+#include "ui/boxes/report_box.h"
 #include "ui/effects/emoji_fly_animation.h"
 #include "ui/effects/message_sending_animation_common.h"
 #include "ui/effects/reaction_fly_animation.h"
@@ -493,6 +495,11 @@ void Controller::initLayout() {
 	});
 }
 
+Data::Story *Controller::story() const {
+	const auto maybeStory = _session->data().stories().lookup(_shown);
+	return maybeStory ? maybeStory->get() : nullptr;
+}
+
 not_null<Ui::RpWidget*> Controller::wrap() const {
 	return _wrap;
 }
@@ -995,25 +1002,6 @@ void Controller::setMenuShown(bool shown) {
 	}
 }
 
-bool Controller::canShare() const {
-	if (const auto maybeStory = _session->data().stories().lookup(_shown)) {
-		const auto story = *maybeStory;
-		const auto user = story->peer()->asUser();
-		return story->isPublic()
-			&& (story->pinned() || !story->expired())
-			&& (!user->username().isEmpty()
-				|| !user->hasPrivateForwardName());
-	}
-	return false;
-}
-
-bool Controller::canDownload() const {
-	if (const auto maybeStory = _session->data().stories().lookup(_shown)) {
-		return (*maybeStory)->peer()->isSelf();
-	}
-	return false;
-}
-
 void Controller::repaintSibling(not_null<Sibling*> sibling) {
 	if (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) {
 		_delegate->storiesRepaint();
@@ -1237,13 +1225,55 @@ void Controller::unfocusReply() {
 	_wrap->setFocus();
 }
 
-void Controller::share() {
+void Controller::shareRequested() {
 	const auto show = _delegate->storiesShow();
 	if (auto box = PrepareShareBox(show, _shown)) {
 		show->show(std::move(box));
 	}
 }
 
+void Controller::deleteRequested() {
+	const auto story = this->story();
+	if (!story) {
+		return;
+	}
+	const auto id = story->fullId();
+	const auto owner = &story->owner();
+	const auto confirmed = [=](Fn<void()> close) {
+		owner->stories().deleteList({ id });
+		close();
+	};
+	uiShow()->show(Ui::MakeConfirmBox({
+		.text = tr::lng_stories_delete_one_sure(),
+		.confirmed = confirmed,
+		.confirmText = tr::lng_selected_delete(),
+		.labelStyle = &st::storiesBoxLabel,
+	}));
+}
+
+void Controller::reportRequested() {
+	const auto story = this->story();
+	if (!story) {
+		return;
+	}
+	const auto id = story->fullId();
+	const auto owner = &story->owner();
+	const auto confirmed = [=](Fn<void()> close) {
+		owner->stories().deleteList({ id });
+		close();
+	};
+	const auto show = uiShow();
+	const auto st = &st::storiesReportBox;
+	show->show(Box(Ui::ReportReasonBox, *st, Ui::ReportSource::Story, [=](
+			Ui::ReportReason reason) {
+		const auto done = [=](const QString &text) {
+			owner->stories().report(show, id, reason, text);
+			show->hideLayer();
+		};
+		show->showBox(Box(Ui::ReportDetailsBox, *st, done));
+	}));
+}
+
 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 405960f39..a680d7f68 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -96,6 +96,7 @@ public:
 	explicit Controller(not_null<Delegate*> delegate);
 	~Controller();
 
+	[[nodiscard]] Data::Story *story() const;
 	[[nodiscard]] not_null<Ui::RpWidget*> wrap() const;
 	[[nodiscard]] Layout layout() const;
 	[[nodiscard]] rpl::producer<Layout> layoutValue() const;
@@ -123,9 +124,6 @@ public:
 	void contentPressed(bool pressed);
 	void setMenuShown(bool shown);
 
-	[[nodiscard]] bool canShare() const;
-	[[nodiscard]] bool canDownload() const;
-
 	void repaintSibling(not_null<Sibling*> sibling);
 	[[nodiscard]] SiblingView sibling(SiblingType type) const;
 
@@ -133,7 +131,9 @@ public:
 	[[nodiscard]] rpl::producer<> moreViewsLoaded() const;
 
 	void unfocusReply();
-	void share();
+	void shareRequested();
+	void deleteRequested();
+	void reportRequested();
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
index 1b2747ee4..b79064e2e 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
@@ -32,12 +32,8 @@ void View::ready() {
 	_controller->ready();
 }
 
-bool View::canShare() const {
-	return _controller->canShare();
-}
-
-bool View::canDownload() const {
-	return _controller->canDownload();
+Data::Story *View::story() const {
+	return _controller->story();
 }
 
 QRect View::finalShownGeometry() const {
@@ -83,8 +79,16 @@ void View::contentPressed(bool pressed) {
 	_controller->contentPressed(pressed);
 }
 
-void View::share() {
-	_controller->share();
+void View::shareRequested() {
+	_controller->shareRequested();
+}
+
+void View::deleteRequested() {
+	_controller->deleteRequested();
+}
+
+void View::reportRequested() {
+	_controller->reportRequested();
 }
 
 SiblingView View::sibling(SiblingType type) const {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h
index ba138ac5d..dabb6952a 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -56,8 +56,7 @@ public:
 	void show(not_null<Data::Story*> story, Data::StoriesContext context);
 	void ready();
 
-	[[nodiscard]] bool canShare() const;
-	[[nodiscard]] bool canDownload() const;
+	[[nodiscard]] Data::Story *story() const;
 	[[nodiscard]] QRect finalShownGeometry() const;
 	[[nodiscard]] rpl::producer<QRect> finalShownGeometryValue() const;
 	[[nodiscard]] ContentLayout contentLayout() const;
@@ -75,7 +74,10 @@ public:
 	[[nodiscard]] bool paused() const;
 	void togglePaused(bool paused);
 	void contentPressed(bool pressed);
-	void share();
+
+	void shareRequested();
+	void deleteRequested();
+	void reportRequested();
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index edc5bd517..85a2716d3 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -509,6 +509,9 @@ storiesPopupMenuWithIcons: PopupMenu(storiesPopupMenu) {
 	menu: storiesMenuWithIcons;
 }
 
+storiesBoxLabel: FlatLabel(boxLabel) {
+	textFg: groupCallMembersFg;
+}
 storiesAttachEmojiInner: IconButton(storiesAttach) {
 	icon: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }};
 	iconOver: icon {{ "chat/input_smile_face", storiesComposeGrayIcon }};
@@ -520,9 +523,7 @@ storiesAttachEmoji: EmojiButton(historyAttachEmoji) {
 	lineFgOver: storiesComposeGrayIcon;
 }
 storiesComposePremium: PremiumLimits(defaultPremiumLimits) {
-	boxLabel: FlatLabel(boxLabel) {
-		textFg: groupCallMembersFg;
-	}
+	boxLabel: storiesBoxLabel;
 	nonPremiumBg: storiesComposeBgOver;
 	nonPremiumFg: storiesComposeWhiteText;
 }
@@ -588,9 +589,7 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) {
 	}
 	search: storiesEmojiTabbedSearch;
 	removeSet: storiesRemoveSet;
-	boxLabel: FlatLabel(boxLabel) {
-		textFg: groupCallMembersFg;
-	}
+	boxLabel: storiesBoxLabel;
 	icons: ComposeIcons {
 		settings: icon {{ "emoji/emoji_settings", storiesComposeGrayIcon }};
 
@@ -642,6 +641,15 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) {
 	}
 	autocompleteBottomSkip: 10px;
 }
+storiesBoxInputField: InputField(defaultComposeFilesField) {
+	textFg: storiesComposeWhiteText;
+	textBg: storiesComposeBg;
+	placeholderFg: storiesComposeGrayText;
+	placeholderFgActive: storiesComposeBlue;
+	borderFg: storiesComposeGrayText;
+	borderFgActive: storiesComposeBlue;
+	menu: storiesPopupMenu;
+}
 storiesComposeControls: ComposeControls(defaultComposeControls) {
 	bg: storiesComposeBg;
 	radius: storiesRadius;
@@ -718,15 +726,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
 			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;
-		}
+		caption: storiesBoxInputField;
 		emoji: EmojiButton(storiesAttachEmoji) {
 			inner: IconButton(storiesAttachEmojiInner) {
 				width: 30px;
@@ -818,3 +818,26 @@ storiesShortInfoBox: ShortInfoBox(shortInfoBox) {
 		palette: mediaviewTextPalette;
 	}
 }
+storiesReportBox: ReportBox(defaultReportBox) {
+	button: SettingsButton(reportReasonButton) {
+		textFg: storiesComposeWhiteText;
+		textFgOver: storiesComposeWhiteText;
+		textBg: storiesComposeBg;
+		textBgOver: storiesComposeBgOver;
+		ripple: storiesComposeRipple;
+	}
+	label: storiesBoxLabel;
+	field: InputField(storiesBoxInputField) {
+		textMargins: margins(1px, 26px, 1px, 4px);
+		heightMax: 116px;
+	}
+	spam: icon {{ "menu/delete", storiesComposeWhiteText }};
+	fake: icon {{ "menu/fake", storiesComposeWhiteText }};
+	violence: icon {{ "menu/violence", storiesComposeWhiteText }};
+	children: icon {{ "menu/block", storiesComposeWhiteText }};
+	pornography: icon {{ "menu/porn", storiesComposeWhiteText }};
+	copyright: icon {{ "menu/copyright", storiesComposeWhiteText }};
+	drugs: icon {{ "menu/drugs", storiesComposeWhiteText }};
+	personal: icon {{ "menu/personal", storiesComposeWhiteText }};
+	other: icon {{ "menu/report", storiesComposeWhiteText }};
+}
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index bb8d12a5b..30c1ae8aa 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -970,7 +970,8 @@ QSize OverlayWidget::flipSizeByRotation(QSize size) const {
 }
 
 bool OverlayWidget::hasCopyMediaRestriction() const {
-	return (_stories && !_stories->canDownload())
+	const auto story = _stories ? _stories->story() : nullptr;
+	return (story && !story->canDownload())
 		|| (_history && !_history->peer->allowsForwarding())
 		|| (_message && _message->forbidsSaving());
 }
@@ -1223,12 +1224,13 @@ void OverlayWidget::updateControls() {
 
 	updateThemePreviewGeometry();
 
+	const auto story = _stories ? _stories->story() : nullptr;
 	const auto overRect = QRect(
 		QPoint(),
 		QSize(st::mediaviewIconOver, st::mediaviewIconOver));
 	_saveVisible = contentCanBeSaved();
-	_shareVisible = _stories && _stories->canShare();
-	_rotateVisible = !_themePreviewShown && !_stories;
+	_shareVisible = story && story->canShare();
+	_rotateVisible = !_themePreviewShown && !story;
 	const auto navRect = [&](int i) {
 		return QRect(width() - st::mediaviewIconSize.width() * i,
 			height() - st::mediaviewIconSize.height(),
@@ -1364,7 +1366,8 @@ void OverlayWidget::refreshCaptionGeometry() {
 }
 
 void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
-	if (_document && _document->loading()) {
+	const auto story = _stories ? _stories->story() : nullptr;
+	if (!story && _document && _document->loading()) {
 		addAction(
 			tr::lng_cancel(tr::now),
 			[=] { saveCancel(); },
@@ -1376,7 +1379,9 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
 			[=] { toMessage(); },
 			&st::mediaMenuIconShowInChat);
 	}
-	if (_document && !_document->filepath(true).isEmpty()) {
+	if ((!story || story->canDownload())
+		&& _document
+		&& !_document->filepath(true).isEmpty()) {
 		const auto text =  Platform::IsMac()
 			? tr::lng_context_show_in_finder(tr::now)
 			: tr::lng_context_show_in_folder(tr::now);
@@ -1406,8 +1411,15 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
 			[=] { forwardMedia(); },
 			&st::mediaMenuIconForward);
 	}
+	if (story && story->canShare()) {
+		addAction(tr::lng_mediaview_forward(tr::now), [=] {
+			_stories->shareRequested();
+		}, &st::mediaMenuIconForward);
+	}
 	const auto canDelete = [&] {
-		if (_message && _message->canDelete()) {
+		if (story && story->canDelete()) {
+			return true;
+		} else if (_message && _message->canDelete()) {
 			return true;
 		} else if (!_message
 			&& _photo
@@ -1527,6 +1539,11 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
 			}
 		}, &st::mediaMenuIconReport);
 	}();
+	if (story && story->canReport()) {
+		addAction(tr::lng_profile_report(tr::now), [=] {
+			_stories->reportRequested();
+		}, &st::mediaMenuIconReport);
+	}
 }
 
 auto OverlayWidget::computeOverviewType() const
@@ -2433,7 +2450,10 @@ void OverlayWidget::forwardMedia() {
 }
 
 void OverlayWidget::deleteMedia() {
-	if (!_session) {
+	if (_stories) {
+		_stories->deleteRequested();
+		return;
+	} else if (!_session) {
 		return;
 	}
 
@@ -5521,7 +5541,7 @@ void OverlayWidget::handleMouseRelease(
 	} else if (_over == Over::Save && _down == Over::Save) {
 		downloadMedia();
 	} else if (_over == Over::Share && _down == Over::Share && _stories) {
-		_stories->share();
+		_stories->shareRequested();
 	} else if (_over == Over::Rotate && _down == Over::Rotate) {
 		playbackControlsRotate();
 	} else if (_over == Over::Icon && _down == Over::Icon) {
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 1ccafbc6f..7509f2c0e 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -2102,3 +2102,4 @@ stories.incrementStoryViews#22126127 user_id:InputUser id:Vector<int> = Bool;
 stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList;
 stories.getStoriesViews#9a75d6a6 id:Vector<int> = stories.StoryViews;
 stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink;
+stories.report#c95be06a user_id:InputUser id:Vector<int> reason:ReportReason message:string = Bool;
diff --git a/Telegram/SourceFiles/ui/boxes/report_box.cpp b/Telegram/SourceFiles/ui/boxes/report_box.cpp
index 97fd61120..fa0ac22ff 100644
--- a/Telegram/SourceFiles/ui/boxes/report_box.cpp
+++ b/Telegram/SourceFiles/ui/boxes/report_box.cpp
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/input_fields.h"
 #include "ui/toast/toast.h"
 #include "info/profile/info_profile_icon.h"
+#include "styles/style_chat_helpers.h"
 #include "styles/style_layers.h"
 #include "styles/style_boxes.h"
 #include "styles/style_profile.h"
@@ -32,6 +33,7 @@ using Reason = ReportReason;
 
 void ReportReasonBox(
 		not_null<GenericBox*> box,
+		const style::ReportBox &st,
 		ReportSource source,
 		Fn<void(Reason)> done) {
 	box->setTitle([&] {
@@ -50,28 +52,27 @@ void ReportReasonBox(
 			return tr::lng_report_channel_photo_title();
 		case Source::ChannelVideo:
 			return tr::lng_report_channel_video_title();
+		case Source::Story:
+			return tr::lng_report_story();
 		}
 		Unexpected("'source' in ReportReasonBox.");
 	}());
-	const auto isProfileSource = (source == Source::ProfilePhoto)
-		|| (source == Source::ProfileVideo);
 	auto margin = style::margins{ 0, st::reportReasonTopSkip, 0, 0 };
 	const auto add = [&](
 			Reason reason,
 			tr::phrase<> text,
 			const style::icon &icon) {
-		const auto &st = st::reportReasonButton;
 		const auto layout = box->verticalLayout();
 		const auto button = layout->add(
-			object_ptr<Ui::SettingsButton>(layout.get(), text(), st),
+			object_ptr<Ui::SettingsButton>(layout.get(), text(), st.button),
 			margin);
 		margin = {};
 		button->setClickedCallback([=] {
 			done(reason);
 		});
-		const auto height = st.padding.top()
-			+ st.height
-			+ st.padding.bottom();
+		const auto height = st.button.padding.top()
+			+ st.button.height
+			+ st.button.padding.bottom();
 		object_ptr<Info::Profile::FloatingIcon>(
 			button,
 			icon,
@@ -80,49 +81,52 @@ void ReportReasonBox(
 				(height - icon.height()) / 2,
 			});
 	};
-	add(Reason::Spam, tr::lng_report_reason_spam, st::menuIconDelete);
-	if (source != Source::Message && !isProfileSource) {
-		add(Reason::Fake, tr::lng_report_reason_fake, st::menuIconFake);
+	add(Reason::Spam, tr::lng_report_reason_spam, st.spam);
+	if (source == Source::Channel
+		|| source == Source::Group
+		|| source == Source::Bot) {
+		add(Reason::Fake, tr::lng_report_reason_fake, st.fake);
 	}
 	add(
 		Reason::Violence,
 		tr::lng_report_reason_violence,
-		st::menuIconViolence);
+		st.violence);
 	add(
 		Reason::ChildAbuse,
 		tr::lng_report_reason_child_abuse,
-		st::menuIconBlock);
+		st.children);
 	add(
 		Reason::Pornography,
 		tr::lng_report_reason_pornography,
-		st::menuIconPorn);
+		st.pornography);
 	add(
 		Reason::Copyright,
 		tr::lng_report_reason_copyright,
-		st::menuIconCopyright);
-	if (source == Source::Message) {
+		st.copyright);
+	if (source == Source::Message || source == Source::Story) {
 		add(
 			Reason::IllegalDrugs,
 			tr::lng_report_reason_illegal_drugs,
-			st::menuIconDrugs);
+			st.drugs);
 		add(
 			Reason::PersonalDetails,
 			tr::lng_report_reason_personal_details,
-			st::menuIconPersonal);
+			st.personal);
 	}
-	add(Reason::Other, tr::lng_report_reason_other, st::menuIconReport);
+	add(Reason::Other, tr::lng_report_reason_other, st.other);
 
 	box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
 }
 
 void ReportDetailsBox(
 		not_null<GenericBox*> box,
+		const style::ReportBox &st,
 		Fn<void(QString)> done) {
 	box->addRow(
 		object_ptr<FlatLabel>(
 			box, // #TODO reports
 			tr::lng_report_details_about(),
-			st::boxLabel),
+			st.label),
 		{
 			st::boxRowPadding.left(),
 			st::boxPadding.top(),
@@ -131,7 +135,7 @@ void ReportDetailsBox(
 	const auto details = box->addRow(
 		object_ptr<InputField>(
 			box,
-			st::newGroupDescription,
+			st.field,
 			InputField::Mode::MultiLine,
 			tr::lng_report_details(),
 			QString()));
diff --git a/Telegram/SourceFiles/ui/boxes/report_box.h b/Telegram/SourceFiles/ui/boxes/report_box.h
index 61d17e612..fb462bad5 100644
--- a/Telegram/SourceFiles/ui/boxes/report_box.h
+++ b/Telegram/SourceFiles/ui/boxes/report_box.h
@@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+namespace style {
+struct ReportBox;
+} // namespace style
+
 namespace Ui {
 
 class GenericBox;
@@ -22,6 +26,7 @@ enum class ReportSource {
 	GroupVideo,
 	ChannelPhoto,
 	ChannelVideo,
+	Story,
 };
 
 enum class ReportReason {
@@ -38,11 +43,13 @@ enum class ReportReason {
 
 void ReportReasonBox(
 	not_null<GenericBox*> box,
+	const style::ReportBox &st,
 	ReportSource source,
 	Fn<void(ReportReason)> done);
 
 void ReportDetailsBox(
 	not_null<GenericBox*> box,
+	const style::ReportBox &st,
 	Fn<void(QString)> done);
 
 } // namespace Ui

From 22b6f27f7bc37a258c01fdbae998bc26ee04be5d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 23 Jun 2023 13:23:50 +0400
Subject: [PATCH 098/259] Put changelog stories above premium stories.

---
 Telegram/SourceFiles/data/data_stories.cpp | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 1ebd7a1ed..8b27104f4 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -973,10 +973,12 @@ void Stories::removeDependencyStory(not_null<Story*> story) {
 void Stories::sort(StorySourcesList list) {
 	const auto index = static_cast<int>(list);
 	auto &sources = _sources[index];
-	const auto self = _owner->session().user()->id;
+	const auto self = _owner->session().userPeerId();
+	const auto changelogSenderId = UserData::kServiceNotificationsId;
 	const auto proj = [&](const StoriesSourceInfo &info) {
 		const auto key = int64(info.last)
-			+ (info.premium ? (int64(1) << 48) : 0)
+			+ (info.premium ? (int64(1) << 47) : 0)
+			+ ((info.id == changelogSenderId) ? (int64(1) << 47) : 0)
 			+ (info.unread ? (int64(1) << 49) : 0)
 			+ ((info.id == self) ? (int64(1) << 50) : 0);
 		return std::make_pair(key, info.id);

From 1b581a1597a6e7ba3144ac0ea6f47b0fca52963e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 23 Jun 2023 16:04:53 +0400
Subject: [PATCH 099/259] Support story link previews.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 Telegram/SourceFiles/apiwrap.cpp              |  2 +-
 .../SourceFiles/data/data_media_types.cpp     | 13 +++--
 Telegram/SourceFiles/data/data_session.cpp    | 51 +++++++++++++++++--
 Telegram/SourceFiles/data/data_session.h      |  1 +
 Telegram/SourceFiles/data/data_stories.cpp    | 11 ++++
 Telegram/SourceFiles/data/data_stories.h      |  1 +
 Telegram/SourceFiles/data/data_web_page.cpp   |  5 ++
 Telegram/SourceFiles/data/data_web_page.h     |  3 ++
 .../history/view/history_view_view_button.cpp |  4 ++
 .../view/media/history_view_web_page.cpp      |  1 +
 11 files changed, 85 insertions(+), 8 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 091cf0610..a562c678a 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3702,6 +3702,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_view_button_background" = "View background";
 "lng_view_button_theme" = "View theme";
 "lng_view_button_message" = "View message";
+"lng_view_button_story" = "View story";
 "lng_view_button_voice_chat" = "Voice chat";
 "lng_view_button_voice_chat_channel" = "Live stream";
 "lng_view_button_request_join" = "Request to Join";
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index ea70c516b..c906e9c1e 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -779,7 +779,7 @@ QString ApiWrap::exportDirectStoryLink(not_null<Data::Story*> story) {
 	const auto fallback = [&] {
 		const auto base = user->username();
 		const auto story = QString::number(storyId.story);
-		const auto query = base + "/s" + story;
+		const auto query = base + "/s/" + story;
 		return session().createInternalLinkFull(query);
 	};
 	const auto i = _unlikelyStoryLinks.find(storyId);
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 8cdaebc25..9e55643b0 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -1982,11 +1982,16 @@ MediaStory::MediaStory(not_null<HistoryItem*> parent, FullStoryId storyId)
 : Media(parent)
 , _storyId(storyId) {
 	const auto stories = &parent->history()->owner().stories();
-	const auto maybeStory = stories->lookup(storyId);
-	if (!maybeStory) {
+	if (const auto maybeStory = stories->lookup(storyId)) {
+		parent->setText((*maybeStory)->caption());
+	} else {
 		if (maybeStory.error() == NoStory::Unknown) {
 			stories->resolve(storyId, crl::guard(this, [=] {
-				_expired = !stories->lookup(storyId);
+				if (const auto maybeStory = stories->lookup(storyId)) {
+					parent->setText((*maybeStory)->caption());
+				} else {
+					_expired = true;
+				}
 				parent->history()->owner().requestItemViewRefresh(parent);
 			}));
 		} else {
@@ -2052,6 +2057,7 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
 	const auto stories = &parent()->history()->owner().stories();
 	const auto maybeStory = stories->lookup(_storyId);
 	if (!maybeStory) {
+		realParent->setText(TextWithEntities());
 		if (maybeStory.error() == Data::NoStory::Deleted) {
 			_expired = true;
 			return nullptr;
@@ -2065,6 +2071,7 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
 	}
 	_expired = false;
 	const auto story = *maybeStory;
+	realParent->setText(story->caption());
 	if (const auto photo = story->photo()) {
 		return std::make_unique<HistoryView::Photo>(
 			message,
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index ebc7ef8ee..2023c9b67 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -3215,6 +3215,7 @@ not_null<WebPageData*> Session::processWebpage(const MTPDwebPagePending &data) {
 		QString(),
 		QString(),
 		TextWithEntities(),
+		FullStoryId(),
 		nullptr,
 		nullptr,
 		WebPageCollage(),
@@ -3269,6 +3270,7 @@ not_null<WebPageData*> Session::webpage(
 		siteName,
 		title,
 		description,
+		FullStoryId(),
 		photo,
 		document,
 		std::move(collage),
@@ -3321,16 +3323,55 @@ void Session::webpageApplyFields(
 		}
 		return nullptr;
 	};
+	auto story = (Data::Story*)nullptr;
+	auto storyId = FullStoryId();
+	if (const auto attributes = data.vattributes()) {
+		for (const auto &attribute : attributes->v) {
+			attribute.match([&](const MTPDwebPageAttributeStory &data) {
+				storyId = FullStoryId{
+					peerFromUser(data.vuser_id()),
+					data.vid().v,
+				};
+				if (const auto embed = data.vstory()) {
+					story = stories().applyFromWebpage(
+						peerFromUser(data.vuser_id()),
+						*embed);
+				} else if (const auto maybe = stories().lookup(storyId)) {
+					story = *maybe;
+				} else if (maybe.error() == Data::NoStory::Unknown) {
+					stories().resolve(storyId, [=] {
+						if (const auto maybe = stories().lookup(storyId)) {
+							const auto story = *maybe;
+							page->document = story->document();
+							page->photo = story->photo();
+							page->description = story->caption();
+							page->type = WebPageType::Story;
+							notifyWebPageUpdateDelayed(page);
+						}
+					});
+				}
+			}, [](const auto &) {});
+		}
+	}
 	webpageApplyFields(
 		page,
-		ParseWebPageType(data),
+		(story ? WebPageType::Story : ParseWebPageType(data)),
 		qs(data.vurl()),
 		qs(data.vdisplay_url()),
 		siteName,
 		qs(data.vtitle().value_or_empty()),
-		description,
-		photo ? processPhoto(*photo).get() : nullptr,
-		document ? processDocument(*document).get() : lookupThemeDocument(),
+		(story ? story->caption() : description),
+		storyId,
+		(story
+			? story->photo()
+			: photo
+			? processPhoto(*photo).get()
+			: nullptr),
+		(story
+			? story->document()
+			: document
+			? processDocument(*document).get()
+			: lookupThemeDocument()),
 		WebPageCollage(this, data),
 		data.vduration().value_or_empty(),
 		qs(data.vauthor().value_or_empty()),
@@ -3345,6 +3386,7 @@ void Session::webpageApplyFields(
 		const QString &siteName,
 		const QString &title,
 		const TextWithEntities &description,
+		FullStoryId storyId,
 		PhotoData *photo,
 		DocumentData *document,
 		WebPageCollage &&collage,
@@ -3359,6 +3401,7 @@ void Session::webpageApplyFields(
 		siteName,
 		title,
 		description,
+		storyId,
 		photo,
 		document,
 		std::move(collage),
diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h
index 154b1b73f..c287713d3 100644
--- a/Telegram/SourceFiles/data/data_session.h
+++ b/Telegram/SourceFiles/data/data_session.h
@@ -811,6 +811,7 @@ private:
 		const QString &siteName,
 		const QString &title,
 		const TextWithEntities &description,
+		FullStoryId storyId,
 		PhotoData *photo,
 		DocumentData *document,
 		WebPageCollage &&collage,
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 8b27104f4..8ac462e05 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -455,6 +455,17 @@ void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {
 	}
 }
 
+Story *Stories::applyFromWebpage(PeerId peerId, const MTPstoryItem &story) {
+	const auto idDates = parseAndApply(
+		_owner->peer(peerId),
+		story,
+		base::unixtime::now());
+	const auto value = idDates
+		? lookup({ peerId, idDates.id })
+		: base::make_unexpected(NoStory::Deleted);
+	return value ? value->get() : nullptr;
+}
+
 void Stories::requestUserStories(not_null<UserData*> user) {
 	if (!_requestingUserStories.emplace(user).second) {
 		return;
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 3199ff82f..2cd0dcccf 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -244,6 +244,7 @@ public:
 	void loadMore(StorySourcesList list);
 	void apply(const MTPDupdateStory &data);
 	void apply(not_null<PeerData*> peer, const MTPUserStories *data);
+	Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story);
 	void loadAround(FullStoryId id, StoriesContext context);
 
 	const StoriesSource *source(PeerId id) const;
diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp
index 57b31d0f6..4a7b036e3 100644
--- a/Telegram/SourceFiles/data/data_web_page.cpp
+++ b/Telegram/SourceFiles/data/data_web_page.cpp
@@ -152,6 +152,8 @@ WebPageType ParseWebPageType(
 		return WebPageType::WallPaper;
 	} else if (type == u"telegram_theme"_q) {
 		return WebPageType::Theme;
+	} else if (type == u"telegram_story"_q) {
+		return WebPageType::Story;
 	} else if (type == u"telegram_channel"_q) {
 		return WebPageType::Channel;
 	} else if (type == u"telegram_channel_request"_q) {
@@ -214,6 +216,7 @@ bool WebPageData::applyChanges(
 		const QString &newSiteName,
 		const QString &newTitle,
 		const TextWithEntities &newDescription,
+		FullStoryId newStoryId,
 		PhotoData *newPhoto,
 		DocumentData *newDocument,
 		WebPageCollage &&newCollage,
@@ -254,6 +257,7 @@ bool WebPageData::applyChanges(
 		&& siteName == resultSiteName
 		&& title == resultTitle
 		&& description.text == newDescription.text
+		&& storyId == newStoryId
 		&& photo == newPhoto
 		&& document == newDocument
 		&& collage.items == newCollage.items
@@ -271,6 +275,7 @@ bool WebPageData::applyChanges(
 	siteName = resultSiteName;
 	title = resultTitle;
 	description = newDescription;
+	storyId = newStoryId;
 	photo = newPhoto;
 	document = newDocument;
 	collage = std::move(newCollage);
diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h
index 8481a7b3b..83e7bde80 100644
--- a/Telegram/SourceFiles/data/data_web_page.h
+++ b/Telegram/SourceFiles/data/data_web_page.h
@@ -35,6 +35,7 @@ enum class WebPageType {
 
 	WallPaper,
 	Theme,
+	Story,
 
 	Article,
 	ArticleWithIV,
@@ -70,6 +71,7 @@ struct WebPageData {
 		const QString &newSiteName,
 		const QString &newTitle,
 		const TextWithEntities &newDescription,
+		FullStoryId newStoryId,
 		PhotoData *newPhoto,
 		DocumentData *newDocument,
 		WebPageCollage &&newCollage,
@@ -89,6 +91,7 @@ struct WebPageData {
 	QString siteName;
 	QString title;
 	TextWithEntities description;
+	FullStoryId storyId;
 	int duration = 0;
 	QString author;
 	PhotoData *photo = nullptr;
diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp
index 1a2db4d6b..d7d406f4b 100644
--- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp
@@ -52,6 +52,8 @@ inline auto WebPageToPhrase(not_null<WebPageData*> webpage) {
 	const auto type = webpage->type;
 	return Ui::Text::Upper((type == WebPageType::Theme)
 		? tr::lng_view_button_theme(tr::now)
+		: (type == WebPageType::Story)
+		? tr::lng_view_button_story(tr::now)
 		: (type == WebPageType::Message)
 		? tr::lng_view_button_message(tr::now)
 		: (type == WebPageType::Group)
@@ -139,6 +141,8 @@ bool ViewButton::MediaHasViewButton(
 		|| ((type == WebPageType::Theme)
 			&& webpage->document
 			&& webpage->document->isTheme())
+		|| ((type == WebPageType::Story)
+			&& (webpage->photo || webpage->document))
 		|| ((type == WebPageType::WallPaper)
 			&& webpage->document
 			&& webpage->document->isWallPaper());
diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp
index 3b909485f..cec353017 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp
@@ -168,6 +168,7 @@ QSize WebPage::countOptimalSize() {
 		&& _data->photo
 		&& _data->type != WebPageType::Photo
 		&& _data->type != WebPageType::Document
+		&& _data->type != WebPageType::Story
 		&& _data->type != WebPageType::Video) {
 		if (_data->type == WebPageType::Profile) {
 			_asArticle = true;

From 859636ff9c0dfa2b5ce2073dd61e7b1bff4ac7ff Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 23 Jun 2023 18:46:06 +0400
Subject: [PATCH 100/259] Show "Expired story" in the reply bar.

---
 Telegram/Resources/langs/lang.strings          |  1 +
 Telegram/SourceFiles/data/data_media_types.cpp | 10 ++++++++--
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index a562c678a..281718c8f 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1980,6 +1980,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_in_dlg_sticker_emoji" = "{emoji} Sticker";
 "lng_in_dlg_poll" = "Poll";
 "lng_in_dlg_story" = "Story";
+"lng_in_dlg_story_expired" = "Expired 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_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 9e55643b0..1a80583c8 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -2016,7 +2016,11 @@ TextWithEntities MediaStory::notificationText() const {
 	const auto stories = &parent()->history()->owner().stories();
 	const auto maybeStory = stories->lookup(_storyId);
 	return WithCaptionNotificationText(
-		tr::lng_in_dlg_story(tr::now),
+		((_expired
+			|| (!maybeStory
+				&& maybeStory.error() == Data::NoStory::Deleted))
+			? tr::lng_in_dlg_story_expired
+			: tr::lng_in_dlg_story)(tr::now),
 		(maybeStory
 			? (*maybeStory)->caption()
 			: TextWithEntities()));
@@ -2028,7 +2032,9 @@ QString MediaStory::pinnedTextSubstring() const {
 
 TextForMimeData MediaStory::clipboardText() const {
 	return WithCaptionClipboardText(
-		tr::lng_in_dlg_story(tr::now),
+		(_expired
+			? tr::lng_in_dlg_story_expired
+			: tr::lng_in_dlg_story)(tr::now),
 		parent()->clipboardText());
 }
 

From 7ad5520b82d0447cb97cea66e2e612c8315bf76f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 23 Jun 2023 19:36:40 +0400
Subject: [PATCH 101/259] Support external links sponsored messages.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 .../data/data_sponsored_messages.cpp          | 51 +++++++++++++------
 .../data/data_sponsored_messages.h            |  3 ++
 Telegram/SourceFiles/history/history_item.cpp |  4 +-
 .../history/history_item_components.h         |  1 +
 .../history/view/history_view_view_button.cpp | 20 +++++++-
 6 files changed, 62 insertions(+), 18 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 281718c8f..425dc2843 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3707,6 +3707,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_view_button_voice_chat" = "Voice chat";
 "lng_view_button_voice_chat_channel" = "Live stream";
 "lng_view_button_request_join" = "Request to Join";
+"lng_view_button_external_link" = "Open link";
 
 "lng_sponsored_hide_ads" = "Hide";
 "lng_sponsored_title" = "What are sponsored messages?";
diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp
index 9f4f992ed..c586b219e 100644
--- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp
+++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp
@@ -270,28 +270,45 @@ void SponsoredMessages::append(
 			.isForceUserpicDisplay = data.is_show_peer_photo(),
 		};
 	};
+	const auto externalLink = data.vwebpage()
+		? qs(data.vwebpage()->data().vurl())
+		: QString();
+	const auto userpicFromPhoto = [&](const MTPphoto &photo) {
+		return photo.match([&](const MTPDphoto &data) {
+			for (const auto &size : data.vsizes().v) {
+				const auto result = Images::FromPhotoSize(
+					_session,
+					data,
+					size);
+				if (result.location.valid()) {
+					return result;
+				}
+			}
+			return ImageWithLocation{};
+		}, [](const MTPDphotoEmpty &) {
+			return ImageWithLocation{};
+		});
+	};
 	const auto from = [&]() -> SponsoredFrom {
-		if (data.vfrom_id()) {
+		if (const auto webpage = data.vwebpage()) {
+			const auto &data = webpage->data();
+			auto userpic = data.vphoto()
+				? userpicFromPhoto(*data.vphoto())
+				: ImageWithLocation{};
+			return SponsoredFrom{
+				.title = qs(data.vsite_name()),
+				.isExternalLink = true,
+				.userpic = std::move(userpic),
+				.isForceUserpicDisplay = message.data().is_show_peer_photo(),
+			};
+		} else if (const auto fromId = data.vfrom_id()) {
 			return makeFrom(
-				_session->data().peer(peerFromMTP(*data.vfrom_id())),
+				_session->data().peer(peerFromMTP(*fromId)),
 				(data.vchannel_post() != nullptr));
 		}
 		Assert(data.vchat_invite());
 		return data.vchat_invite()->match([&](const MTPDchatInvite &data) {
-			auto userpic = data.vphoto().match([&](const MTPDphoto &data) {
-				for (const auto &size : data.vsizes().v) {
-					const auto result = Images::FromPhotoSize(
-						_session,
-						data,
-						size);
-					if (result.location.valid()) {
-						return result;
-					}
-				}
-				return ImageWithLocation{};
-			}, [](const MTPDphotoEmpty &) {
-				return ImageWithLocation{};
-			});
+			auto userpic = userpicFromPhoto(data.vphoto());
 			return SponsoredFrom{
 				.title = qs(data.vtitle()),
 				.isBroadcast = data.is_broadcast(),
@@ -336,6 +353,7 @@ void SponsoredMessages::append(
 		.history = history,
 		.msgId = data.vchannel_post().value_or_empty(),
 		.chatInviteHash = hash,
+		.externalLink = externalLink,
 		.sponsorInfo = std::move(sponsorInfo),
 		.additionalInfo = std::move(additionalInfo),
 	};
@@ -423,6 +441,7 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
 		.peer = data.from.peer,
 		.msgId = data.msgId,
 		.info = std::move(info),
+		.externalLink = data.externalLink,
 	};
 }
 
diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.h b/Telegram/SourceFiles/data/data_sponsored_messages.h
index 7abbe2ea3..9c309ade3 100644
--- a/Telegram/SourceFiles/data/data_sponsored_messages.h
+++ b/Telegram/SourceFiles/data/data_sponsored_messages.h
@@ -31,6 +31,7 @@ struct SponsoredFrom {
 	bool isBot = false;
 	bool isExactPost = false;
 	bool isRecommended = false;
+	bool isExternalLink = false;
 	ImageWithLocation userpic;
 	bool isForceUserpicDisplay = false;
 };
@@ -42,6 +43,7 @@ struct SponsoredMessage {
 	History *history = nullptr;
 	MsgId msgId;
 	QString chatInviteHash;
+	QString externalLink;
 	TextWithEntities sponsorInfo;
 	TextWithEntities additionalInfo;
 };
@@ -58,6 +60,7 @@ public:
 		PeerData *peer = nullptr;
 		MsgId msgId;
 		std::vector<TextWithEntities> info;
+		QString externalLink;
 	};
 	using RandomId = QByteArray;
 	explicit SponsoredMessages(not_null<Session*> owner);
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index e2bee8c06..039a7bc8d 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -3201,7 +3201,9 @@ void HistoryItem::setSponsoredFrom(const Data::SponsoredFrom &from) {
 	}
 
 	using Type = HistoryMessageSponsored::Type;
-	sponsored->type = from.isExactPost
+	sponsored->type = from.isExternalLink
+		? Type::ExternalLink
+		: from.isExactPost
 		? Type::Post
 		: from.isBot
 		? Type::Bot
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 57d38514e..5a8db07c9 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -139,6 +139,7 @@ struct HistoryMessageSponsored : public RuntimeComponent<HistoryMessageSponsored
 		Broadcast,
 		Post,
 		Bot,
+		ExternalLink,
 	};
 	std::unique_ptr<HiddenSenderInfo> sender;
 	Type type = Type::User;
diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp
index d7d406f4b..71b258ed9 100644
--- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "api/api_chat_invite.h"
 #include "core/application.h"
 #include "core/click_handler_types.h"
+#include "core/file_utilities.h"
 #include "data/data_cloud_themes.h"
 #include "data/data_session.h"
 #include "data/data_sponsored_messages.h"
@@ -42,6 +43,8 @@ inline auto SponsoredPhrase(SponsoredType type) {
 		case SponsoredType::Broadcast: return tr::lng_view_button_channel;
 		case SponsoredType::Post: return tr::lng_view_button_message;
 		case SponsoredType::Bot: return tr::lng_view_button_bot;
+		case SponsoredType::ExternalLink:
+			return tr::lng_view_button_external_link;
 		}
 		Unexpected("SponsoredType in SponsoredPhrase.");
 	}();
@@ -115,6 +118,7 @@ struct ViewButton::Inner {
 	const ClickHandlerPtr link;
 	const Fn<void()> updateCallback;
 	bool belowInfo = true;
+	bool externalLink = false;
 	int lastWidth = 0;
 	QPoint lastPoint;
 	std::unique_ptr<Ui::RippleAnimation> ripple;
@@ -158,7 +162,9 @@ ViewButton::Inner::Inner(
 		const auto &data = controller->session().data();
 		const auto itemId = my.itemId;
 		const auto details = data.sponsoredMessages().lookupDetails(itemId);
-		if (details.hash) {
+		if (!details.externalLink.isEmpty()) {
+			File::OpenUrl(details.externalLink);
+		} else if (details.hash) {
 			Api::CheckChatInvite(controller, *details.hash);
 		} else if (details.peer) {
 			controller->showPeerHistory(
@@ -169,6 +175,7 @@ ViewButton::Inner::Inner(
 	}
 }))
 , updateCallback(std::move(updateCallback))
+, externalLink(sponsored->type == SponsoredType::ExternalLink)
 , text(st::historyViewButtonTextStyle, SponsoredPhrase(sponsored->type)) {
 }
 
@@ -260,6 +267,17 @@ void ViewButton::draw(
 			r.width(),
 			1,
 			style::al_center);
+
+		if (_inner->externalLink) {
+			const auto &icon = st::msgBotKbUrlIcon;
+			const auto padding = st::msgBotKbIconPadding;
+			icon.paint(
+				p,
+				r.left() + r.width() - icon.width() - padding,
+				r.top() + padding,
+				r.width(),
+				stm->fwdTextPalette.linkFg->c);
+		}
 	}
 	p.restore();
 	if (_inner->lastWidth != r.width()) {

From 3c44cf0251c37bd24fa2ab2de005473e5c4d3c10 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 23 Jun 2023 20:05:58 +0400
Subject: [PATCH 102/259] Track all sponsored messages clicks.

---
 Telegram/SourceFiles/core/ui_integration.cpp       | 12 ++++++++++++
 Telegram/SourceFiles/core/ui_integration.h         |  3 +++
 .../SourceFiles/data/data_sponsored_messages.cpp   | 14 ++++++++++++++
 .../SourceFiles/data/data_sponsored_messages.h     |  1 +
 .../history/view/history_view_element.cpp          |  5 ++++-
 Telegram/lib_ui                                    |  2 +-
 6 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp
index d0d7580cc..e7351e8fd 100644
--- a/Telegram/SourceFiles/core/ui_integration.cpp
+++ b/Telegram/SourceFiles/core/ui_integration.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/click_handler_types.h"
 #include "data/stickers/data_custom_emoji.h"
 #include "data/data_session.h"
+#include "data/data_sponsored_messages.h"
 #include "ui/text/text_custom_emoji.h"
 #include "ui/basic_click_handlers.h"
 #include "ui/emoji_config.h"
@@ -26,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "main/main_app_config.h"
 #include "mtproto/mtproto_config.h"
 #include "window/window_controller.h"
+#include "window/window_session_controller.h"
 #include "mainwindow.h"
 
 namespace Core {
@@ -264,6 +266,16 @@ Fn<void()> UiIntegration::createSpoilerRepaint(const std::any &context) {
 	return my ? my->customEmojiRepaint : nullptr;
 }
 
+bool UiIntegration::allowClickHandlerActivation(
+		const std::shared_ptr<ClickHandler> &handler,
+		const ClickContext &context) {
+	const auto my = context.other.value<ClickHandlerContext>();
+	if (const auto window = my.sessionWindow.get()) {
+		window->session().data().sponsoredMessages().clicked(my.itemId);
+	}
+	return true;
+}
+
 rpl::producer<> UiIntegration::forcePopupMenuHideRequests() {
 	return Core::App().passcodeLockChanges() | rpl::to_empty;
 }
diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h
index a71c9cfbe..65661ecfd 100644
--- a/Telegram/SourceFiles/core/ui_integration.h
+++ b/Telegram/SourceFiles/core/ui_integration.h
@@ -60,6 +60,9 @@ public:
 		const QString &data,
 		const std::any &context) override;
 	Fn<void()> createSpoilerRepaint(const std::any &context) override;
+	bool allowClickHandlerActivation(
+		const std::shared_ptr<ClickHandler> &handler,
+		const ClickContext &context) override;
 
 	QString phraseContextCopyText() override;
 	QString phraseContextCopyEmail() override;
diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp
index c586b219e..bbf6273c5 100644
--- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp
+++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp
@@ -445,6 +445,20 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
 	};
 }
 
+void SponsoredMessages::clicked(const FullMsgId &fullId) {
+	const auto entryPtr = find(fullId);
+	if (!entryPtr) {
+		return;
+	}
+	const auto randomId = entryPtr->sponsored.randomId;
+	const auto channel = entryPtr->item->history()->peer->asChannel();
+	Assert(channel != nullptr);
+	_session->api().request(MTPchannels_ClickSponsoredMessage(
+		channel->inputChannel,
+		MTP_bytes(randomId)
+	)).send();
+}
+
 SponsoredMessages::State SponsoredMessages::state(
 		not_null<History*> history) const {
 	const auto it = _data.find(history);
diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.h b/Telegram/SourceFiles/data/data_sponsored_messages.h
index 9c309ade3..e9cd46b45 100644
--- a/Telegram/SourceFiles/data/data_sponsored_messages.h
+++ b/Telegram/SourceFiles/data/data_sponsored_messages.h
@@ -72,6 +72,7 @@ public:
 	void request(not_null<History*> history, Fn<void()> done);
 	void clearItems(not_null<History*> history);
 	[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
+	void clicked(const FullMsgId &fullId);
 
 	[[nodiscard]] bool append(not_null<History*> history);
 	void inject(
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index 763978823..6a3f78b8a 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/application.h"
 #include "core/core_settings.h"
 #include "core/click_handler_types.h"
+#include "core/file_utilities.h"
 #include "core/ui_integration.h"
 #include "main/main_session.h"
 #include "main/main_domain.h"
@@ -992,7 +993,9 @@ ClickHandlerPtr Element::fromLink() const {
 				auto &sponsored = session->data().sponsoredMessages();
 				const auto itemId = my.itemId ? my.itemId : item->fullId();
 				const auto details = sponsored.lookupDetails(itemId);
-				if (const auto &hash = details.hash) {
+				if (!details.externalLink.isEmpty()) {
+					File::OpenUrl(details.externalLink);
+				} else if (const auto &hash = details.hash) {
 					Api::CheckChatInvite(window, *hash);
 				} else if (const auto peer = details.peer) {
 					window->showPeerInfo(peer);
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 8a5604880..fc8d4d25d 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 8a560488011f833b7ba534999f878d20d5152e6c
+Subproject commit fc8d4d25dee6e3ded99749c06fd870e6df4615f8

From 01837905185baf4ce764083d39c6522625154de8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 23 Jun 2023 20:39:57 +0400
Subject: [PATCH 103/259] Improve archive / unarchive story toast.

---
 Telegram/Resources/langs/lang.strings      |  4 ++--
 Telegram/SourceFiles/data/data_stories.cpp | 12 +++++++++---
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 425dc2843..f0dddd290 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3826,8 +3826,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_archive_title" = "Stories Archive";
 "lng_stories_archive_about" = "Only you can see archived stories unless you choose to save them to your profile.";
 "lng_stories_reply_sent" = "Message Sent";
-"lng_stories_hidden_to_contacts" = "Those stories are now shown only in your Contacts list.";
-"lng_stories_shown_in_chats" = "Those stories are now shown in your Chats list.";
+"lng_stories_hidden_to_contacts" = "Stories from {user} will now be shown in Contacts, not Chats.";
+"lng_stories_shown_in_chats" = "Stories from {user} will now be shown in Chats, not Contacts.";
 "lng_stories_delete_one_sure" = "Are you sure you want to delete this story?";
 "lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?";
 "lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?";
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 8ac462e05..30d7fbc8f 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -1197,11 +1197,17 @@ void Stories::toggleHidden(
 		)).send();
 	}
 
+	const auto name = user->shortName();
 	const auto guard = gsl::finally([&] {
 		if (show) {
-			show->showToast(hidden
-				? tr::lng_stories_hidden_to_contacts(tr::now)
-				: tr::lng_stories_shown_in_chats(tr::now));
+			const auto phrase = hidden
+				? tr::lng_stories_hidden_to_contacts
+				: tr::lng_stories_shown_in_chats;
+			show->showToast(phrase(
+				tr::now,
+				lt_user,
+				Ui::Text::Bold(name),
+				Ui::Text::WithEntities));
 		}
 	});
 

From 6eaa192f515b1a81932e0e1c91cb5416d9facb59 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 23 Jun 2023 21:10:08 +0400
Subject: [PATCH 104/259] Don't allow reply to changelog stories.

---
 .../stories/media_stories_controller.cpp      | 33 +-----------
 .../media/stories/media_stories_reply.cpp     | 51 +++++++++++++++++--
 .../media/stories/media_stories_reply.h       |  3 ++
 3 files changed, 53 insertions(+), 34 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index bc7d0eaec..af4abbb4b 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -96,7 +96,6 @@ private:
 
 	const not_null<Controller*> _controller;
 	std::unique_ptr<Ui::RpWidget> _bg;
-	std::unique_ptr<Ui::RpWidget> _reply;
 	std::unique_ptr<Ui::FlatLabel> _text;
 	std::unique_ptr<Ui::RoundButton> _button;
 	Ui::RoundRect _bgRound;
@@ -172,32 +171,9 @@ void Controller::Unsupported::setup(not_null<UserData*> user) {
 		_bgRound.paint(p, _bg->rect());
 	}, _bg->lifetime());
 
-	if (!user->isSelf()) {
-		_reply = std::make_unique<Ui::RpWidget>(wrap);
-		_reply->show();
-		_reply->paintRequest() | rpl::start_with_next([=] {
-			auto p = QPainter(_reply.get());
-			_bgRound.paint(p, _reply->rect());
-
-			p.setPen(st::storiesComposeGrayText);
-			p.setFont(st::normalFont);
-			p.drawText(
-				_reply->rect(),
-				tr::lng_stories_cant_reply(tr::now),
-				style::al_center);
-		}, _reply->lifetime());
-	}
-
 	_controller->layoutValue(
 	) | rpl::start_with_next([=](const Layout &layout) {
 		_bg->setGeometry(layout.content);
-		if (_reply) {
-			const auto height = st::storiesComposeControls.attach.height;
-			const auto position = layout.controlsBottomPosition
-				- QPoint(0, height);
-			_reply->setGeometry(
-				{ position, QSize{ layout.controlsWidth, height } });
-		}
 	}, _bg->lifetime());
 
 	_text = std::make_unique<Ui::FlatLabel>(
@@ -1141,7 +1117,6 @@ void Controller::rebuildCachedSourcesList(
 	} else {
 		// All that go before the current push to front.
 		for (auto before = index; before > 0;) {
-			--before;
 			const auto peerId = lists[--before].id;
 			if (!ranges::contains(_cachedSourcesList, peerId)) {
 				_cachedSourcesList.insert(
@@ -1160,12 +1135,8 @@ void Controller::rebuildCachedSourcesList(
 	}
 
 	Ensures(_cachedSourcesList.size() == lists.size());
-	Ensures(ranges::equal(
-		lists,
-		_cachedSourcesList,
-		ranges::equal_to(),
-		&Data::StoriesSourceInfo::id));
-	Ensures(_cachedSourceIndex >= 0 && _cachedSourceIndex < _cachedSourcesList.size());
+	Ensures(_cachedSourceIndex >= 0
+		&& _cachedSourceIndex < _cachedSourcesList.size());
 }
 
 void Controller::refreshViewsFromData() {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 2929d52c9..d032533b3 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "storage/storage_account.h"
 #include "storage/storage_media_prepare.h"
 #include "ui/chat/attach/attach_prepare.h"
+#include "ui/round_rect.h"
 #include "window/section_widget.h"
 #include "styles/style_boxes.h" // sendMediaPreviewSize.
 #include "styles/style_chat_helpers.h"
@@ -42,6 +43,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace Media::Stories {
 
+class ReplyArea::Cant final : public Ui::RpWidget {
+public:
+	explicit Cant(not_null<QWidget*> parent);
+
+private:
+	void paintEvent(QPaintEvent *e) override;
+
+	Ui::RoundRect _bg;
+
+};
+
+ReplyArea::Cant::Cant(not_null<QWidget*> parent)
+: RpWidget(parent)
+, _bg(st::storiesRadius, st::storiesComposeBg) {
+	show();
+}
+
+void ReplyArea::Cant::paintEvent(QPaintEvent *e) {
+	auto p = QPainter(this);
+	_bg.paint(p, rect());
+
+	p.setPen(st::storiesComposeGrayText);
+	p.setFont(st::normalFont);
+	p.drawText(
+		rect(),
+		tr::lng_stories_cant_reply(tr::now),
+		style::al_center);
+}
+
 ReplyArea::ReplyArea(not_null<Controller*> controller)
 : _controller(controller)
 , _controls(std::make_unique<HistoryView::ComposeControls>(
@@ -599,10 +629,25 @@ void ReplyArea::show(ReplyAreaData data) {
 		.history = history,
 	});
 	_controls->clear();
-	if (!user || user->isSelf()) {
-		_controls->hide();
-	} else {
+	const auto hidden = user && user->isSelf();
+	const auto cant = !user || user->isServiceUser();
+	if (!hidden && !cant) {
 		_controls->show();
+	} else {
+		_controls->hide();
+		if (cant) {
+			_cant = std::make_unique<Cant>(_controller->wrap());
+			_controller->layoutValue(
+			) | rpl::start_with_next([=](const Layout &layout) {
+				const auto height = st::storiesComposeControls.attach.height;
+				const auto position = layout.controlsBottomPosition
+					- QPoint(0, height);
+				_cant->setGeometry(
+					{ position, QSize{ layout.controlsWidth, height } });
+			}, _cant->lifetime());
+		} else {
+			_cant = nullptr;
+		}
 	}
 }
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h
index 5853ffc0e..eeaa54726 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h
@@ -68,6 +68,8 @@ public:
 	[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
 
 private:
+	class Cant;
+
 	using VoiceToSend = HistoryView::Controls::VoiceToSend;
 
 	[[nodiscard]] Main::Session &session() const;
@@ -134,6 +136,7 @@ private:
 
 	const not_null<Controller*> _controller;
 	const std::unique_ptr<HistoryView::ComposeControls> _controls;
+	std::unique_ptr<Cant> _cant;
 
 	ReplyAreaData _data;
 	base::has_weak_ptr _shownUserGuard;

From e7ccf5d8ad8a9812c15bb0331776fb22edf47893 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 26 Jun 2023 12:24:17 +0400
Subject: [PATCH 105/259] Fix possible std::clamp contract violation.

---
 .../media/streaming/media_streaming_utility.cpp            | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
index 5b3ac86dd..fdfbe20f8 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp
@@ -28,7 +28,12 @@ crl::time FramePosition(const Stream &stream) {
 		: (stream.decodedFrame->pts != AV_NOPTS_VALUE)
 		? stream.decodedFrame->pts
 		: stream.decodedFrame->pkt_dts;
-	return FFmpeg::PtsToTime(pts, stream.timeBase);
+	const auto result = FFmpeg::PtsToTime(pts, stream.timeBase);
+
+	// Sometimes the result here may be larger than the stream duration.
+	return (stream.duration == kDurationUnavailable)
+		? result
+		: std::min(result, stream.duration);
 }
 
 FFmpeg::AvErrorWrap ProcessPacket(Stream &stream, FFmpeg::Packet &&packet) {

From a57e9990b0c953117209983641ea22a7ba4a8bec Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 26 Jun 2023 12:24:32 +0400
Subject: [PATCH 106/259] Update API scheme on layer 160.

---
 Telegram/SourceFiles/apiwrap.cpp                   |  6 +++++-
 .../data/notify/data_notify_settings.cpp           | 12 ++++++++++--
 .../data/notify/data_peer_notify_settings.cpp      | 14 +++++++++++---
 Telegram/SourceFiles/mtproto/scheme/api.tl         |  8 ++++----
 4 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index c906e9c1e..3b030e7a7 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -1830,7 +1830,11 @@ void ApiWrap::requestNotifySettings(const MTPInputNotifyPeer &peer) {
 				MTPNotificationSound(),
 				MTPNotificationSound(),
 				MTPNotificationSound(),
-				MTPBool()));
+				MTPBool(),
+				MTPBool(),
+				MTPNotificationSound(),
+				MTPNotificationSound(),
+				MTPNotificationSound()));
 		_notifySettingRequests.erase(key);
 	}).send();
 	_notifySettingRequests.emplace(key, requestId);
diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp
index 3beba423d..1675d1e92 100644
--- a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp
+++ b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp
@@ -195,7 +195,11 @@ void NotifySettings::resetToDefault(not_null<Data::Thread*> thread) {
 		MTPNotificationSound(),
 		MTPNotificationSound(),
 		MTPNotificationSound(),
-		MTPBool());
+		MTPBool(),
+		MTPBool(),
+		MTPNotificationSound(),
+		MTPNotificationSound(),
+		MTPNotificationSound());
 	if (thread->notify().change(empty)) {
 		updateLocal(thread);
 		thread->session().api().updateNotifySettingsDelayed(thread);
@@ -227,7 +231,11 @@ void NotifySettings::resetToDefault(not_null<PeerData*> peer) {
 		MTPNotificationSound(),
 		MTPNotificationSound(),
 		MTPNotificationSound(),
-		MTPBool());
+		MTPBool(),
+		MTPBool(),
+		MTPNotificationSound(),
+		MTPNotificationSound(),
+		MTPNotificationSound());
 	if (peer->notify().change(empty)) {
 		updateLocal(peer);
 		peer->session().api().updateNotifySettingsDelayed(peer);
diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp
index da03d14f2..4b756a611 100644
--- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp
+++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp
@@ -19,7 +19,9 @@ namespace {
 		MTPBool(),
 		MTPint(),
 		MTPNotificationSound(),
-		MTPBool());
+		MTPBool(),
+		MTPBool(),
+		MTPNotificationSound());
 }
 
 [[nodiscard]] NotifySound ParseSound(const MTPNotificationSound &sound) {
@@ -194,7 +196,9 @@ MTPinputPeerNotifySettings NotifyPeerSettingsValue::serialize() const {
 		MTP_bool(_silent.value_or(false)),
 		MTP_int(_mute.value_or(false)),
 		SerializeSound(_sound),
-		MTP_bool(_storiesMuted.value_or(false)));
+		MTP_bool(_storiesMuted.value_or(false)),
+		MTP_bool(false), // stories_hide_sender
+		SerializeSound(std::nullopt)); // stories_sound
 }
 
 PeerNotifySettings::PeerNotifySettings() = default;
@@ -245,7 +249,11 @@ bool PeerNotifySettings::change(
 		MTPNotificationSound(),
 		MTPNotificationSound(),
 		SerializeSound(sound),
-		storiesMuted ? MTP_bool(*storiesMuted) : MTPBool()));
+		storiesMuted ? MTP_bool(*storiesMuted) : MTPBool(),
+		MTPBool(), // stories_hide_sender
+		MTPNotificationSound(),
+		MTPNotificationSound(),
+		SerializeSound(std::nullopt))); // stories_sound
 }
 
 std::optional<TimeId> PeerNotifySettings::muteUntil() const {
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 7509f2c0e..b9197b033 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -201,9 +201,9 @@ inputNotifyChats#4a95e84e = InputNotifyPeer;
 inputNotifyBroadcasts#b1db7c7e = InputNotifyPeer;
 inputNotifyForumTopic#5c467992 peer:InputPeer top_msg_id:int = InputNotifyPeer;
 
-inputPeerNotifySettings#e1e51e85 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound stories_muted:flags.6?Bool = InputPeerNotifySettings;
+inputPeerNotifySettings#cacb6ae2 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_sound:flags.8?NotificationSound = InputPeerNotifySettings;
 
-peerNotifySettings#6cdc6e52 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound stories_muted:flags.6?Bool = PeerNotifySettings;
+peerNotifySettings#99622c0c flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_ios_sound:flags.8?NotificationSound stories_android_sound:flags.9?NotificationSound stories_other_sound:flags.10?NotificationSound = PeerNotifySettings;
 
 peerSettings#a518110d flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int = PeerSettings;
 
@@ -529,7 +529,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#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;
+documentAttributeVideo#d38ff1c2 flags:# round_message:flags.0?true supports_streaming:flags.1?true nosound:flags.3?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;
@@ -1632,7 +1632,7 @@ account.resendPasswordEmail#7a7f2a15 = Bool;
 account.cancelPasswordEmail#c1cbd5b6 = Bool;
 account.getContactSignUpNotification#9f07c728 = Bool;
 account.setContactSignUpNotification#cff43f61 silent:Bool = Bool;
-account.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true peer:flags.0?InputNotifyPeer = Updates;
+account.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true compare_stories:flags.2?true peer:flags.0?InputNotifyPeer = Updates;
 account.getWallPaper#fc8ddbea wallpaper:InputWallPaper = WallPaper;
 account.uploadWallPaper#e39a8f03 flags:# for_chat:flags.0?true file:InputFile mime_type:string settings:WallPaperSettings = WallPaper;
 account.saveWallPaper#6c5a5b37 wallpaper:InputWallPaper unsave:Bool settings:WallPaperSettings = Bool;

From 21543338d78833a26e842870c6461580d691fa86 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 26 Jun 2023 16:44:02 +0400
Subject: [PATCH 107/259] On typing in messages selection focus input field.

---
 Telegram/SourceFiles/history/history_inner_widget.cpp      | 3 +++
 Telegram/SourceFiles/history/history_widget.cpp            | 7 +++++++
 Telegram/SourceFiles/history/history_widget.h              | 2 ++
 .../view/controls/history_view_compose_controls.cpp        | 7 +++++++
 .../history/view/controls/history_view_compose_controls.h  | 1 +
 .../SourceFiles/history/view/history_view_list_widget.cpp  | 3 +++
 .../SourceFiles/history/view/history_view_list_widget.h    | 1 +
 .../history/view/history_view_pinned_section.cpp           | 3 +++
 .../SourceFiles/history/view/history_view_pinned_section.h | 1 +
 .../history/view/history_view_replies_section.cpp          | 4 ++++
 .../history/view/history_view_replies_section.h            | 1 +
 .../history/view/history_view_scheduled_section.cpp        | 4 ++++
 .../history/view/history_view_scheduled_section.h          | 1 +
 13 files changed, 38 insertions(+)

diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 36a1a29e6..857803c17 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -2915,6 +2915,9 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) {
 			&& selectedState.canDeleteCount == selectedState.count) {
 			_widget->confirmDeleteSelected();
 		}
+	} else if (!(e->modifiers() & ~Qt::ShiftModifier)
+		&& e->key() != Qt::Key_Shift) {
+		_widget->tryProcessKeyInput(e);
 	} else {
 		e->ignore();
 	}
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index e99b55c85..aaa9733fe 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -1803,6 +1803,13 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(
 	return false;
 }
 
+void HistoryWidget::tryProcessKeyInput(not_null<QKeyEvent*> e) {
+	if (_canSendTexts && _field->isVisible()) {
+		_field->setFocusFast();
+		QCoreApplication::sendEvent(_field->rawTextEdit(), e);
+	}
+}
+
 void HistoryWidget::setupShortcuts() {
 	Shortcuts::Requests(
 	) | rpl::filter([=] {
diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h
index 46d8287b9..ae1f9bf54 100644
--- a/Telegram/SourceFiles/history/history_widget.h
+++ b/Telegram/SourceFiles/history/history_widget.h
@@ -280,6 +280,8 @@ public:
 
 	bool notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo);
 
+	void tryProcessKeyInput(not_null<QKeyEvent*> e);
+
 	~HistoryWidget();
 
 protected:
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 235091a00..089767c2c 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -2843,6 +2843,13 @@ bool ComposeControls::handleCancelRequest() {
 	return false;
 }
 
+void ComposeControls::tryProcessKeyInput(not_null<QKeyEvent*> e) {
+	if (_field->isVisible()) {
+		_field->setFocusFast();
+		QCoreApplication::sendEvent(_field->rawTextEdit(), e);
+	}
+}
+
 void ComposeControls::initWebpageProcess() {
 	if (!_history) {
 		_preview = nullptr;
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 f8c887593..fa63a8b3d 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
@@ -200,6 +200,7 @@ public:
 	void cancelForward();
 
 	bool handleCancelRequest();
+	void tryProcessKeyInput(not_null<QKeyEvent*> e);
 
 	[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;
 	[[nodiscard]] WebPageId webPageId() const;
diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
index f45f4f62b..9ca933844 100644
--- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
@@ -2460,6 +2460,9 @@ void ListWidget::keyPressEvent(QKeyEvent *e) {
 #endif // Q_OS_MAC
 	} else if (e == QKeySequence::Delete) {
 		_delegate->listDeleteRequest();
+	} else if (!(e->modifiers() & ~Qt::ShiftModifier)
+		&& e->key() != Qt::Key_Shift) {
+		_delegate->listTryProcessKeyInput(e);
 	} else {
 		e->ignore();
 	}
diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h
index ad3517723..704fb1685 100644
--- a/Telegram/SourceFiles/history/view/history_view_list_widget.h
+++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h
@@ -93,6 +93,7 @@ public:
 	virtual bool listScrollTo(int top, bool syntetic = true) = 0;
 	virtual void listCancelRequest() = 0;
 	virtual void listDeleteRequest() = 0;
+	virtual void listTryProcessKeyInput(not_null<QKeyEvent*> e) = 0;
 	virtual rpl::producer<Data::MessagesSlice> listSource(
 		Data::MessagePosition aroundId,
 		int limitBefore,
diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
index 263d057ff..0fbaf0349 100644
--- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
@@ -497,6 +497,9 @@ void PinnedWidget::listDeleteRequest() {
 	confirmDeleteSelected();
 }
 
+void PinnedWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) {
+}
+
 rpl::producer<Data::MessagesSlice> PinnedWidget::listSource(
 		Data::MessagePosition aroundId,
 		int limitBefore,
diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h
index 1ee0a9b7d..7125a1f18 100644
--- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h
+++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h
@@ -82,6 +82,7 @@ public:
 	bool listScrollTo(int top, bool syntetic = true) override;
 	void listCancelRequest() override;
 	void listDeleteRequest() override;
+	void listTryProcessKeyInput(not_null<QKeyEvent*> e) override;
 	rpl::producer<Data::MessagesSlice> listSource(
 		Data::MessagePosition aroundId,
 		int limitBefore,
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index 65c9e503a..7b878ec52 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -2383,6 +2383,10 @@ void RepliesWidget::listDeleteRequest() {
 	confirmDeleteSelected();
 }
 
+void RepliesWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) {
+	_composeControls->tryProcessKeyInput(e);
+}
+
 rpl::producer<Data::MessagesSlice> RepliesWidget::listSource(
 		Data::MessagePosition aroundId,
 		int limitBefore,
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h
index a8645a5c5..b048dca36 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.h
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h
@@ -126,6 +126,7 @@ public:
 	bool listScrollTo(int top, bool syntetic = true) override;
 	void listCancelRequest() override;
 	void listDeleteRequest() override;
+	void listTryProcessKeyInput(not_null<QKeyEvent*> e) override;
 	rpl::producer<Data::MessagesSlice> listSource(
 		Data::MessagePosition aroundId,
 		int limitBefore,
diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
index ca5e7b8bd..602f46ae2 100644
--- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp
@@ -1078,6 +1078,10 @@ void ScheduledWidget::listDeleteRequest() {
 	confirmDeleteSelected();
 }
 
+void ScheduledWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) {
+	_composeControls->tryProcessKeyInput(e);
+}
+
 rpl::producer<Data::MessagesSlice> ScheduledWidget::listSource(
 		Data::MessagePosition aroundId,
 		int limitBefore,
diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h
index acbec32d8..2173d3dcc 100644
--- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h
+++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h
@@ -104,6 +104,7 @@ public:
 	bool listScrollTo(int top, bool syntetic = true) override;
 	void listCancelRequest() override;
 	void listDeleteRequest() override;
+	void listTryProcessKeyInput(not_null<QKeyEvent*> e) override;
 	rpl::producer<Data::MessagesSlice> listSource(
 		Data::MessagePosition aroundId,
 		int limitBefore,

From d567282430ad3bbc4c069729323a4283f51c98f8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 26 Jun 2023 20:22:56 +0400
Subject: [PATCH 108/259] Preload stories in the sources lists.

---
 Telegram/CMakeLists.txt                       |   2 +
 .../boxes/peer_list_controllers.cpp           |  17 +-
 Telegram/SourceFiles/data/data_document.cpp   |  22 +
 Telegram/SourceFiles/data/data_document.h     |   3 +
 .../SourceFiles/data/data_media_types.cpp     |   1 +
 .../SourceFiles/data/data_photo_media.cpp     |  14 +-
 Telegram/SourceFiles/data/data_photo_media.h  |   3 +-
 Telegram/SourceFiles/data/data_session.cpp    |   1 +
 Telegram/SourceFiles/data/data_stories.cpp    | 447 ++++++------------
 Telegram/SourceFiles/data/data_stories.h      | 139 +-----
 .../streaming/media_streaming_reader.cpp      |  39 +-
 .../media/streaming/media_streaming_reader.h  |   3 +
 .../window/window_session_controller.cpp      |  16 +-
 13 files changed, 264 insertions(+), 443 deletions(-)

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 3a3d7e5a7..a4ac996e8 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -553,6 +553,8 @@ PRIVATE
     data/data_stories.h
     data/data_stories_ids.cpp
     data/data_stories_ids.h
+    data/data_story.cpp
+    data/data_story.h
     data/data_streaming.cpp
     data/data_streaming.h
     data/data_thread.cpp
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 1b62876ed..1e104b5a3 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -65,6 +65,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 			Mode mode = ContactsBoxController::SortMode::Online;
 		};
 
+		const auto stories = &sessionController->session().data().stories();
 		const auto state = box->lifetime().make_state<State>();
 		box->addButton(tr::lng_close(), [=] { box->closeBox(); });
 		box->addLeftButton(
@@ -80,17 +81,17 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 		});
 		raw->setSortMode(Mode::Online);
 
-		auto stories = object_ptr<Stories::List>(
+		auto list = object_ptr<Stories::List>(
 			box,
 			st::dialogsStoriesList,
 			Stories::ContentForSession(
 				&sessionController->session(),
 				Data::StorySourcesList::Hidden),
 			[=] { return state->stories->height() - box->scrollTop(); });
-		const auto raw = state->stories = stories.data();
+		const auto raw = state->stories = list.data();
 		box->peerListSetAboveWidget(object_ptr<::Ui::PaddingWrap<>>(
 			box,
-			std::move(stories),
+			std::move(list),
 			style::margins(0, st::membersMarginTop, 0, 0)));
 
 		raw->clicks(
@@ -107,7 +108,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 
 		raw->toggleShown(
 		) | rpl::start_with_next([=](Stories::ToggleShownRequest request) {
-			sessionController->session().data().stories().toggleHidden(
+			stories->toggleHidden(
 				PeerId(int64(request.id)),
 				!request.shown,
 				sessionController->uiShow());
@@ -115,9 +116,13 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 
 		raw->loadMoreRequests(
 		) | rpl::start_with_next([=] {
-			sessionController->session().data().stories().loadMore(
-				Data::StorySourcesList::Hidden);
+			stories->loadMore(Data::StorySourcesList::Hidden);
 		}, raw->lifetime());
+
+		stories->incrementPreloadingHiddenSources();
+		raw->lifetime().add([=] {
+			stories->decrementPreloadingHiddenSources();
+		});
 	};
 	return Box<PeerListBox>(std::move(controller), std::move(init));
 }
diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 473b7027d..173cfbb91 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -335,6 +335,7 @@ void DocumentData::setattributes(
 
 	validateLottieSticker();
 
+	_videoPreloadPrefix = 0;
 	for (const auto &attribute : attributes) {
 		attribute.match([&](const MTPDdocumentAttributeImageSize &data) {
 			dimensions = QSize(data.vw().v, data.vh().v);
@@ -390,6 +391,10 @@ void DocumentData::setattributes(
 					: VideoDocument;
 				if (data.is_round_message()) {
 					_additional = std::make_unique<RoundData>();
+				} else if (const auto size = data.vpreload_prefix_size()) {
+					if (size->v > 0) {
+						_videoPreloadPrefix = size->v;
+					}
 				}
 			} else if (const auto info = sticker()) {
 				info->type = StickerType::Webm;
@@ -1334,6 +1339,23 @@ bool DocumentData::inappPlaybackFailed() const {
 	return (_flags & Flag::StreamingPlaybackFailed);
 }
 
+int DocumentData::videoPreloadPrefix() const {
+	return _videoPreloadPrefix;
+}
+
+StorageFileLocation DocumentData::videoPreloadLocation() const {
+	return hasRemoteLocation()
+		? StorageFileLocation(
+			_dc,
+			session().userId(),
+			MTP_inputDocumentFileLocation(
+				MTP_long(id),
+				MTP_long(_access),
+				MTP_bytes(_fileReference),
+				MTP_string()))
+		: StorageFileLocation();
+}
+
 auto DocumentData::createStreamingLoader(
 	Data::FileOrigin origin,
 	bool forceRemoteLoader) const
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index 617eb331c..ded87abfd 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -274,6 +274,8 @@ public:
 
 	void setInappPlaybackFailed();
 	[[nodiscard]] bool inappPlaybackFailed() const;
+	[[nodiscard]] int videoPreloadPrefix() const;
+	[[nodiscard]] StorageFileLocation videoPreloadLocation() const;
 
 	DocumentId id = 0;
 	int64 size = 0;
@@ -344,6 +346,7 @@ private:
 
 	const not_null<Data::Session*> _owner;
 
+	int _videoPreloadPrefix = 0;
 	// Two types of location: from MTProto by dc+access or from web by url
 	int32 _dc = 0;
 	uint64 _access = 0;
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 1a80583c8..79f99591d 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -57,6 +57,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_channel.h"
 #include "data/data_file_origin.h"
 #include "data/data_stories.h"
+#include "data/data_story.h"
 #include "main/main_session.h"
 #include "main/main_session_settings.h"
 #include "core/application.h"
diff --git a/Telegram/SourceFiles/data/data_photo_media.cpp b/Telegram/SourceFiles/data/data_photo_media.cpp
index 7bbdcdd1e..be713b4a9 100644
--- a/Telegram/SourceFiles/data/data_photo_media.cpp
+++ b/Telegram/SourceFiles/data/data_photo_media.cpp
@@ -166,14 +166,22 @@ bool PhotoMedia::autoLoadThumbnailAllowed(not_null<PeerData*> peer) const {
 }
 
 void PhotoMedia::automaticLoad(
-		Data::FileOrigin origin,
+		FileOrigin origin,
 		const HistoryItem *item) {
-	if (!item || loaded() || _owner->cancelled()) {
+	if (item) {
+		automaticLoad(origin, item->history()->peer);
+	}
+}
+
+void PhotoMedia::automaticLoad(
+		FileOrigin origin,
+		not_null<PeerData*> peer) {
+	if (loaded() || _owner->cancelled()) {
 		return;
 	}
 	const auto loadFromCloud = Data::AutoDownload::Should(
 		_owner->session().settings().autoDownload(),
-		item->history()->peer,
+		peer,
 		_owner);
 	_owner->load(
 		origin,
diff --git a/Telegram/SourceFiles/data/data_photo_media.h b/Telegram/SourceFiles/data/data_photo_media.h
index 7c5c34bd4..f9e3c7708 100644
--- a/Telegram/SourceFiles/data/data_photo_media.h
+++ b/Telegram/SourceFiles/data/data_photo_media.h
@@ -43,7 +43,8 @@ public:
 
 	[[nodiscard]] bool autoLoadThumbnailAllowed(
 		not_null<PeerData*> peer) const;
-	void automaticLoad(Data::FileOrigin origin, const HistoryItem *item);
+	void automaticLoad(FileOrigin origin, const HistoryItem *item);
+	void automaticLoad(FileOrigin origin, not_null<PeerData*> peer);
 
 	void collectLocalData(not_null<PhotoMedia*> local);
 
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 2023c9b67..6c3100d4d 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -67,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_forum_icons.h"
 #include "data/data_cloud_themes.h"
 #include "data/data_stories.h"
+#include "data/data_story.h"
 #include "data/data_streaming.h"
 #include "data/data_media_rotation.h"
 #include "data/data_histories.h"
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 30d7fbc8f..0c7586151 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -9,17 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "api/api_report.h"
 #include "base/unixtime.h"
-#include "api/api_text_entities.h"
 #include "apiwrap.h"
 #include "core/application.h"
 #include "data/data_changes.h"
-#include "data/data_chat_participant_status.h"
 #include "data/data_document.h"
-#include "data/data_file_origin.h"
 #include "data/data_photo.h"
 #include "data/data_user.h"
 #include "data/data_session.h"
-#include "data/data_thread.h"
 #include "history/history.h"
 #include "history/history_item.h"
 #include "lang/lang_keys.h"
@@ -39,6 +35,8 @@ constexpr auto kArchiveFirstPerPage = 30;
 constexpr auto kArchivePerPage = 100;
 constexpr auto kSavedFirstPerPage = 30;
 constexpr auto kSavedPerPage = 100;
+constexpr auto kMaxPreloadSources = 10;
+constexpr auto kStillPreloadFromFirst = 3;
 
 using UpdateFlag = StoryUpdate::Flag;
 
@@ -86,296 +84,12 @@ bool StoriesSource::unread() const {
 	return !ids.empty() && readTill < ids.back().id;
 }
 
-Story::Story(
-	StoryId id,
-	not_null<PeerData*> peer,
-	StoryMedia media,
-	TimeId date,
-	TimeId expires)
-: _id(id)
-, _peer(peer)
-, _media(std::move(media))
-, _date(date)
-, _expires(expires) {
-}
-
-Session &Story::owner() const {
-	return _peer->owner();
-}
-
-Main::Session &Story::session() const {
-	return _peer->session();
-}
-
-not_null<PeerData*> Story::peer() const {
-	return _peer;
-}
-
-StoryId Story::id() const {
-	return _id;
-}
-
-bool Story::mine() const {
-	return _peer->isSelf();
-}
-
-StoryIdDates Story::idDates() const {
-	return { _id, _date, _expires };
-}
-
-FullStoryId Story::fullId() const {
-	return { _peer->id, _id };
-}
-
-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());
-}
-
-bool Story::unsupported() const {
-	return v::is_null(_media.data);
-}
-
-const StoryMedia &Story::media() const {
-	return _media;
-}
-
-PhotoData *Story::photo() const {
-	const auto result = std::get_if<not_null<PhotoData*>>(&_media.data);
-	return result ? result->get() : nullptr;
-}
-
-DocumentData *Story::document() const {
-	const auto result = std::get_if<not_null<DocumentData*>>(&_media.data);
-	return result ? result->get() : nullptr;
-}
-
-bool Story::hasReplyPreview() const {
-	return v::match(_media.data, [](not_null<PhotoData*> photo) {
-		return !photo->isNull();
-	}, [](not_null<DocumentData*> document) {
-		return document->hasThumbnail();
-	}, [](v::null_t) {
-		return false;
-	});
-}
-
-Image *Story::replyPreview() const {
-	return v::match(_media.data, [&](not_null<PhotoData*> photo) {
-		return photo->getReplyPreview(
-			Data::FileOriginStory(_peer->id, _id),
-			_peer,
-			false);
-	}, [&](not_null<DocumentData*> document) {
-		return document->getReplyPreview(
-			Data::FileOriginStory(_peer->id, _id),
-			_peer,
-			false);
-	}, [](v::null_t) {
-		return (Image*)nullptr;
-	});
-}
-
-TextWithEntities Story::inReplyText() const {
-	const auto type = tr::lng_in_dlg_story(tr::now);
-	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;
-}
-
-bool Story::pinned() const {
-	return _pinned;
-}
-
-void Story::setIsPublic(bool isPublic) {
-	_isPublic = isPublic;
-}
-
-bool Story::isPublic() const {
-	return _isPublic;
-}
-
-void Story::setCloseFriends(bool closeFriends) {
-	_closeFriends = closeFriends;
-}
-
-bool Story::closeFriends() const {
-	return _closeFriends;
-}
-
-bool Story::canDownload() const {
-	return _peer->isSelf();
-}
-
-bool Story::canShare() const {
-	return isPublic() && (pinned() || !expired());
-}
-
-bool Story::canDelete() const {
-	return _peer->isSelf();
-}
-
-bool Story::canReport() const {
-	return !_peer->isSelf();
-}
-
-bool Story::hasDirectLink() const {
-	if (!_isPublic || (!_pinned && expired())) {
-		return false;
+StoryIdDates StoriesSource::toOpen() const {
+	if (ids.empty()) {
+		return {};
 	}
-	const auto user = _peer->asUser();
-	return user && !user->username().isEmpty();
-}
-
-std::optional<QString> Story::errorTextForForward(
-		not_null<Thread*> to) const {
-	const auto peer = to->peer();
-	const auto holdsPhoto = v::is<not_null<PhotoData*>>(_media.data);
-	const auto first = holdsPhoto
-		? ChatRestriction::SendPhotos
-		: ChatRestriction::SendVideos;
-	const auto second = holdsPhoto
-		? ChatRestriction::SendVideos
-		: ChatRestriction::SendPhotos;
-	if (const auto error = Data::RestrictionError(peer, first)) {
-		return *error;
-	} else if (const auto error = Data::RestrictionError(peer, second)) {
-		return *error;
-	} else if (!Data::CanSend(to, first, false)
-		|| !Data::CanSend(to, second, false)) {
-		return tr::lng_forward_cant(tr::now);
-	}
-	return {};
-}
-
-void Story::setCaption(TextWithEntities &&caption) {
-	_caption = std::move(caption);
-}
-
-const TextWithEntities &Story::caption() const {
-	static const auto empty = TextWithEntities();
-	return unsupported() ? empty : _caption;
-}
-
-void Story::setViewsData(
-		std::vector<not_null<PeerData*>> recent,
-		int total) {
-	_recentViewers = std::move(recent);
-	_views = total;
-}
-
-const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
-	return _recentViewers;
-}
-
-const std::vector<StoryView> &Story::viewsList() const {
-	return _viewsList;
-}
-
-int Story::views() const {
-	return _views;
-}
-
-void Story::applyViewsSlice(
-		const std::optional<StoryView> &offset,
-		const std::vector<StoryView> &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();
-	const auto isPublic = data.is_public();
-	const auto closeFriends = data.is_close_friends();
-	auto caption = TextWithEntities{
-		data.vcaption().value_or_empty(),
-		Api::EntitiesFromMTP(
-			&owner().session(),
-			data.ventities().value_or_empty()),
-	};
-	auto views = -1;
-	auto recent = std::vector<not_null<PeerData*>>();
-	if (!data.is_min()) {
-		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)
-		|| (_isPublic != isPublic)
-		|| (_closeFriends != closeFriends)
-		|| (_caption != caption)
-		|| (views >= 0 && _views != views)
-		|| (_recentViewers != recent);
-	if (!changed) {
-		return false;
-	}
-	_media = std::move(media);
-	_pinned = pinned;
-	_isPublic = isPublic;
-	_closeFriends = closeFriends;
-	_caption = std::move(caption);
-	if (views >= 0) {
-		_views = views;
-	}
-	_recentViewers = std::move(recent);
-	return true;
+	const auto i = ids.lower_bound(StoryIdDates{ readTill + 1 });
+	return (i != end(ids)) ? *i : ids.front();
 }
 
 Stories::Stories(not_null<Session*> owner)
@@ -622,6 +336,7 @@ Story *Stories::parseAndApply(
 	if (i != end(stories)) {
 		const auto result = i->second.get();
 		const auto pinned = result->pinned();
+		const auto mediaChanged = (result->media() != *media);
 		if (result->applyChanges(*media, data)) {
 			if (result->pinned() != pinned) {
 				savedStateUpdated(result);
@@ -633,6 +348,16 @@ Story *Stories::parseAndApply(
 				item->applyChanges(result);
 			}
 		}
+		if (mediaChanged) {
+			const auto fullId = result->fullId();
+			_preloaded.remove(fullId);
+			if (_preloading && _preloading->id() == fullId) {
+				_preloading = nullptr;
+				rebuildPreloadSources(StorySourcesList::NotHidden);
+				rebuildPreloadSources(StorySourcesList::Hidden);
+				continuePreloading();
+			}
+		}
 		return result;
 	}
 	const auto result = stories.emplace(id, std::make_unique<Story>(
@@ -911,6 +636,9 @@ void Stories::applyDeleted(FullStoryId id) {
 					}
 				}
 			}
+			if (_preloading && _preloading->id() == id) {
+				preloadFinished();
+			}
 			if (i->second.empty()) {
 				_stories.erase(i);
 			}
@@ -996,6 +724,7 @@ void Stories::sort(StorySourcesList list) {
 	};
 	ranges::sort(sources, ranges::greater(), proj);
 	_sourcesChanged[index].fire({});
+	preloadSourcesChanged(list);
 }
 
 std::shared_ptr<HistoryItem> Stories::lookupItem(not_null<Story*> story) {
@@ -1225,6 +954,7 @@ void Stories::toggleHidden(
 		if (i != end(_sources[main])) {
 			_sources[main].erase(i);
 			_sourcesChanged[main].fire({});
+			preloadSourcesChanged(StorySourcesList::NotHidden);
 		}
 		const auto j = ranges::find(_sources[other], peerId, proj);
 		if (j == end(_sources[other])) {
@@ -1238,6 +968,7 @@ void Stories::toggleHidden(
 		if (i != end(_sources[other])) {
 			_sources[other].erase(i);
 			_sourcesChanged[other].fire({});
+			preloadSourcesChanged(StorySourcesList::Hidden);
 		}
 		const auto j = ranges::find(_sources[main], peerId, proj);
 		if (j == end(_sources[main])) {
@@ -1563,4 +1294,134 @@ bool Stories::isQuitPrevent() {
 	return true;
 }
 
+void Stories::incrementPreloadingHiddenSources() {
+	Expects(_preloadingHiddenSourcesCounter >= 0);
+
+	if (++_preloadingHiddenSourcesCounter == 1
+		&& rebuildPreloadSources(StorySourcesList::Hidden)) {
+		continuePreloading();
+	}
+}
+
+void Stories::decrementPreloadingHiddenSources() {
+	Expects(_preloadingHiddenSourcesCounter > 0);
+
+	if (!--_preloadingHiddenSourcesCounter
+		&& rebuildPreloadSources(StorySourcesList::Hidden)) {
+		continuePreloading();
+	}
+}
+
+void Stories::setPreloadingInViewer(std::vector<FullStoryId> ids) {
+	ids.erase(ranges::remove_if(ids, [&](FullStoryId id) {
+		return _preloaded.contains(id);
+	}), end(ids));
+	if (_toPreloadViewer != ids) {
+		_toPreloadViewer = std::move(ids);
+		continuePreloading();
+	}
+}
+
+void Stories::preloadSourcesChanged(StorySourcesList list) {
+	if (rebuildPreloadSources(list)) {
+		continuePreloading();
+	}
+}
+
+bool Stories::rebuildPreloadSources(StorySourcesList list) {
+	const auto index = static_cast<int>(list);
+	if (!_preloadingHiddenSourcesCounter
+		&& list == StorySourcesList::Hidden) {
+		return !base::take(_toPreloadSources[index]).empty();
+	}
+	auto now = std::vector<FullStoryId>();
+	auto processed = 0;
+	for (const auto &source : _sources[index]) {
+		const auto i = _all.find(source.id);
+		if (i != end(_all)) {
+			if (const auto id = i->second.toOpen().id) {
+				const auto fullId = FullStoryId{ source.id, id };
+				if (!_preloaded.contains(fullId)) {
+					now.push_back(fullId);
+				}
+			}
+		}
+		if (++processed >= kMaxPreloadSources) {
+			break;
+		}
+	}
+	if (now != _toPreloadSources[index]) {
+		_toPreloadSources[index] = std::move(now);
+		return true;
+	}
+	return false;
+}
+
+void Stories::continuePreloading() {
+	const auto now = _preloading ? _preloading->id() : FullStoryId();
+	if (now) {
+		if (shouldContinuePreload(now)) {
+			return;
+		}
+		_preloading = nullptr;
+	}
+	const auto id = nextPreloadId();
+	if (!id) {
+		return;
+	} else if (const auto maybeStory = lookup(id)) {
+		startPreloading(*maybeStory);
+	}
+}
+
+bool Stories::shouldContinuePreload(FullStoryId id) const {
+	const auto first = ranges::views::concat(
+		_toPreloadViewer,
+		_toPreloadSources[static_cast<int>(StorySourcesList::Hidden)],
+		_toPreloadSources[static_cast<int>(StorySourcesList::NotHidden)]
+	) | ranges::views::take(kStillPreloadFromFirst);
+	return ranges::contains(first, id);
+}
+
+FullStoryId Stories::nextPreloadId() const {
+	const auto hidden = static_cast<int>(StorySourcesList::Hidden);
+	const auto main = static_cast<int>(StorySourcesList::NotHidden);
+	const auto result = !_toPreloadViewer.empty()
+		? _toPreloadViewer.front()
+		: !_toPreloadSources[hidden].empty()
+		? _toPreloadSources[hidden].front()
+		: !_toPreloadSources[main].empty()
+		? _toPreloadSources[main].front()
+		: FullStoryId();
+
+	Ensures(!_preloaded.contains(result));
+	return result;
+}
+
+void Stories::startPreloading(not_null<Story*> story) {
+	Expects(!_preloaded.contains(story->fullId()));
+
+	const auto id = story->fullId();
+	_preloading = std::make_unique<StoryPreload>(story, [=] {
+		preloadFinished(true);
+	});
+}
+
+void Stories::preloadFinished(bool markAsPreloaded) {
+	Expects(_preloading != nullptr);
+
+	const auto id = base::take(_preloading)->id();
+	for (auto &sources : _toPreloadSources) {
+		sources.erase(ranges::remove(sources, id), end(sources));
+	}
+	_toPreloadViewer.erase(
+		ranges::remove(_toPreloadViewer, id),
+		end(_toPreloadViewer));
+	if (markAsPreloaded) {
+		_preloaded.emplace(id);
+	}
+	crl::on_main(this, [=] {
+		continuePreloading();
+	});
+}
+
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 2cd0dcccf..80d4a9732 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -11,11 +11,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;
-class DocumentData;
-enum class ChatRestriction;
+#include "data/data_story.h"
 
 namespace Main {
 class Session;
@@ -29,23 +25,10 @@ enum class ReportReason;
 namespace Data {
 
 class Session;
-class Thread;
-
-struct StoryIdDates {
-	StoryId id = 0;
-	TimeId date = 0;
-	TimeId expires = 0;
-
-	[[nodiscard]] bool valid() const {
-		return id != 0;
-	}
-	explicit operator bool() const {
-		return valid();
-	}
-
-	friend inline auto operator<=>(StoryIdDates, StoryIdDates) = default;
-	friend inline bool operator==(StoryIdDates, StoryIdDates) = default;
-};
+struct StoryView;
+struct StoryIdDates;
+class Story;
+class StoryPreload;
 
 struct StoriesIds {
 	base::flat_set<StoryId, std::greater<>> list;
@@ -55,98 +38,6 @@ struct StoriesIds {
 		const StoriesIds&) = default;
 };
 
-struct StoryMedia {
-	std::variant<
-		v::null_t,
-		not_null<PhotoData*>,
-		not_null<DocumentData*>> data;
-
-	friend inline bool operator==(StoryMedia, StoryMedia) = default;
-};
-
-struct StoryView {
-	not_null<PeerData*> peer;
-	TimeId date = 0;
-
-	friend inline bool operator==(StoryView, StoryView) = default;
-};
-
-class Story final {
-public:
-	Story(
-		StoryId id,
-		not_null<PeerData*> peer,
-		StoryMedia media,
-		TimeId date,
-		TimeId expires);
-
-	[[nodiscard]] Session &owner() const;
-	[[nodiscard]] Main::Session &session() const;
-	[[nodiscard]] not_null<PeerData*> peer() const;
-
-	[[nodiscard]] StoryId id() const;
-	[[nodiscard]] bool mine() const;
-	[[nodiscard]] StoryIdDates idDates() const;
-	[[nodiscard]] FullStoryId fullId() const;
-	[[nodiscard]] TimeId date() const;
-	[[nodiscard]] TimeId expires() const;
-	[[nodiscard]] bool unsupported() const;
-	[[nodiscard]] bool expired(TimeId now = 0) 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;
-	void setIsPublic(bool isPublic);
-	[[nodiscard]] bool isPublic() const;
-	void setCloseFriends(bool closeFriends);
-	[[nodiscard]] bool closeFriends() const;
-
-	[[nodiscard]] bool canDownload() const;
-	[[nodiscard]] bool canShare() const;
-	[[nodiscard]] bool canDelete() const;
-	[[nodiscard]] bool canReport() const;
-
-	[[nodiscard]] bool hasDirectLink() const;
-	[[nodiscard]] std::optional<QString> errorTextForForward(
-		not_null<Thread*> to) const;
-
-	void setCaption(TextWithEntities &&caption);
-	[[nodiscard]] const TextWithEntities &caption() const;
-
-	void setViewsData(std::vector<not_null<PeerData*>> recent, int total);
-	[[nodiscard]] auto recentViewers() const
-		-> const std::vector<not_null<PeerData*>> &;
-	[[nodiscard]] const std::vector<StoryView> &viewsList() const;
-	[[nodiscard]] int views() const;
-	void applyViewsSlice(
-		const std::optional<StoryView> &offset,
-		const std::vector<StoryView> &slice,
-		int total);
-
-	bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
-
-private:
-	const StoryId _id = 0;
-	const not_null<PeerData*> _peer;
-	StoryMedia _media;
-	TextWithEntities _caption;
-	std::vector<not_null<PeerData*>> _recentViewers;
-	std::vector<StoryView> _viewsList;
-	int _views = 0;
-	const TimeId _date = 0;
-	const TimeId _expires = 0;
-	bool _pinned : 1 = false;
-	bool _isPublic : 1 = false;
-	bool _closeFriends : 1 = false;
-
-};
-
 struct StoriesSourceInfo {
 	PeerId id = 0;
 	TimeId last = 0;
@@ -167,6 +58,7 @@ struct StoriesSource {
 
 	[[nodiscard]] StoriesSourceInfo info() const;
 	[[nodiscard]] bool unread() const;
+	[[nodiscard]] StoryIdDates toOpen() const;
 
 	friend inline bool operator==(StoriesSource, StoriesSource) = default;
 };
@@ -298,6 +190,10 @@ public:
 		Ui::ReportReason reason,
 		QString text);
 
+	void incrementPreloadingHiddenSources();
+	void decrementPreloadingHiddenSources();
+	void setPreloadingInViewer(std::vector<FullStoryId> ids);
+
 private:
 	struct Saved {
 		StoriesIds ids;
@@ -339,11 +235,18 @@ private:
 	void checkQuitPreventFinished();
 
 	void requestUserStories(not_null<UserData*> user);
-	void addToArchive(not_null<Story*> story);
 	void registerExpiring(TimeId expires, FullStoryId id);
 	void scheduleExpireTimer();
 	void processExpired();
 
+	void preloadSourcesChanged(StorySourcesList list);
+	bool rebuildPreloadSources(StorySourcesList list);
+	void continuePreloading();
+	[[nodiscard]] bool shouldContinuePreload(FullStoryId id) const;
+	[[nodiscard]] FullStoryId nextPreloadId() const;
+	void startPreloading(not_null<Story*> story);
+	void preloadFinished(bool markAsPreloaded = false);
+
 	const not_null<Session*> _owner;
 	std::unordered_map<
 		PeerId,
@@ -402,6 +305,12 @@ private:
 	Fn<void(std::vector<StoryView>)> _viewsDone;
 	mtpRequestId _viewsRequestId = 0;
 
+	base::flat_set<FullStoryId> _preloaded;
+	std::vector<FullStoryId> _toPreloadSources[kStorySourcesListCount];
+	std::vector<FullStoryId> _toPreloadViewer;
+	std::unique_ptr<StoryPreload> _preloading;
+	int _preloadingHiddenSourcesCounter = 0;
+
 };
 
 } // namespace Data
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
index d87ad902f..aa5841c53 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
@@ -807,23 +807,7 @@ void Reader::Slices::unloadSlice(Slice &slice) const {
 }
 
 QByteArray Reader::Slices::serializeComplexSlice(const Slice &slice) const {
-	auto result = QByteArray();
-	const auto count = slice.parts.size();
-	const auto intSize = sizeof(int32);
-	result.reserve(count * kPartSize + 2 * intSize * (count + 1));
-	const auto appendInt = [&](int value) {
-		auto serialized = int32(value);
-		result.append(
-			reinterpret_cast<const char*>(&serialized),
-			intSize);
-	};
-	appendInt(count);
-	for (const auto &[offset, part] : slice.parts) {
-		appendInt(offset);
-		appendInt(part.size());
-		result.append(part);
-	}
-	return result;
+	return SerializeComplexPartsMap(slice.parts);
 }
 
 QByteArray Reader::Slices::serializeAndUnloadFirstSliceNoHeader() {
@@ -1411,5 +1395,26 @@ Reader::~Reader() {
 	finalizeCache();
 }
 
+QByteArray SerializeComplexPartsMap(
+		const base::flat_map<uint32, QByteArray> &parts) {
+	auto result = QByteArray();
+	const auto count = parts.size();
+	const auto intSize = sizeof(int32);
+	result.reserve(count * kPartSize + 2 * intSize * (count + 1));
+	const auto appendInt = [&](int value) {
+		auto serialized = int32(value);
+		result.append(
+			reinterpret_cast<const char*>(&serialized),
+			intSize);
+	};
+	appendInt(count);
+	for (const auto &[offset, part] : parts) {
+		appendInt(offset);
+		appendInt(part.size());
+		result.append(part);
+	}
+	return result;
+}
+
 } // namespace Streaming
 } // namespace Media
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
index c588823ac..179829846 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
@@ -277,5 +277,8 @@ private:
 
 };
 
+[[nodiscard]] QByteArray SerializeComplexPartsMap(
+	const base::flat_map<uint32, QByteArray> &parts);
+
 } // namespace Streaming
 } // namespace Media
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index 9758d8545..e35e83d03 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -2542,14 +2542,14 @@ void SessionController::openPeerStories(
 
 	auto &stories = session().data().stories();
 	if (const auto source = stories.source(peerId)) {
-		const auto j = source->ids.lower_bound(
-			StoryIdDates{ source->readTill + 1 });
-		openPeerStory(
-			source->user,
-			j != source->ids.end() ? j->id : source->ids.front().id,
-			(list
-				? StoriesContext{ *list }
-				: StoriesContext{ StoriesContextPeer() }));
+		if (const auto idDates = source->toOpen()) {
+			openPeerStory(
+				source->user,
+				idDates.id,
+				(list
+					? StoriesContext{ *list }
+					: StoriesContext{ StoriesContextPeer() }));
+		}
 	}
 }
 

From 010c666d2373453ebdd0e5bda96e0f77b3d7189c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 12:38:02 +0400
Subject: [PATCH 109/259] Preload next stories inside the media viewer.

---
 Telegram/SourceFiles/data/data_stories.cpp    |  39 +-
 Telegram/SourceFiles/data/data_stories.h      |   5 +-
 Telegram/SourceFiles/data/data_story.cpp      | 515 ++++++++++++++++++
 Telegram/SourceFiles/data/data_story.h        | 158 ++++++
 .../dialogs/dialogs_inner_widget.cpp          |   3 +
 .../stories/media_stories_controller.cpp      |  40 +-
 .../media/stories/media_stories_controller.h  |   1 +
 7 files changed, 749 insertions(+), 12 deletions(-)
 create mode 100644 Telegram/SourceFiles/data/data_story.cpp
 create mode 100644 Telegram/SourceFiles/data/data_story.h

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 0c7586151..86ee493e6 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -637,7 +637,7 @@ void Stories::applyDeleted(FullStoryId id) {
 				}
 			}
 			if (_preloading && _preloading->id() == id) {
-				preloadFinished();
+				preloadFinished(id);
 			}
 			if (i->second.empty()) {
 				_stories.erase(i);
@@ -1294,6 +1294,24 @@ bool Stories::isQuitPrevent() {
 	return true;
 }
 
+void Stories::incrementPreloadingMainSources() {
+	Expects(_preloadingMainSourcesCounter >= 0);
+
+	if (++_preloadingMainSourcesCounter == 1
+		&& rebuildPreloadSources(StorySourcesList::NotHidden)) {
+		continuePreloading();
+	}
+}
+
+void Stories::decrementPreloadingMainSources() {
+	Expects(_preloadingMainSourcesCounter > 0);
+
+	if (!--_preloadingMainSourcesCounter
+		&& rebuildPreloadSources(StorySourcesList::NotHidden)) {
+		continuePreloading();
+	}
+}
+
 void Stories::incrementPreloadingHiddenSources() {
 	Expects(_preloadingHiddenSourcesCounter >= 0);
 
@@ -1330,8 +1348,10 @@ void Stories::preloadSourcesChanged(StorySourcesList list) {
 
 bool Stories::rebuildPreloadSources(StorySourcesList list) {
 	const auto index = static_cast<int>(list);
-	if (!_preloadingHiddenSourcesCounter
-		&& list == StorySourcesList::Hidden) {
+	const auto &counter = (list == StorySourcesList::Hidden)
+		? _preloadingHiddenSourcesCounter
+		: _preloadingMainSourcesCounter;
+	if (!counter) {
 		return !base::take(_toPreloadSources[index]).empty();
 	}
 	auto now = std::vector<FullStoryId>();
@@ -1401,15 +1421,16 @@ void Stories::startPreloading(not_null<Story*> story) {
 	Expects(!_preloaded.contains(story->fullId()));
 
 	const auto id = story->fullId();
-	_preloading = std::make_unique<StoryPreload>(story, [=] {
-		preloadFinished(true);
+	auto preloading = std::make_unique<StoryPreload>(story, [=] {
+		_preloading = nullptr;
+		preloadFinished(id, true);
 	});
+	if (!_preloaded.contains(id)) {
+		_preloading = std::move(preloading);
+	}
 }
 
-void Stories::preloadFinished(bool markAsPreloaded) {
-	Expects(_preloading != nullptr);
-
-	const auto id = base::take(_preloading)->id();
+void Stories::preloadFinished(FullStoryId id, bool markAsPreloaded) {
 	for (auto &sources : _toPreloadSources) {
 		sources.erase(ranges::remove(sources, id), end(sources));
 	}
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 80d4a9732..648535187 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -190,6 +190,8 @@ public:
 		Ui::ReportReason reason,
 		QString text);
 
+	void incrementPreloadingMainSources();
+	void decrementPreloadingMainSources();
 	void incrementPreloadingHiddenSources();
 	void decrementPreloadingHiddenSources();
 	void setPreloadingInViewer(std::vector<FullStoryId> ids);
@@ -245,7 +247,7 @@ private:
 	[[nodiscard]] bool shouldContinuePreload(FullStoryId id) const;
 	[[nodiscard]] FullStoryId nextPreloadId() const;
 	void startPreloading(not_null<Story*> story);
-	void preloadFinished(bool markAsPreloaded = false);
+	void preloadFinished(FullStoryId id, bool markAsPreloaded = false);
 
 	const not_null<Session*> _owner;
 	std::unordered_map<
@@ -310,6 +312,7 @@ private:
 	std::vector<FullStoryId> _toPreloadViewer;
 	std::unique_ptr<StoryPreload> _preloading;
 	int _preloadingHiddenSourcesCounter = 0;
+	int _preloadingMainSourcesCounter = 0;
 
 };
 
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
new file mode 100644
index 000000000..cf31749f4
--- /dev/null
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -0,0 +1,515 @@
+/*
+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_story.h"
+
+#include "base/unixtime.h"
+#include "api/api_text_entities.h"
+#include "data/data_document.h"
+#include "data/data_file_origin.h"
+#include "data/data_photo.h"
+#include "data/data_photo_media.h"
+#include "data/data_user.h"
+#include "data/data_session.h"
+#include "data/data_thread.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "media/streaming/media_streaming_reader.h"
+#include "storage/download_manager_mtproto.h"
+#include "ui/text/text_utilities.h"
+
+namespace Data {
+
+class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask {
+public:
+	LoadTask(
+		FullStoryId id,
+		not_null<DocumentData*> document,
+		Fn<void(QByteArray)> done);
+	~LoadTask();
+
+private:
+	bool readyToRequest() const override;
+	int64 takeNextRequestOffset() override;
+	bool feedPart(int64 offset, const QByteArray &bytes) override;
+	void cancelOnFail() override;
+	bool setWebFileSizeHook(int64 size) override;
+
+	base::flat_map<uint32, QByteArray> _parts;
+	Fn<void(QByteArray)> _done;
+	base::flat_set<int> _requestedOffsets;
+	int64 _full = 0;
+	int  _nextRequestOffset = 0;
+	bool _finished = false;
+	bool _failed = false;
+
+};
+
+StoryPreload::LoadTask::LoadTask(
+	FullStoryId id,
+	not_null<DocumentData*> document,
+	Fn<void(QByteArray)> done)
+: DownloadMtprotoTask(
+	&document->session().downloader(),
+	document->videoPreloadLocation(),
+	FileOriginStory(id.peer, id.story))
+, _done(std::move(done))
+, _full(document->size) {
+	const auto prefix = document->videoPreloadPrefix();
+	Assert(prefix > 0 && prefix <= document->size);
+	const auto part = Storage::kDownloadPartSize;
+	const auto parts = (prefix + part - 1) / part;
+	for (auto i = 0; i != parts; ++i) {
+		_parts.emplace(i * part, QByteArray());
+	}
+	addToQueue();
+}
+
+StoryPreload::LoadTask::~LoadTask() {
+	if (!_finished && !_failed) {
+		cancelAllRequests();
+	}
+}
+
+bool StoryPreload::LoadTask::readyToRequest() const {
+	const auto part = Storage::kDownloadPartSize;
+	return !_failed && (_nextRequestOffset < _parts.size() * part);
+}
+
+int64 StoryPreload::LoadTask::takeNextRequestOffset() {
+	Expects(readyToRequest());
+
+	_requestedOffsets.emplace(_nextRequestOffset);
+	_nextRequestOffset += Storage::kDownloadPartSize;
+	return _requestedOffsets.back();
+}
+
+bool StoryPreload::LoadTask::feedPart(
+		int64 offset,
+		const QByteArray &bytes) {
+	Expects(offset < _parts.size() * Storage::kDownloadPartSize);
+	Expects(_requestedOffsets.contains(int(offset)));
+	Expects(bytes.size() <= Storage::kDownloadPartSize);
+
+	const auto part = Storage::kDownloadPartSize;
+	const auto index = offset / part;
+	_requestedOffsets.remove(int(offset));
+	_parts[offset] = bytes;
+	if ((_nextRequestOffset + part >= _parts.size() * part)
+		&& _requestedOffsets.empty()) {
+		_finished = true;
+		removeFromQueue();
+		auto result = Media::Streaming::SerializeComplexPartsMap(_parts);
+		if (result.size() == _full) {
+			// Make sure it is parsed as a complex map.
+			result.push_back(char(0));
+		}
+		_done(result);
+	}
+	return true;
+}
+
+void StoryPreload::LoadTask::cancelOnFail() {
+	_failed = true;
+	cancelAllRequests();
+	_done({});
+}
+
+bool StoryPreload::LoadTask::setWebFileSizeHook(int64 size) {
+	_failed = true;
+	cancelAllRequests();
+	_done({});
+	return false;
+}
+
+Story::Story(
+	StoryId id,
+	not_null<PeerData*> peer,
+	StoryMedia media,
+	TimeId date,
+	TimeId expires)
+: _id(id)
+, _peer(peer)
+, _media(std::move(media))
+, _date(date)
+, _expires(expires) {
+}
+
+Session &Story::owner() const {
+	return _peer->owner();
+}
+
+Main::Session &Story::session() const {
+	return _peer->session();
+}
+
+not_null<PeerData*> Story::peer() const {
+	return _peer;
+}
+
+StoryId Story::id() const {
+	return _id;
+}
+
+bool Story::mine() const {
+	return _peer->isSelf();
+}
+
+StoryIdDates Story::idDates() const {
+	return { _id, _date, _expires };
+}
+
+FullStoryId Story::fullId() const {
+	return { _peer->id, _id };
+}
+
+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());
+}
+
+bool Story::unsupported() const {
+	return v::is_null(_media.data);
+}
+
+const StoryMedia &Story::media() const {
+	return _media;
+}
+
+PhotoData *Story::photo() const {
+	const auto result = std::get_if<not_null<PhotoData*>>(&_media.data);
+	return result ? result->get() : nullptr;
+}
+
+DocumentData *Story::document() const {
+	const auto result = std::get_if<not_null<DocumentData*>>(&_media.data);
+	return result ? result->get() : nullptr;
+}
+
+bool Story::hasReplyPreview() const {
+	return v::match(_media.data, [](not_null<PhotoData*> photo) {
+		return !photo->isNull();
+	}, [](not_null<DocumentData*> document) {
+		return document->hasThumbnail();
+	}, [](v::null_t) {
+		return false;
+	});
+}
+
+Image *Story::replyPreview() const {
+	return v::match(_media.data, [&](not_null<PhotoData*> photo) {
+		return photo->getReplyPreview(
+			Data::FileOriginStory(_peer->id, _id),
+			_peer,
+			false);
+	}, [&](not_null<DocumentData*> document) {
+		return document->getReplyPreview(
+			Data::FileOriginStory(_peer->id, _id),
+			_peer,
+			false);
+	}, [](v::null_t) {
+		return (Image*)nullptr;
+	});
+}
+
+TextWithEntities Story::inReplyText() const {
+	const auto type = tr::lng_in_dlg_story(tr::now);
+	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;
+}
+
+bool Story::pinned() const {
+	return _pinned;
+}
+
+void Story::setIsPublic(bool isPublic) {
+	_isPublic = isPublic;
+}
+
+bool Story::isPublic() const {
+	return _isPublic;
+}
+
+void Story::setCloseFriends(bool closeFriends) {
+	_closeFriends = closeFriends;
+}
+
+bool Story::closeFriends() const {
+	return _closeFriends;
+}
+
+bool Story::canDownload() const {
+	return _peer->isSelf();
+}
+
+bool Story::canShare() const {
+	return isPublic() && (pinned() || !expired());
+}
+
+bool Story::canDelete() const {
+	return _peer->isSelf();
+}
+
+bool Story::canReport() const {
+	return !_peer->isSelf();
+}
+
+bool Story::hasDirectLink() const {
+	if (!_isPublic || (!_pinned && expired())) {
+		return false;
+	}
+	const auto user = _peer->asUser();
+	return user && !user->username().isEmpty();
+}
+
+std::optional<QString> Story::errorTextForForward(
+		not_null<Thread*> to) const {
+	const auto peer = to->peer();
+	const auto holdsPhoto = v::is<not_null<PhotoData*>>(_media.data);
+	const auto first = holdsPhoto
+		? ChatRestriction::SendPhotos
+		: ChatRestriction::SendVideos;
+	const auto second = holdsPhoto
+		? ChatRestriction::SendVideos
+		: ChatRestriction::SendPhotos;
+	if (const auto error = Data::RestrictionError(peer, first)) {
+		return *error;
+	} else if (const auto error = Data::RestrictionError(peer, second)) {
+		return *error;
+	} else if (!Data::CanSend(to, first, false)
+		|| !Data::CanSend(to, second, false)) {
+		return tr::lng_forward_cant(tr::now);
+	}
+	return {};
+}
+
+void Story::setCaption(TextWithEntities &&caption) {
+	_caption = std::move(caption);
+}
+
+const TextWithEntities &Story::caption() const {
+	static const auto empty = TextWithEntities();
+	return unsupported() ? empty : _caption;
+}
+
+void Story::setViewsData(
+		std::vector<not_null<PeerData*>> recent,
+		int total) {
+	_recentViewers = std::move(recent);
+	_views = total;
+}
+
+const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
+	return _recentViewers;
+}
+
+const std::vector<StoryView> &Story::viewsList() const {
+	return _viewsList;
+}
+
+int Story::views() const {
+	return _views;
+}
+
+void Story::applyViewsSlice(
+		const std::optional<StoryView> &offset,
+		const std::vector<StoryView> &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();
+	const auto isPublic = data.is_public();
+	const auto closeFriends = data.is_close_friends();
+	auto caption = TextWithEntities{
+		data.vcaption().value_or_empty(),
+		Api::EntitiesFromMTP(
+			&owner().session(),
+			data.ventities().value_or_empty()),
+	};
+	auto views = -1;
+	auto recent = std::vector<not_null<PeerData*>>();
+	if (!data.is_min()) {
+		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)
+		|| (_isPublic != isPublic)
+		|| (_closeFriends != closeFriends)
+		|| (_caption != caption)
+		|| (views >= 0 && _views != views)
+		|| (_recentViewers != recent);
+	if (!changed) {
+		return false;
+	}
+	_media = std::move(media);
+	_pinned = pinned;
+	_isPublic = isPublic;
+	_closeFriends = closeFriends;
+	_caption = std::move(caption);
+	if (views >= 0) {
+		_views = views;
+	}
+	_recentViewers = std::move(recent);
+	return true;
+}
+
+StoryPreload::StoryPreload(not_null<Story*> story, Fn<void()> done)
+: _story(story)
+, _done(std::move(done)) {
+	start();
+}
+
+StoryPreload::~StoryPreload() {
+	if (_photo) {
+		base::take(_photo)->owner()->cancel();
+	}
+}
+
+FullStoryId StoryPreload::id() const {
+	return _story->fullId();
+}
+
+not_null<Story*> StoryPreload::story() const {
+	return _story;
+}
+
+void StoryPreload::start() {
+	const auto origin = FileOriginStory(
+		_story->peer()->id,
+		_story->id());
+	if (const auto photo = _story->photo()) {
+		_photo = photo->createMediaView();
+		if (_photo->loaded()) {
+			callDone();
+		} else {
+			_photo->automaticLoad(origin, _story->peer());
+			photo->session().downloaderTaskFinished(
+			) | rpl::filter([=] {
+				return _photo->loaded();
+			}) | rpl::start_with_next([=] { callDone(); }, _lifetime);
+		}
+	} else if (const auto video = _story->document()) {
+		if (video->canBeStreamed(nullptr) && video->videoPreloadPrefix()) {
+			const auto key = video->bigFileBaseCacheKey();
+			if (key) {
+				const auto weak = base::make_weak(this);
+				video->owner().cacheBigFile().get(key, [weak](
+						const QByteArray &result) {
+					if (!result.isEmpty()) {
+						crl::on_main([weak] {
+							if (const auto strong = weak.get()) {
+								strong->callDone();
+							}
+						});
+					} else {
+						crl::on_main([weak] {
+							if (const auto strong = weak.get()) {
+								strong->load();
+							}
+						});
+					}
+				});
+			} else {
+				callDone();
+			}
+		} else {
+			callDone();
+		}
+	} else {
+		callDone();
+	}
+}
+
+void StoryPreload::load() {
+	Expects(_story->document() != nullptr);
+
+	const auto video = _story->document();
+	const auto valid = video->videoPreloadLocation().valid();
+	const auto prefix = video->videoPreloadPrefix();
+	const auto key = video->bigFileBaseCacheKey();
+	if (!valid || prefix <= 0 || prefix > video->size || !key) {
+		callDone();
+		return;
+	}
+	_task = std::make_unique<LoadTask>(id(), video, [=](QByteArray data) {
+		if (!data.isEmpty()) {
+			_story->owner().cacheBigFile().putIfEmpty(
+				key,
+				Storage::Cache::Database::TaggedValue(std::move(data), 0));
+		}
+		callDone();
+	});
+}
+
+void StoryPreload::callDone() {
+	if (const auto onstack = _done) {
+		onstack();
+	}
+}
+
+} // namespace Data
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
new file mode 100644
index 000000000..1f4801efd
--- /dev/null
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -0,0 +1,158 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/weak_ptr.h"
+
+class Image;
+class PhotoData;
+class DocumentData;
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Data {
+
+class Session;
+class Thread;
+class PhotoMedia;
+
+struct StoryIdDates {
+	StoryId id = 0;
+	TimeId date = 0;
+	TimeId expires = 0;
+
+	[[nodiscard]] bool valid() const {
+		return id != 0;
+	}
+	explicit operator bool() const {
+		return valid();
+	}
+
+	friend inline auto operator<=>(StoryIdDates, StoryIdDates) = default;
+	friend inline bool operator==(StoryIdDates, StoryIdDates) = default;
+};
+
+struct StoryMedia {
+	std::variant<
+		v::null_t,
+		not_null<PhotoData*>,
+		not_null<DocumentData*>> data;
+
+	friend inline bool operator==(StoryMedia, StoryMedia) = default;
+};
+
+struct StoryView {
+	not_null<PeerData*> peer;
+	TimeId date = 0;
+
+	friend inline bool operator==(StoryView, StoryView) = default;
+};
+
+class Story final {
+public:
+	Story(
+		StoryId id,
+		not_null<PeerData*> peer,
+		StoryMedia media,
+		TimeId date,
+		TimeId expires);
+
+	[[nodiscard]] Session &owner() const;
+	[[nodiscard]] Main::Session &session() const;
+	[[nodiscard]] not_null<PeerData*> peer() const;
+
+	[[nodiscard]] StoryId id() const;
+	[[nodiscard]] bool mine() const;
+	[[nodiscard]] StoryIdDates idDates() const;
+	[[nodiscard]] FullStoryId fullId() const;
+	[[nodiscard]] TimeId date() const;
+	[[nodiscard]] TimeId expires() const;
+	[[nodiscard]] bool unsupported() const;
+	[[nodiscard]] bool expired(TimeId now = 0) 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;
+	void setIsPublic(bool isPublic);
+	[[nodiscard]] bool isPublic() const;
+	void setCloseFriends(bool closeFriends);
+	[[nodiscard]] bool closeFriends() const;
+
+	[[nodiscard]] bool canDownload() const;
+	[[nodiscard]] bool canShare() const;
+	[[nodiscard]] bool canDelete() const;
+	[[nodiscard]] bool canReport() const;
+
+	[[nodiscard]] bool hasDirectLink() const;
+	[[nodiscard]] std::optional<QString> errorTextForForward(
+		not_null<Thread*> to) const;
+
+	void setCaption(TextWithEntities &&caption);
+	[[nodiscard]] const TextWithEntities &caption() const;
+
+	void setViewsData(std::vector<not_null<PeerData*>> recent, int total);
+	[[nodiscard]] auto recentViewers() const
+		-> const std::vector<not_null<PeerData*>> &;
+	[[nodiscard]] const std::vector<StoryView> &viewsList() const;
+	[[nodiscard]] int views() const;
+	void applyViewsSlice(
+		const std::optional<StoryView> &offset,
+		const std::vector<StoryView> &slice,
+		int total);
+
+	bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
+
+private:
+	const StoryId _id = 0;
+	const not_null<PeerData*> _peer;
+	StoryMedia _media;
+	TextWithEntities _caption;
+	std::vector<not_null<PeerData*>> _recentViewers;
+	std::vector<StoryView> _viewsList;
+	int _views = 0;
+	const TimeId _date = 0;
+	const TimeId _expires = 0;
+	bool _pinned : 1 = false;
+	bool _isPublic : 1 = false;
+	bool _closeFriends : 1 = false;
+
+};
+
+class StoryPreload final : public base::has_weak_ptr {
+public:
+	StoryPreload(not_null<Story*> story, Fn<void()> done);
+	~StoryPreload();
+
+	[[nodiscard]] FullStoryId id() const;
+	[[nodiscard]] not_null<Story*> story() const;
+
+private:
+	class LoadTask;
+
+	void start();
+	void load();
+	void callDone();
+
+	const not_null<Story*> _story;
+	Fn<void()> _done;
+
+	std::shared_ptr<Data::PhotoMedia> _photo;
+	std::unique_ptr<LoadTask> _task;
+	rpl::lifetime _lifetime;
+
+};
+
+} // namespace Data
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index c65f9a2a0..1ab020d36 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -366,6 +366,8 @@ InnerWidget::InnerWidget(
 			Data::StorySourcesList::NotHidden);
 	}, lifetime());
 
+	session().data().stories().incrementPreloadingMainSources();
+
 	handleChatListEntryRefreshes();
 
 	refreshWithCollapsedRows(true);
@@ -2426,6 +2428,7 @@ void InnerWidget::appendToFiltered(Key key) {
 }
 
 InnerWidget::~InnerWidget() {
+	session().data().stories().decrementPreloadingMainSources();
 	clearSearchResults();
 }
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index af4abbb4b..40cd9fc4a 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -64,6 +64,8 @@ constexpr auto kSiblingUserpicSize = 0.3;
 constexpr auto kInnerHeightMultiplier = 1.6;
 constexpr auto kPreloadUsersCount = 3;
 constexpr auto kPreloadStoriesCount = 5;
+constexpr auto kPreloadNextMediaCount = 3;
+constexpr auto kPreloadPreviousMediaCount = 1;
 constexpr auto kMarkAsReadAfterSeconds = 1;
 constexpr auto kMarkAsReadAfterProgress = 0.2;
 
@@ -637,9 +639,28 @@ void Controller::rebuildFromContext(
 			}
 		}
 	}
+	preloadNext();
 	_slider->show({ .index = _index, .total = shownCount() });
 }
 
+void Controller::preloadNext() {
+	Expects(shown());
+
+	auto ids = std::vector<FullStoryId>();
+	ids.reserve(kPreloadPreviousMediaCount + kPreloadNextMediaCount);
+	const auto user = shownUser();
+	const auto count = shownCount();
+	const auto till = std::min(_index + kPreloadNextMediaCount, count);
+	for (auto i = _index + 1; i != till; ++i) {
+		ids.push_back({ .peer = user->id, .story = shownId(i) });
+	}
+	const auto from = std::max(_index - kPreloadPreviousMediaCount, 0);
+	for (auto i = _index; i != from;) {
+		ids.push_back({ .peer = user->id, .story = shownId(--i) });
+	}
+	user->owner().stories().setPreloadingInViewer(std::move(ids));
+}
+
 void Controller::checkMoveByDelta() {
 	const auto index = _index + _waitingForDelta;
 	if (_waitingForDelta && shown() && index >= 0 && index < shownCount()) {
@@ -659,7 +680,17 @@ void Controller::show(
 
 	rebuildFromContext(user, storyId);
 	_contextLifetime.destroy();
-	v::match(_context.data, [&](Data::StoriesContextSaved) {
+	const auto subscribeToSource = [&] {
+		stories.sourceChanged() | rpl::filter(
+			rpl::mappers::_1 == storyId.peer
+		) | rpl::start_with_next([=] {
+			rebuildFromContext(user, storyId);
+		}, _contextLifetime);
+	};
+	v::match(_context.data, [&](Data::StoriesContextSingle) {
+	}, [&](Data::StoriesContextPeer) {
+		subscribeToSource();
+	}, [&](Data::StoriesContextSaved) {
 		stories.savedChanged() | rpl::filter(
 			rpl::mappers::_1 == storyId.peer
 		) | rpl::start_with_next([=] {
@@ -672,7 +703,9 @@ void Controller::show(
 			rebuildFromContext(user, storyId);
 			checkMoveByDelta();
 		}, _contextLifetime);
-	}, [](const auto &) {});
+	}, [&](Data::StorySourcesList) {
+		subscribeToSource();
+	});
 
 	const auto guard = gsl::finally([&] {
 		_paused = false;
@@ -733,6 +766,9 @@ void Controller::show(
 				checkWaitingFor();
 			}
 		}, _sessionLifetime);
+		_sessionLifetime.add([=] {
+			session->data().stories().setPreloadingInViewer({});
+		});
 	}
 
 	stories.loadAround(storyId, context);
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index a680d7f68..89dda44d4 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -183,6 +183,7 @@ private:
 	void rebuildFromContext(not_null<UserData*> user, FullStoryId storyId);
 	void checkMoveByDelta();
 	void loadMoreToList();
+	void preloadNext();
 	void rebuildCachedSourcesList(
 		const std::vector<Data::StoriesSourceInfo> &lists,
 		int index);

From bc7da9309dc244013ed314568a60be09052a34d8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 13:36:12 +0400
Subject: [PATCH 110/259] Fix controls in regular media viewer.

---
 Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 30c1ae8aa..29415489f 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -5092,6 +5092,7 @@ void OverlayWidget::setStoriesPeer(PeerData *peer) {
 		_stories = nullptr;
 		_storiesSession = nullptr;
 		_storiesChanged.fire({});
+		updateNavigationControlsGeometry();
 	} else if (_storiesSession != session) {
 		_stories = nullptr;
 		_storiesSession = session;

From aa2cf2f6cae3e040b49e2ed2147fc265f0ddb1e2 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 13:36:25 +0400
Subject: [PATCH 111/259] Collapse stories on chat open.

---
 .../dialogs/dialogs_inner_widget.cpp            | 17 ++++++++++-------
 .../SourceFiles/dialogs/dialogs_inner_widget.h  |  3 ++-
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 11 +++++++----
 .../dialogs/ui/dialogs_stories_list.cpp         | 16 ++++++++++------
 .../dialogs/ui/dialogs_stories_list.h           |  7 +++++--
 5 files changed, 34 insertions(+), 20 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 1ab020d36..7587706a8 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -473,8 +473,10 @@ int InnerWidget::dialogsOffset() const {
 		- skipTopHeight();
 }
 
-rpl::producer<> InnerWidget::scrollToVeryTopRequests() const {
-	return _stories->expandRequests();
+rpl::producer<bool> InnerWidget::storiesExpandedRequests() const {
+	return rpl::merge(
+		_stories->toggleExpandedRequests(),
+		_storiesExpandedRequests.events());
 }
 
 int InnerWidget::defaultScrollTop() const {
@@ -623,11 +625,6 @@ 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);
@@ -648,6 +645,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
 		.paused = videoPaused,
 		.narrow = (fullWidth < st::columnMinimalWidthLeft / 2),
 	};
+	_stories->setBgOverride(context.currentBg);
+	const auto fillGuard = gsl::finally([&] {
+		// We translate painter down, but it'll be cropped below rect.
+		p.fillRect(rect(), context.currentBg);
+	});
 	const auto paintRow = [&](
 			not_null<Row*> row,
 			bool selected,
@@ -3408,6 +3410,7 @@ ChosenRow InnerWidget::computeChosenRow() const {
 bool InnerWidget::chooseRow(
 		Qt::KeyboardModifiers modifiers,
 		MsgId pressedTopicRootId) {
+	_storiesExpandedRequests.fire(false);
 	if (chooseCollapsedRow()) {
 		return true;
 	} else if (chooseHashtag()) {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index 1dcaf68fe..ce70f3bb3 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -104,7 +104,7 @@ public:
 		const QVector<MTPPeer> &my,
 		const QVector<MTPPeer> &result);
 
-	[[nodiscard]] rpl::producer<> scrollToVeryTopRequests() const;
+	[[nodiscard]] rpl::producer<bool> storiesExpandedRequests() const;
 	[[nodiscard]] int defaultScrollTop() const;
 	void setViewportHeight(int viewportHeight);
 
@@ -408,6 +408,7 @@ private:
 	const not_null<Window::SessionController*> _controller;
 
 	const std::unique_ptr<Stories::List> _stories;
+	rpl::event_stream<bool> _storiesExpandedRequests;
 	int _viewportHeight = 0;
 
 	not_null<IndexedList*> _shownList;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 008102df0..de20a5fac 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -261,9 +261,11 @@ Widget::Widget(
 		}
 	}, lifetime());
 
-	_inner->scrollToVeryTopRequests(
-	) | rpl::start_with_next([=] {
-		scrollToDefaultChecked(true);
+	_inner->storiesExpandedRequests(
+	) | rpl::start_with_next([=](bool expanded) {
+		if (expanded || _scroll->scrollTop() < _inner->defaultScrollTop()) {
+			scrollToDefaultChecked(expanded);
+		}
 	}, lifetime());
 
 	_inner->mustScrollTo(
@@ -1123,7 +1125,7 @@ void Widget::scrollToDefault(bool verytop) {
 	_scrollToAnimation.stop();
 	auto scrollTop = _scroll->scrollTop();
 	const auto scrollTo = verytop ? 0 : _inner->defaultScrollTop();
-	if (scrollTop <= scrollTo) {
+	if (scrollTop == scrollTo) {
 		return;
 	}
 	const auto maxAnimatedDelta = _scroll->height();
@@ -2017,6 +2019,7 @@ void Widget::dropEvent(QDropEvent *e) {
 
 void Widget::listScrollUpdated() {
 	const auto scrollTop = _scroll->scrollTop();
+	PROFILE_LOG(("SCROLLED: %1").arg(scrollTop));
 	_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
 	updateScrollUpVisibility();
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index c54803ef6..b8d9c5a5b 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -239,8 +239,8 @@ rpl::producer<ToggleShownRequest> List::toggleShown() const {
 	return _toggleShown.events();
 }
 
-rpl::producer<> List::expandRequests() const {
-	return _expandRequests.events();
+rpl::producer<bool> List::toggleExpandedRequests() const {
+	return _toggleExpandedRequests.events();
 }
 
 rpl::producer<> List::entered() const {
@@ -347,7 +347,7 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto readUserpicAppearingOpacity = lerp(_st.readOpacity, 0.);
 
 	auto p = QPainter(this);
-	p.fillRect(e->rect(), _st.bg);
+	p.fillRect(e->rect(), _bgOverride.value_or(_st.bg));
 	p.translate(0, height() - layout.shownHeight);
 
 	const auto drawSmall = (ratio < 1.);
@@ -670,7 +670,7 @@ void List::wheelEvent(QWheelEvent *e) {
 	const auto used = now - delta;
 	const auto next = std::clamp(used, 0, _scrollLeftMax);
 	if (next != now) {
-		_expandRequests.fire({});
+		_toggleExpandedRequests.fire(true);
 		_scrollLeft = next;
 		updateSelected();
 		checkLoadMore();
@@ -698,7 +698,7 @@ void List::mouseMoveEvent(QMouseEvent *e) {
 		if ((_lastMousePosition - *_mouseDownPosition).manhattanLength()
 			>= QApplication::startDragDistance()) {
 			if (_shownHeight() < _st.full.height) {
-				_expandRequests.fire({});
+				_toggleExpandedRequests.fire(true);
 			}
 			_dragging = true;
 			_startDraggingLeft = _scrollLeft;
@@ -742,13 +742,17 @@ void List::mouseReleaseEvent(QMouseEvent *e) {
 	updateSelected();
 	if (_selected == pressed) {
 		if (_selected < 0) {
-			_expandRequests.fire({});
+			_toggleExpandedRequests.fire(true);
 		} else if (_selected < _data.items.size()) {
 			_clicks.fire_copy(_data.items[_selected].element.id);
 		}
 	}
 }
 
+void List::setBgOverride(QBrush brush) {
+	_bgOverride = std::move(brush);
+}
+
 void List::contextMenuEvent(QContextMenuEvent *e) {
 	_menu = nullptr;
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 8381a4289..1f1cabb45 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -67,10 +67,12 @@ public:
 		rpl::producer<Content> content,
 		Fn<int()> shownHeight);
 
+	void setBgOverride(QBrush brush);
+
 	[[nodiscard]] rpl::producer<uint64> clicks() const;
 	[[nodiscard]] rpl::producer<uint64> showProfileRequests() const;
 	[[nodiscard]] rpl::producer<ToggleShownRequest> toggleShown() const;
-	[[nodiscard]] rpl::producer<> expandRequests() const;
+	[[nodiscard]] rpl::producer<bool> toggleExpandedRequests() const;
 	[[nodiscard]] rpl::producer<> entered() const;
 	[[nodiscard]] rpl::producer<> loadMoreRequests() const;
 
@@ -165,10 +167,11 @@ private:
 	rpl::event_stream<uint64> _clicks;
 	rpl::event_stream<uint64> _showProfileRequests;
 	rpl::event_stream<ToggleShownRequest> _toggleShown;
-	rpl::event_stream<> _expandRequests;
+	rpl::event_stream<bool> _toggleExpandedRequests;
 	rpl::event_stream<> _entered;
 	rpl::event_stream<> _loadMoreRequests;
 
+	std::optional<QBrush> _bgOverride;
 	Ui::Animations::Simple _shownAnimation;
 
 	QPoint _lastMousePosition;

From aff094f278b8cda86f2a2de6e122d72732994da6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 13:59:36 +0400
Subject: [PATCH 112/259] Prevent some accidental story closing.

---
 .../media/stories/media_stories_controller.cpp           | 9 +++++++++
 .../SourceFiles/media/stories/media_stories_controller.h | 1 +
 .../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 | 4 +++-
 5 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 40cd9fc4a..ab7e2ab48 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -505,6 +505,15 @@ ContentLayout Controller::contentLayout() const {
 	};
 }
 
+bool Controller::closeByClickAt(QPoint position) const {
+	const auto &current = _layout.current();
+	Assert(current.has_value());
+
+	return (position.x() < current->content.x() - st::storiesControlSize)
+		|| (position.x() > current->content.x() + current->content.width()
+			+ st::storiesControlSize);
+}
+
 Data::FileOrigin Controller::fileOrigin() const {
 	return Data::FileOriginStory(_shown.peer, _shown.story);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 89dda44d4..c7137a250 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -101,6 +101,7 @@ public:
 	[[nodiscard]] Layout layout() const;
 	[[nodiscard]] rpl::producer<Layout> layoutValue() const;
 	[[nodiscard]] ContentLayout contentLayout() const;
+	[[nodiscard]] bool closeByClickAt(QPoint position) 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 b79064e2e..79528eb11 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
@@ -51,6 +51,10 @@ ContentLayout View::contentLayout() const {
 	return _controller->contentLayout();
 }
 
+bool View::closeByClickAt(QPoint position) const {
+	return _controller->closeByClickAt(position);
+}
+
 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 dabb6952a..d2f62a0ac 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -60,6 +60,7 @@ public:
 	[[nodiscard]] QRect finalShownGeometry() const;
 	[[nodiscard]] rpl::producer<QRect> finalShownGeometryValue() const;
 	[[nodiscard]] ContentLayout contentLayout() const;
+	[[nodiscard]] bool closeByClickAt(QPoint position) const;
 	[[nodiscard]] SiblingView sibling(SiblingType type) const;
 	[[nodiscard]] Data::FileOrigin fileOrigin() const;
 	[[nodiscard]] TextWithEntities captionText() const;
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 29415489f..6fc85848f 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -5576,7 +5576,9 @@ void OverlayWidget::handleMouseRelease(
 				|| documentContentShown()
 				|| !documentBubbleShown()
 				|| !_docRect.contains(position)) {
-				close();
+				if (!_stories || _stories->closeByClickAt(position)) {
+					close();
+				}
 			}
 		}
 		_pressed = false;

From 7f583f86c0d4c2945e68e8413937df5c4d988712 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 14:49:04 +0400
Subject: [PATCH 113/259] Nice expand / collapse animations.

---
 .../SourceFiles/dialogs/dialogs_widget.cpp    |  12 +-
 .../dialogs/ui/dialogs_stories_list.cpp       | 132 ++++++++++++++----
 .../dialogs/ui/dialogs_stories_list.h         |  11 +-
 3 files changed, 125 insertions(+), 30 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index de20a5fac..03679cfb8 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -1137,7 +1137,16 @@ void Widget::scrollToDefault(bool verytop) {
 	startScrollUpButtonAnimation(false);
 
 	const auto scroll = [=] {
-		_scroll->scrollToY(qRound(_scrollToAnimation.value(scrollTo)));
+		const auto animated = qRound(_scrollToAnimation.value(scrollTo));
+		const auto animatedDelta = animated - scrollTo;
+		const auto realDelta = _scroll->scrollTop() - scrollTo;
+		if (realDelta * animatedDelta < 0) {
+			// We scrolled manually to the other side of target 'scrollTo'.
+			_scrollToAnimation.stop();
+		} else if (std::abs(realDelta) > std::abs(animatedDelta)) {
+			// We scroll by animation only if it gets us closer to target.
+			_scroll->scrollToY(animated);
+		}
 	};
 
 	_scrollToAnimation.start(
@@ -2019,7 +2028,6 @@ void Widget::dropEvent(QDropEvent *e) {
 
 void Widget::listScrollUpdated() {
 	const auto scrollTop = _scroll->scrollTop();
-	PROFILE_LOG(("SCROLLED: %1").arg(scrollTop));
 	_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
 	updateScrollUpVisibility();
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index b8d9c5a5b..f1490669f 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -18,8 +18,12 @@ namespace Dialogs::Stories {
 namespace {
 
 constexpr auto kSmallThumbsShown = 3;
-constexpr auto kSummaryExpandLeft = 1.5;
+constexpr auto kSummaryExpandLeft = 1;
 constexpr auto kPreloadPages = 2;
+constexpr auto kExpandAfterRatio = 0.85;
+constexpr auto kCollapseAfterRatio = 0.72;
+constexpr auto kFrictionRatio = 0.15;
+constexpr auto kSnapExpandedTimeout = crl::time(200);
 
 [[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) {
 	const auto &full = st.full;
@@ -33,6 +37,7 @@ constexpr auto kPreloadPages = 2;
 struct List::Layout {
 	int itemsCount = 0;
 	int shownHeight = 0;
+	float64 expandedRatio = 0.;
 	float64 ratio = 0.;
 	float64 thumbnailLeft = 0.;
 	float64 photoLeft = 0.;
@@ -56,7 +61,8 @@ List::List(
 	Fn<int()> shownHeight)
 : RpWidget(parent)
 , _st(st)
-, _shownHeight(shownHeight) {
+, _shownHeight(shownHeight)
+, _snapExpandedTimer([=] { requestExpanded(_expanded); }) {
 	setCursor(style::cur_default);
 
 	std::move(content) | rpl::start_with_next([=](Content &&content) {
@@ -251,6 +257,20 @@ rpl::producer<> List::loadMoreRequests() const {
 	return _loadMoreRequests.events();
 }
 
+void List::requestExpanded(bool expanded) {
+	_snapExpandedTimer.cancel();
+	if (_expanded != expanded) {
+		_expanded = expanded;
+		_expandedAnimation.start(
+			[=] { update(); },
+			_expanded ? 0. : 1.,
+			_expanded ? 1. : 0.,
+			st::slideWrapDuration,
+			anim::sineInOut);
+	}
+	_toggleExpandedRequests.fire_copy(_expanded);
+}
+
 void List::enterEventHook(QEnterEvent *e) {
 	_entered.fire({});
 }
@@ -259,12 +279,46 @@ void List::resizeEvent(QResizeEvent *e) {
 	updateScrollMax();
 }
 
-List::Layout List::computeLayout() const {
+void List::updateExpanding(int minHeight, int shownHeight, int fullHeight) {
+	Expects(shownHeight == minHeight || fullHeight > minHeight);
+
+	const auto ratio = (shownHeight == minHeight)
+		? 0.
+		: (float64(shownHeight - minHeight) / (fullHeight - minHeight));
+	if (_lastRatio == ratio) {
+		return;
+	}
+	const auto expanding = (ratio > _lastRatio);
+	_lastRatio = ratio;
+	const auto change = _expanded
+		? (!expanding && ratio < kCollapseAfterRatio)
+		: (expanding && ratio > kExpandAfterRatio);
+	if (change) {
+		requestExpanded(!_expanded);
+	}
+}
+
+List::Layout List::computeLayout() {
 	const auto &st = _st.small;
 	const auto &full = _st.full;
 	const auto shownHeight = std::max(_shownHeight(), st.height);
-	const auto ratio = float64(shownHeight - st.height)
+	if (_lastHeight != shownHeight) {
+		_lastHeight = shownHeight;
+		if (_lastHeight == st.height || _lastHeight == full.height) {
+			_snapExpandedTimer.cancel();
+		} else {
+			_snapExpandedTimer.callOnce(kSnapExpandedTimeout);
+		}
+	}
+	updateExpanding(st.height, shownHeight, full.height);
+
+	const auto expanded = _expandedAnimation.value(_expanded ? 1. : 0.);
+	const auto expandedRatio = float64(shownHeight - st.height)
 		/ (full.height - st.height);
+	const auto collapsedRatio = expandedRatio * kFrictionRatio;
+	const auto ratio = expandedRatio * expanded
+		+ collapsedRatio * (1. - expanded);
+
 	const auto lerp = [&](float64 a, float64 b) {
 		return a + (b - a) * ratio;
 	};
@@ -304,6 +358,7 @@ List::Layout List::computeLayout() const {
 	return Layout{
 		.itemsCount = itemsCount,
 		.shownHeight = shownHeight,
+		.expandedRatio = expandedRatio,
 		.ratio = ratio,
 		.thumbnailLeft = thumbnailLeft,
 		.photoLeft = photoLeft,
@@ -326,14 +381,24 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto &full = _st.full;
 	const auto layout = computeLayout();
 	const auto ratio = layout.ratio;
+	const auto expandRatio = (ratio >= kCollapseAfterRatio)
+		? 1.
+		: (ratio <= kExpandAfterRatio * kFrictionRatio)
+		? 0.
+		: ((ratio - kExpandAfterRatio * kFrictionRatio)
+			/ (kCollapseAfterRatio - kExpandAfterRatio * kFrictionRatio));
 	const auto lerp = [&](float64 a, float64 b) {
 		return a + (b - a) * ratio;
 	};
+	const auto elerp = [&](float64 a, float64 b) {
+		return a + (b - a) * expandRatio;
+	};
 	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 line = elerp(st.lineTwice, full.lineTwice) / 2.;
+	const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.;
 	const auto photoTopSmall = (st.height - st.photo) / 2.;
-	const auto photoTop = lerp(photoTopSmall, full.photoTop);
+	const auto photoTop = photoTopSmall
+		+ (full.photoTop - photoTopSmall) * layout.expandedRatio;
 	const auto photo = lerp(st.photo, full.photo);
 	const auto summaryTop = st.nameTop
 		- (st.photoTop + (st.photo / 2.))
@@ -343,15 +408,15 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto nameWidth = nameScale * AvailableNameWidth(_st);
 	const auto nameHeight = nameScale * full.nameStyle.font->height;
 	const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;
-	const auto readUserpicOpacity = lerp(_st.readOpacity, 1.);
-	const auto readUserpicAppearingOpacity = lerp(_st.readOpacity, 0.);
+	const auto readUserpicOpacity = elerp(_st.readOpacity, 1.);
+	const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.);
 
 	auto p = QPainter(this);
 	p.fillRect(e->rect(), _bgOverride.value_or(_st.bg));
 	p.translate(0, height() - layout.shownHeight);
 
-	const auto drawSmall = (ratio < 1.);
-	const auto drawFull = (ratio > 0.);
+	const auto drawSmall = (expandRatio < 1.);
+	const auto drawFull = (expandRatio > 0.);
 	auto hq = PainterHighQualityEnabler(p);
 
 	const auto count = std::max(
@@ -364,6 +429,7 @@ void List::paintEvent(QPaintEvent *e) {
 		Item *itemSmall = nullptr;
 		int indexFull = 0;
 		Item *itemFull = nullptr;
+		float64 photoTop = 0.;
 
 		explicit operator bool() const {
 			return itemSmall || itemFull;
@@ -372,6 +438,11 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto lookup = [&](int index) {
 		const auto indexSmall = layout.startIndexSmall + index;
 		const auto indexFull = layout.startIndexFull + index;
+		const auto ySmall = photoTopSmall
+			+ ((indexSmall - layout.smallSkip + 1)
+				* (photoTop - photoTopSmall) / 3.);
+		const auto y = elerp(ySmall, photoTop);
+
 		const auto small = (drawSmall
 			&& indexSmall < layout.endIndexSmall
 			&& indexSmall >= layout.smallSkip)
@@ -381,7 +452,7 @@ void List::paintEvent(QPaintEvent *e) {
 			? &rendering.items[indexFull]
 			: nullptr;
 		const auto x = layout.left + layout.single * index;
-		return Single{ x, indexSmall, small, indexFull, full };
+		return Single{ x, indexSmall, small, indexFull, full, y };
 	};
 	const auto hasUnread = [&](const Single &single) {
 		return (single.itemSmall && single.itemSmall->element.unread)
@@ -427,18 +498,23 @@ 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);
+			if (expandRatio > 0.) {
+				p.setOpacity(expandRatio);
+				p.drawImage(QRectF(
+					single.x + nameLeft,
+					nameTop,
+					nameWidth,
+					nameHeight
+				), full->nameCache);
+			}
 		}
 
 		// Unread gradient.
 		const auto x = single.x;
 		const auto userpic = QRectF(
 			x + layout.photoLeft,
-			photoTop,
+			single.photoTop,
 			photo,
 			photo);
 		const auto small = single.itemSmall;
@@ -448,9 +524,9 @@ void List::paintEvent(QPaintEvent *e) {
 		const auto unreadOpacity = (smallUnread && fullUnread)
 			? 1.
 			: smallUnread
-			? (1. - ratio)
+			? (1. - expandRatio)
 			: fullUnread
-			? ratio
+			? expandRatio
 			: 0.;
 		if (unreadOpacity > 0.) {
 			p.setOpacity(unreadOpacity);
@@ -475,7 +551,7 @@ void List::paintEvent(QPaintEvent *e) {
 		const auto x = single.x;
 		const auto userpic = QRectF(
 			x + layout.photoLeft,
-			photoTop,
+			single.photoTop,
 			photo,
 			photo);
 		const auto small = single.itemSmall;
@@ -487,7 +563,7 @@ void List::paintEvent(QPaintEvent *e) {
 		const auto hasReadLine = (itemFull && !fullUnread);
 		if (hasReadLine) {
 			auto color = st::dialogsUnreadBgMuted->c;
-			color.setAlphaF(color.alphaF() * ratio);
+			color.setAlphaF(color.alphaF() * expandRatio);
 			auto pen = QPen(color);
 			pen.setWidthF(lineRead);
 			p.setPen(pen);
@@ -508,16 +584,18 @@ void List::paintEvent(QPaintEvent *e) {
 		} else {
 			if (small) {
 				p.setOpacity(smallUnread
-					? (itemFull ? 1. : (1. - ratio))
+					? (itemFull ? 1. : (1. - expandRatio))
 					: (itemFull
 						? _st.readOpacity
 						: readUserpicAppearingOpacity));
 				validateThumbnail(small);
-				const auto size = (ratio > 0.) ? full.photo : st.photo;
+				const auto size = (expandRatio > 0.)
+					? full.photo
+					: st.photo;
 				p.drawImage(userpic, small->element.thumbnail->image(size));
 			}
 			if (itemFull) {
-				p.setOpacity(ratio);
+				p.setOpacity(expandRatio);
 				validateThumbnail(itemFull);
 				const auto size = full.photo;
 				p.drawImage(
@@ -670,7 +748,7 @@ void List::wheelEvent(QWheelEvent *e) {
 	const auto used = now - delta;
 	const auto next = std::clamp(used, 0, _scrollLeftMax);
 	if (next != now) {
-		_toggleExpandedRequests.fire(true);
+		requestExpanded(true);
 		_scrollLeft = next;
 		updateSelected();
 		checkLoadMore();
@@ -698,7 +776,7 @@ void List::mouseMoveEvent(QMouseEvent *e) {
 		if ((_lastMousePosition - *_mouseDownPosition).manhattanLength()
 			>= QApplication::startDragDistance()) {
 			if (_shownHeight() < _st.full.height) {
-				_toggleExpandedRequests.fire(true);
+				requestExpanded(true);
 			}
 			_dragging = true;
 			_startDraggingLeft = _scrollLeft;
@@ -742,7 +820,7 @@ void List::mouseReleaseEvent(QMouseEvent *e) {
 	updateSelected();
 	if (_selected == pressed) {
 		if (_selected < 0) {
-			_toggleExpandedRequests.fire(true);
+			requestExpanded(true);
 		} else if (_selected < _data.items.size()) {
 			_clicks.fire_copy(_data.items[_selected].element.id);
 		}
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 1f1cabb45..1259a0beb 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/timer.h"
 #include "base/weak_ptr.h"
 #include "ui/rp_widget.h"
 
@@ -148,7 +149,9 @@ private:
 	void checkDragging();
 	bool finishDragging();
 	void checkLoadMore();
+	void requestExpanded(bool expanded);
 
+	void updateExpanding(int minHeight, int shownHeight, int fullHeight);
 	void updateHeight();
 	void toggleAnimated(bool shown);
 	void paintSummary(
@@ -157,7 +160,7 @@ private:
 		float64 summaryTop,
 		float64 hidden);
 
-	[[nodiscard]] Layout computeLayout() const;
+	[[nodiscard]] Layout computeLayout();
 
 	const style::DialogsStoriesList &_st;
 	Content _content;
@@ -181,6 +184,12 @@ private:
 	int _scrollLeftMax = 0;
 	bool _dragging = false;
 
+	Ui::Animations::Simple _expandedAnimation;
+	base::Timer _snapExpandedTimer;
+	float64 _lastRatio = 0.;
+	int _lastHeight = 0;
+	bool _expanded = false;
+
 	int _selected = -1;
 	int _pressed = -1;
 

From c61e1b9139e89c7b95b498d228d90436dbb1303d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 19:40:45 +0400
Subject: [PATCH 114/259] Don't scroll through from chats list to stories.

---
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 28 +++++++++++++++++++
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  2 ++
 Telegram/lib_ui                               |  2 +-
 3 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 03679cfb8..9268b4fcd 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -73,11 +73,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/qt/qt_common_adapters.h"
 
 #include <QtCore/QMimeData>
+#include <QtWidgets/QScrollBar>
 
 namespace Dialogs {
 namespace {
 
 constexpr auto kSearchPerPage = 50;
+constexpr auto kWaitTillAllowStoriesExpand = crl::time(200);
 
 } // namespace
 
@@ -208,6 +210,9 @@ Widget::Widget(
 , _cancelSearch(_searchControls, st::dialogsCancelSearch)
 , _lockUnlock(_searchControls, st::dialogsLock)
 , _scroll(this)
+, _allowStoriesExpandTimer([=] {
+	_scroll->verticalScrollBar()->setMinimum(0);
+})
 , _scrollToTop(_scroll, st::dialogsToUp)
 , _searchTimer([=] { searchMessages(); })
 , _singleMessageSearch(&controller->session()) {
@@ -319,6 +324,9 @@ Widget::Widget(
 	) | rpl::start_with_next([=] {
 		listScrollUpdated();
 	}, lifetime());
+	_scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
+		return customWheelProcess(e);
+	});
 
 	session().data().chatsListChanges(
 	) | rpl::filter([=](Data::Folder *folder) {
@@ -1122,6 +1130,9 @@ void Widget::jumpToTop(bool belowPinned) {
 }
 
 void Widget::scrollToDefault(bool verytop) {
+	if (verytop) {
+		_scroll->verticalScrollBar()->setMinimum(0);
+	}
 	_scrollToAnimation.stop();
 	auto scrollTop = _scroll->scrollTop();
 	const auto scrollTo = verytop ? 0 : _inner->defaultScrollTop();
@@ -2365,6 +2376,23 @@ void Widget::completeHashtag(QString tag) {
 	applyFilterUpdate(true);
 }
 
+bool Widget::customWheelProcess(not_null<QWheelEvent*> e) {
+	const auto now = _scroll->scrollTop();
+	const auto def = _inner->defaultScrollTop();
+	const auto bar = _scroll->verticalScrollBar();
+	const auto allow = (def <= 0)
+		|| (now < def)
+		|| (now == def && !_allowStoriesExpandTimer.isActive());
+	if (allow) {
+		_scroll->verticalScrollBar()->setMinimum(0);
+		_allowStoriesExpandTimer.cancel();
+	} else {
+		bar->setMinimum(def);
+		_allowStoriesExpandTimer.callOnce(kWaitTillAllowStoriesExpand);
+	}
+	return false;
+}
+
 void Widget::resizeEvent(QResizeEvent *e) {
 	updateControlsGeometry();
 }
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 891afef3a..11a9e4648 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -136,6 +136,7 @@ private:
 	void cancelSearchInChat();
 	void filterCursorMoved();
 	void completeHashtag(QString tag);
+	bool customWheelProcess(not_null<QWheelEvent*> e);
 
 	[[nodiscard]] QString currentSearchQuery() const;
 	void clearSearchField();
@@ -245,6 +246,7 @@ private:
 	std::unique_ptr<HistoryView::ContactStatus> _forumReportBar;
 
 	object_ptr<Ui::ScrollArea> _scroll;
+	base::Timer _allowStoriesExpandTimer;
 	QPointer<InnerWidget> _inner;
 	class BottomButton;
 	object_ptr<BottomButton> _updateTelegram = { nullptr };
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index fc8d4d25d..8908c9b5c 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit fc8d4d25dee6e3ded99749c06fd870e6df4615f8
+Subproject commit 8908c9b5c041f51176383a55e0b324588b28b68d

From a73490e5ad6fe2277299aca3d0503e60b198107b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 19:40:59 +0400
Subject: [PATCH 115/259] Add simple scroll control to Hidden Stories.

---
 Telegram/SourceFiles/boxes/peer_list_box.cpp  |  3 +
 .../boxes/peer_list_controllers.cpp           | 61 +++++++++++++++++++
 2 files changed, 64 insertions(+)

diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index 66f10501a..2ceba422b 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -1254,6 +1254,9 @@ void PeerListContent::initDecorateWidget(Ui::RpWidget *widget) {
 		}) | rpl::start_with_next([=] {
 			mouseLeftGeometry();
 		}, widget->lifetime());
+		widget->heightValue() | rpl::skip(1) | rpl::start_with_next([=] {
+			resizeToWidth(width());
+		}, widget->lifetime());
 	}
 }
 
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 1e104b5a3..b1c87eaff 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -63,6 +63,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 			Stories::List *stories = nullptr;
 			QPointer<::Ui::IconButton> toggleSort;
 			Mode mode = ContactsBoxController::SortMode::Online;
+			::Ui::Animations::Simple scrollAnimation;
 		};
 
 		const auto stories = &sessionController->session().data().stories();
@@ -119,6 +120,66 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 			stories->loadMore(Data::StorySourcesList::Hidden);
 		}, raw->lifetime());
 
+		const auto defaultScrollTop = [=] {
+			return std::max(raw->height() - st::dialogsStories.height, 0);
+		};
+		const auto scrollToDefault = [=](bool verytop) {
+			if (state->scrollAnimation.animating()) {
+				return;
+			}
+			if (verytop) {
+				//_scroll->verticalScrollBar()->setMinimum(0);
+			}
+			state->scrollAnimation.stop();
+			auto scrollTop = box->scrollTop();
+			const auto scrollTo = verytop ? 0 : defaultScrollTop();
+			if (scrollTop == scrollTo) {
+				return;
+			}
+			const auto maxAnimatedDelta = box->height();
+			if (scrollTo + maxAnimatedDelta < scrollTop) {
+				scrollTop = scrollTo + maxAnimatedDelta;
+				box->scrollToY(scrollTop);
+			}
+
+			const auto scroll = [=] {
+				const auto animated = qRound(
+					state->scrollAnimation.value(scrollTo));
+				const auto animatedDelta = animated - scrollTo;
+				const auto realDelta = box->scrollTop() - scrollTo;
+				if (realDelta * animatedDelta < 0) {
+					// We scrolled manually to the other side of target 'scrollTo'.
+					state->scrollAnimation.stop();
+				} else if (std::abs(realDelta) > std::abs(animatedDelta)) {
+					// We scroll by animation only if it gets us closer to target.
+					box->scrollToY(animated);
+				}
+			};
+
+			state->scrollAnimation.start(
+				scroll,
+				scrollTop,
+				scrollTo,
+				st::slideDuration,
+				anim::sineInOut);
+		};
+		const auto top = box->scrollTop();
+		raw->toggleExpandedRequests(
+		) | rpl::start_with_next([=](bool expanded) {
+			if (expanded || box->scrollTop() < defaultScrollTop()) {
+				scrollToDefault(expanded);
+			}
+		}, raw->lifetime());
+
+		raw->heightValue(
+		) | rpl::filter([=] {
+			return (box->scrollHeight() > 0)
+				&& (defaultScrollTop() > box->scrollTop());
+		}) | rpl::start_with_next([=] {
+			//refreshForDefaultScroll();
+			box->scrollToY(defaultScrollTop());
+		}, raw->lifetime());
+
 		stories->incrementPreloadingHiddenSources();
 		raw->lifetime().add([=] {
 			stories->decrementPreloadingHiddenSources();

From b46659eb222c2a4f40cf01b78ce6ed4dc60120cc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 20:26:00 +0400
Subject: [PATCH 116/259] Restore scroll to collapsed stories in chats list.

---
 Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 7587706a8..6789e88de 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -3342,7 +3342,8 @@ void InnerWidget::saveChatsFilterScrollState(FilterId filterId) {
 void InnerWidget::restoreChatsFilterScrollState(FilterId filterId) {
 	const auto it = _chatsFilterScrollStates.find(filterId);
 	if (it != end(_chatsFilterScrollStates)) {
-		_mustScrollTo.fire({ it->second, -1 });
+		const auto top = std::max(it->second, defaultScrollTop());
+		_mustScrollTo.fire({ top, -1 });
 	}
 }
 

From fda2f56fd80b29f9b5267127246272bf85a2b83a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 20:26:43 +0400
Subject: [PATCH 117/259] Don't copy empty selected text.

---
 Telegram/SourceFiles/history/history_inner_widget.cpp       | 6 ++++--
 .../SourceFiles/history/view/history_view_context_menu.cpp  | 4 +++-
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 857803c17..cd2fa4685 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -2341,7 +2341,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 		const auto item = _dragStateItem;
 		const auto itemId = item ? item->fullId() : FullMsgId();
 		if (isUponSelected > 0) {
-			if (!hasCopyRestrictionForSelected()) {
+			if (!hasCopyRestrictionForSelected()
+				&& !getSelectedText().empty()) {
 				_menu->addAction(
 					(isUponSelected > 1
 						? tr::lng_context_copy_selected_items(tr::now)
@@ -2442,7 +2443,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 			: QString();
 
 		if (isUponSelected > 0) {
-			if (!hasCopyRestrictionForSelected()) {
+			if (!hasCopyRestrictionForSelected()
+				&& !getSelectedText().empty()) {
 				_menu->addAction(
 					((isUponSelected > 1)
 						? tr::lng_context_copy_selected_items(tr::now)
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
index 4992cca85..be51f4b50 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
@@ -995,7 +995,9 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
 		list,
 		st::popupMenuWithIcons);
 
-	if (request.overSelection && !list->hasCopyRestrictionForSelected()) {
+	if (request.overSelection
+		&& !list->hasCopyRestrictionForSelected()
+		&& !list->getSelectedText().empty()) {
 		const auto text = request.selectedItems.empty()
 			? tr::lng_context_copy_selected(tr::now)
 			: tr::lng_context_copy_selected_items(tr::now);

From fba1b7925246a26bc2007d8a6c9de331577ccae1 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 20:27:02 +0400
Subject: [PATCH 118/259] Don't delete files on Shift+Drop on Windows.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp    | 3 ++-
 Telegram/SourceFiles/history/history_drag_area.cpp | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 9268b4fcd..11f72ace9 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -2024,7 +2024,8 @@ void Widget::dropEvent(QDropEvent *e) {
 	if (_scroll->geometry().contains(e->pos())) {
 		const auto point = mapToGlobal(e->pos());
 		if (const auto thread = _inner->updateFromParentDrag(point)) {
-			e->acceptProposedAction();
+			e->setDropAction(Qt::CopyAction);
+			e->accept();
 			controller()->content()->filesOrForwardDrop(
 				thread,
 				e->mimeData());
diff --git a/Telegram/SourceFiles/history/history_drag_area.cpp b/Telegram/SourceFiles/history/history_drag_area.cpp
index 8b64a46bf..5c2cebf38 100644
--- a/Telegram/SourceFiles/history/history_drag_area.cpp
+++ b/Telegram/SourceFiles/history/history_drag_area.cpp
@@ -206,7 +206,8 @@ DragArea::Areas DragArea::SetupDragAreaToContainer(
 
 		*attachDragState = DragState::None;
 		updateDragAreas();
-		e->acceptProposedAction();
+		e->setDropAction(Qt::CopyAction);
+		e->accept();
 	};
 
 	const auto processDragEvents = [=](not_null<QEvent*> event) {

From 71e341237de50072baacc20221d7d8738cd3439b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 27 Jun 2023 21:25:36 +0400
Subject: [PATCH 119/259] Improve touchscreen chats list stories physics.

---
 .../dialogs/dialogs_inner_widget.cpp          |  4 +++
 .../dialogs/dialogs_inner_widget.h            |  1 +
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 25 +++++++++++++++++++
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  1 +
 .../dialogs/ui/dialogs_stories_list.cpp       | 13 +++++++++-
 .../dialogs/ui/dialogs_stories_list.h         |  2 ++
 Telegram/lib_ui                               |  2 +-
 7 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 6789e88de..35a19da9a 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -479,6 +479,10 @@ rpl::producer<bool> InnerWidget::storiesExpandedRequests() const {
 		_storiesExpandedRequests.events());
 }
 
+void InnerWidget::setTouchScrollActive(bool active) {
+	_stories->setTouchScrollActive(active);
+}
+
 int InnerWidget::defaultScrollTop() const {
 	return storiesShown()
 		? std::max(_stories->height() - st::dialogsStories.height, 0)
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index ce70f3bb3..b23aefa0d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -104,6 +104,7 @@ public:
 		const QVector<MTPPeer> &my,
 		const QVector<MTPPeer> &result);
 
+	void setTouchScrollActive(bool active);
 	[[nodiscard]] rpl::producer<bool> storiesExpandedRequests() const;
 	[[nodiscard]] int defaultScrollTop() const;
 	void setViewportHeight(int viewportHeight);
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 11f72ace9..74bbd6da8 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -327,6 +327,9 @@ Widget::Widget(
 	_scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
 		return customWheelProcess(e);
 	});
+	_scroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
+		return customTouchProcess(e);
+	});
 
 	session().data().chatsListChanges(
 	) | rpl::filter([=](Data::Folder *folder) {
@@ -856,6 +859,7 @@ void Widget::changeOpenedSubsection(
 		}
 		oldContentCache = grabForFolderSlideAnimation();
 	}
+	_scroll->verticalScrollBar()->setMinimum(0);
 	_showAnimation = nullptr;
 	destroyChildListCanvas();
 	change();
@@ -2394,6 +2398,27 @@ bool Widget::customWheelProcess(not_null<QWheelEvent*> e) {
 	return false;
 }
 
+bool Widget::customTouchProcess(not_null<QTouchEvent*> e) {
+	const auto type = e->type();
+	const auto now = _scroll->scrollTop();
+	const auto def = _inner->defaultScrollTop();
+	const auto bar = _scroll->verticalScrollBar();
+	_allowStoriesExpandTimer.cancel();
+	if (type == QEvent::TouchBegin
+		|| type == QEvent::TouchUpdate) {
+		_inner->setTouchScrollActive(true);
+		bar->setMinimum(0);
+	} else if (type == QEvent::TouchEnd || type == QEvent::TouchCancel) {
+		_inner->setTouchScrollActive(false);
+		if (def > 0 && now >= def) {
+			bar->setMinimum(def);
+		} else {
+			bar->setMinimum(0);
+		}
+	}
+	return false;
+}
+
 void Widget::resizeEvent(QResizeEvent *e) {
 	updateControlsGeometry();
 }
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 11a9e4648..901f0c609 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -137,6 +137,7 @@ private:
 	void filterCursorMoved();
 	void completeHashtag(QString tag);
 	bool customWheelProcess(not_null<QWheelEvent*> e);
+	bool customTouchProcess(not_null<QTouchEvent*> e);
 
 	[[nodiscard]] QString currentSearchQuery() const;
 	void clearSearchField();
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index f1490669f..a76cb5deb 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -306,7 +306,7 @@ List::Layout List::computeLayout() {
 		_lastHeight = shownHeight;
 		if (_lastHeight == st.height || _lastHeight == full.height) {
 			_snapExpandedTimer.cancel();
-		} else {
+		} else if (!_touchScrollActive) {
 			_snapExpandedTimer.callOnce(kSnapExpandedTimeout);
 		}
 	}
@@ -831,6 +831,17 @@ void List::setBgOverride(QBrush brush) {
 	_bgOverride = std::move(brush);
 }
 
+void List::setTouchScrollActive(bool active) {
+	if (_touchScrollActive != active) {
+		_touchScrollActive = active;
+		if (active) {
+			_snapExpandedTimer.cancel();
+		} else {
+			requestExpanded(_expanded);
+		}
+	}
+}
+
 void List::contextMenuEvent(QContextMenuEvent *e) {
 	_menu = nullptr;
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 1259a0beb..e1bebe00f 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -69,6 +69,7 @@ public:
 		Fn<int()> shownHeight);
 
 	void setBgOverride(QBrush brush);
+	void setTouchScrollActive(bool active);
 
 	[[nodiscard]] rpl::producer<uint64> clicks() const;
 	[[nodiscard]] rpl::producer<uint64> showProfileRequests() const;
@@ -189,6 +190,7 @@ private:
 	float64 _lastRatio = 0.;
 	int _lastHeight = 0;
 	bool _expanded = false;
+	bool _touchScrollActive = false;
 
 	int _selected = -1;
 	int _pressed = -1;
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 8908c9b5c..6fe9e0838 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 8908c9b5c041f51176383a55e0b324588b28b68d
+Subproject commit 6fe9e0838685b0a799057a9218c4beca0cd7c52e

From 24012a76b34f250dc27e1b51ac0f1ca1affbe034 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 28 Jun 2023 15:05:26 +0400
Subject: [PATCH 120/259] Fix stories snap-by-timeout on macOS.

---
 Telegram/lib_ui | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 6fe9e0838..a6d472ee6 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 6fe9e0838685b0a799057a9218c4beca0cd7c52e
+Subproject commit a6d472ee686dd5465d3e1ab58bea11a21d0dd24a

From 1cd20ff5e2f524f12cab3bbf89666e33eb5d6e1c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 28 Jun 2023 18:55:35 +0400
Subject: [PATCH 121/259] Implement custom scrolling using DirectManipulation.

Use it in Ui::RpWindow. This is an experiment.

Thanks Chromium and Firefox.
---
 Telegram/lib_base | 2 +-
 Telegram/lib_ui   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/lib_base b/Telegram/lib_base
index 4efa613fe..74be75339 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit 4efa613fe3fb5e1db8a4c53b08852cfe538d2601
+Subproject commit 74be75339d474df1a2863028ec146744597bd0bb
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index a6d472ee6..855f8f7b7 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit a6d472ee686dd5465d3e1ab58bea11a21d0dd24a
+Subproject commit 855f8f7b75f83127642eb5bbb1457f2087b3f1de

From 75d4ba7be1b3fdfbce698f24f9b5ee4b872c2846 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 29 Jun 2023 11:09:46 +0400
Subject: [PATCH 122/259] Use scroll phase information from wheel events.

---
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 49 ++++++++++++-------
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  1 +
 2 files changed, 31 insertions(+), 19 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 74bbd6da8..e921e8ec1 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -2382,40 +2382,51 @@ void Widget::completeHashtag(QString tag) {
 }
 
 bool Widget::customWheelProcess(not_null<QWheelEvent*> e) {
-	const auto now = _scroll->scrollTop();
-	const auto def = _inner->defaultScrollTop();
-	const auto bar = _scroll->verticalScrollBar();
-	const auto allow = (def <= 0)
-		|| (now < def)
-		|| (now == def && !_allowStoriesExpandTimer.isActive());
-	if (allow) {
-		_scroll->verticalScrollBar()->setMinimum(0);
-		_allowStoriesExpandTimer.cancel();
-	} else {
-		bar->setMinimum(def);
-		_allowStoriesExpandTimer.callOnce(kWaitTillAllowStoriesExpand);
-	}
+	customScrollProcess(e->phase());
 	return false;
 }
 
-bool Widget::customTouchProcess(not_null<QTouchEvent*> e) {
-	const auto type = e->type();
+void Widget::customScrollProcess(Qt::ScrollPhase phase) {
 	const auto now = _scroll->scrollTop();
 	const auto def = _inner->defaultScrollTop();
 	const auto bar = _scroll->verticalScrollBar();
-	_allowStoriesExpandTimer.cancel();
-	if (type == QEvent::TouchBegin
-		|| type == QEvent::TouchUpdate) {
+	if (phase == Qt::ScrollBegin || phase == Qt::ScrollUpdate) {
+		_allowStoriesExpandTimer.cancel();
 		_inner->setTouchScrollActive(true);
 		bar->setMinimum(0);
-	} else if (type == QEvent::TouchEnd || type == QEvent::TouchCancel) {
+	} else if (phase == Qt::ScrollEnd || phase == Qt::ScrollMomentum) {
+		_allowStoriesExpandTimer.cancel();
 		_inner->setTouchScrollActive(false);
 		if (def > 0 && now >= def) {
 			bar->setMinimum(def);
 		} else {
 			bar->setMinimum(0);
 		}
+	} else {
+		const auto allow = (def <= 0)
+			|| (now < def)
+			|| (now == def && !_allowStoriesExpandTimer.isActive());
+		if (allow) {
+			_scroll->verticalScrollBar()->setMinimum(0);
+			_allowStoriesExpandTimer.cancel();
+		} else {
+			bar->setMinimum(def);
+			_allowStoriesExpandTimer.callOnce(kWaitTillAllowStoriesExpand);
+		}
 	}
+}
+
+bool Widget::customTouchProcess(not_null<QTouchEvent*> e) {
+	const auto type = e->type();
+	customScrollProcess([&] {
+		switch (e->type()) {
+		case QEvent::TouchBegin: return Qt::ScrollBegin;
+		case QEvent::TouchUpdate: return Qt::ScrollUpdate;
+		case QEvent::TouchEnd:
+		case QEvent::TouchCancel: return Qt::ScrollEnd;
+		}
+		Unexpected("Touch event type in Widdget::customTouchProcess.");
+	}());
 	return false;
 }
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 901f0c609..686ce1b23 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -138,6 +138,7 @@ private:
 	void completeHashtag(QString tag);
 	bool customWheelProcess(not_null<QWheelEvent*> e);
 	bool customTouchProcess(not_null<QTouchEvent*> e);
+	void customScrollProcess(Qt::ScrollPhase phase);
 
 	[[nodiscard]] QString currentSearchQuery() const;
 	void clearSearchField();

From 3d795f2f67f07466271bc1949d3effbd14082c63 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 29 Jun 2023 14:48:14 +0400
Subject: [PATCH 123/259] Implement story mention messages.

---
 Telegram/CMakeLists.txt                       |   2 +
 Telegram/Resources/langs/lang.strings         |   5 +
 Telegram/SourceFiles/data/data_document.h     |   1 +
 .../SourceFiles/data/data_media_types.cpp     |  65 ++++++---
 Telegram/SourceFiles/data/data_media_types.h  |   8 +-
 Telegram/SourceFiles/data/data_story.cpp      |  19 ++-
 Telegram/SourceFiles/data/data_story.h        |   4 +-
 .../dialogs/ui/dialogs_stories_content.cpp    |  41 +++---
 .../dialogs/ui/dialogs_stories_content.h      |   7 +
 Telegram/SourceFiles/history/history_item.cpp |  59 +++++---
 Telegram/SourceFiles/history/history_item.h   |   2 +
 .../history/history_item_helpers.cpp          |   4 +-
 .../history/history_item_helpers.h            |   1 +
 .../history/view/history_view_element.cpp     |  13 +-
 .../view/media/history_view_premium_gift.cpp  |   4 +-
 .../view/media/history_view_premium_gift.h    |   2 +-
 .../view/media/history_view_service_box.cpp   |  12 +-
 .../view/media/history_view_service_box.h     |   2 +-
 .../view/media/history_view_story_mention.cpp | 134 ++++++++++++++++++
 .../view/media/history_view_story_mention.h   |  65 +++++++++
 .../media/history_view_theme_document.cpp     |   4 +-
 .../view/media/history_view_theme_document.h  |   2 +-
 .../media/history_view_userpic_suggestion.cpp |   4 +-
 .../media/history_view_userpic_suggestion.h   |   2 +-
 Telegram/SourceFiles/mtproto/scheme/api.tl    |   6 +-
 Telegram/lib_ui                               |   2 +-
 26 files changed, 386 insertions(+), 84 deletions(-)
 create mode 100644 Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
 create mode 100644 Telegram/SourceFiles/history/view/media/history_view_story_mention.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index a4ac996e8..329e16226 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -700,6 +700,8 @@ PRIVATE
     history/view/media/history_view_sticker_player.cpp
     history/view/media/history_view_sticker_player.h
     history/view/media/history_view_sticker_player_abstract.h
+    history/view/media/history_view_story_mention.cpp
+    history/view/media/history_view_story_mention.h
     history/view/media/history_view_theme_document.cpp
     history/view/media/history_view_theme_document.h
     history/view/media/history_view_userpic_suggestion.cpp
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index f0dddd290..23d4d4649 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1574,6 +1574,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_action_topic_icon_changed" = "{from} changed the {link} icon to {emoji}";
 "lng_action_topic_icon_removed" = "{from} removed the {link} icon";
 "lng_action_shared_chat_with_bot" = "You shared {chat} with {bot}";
+"lng_action_story_mention_me" = "You mentioned {user} in a story";
+"lng_action_story_mention" = "{user} mentioned you in a story";
+"lng_action_story_mention_button" = "View Story";
+"lng_action_story_mention_me_unavailable" = "The story where you mentioned {user} is no longer available.";
+"lng_action_story_mention_unavailable" = "The story where {user} mentioned you is no longer available.";
 
 "lng_premium_gift_duration_months#one" = "for {count} month";
 "lng_premium_gift_duration_months#other" = "for {count} months";
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index ded87abfd..9f013e373 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -301,6 +301,7 @@ private:
 		PossibleCoverThumbnail = 0x0400,
 		UseTextColor = 0x0800,
 		StoryDocument = 0x1000,
+		SilentVideo = 0x2000,
 	};
 	using Flags = base::flags<Flag>;
 	friend constexpr bool is_flag_type(Flag) { return true; };
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 79f99591d..ac5476a1b 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/view/media/history_view_slot_machine.h"
 #include "history/view/media/history_view_dice.h"
 #include "history/view/media/history_view_service_box.h"
+#include "history/view/media/history_view_story_mention.h"
 #include "history/view/media/history_view_premium_gift.h"
 #include "history/view/media/history_view_userpic_suggestion.h"
 #include "dialogs/ui/dialogs_message_view.h"
@@ -415,6 +416,10 @@ bool Media::storyExpired() const {
 	return false;
 }
 
+bool Media::storyMention() const {
+	return false;
+}
+
 bool Media::uploading() const {
 	return false;
 }
@@ -1979,20 +1984,31 @@ std::unique_ptr<HistoryView::Media> MediaWallPaper::createView(
 		std::make_unique<HistoryView::ThemeDocumentBox>(message, _paper));
 }
 
-MediaStory::MediaStory(not_null<HistoryItem*> parent, FullStoryId storyId)
+MediaStory::MediaStory(
+	not_null<HistoryItem*> parent,
+	FullStoryId storyId,
+	bool mention)
 : Media(parent)
-, _storyId(storyId) {
+, _storyId(storyId)
+, _mention(mention) {
 	const auto stories = &parent->history()->owner().stories();
 	if (const auto maybeStory = stories->lookup(storyId)) {
-		parent->setText((*maybeStory)->caption());
+		if (!_mention) {
+			parent->setText((*maybeStory)->caption());
+		}
 	} else {
 		if (maybeStory.error() == NoStory::Unknown) {
 			stories->resolve(storyId, crl::guard(this, [=] {
 				if (const auto maybeStory = stories->lookup(storyId)) {
-					parent->setText((*maybeStory)->caption());
+					if (!_mention) {
+						parent->setText((*maybeStory)->caption());
+					}
 				} else {
 					_expired = true;
 				}
+				if (_mention) {
+					parent->updateStoryMentionText();
+				}
 				parent->history()->owner().requestItemViewRefresh(parent);
 			}));
 		} else {
@@ -2002,7 +2018,7 @@ MediaStory::MediaStory(not_null<HistoryItem*> parent, FullStoryId storyId)
 }
 
 std::unique_ptr<Media> MediaStory::clone(not_null<HistoryItem*> parent) {
-	return std::make_unique<MediaStory>(parent, _storyId);
+	return std::make_unique<MediaStory>(parent, _storyId, false);
 }
 
 FullStoryId MediaStory::storyId() const {
@@ -2013,6 +2029,10 @@ bool MediaStory::storyExpired() const {
 	return _expired;
 }
 
+bool MediaStory::storyMention() const {
+	return _mention;
+}
+
 TextWithEntities MediaStory::notificationText() const {
 	const auto stories = &parent()->history()->owner().stories();
 	const auto maybeStory = stories->lookup(_storyId);
@@ -2064,12 +2084,17 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
 	const auto stories = &parent()->history()->owner().stories();
 	const auto maybeStory = stories->lookup(_storyId);
 	if (!maybeStory) {
-		realParent->setText(TextWithEntities());
+		if (!_mention) {
+			realParent->setText(TextWithEntities());
+		}
 		if (maybeStory.error() == Data::NoStory::Deleted) {
 			_expired = true;
 			return nullptr;
 		}
 		_expired = false;
+		if (_mention) {
+			return nullptr;
+		}
 		return std::make_unique<HistoryView::Photo>(
 			message,
 			realParent,
@@ -2078,19 +2103,25 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
 	}
 	_expired = false;
 	const auto story = *maybeStory;
-	realParent->setText(story->caption());
-	if (const auto photo = story->photo()) {
-		return std::make_unique<HistoryView::Photo>(
+	if (_mention) {
+		return std::make_unique<HistoryView::ServiceBox>(
 			message,
-			realParent,
-			photo,
-			spoiler);
+			std::make_unique<HistoryView::StoryMention>(message, story));
 	} else {
-		return std::make_unique<HistoryView::Gif>(
-			message,
-			realParent,
-			story->document(),
-			spoiler);
+		realParent->setText(story->caption());
+		if (const auto photo = story->photo()) {
+			return std::make_unique<HistoryView::Photo>(
+				message,
+				realParent,
+				photo,
+				spoiler);
+		} else {
+			return std::make_unique<HistoryView::Gif>(
+				message,
+				realParent,
+				story->document(),
+				spoiler);
+		}
 	}
 }
 
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index a77e26e46..f60bc82a1 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -115,6 +115,7 @@ public:
 	virtual const WallPaper *paper() const;
 	virtual FullStoryId storyId() const;
 	virtual bool storyExpired() const;
+	virtual bool storyMention() const;
 
 	virtual bool uploading() const;
 	virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;
@@ -570,12 +571,16 @@ private:
 
 class MediaStory final : public Media, public base::has_weak_ptr {
 public:
-	MediaStory(not_null<HistoryItem*> parent, FullStoryId storyId);
+	MediaStory(
+		not_null<HistoryItem*> parent,
+		FullStoryId storyId,
+		bool mention);
 
 	std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
 
 	FullStoryId storyId() const override;
 	bool storyExpired() const override;
+	bool storyMention() const override;
 
 	TextWithEntities notificationText() const override;
 	QString pinnedTextSubstring() const override;
@@ -594,6 +599,7 @@ public:
 
 private:
 	const FullStoryId _storyId;
+	const bool _mention = false;
 	bool _expired = false;
 
 };
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index cf31749f4..dd11bcc0e 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -248,28 +248,24 @@ bool Story::pinned() const {
 	return _pinned;
 }
 
-void Story::setIsPublic(bool isPublic) {
-	_isPublic = isPublic;
-}
-
 bool Story::isPublic() const {
 	return _isPublic;
 }
 
-void Story::setCloseFriends(bool closeFriends) {
-	_closeFriends = closeFriends;
-}
-
 bool Story::closeFriends() const {
 	return _closeFriends;
 }
 
+bool Story::forbidsForward() const {
+	return _noForwards;
+}
+
 bool Story::canDownload() const {
-	return _peer->isSelf();
+	return !forbidsForward() || _peer->isSelf();
 }
 
 bool Story::canShare() const {
-	return isPublic() && (pinned() || !expired());
+	return isPublic() && !forbidsForward() && (pinned() || !expired());
 }
 
 bool Story::canDelete() const {
@@ -375,6 +371,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 	const auto pinned = data.is_pinned();
 	const auto isPublic = data.is_public();
 	const auto closeFriends = data.is_close_friends();
+	const auto noForwards = data.is_noforwards();
 	auto caption = TextWithEntities{
 		data.vcaption().value_or_empty(),
 		Api::EntitiesFromMTP(
@@ -400,6 +397,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 		|| (_pinned != pinned)
 		|| (_isPublic != isPublic)
 		|| (_closeFriends != closeFriends)
+		|| (_noForwards != noForwards)
 		|| (_caption != caption)
 		|| (views >= 0 && _views != views)
 		|| (_recentViewers != recent);
@@ -410,6 +408,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 	_pinned = pinned;
 	_isPublic = isPublic;
 	_closeFriends = closeFriends;
+	_noForwards = noForwards;
 	_caption = std::move(caption);
 	if (views >= 0) {
 		_views = views;
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
index 1f4801efd..183e46808 100644
--- a/Telegram/SourceFiles/data/data_story.h
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -86,10 +86,9 @@ public:
 
 	void setPinned(bool pinned);
 	[[nodiscard]] bool pinned() const;
-	void setIsPublic(bool isPublic);
 	[[nodiscard]] bool isPublic() const;
-	void setCloseFriends(bool closeFriends);
 	[[nodiscard]] bool closeFriends() const;
+	[[nodiscard]] bool forbidsForward() const;
 
 	[[nodiscard]] bool canDownload() const;
 	[[nodiscard]] bool canShare() const;
@@ -128,6 +127,7 @@ private:
 	bool _pinned : 1 = false;
 	bool _isPublic : 1 = false;
 	bool _closeFriends : 1 = false;
+	bool _noForwards : 1 = false;
 
 };
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index acce7637e..e686811aa 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -144,11 +144,12 @@ QImage PeerUserpic::image(int size) {
 	const auto good = (_frame.width() == size * _frame.devicePixelRatio());
 	const auto key = _peer->userpicUniqueKey(_subscribed->view);
 	if (!good || (_subscribed->key != key && !waitingUserpicLoad())) {
+		const auto ratio = style::DevicePixelRatio();
 		_subscribed->key = key;
 		_frame = QImage(
-			QSize(size, size) * style::DevicePixelRatio(),
+			QSize(size, size) * ratio,
 			QImage::Format_ARGB32_Premultiplied);
-		_frame.setDevicePixelRatio(style::DevicePixelRatio());
+		_frame.setDevicePixelRatio(ratio);
 		_frame.fill(Qt::transparent);
 
 		auto p = Painter(&_frame);
@@ -216,6 +217,7 @@ QImage StoryThumbnail::image(int size) {
 				Qt::SmoothTransformation);
 		}
 		_prepared = Images::Circle(std::move(_prepared));
+		_prepared.setDevicePixelRatio(ratio);
 	}
 	return _prepared;
 }
@@ -309,6 +311,7 @@ QImage EmptyThumbnail::image(int size) {
 			QSize(size, size) * ratio,
 			QImage::Format_ARGB32_Premultiplied);
 		_cached.fill(Qt::black);
+		_cached.setDevicePixelRatio(ratio);
 	}
 	return _cached;
 }
@@ -336,7 +339,7 @@ Content State::next() {
 		if (const auto i = _userpics.find(user); i != end(_userpics)) {
 			userpic = i->second;
 		} else {
-			userpic = std::make_shared<PeerUserpic>(user);
+			userpic = MakeUserpicThumbnail(user);
 			_userpics.emplace(user, userpic);
 		}
 		result.elements.push_back({
@@ -375,19 +378,6 @@ rpl::producer<Content> ContentForSession(
 	};
 }
 
-[[nodiscard]] std::shared_ptr<Thumbnail> PrepareThumbnail(
-		not_null<Data::Story*> story) {
-	using Result = std::shared_ptr<Thumbnail>;
-	const auto id = story->fullId();
-	return v::match(story->media().data, [](v::null_t) -> Result {
-		return std::make_shared<EmptyThumbnail>();
-	}, [&](not_null<PhotoData*> photo) -> Result {
-		return std::make_shared<PhotoThumbnail>(photo, id);
-	}, [&](not_null<DocumentData*> video) -> Result {
-		return std::make_shared<VideoThumbnail>(video, id);
-	});
-}
-
 rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 	using namespace rpl::mappers;
 
@@ -432,7 +422,7 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 							result.elements.reserve(ids.size());
 							result.elements.push_back({
 								.id = uint64(id),
-								.thumbnail = PrepareThumbnail(*maybe),
+								.thumbnail = MakeStoryThumbnail(*maybe),
 								.unread = (id > readTill),
 							});
 						}
@@ -459,4 +449,21 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 	}) | rpl::flatten_latest();
 }
 
+std::shared_ptr<Thumbnail> MakeUserpicThumbnail(not_null<PeerData*> peer) {
+	return std::make_shared<PeerUserpic>(peer);
+}
+
+std::shared_ptr<Thumbnail> MakeStoryThumbnail(
+		not_null<Data::Story*> story) {
+	using Result = std::shared_ptr<Thumbnail>;
+	const auto id = story->fullId();
+	return v::match(story->media().data, [](v::null_t) -> Result {
+		return std::make_shared<EmptyThumbnail>();
+	}, [&](not_null<PhotoData*> photo) -> Result {
+		return std::make_shared<PhotoThumbnail>(photo, id);
+	}, [&](not_null<DocumentData*> video) -> Result {
+		return std::make_shared<VideoThumbnail>(video, id);
+	});
+}
+
 } // namespace Dialogs::Stories
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
index 8b25c8313..bdbb3afea 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace Data {
 enum class StorySourcesList : uchar;
+class Story;
 } // namespace Data
 
 namespace Main {
@@ -18,6 +19,7 @@ class Session;
 namespace Dialogs::Stories {
 
 struct Content;
+class Thumbnail;
 
 [[nodiscard]] rpl::producer<Content> ContentForSession(
 	not_null<Main::Session*> session,
@@ -25,4 +27,9 @@ struct Content;
 
 [[nodiscard]] rpl::producer<Content> LastForPeer(not_null<PeerData*> peer);
 
+[[nodiscard]] std::shared_ptr<Thumbnail> MakeUserpicThumbnail(
+	not_null<PeerData*> peer);
+[[nodiscard]] std::shared_ptr<Thumbnail> MakeStoryThumbnail(
+	not_null<Data::Story*> story);
+
 } // namespace Dialogs::Stories
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 039a7bc8d..72745e74d 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -294,7 +294,7 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
 		return std::make_unique<Data::MediaStory>(item, FullStoryId{
 			peerFromUser(media.vuser_id()),
 			media.vid().v,
-		});
+		}, media.is_via_mention());
 	}, [](const MTPDmessageMediaEmpty &) -> Result {
 		return nullptr;
 	}, [](const MTPDmessageMediaUnsupported &) -> Result {
@@ -330,6 +330,10 @@ HistoryItem::HistoryItem(
 	} else if (checked == MediaCheckResult::HasTimeToLive) {
 		createServiceFromMtp(data);
 		applyTTL(data);
+	} else if (checked == MediaCheckResult::HasStoryMention) {
+		setMedia(*data.vmedia());
+		createServiceFromMtp(data);
+		applyTTL(data);
 	} else {
 		createComponents(data);
 		if (const auto media = data.vmedia()) {
@@ -1055,6 +1059,10 @@ void HistoryItem::updateServiceText(PreparedServiceText &&text) {
 	_history->owner().updateDependentMessages(this);
 }
 
+void HistoryItem::updateStoryMentionText() {
+	setServiceText(prepareStoryMentionText());
+}
+
 HistoryMessageReplyMarkup *HistoryItem::inlineReplyMarkup() {
 	if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
 		if (markup->data.flags & ReplyMarkupFlag::Inline) {
@@ -3386,15 +3394,13 @@ void HistoryItem::refreshSentMedia(const MTPMessageMedia *media) {
 void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
 	AddComponents(HistoryServiceData::Bit());
 
+	const auto unread = message.is_media_unread();
 	const auto media = message.vmedia();
 	Assert(media != nullptr);
 
-	const auto mediaType = media->type();
-	switch (mediaType) {
-	case mtpc_messageMediaPhoto: {
-		if (message.is_media_unread()) {
-			const auto &photo = media->c_messageMediaPhoto();
-			const auto ttl = photo.vttl_seconds();
+	media->match([&](const MTPDmessageMediaPhoto &data) {
+		if (unread) {
+			const auto ttl = data.vttl_seconds();
 			Assert(ttl != nullptr);
 
 			setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, ttl->v);
@@ -3417,11 +3423,9 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
 				tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities)
 			});
 		}
-	} break;
-	case mtpc_messageMediaDocument: {
-		if (message.is_media_unread()) {
-			const auto &document = media->c_messageMediaDocument();
-			const auto ttl = document.vttl_seconds();
+	}, [&](const MTPDmessageMediaDocument &data) {
+		if (unread) {
+			const auto ttl = data.vttl_seconds();
 			Assert(ttl != nullptr);
 
 			setSelfDestruct(HistoryServiceSelfDestruct::Type::Video, ttl->v);
@@ -3444,10 +3448,11 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) {
 				tr::lng_ttl_video_expired(tr::now, Ui::Text::WithEntities)
 			});
 		}
-	} break;
-
-	default: Unexpected("Media type in HistoryItem::createServiceFromMtp()");
-	}
+	}, [&](const MTPDmessageMediaStory &data) {
+		setServiceText(prepareStoryMentionText());
+	}, [](const auto &) {
+		Unexpected("Media type in HistoryItem::createServiceFromMtp()");
+	});
 
 	if (const auto reactions = message.vreactions()) {
 		updateReactions(reactions);
@@ -4816,6 +4821,28 @@ PreparedServiceText HistoryItem::preparePaymentSentText() {
 	return result;
 }
 
+PreparedServiceText HistoryItem::prepareStoryMentionText() {
+	auto result = PreparedServiceText();
+	const auto peer = history()->peer;
+	result.links.push_back(peer->createOpenLink());
+	const auto phrase = (this->media() && this->media()->storyExpired())
+		? (out()
+			? tr::lng_action_story_mention_me_unavailable
+			: tr::lng_action_story_mention_unavailable)
+		: (out()
+			? tr::lng_action_story_mention_me
+			: tr::lng_action_story_mention);
+	result.text = phrase(
+		tr::now,
+		lt_user,
+		Ui::Text::Wrapped(
+			Ui::Text::Bold(peer->shortName()),
+			EntityType::CustomUrl,
+			u"internal:index"_q + QChar(1)),
+		Ui::Text::WithEntities);
+	return result;
+}
+
 PreparedServiceText HistoryItem::prepareCallScheduledText(
 		TimeId scheduleDate) {
 	const auto call = Get<HistoryServiceOngoingCall>();
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index a5bbb3ced..973141ba5 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -196,6 +196,7 @@ public:
 	void checkBuyButton();
 
 	void updateServiceText(PreparedServiceText &&text);
+	void updateStoryMentionText();
 
 	[[nodiscard]] UserData *viaBot() const;
 	[[nodiscard]] UserData *getMessageBot() const;
@@ -608,6 +609,7 @@ private:
 	[[nodiscard]] PreparedServiceText preparePinnedText();
 	[[nodiscard]] PreparedServiceText prepareGameScoreText();
 	[[nodiscard]] PreparedServiceText preparePaymentSentText();
+	[[nodiscard]] PreparedServiceText prepareStoryMentionText();
 	[[nodiscard]] PreparedServiceText prepareInvitedToCallText(
 		const std::vector<not_null<UserData*>> &users,
 		CallId linkCallId);
diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp
index df971938c..c89788248 100644
--- a/Telegram/SourceFiles/history/history_item_helpers.cpp
+++ b/Telegram/SourceFiles/history/history_item_helpers.cpp
@@ -450,7 +450,9 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) {
 	}, [](const MTPDmessageMediaDice &) {
 		return Result::Good;
 	}, [](const MTPDmessageMediaStory &data) {
-		return Result::Good;
+		return data.is_via_mention()
+			? Result::HasStoryMention
+			: Result::Good;
 	}, [](const MTPDmessageMediaUnsupported &) {
 		return Result::Unsupported;
 	});
diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h
index 2d9765886..e7ed74290 100644
--- a/Telegram/SourceFiles/history/history_item_helpers.h
+++ b/Telegram/SourceFiles/history/history_item_helpers.h
@@ -44,6 +44,7 @@ enum class MediaCheckResult {
 	Unsupported,
 	Empty,
 	HasTimeToLive,
+	HasStoryMention,
 };
 [[nodiscard]] MediaCheckResult CheckMessageMedia(
 	const MTPMessageMedia &media);
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index 6a3f78b8a..f20499bb9 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -841,13 +841,18 @@ void Element::validateText() {
 	const auto item = data();
 	const auto &text = item->_text;
 	const auto media = item->media();
+	const auto storyMention = media && media->storyMention();
 	if (media && media->storyExpired()) {
 		_media = nullptr;
-		if (_text.isEmpty()) {
-			setTextWithLinks(
-				Ui::Text::Italic(u"This story has expired"_q));
+		if (!storyMention) {
+			if (_text.isEmpty()) {
+				setTextWithLinks(
+					Ui::Text::Italic(u"This story has expired"_q));
+			}
+			return;
 		}
-	} else if (_text.isEmpty() == text.empty()) {
+	}
+	if (_text.isEmpty() == text.empty()) {
 	} else if (_flags & Flag::ServiceMessage) {
 		const auto contextDependentText = contextDependentServiceText();
 		const auto &markedText = contextDependentText.text.empty()
diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
index 252b1e621..ec292be93 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
@@ -54,8 +54,8 @@ QString PremiumGift::title() {
 	return tr::lng_premium_summary_title(tr::now);
 }
 
-QString PremiumGift::subtitle() {
-	return FormatGiftMonths(_gift->months());
+TextWithEntities PremiumGift::subtitle() {
+	return { FormatGiftMonths(_gift->months()) };
 }
 
 QString PremiumGift::button() {
diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
index 53431ad2b..edb79ad52 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
@@ -26,7 +26,7 @@ public:
 	int top() override;
 	QSize size() override;
 	QString title() override;
-	QString subtitle() override;
+	TextWithEntities subtitle() override;
 	QString button() override;
 	void draw(
 		Painter &p,
diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp
index e129d28f5..e425bdf65 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "lang/lang_keys.h"
 #include "ui/chat/chat_style.h"
 #include "ui/effects/ripple_animation.h"
+#include "ui/text/text_utilities.h"
 #include "ui/painter.h"
 #include "styles/style_chat.h"
 #include "styles/style_premium.h"
@@ -57,8 +58,15 @@ ServiceBox::ServiceBox(
 	_maxWidth)
 , _subtitle(
 	st::premiumPreviewAbout.style,
-	_content->subtitle(),
-	kDefaultTextOptions,
+	Ui::Text::Filtered(
+		_content->subtitle(),
+		{
+			EntityType::Bold,
+			EntityType::StrikeOut,
+			EntityType::Underline,
+			EntityType::Italic,
+		}),
+	kMarkupTextOptions,
 	_maxWidth)
 , _size(
 	st::msgServiceGiftBoxSize.width(),
diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h
index a986b01a8..94347d401 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h
@@ -22,7 +22,7 @@ public:
 	[[nodiscard]] virtual int top() = 0;
 	[[nodiscard]] virtual QSize size() = 0;
 	[[nodiscard]] virtual QString title() = 0;
-	[[nodiscard]] virtual QString subtitle() = 0;
+	[[nodiscard]] virtual TextWithEntities subtitle() = 0;
 	[[nodiscard]] virtual QString button() = 0;
 	virtual void draw(
 		Painter &p,
diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
new file mode 100644
index 000000000..fb9f62f29
--- /dev/null
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
@@ -0,0 +1,134 @@
+/*
+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 "history/view/media/history_view_story_mention.h"
+
+#include "core/click_handler_types.h" // ClickHandlerContext
+#include "data/data_document.h"
+#include "data/data_photo.h"
+#include "data/data_user.h"
+#include "data/data_photo_media.h"
+#include "data/data_file_click_handler.h"
+#include "data/data_session.h"
+#include "data/data_stories.h"
+#include "dialogs/ui/dialogs_stories_content.h"
+#include "dialogs/ui/dialogs_stories_list.h"
+#include "editor/photo_editor_common.h"
+#include "editor/photo_editor_layer_widget.h"
+#include "history/history.h"
+#include "history/history_item.h"
+#include "history/view/media/history_view_sticker_player_abstract.h"
+#include "history/view/history_view_element.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "window/window_session_controller.h"
+#include "ui/boxes/confirm_box.h"
+#include "ui/text/text_utilities.h"
+#include "ui/toast/toast.h"
+#include "ui/painter.h"
+#include "mainwidget.h"
+#include "apiwrap.h"
+#include "api/api_peer_photo.h"
+#include "settings/settings_information.h" // UpdatePhotoLocally
+#include "styles/style_chat.h"
+
+namespace HistoryView {
+namespace {
+
+} // namespace
+
+StoryMention::StoryMention(
+	not_null<Element*> parent,
+	not_null<Data::Story*> story)
+: _parent(parent)
+, _story(story) {
+}
+
+StoryMention::~StoryMention() = default;
+
+int StoryMention::top() {
+	return st::msgServiceGiftBoxButtonMargins.top();
+}
+
+QSize StoryMention::size() {
+	return { st::msgServicePhotoWidth, st::msgServicePhotoWidth };
+}
+
+QString StoryMention::title() {
+	return QString();
+}
+
+QString StoryMention::button() {
+	return tr::lng_action_story_mention_button(tr::now);
+}
+
+TextWithEntities StoryMention::subtitle() {
+	return _parent->data()->notificationText();
+}
+
+ClickHandlerPtr StoryMention::createViewLink() {
+	const auto itemId = _parent->data()->fullId();
+	return std::make_shared<LambdaClickHandler>(crl::guard(this, [=](
+			ClickContext) {
+		if (const auto photo = _story->photo()) {
+			_parent->delegate()->elementOpenPhoto(photo, itemId);
+		} else if (const auto video = _story->document()) {
+			_parent->delegate()->elementOpenDocument(video, itemId);
+		}
+	}));
+}
+
+void StoryMention::draw(
+		Painter &p,
+		const PaintContext &context,
+		const QRect &geometry) {
+	const auto showStory = !_story->forbidsForward();
+	if (!_thumbnail || _thumbnailFromStory != showStory) {
+		using namespace Dialogs::Stories;
+		const auto item = _parent->data();
+		const auto history = item->history();
+		_thumbnail = showStory
+			? MakeStoryThumbnail(_story)
+			: MakeUserpicThumbnail(item->out()
+				? history->session().user()
+				: history->peer);
+		_thumbnailFromStory = showStory;
+		_subscribed = false;
+	}
+	if (!_subscribed) {
+		_thumbnail->subscribeToUpdates([=] {
+			_parent->data()->history()->owner().requestViewRepaint(_parent);
+		});
+		_subscribed = true;
+	}
+
+	p.drawImage(
+		geometry.topLeft(),
+		_thumbnail->image(st::msgServicePhotoWidth));
+}
+
+void StoryMention::stickerClearLoopPlayed() {
+}
+
+std::unique_ptr<StickerPlayer> StoryMention::stickerTakePlayer(
+		not_null<DocumentData*> data,
+		const Lottie::ColorReplacements *replacements) {
+	return nullptr;
+}
+
+bool StoryMention::hasHeavyPart() {
+	return _subscribed;
+}
+
+void StoryMention::unloadHeavyPart() {
+	if (_subscribed) {
+		_subscribed = false;
+		_thumbnail->subscribeToUpdates(nullptr);
+	}
+}
+
+} // namespace HistoryView
diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.h b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
new file mode 100644
index 000000000..9cdacebcb
--- /dev/null
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
@@ -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
+*/
+#pragma once
+
+#include "history/view/media/history_view_media.h"
+#include "history/view/media/history_view_media_unwrapped.h"
+#include "history/view/media/history_view_service_box.h"
+
+namespace Data {
+class Story;
+} // namespace Data
+
+namespace Dialogs::Stories {
+class Thumbnail;
+} // namespace Dialogs::Stories
+
+namespace HistoryView {
+
+class StoryMention final
+	: public ServiceBoxContent
+	, public base::has_weak_ptr {
+public:
+	StoryMention(not_null<Element*> parent, not_null<Data::Story*> story);
+	~StoryMention();
+
+	int top() override;
+	QSize size() override;
+	QString title() override;
+	TextWithEntities subtitle() override;
+	QString button() override;
+	void draw(
+		Painter &p,
+		const PaintContext &context,
+		const QRect &geometry) override;
+	ClickHandlerPtr createViewLink() override;
+
+	bool hideServiceText() override {
+		return true;
+	}
+
+	void stickerClearLoopPlayed() override;
+	std::unique_ptr<StickerPlayer> stickerTakePlayer(
+		not_null<DocumentData*> data,
+		const Lottie::ColorReplacements *replacements) override;
+
+	bool hasHeavyPart() override;
+	void unloadHeavyPart() override;
+
+private:
+	using Thumbnail = Dialogs::Stories::Thumbnail;
+
+	const not_null<Element*> _parent;
+	const not_null<Data::Story*> _story;
+	std::shared_ptr<Thumbnail> _thumbnail;
+	bool _thumbnailFromStory = false;
+	bool _subscribed = false;
+
+};
+
+} // namespace HistoryView
diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp
index 0cd31f483..ad5dfcac1 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp
@@ -460,8 +460,8 @@ QString ThemeDocumentBox::title() {
 	return QString();
 }
 
-QString ThemeDocumentBox::subtitle() {
-	return _parent->data()->notificationText().text;
+TextWithEntities ThemeDocumentBox::subtitle() {
+	return _parent->data()->notificationText();
 }
 
 QString ThemeDocumentBox::button() {
diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h
index f923ca253..94549e951 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h
@@ -99,7 +99,7 @@ public:
 	int top() override;
 	QSize size() override;
 	QString title() override;
-	QString subtitle() override;
+	TextWithEntities subtitle() override;
 	QString button() override;
 	void draw(
 		Painter &p,
diff --git a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp
index 7056a7780..5c37ccb04 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp
@@ -211,8 +211,8 @@ QString UserpicSuggestion::button() {
 		: tr::lng_action_suggested_photo_button(tr::now);
 }
 
-QString UserpicSuggestion::subtitle() {
-	return _photo.parent()->data()->notificationText().text;
+TextWithEntities UserpicSuggestion::subtitle() {
+	return _photo.parent()->data()->notificationText();
 }
 
 ClickHandlerPtr UserpicSuggestion::createViewLink() {
diff --git a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h
index bed87c324..42ad65f44 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h
@@ -30,7 +30,7 @@ public:
 	int top() override;
 	QSize size() override;
 	QString title() override;
-	QString subtitle() override;
+	TextWithEntities subtitle() override;
 	QString button() override;
 	void draw(
 		Painter &p,
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index b9197b033..20a0dd4f4 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -1135,7 +1135,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#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;
+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 stories_preload:flags.4?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;
 
@@ -1530,7 +1530,7 @@ storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector<long>
 
 storyItemDeleted#51e6ee4f id:int = 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 min:flags.9?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
+storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
 
 userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = UserStories;
 
@@ -2087,7 +2087,7 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool;
 chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector<Peer>;
 chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = Updates;
 
-stories.sendStory#424cd47a flags:# pinned:flags.2?true media:InputMedia caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> random_id:long period:flags.3?int = Updates;
+stories.sendStory#424cd47a flags:# pinned:flags.2?true noforwards:flags.4?true media:InputMedia caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> 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<MessageEntity> privacy_rules:flags.2?Vector<InputPrivacyRule> = Updates;
 stories.deleteStories#b5d501d7 id:Vector<int> = Vector<int>;
 stories.togglePinned#51602944 id:Vector<int> pinned:Bool = Vector<int>;
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 855f8f7b7..ed00cd280 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 855f8f7b75f83127642eb5bbb1457f2087b3f1de
+Subproject commit ed00cd28098e1b04979e7b2dd3a582b7a8a6713a

From 1d5b57c39c4fd7db83662a65f4791463da6d9be5 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 29 Jun 2023 20:50:32 +0400
Subject: [PATCH 124/259] Add video story saved toast to viewer.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 .../media/view/media_view_overlay_widget.cpp  | 88 +++++++++++++------
 .../media/view/media_view_overlay_widget.h    |  9 +-
 Telegram/lib_ui                               |  2 +-
 4 files changed, 69 insertions(+), 31 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 23d4d4649..04a9b2710 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2502,6 +2502,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_mediaview_title" = "Media viewer";
 "lng_mediaview_saved_to" = "Image was saved to your {downloads} folder";
 "lng_mediaview_saved_images_to" = "Images were saved to your {downloads} folder";
+"lng_mediaview_video_saved_to" = "Video file was saved to your {downloads} folder";
 "lng_mediaview_downloads" = "Downloads";
 "lng_mediaview_playback_speed" = "Playback speed: {speed}";
 "lng_mediaview_rotate_video" = "Rotate video";
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 6fc85848f..1c83f2d5c 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -444,20 +444,8 @@ OverlayWidget::OverlayWidget()
 		? Core::App().settings().videoVolume()
 		: Core::Settings::kDefaultVolume;
 
-	const auto text = tr::lng_mediaview_saved_to(
-		tr::now,
-		lt_downloads,
-		Ui::Text::Link(
-			tr::lng_mediaview_downloads(tr::now),
-			"internal:show_saved_message"),
-		Ui::Text::WithEntities);
-	_saveMsgText.setMarkedText(st::mediaviewSaveMsgStyle, text);
-	_saveMsg = QRect(0, 0, _saveMsgText.maxWidth() + st::mediaviewSaveMsgPadding.left() + st::mediaviewSaveMsgPadding.right(), st::mediaviewSaveMsgStyle.font->height + st::mediaviewSaveMsgPadding.top() + st::mediaviewSaveMsgPadding.bottom());
-	_saveMsgImage = QImage(
-		_saveMsg.size() * cIntRetinaFactor(),
-		QImage::Format_ARGB32_Premultiplied);
 	_saveMsgTimer.setCallback([=, delay = st::mediaviewSaveMsgHiding] {
-		_saveMsgAnimation.start([=] { updateImage(); }, 1., 0., delay);
+		_saveMsgAnimation.start([=] { updateSaveMsg(); }, 1., 0., delay);
 	});
 
 	_docRectImage = QImage(
@@ -663,6 +651,40 @@ OverlayWidget::OverlayWidget()
 	orderWidgets();
 }
 
+void OverlayWidget::showSaveMsgToast(const QString &path, auto phrase) {
+	showSaveMsgToastWith(path, phrase(
+		tr::now,
+		lt_downloads,
+		Ui::Text::Link(
+			tr::lng_mediaview_downloads(tr::now),
+			"internal:show_saved_message"),
+		Ui::Text::WithEntities));
+}
+
+void OverlayWidget::showSaveMsgToastWith(
+		const QString &path,
+		const TextWithEntities &text) {
+	_saveMsgFilename = path;
+	_saveMsgText.setMarkedText(st::mediaviewSaveMsgStyle, text);
+	const auto w = _saveMsgText.maxWidth()
+		+ st::mediaviewSaveMsgPadding.left()
+		+ st::mediaviewSaveMsgPadding.right();
+	const auto h = st::mediaviewSaveMsgStyle.font->height
+		+ st::mediaviewSaveMsgPadding.top()
+		+ st::mediaviewSaveMsgPadding.bottom();
+	_saveMsg = QRect((width() - w) / 2, (height() - h) / 2, w, h);
+	const auto toIn = 1.;
+	const auto callback = [=](float64 value) {
+		updateSaveMsg();
+		if (!_saveMsgAnimation.animating()) {
+			_saveMsgTimer.callOnce(st::mediaviewSaveMsgShown);
+		}
+	};
+	const auto duration = st::mediaviewSaveMsgShowing;
+	_saveMsgAnimation.start(callback, 0., 1., duration);
+	updateSaveMsg();
+}
+
 void OverlayWidget::orderWidgets() {
 	_helper->orderWidgets();
 }
@@ -1104,6 +1126,13 @@ void OverlayWidget::documentUpdated(not_null<DocumentData*> document) {
 			: 0;
 		_streamed->controls->setLoadingProgress(ready, _document->size);
 	}
+	if (_stories
+		&& !_documentLoadingTo.isEmpty()
+		&& _document->location(true).isEmpty()) {
+		showSaveMsgToast(
+			base::take(_documentLoadingTo),
+			tr::lng_mediaview_video_saved_to);
+	}
 }
 
 void OverlayWidget::changingMsgId(FullMsgId newId, MsgId oldId) {
@@ -1995,6 +2024,7 @@ void OverlayWidget::assignMediaPointer(DocumentData *document) {
 		} else {
 			_documentMedia = nullptr;
 		}
+		_documentLoadingTo = QString();
 	}
 }
 
@@ -2002,6 +2032,7 @@ void OverlayWidget::assignMediaPointer(not_null<PhotoData*> photo) {
 	_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
 	_document = nullptr;
 	_documentMedia = nullptr;
+	_documentLoadingTo = QString();
 	if (_photo != photo) {
 		_photo = photo;
 		_photoMedia = _photo->createMediaView();
@@ -2349,6 +2380,9 @@ void OverlayWidget::downloadMedia() {
 					}, toName, manager.computeNextStartDate());
 				}
 			}
+			if (_stories && !toName.isEmpty()) {
+				showSaveMsgToast(toName, tr::lng_mediaview_video_saved_to);
+			}
 			location.accessDisable();
 		} else {
 			if (_document->filepath(true).isEmpty()
@@ -2357,7 +2391,15 @@ void OverlayWidget::downloadMedia() {
 					_message ? _message->fullId() : FullMsgId(),
 					_document,
 					DocumentSaveClickHandler::Mode::ToFile);
-				updateControls();
+				_documentLoadingTo = _document->loadingFilePath();
+				if (_stories && _documentLoadingTo.isEmpty()) {
+					toName = _document->filepath(true);
+					if (!toName.isEmpty()) {
+						showSaveMsgToast(
+							toName,
+							tr::lng_mediaview_video_saved_to);
+					}
+				}
 			} else {
 				_saveVisible = contentCanBeSaved();
 				update(_saveNavOver);
@@ -2393,19 +2435,9 @@ void OverlayWidget::downloadMedia() {
 		}
 	}
 	if (!toName.isEmpty()) {
-		_saveMsgFilename = toName;
-		const auto toIn = 1.;
-		_saveMsgAnimation.start(
-			[=](float64 value) {
-				updateImage();
-				if (value == toIn) {
-					_saveMsgTimer.callOnce(st::mediaviewSaveMsgShown);
-				}
-			},
-			0.,
-			toIn,
-			st::mediaviewSaveMsgShowing);
-		updateImage();
+		showSaveMsgToast(toName, (_stories && _document)
+			? tr::lng_mediaview_video_saved_to
+			: tr::lng_mediaview_saved_to);
 	}
 }
 
@@ -5879,7 +5911,7 @@ void OverlayWidget::handleTouchTimer() {
 	_touchRightButton = true;
 }
 
-void OverlayWidget::updateImage() {
+void OverlayWidget::updateSaveMsg() {
 	update(_saveMsg);
 }
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index d323a640e..d90cf8a02 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -278,7 +278,12 @@ private:
 	void showDropdown();
 	void handleTouchTimer();
 	void handleDocumentClick();
-	void updateImage();
+
+	void showSaveMsgToast(const QString &path, auto phrase);
+	void showSaveMsgToastWith(
+		const QString &path,
+		const TextWithEntities &text);
+	void updateSaveMsg();
 
 	void clearBeforeHide();
 	void clearAfterHide();
@@ -533,6 +538,7 @@ private:
 	rpl::lifetime _sessionLifetime;
 	PhotoData *_photo = nullptr;
 	DocumentData *_document = nullptr;
+	QString _documentLoadingTo;
 	std::shared_ptr<Data::PhotoMedia> _photoMedia;
 	std::shared_ptr<Data::DocumentMedia> _documentMedia;
 	base::flat_set<std::shared_ptr<Data::PhotoMedia>> _preloadPhotos;
@@ -689,7 +695,6 @@ private:
 
 	QString _saveMsgFilename;
 	QRect _saveMsg;
-	QImage _saveMsgImage;
 	Ui::Text::String _saveMsgText;
 	SavePhotoVideo _savePhotoVideoWhenLoaded = SavePhotoVideo::None;
 	// _saveMsgAnimation -> _saveMsgTimer -> _saveMsgAnimation.
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index ed00cd280..08f805486 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit ed00cd28098e1b04979e7b2dd3a582b7a8a6713a
+Subproject commit 08f80548668df2737905365b254624055642b937

From 074a4e3c929d84f57b4697be1af33a6ea19867bc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 11:48:18 +0400
Subject: [PATCH 125/259] Track stories deletion and refresh views.

---
 .../SourceFiles/data/data_media_types.cpp     | 10 ++++++-
 Telegram/SourceFiles/data/data_media_types.h  |  1 +
 Telegram/SourceFiles/data/data_msg_id.h       |  9 +++++++
 Telegram/SourceFiles/data/data_session.cpp    | 27 +++++++++++++++++++
 Telegram/SourceFiles/data/data_session.h      |  6 +++++
 Telegram/SourceFiles/data/data_stories.cpp    | 17 ++++++++----
 6 files changed, 64 insertions(+), 6 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index ac5476a1b..1367970fc 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -1991,7 +1991,10 @@ MediaStory::MediaStory(
 : Media(parent)
 , _storyId(storyId)
 , _mention(mention) {
-	const auto stories = &parent->history()->owner().stories();
+	const auto owner = &parent->history()->owner();
+	owner->registerStoryItem(storyId, parent);
+
+	const auto stories = &owner->stories();
 	if (const auto maybeStory = stories->lookup(storyId)) {
 		if (!_mention) {
 			parent->setText((*maybeStory)->caption());
@@ -2017,6 +2020,11 @@ MediaStory::MediaStory(
 	}
 }
 
+MediaStory::~MediaStory() {
+	const auto owner = &parent()->history()->owner();
+	owner->unregisterStoryItem(_storyId, parent());
+}
+
 std::unique_ptr<Media> MediaStory::clone(not_null<HistoryItem*> parent) {
 	return std::make_unique<MediaStory>(parent, _storyId, false);
 }
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index f60bc82a1..81e034c02 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -575,6 +575,7 @@ public:
 		not_null<HistoryItem*> parent,
 		FullStoryId storyId,
 		bool mention);
+	~MediaStory();
 
 	std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
 
diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h
index aa3bf1785..49357d88f 100644
--- a/Telegram/SourceFiles/data/data_msg_id.h
+++ b/Telegram/SourceFiles/data/data_msg_id.h
@@ -194,4 +194,13 @@ struct hash<MsgId> : private hash<int64> {
 	}
 };
 
+template <>
+struct hash<FullStoryId> {
+	size_t operator()(FullStoryId value) const {
+		return QtPrivate::QHashCombine().operator()(
+			std::hash<BareId>()(value.peer.value),
+			value.story);
+	}
+};
+
 } // namespace std
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 6c3100d4d..02437c67c 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -3881,6 +3881,33 @@ void Session::destroyAllCallItems() {
 	}
 }
 
+void Session::registerStoryItem(
+		FullStoryId id,
+		not_null<HistoryItem*> item) {
+	_storyItems[id].emplace(item);
+}
+
+void Session::unregisterStoryItem(
+		FullStoryId id,
+		not_null<HistoryItem*> item) {
+	const auto i = _storyItems.find(id);
+	if (i != _storyItems.end()) {
+		auto &items = i->second;
+		if (items.remove(item) && items.empty()) {
+			_storyItems.erase(i);
+		}
+	}
+}
+
+void Session::refreshStoryItemViews(FullStoryId id) {
+	const auto i = _storyItems.find(id);
+	if (i != _storyItems.end()) {
+		for (const auto item : i->second) {
+			requestItemViewRefresh(item);
+		}
+	}
+}
+
 void Session::documentMessageRemoved(not_null<DocumentData*> document) {
 	if (_documentItems.find(document) != _documentItems.end()) {
 		return;
diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h
index c287713d3..9ba13d1a9 100644
--- a/Telegram/SourceFiles/data/data_session.h
+++ b/Telegram/SourceFiles/data/data_session.h
@@ -637,6 +637,9 @@ public:
 		not_null<HistoryItem*> item);
 	void registerCallItem(not_null<HistoryItem*> item);
 	void unregisterCallItem(not_null<HistoryItem*> item);
+	void registerStoryItem(FullStoryId id, not_null<HistoryItem*> item);
+	void unregisterStoryItem(FullStoryId id, not_null<HistoryItem*> item);
+	void refreshStoryItemViews(FullStoryId id);
 
 	void documentMessageRemoved(not_null<DocumentData*> document);
 
@@ -949,6 +952,9 @@ private:
 		UserId,
 		base::flat_set<not_null<ViewElement*>>> _contactViews;
 	std::unordered_set<not_null<HistoryItem*>> _callItems;
+	std::unordered_map<
+		FullStoryId,
+		base::flat_set<not_null<HistoryItem*>>> _storyItems;
 
 	base::flat_set<not_null<WebPageData*>> _webpagesUpdated;
 	base::flat_set<not_null<GameData*>> _gamesUpdated;
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 86ee493e6..15710d021 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -331,6 +331,7 @@ Story *Stories::parseAndApply(
 		return nullptr;
 	}
 	const auto id = data.vid().v;
+	const auto fullId = FullStoryId{ peer->id, id };
 	auto &stories = _stories[peer->id];
 	const auto i = stories.find(id);
 	if (i != end(stories)) {
@@ -349,7 +350,6 @@ Story *Stories::parseAndApply(
 			}
 		}
 		if (mediaChanged) {
-			const auto fullId = result->fullId();
 			_preloaded.remove(fullId);
 			if (_preloading && _preloading->id() == fullId) {
 				_preloading = nullptr;
@@ -357,9 +357,11 @@ Story *Stories::parseAndApply(
 				rebuildPreloadSources(StorySourcesList::Hidden);
 				continuePreloading();
 			}
+			_owner->refreshStoryItemViews(fullId);
 		}
 		return result;
 	}
+	const auto wasDeleted = _deleted.remove(fullId);
 	const auto result = stories.emplace(id, std::make_unique<Story>(
 		id,
 		peer,
@@ -382,10 +384,14 @@ Story *Stories::parseAndApply(
 	}
 
 	if (expired) {
-		_expiring.remove(expires, result->fullId());
-		applyExpired(result->fullId());
+		_expiring.remove(expires, fullId);
+		applyExpired(fullId);
 	} else {
-		registerExpiring(expires, result->fullId());
+		registerExpiring(expires, fullId);
+	}
+
+	if (wasDeleted) {
+		_owner->refreshStoryItemViews(fullId);
 	}
 
 	return result;
@@ -605,7 +611,7 @@ void Stories::finalizeResolve(FullStoryId id) {
 void Stories::applyDeleted(FullStoryId id) {
 	applyRemovedFromActive(id);
 
-	_deleted.emplace(id);
+	_deleted.emplace(id).second;
 	const auto i = _stories.find(id.peer);
 	if (i != end(_stories)) {
 		const auto j = i->second.find(id.story);
@@ -639,6 +645,7 @@ void Stories::applyDeleted(FullStoryId id) {
 			if (_preloading && _preloading->id() == id) {
 				preloadFinished(id);
 			}
+			_owner->refreshStoryItemViews(id);
 			if (i->second.empty()) {
 				_stories.erase(i);
 			}

From 6c960243a922bcb4289937f8b9e4c4d3228e49be Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 12:48:42 +0400
Subject: [PATCH 126/259] Improve story source context menus.

---
 .../Resources/icons/menu/stories_archive.png  | Bin 0 -> 1847 bytes
 .../icons/menu/stories_archive@2x.png         | Bin 0 -> 2395 bytes
 .../icons/menu/stories_archive@3x.png         | Bin 0 -> 2952 bytes
 .../Resources/icons/menu/stories_saved.png    | Bin 0 -> 1935 bytes
 .../Resources/icons/menu/stories_saved@2x.png | Bin 0 -> 2521 bytes
 .../Resources/icons/menu/stories_saved@3x.png | Bin 0 -> 3136 bytes
 Telegram/Resources/langs/lang.strings         |   8 +--
 .../boxes/peer_list_controllers.cpp           |  22 +++----
 Telegram/SourceFiles/data/data_stories.cpp    |   3 +-
 Telegram/SourceFiles/data/data_stories.h      |   1 -
 .../dialogs/dialogs_inner_widget.cpp          |  14 +----
 .../dialogs/ui/dialogs_stories_content.cpp    |  59 ++++++++++++++++--
 .../dialogs/ui/dialogs_stories_content.h      |   9 +++
 .../dialogs/ui/dialogs_stories_list.cpp       |  48 +++++---------
 .../dialogs/ui/dialogs_stories_list.h         |  15 ++---
 Telegram/SourceFiles/ui/menu_icons.style      |   2 +
 16 files changed, 99 insertions(+), 82 deletions(-)
 create mode 100644 Telegram/Resources/icons/menu/stories_archive.png
 create mode 100644 Telegram/Resources/icons/menu/stories_archive@2x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_archive@3x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_saved.png
 create mode 100644 Telegram/Resources/icons/menu/stories_saved@2x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_saved@3x.png

diff --git a/Telegram/Resources/icons/menu/stories_archive.png b/Telegram/Resources/icons/menu/stories_archive.png
new file mode 100644
index 0000000000000000000000000000000000000000..d86b5dc87df7c9354b102987cacf282e43efe99b
GIT binary patch
literal 1847
zcmbVNZEVzJ9Pf<{2V@R{FchPuB#Vdi?Rq!v8cue*-q^^BxLe@(BJ0|2*W6uO+TPoS
zun`cKL13C8*_@~(V1R(a7hnvQK%$XNKt%W;L0%$akco^T6NXQ3w+k4QNRzft+u!s1
z{omWXXyN?4@h^>sASkcAjHv|oY37-m1+L1JoqNG;oLaV2hoA|Q&C>$4e*_TOH>Ik8
z5nwB5UWs6wpwx(1BBBB`1kIh7P&qy%8gPwRC&_N4zw0OhOM)9&;$R6@^@>5MtX>oS
z^$V-``Vj9Dka=&wa}zWmh=>LUCn8~4rxR`@#Y=;=`5H&y6vPO*5sxVd2iQfhSJ6b+
zg^?&v5F|Xug>g>7KF2}LhHZq6!U+nuQ7GZ09Yr)X2TmUdkkf=(x{@hPi-9LM5;P2z
z#_@POj>RcV(duy0<#OSK4Y%1)fI#)6Y;Xxw)@P0~Frv<Dl4?kb44aHxjS@B72rzZT
zhlrY?mGyL-K*aC_r{W|=m_DU|0-wRDQ7xPj7kFF@ixE*abbuu@ST(2^iXK${g*x;3
zF#!-;md$89(UwRgBcU6<7;qyUkSC(`s-!C7m7=ahHD2_^fSEH*Z&cc=iJYNmRf-ZG
z&D5e%lVO`3D}rBJBFTah*R79i5E;%8-H4erlqf<eqKdT9b{lP@UMK7{L5x6IMUZNf
zPlQr5VSfw?YDVA;?x|pbr)w20!U2b+2v;ZKs$7S_nK05`C9G&b7+6Q8tYBHXT-FUv
z=EZWxjR2o9NfKx$!8@I`B!N1_A`T_(_8Qbl)Q~7a)YRGt&fy|Ca&$kV@KH0fqx*&b
zx&KW~0<Fb`pP9#OII|?^GD!!?N~Sx)FD_57!V;XS0-EE^>UJZ%nO0Fi(yygwj=%_Q
zJSYOv|48=;Ojl}+IH!r8IuNa=mJ<$)$IU(*@dN*-i>Ysq^z0}c3<7hR8A0G7GsHw0
zOfC(Ks5KQN13{Mla>i5ToWxg8t8ZMp))xqDX>M+=crY|H<lkO#aMi=%i$_;I1jk=@
z`$OmUL}Rg8R_mltc0s|p!^zvj_wVgr`C#b$`GD8!&B@8}`_Bvv40Jc#9Ui)~tE;P{
zt*x!mT2fMS?c2_-75DZvHCYzt3=R$!*WJ8$bZbY)^c~jT-rk168!qi?E{@}`oM0FR
zd;9PK`2t;9T6$wvrQg5o$O<clJzrpH4~9Z5*C)AL2k-V@obU5-WKQt@w)q=1U2pX@
zuDpNi%2z<xh(=F;|6yC(h3<xe7SGhFQ|GN3W(Hq4CH4GSxp?u%j?fR+PHr7DW=!kZ
z3Awq@w(pJ~&%W@<<;$0kt;?U;Qnp}0R$kWo#j#kd=v4cmL-bYERX-5qxEY5$-QC?M
zHlxSHi4&bPeI$9ikfJ8<fj&R7W5;u!av%;5TcJ%YXW?WrS=}BE279E5`S~Y*?(3Mb
z9eS}C+wSQ(&~WG1v+w3j|7qX(?y(h~2i1^JU5}z@-n8PrzP_(5Yp5^2EPQWW{;OI3
zo^yMbfq*t|Sh*Ye<>l(|&Rx4c*aTmKoz5+tot>Y}&a%EE|9D{e(166U>>m`G{rjz3
zmMc>N>vu2RUszZ;HbAuc*Y`BFww@liex_+N$lb=?)f>L?{nluK#$C!M-pXYi=C7^X
Lw~*OWQoZ_bTX&P*

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_archive@2x.png b/Telegram/Resources/icons/menu/stories_archive@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2c59be33802eaa0d1781a5f0b985d2794cc3840
GIT binary patch
literal 2395
zcmbVO3s4hx8jm(0<&9XBR}D)C0ZDd~5CSXV87O#!3e5qb)p5x#$(AG=vx^B2tpr+%
zZMj1cEa)iM6VV<Bh^Hb#rM=>`M*tP^C|ElR(xbI_AS$A_;Wh0|d!6gf?CyVefB)b2
zeZTK9Te2nbRXb}pYX*a17au21rtgsXv|34D2e)f_>D!CiI2p-c*f^Wd6^yDw>lh5?
z54bdqN|PkOazf2PP$C247}Q!C&0vIv8?=Z#3!_*Wm;%>`fctG701HP&K&n6jNwm?J
z5|7K(VXx;VO69p(@(>gV4`YQIV46UUQ3%VRR%uAsAOhxjVR~(T4Farr2$dxQBFzR_
zX_76hXhMgvLO5Ku9D=wkVF(8aMgxTcegKOH@%SLb2YGxp6buW3V7`#GcmT9H9hwOz
zi(?m!(N7{kNl{uD1oe77N6+UFIt9oL2?+rq9?0XdX#|_h(NKtits%Xa7{nMU*Wp?U
zCp0WGBa%U6QzC$Nbs+||c9~W~F6u-p1{x48$mKxhnC5|~d>N<B)~V)=qjC^aVQNf6
zku;XOjMXX$iXfH5uTU?4{+R(<TZv@Z#xMG!Rxg_%shFK~7>k1ZBAS%uXfZGuBZ+LC
z9E;gWyXkF?Mhi#lFoYs>Qi4z|WopZk%Pd|XCy3>nifd3pPx}32113f&Oaz#-#)g8}
zd`QaW!GS!O$KMDA!Vt6ol@KVNne!`9!Ox*|&7cT{{3aNc!<mFmjnDz(YD9s7T8#o=
zEz1Z;6DmSS8>YSE&wC+}!0{TALNszLUMvFWm^nC(!U&(w%Lt^)1cM+UJA@m^X9sin
zLbf0?1I2=bkbox;EbSK)@@#Wvm-eIo=l<7pINe%^>UZ;)8_rx3a2!t3$;w&mh}W?l
ziz^k*ny&&Fk(;Yq1jx;4#ZX}JHU7I5SfJG_F`D#$q<aBI5}A}9(P5DaTCLwKCy;g?
zH22{`9N>S4IREV@J-Y;_2Z4FHJc8(l<spV?=*gv{M-*!8rROBmIbIwoP3!tT?GL_F
zNo!^*X6}4cZUOLf!uP$pX5V2Y@a}<t*s^37z}?g+{%qxkzFy@Meoa`k?4a-c8AHde
zTVUNYN#--tN8DQ(yH7oTK0=!2hDnoX?CUIrqR=jAT|t7+`=zD!HZ~+lw%EU**U!|}
z0w2b<w6)DnO--db2!+Dx>gugqw~mjG+uPf_)U9@NgTum}6*Ap>dV12|ctaE#8qf-g
zL?V;P^i}@s>}+9S;q>(M=D+axd_D*^Ha9od*W<alPaZz3aM~mi{qfZG#Kgo=)VuP_
z)^xVy#|IDkhlbqOuKmL6{=~$gBS$(;pYH5Um&qn)W`qvamoG<Ue(vn-JSey=zuMoQ
zlA4N7jErnksXlQX%+Jq{@O*mw%o(ZCGB0oT?Af!8jiB%Owzl_6O3HeC{QWoAU5)I!
zawT4;Yxc9*$l>hH&ws^aV{2PpdwFOmPoY@l|4p|~GZI{Ocwk`Q8?^QdJw^47oRa#J
zo~5Isnf)a#ei~G(g=B`e-r5@-;OAH0-rnBQ!Ve4Ec69XaUF<~r#fwvulTMLJrP8Wv
zWMt%WSJzGS<jIrWw+d)ADk@yOs+uhS85|Jp>z<l=qqun02P@b?u1t8Z#RbCguQzYX
z(3iH&orYbn^z|j(OR9S@IXQU{U1M!=<<97+rG>?(uZ^!Q`9Qek?wvb-hQgRmZZE(5
zE^Ut+WmSAP-Y@LOe|`Hc4~93h$HvBFGMUE{WZl74`{w56j8=X4dRN!mJ1ura6<xS+
zVP;0JyHr)>R(iX;*KufM#6|p`$BCMn$N(?KCPQ|1%+|PeRY+@7Q`t&kxH|}f$*CzR
zJ_U|@3eUH;#<bIEj4iILtlYn+u5N=vO?P+P>VNd~RK&iQ(t7S(w^!#Q!Et`A=ifUU
zTCU6P@y@siy^hw_8kyGC)?(+)5=nbUhr+vv)AetY#LCZDyS}-(Z*SS#Upme@I5^nd
zJldyB-|k)1Y1L8FKo?F?5pz7zbM<QD>EkBL4&|C3qOD5Y-QBNUx^$^@5B^<K@rufd
zia77E;L<%_Ugg(Md{@6irLwKrCxjkOPfxDTi+ldqQePXNknr*Odg;lAhTXe%eGc}s
z3ET_Lwu+31VE7%bu8yve+`4h&#<{$M;peTHC+=<X@Gu@r;nqKOw0iSU(veR8w2c>6
zeA;WYsCtLhUewA=U_Wr}RL~;F3_GxOcEF>@kCSfa@7Pf=)N}yc%WO@T)UQ1ddMNeU
zlds-B;De&5ho@)RwsK;4c=+74^zWMr_Iy@U6!DUO<f-fSwzk&>1}e(SS;Wzlr|o0w
zES;R3_V3^S<cwkcCy%nCt`;0TF7+3<*?nzH9{ke=(Zo0?8h3rCv9a+@&fi9l)!79-
b?Rd0mLt_MWZ1Q7A^Y2f5Orp3UYP<120HT}I

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_archive@3x.png b/Telegram/Resources/icons/menu/stories_archive@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..f6369c74cd52818298a5dcf01c0280391ccde761
GIT binary patch
literal 2952
zcmbVO2~-o;8V)`{1kox|u(E`>fJ^p-kdZ|sNKhk70z?bNAu}N%kcmlvB%&xNi-H2B
zE>%#Z;!y;Ja}=uxNCN^Xh;0?csI?aQEC{F=K(KuiL{8h|dp)l?XEJy0eE0j;?@zLW
zHw9Q*eqxEiV5|e#tWfmMHeMDp(C_YpOFlwxvt;be3Jk`|#(0@vzC3P^!I%z+d3+_G
zvk?#=2?P*AVqk(QL589+7*8LS3=~LUB{l|*6-&KvgV*YDSTW><i=cBz9GM?160<dO
zn5)^u6KEs?CWQ0x#(Js%lpq0Cf>>2Tyi@_Gyl|7e0J=7A6LHu{h*IK(^EDd8@;Sj+
zKST~=nFKOkKq8T`3?>2efM^UlbrqIEqELw>Dv?6PlRN;rJ3wV%r#?8;oE#DYp{#XN
z#?U7(oJgsZ0YqYQaxx*AN<ieXL^6}fB$6mZ3I&fM@Cvn5399f?#p3r2ELb6si)BhN
zBE=dR!5Ab_>4igGeHTN5Y?@Z8n938HF`^2T5y=FSF{Vi%B$&p@66Nue#vuU_j)xOq
zsZxPr$<tVw2vH&m5%OQCr(gfc0GeA4XWGUGeMv}|Hla}ZC!t|XCFFx>1y3!5iJ`Cp
zNt6p<|0LAS#l~o4fS()&m57{&Ao1@j75v_1EQLmJ$GSy`r4W*=SpJ6tFbh<|UN~db
z@FaIUmBb@c02&3LP*;*@0EzSt%0VEpQ2jwD6`+&<1VwuW0+rz3f*}DQMC1t|8n8G4
zjD?9ZX)F#qokqY9iAUt9VbnY7q!%0x5GYkBL8$-^WO?Dxm<eJr1d!<<iSEvX@e~Gy
zhNs2QAUu=GbjLI39t<c(K#oBd@6WRkL87s;@6SX3=lLMH7?l<n|Brc$f-|-Rz!ode
zVyUMT!G*U?t>VSl$u0mufw8;2Z~|jlVF)+1E&j(Dct@Kof>F}{QSNsz1tL@?gL2q6
z7R}b*wi6L`o@mtJyEuq{4RP|_A2j<Ojt&Cja(V=z57R>omZFnOj*h4t>dg@h#(Yg6
z%a<p5^n8oNtr)Rqxa&&V#r6rWe7_6+`N+KH489vjM{;q=Jn}SU%qGUYbVuiw7rE_O
zZ2sjt2in2xd-BNq+~dmxlF{G0xyi|cL*9c5RrIf8J37bf9*tkU@OWH5lK8f=ukOwd
zOW1i>-!#)%Gkv+-@7vninwolE_UKGZOq`sY3<g74Sy@0qPHrwIE-r4v2Ax)`J>ip@
zG}74J-F@?*b9s5WpP%3B;o&)SXNBTbT3T9FRaKPPi}8`c%a<<?4Gld-L4@_Atva2K
z!C)khfhj2yZ(jG^JmARnI{mq8kPwFPc>LSG9)lq%IyyQkDr)D>osp3ap3Ud3sb2}@
za=E|1nbYlwiHYK2lI5b%FV5>9K8zI#A$_Alf*=PE9%PWoOqEA*PI<XREPmF~(o#PR
z06<Po4zFPa0B9tV{YkJ|J!W|R0Nr*tzxmTo8Sd@_{r#QhXUfX@`}+$60;8fFSMM`m
zW)D8PUVh=it?#Q#OP{{_<(632*VhMv;QDUfJ*Qzk^0>|`^|#txTvKy%0)a4pT$PhN
zpr2P{pJvj2uCC6BXLh2gu`xy@$|`mvlS5eH&z?O~cIsMN=OKjtJF&NlZ-3o!^JYNd
zQ8XCQ!fkzb@l_Es8+%Iw4z}d5R~$NYXvVE=Pa4gD%@6BSqnW!cxBT3+-wyh$<r6e1
zF0LYx2zz(CMnj{~3P0XsJ$v>^c1wR#Q&ZZ}B$*7mP^i&-nackquV?4P=napHRaKx+
zxGd*kWhLQdx<89`(P8hIFZ1(v*ABn#>x#JW+0vy;U#L4eVQnT_AFGD82M><%9qjG5
zo(-<D@lF}VmKMAjAHP+!`ewT|9$#_){^sU6G4?AK*J(eiO9}M6rg^<W7jo&+rR|pb
z!N>K{SC!evT+za7H*vW(kGHau><-!JLeLIbLdO1HtA5qnbs<{oTARV@UDlJ4nVCHg
z?U@1X%a<>+{M@|mmG7jipi*s4crcmyd$lLa$};>m&&ubl`s%B%WUhucuO+y9KOb^2
zy>a6Pq^T8p)9CaaBLj}@6*V<>C6O+vc_&+k1_q?`j`1&6xv*v!{x!0s*+R7NEc%-&
zmPpF;A|N_$KoS@CGB?xA%xof?yk^eH(+e#>&&&JB(42p=m49AM(B`SH9Q8P2FY9Tu
z<(*c7p?rSJcUKM{wp(~4FRx)QKP=3aToP<iup#F@zO=M-xH{#{@bbp9kx!eOn;|v~
zFSUzlZ*Tv0^v<2Iz2?;}=Es4=bJwpI^=4=rgb&VFRJ>>tTC8R{@AJ(G<iIdYW7K{j
zIibw+Fkd&nXkHcz*ZgC1?joPm-=N<Y7c^MR?MbmM|4Db4*uVN%dV2cIAN=>tPxr_;
z57zhFVlx_9w<{|$Gc&Q;VAI=0XGaDfTh%Ub_Ea?#hb1On!ftr+Q`3Isyjf_IS~`1%
za=9a`9b6jhhUmVAdzU0#?6DU9rVSfxR_ec*%ijH#|0FuzFndvqn?p@aO>0+pc=*KI
zw@sW*RQQzj$4$Jo^`im)?BJ{EGg5w=xZl>ceqlkIUO(~unV@04YX97^%<a5Rw{OI{
zqN1WRK_|kCE!NRot1-j{ahm4=vwenYkjJ8MTU*<f+xoUcXfGast-jNxrk03ombtlQ
zZ44@0{VVV4fk%41QLdXyTCLsP-Jv&k?}qat=Q?{A-mmQF=r|W|E&C?h#<jP%*I-q_
zzJUrUY#TL`dg|0E1JpF8TsLZ16x-sUMkRSl-V<-Vw6?*T>%D6ZYfno@M|1SQ`~A9p
z3dgfN3xi!)KI;mWKSJeKDC<3ylkPLa(Xl8ydsdm%U|-)#r<Fa9cPKK2qSVSdOG%bm
zEFYIeJs;eizR{+nqJraMyVCC5*K<tP=sE*-H#9b`4G;fV?iQpwSKF`_`ZOdY|6A*l
zu`%_6F8!XSk}Gmnx#}vH6Zgb5sCZ4%ai(IOv{R9oh+n>(8F^T1-!Q&?WFWut$NH`}
z!K)&|!g_jpo3+}lA&$9{9j3v3bUK}K_{jl-|Hh-$^%pOG6gKPOX=ZmsEWGRb?rO9B
zGL1&#QxNd1`c?7P$}@}EYsySbO;s9=)04hu^WTkg%8-xCi(`xTbk-REVPgXQH?b<$
HZcY0S{h81^

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_saved.png b/Telegram/Resources/icons/menu/stories_saved.png
new file mode 100644
index 0000000000000000000000000000000000000000..727dc35f301f8aa11736e35bbed1c896f5cb5f02
GIT binary patch
literal 1935
zcmbVN3ry5z96u4A!jKtaU~04}qKMwL_vEg69*jE$-62B~Krm9;@9rvh*Rw6&LnqE?
zHW`b17%@(VY;lQ;GB;!>Xg~#2Hs4G^nat@qVo+ibO*G)X!g0|>XJ(VOU)$gJ`~BbD
zl99eBB5ZP)L?VeuOEqS~dyIIF3xQX5(honv+jv)ME-#UUM~UYs$<AF65qh7=$`NwR
z?-La3kdZW72xK0I3!){G`38@Rq>6xm6ap*b)T6!i4Jg9UdNf;Y#?7u2U}I8!99ZT{
z&!T)ql#WIX3y}F90unfYKq4N8-N_RkJvzioz_s`qLy;kfP^3qbML{IToPngU96)q3
zxs<|jIg+T8ky=`ns8-HJ6u3f(;Yv)Ql;T=Kts#_&NZ>%B97kJ-Oyk0U7<|&BHbHO^
z80L1nWp1U6<*b-or_*7$0#hiY5FzEgPJ#4DoqX&FgAwo)$G8NBbs{1oS;!U(dK8*E
z?1RG<q;>LvIKhZv9@2%$Ww_|m5Rj&VI9D-e9}=f24A_AKI0YVJ<w2~=#tJNNV_!iX
z{QQCdjIG%m)Oe{a4o6Ue7ffrQ8-akl6wPOOT>#4jJX_3Bz_bRM87q3@B2qX&3M`k!
zvi6ZoWsI1NC{!{H5}(aDY1Ylper^LWk^<19V%DU%MykZK<O)KiA>^7lxQf8>VW^p<
z8H@K7P=)pdD6AQp6v)?tX^OD0oP&f8GY--UFqhMcBEc{cDXg94pfI#fIb?;|Or$w^
zfpk(J&8SDA&oYLg2})(r=zvZmRV8W@r7F3~BGt)Na;aQSYk)?fRO<>ABm0dkRV-$9
zWIz2s_b=rb*jl9hjd{d|6H9_fWq6n@Z=fTVfz^SPok51GfFLQcy7ed}rWMd=;5GBc
z5g4X*+W;i}k8}^ic-A7gNe(1iVYFUbP8c*E6Z>%35A5GA4t;yBXGh?05Qxj*2!ap6
zAqJdqa&d4(S<C*MFOh^SN;4*BX(v(zF^Z(z+#`n%doF%!s(1JPQd2XgG&ME#bZhIE
z1)J*Xp0s>6<=L}mC69VKwnWX^zJ2@R#fw9l^Oi4PKk!8DzMnsB+O!kjZ`!>1!>5lN
zXLqVsty*QvEGsLktgKx7sAtqQW8=2&?(Pah$;`O8iH9P)7ka(k9?s|UCDk^aIu&(m
zprxe+s&X7x_{H^_qG;T9F*G6~!hicVJ}EprykY%7T~*bD6(`!;V`4tH`8HII4jUXC
zm|`>F8!jBuYPE;!>%W`Q*4g=XnMIYnXY{P4-`H6;^3b|J>WYn~D>sg<os*DoA+&sM
z?xm|&-=gE=<MWSt9$ebHq_nj3bQrwsKXBlKhxrZb^)qLVEl*BrJbU)ontRvsYMnnF
zJlHtN-tlvsQW<4BfA4zj$&)7^#Ds?S_xDd<u&w}r&7bbiKcYxXj5eLGO!+u0Jbd-L
zP0h_Uh0Uez-W$4k^J3>*XlvUv!xt(Un}DmS$!%R-k-x3{``qq&Uw;Drc;W<mbaeUo
z3PWFCUu^gtzh7T6I5h@*t#SP2_nR&kTCL?36<eL}L`7Zp|5~1xmnT<7<6RPPv8}qg
zx~tyJ-fi2LGu40d=7U?OA|G})?XK-=D8W#4<BG{S8ApBnt(~2__9Q1or!84>Yunf-
zd#kJao<8nvY&vuN_(%Java{X&cYoh8d&0bNVPPS6ddjht=XU9o8qJk6XU5wM8B=r8
z2M3-;@5oT8RG&y^bhNi`s61%zP!#MR{ngfn2Ez<>Zf<VG?$!{=^tkT!F?RnA@t2uq
LN;g)$SFrIP<9Nc5

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_saved@2x.png b/Telegram/Resources/icons/menu/stories_saved@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e68c618230e69169da1a5630e61bec71deda642
GIT binary patch
literal 2521
zcmbVO2~-nj9*>H0xN%i1CBiOY)RhWJCMPgWbTL7Wbp$nXxo*udftX|_Bm)T%+GjX~
zbVWghRiV$KrLG0kS`>6ww~4kKUSJh;mAX_^+=}7>5-sc(uGj8sw{QEsH~GGq`ThU@
z-~XHx#jlE@dd>HuP$<-xXvrF|!>-F?D){Z(h+hGlr#?E_NTJO1c3o2_dn<e?6t_OL
zG>J??S0V~R$CN1vImR^W^Z-qv2$z}lGKB^s>2fSpjf<eir_VxkwNeBn@=+Moi?K9y
zw8em}wXBjVEE+|)5?Z#DE;J)Rfes^ObhA#28xgYz8r6${v+FSnqK`sIjR=Zx8Kftn
z@pLg^!06#jHbVi!Y<g%oQx>M=h4Q%}bPmknvS2QY!)3r>2w#A3L+RrS0_F@#6|zRM
zV%!+G6G3StsYh5WlgY$1ahZf6mBkJZ4`;y~7Kg(C2!=5WCuL>^Zd@>-L4p|-2DP44
z6FA+aQ6?ucND&06j`^U|PpZX@<8cBJW0_@o7MlsXd>RER6_Yr9hCw@OT&ZATT1<!G
zq!D1*lURKkK@!F^;wMy3zW#v$5L*<TwDF@{bh=3sMlv!JxG^4(A4MCbS$d4M1~U>F
z1_c(G31}{Gd80?f224g01}Q;kCo&a3L7C3sF$MIXL^ZA?OvbmqCxA(0BqoAfS!2Kg
z1{apHIS5aHum!;|4}sw^C`u^Rs;r-Y^7uc1f|^mvNZHSVl?p^f7<4k=uv#Ze#aMbg
z6{1gu5fKww!T<~dcHB`GD2l}3MpA|=uo#I50zNa<Y9+#E!+d@i#%IXG_$r2+$CER{
z*aAL-s|*Vls1))r6;ClCFCi2euFOuzEB{aVID;CrmQ4GL^|%_&RT4<F+6a=BHQo_x
zvGnniR!twR0z{^8RksLIxYCL#q4CG+UmSrkwWc%-DE%Ml9)lSP6={+gu!vL;t)DF?
z79h`Z_2HNwtp9d#^xgM*b^;Ctf$KOqg22t>5W{dVxeQ=L)xY#*fyq}ABZ-hEIUXe?
z2kB(<1eZCjLRDdLQ*L8k9_OHWhmc+x2sv{1K+wX-d!;MS?|h3EV2;0=|Erh}7mCB~
zZ9dy~>d`sTt8c&2HvanhRoa>QVPa&&+2OoiRx)%f;1%;~TU-B=CxykumztV_f`h$1
z+`e`=>~<(PScBtzLqkIye(N`E$eH;Eu;Stp6B9?4FA)d?xw*M#mv%ZFj=nzJVBq9F
z9~h`Ud+E|8o6W}K^Rud}YHM$Ib{6KV_mr2HpFe-TqQW1@=H%ogC*Lrcr%lVPto%(u
z|Ikq5>C<l$T7Q54zdQhQaNW9fNpUshHe0Mpb+4^$PIy9M;xotZf`T4&cU!HQCeyyX
zdt>j`9Xz;e*RH(Vd=9633o4PEyL#38kVn3~p`n2|a}AK_=;+vJv{)YY_BJPW0;ttG
zH8L`?`4*Xxk)HmKdjkVLds^T0(`YovccrCsn}zN(W;AgHg0e3+0J6)3LP_VVSDV4A
z!@)Xo<cMCAmUc(vZFe|&SFcWKbgo3v)%{o6+vQQtp`mNnuO}oVRPprpc?c3)k)54=
z?!Xj?#Bt`^bMs3Ca|d3&EL`z<$&R9;=B}=;o}Q0tYirG<(a7DMEfoyZd~B!Qy?Bw@
zwuvC_n{%YYzKfmi?(WiQH)O-Z!xB3PT}#U*g(CM1lfj_PpZ~Nhy>M0ffvT#`lEA>g
zJYHz%mx<(yl9D;j>4SwQW4H8)Z?CV>nxi}leAcI=#3tuIe8by&!HG{cHeR_BFNjA`
zs{PU9$M1hMTr!*XeE!Zg4LJe%6vZLEbVq0DUr(NVbAL}yPn>s^@ncC|{fQGc8>KSF
zH_fl4^}>Y<6t++pLJjnlw7-5WoU$Jtp-r(CQ!Ew>RQ`DvrLCi5@uI+_%uK<Q_`wm+
z!tL7^Q~p_du8YDJi5S#CUX9afahf)7ULO&$MKpEatpkS}>g!*qmq!;iH^&rgtE{X%
z<k8a70(KNdwXO4fe4yD2yYG91BZ$@QyICpP8%xx|J{eC!^6Yl|l@A_7`O{_<Z?zvf
zeAtV&y)Y=<-!nNSrCamvyGvKhIUMZi>6!g-cYlBSxef2Ule!grR@p7@X|;b|8W0em
zDC_yu3s2DNk5*JvnA7BPD01+{3oMEm67siwfBZ1G;ppnMYY#}hKhY0Y_3!`vk(!#L
z3j)86KUP;)2T$La^v#mRN@YuHYcCBhH5#H(bkWwPrs%FkA%^1id5`k{&E3DNt?Yw}
zifDvp<A3#AOUh?5&s$TcP3!IX;^xiygU9d4zhb7QrY;)DQ&vZTj9N4Q_5SRKJ_Q;o
zb=&2z!ROCm`>xiR1+37|{8aM1P0yY_b>3>-dFs@ulf?Fe_1)ca)vYc5cX+2?v$@>k
z#l^E)A|-d8)xUwe*{ED=#AWftql2p&Jo1-UYX=4gpJiu9J8U+uLnaj!7H53nd$YaW
z`ll7>wqr#_p4C=&H@EC{2{b=H@hLH!lYRAN-KR6%tS3$gnr+*7GkSKHmv>ydHvRVT
l#>OH%7cF%SjDT-L-JqJeYu5jLyX((aOynv_-SUmr{{S3>=r#ZV

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_saved@3x.png b/Telegram/Resources/icons/menu/stories_saved@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..9fc3db0d68c9d57901062163af4889d99e93d545
GIT binary patch
literal 3136
zcmbVO2~-o;8V+DU5y2`_5YQMvWXVES2n3!53K3b$5<roV3`q>hq{#pQ1S{AEMG$2d
zl%-{J305oM!;QKHrR=3FBBw4?<Ozx(MG-qe<h1X!ujh5nnYnl7e)nI$|DSun&&Okx
z_Ihmu0<ntW=@tNwo~nDfI{X|sY`z*ER){@Ar3l2zwW?bUaje7;fmk-q4`f12@9i`;
zD8#ZjU=)Cr3B@oPfpBt>iCJs`0HLA)E?-1PPc&Ucqxc*;I)v<v_ZGVXJice51o$k`
zCy<>eU^{ToF3u<?84VU71RxelCX5kDX)-!`K`srRt3Kn<s09clprf~{6rz~kekfN^
z0-zkQb{IAuZ-=5fU|IGY5|vD}MG^1>A`VZ)5r`PPJ&jDE5vi!f3k|E2aH44eZl5kH
zgWu?A9t4SLI9z;uJT{()1tnaZor8k|4o|=l2pAZFktT>BmJB13nthPq21wZwz8K<z
zB9uxZD+-K-=xEr~_dW>4OJYUR#W=wc!^v1;oE;Xg@@WCcVK3ptv67et<s3E+hyjFv
z2$I5ByCtlc2ST8f2mXcVrR6^;fMe_Jy`<w$Z4nBWR7fHBIM|KFfcz<18kiskZ~=f6
zjFqqf_c+*0GnF@DnyUn0L7*fM1Y<sA%I|~8C;|yfL2U`)i#TAs)arK|05=u{(9x=_
zVek|T5g%wrpphsvJBkgSM8o6XL%l%`KRV$rphWT?pfG1REQs~DU=EuW4N8P8*kQhq
z#RYI;5f_bG3M0)Gi~%LEV%R$3f)(E0G=@kDu|#Zu;YLTpK4bZO4viG;V8<cDwmT5*
z2pBs8iHwONvj7a9NQ60QPr~B?;KP15kR7Ya?1%lF|8u{ugb$Y%E9M`|Q5Bqu5;RY~
z6i!ycVnuuge7-n~;iDE<Kx460?53mHs<Z+e^x|j!KaRkAvGF_r7WzNZ{T?O-qoH_~
z1lY=jqxH9R!okMlRCV~?58QvdxUlxSn*9KW8-Z%N)Pmrbr6vZ5;Lat1TNJ6?JO+W#
z{FULhHIR4zSy+N+NcigB9$T;QN)lDy%G!%$fw%5@Add`->ieX;a$j!ca_ikl3(q54
zcYlOLYKzJ}S02bW+~;mlA6fr%Fw6L2IClEV%sAWrUg_P-zeG$VJEy#qm)T249Jy_+
zRH>q<h_O9tO-`Yk&9~>yO-@Zs&CKZO=;-L`Mx2do*4I1_jdnFQHg<J&RaI4$mzTR7
zf*`1GaM0JzNuf}Tj*f!&FU=Aa`}glp*I%W1{XDmQeE8~BaM)QO7<;$+o1jy{dAYfV
z4jq~+@*Ef(%>4Sro0*9zy<J!Kv?!H@WI=vgc1cMIVsvI=_?F>Ua(Ss!!qL*w()|4T
zz3=8t9pjcEk^TMs2W**4=G3cK2O*b~IYU!Zp-80AFh6}PJG-O1+ibG-)TyyMc{b@(
zwhp^xe5eV&G8;BKI))1ZDHO_2b-O;E?Hm|bziu5_I<j3it?x@SkE~0{Z}Q2%j&@sm
z`(r}SJsRz<KCN~9`0<vO7GiX(AJ2$oWNe(#YE|fND3IT2Z`U(sWQ&{HN?qzivF9#a
zKwc^=G<@u65kwIV^b8OC*1UfG`upqGtDWTH_;~KiNx9y}J$v?4Ii2i3&%L^5_NX5{
zWv;Ked8?OJ@9EI)zP@yV;YS~pM@QpuILkog&X*%Ubj!E`f#Ay@wKO$@PczH2I@enI
zthUxKd3?7(u#pfH6!gf|)%E(+Xm?)N)Z`=@ong`#I~Kfi{QK9_W1%T&mxD2ysM}el
z-7ik0b(~#c^f-68w_Z4?>}`sl=-4s3JZ64wwoLh`{l`-wv%*bpj+85vo)@30sj0cU
zyYo0=o5{&Z6I0W5QG@5Xdb2Obe!{h7Y8qMEOixcsF2-%!cChA%T87r`ko0G5#~o8I
z<mVgstbO94tj2`bE9s}c9XOw3>1#deVSwEsd-1@1nfkf)hv!Qiq<(hGLOOz5f^fFB
z=H~6CE*^VtMxCmuL2T;zrK-Zq(xt;s8C0#N=J#_5bfbcz<)mritrdwxA_JQYFk$8)
zZwDK|RNC{^$qp%V_Td@VoHR5vE@*0Ic>yO}hYSl*@G@6RZOdCY+P%7Y#l^;FlE?2k
ztlPIDr*vc_nqupp;IL&)SZqfb?DgFaJ42oCo+@@W^&mHM>W>{;Z!nmqrN==!sW&MU
zJ{@_@GY=m=<TIvLp43QVhJ{5miuc{@?d{!sx%uVu=ct>-RB&6YjiY06Pxg$ZMj=W|
zZ-a@63CE}mX6ucMEBP%opSn@ya`~*N)-6(9$TdQRF-%7GzdCc~O#f(MV8g+T4E`Lc
z|61#OQKs3<(XWjuG1=#qWrft|^x)XI*)KXFH*LIfpN7_QnCgZ4d8ICU1L(O=gHuwR
zk(V_s<EEbY-*9@d54fAp2u*F|d3eYJ8?F)f?`&;tH|rTHv<Ivy)CUHhJ5sC0>LM;5
zecD_5SvAjs%l(C+UN3tQb<#cc-sPl_*Ed|~^qrnyXJ@CAPT}D;@8vp=k1&f-@Bt)X
za+3`fi|sQt(iZ%rsGQa{rF}!gpEZiBI22*n;<{3iE-M?fr9Y`e6jOv@!NCSeez1)Z
z4Wi?5PK8ybOeQ9>57)8LASgMRK5RNt&|U4@J(<4!;?rGMQ`SNLYrecHOH4#3A6QeC
znVG4pv27AXr53!o_BjNTurUy>>&T6zaF@w#yghU2S8!KHSJx+PEy>TEr6aA(iLs%f
zp>4NXzJMm~)BBozePl7@yGo@W;U1@2p<SxqCtsFpv0*K5Rd)Jli^)*AZ7h7cdi8k4
z&mn$(h-=#N=I5Le-%r1}0~b?X>+*ly7Kh~I<gnT75XFw#xsmgkQEax^L9Oad|Dl<#
z@B6N}xVR|g!Ni2`k3sj{A_H4mTg}7uHaho=<)BRFGd{*Cb36`juzGt5S@~6$*`t9A
zf`21iXIcXALF#WN49gU{sjnDGku?+g^4-qe&xV=`-g@4N#PbegNTly7D$rzdo(uPo
zHMjkQMNC>++WHrt3l$&$R6a4o=23ldMUL?bUTV@<FXl?~!~Le_Cx_vRyKp@k0B!~g
z%j*tgWaJFaWf75EMP-!She)p@=H}+<8*Gb@9W&4R;TEf*57n9E9vWKI%Q7-Cx!2!s
z^`J2}HrC(Y-=r=toyP9cNvv)6;ALfHk%-Zib#)WBGL5&yU%7H6AdHceloXfh>wChO
zD(WGA4R_uHRlJIwpg^FZaZ;hcVlbxUjrNX?8CYRdR7F@pFjCe44o5SdG9Ns9M&DgP
z#^ayAo)+vN&X3C`Z!|Y=va+hJug_0ZXB!FpYTK!UCOrvoqf4*xD!`V@K^{-`Zax{-
n84+>62N4PXfo_<;bDUk7e>?K5%S9`>>Wt5D_i<Bv5}EoNc7hCC

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 04a9b2710..228366602 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3814,8 +3814,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji";
 
 "lng_stories_my_name" = "My Story";
-"lng_stories_hide_to_contacts" = "Archive";
-"lng_stories_show_in_chats" = "Unarchive";
+"lng_stories_hide_to_contacts" = "Hide";
+"lng_stories_show_in_chats" = "Show in Chats";
 "lng_stories_row_count#one" = "{count} Story";
 "lng_stories_row_count#other" = "{count} Stories";
 "lng_stories_row_unread_and_one" = "{accumulated}, {user}";
@@ -3832,8 +3832,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_archive_title" = "Stories Archive";
 "lng_stories_archive_about" = "Only you can see archived stories unless you choose to save them to your profile.";
 "lng_stories_reply_sent" = "Message Sent";
-"lng_stories_hidden_to_contacts" = "Stories from {user} will now be shown in Contacts, not Chats.";
-"lng_stories_shown_in_chats" = "Stories from {user} will now be shown in Chats, not Contacts.";
+"lng_stories_hidden_to_contacts" = "Stories of {user} were moved to **Contacts**.";
+"lng_stories_shown_in_chats" = "Stories of {user} were moved above the **Chats List**.";
 "lng_stories_delete_one_sure" = "Are you sure you want to delete this story?";
 "lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?";
 "lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?";
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index b1c87eaff..2bf72ee92 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -57,10 +57,10 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 		&sessionController->session());
 	const auto raw = controller.get();
 	auto init = [=](not_null<PeerListBox*> box) {
-		using namespace Dialogs;
+		using namespace Dialogs::Stories;
 
 		struct State {
-			Stories::List *stories = nullptr;
+			List *stories = nullptr;
 			QPointer<::Ui::IconButton> toggleSort;
 			Mode mode = ContactsBoxController::SortMode::Online;
 			::Ui::Animations::Simple scrollAnimation;
@@ -82,10 +82,10 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 		});
 		raw->setSortMode(Mode::Online);
 
-		auto list = object_ptr<Stories::List>(
+		auto list = object_ptr<List>(
 			box,
 			st::dialogsStoriesList,
-			Stories::ContentForSession(
+			ContentForSession(
 				&sessionController->session(),
 				Data::StorySourcesList::Hidden),
 			[=] { return state->stories->height() - box->scrollTop(); });
@@ -102,17 +102,9 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 				Data::StorySourcesList::Hidden);
 		}, 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) {
-			stories->toggleHidden(
-				PeerId(int64(request.id)),
-				!request.shown,
-				sessionController->uiShow());
+		raw->showMenuRequests(
+		) | rpl::start_with_next([=](const ShowMenuRequest &request) {
+			FillSourceMenu(sessionController, request);
 		}, raw->lifetime());
 
 		raw->loadMoreRequests(
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 15710d021..61c41ffa3 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -76,7 +76,6 @@ StoriesSourceInfo StoriesSource::info() const {
 		.last = ids.empty() ? 0 : ids.back().date,
 		.unread = unread(),
 		.premium = user->isPremium(),
-		.hidden = hidden,
 	};
 }
 
@@ -943,7 +942,7 @@ void Stories::toggleHidden(
 				tr::now,
 				lt_user,
 				Ui::Text::Bold(name),
-				Ui::Text::WithEntities));
+				Ui::Text::RichLangValue));
 		}
 	});
 
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 648535187..630e8cc31 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -43,7 +43,6 @@ struct StoriesSourceInfo {
 	TimeId last = 0;
 	bool unread = false;
 	bool premium = false;
-	bool hidden = false;
 
 	friend inline bool operator==(
 		StoriesSourceInfo,
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 35a19da9a..091ebaf16 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -347,17 +347,9 @@ InnerWidget::InnerWidget(
 			Data::StorySourcesList::NotHidden);
 	}, lifetime());
 
-	_stories->showProfileRequests(
-	) | rpl::start_with_next([=](uint64 id) {
-		_controller->showPeerInfo(PeerId(int64(id)));
-	}, lifetime());
-
-	_stories->toggleShown(
-	) | rpl::start_with_next([=](Stories::ToggleShownRequest request) {
-		session().data().stories().toggleHidden(
-			PeerId(int64(request.id)),
-			!request.shown,
-			_controller->uiShow());
+	_stories->showMenuRequests(
+	) | rpl::start_with_next([=](const Stories::ShowMenuRequest &request) {
+		FillSourceMenu(_controller, request);
 	}, lifetime());
 
 	_stories->loadMoreRequests(
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index e686811aa..253b789ce 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -17,9 +17,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_stories.h"
 #include "data/data_user.h"
 #include "dialogs/ui/dialogs_stories_list.h"
+#include "info/stories/info_stories_widget.h"
+#include "info/info_controller.h"
+#include "info/info_memento.h"
 #include "main/main_session.h"
 #include "lang/lang_keys.h"
 #include "ui/painter.h"
+#include "window/window_session_controller.h"
+#include "styles/style_menu_icons.h"
 
 namespace Dialogs::Stories {
 namespace {
@@ -325,9 +330,7 @@ State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
 }
 
 Content State::next() {
-	auto result = Content{
-		.hidden = (_list == Data::StorySourcesList::Hidden)
-	};
+	auto result = Content();
 	const auto &sources = _data->sources(_list);
 	result.elements.reserve(sources.size());
 	for (const auto &info : sources) {
@@ -349,9 +352,6 @@ Content State::next() {
 				: user->shortName()),
 			.thumbnail = std::move(userpic),
 			.unread = info.unread,
-			.suggestHide = !info.hidden,
-			.suggestUnhide = info.hidden,
-			.profile = true,
 			.skipSmall = user->isSelf(),
 		});
 	}
@@ -466,4 +466,51 @@ std::shared_ptr<Thumbnail> MakeStoryThumbnail(
 	});
 }
 
+void FillSourceMenu(
+		not_null<Window::SessionController*> controller,
+		const ShowMenuRequest &request) {
+	const auto owner = &controller->session().data();
+	const auto peer = owner->peer(PeerId(request.id));
+	const auto &add = request.callback;
+	if (peer->isSelf()) {
+		add(tr::lng_stories_archive_button(tr::now), [=] {
+			controller->showSection(Info::Stories::Make(
+				peer,
+				Info::Stories::Tab::Archive));
+		}, &st::menuIconStoriesArchive);
+		add(tr::lng_stories_my_title(tr::now), [=] {
+			controller->showSection(Info::Stories::Make(peer));
+		}, &st::menuIconStoriesSaved);
+	} else {
+		add(tr::lng_profile_send_message(tr::now), [=] {
+			controller->showPeerHistory(peer);
+		}, &st::menuIconChatBubble);
+		add(tr::lng_context_view_profile(tr::now), [=] {
+			controller->showPeerInfo(peer);
+		}, &st::menuIconProfile);
+		const auto in = [&](Data::StorySourcesList list) {
+			return ranges::contains(
+				owner->stories().sources(list),
+				peer->id,
+				&Data::StoriesSourceInfo::id);
+		};
+		const auto toggle = [=](bool shown) {
+			owner->stories().toggleHidden(
+				peer->id,
+				!shown,
+				controller->uiShow());
+		};
+		if (in(Data::StorySourcesList::NotHidden)) {
+			add(tr::lng_stories_hide_to_contacts(tr::now), [=] {
+				toggle(false);
+			}, &st::menuIconCancel);
+		}
+		if (in(Data::StorySourcesList::Hidden)) {
+			add(tr::lng_stories_show_in_chats(tr::now), [=] {
+				toggle(true);
+			}, &st::menuIconAddToFolder);
+		}
+	}
+}
+
 } // namespace Dialogs::Stories
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
index bdbb3afea..b42715d54 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
@@ -16,10 +16,15 @@ namespace Main {
 class Session;
 } // namespace Main
 
+namespace Window {
+class SessionController;
+} // namespace Window
+
 namespace Dialogs::Stories {
 
 struct Content;
 class Thumbnail;
+struct ShowMenuRequest;
 
 [[nodiscard]] rpl::producer<Content> ContentForSession(
 	not_null<Main::Session*> session,
@@ -32,4 +37,8 @@ class Thumbnail;
 [[nodiscard]] std::shared_ptr<Thumbnail> MakeStoryThumbnail(
 	not_null<Data::Story*> story);
 
+void FillSourceMenu(
+	not_null<Window::SessionController*> controller,
+	const ShowMenuRequest &request);
+
 } // namespace Dialogs::Stories
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index a76cb5deb..b94ef43b1 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/menu/menu_add_action_callback_factory.h"
 #include "ui/widgets/popup_menu.h"
 #include "ui/painter.h"
 #include "styles/style_dialogs.h"
@@ -108,9 +109,6 @@ void List::showContent(Content &&content) {
 				item.nameCache = QImage();
 			}
 			item.element.unread = element.unread;
-			item.element.suggestHide = element.suggestHide;
-			item.element.suggestUnhide = element.suggestUnhide;
-			item.element.profile = element.profile;
 		} else {
 			_data.items.emplace_back(Item{ .element = element });
 		}
@@ -237,12 +235,8 @@ rpl::producer<uint64> List::clicks() const {
 	return _clicks.events();
 }
 
-rpl::producer<uint64> List::showProfileRequests() const {
-	return _showProfileRequests.events();
-}
-
-rpl::producer<ToggleShownRequest> List::toggleShown() const {
-	return _toggleShown.events();
+rpl::producer<ShowMenuRequest> List::showMenuRequests() const {
+	return _showMenuRequests.events();
 }
 
 rpl::producer<bool> List::toggleExpandedRequests() const {
@@ -852,24 +846,16 @@ void List::contextMenuEvent(QContextMenuEvent *e) {
 	if (_selected < 0 || _data.empty()) {
 		return;
 	}
-
-	auto &item = _data.items[_selected];
-	_menu = base::make_unique_q<Ui::PopupMenu>(this);
-
-	const auto id = item.element.id;
-	if (item.element.profile) {
-		_menu->addAction(tr::lng_context_view_profile(tr::now), [=] {
-			_showProfileRequests.fire_copy(id);
-		});
-	}
-	if (item.element.suggestHide) {
-		_menu->addAction(
-			tr::lng_stories_hide_to_contacts(tr::now),
-			[=] { _toggleShown.fire({ .id = id, .shown = false }); });
-	} else if (item.element.suggestUnhide) {
-		_menu->addAction(
-			tr::lng_stories_show_in_chats(tr::now),
-			[=] { _toggleShown.fire({ .id = id, .shown = true }); });
+	_menu = base::make_unique_q<Ui::PopupMenu>(
+		this,
+		st::popupMenuWithIcons);
+	_showMenuRequests.fire({
+		_data.items[_selected].element.id,
+		Ui::Menu::CreateAddActionCallback(_menu),
+	});
+	if (_menu->empty()) {
+		_menu = nullptr;
+		return;
 	}
 	const auto updateAfterMenuDestroyed = [=] {
 		const auto globalPosition = QCursor::pos();
@@ -882,12 +868,8 @@ void List::contextMenuEvent(QContextMenuEvent *e) {
 		_menu.get(),
 		&QObject::destroyed,
 		crl::guard(&_menuGuard, updateAfterMenuDestroyed));
-	if (_menu->empty()) {
-		_menu = nullptr;
-	} else {
-		_menu->popup(e->globalPos());
-		e->accept();
-	}
+	_menu->popup(e->globalPos());
+	e->accept();
 }
 
 bool List::finishDragging() {
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index e1bebe00f..49391432c 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/qt/qt_compare.h"
 #include "base/timer.h"
 #include "base/weak_ptr.h"
+#include "ui/widgets/menu/menu_add_action_callback.h"
 #include "ui/rp_widget.h"
 
 class QPainter;
@@ -36,9 +37,6 @@ struct Element {
 	QString name;
 	std::shared_ptr<Thumbnail> thumbnail;
 	bool unread : 1 = false;
-	bool suggestHide : 1 = false;
-	bool suggestUnhide : 1 = false;
-	bool profile : 1 = false;
 	bool skipSmall : 1 = false;
 
 	friend inline bool operator==(
@@ -48,16 +46,15 @@ struct Element {
 
 struct Content {
 	std::vector<Element> elements;
-	bool hidden = false;
 
 	friend inline bool operator==(
 		const Content &a,
 		const Content &b) = default;
 };
 
-struct ToggleShownRequest {
+struct ShowMenuRequest {
 	uint64 id = 0;
-	bool shown = false;
+	Ui::Menu::MenuCallback callback;
 };
 
 class List final : public Ui::RpWidget {
@@ -72,8 +69,7 @@ public:
 	void setTouchScrollActive(bool active);
 
 	[[nodiscard]] rpl::producer<uint64> clicks() const;
-	[[nodiscard]] rpl::producer<uint64> showProfileRequests() const;
-	[[nodiscard]] rpl::producer<ToggleShownRequest> toggleShown() const;
+	[[nodiscard]] rpl::producer<ShowMenuRequest> showMenuRequests() const;
 	[[nodiscard]] rpl::producer<bool> toggleExpandedRequests() const;
 	[[nodiscard]] rpl::producer<> entered() const;
 	[[nodiscard]] rpl::producer<> loadMoreRequests() const;
@@ -169,8 +165,7 @@ private:
 	Data _hidingData;
 	Fn<int()> _shownHeight = 0;
 	rpl::event_stream<uint64> _clicks;
-	rpl::event_stream<uint64> _showProfileRequests;
-	rpl::event_stream<ToggleShownRequest> _toggleShown;
+	rpl::event_stream<ShowMenuRequest> _showMenuRequests;
 	rpl::event_stream<bool> _toggleExpandedRequests;
 	rpl::event_stream<> _entered;
 	rpl::event_stream<> _loadMoreRequests;
diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style
index 7be43b08f..922cbaab8 100644
--- a/Telegram/SourceFiles/ui/menu_icons.style
+++ b/Telegram/SourceFiles/ui/menu_icons.style
@@ -104,6 +104,8 @@ menuIconNewWindow: icon {{ "menu/new_window", menuIconColor }};
 menuIconChatBubble: icon {{ "menu/chat_bubble", menuIconColor }};
 menuIconPhone: icon {{ "menu/phone", menuIconColor }};
 menuIconChannel: icon {{ "menu/channel", menuIconColor }};
+menuIconStoriesSaved: icon {{ "menu/stories_saved", menuIconColor }};
+menuIconStoriesArchive: icon {{ "menu/stories_archive", menuIconColor }};
 
 menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
 menuIconTTLAnyTextPosition: point(11px, 22px);

From af0e578da5e61d4c7c6404fc3efa2f0146732fd8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 15:24:59 +0400
Subject: [PATCH 127/259] Save to Profile / Archive stories from the viewer.

---
 Telegram/Resources/langs/lang.strings         | 15 +++++
 Telegram/SourceFiles/data/data_stories.cpp    | 57 +++++++++++++++++++
 Telegram/SourceFiles/data/data_stories.h      |  3 +
 Telegram/SourceFiles/info/info.style          | 39 +++++++++++++
 .../stories/info_stories_inner_widget.cpp     |  1 +
 .../stories/media_stories_controller.cpp      | 42 ++++++++++++++
 .../media/stories/media_stories_controller.h  |  4 +-
 .../media/stories/media_stories_view.cpp      |  8 +++
 .../media/stories/media_stories_view.h        |  2 +
 .../SourceFiles/media/view/media_view.style   |  3 +
 .../media/view/media_view_overlay_widget.cpp  | 27 ++++++++-
 .../SourceFiles/settings/settings_common.cpp  |  1 +
 Telegram/SourceFiles/ui/menu_icons.style      |  2 +
 Telegram/lib_ui                               |  2 +-
 14 files changed, 203 insertions(+), 3 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 228366602..fa47c0a99 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2477,6 +2477,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_mediaview_copy" = "Copy";
 "lng_mediaview_forward" = "Forward";
 "lng_mediaview_delete" = "Delete";
+"lng_mediaview_save_to_profile" = "Save to Profile";
+"lng_mediaview_archive_story" = "Archive Story";
 "lng_mediaview_photos_all" = "View all photos";
 "lng_mediaview_files_all" = "View all files";
 "lng_mediaview_single_photo" = "Single Photo";
@@ -3837,6 +3839,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_delete_one_sure" = "Are you sure you want to delete this story?";
 "lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?";
 "lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?";
+"lng_stories_save_sure" = "Do you want to save this story to your profile?";
+"lng_stories_save_sure_many#one" = "Do you want to save {count} story to your profile?";
+"lng_stories_save_sure_many#other" = "Do you want to save {count} stories to your profile?";
+"lng_stories_save_done" = "This story is saved to your profile.";
+"lng_stories_save_done_many#one" = "{count} story is saved to your profile.";
+"lng_stories_save_done_many#other" = "{count} stories are saved to your profile.";
+"lng_stories_save_done_about" = "Saved stories can be viewed by others on your profile until you remove them.";
+"lng_stories_archive_sure" = "Do you want to hide this story from your profile?";
+"lng_stories_archive_sure_many#one" = "Do you want to hide {count} story from your profile?";
+"lng_stories_archive_sure_many#other" = "Do you want to hide {count} stories from your profile?";
+"lng_stories_archive_done" = "This story is hidden from your profile.";
+"lng_stories_archive_done_many#one" = "{count} story is hidden from your profile.";
+"lng_stories_archive_done_many#other" = "{count} stories are hidden from your profile.";
 
 "lng_stories_link_invalid" = "This link is broken or has expired.";
 
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 61c41ffa3..a6db98e37 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -1275,6 +1275,63 @@ void Stories::deleteList(const std::vector<FullStoryId> &ids) {
 	}
 }
 
+void Stories::togglePinnedList(
+		const std::vector<FullStoryId> &ids,
+		bool pinned) {
+	auto list = QVector<MTPint>();
+	list.reserve(ids.size());
+	const auto selfId = session().userPeerId();
+	for (const auto &id : ids) {
+		if (id.peer == selfId) {
+			list.push_back(MTP_int(id.story));
+		}
+	}
+	if (list.empty()) {
+		return;
+	}
+	const auto api = &_owner->session().api();
+	api->request(MTPstories_TogglePinned(
+		MTP_vector<MTPint>(list),
+		MTP_bool(pinned)
+	)).done([=](const MTPVector<MTPint> &result) {
+		auto &saved = _saved[selfId];
+		const auto loaded = saved.loaded;
+		const auto lastId = !saved.ids.list.empty()
+			? saved.ids.list.back()
+			: std::numeric_limits<StoryId>::max();
+		auto dirty = false;
+		for (const auto &id : result.v) {
+			if (const auto maybeStory = lookup({ selfId, id.v })) {
+				const auto story = *maybeStory;
+				story->setPinned(pinned);
+				if (pinned) {
+					const auto add = loaded || (id.v >= lastId);
+					if (!add) {
+						dirty = true;
+					} else if (saved.ids.list.emplace(id.v).second) {
+						if (saved.total >= 0) {
+							++saved.total;
+						}
+					}
+				} else if (saved.ids.list.remove(id.v)) {
+					if (saved.total > 0) {
+						--saved.total;
+					}
+				} else if (!loaded) {
+					dirty = true;
+				}
+			} else if (!loaded) {
+				dirty = true;
+			}
+		}
+		if (dirty) {
+			savedLoadMore(selfId);
+		} else {
+			_savedChanged.fire_copy(selfId);
+		}
+	}).send();
+}
+
 void Stories::report(
 		std::shared_ptr<Ui::Show> show,
 		FullStoryId id,
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 630e8cc31..6cb415977 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -121,6 +121,8 @@ public:
 	explicit Stories(not_null<Session*> owner);
 	~Stories();
 
+	static constexpr auto kPinnedToastDuration = 4 * crl::time(1000);
+
 	[[nodiscard]] Session &owner() const;
 	[[nodiscard]] Main::Session &session() const;
 
@@ -183,6 +185,7 @@ public:
 	void savedLoadMore(PeerId peerId);
 
 	void deleteList(const std::vector<FullStoryId> &ids);
+	void togglePinnedList(const std::vector<FullStoryId> &ids, bool pinned);
 	void report(
 		std::shared_ptr<Ui::Show> show,
 		FullStoryId id,
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index b6958d4fa..21687e562 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -28,6 +28,25 @@ InfoPeerBadge {
 	sizeTag: int;
 }
 
+InfoTopBar {
+	height: pixels;
+	back: IconButton;
+	title: FlatLabel;
+	titlePosition: point;
+	bg: color;
+	mediaCancel: IconButton;
+	mediaActionsSkip: pixels;
+	mediaForward: IconButton;
+	mediaDelete: IconButton;
+	storiesSave: IconButton;
+	storiesArchive: IconButton;
+	search: IconButton;
+	searchRow: SearchFieldRow;
+	highlightBg: color;
+	highlightDuration: int;
+	radius: pixels;
+}
+
 infoMediaHeaderFg: windowFg;
 
 infoToggle: InfoToggle {
@@ -156,6 +175,14 @@ infoTopBarDelete: IconButton(infoTopBarForward) {
 	icon: icon {{ "info/info_media_delete", boxTitleCloseFg }};
 	iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }};
 }
+infoTopBarSaveStories: IconButton(infoTopBarForward) {
+	icon: icon {{ "menu/stories_saved", boxTitleCloseFg }};
+	iconOver: icon {{ "menu/stories_saved", boxTitleCloseFgOver }};
+}
+infoTopBarArchiveStories: IconButton(infoTopBarForward) {
+	icon: icon {{ "menu/archive", boxTitleCloseFg }};
+	iconOver: icon {{ "menu/archive", boxTitleCloseFgOver }};
+}
 infoTopBar: InfoTopBar {
 	height: infoTopBarHeight;
 	back: infoTopBarBack;
@@ -166,6 +193,8 @@ infoTopBar: InfoTopBar {
 	mediaActionsSkip: 4px;
 	mediaForward: infoTopBarForward;
 	mediaDelete: infoTopBarDelete;
+	storiesSave: infoTopBarSaveStories;
+	storiesArchive: infoTopBarArchiveStories;
 	search: infoTopBarSearch;
 	searchRow: infoTopBarSearchRow;
 	highlightBg: windowBgOver;
@@ -221,6 +250,14 @@ infoLayerTopBarDelete: IconButton(infoLayerTopBarForward) {
 	icon: icon {{ "info/info_media_delete", boxTitleCloseFg }};
 	iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }};
 }
+infoLayerTopBarSaveStories: IconButton(infoLayerTopBarForward) {
+	icon: icon {{ "menu/stories_saved", boxTitleCloseFg }};
+	iconOver: icon {{ "menu/stories_saved", boxTitleCloseFgOver }};
+}
+infoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) {
+	icon: icon {{ "menu/archive", boxTitleCloseFg }};
+	iconOver: icon {{ "menu/archive", boxTitleCloseFgOver }};
+}
 infoLayerTopBar: InfoTopBar(infoTopBar) {
 	height: infoLayerTopBarHeight;
 	back: infoLayerTopBarBack;
@@ -231,6 +268,8 @@ infoLayerTopBar: InfoTopBar(infoTopBar) {
 	mediaActionsSkip: 6px;
 	mediaForward: infoLayerTopBarForward;
 	mediaDelete: infoLayerTopBarDelete;
+	storiesSave: infoLayerTopBarSaveStories;
+	storiesArchive: infoLayerTopBarArchiveStories;
 	search: infoTopBarSearch;
 	searchRow: infoTopBarSearchRow;
 	radius: boxRadius;
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index 4f7af1f68..15473babc 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -172,6 +172,7 @@ void InnerWidget::createButtons() {
 			return !content.elements.empty();
 		}),
 		[] { return st::dialogsStories.height; });
+	thumbs->show();
 	rpl::combine(
 		recent->sizeValue(),
 		rpl::duplicate(last)
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index ab7e2ab48..3259f0958 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -39,6 +39,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/message_sending_animation_common.h"
 #include "ui/effects/reaction_fly_animation.h"
 #include "ui/layers/box_content.h"
+#include "ui/text/text_utilities.h"
+#include "ui/toast/toast.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
 #include "ui/round_rect.h"
@@ -1254,8 +1256,16 @@ void Controller::deleteRequested() {
 		return;
 	}
 	const auto id = story->fullId();
+	const auto weak = base::make_weak(this);
 	const auto owner = &story->owner();
 	const auto confirmed = [=](Fn<void()> close) {
+		if (const auto strong = weak.get()) {
+			if (const auto story = strong->story()) {
+				if (story->fullId() == id) {
+					moveFromShown();
+				}
+			}
+		}
 		owner->stories().deleteList({ id });
 		close();
 	};
@@ -1290,6 +1300,38 @@ void Controller::reportRequested() {
 	}));
 }
 
+void Controller::togglePinnedRequested(bool pinned) {
+	const auto story = this->story();
+	if (!story || !story->peer()->isSelf()) {
+		return;
+	}
+	if (!pinned && v::is<Data::StoriesContextSaved>(_context.data)) {
+		moveFromShown();
+	}
+	story->owner().stories().togglePinnedList({ story->fullId() }, pinned);
+	uiShow()->showToast({
+		.text = (pinned
+			? tr::lng_stories_save_done(
+				tr::now,
+				Ui::Text::Bold).append(
+					'\n').append(
+						tr::lng_stories_save_done_about(tr::now))
+			: tr::lng_stories_archive_done(
+				tr::now,
+				Ui::Text::WithEntities)),
+		.st = &st::storiesActionToast,
+		.duration = (pinned
+			? Data::Stories::kPinnedToastDuration
+			: Ui::Toast::kDefaultDuration),
+	});
+}
+
+void Controller::moveFromShown() {
+	if (!subjumpFor(1)) {
+		[[maybe_unused]] const auto jumped = subjumpFor(-1);
+	}
+}
+
 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 c7137a250..87c588f37 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -91,7 +91,7 @@ struct ViewsSlice {
 	int left = 0;
 };
 
-class Controller final {
+class Controller final : public base::has_weak_ptr {
 public:
 	explicit Controller(not_null<Delegate*> delegate);
 	~Controller();
@@ -135,6 +135,7 @@ public:
 	void shareRequested();
 	void deleteRequested();
 	void reportRequested();
+	void togglePinnedRequested(bool pinned);
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
@@ -171,6 +172,7 @@ private:
 
 	void subjumpTo(int index);
 	void checkWaitingFor();
+	void moveFromShown();
 
 	void refreshViewsFromData();
 	bool sliceViewsTo(PeerId offset);
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
index 79528eb11..6a6a0dbd4 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
@@ -83,6 +83,10 @@ void View::contentPressed(bool pressed) {
 	_controller->contentPressed(pressed);
 }
 
+void View::menuShown(bool shown) {
+	_controller->setMenuShown(shown);
+}
+
 void View::shareRequested() {
 	_controller->shareRequested();
 }
@@ -95,6 +99,10 @@ void View::reportRequested() {
 	_controller->reportRequested();
 }
 
+void View::togglePinnedRequested(bool pinned) {
+	_controller->togglePinnedRequested(pinned);
+}
+
 SiblingView View::sibling(SiblingType type) const {
 	return _controller->sibling(type);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h
index d2f62a0ac..24b58feed 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -75,10 +75,12 @@ public:
 	[[nodiscard]] bool paused() const;
 	void togglePaused(bool paused);
 	void contentPressed(bool pressed);
+	void menuShown(bool shown);
 
 	void shareRequested();
 	void deleteRequested();
 	void reportRequested();
+	void togglePinnedRequested(bool pinned);
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index 85a2716d3..00db429d1 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -841,3 +841,6 @@ storiesReportBox: ReportBox(defaultReportBox) {
 	personal: icon {{ "menu/personal", storiesComposeWhiteText }};
 	other: icon {{ "menu/report", storiesComposeWhiteText }};
 }
+storiesActionToast: Toast(defaultToast) {
+	maxWidth: 320px;
+}
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 1c83f2d5c..12b6e0332 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -1408,6 +1408,19 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
 			[=] { toMessage(); },
 			&st::mediaMenuIconShowInChat);
 	}
+	if (story && story->peer()->isSelf()) {
+		const auto pinned = story->pinned();
+		const auto text = pinned
+			? tr::lng_mediaview_archive_story(tr::now)
+			: tr::lng_mediaview_save_to_profile(tr::now);
+		addAction(text, [=] {
+			if (_stories) {
+				_stories->togglePinnedRequested(!pinned);
+			}
+		}, pinned
+			? &st::mediaMenuIconArchiveStory
+			: &st::mediaMenuIconSaveStory);
+	}
 	if ((!story || story->canDownload())
 		&& _document
 		&& !_document->filepath(true).isEmpty()) {
@@ -2156,6 +2169,9 @@ void OverlayWidget::hideControls(bool force) {
 
 void OverlayWidget::dropdownHidden() {
 	setFocus();
+	if (_stories) {
+		_stories->menuShown(false);
+	}
 	_ignoringDropdown = true;
 	_lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos());
 	updateOver(_lastMouseMovePos);
@@ -5387,7 +5403,7 @@ void OverlayWidget::updateOverRect(Over state) {
 bool OverlayWidget::updateOverState(Over newState) {
 	bool result = true;
 	if (_over != newState) {
-		if (newState == Over::More && !_ignoringDropdown) {
+		if (!_stories && newState == Over::More && !_ignoringDropdown) {
 			_dropdownShowTimer.callOnce(0);
 		} else {
 			_dropdownShowTimer.cancel();
@@ -5637,7 +5653,13 @@ bool OverlayWidget::handleContextMenu(std::optional<QPoint> position) {
 	if (_menu->empty()) {
 		_menu = nullptr;
 	} else {
+		if (_stories) {
+			_stories->menuShown(true);
+		}
 		_menu->setDestroyedCallback(crl::guard(_widget, [=] {
+			if (_stories) {
+				_stories->menuShown(false);
+			}
 			activateControls();
 			_receiveMouse = false;
 			InvokeQueued(_widget, [=] { receiveMouse(); });
@@ -5905,6 +5927,9 @@ void OverlayWidget::showDropdown() {
 	_dropdown->moveToRight(0, height() - _dropdown->height());
 	_dropdown->showAnimated(Ui::PanelAnimation::Origin::BottomRight);
 	_dropdown->setFocus();
+	if (_stories) {
+		_stories->menuShown(true);
+	}
 }
 
 void OverlayWidget::handleTouchTimer() {
diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp
index f2144d1cc..e48d6b742 100644
--- a/Telegram/SourceFiles/settings/settings_common.cpp
+++ b/Telegram/SourceFiles/settings/settings_common.cpp
@@ -219,6 +219,7 @@ void CreateRightLabel(
 	const auto name = Ui::CreateChild<Ui::FlatLabel>(
 		button.get(),
 		st.rightLabel);
+	name->show();
 	rpl::combine(
 		button->widthValue(),
 		std::move(buttonText),
diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style
index 922cbaab8..3c697f316 100644
--- a/Telegram/SourceFiles/ui/menu_icons.style
+++ b/Telegram/SourceFiles/ui/menu_icons.style
@@ -126,6 +126,8 @@ mediaMenuIconDelete: icon {{ "menu/delete", mediaviewMenuFg }};
 mediaMenuIconShowAll: icon {{ "menu/all_media", mediaviewMenuFg }};
 mediaMenuIconProfile: icon {{ "menu/profile", mediaviewMenuFg }};
 mediaMenuIconReport: icon {{ "menu/report", mediaviewMenuFg }};
+mediaMenuIconSaveStory: icon {{ "menu/stories_saved", mediaviewMenuFg }};
+mediaMenuIconArchiveStory: icon {{ "menu/archive", mediaviewMenuFg }};
 
 menuIconDeleteAttention: icon {{ "menu/delete", menuIconAttentionColor }};
 menuIconLeaveAttention: icon {{ "menu/leave", menuIconAttentionColor }};
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 08f805486..67dc933d7 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 08f80548668df2737905365b254624055642b937
+Subproject commit 67dc933d72fa5e20e6480bbff5a88a2c52d9d0d0

From 5f72a5238c6558fc77ac5211f2d693cd233e1e6b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 16:45:41 +0400
Subject: [PATCH 128/259] Save to Profile / Archive / Delete in list.

---
 Telegram/SourceFiles/data/data_stories.cpp    |  2 +
 .../SourceFiles/data/data_stories_ids.cpp     |  2 +-
 Telegram/SourceFiles/history/history_item.cpp |  2 +-
 Telegram/SourceFiles/info/info_top_bar.cpp    | 39 +++++++++-
 Telegram/SourceFiles/info/info_top_bar.h      |  6 ++
 .../SourceFiles/info/info_wrap_widget.cpp     |  2 +
 Telegram/SourceFiles/info/info_wrap_widget.h  |  2 +
 .../info/media/info_media_common.h            | 13 ++--
 .../info/media/info_media_list_section.cpp    |  4 +-
 .../info/media/info_media_list_widget.cpp     | 75 ++++++++++++++++++-
 .../info/media/info_media_list_widget.h       |  1 +
 .../info/stories/info_stories_provider.cpp    | 13 ++--
 .../stories/media_stories_controller.cpp      | 46 ++++++++----
 .../media/stories/media_stories_controller.h  |  8 ++
 14 files changed, 179 insertions(+), 36 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index a6db98e37..dfb5129e5 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -1298,6 +1298,8 @@ void Stories::togglePinnedList(
 		const auto loaded = saved.loaded;
 		const auto lastId = !saved.ids.list.empty()
 			? saved.ids.list.back()
+			: saved.lastId
+			? saved.lastId
 			: std::numeric_limits<StoryId>::max();
 		auto dirty = false;
 		for (const auto &id : result.v) {
diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp
index 31604f2eb..6a96f4e5f 100644
--- a/Telegram/SourceFiles/data/data_stories_ids.cpp
+++ b/Telegram/SourceFiles/data/data_stories_ids.cpp
@@ -44,7 +44,7 @@ rpl::producer<StoriesIdsSlice> SavedStoriesIds(
 			const auto hasBefore = int(around - begin(saved->list));
 			const auto hasAfter = int(end(saved->list) - around);
 			if (hasAfter < limit) {
-				//stories->savedLoadMore(peer->id);
+				stories->savedLoadMore(peer->id);
 			}
 			const auto takeBefore = std::min(hasBefore, limit);
 			const auto takeAfter = std::min(hasAfter, limit);
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 72745e74d..c6ddbb4c4 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -1988,7 +1988,7 @@ bool HistoryItem::canDelete() const {
 	if (isSponsored()) {
 		return false;
 	} else if (IsStoryMsgId(id)) {
-		return false && _history->peer->isSelf(); // #TODO stories
+		return false;
 	} else if (isService() && !isRegular()) {
 		return false;
 	} else if (topicRootId() == id) {
diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp
index 309ca167d..68aae3228 100644
--- a/Telegram/SourceFiles/info/info_top_bar.cpp
+++ b/Telegram/SourceFiles/info/info_top_bar.cpp
@@ -352,6 +352,10 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) {
 		_delete->moveToRight(right, 0, newWidth);
 		right += _delete->width();
 	}
+	if (_canToggleStoryPin) {
+		_toggleStoryPin->moveToRight(right, 0, newWidth);
+		right += _toggleStoryPin->width();
+	}
 	if (_canForward) {
 		_forward->moveToRight(right, 0, newWidth);
 		right += _forward->width();
@@ -496,6 +500,10 @@ void TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) {
 	updateControlsVisibility(anim::type::instant);
 }
 
+void TopBar::setStoriesArchive(bool archive) {
+	_storiesArchive = archive;
+}
+
 void TopBar::setSelectedItems(SelectedItems &&items) {
 	auto wasSelectionMode = selectionMode();
 	_selectedItems = std::move(items);
@@ -523,13 +531,14 @@ rpl::producer<SelectionAction> TopBar::selectionActionRequests() const {
 }
 
 void TopBar::updateSelectionState() {
-	Expects(_selectionText && _delete && _forward);
+	Expects(_selectionText && _delete && _forward && _toggleStoryPin);
 
 	_canDelete = computeCanDelete();
 	_canForward = computeCanForward();
 	_selectionText->entity()->setValue(generateSelectedText());
 	_delete->toggle(_canDelete, anim::type::instant);
 	_forward->toggle(_canForward, anim::type::instant);
+	_toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant);
 
 	updateSelectionControlsGeometry(width());
 }
@@ -544,6 +553,7 @@ void TopBar::createSelectionControls() {
 	};
 	_canDelete = computeCanDelete();
 	_canForward = computeCanForward();
+	_canToggleStoryPin = computeCanToggleStoryPin();
 	_cancelSelection = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
 		this,
 		object_ptr<Ui::IconButton>(this, _st.mediaCancel),
@@ -595,6 +605,24 @@ void TopBar::createSelectionControls() {
 		_selectionActionRequests,
 		_cancelSelection->lifetime());
 	_delete->entity()->setVisible(_canDelete);
+	const auto archive =
+	_toggleStoryPin = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
+		this,
+		object_ptr<Ui::IconButton>(
+			this,
+			_storiesArchive ? _st.storiesSave : _st.storiesArchive),
+		st::infoTopBarScale));
+	registerToggleControlCallback(
+		_toggleStoryPin.data(),
+		[this] { return selectionMode() && _canToggleStoryPin; });
+	_toggleStoryPin->setDuration(st::infoTopBarDuration);
+	_toggleStoryPin->entity()->clicks(
+	) | rpl::map_to(
+		SelectionAction::ToggleStoryPin
+	) | rpl::start_to_stream(
+		_selectionActionRequests,
+		_cancelSelection->lifetime());
+	_toggleStoryPin->entity()->setVisible(_canToggleStoryPin);
 
 	updateControlsGeometry(width());
 }
@@ -607,6 +635,12 @@ bool TopBar::computeCanForward() const {
 	return ranges::all_of(_selectedItems.list, &SelectedItem::canForward);
 }
 
+bool TopBar::computeCanToggleStoryPin() const {
+	return ranges::all_of(
+		_selectedItems.list,
+		&SelectedItem::canToggleStoryPin);
+}
+
 Ui::StringWithNumbers TopBar::generateSelectedText() const {
 	using Type = Storage::SharedMediaType;
 	const auto phrase = [&] {
@@ -618,8 +652,7 @@ Ui::StringWithNumbers TopBar::generateSelectedText() const {
 		case Type::MusicFile: return tr::lng_media_selected_song;
 		case Type::Link: return tr::lng_media_selected_link;
 		case Type::RoundVoiceFile: return tr::lng_media_selected_audio;
-			// #TODO stories
-		case Type::PhotoVideo: return tr::lng_media_selected_photo;
+		case Type::PhotoVideo: return tr::lng_stories_row_count;
 		}
 		Unexpected("Type in TopBar::generateSelectedText()");
 	}();
diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h
index b038e213a..c41125904 100644
--- a/Telegram/SourceFiles/info/info_top_bar.h
+++ b/Telegram/SourceFiles/info/info_top_bar.h
@@ -57,6 +57,7 @@ public:
 
 	void setTitle(rpl::producer<QString> &&title);
 	void setStories(rpl::producer<Dialogs::Stories::Content> content);
+	void setStoriesArchive(bool archive);
 	void enableBackButton();
 	void highlight();
 
@@ -120,11 +121,13 @@ private:
 	[[nodiscard]] Ui::StringWithNumbers generateSelectedText() const;
 	[[nodiscard]] bool computeCanDelete() const;
 	[[nodiscard]] bool computeCanForward() const;
+	[[nodiscard]] bool computeCanToggleStoryPin() const;
 	void updateSelectionState();
 	void createSelectionControls();
 
 	void performForward();
 	void performDelete();
+	void performToggleStoryPin();
 
 	void setSearchField(
 		base::unique_qptr<Ui::InputField> field,
@@ -163,10 +166,13 @@ private:
 	SelectedItems _selectedItems;
 	bool _canDelete = false;
 	bool _canForward = false;
+	bool _canToggleStoryPin = false;
+	bool _storiesArchive = false;
 	QPointer<Ui::FadeWrap<Ui::IconButton>> _cancelSelection;
 	QPointer<Ui::FadeWrap<Ui::LabelWithNumbers>> _selectionText;
 	QPointer<Ui::FadeWrap<Ui::IconButton>> _forward;
 	QPointer<Ui::FadeWrap<Ui::IconButton>> _delete;
+	QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryPin;
 	rpl::event_stream<SelectionAction> _selectionActionRequests;
 
 	QPointer<Ui::FadeWrap<Dialogs::Stories::List>> _stories;
diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp
index 2d8e2669a..214e191fb 100644
--- a/Telegram/SourceFiles/info/info_wrap_widget.cpp
+++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp
@@ -585,6 +585,8 @@ void WrapWidget::finishShowContent() {
 	if (_topBar) {
 		_topBar->setTitle(_content->title());
 		_topBar->setStories(_content->titleStories());
+		_topBar->setStoriesArchive(
+			_controller->key().storiesTab() == Stories::Tab::Archive);
 	}
 	_desiredHeights.fire(desiredHeightForContent());
 	_desiredShadowVisibilities.fire(_content->desiredShadowVisibility());
diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h
index a04c4d89c..47215d340 100644
--- a/Telegram/SourceFiles/info/info_wrap_widget.h
+++ b/Telegram/SourceFiles/info/info_wrap_widget.h
@@ -58,6 +58,7 @@ struct SelectedItem {
 	GlobalMsgId globalId;
 	bool canDelete = false;
 	bool canForward = false;
+	bool canToggleStoryPin = false;
 };
 
 struct SelectedItems {
@@ -73,6 +74,7 @@ enum class SelectionAction {
 	Clear,
 	Forward,
 	Delete,
+	ToggleStoryPin,
 };
 
 class WrapWidget final : public Window::SectionWidget {
diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h
index 20c3051ca..27e8d4b9f 100644
--- a/Telegram/SourceFiles/info/media/info_media_common.h
+++ b/Telegram/SourceFiles/info/media/info_media_common.h
@@ -30,15 +30,12 @@ struct ListItemSelectionData {
 	TextSelection text;
 	bool canDelete = false;
 	bool canForward = false;
-};
+	bool canToggleStoryPin = false;
 
-inline bool operator==(
-		ListItemSelectionData a,
-		ListItemSelectionData b) {
-	return (a.text == b.text)
-		&& (a.canDelete == b.canDelete)
-		&& (a.canForward == b.canForward);
-}
+	friend inline bool operator==(
+		ListItemSelectionData,
+		ListItemSelectionData) = default;
+};
 
 using ListSelectedMap = base::flat_map<
 	not_null<const HistoryItem*>,
diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp
index ed70623ce..2bf957192 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp
@@ -338,7 +338,7 @@ void ListSection::resizeToWidth(int newWidth) {
 	switch (_type) {
 	case Type::Photo:
 	case Type::Video:
-	case Type::PhotoVideo: // #TODO stories
+	case Type::PhotoVideo:
 	case Type::RoundFile: {
 		const auto skip = st::infoMediaSkip;
 		_itemsLeft = st::infoMediaLeft;
@@ -379,7 +379,7 @@ int ListSection::recountHeight() {
 	switch (_type) {
 	case Type::Photo:
 	case Type::Video:
-	case Type::PhotoVideo: // #TODO stories
+	case Type::PhotoVideo:
 	case Type::RoundFile: {
 		auto itemHeight = _itemHeight + 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 7940bca59..985fde209 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/history.h"
 #include "history/view/history_view_cursor_state.h"
 #include "history/view/history_view_service_message.h"
+#include "media/stories/media_stories_controller.h" // ...TogglePinnedToast.
 #include "window/window_session_controller.h"
 #include "window/window_peer_menu.h"
 #include "ui/widgets/popup_menu.h"
@@ -55,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "boxes/peer_list_controllers.h"
 #include "core/file_utilities.h"
 #include "core/application.h"
+#include "ui/toast/toast.h"
 #include "styles/style_overview.h"
 #include "styles/style_info.h"
 #include "styles/style_layers.h"
@@ -258,6 +260,7 @@ void ListWidget::selectionAction(SelectionAction action) {
 	case SelectionAction::Clear: clearSelected(); return;
 	case SelectionAction::Forward: forwardSelected(); return;
 	case SelectionAction::Delete: deleteSelected(); return;
+	case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return;
 	}
 }
 
@@ -335,6 +338,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems {
 		auto result = SelectedItem(item->globalId());
 		result.canDelete = selection.canDelete;
 		result.canForward = selection.canForward;
+		result.canToggleStoryPin = selection.canToggleStoryPin;
 		return result;
 	};
 	auto transformation = [&](const auto &item) {
@@ -349,6 +353,12 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems {
 			std::back_inserter(items.list),
 			transformation);
 	}
+	if (_controller->storiesPeer() && items.list.size() > 1) {
+		// Don't allow forwarding more than one story.
+		for (auto &entry : items.list) {
+			entry.canForward = false;
+		}
+	}
 	return items;
 }
 
@@ -1126,6 +1136,43 @@ void ListWidget::deleteSelected() {
 	}));
 }
 
+void ListWidget::toggleStoryPinSelected() {
+	auto list = std::vector<FullStoryId>();
+	const auto confirmed = crl::guard(this, [=] {
+		clearSelected();
+	});
+	for (const auto &item : collectSelectedItems().list) {
+		const auto id = item.globalId.itemId;
+		if (IsStoryMsgId(id.msg)) {
+			list.push_back({ id.peer, StoryIdFromMsgId(id.msg) });
+		}
+	}
+	const auto count = int(list.size());
+	const auto pin = (_controller->storiesTab() == Stories::Tab::Archive);
+	const auto controller = _controller;
+	const auto sure = [=](Fn<void()> close) {
+		controller->session().data().stories().togglePinnedList(list, pin);
+		controller->showToast(
+			::Media::Stories::PrepareTogglePinnedToast(count, pin));
+		close();
+		confirmed();
+	};
+	const auto session = &_controller->session();
+	const auto onePhrase = pin
+		? tr::lng_stories_save_sure
+		: tr::lng_stories_archive_sure;
+	const auto manyPhrase = pin
+		? tr::lng_stories_save_sure_many
+		: tr::lng_stories_archive_sure_many;
+	_controller->parentController()->show(Ui::MakeConfirmBox({
+		.text = (count == 1
+			? onePhrase()
+			: manyPhrase(lt_count, rpl::single(count) | tr::to_count())),
+		.confirmed = sure,
+		.confirmText = tr::lng_box_ok(),
+	}));
+}
+
 void ListWidget::deleteItem(GlobalMsgId globalId) {
 	if (const auto item = MessageByGlobalId(globalId)) {
 		auto items = SelectedItems(_provider->type());
@@ -1134,7 +1181,6 @@ void ListWidget::deleteItem(GlobalMsgId globalId) {
 			item,
 			FullSelection);
 		items.list.back().canDelete = selectionData.canDelete;
-		items.list.back().canForward = selectionData.canForward;
 		deleteItems(std::move(items));
 	}
 }
@@ -1180,6 +1226,33 @@ void ListWidget::deleteItems(SelectedItems &&items, Fn<void()> confirmed) {
 			.confirmText = tr::lng_box_delete(tr::now),
 			.confirmStyle = &st::attentionBoxButton,
 		})));
+	} else if (_controller->storiesPeer()) {
+		auto list = std::vector<FullStoryId>();
+		for (const auto &item : items.list) {
+			const auto id = item.globalId.itemId;
+			if (IsStoryMsgId(id.msg)) {
+				list.push_back({ id.peer, StoryIdFromMsgId(id.msg) });
+			}
+		}
+		const auto session = &_controller->session();
+		const auto sure = [=](Fn<void()> close) {
+			session->data().stories().deleteList(list);
+			close();
+			if (confirmed) {
+				confirmed();
+			}
+		};
+		const auto count = int(list.size());
+		window->show(Ui::MakeConfirmBox({
+			.text = (count == 1
+				? tr::lng_stories_delete_one_sure()
+				: tr::lng_stories_delete_sure(
+					lt_count,
+					rpl::single(count) | tr::to_count())),
+			.confirmed = sure,
+			.confirmText = tr::lng_selected_delete(),
+			.confirmStyle = &st::attentionBoxButton,
+		}));
 	} else if (auto list = collectSelectedIds(items); !list.empty()) {
 		auto box = Box<DeleteMessagesBox>(
 			&_controller->session(),
diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h
index 701754c13..b518e212e 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.h
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h
@@ -189,6 +189,7 @@ private:
 	void forwardItem(GlobalMsgId globalId);
 	void forwardItems(MessageIdsList &&items);
 	void deleteSelected();
+	void toggleStoryPinSelected();
 	void deleteItem(GlobalMsgId globalId);
 	void deleteItems(SelectedItems &&items, Fn<void()> confirmed = nullptr);
 	void applyItemSelection(
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
index 2c6621fff..c42569c8d 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
@@ -61,7 +61,7 @@ Type Provider::type() {
 }
 
 bool Provider::hasSelectRestriction() {
-	return true; // #TODO stories
+	return !_peer->isSelf();
 }
 
 rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
@@ -312,8 +312,10 @@ ListItemSelectionData Provider::computeSelectionData(
 		not_null<const HistoryItem*> item,
 		TextSelection selection) {
 	auto result = ListItemSelectionData(selection);
-	result.canDelete = item->canDelete();
-	result.canForward = item->allowsForward();
+	const auto peer = item->history()->peer;
+	result.canDelete = peer->isSelf();
+	result.canForward = peer->isSelf();
+	result.canToggleStoryPin = peer->isSelf();
 	return result;
 }
 
@@ -334,9 +336,10 @@ void Provider::applyDragSelection(
 		}
 	}
 	for (auto &layoutItem : _layouts) {
-		const auto id = StoryIdToMsgId(layoutItem.first);
+		const auto storyId = layoutItem.first;
+		const auto id = StoryIdToMsgId(storyId);
 		if (id <= fromId && id > tillId) {
-			const auto i = _items.find(id);
+			const auto i = _items.find(storyId);
 			Assert(i != end(_items));
 			const auto item = i->second.get();
 			ChangeItemSelection(
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 3259f0958..7ae534475 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -1309,21 +1309,7 @@ void Controller::togglePinnedRequested(bool pinned) {
 		moveFromShown();
 	}
 	story->owner().stories().togglePinnedList({ story->fullId() }, pinned);
-	uiShow()->showToast({
-		.text = (pinned
-			? tr::lng_stories_save_done(
-				tr::now,
-				Ui::Text::Bold).append(
-					'\n').append(
-						tr::lng_stories_save_done_about(tr::now))
-			: tr::lng_stories_archive_done(
-				tr::now,
-				Ui::Text::WithEntities)),
-		.st = &st::storiesActionToast,
-		.duration = (pinned
-			? Data::Stories::kPinnedToastDuration
-			: Ui::Toast::kDefaultDuration),
-	});
+	uiShow()->showToast(PrepareTogglePinnedToast(1, pinned));
 }
 
 void Controller::moveFromShown() {
@@ -1375,4 +1361,34 @@ void Controller::startReactionAnimation(
 	}, layer->lifetime());
 }
 
+Ui::Toast::Config PrepareTogglePinnedToast(int count, bool pinned) {
+	return {
+		.text = (pinned
+			? (count == 1
+				? tr::lng_stories_save_done(
+					tr::now,
+					Ui::Text::Bold)
+				: tr::lng_stories_save_done_many(
+					tr::now,
+					lt_count,
+					count,
+					Ui::Text::Bold)).append(
+						'\n').append(
+							tr::lng_stories_save_done_about(tr::now))
+			: (count == 1
+				? tr::lng_stories_archive_done(
+					tr::now,
+					Ui::Text::WithEntities)
+				: tr::lng_stories_archive_done_many(
+					tr::now,
+					lt_count,
+					count,
+					Ui::Text::WithEntities))),
+		.st = &st::storiesActionToast,
+		.duration = (pinned
+			? Data::Stories::kPinnedToastDuration
+			: Ui::Toast::kDefaultDuration),
+	};
+}
+
 } // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 87c588f37..9bcbe4049 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -34,6 +34,10 @@ struct MessageSendingAnimationFrom;
 class EmojiFlyAnimation;
 } // namespace Ui
 
+namespace Ui::Toast {
+struct Config;
+} // namespace Ui::Toast
+
 namespace Main {
 class Session;
 } // namespace Main
@@ -253,4 +257,8 @@ private:
 
 };
 
+[[nodiscard]] Ui::Toast::Config PrepareTogglePinnedToast(
+	int count,
+	bool pinned);
+
 } // namespace Media::Stories

From a733b836429e522abd1d366d4ca48a1dc4ecebe4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 17:04:11 +0400
Subject: [PATCH 129/259] Use stories ShareBox from the saved stories list.

---
 .../info/media/info_media_list_widget.cpp       | 12 +++++++++++-
 .../media/stories/media_stories_controller.cpp  |  2 +-
 .../media/stories/media_stories_share.cpp       | 17 ++++++++++-------
 .../media/stories/media_stories_share.h         |  3 ++-
 4 files changed, 24 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
index 985fde209..ee7ee0ad3 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/view/history_view_cursor_state.h"
 #include "history/view/history_view_service_message.h"
 #include "media/stories/media_stories_controller.h" // ...TogglePinnedToast.
+#include "media/stories/media_stories_share.h" // PrepareShareBox.
 #include "window/window_session_controller.h"
 #include "window/window_peer_menu.h"
 #include "ui/widgets/popup_menu.h"
@@ -1104,7 +1105,16 @@ void ListWidget::contextMenuEvent(QContextMenuEvent *e) {
 }
 
 void ListWidget::forwardSelected() {
-	if (auto items = collectSelectedIds(); !items.empty()) {
+	if (_controller->storiesPeer()) {
+		const auto ids = collectSelectedIds();
+		if (ids.size() == 1 && IsStoryMsgId(ids.front().msg)) {
+			const auto id = ids.front();
+			_controller->parentController()->show(
+				::Media::Stories::PrepareShareBox(
+					_controller->parentController()->uiShow(),
+					{ id.peer, StoryIdFromMsgId(id.msg) }));
+		}
+	} else if (auto items = collectSelectedIds(); !items.empty()) {
 		forwardItems(std::move(items));
 	}
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 7ae534475..555611ea4 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -1245,7 +1245,7 @@ void Controller::unfocusReply() {
 
 void Controller::shareRequested() {
 	const auto show = _delegate->storiesShow();
-	if (auto box = PrepareShareBox(show, _shown)) {
+	if (auto box = PrepareShareBox(show, _shown, true)) {
 		show->show(std::move(box));
 	}
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp
index 4d1774ff8..0a9b87df5 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp
@@ -32,7 +32,8 @@ namespace Media::Stories {
 
 [[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(
 		std::shared_ptr<ChatHelpers::Show> show,
-		FullStoryId id) {
+		FullStoryId id,
+		bool viewerStyle) {
 	const auto session = &show->session();
 	const auto resolve = [=] {
 		const auto maybeStory = session->data().stories().lookup(id);
@@ -170,7 +171,7 @@ namespace Media::Stories {
 		}
 	};
 
-	const auto scheduleStyle = [&] {
+	const auto viewerScheduleStyle = [&] {
 		auto date = Ui::ChooseDateTimeStyleArgs();
 		date.labelStyle = &st::groupCallBoxLabel;
 		date.dateFieldStyle = &st::groupCallScheduleDateField;
@@ -191,11 +192,13 @@ namespace Media::Stories {
 		.copyCallback = std::move(copyLinkCallback),
 		.submitCallback = std::move(submitCallback),
 		.filterCallback = std::move(filterCallback),
-		.stMultiSelect = &st::groupCallMultiSelect,
-		.stComment = &st::groupCallShareBoxComment,
-		.st = &st::groupCallShareBoxList,
-		.stLabel = &st::groupCallField,
-		.scheduleBoxStyle = scheduleStyle(),
+		.stMultiSelect = viewerStyle ? &st::groupCallMultiSelect : nullptr,
+		.stComment = viewerStyle ? &st::groupCallShareBoxComment : nullptr,
+		.st = viewerStyle ? &st::groupCallShareBoxList : nullptr,
+		.stLabel = viewerStyle ? &st::groupCallField : nullptr,
+		.scheduleBoxStyle = (viewerStyle
+			? viewerScheduleStyle()
+			: HistoryView::ScheduleBoxStyleArgs()),
 	});
 }
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.h b/Telegram/SourceFiles/media/stories/media_stories_share.h
index e3dab6245..efb633930 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_share.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_share.h
@@ -21,6 +21,7 @@ namespace Media::Stories {
 
 [[nodiscard]] object_ptr<Ui::BoxContent> PrepareShareBox(
 	std::shared_ptr<ChatHelpers::Show> show,
-	FullStoryId id);
+	FullStoryId id,
+	bool viewerStyle = false);
 
 } // namespace Media::Stories

From ac534780cc3b56eca7164ce1e9999ead49988c29 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 17:56:54 +0400
Subject: [PATCH 130/259] Implement context menu actions in stories list.

---
 .../info/media/info_media_list_widget.cpp     | 96 ++++++++++++++-----
 .../info/media/info_media_list_widget.h       |  3 +
 .../info/stories/info_stories_provider.cpp    | 16 +++-
 .../stories/media_stories_controller.cpp      | 37 ++++---
 .../media/stories/media_stories_controller.h  |  9 ++
 5 files changed, 116 insertions(+), 45 deletions(-)

diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
index ee7ee0ad3..7fb64b9d2 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -915,6 +915,11 @@ void ListWidget::showContextMenu(
 	auto canForwardAll = [&] {
 		return ranges::none_of(_selected, [](auto &&item) {
 			return !item.second.canForward;
+		}) && (!_controller->key().storiesPeer() || _selected.size() == 1);
+	};
+	auto canToggleStoryPinAll = [&] {
+		return ranges::none_of(_selected, [](auto &&item) {
+			return !item.second.canToggleStoryPin;
 		});
 	};
 
@@ -1016,6 +1021,16 @@ void ListWidget::showContextMenu(
 		}
 	}
 	if (overSelected == SelectionState::OverSelectedItems) {
+		if (canToggleStoryPinAll()) {
+			const auto tab = _controller->key().storiesTab();
+			const auto pin = (tab == Stories::Tab::Archive);
+			_contextMenu->addAction(
+				(pin
+					? tr::lng_mediaview_save_to_profile
+					: tr::lng_archived_add)(tr::now),
+				crl::guard(this, [this] { toggleStoryPinSelected(); }),
+				(pin ? &st::menuIconStoriesSaved : &st::menuIconArchive));
+		}
 		if (canForwardAll()) {
 			_contextMenu->addAction(
 				tr::lng_context_forward_selected(tr::now),
@@ -1045,6 +1060,18 @@ void ListWidget::showContextMenu(
 			const auto selectionData = _provider->computeSelectionData(
 				item,
 				FullSelection);
+			if (selectionData.canToggleStoryPin) {
+				const auto tab = _controller->key().storiesTab();
+				const auto pin = (tab == Stories::Tab::Archive);
+				_contextMenu->addAction(
+					(pin
+						? tr::lng_mediaview_save_to_profile
+						: tr::lng_mediaview_archive_story)(tr::now),
+					crl::guard(this, [=] {
+						toggleStoryPin({ 1, globalId.itemId });
+					}),
+					(pin ? &st::menuIconStoriesSaved : &st::menuIconArchive));
+			}
 			if (selectionData.canForward) {
 				_contextMenu->addAction(
 					tr::lng_context_forward_msg(tr::now),
@@ -1066,6 +1093,20 @@ void ListWidget::showContextMenu(
 				}
 			}
 		}
+		if (const auto peer = _controller->key().storiesPeer()) {
+			if (!peer->isSelf() && IsStoryMsgId(globalId.itemId.msg)) {
+				const auto storyId = FullStoryId{
+					globalId.itemId.peer,
+					StoryIdFromMsgId(globalId.itemId.msg),
+				};
+				_contextMenu->addAction(
+					tr::lng_profile_report(tr::now),
+					[=] { ::Media::Stories::ReportRequested(
+						_controller->uiShow(),
+						storyId); },
+					&st::menuIconReport);
+			}
+		}
 		if (!_provider->hasSelectRestriction()) {
 			_contextMenu->addAction(
 				tr::lng_context_select_msg(tr::now),
@@ -1105,16 +1146,7 @@ void ListWidget::contextMenuEvent(QContextMenuEvent *e) {
 }
 
 void ListWidget::forwardSelected() {
-	if (_controller->storiesPeer()) {
-		const auto ids = collectSelectedIds();
-		if (ids.size() == 1 && IsStoryMsgId(ids.front().msg)) {
-			const auto id = ids.front();
-			_controller->parentController()->show(
-				::Media::Stories::PrepareShareBox(
-					_controller->parentController()->uiShow(),
-					{ id.peer, StoryIdFromMsgId(id.msg) }));
-		}
-	} else if (auto items = collectSelectedIds(); !items.empty()) {
+	if (auto items = collectSelectedIds(); !items.empty()) {
 		forwardItems(std::move(items));
 	}
 }
@@ -1129,15 +1161,25 @@ void ListWidget::forwardItem(GlobalMsgId globalId) {
 }
 
 void ListWidget::forwardItems(MessageIdsList &&items) {
-	auto callback = [weak = Ui::MakeWeak(this)] {
-		if (const auto strong = weak.data()) {
-			strong->clearSelected();
+	if (_controller->storiesPeer()) {
+		if (items.size() == 1 && IsStoryMsgId(items.front().msg)) {
+			const auto id = items.front();
+			_controller->parentController()->show(
+				::Media::Stories::PrepareShareBox(
+					_controller->parentController()->uiShow(),
+					{ id.peer, StoryIdFromMsgId(id.msg) }));
 		}
-	};
-	setActionBoxWeak(Window::ShowForwardMessagesBox(
-		_controller,
-		std::move(items),
-		std::move(callback)));
+	} else {
+		auto callback = [weak = Ui::MakeWeak(this)] {
+			if (const auto strong = weak.data()) {
+				strong->clearSelected();
+			}
+		};
+		setActionBoxWeak(Window::ShowForwardMessagesBox(
+			_controller,
+			std::move(items),
+			std::move(callback)));
+	}
 }
 
 void ListWidget::deleteSelected() {
@@ -1147,12 +1189,16 @@ void ListWidget::deleteSelected() {
 }
 
 void ListWidget::toggleStoryPinSelected() {
-	auto list = std::vector<FullStoryId>();
-	const auto confirmed = crl::guard(this, [=] {
+	toggleStoryPin(collectSelectedIds(), crl::guard(this, [=] {
 		clearSelected();
-	});
-	for (const auto &item : collectSelectedItems().list) {
-		const auto id = item.globalId.itemId;
+	}));
+}
+
+void ListWidget::toggleStoryPin(
+		MessageIdsList &&items,
+		Fn<void()> confirmed) {
+	auto list = std::vector<FullStoryId>();
+	for (const auto &id : items) {
 		if (IsStoryMsgId(id.msg)) {
 			list.push_back({ id.peer, StoryIdFromMsgId(id.msg) });
 		}
@@ -1165,7 +1211,9 @@ void ListWidget::toggleStoryPinSelected() {
 		controller->showToast(
 			::Media::Stories::PrepareTogglePinnedToast(count, pin));
 		close();
-		confirmed();
+		if (confirmed) {
+			confirmed();
+		}
 	};
 	const auto session = &_controller->session();
 	const auto onePhrase = pin
diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h
index b518e212e..10984f27e 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.h
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h
@@ -192,6 +192,9 @@ private:
 	void toggleStoryPinSelected();
 	void deleteItem(GlobalMsgId globalId);
 	void deleteItems(SelectedItems &&items, Fn<void()> confirmed = nullptr);
+	void toggleStoryPin(
+		MessageIdsList &&items,
+		Fn<void()> confirmed = nullptr);
 	void applyItemSelection(
 		HistoryItem *item,
 		TextSelection selection);
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
index c42569c8d..b4cf680c6 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
@@ -314,7 +314,21 @@ ListItemSelectionData Provider::computeSelectionData(
 	auto result = ListItemSelectionData(selection);
 	const auto peer = item->history()->peer;
 	result.canDelete = peer->isSelf();
-	result.canForward = peer->isSelf();
+	result.canForward = [&] {
+		if (!peer->isSelf()) {
+			return false;
+		}
+		const auto id = item->id;
+		if (!IsStoryMsgId(id)) {
+			return false;
+		}
+		const auto maybeStory = peer->owner().stories().lookup(
+			{ peer->id, StoryIdFromMsgId(id) });
+		if (!maybeStory) {
+			return false;
+		}
+		return (*maybeStory)->canShare();
+	}();
 	result.canToggleStoryPin = peer->isSelf();
 	return result;
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 555611ea4..e66172ee5 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -1278,26 +1278,7 @@ void Controller::deleteRequested() {
 }
 
 void Controller::reportRequested() {
-	const auto story = this->story();
-	if (!story) {
-		return;
-	}
-	const auto id = story->fullId();
-	const auto owner = &story->owner();
-	const auto confirmed = [=](Fn<void()> close) {
-		owner->stories().deleteList({ id });
-		close();
-	};
-	const auto show = uiShow();
-	const auto st = &st::storiesReportBox;
-	show->show(Box(Ui::ReportReasonBox, *st, Ui::ReportSource::Story, [=](
-			Ui::ReportReason reason) {
-		const auto done = [=](const QString &text) {
-			owner->stories().report(show, id, reason, text);
-			show->hideLayer();
-		};
-		show->showBox(Box(Ui::ReportDetailsBox, *st, done));
-	}));
+	ReportRequested(uiShow(), _shown, &st::storiesReportBox);
 }
 
 void Controller::togglePinnedRequested(bool pinned) {
@@ -1391,4 +1372,20 @@ Ui::Toast::Config PrepareTogglePinnedToast(int count, bool pinned) {
 	};
 }
 
+void ReportRequested(
+		std::shared_ptr<Main::SessionShow> show,
+		FullStoryId id,
+		const style::ReportBox *stOverride) {
+	const auto owner = &show->session().data();
+	const auto st = stOverride ? stOverride : &st::defaultReportBox;
+	show->show(Box(Ui::ReportReasonBox, *st, Ui::ReportSource::Story, [=](
+			Ui::ReportReason reason) {
+		const auto done = [=](const QString &text) {
+			owner->stories().report(show, id, reason, text);
+			show->hideLayer();
+		};
+		show->showBox(Box(Ui::ReportDetailsBox, *st, done));
+	}));
+}
+
 } // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 9bcbe4049..694082a04 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_stories.h"
 #include "ui/effects/animations.h"
 
+namespace style {
+struct ReportBox;
+} // namespace style
+
 namespace base {
 class PowerSaveBlocker;
 } // namespace base
@@ -40,6 +44,7 @@ struct Config;
 
 namespace Main {
 class Session;
+class SessionShow;
 } // namespace Main
 
 namespace Media::Player {
@@ -260,5 +265,9 @@ private:
 [[nodiscard]] Ui::Toast::Config PrepareTogglePinnedToast(
 	int count,
 	bool pinned);
+void ReportRequested(
+	std::shared_ptr<Main::SessionShow> show,
+	FullStoryId id,
+	const style::ReportBox *stOverride = nullptr);
 
 } // namespace Media::Stories

From fcc15dd52de878b33ff9c5c68cd24279f59d31b5 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 19:00:52 +0400
Subject: [PATCH 131/259] Open profile on header click.

---
 .../stories/media_stories_controller.cpp      | 23 +++++++++++++++++
 .../media/stories/media_stories_controller.h  |  4 +++
 .../media/stories/media_stories_header.cpp    | 11 ++++++--
 .../stories/media_stories_recent_views.cpp    | 25 ++-----------------
 4 files changed, 38 insertions(+), 25 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index e66172ee5..cf411bc7b 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -10,7 +10,9 @@ 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 "boxes/peers/prepare_short_info_box.h"
 #include "chat_helpers/compose/compose_show.h"
+#include "core/application.h"
 #include "core/update_checker.h"
 #include "data/stickers/data_custom_emoji.h"
 #include "data/data_changes.h"
@@ -45,6 +47,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/labels.h"
 #include "ui/round_rect.h"
 #include "ui/rp_widget.h"
+#include "window/window_controller.h"
+#include "window/window_session_controller.h"
 #include "styles/style_chat.h"
 #include "styles/style_chat_helpers.h"
 #include "styles/style_media_view.h"
@@ -1388,4 +1392,23 @@ void ReportRequested(
 	}));
 }
 
+object_ptr<Ui::BoxContent> PrepareShortInfoBox(not_null<PeerData*> peer) {
+	const auto open = [=] {
+		if (const auto window = Core::App().windowFor(peer)) {
+			window->invokeForSessionController(
+				&peer->session().account(),
+				peer,
+				[&](not_null<Window::SessionController*> controller) {
+					Core::App().hideMediaView();
+					controller->showPeerHistory(peer);
+				});
+		}
+	};
+	return ::PrepareShortInfoBox(
+		peer,
+		open,
+		[] { return false; },
+		&st::storiesShortInfoBox);
+}
+
 } // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 694082a04..5463cdc83 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include "base/object_ptr.h"
 #include "data/data_stories.h"
 #include "ui/effects/animations.h"
 
@@ -36,6 +37,7 @@ namespace Ui {
 class RpWidget;
 struct MessageSendingAnimationFrom;
 class EmojiFlyAnimation;
+class BoxContent;
 } // namespace Ui
 
 namespace Ui::Toast {
@@ -269,5 +271,7 @@ void ReportRequested(
 	std::shared_ptr<Main::SessionShow> show,
 	FullStoryId id,
 	const style::ReportBox *stOverride = nullptr);
+[[nodiscard]] object_ptr<Ui::BoxContent> PrepareShortInfoBox(
+	not_null<PeerData*> peer);
 
 } // namespace Media::Stories
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index cbd67f5e6..dc33da5f4 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -8,10 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/stories/media_stories_header.h"
 
 #include "base/unixtime.h"
+#include "chat_helpers/compose/compose_show.h"
 #include "data/data_user.h"
 #include "media/stories/media_stories_controller.h"
 #include "lang/lang_keys.h"
 #include "ui/controls/userpic_button.h"
+#include "ui/layers/box_content.h"
 #include "ui/text/format_values.h"
 #include "ui/widgets/labels.h"
 #include "ui/painter.h"
@@ -92,13 +94,16 @@ void Header::show(HeaderData data) {
 	if (userChanged) {
 		_date = nullptr;
 		const auto parent = _controller->wrap();
-		auto widget = std::make_unique<Ui::RpWidget>(parent);
+		auto widget = std::make_unique<Ui::AbstractButton>(parent);
 		const auto raw = widget.get();
-		raw->setAttribute(Qt::WA_TransparentForMouseEvents);
+		raw->setClickedCallback([=] {
+			_controller->uiShow()->show(PrepareShortInfoBox(_data->user));
+		});
 		const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
 			raw,
 			data.user,
 			st::storiesHeaderPhoto);
+		userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
 		userpic->show();
 		userpic->move(
 			st::storiesHeaderMargin.left(),
@@ -107,6 +112,7 @@ void Header::show(HeaderData data) {
 			raw,
 			data.user->firstName,
 			st::storiesHeaderName);
+		name->setAttribute(Qt::WA_TransparentForMouseEvents);
 		name->setOpacity(kNameOpacity);
 		name->move(st::storiesHeaderNamePosition);
 		raw->show();
@@ -122,6 +128,7 @@ void Header::show(HeaderData data) {
 		_widget.get(),
 		std::move(timestamp.text),
 		st::storiesHeaderDate);
+	_date->setAttribute(Qt::WA_TransparentForMouseEvents);
 	_date->setOpacity(kDateOpacity);
 	_date->show();
 	_date->move(st::storiesHeaderDatePosition);
diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
index 306e1a1f0..5f24fe717 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
@@ -8,8 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "media/stories/media_stories_recent_views.h"
 
 #include "api/api_who_reacted.h" // FormatReadDate.
-#include "boxes/peers/prepare_short_info_box.h"
-#include "core/application.h"
+#include "chat_helpers/compose/compose_show.h"
 #include "data/data_peer.h"
 #include "data/data_stories.h"
 #include "main/main_session.h"
@@ -22,8 +21,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/painter.h"
 #include "ui/rp_widget.h"
 #include "ui/userpic_view.h"
-#include "window/window_controller.h"
-#include "window/window_session_controller.h"
 #include "styles/style_chat_helpers.h"
 #include "styles/style_media_view.h"
 
@@ -332,24 +329,6 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
 	const auto show = _controller->uiShow();
 	const auto prepare = [&](Ui::PeerUserpicView &view) {
 		const auto size = st::storiesWhoViewed.photoSize;
-		auto callback = [=] {
-			const auto open = [=] {
-				if (const auto window = Core::App().windowFor(peer)) {
-					window->invokeForSessionController(
-						&peer->session().account(),
-						peer,
-						[&](not_null<Window::SessionController*> controller) {
-							Core::App().hideMediaView();
-							controller->showPeerHistory(peer);
-						});
-				}
-			};
-			show->show(PrepareShortInfoBox(
-				peer,
-				open,
-				[] { return false; },
-				&st::storiesShortInfoBox));
-		};
 		auto userpic = peer->generateUserpicImage(
 			view,
 			size * style::DevicePixelRatio());
@@ -358,7 +337,7 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
 			.text = peer->name(),
 			.date = date,
 			.userpic = std::move(userpic),
-			.callback = std::move(callback),
+			.callback = [=] { show->show(PrepareShortInfoBox(peer)); },
 		};
 	};
 	if (_menuPlaceholderCount > 0) {

From e66d9d5d249d03fd64f5e45d0bad2a9770faf68b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 20:43:18 +0400
Subject: [PATCH 132/259] Fix jump to top on a new message send.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index e921e8ec1..6c3e2e059 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -1111,7 +1111,7 @@ void Widget::jumpToTop(bool belowPinned) {
 		return;
 	}
 	if ((currentSearchQuery().trimmed().isEmpty() && !_searchInChat)) {
-		auto to = 0;
+		auto to = _inner->defaultScrollTop();
 		if (belowPinned) {
 			const auto list = _openedForum
 				? _openedForum->topicsList()

From ef1f1846a46e1764e4d18939db2b87041a85eea7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 30 Jun 2023 21:12:44 +0400
Subject: [PATCH 133/259] Respect PowerSaver in TranslateBox.

---
 Telegram/SourceFiles/boxes/translate_box.cpp | 11 +++++++++++
 Telegram/lib_ui                              |  2 +-
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp
index 066ba855b..b21cf229c 100644
--- a/Telegram/SourceFiles/boxes/translate_box.cpp
+++ b/Telegram/SourceFiles/boxes/translate_box.cpp
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/layers/generic_box.h"
 #include "ui/text/text_utilities.h"
 #include "ui/painter.h"
+#include "ui/power_saving.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
 #include "ui/widgets/multi_select.h"
@@ -131,6 +132,14 @@ void TranslateBox(
 	// 	container,
 	// 	tr::lng_translate_box_original());
 
+	const auto animationsPaused = [] {
+		using Which = FlatLabel::WhichAnimationsPaused;
+		const auto emoji = On(PowerSaving::kEmojiChat);
+		const auto spoiler = On(PowerSaving::kChatSpoiler);
+		return emoji
+			? (spoiler ? Which::All : Which::CustomEmoji)
+			: (spoiler ? Which::Spoiler : Which::None);
+	};
 	const auto original = box->addRow(object_ptr<SlideWrap<FlatLabel>>(
 		box,
 		object_ptr<FlatLabel>(box, stLabel)));
@@ -139,6 +148,7 @@ void TranslateBox(
 			original->entity()->setContextMenuHook([](auto&&) {
 			});
 		}
+		original->entity()->setAnimationsPausedCallback(animationsPaused);
 		original->entity()->setMarkedText(
 			text,
 			Core::MarkedTextContext{
@@ -194,6 +204,7 @@ void TranslateBox(
 		box,
 		object_ptr<FlatLabel>(box, stLabel)));
 	translated->entity()->setSelectable(!hasCopyRestriction);
+	translated->entity()->setAnimationsPausedCallback(animationsPaused);
 
 	constexpr auto kMaxLines = 3;
 	container->resizeToWidth(box->width());
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 67dc933d7..c3aab1bd1 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 67dc933d72fa5e20e6480bbff5a88a2c52d9d0d0
+Subproject commit c3aab1bd141101c32393edc07032113ae4f1122a

From f0ab6e5690deb3158e487bad6b1f990615edcf00 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 3 Jul 2023 11:33:28 +0400
Subject: [PATCH 134/259] Update main menu and stories icons.

---
 .../icons/info/info_stories_to_archive.png    | Bin 0 -> 671 bytes
 .../icons/info/info_stories_to_archive@2x.png | Bin 0 -> 1236 bytes
 .../icons/info/info_stories_to_archive@3x.png | Bin 0 -> 1869 bytes
 .../icons/info/info_stories_to_profile.png    | Bin 0 -> 770 bytes
 .../icons/info/info_stories_to_profile@2x.png | Bin 0 -> 1482 bytes
 .../icons/info/info_stories_to_profile@3x.png | Bin 0 -> 2200 bytes
 .../Resources/icons/menu/archive_open.png     | Bin 0 -> 473 bytes
 .../Resources/icons/menu/archive_open@2x.png  | Bin 0 -> 654 bytes
 .../Resources/icons/menu/archive_open@3x.png  | Bin 0 -> 952 bytes
 Telegram/Resources/icons/menu/channel.png     | Bin 610 -> 613 bytes
 Telegram/Resources/icons/menu/channel@2x.png  | Bin 1098 -> 1099 bytes
 Telegram/Resources/icons/menu/channel@3x.png  | Bin 1649 -> 1650 bytes
 Telegram/Resources/icons/menu/groups.png      | Bin 0 -> 685 bytes
 Telegram/Resources/icons/menu/groups@2x.png   | Bin 0 -> 1315 bytes
 Telegram/Resources/icons/menu/groups@3x.png   | Bin 0 -> 1946 bytes
 Telegram/Resources/icons/menu/night_mode.png  | Bin 0 -> 656 bytes
 .../Resources/icons/menu/night_mode@2x.png    | Bin 0 -> 1261 bytes
 .../Resources/icons/menu/night_mode@3x.png    | Bin 0 -> 1834 bytes
 Telegram/Resources/icons/menu/profile.png     | Bin 695 -> 602 bytes
 Telegram/Resources/icons/menu/profile@2x.png  | Bin 1430 -> 1306 bytes
 Telegram/Resources/icons/menu/profile@3x.png  | Bin 2186 -> 1961 bytes
 .../Resources/icons/menu/saved_messages.png   | Bin 0 -> 469 bytes
 .../icons/menu/saved_messages@2x.png          | Bin 0 -> 758 bytes
 .../icons/menu/saved_messages@3x.png          | Bin 0 -> 1076 bytes
 Telegram/Resources/icons/menu/settings.png    | Bin 567 -> 698 bytes
 Telegram/Resources/icons/menu/settings@2x.png | Bin 1189 -> 1256 bytes
 Telegram/Resources/icons/menu/settings@3x.png | Bin 1764 -> 2009 bytes
 .../Resources/icons/menu/stories_archive.png  | Bin 1847 -> 651 bytes
 .../icons/menu/stories_archive@2x.png         | Bin 2395 -> 1188 bytes
 .../icons/menu/stories_archive@3x.png         | Bin 2952 -> 1807 bytes
 .../icons/menu/stories_archive_section.png    | Bin 0 -> 1847 bytes
 .../icons/menu/stories_archive_section@2x.png | Bin 0 -> 2395 bytes
 .../icons/menu/stories_archive_section@3x.png | Bin 0 -> 2952 bytes
 .../Resources/icons/menu/stories_save.png     | Bin 0 -> 729 bytes
 .../Resources/icons/menu/stories_save@2x.png  | Bin 0 -> 1408 bytes
 .../Resources/icons/menu/stories_save@3x.png  | Bin 0 -> 2128 bytes
 .../Resources/icons/menu/stories_saved.png    | Bin 1935 -> 0 bytes
 .../Resources/icons/menu/stories_saved@2x.png | Bin 2521 -> 0 bytes
 .../Resources/icons/menu/stories_saved@3x.png | Bin 3136 -> 0 bytes
 .../icons/menu/stories_saved_section.png      | Bin 0 -> 774 bytes
 .../icons/menu/stories_saved_section@2x.png   | Bin 0 -> 1492 bytes
 .../icons/menu/stories_saved_section@3x.png   | Bin 0 -> 2223 bytes
 .../Resources/icons/menu/stories_to_chats.png | Bin 0 -> 793 bytes
 .../icons/menu/stories_to_chats@2x.png        | Bin 0 -> 1579 bytes
 .../icons/menu/stories_to_chats@3x.png        | Bin 0 -> 2274 bytes
 .../dialogs/ui/dialogs_stories_content.cpp    |   6 ++--
 Telegram/SourceFiles/info/info.style          |  16 +++++------
 .../info/media/info_media_list_widget.cpp     |   8 ++++--
 Telegram/SourceFiles/settings/settings.style  |   6 ----
 Telegram/SourceFiles/ui/menu_icons.style      |  13 +++++++--
 .../SourceFiles/window/window_main_menu.cpp   |  27 ++++++++----------
 51 files changed, 39 insertions(+), 37 deletions(-)
 create mode 100644 Telegram/Resources/icons/info/info_stories_to_archive.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_to_archive@2x.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_to_archive@3x.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_to_profile.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_to_profile@2x.png
 create mode 100644 Telegram/Resources/icons/info/info_stories_to_profile@3x.png
 create mode 100644 Telegram/Resources/icons/menu/archive_open.png
 create mode 100644 Telegram/Resources/icons/menu/archive_open@2x.png
 create mode 100644 Telegram/Resources/icons/menu/archive_open@3x.png
 create mode 100644 Telegram/Resources/icons/menu/groups.png
 create mode 100644 Telegram/Resources/icons/menu/groups@2x.png
 create mode 100644 Telegram/Resources/icons/menu/groups@3x.png
 create mode 100644 Telegram/Resources/icons/menu/night_mode.png
 create mode 100644 Telegram/Resources/icons/menu/night_mode@2x.png
 create mode 100644 Telegram/Resources/icons/menu/night_mode@3x.png
 create mode 100644 Telegram/Resources/icons/menu/saved_messages.png
 create mode 100644 Telegram/Resources/icons/menu/saved_messages@2x.png
 create mode 100644 Telegram/Resources/icons/menu/saved_messages@3x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_archive_section.png
 create mode 100644 Telegram/Resources/icons/menu/stories_archive_section@2x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_archive_section@3x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_save.png
 create mode 100644 Telegram/Resources/icons/menu/stories_save@2x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_save@3x.png
 delete mode 100644 Telegram/Resources/icons/menu/stories_saved.png
 delete mode 100644 Telegram/Resources/icons/menu/stories_saved@2x.png
 delete mode 100644 Telegram/Resources/icons/menu/stories_saved@3x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_saved_section.png
 create mode 100644 Telegram/Resources/icons/menu/stories_saved_section@2x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_saved_section@3x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_to_chats.png
 create mode 100644 Telegram/Resources/icons/menu/stories_to_chats@2x.png
 create mode 100644 Telegram/Resources/icons/menu/stories_to_chats@3x.png

diff --git a/Telegram/Resources/icons/info/info_stories_to_archive.png b/Telegram/Resources/icons/info/info_stories_to_archive.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9c81896eb1a9ab8a6470126e2e51693a1e9e5e0
GIT binary patch
literal 671
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfr-=8#WBP}
z@M*~Y(}jr=ZXzf9J-MD9f8}o0+3Rvr;bg)_!wB9S56OrrJZ#NZmHx4-8X0Ib^YDnU
zojU)$>i%cT=X;J<?hk(diT8fwxvw$bYva%VjX8h!@Z-Pr^DkzcNiq6cS8p`)Nzu;x
z5(@$}UVZ)bwyfGl?(xSRmtVFxDRN|Py>&J%SZk_^5a;%FaqH)wO`CptY1G;*6RE1k
z0F4|IsQ`Vk?vi<|r&Ekp<ZO@Le*3J@zePG?-Cj$7Y5r<I-u9w(p^{K158L5t)v{W3
zd&$WsOZLWTPvugc<F(Xpx$rsH=gf@^9y{D7F$uXY4s3J?(3|er#&AROol(oO%PS*v
z`i?(NF;ac<v7+s;;=9(KzpOgbPe-lYcKhuKnN^8%{mdnJPB5r@EWi9kW|i`ndb{~A
z4Vl(o&u&QD{B!Z{9;rF2ufI0*(D_+YXEyt2&AI2ryYKp`{rFqA{<^oGX2b6L@4xTJ
z=~|>X{q)wTwF$fjA4XlsTzz%X(me`g6?XGCM!az~`0us!Qr1?XHD}YDi!5Z))DnA@
zd)yWuO#<qPJak{v$5}<lG5-F`>!)6pByD;qlJ?~2-*59hRNj5Jes&<oY=8EH+=Iyz
z&Og8Jtm9)Kab(HES-bA$8O{9iv!;N(GHB(O3L7~-_Ok4VDHS#$VqC00SlvHvy`8JX
uE0nc$*4CEhBRi~R_}-N_J3pJ>V$ZO`<A7I*U`hxmad^7=xvX<aXaWG23K|Un

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_to_archive@2x.png b/Telegram/Resources/icons/info/info_stories_to_archive@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef905891c427675c553c682c341f4aa067b63afe
GIT binary patch
literal 1236
zcmV;_1S|WAP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGK1oDDR9Fe^SW76aQ4~J7Nb*i`
z^T=Bsr6eg*%1jY?zX#r7Aek`0KoQAAN=!^dN}>=GL_{Pqk`SR#-p_l#d)Kx7=lthv
z|Kk|la|Y+^z1I4^wa)t2UVHCfqcMF<88Btwdoy6BnBL9JEip0C+uQr+-_P0ESzTS-
z?d`2%LWMvV7nl0_`lY3%_xJZ-+Mnv`YK5fAVAj^w2<7VPN;`~yv)9+x`1p9njQnkE
zY#bjS=jZ1$p)m)sv9SjS2f|REpPy%DW?EZYD=I3IlaqaXe1!hW%gdSZ{QP`iV4zT|
zc3fCk`1JI|9PI4u;D0+hI&yfOoSb0BAm``j4h{|+dl{<c=4Qr=$}KG|wXm>|g151;
zX=`gkA_?y4=_v*Eg-uaW5sL;j;o;%&1wf<@2?=R$Z_m!o7HQ-QqNAf9A0H{8iHQkY
zTU+@Y#zP?CM@L5#4RV92@wgQ+H8eC(G^j@}FE2g^>H}RdG&GcNMp`6Zc7bD!uzpiq
zT>Pb*wY4=wkCHbxH|HBSH8sI<Z*R}u-kxu%rEP3%r1b3W?n+rxEP8$~fREYPSz<0J
zDbaGtpRBL16D%t$i*HF<=>b($RT1;z;-VDhKQ>s~gn4*)khrp>2juMR%zCV!pPx7;
zJ&!yoDT(aR^6&2M#27V_%gf7=kr86RDM3v2Lo5mo4kqE;+?-geUQ$z2gOkGQ>gvM6
zf|yNDK<@7DWQRT^2B?=HNd*N3hIsTmGM35H(^EsR@005Zh%Mdg>uZUQQT}Av*ViYn
z!H68_3FzYDg3Mf9T}Af#1!H4lq{zs~z&;Zh8L8h+nRIAqh?aeBZmvWID=Vws-d;lC
z8w<O(ww8m%PGDze$1%`Sb#!zPHTt!d;;Tt|dODsm3u}CQ99>5Y3JVLnzrRO;O5FQ2
zIXO8*Dx;Zxxb^k*#d>Bj;n4D-`TQLo9wvjz%F0hEXN01Dd3gcm?d?r^88JdjOUtgV
zE^@iOz0Kq_z%d3_CDH~31?gxFPSK+Y!!HGcc#S3@A)&LgGdw(8$0jv3m6*4-wscgy
zX<1nr!EmCGxSjFn1Ufo?fLvaIV%6;L?^8^m5D=;zVtq|dPmhj{%Ko2ud3h8N)^JQr
zjM_ld<8k(5>*eI+gzYElnUyo(q>LJ(JmJ=-b2nAas|JZ1G`s0xOTknwo4>#R;NalE
zz<}(7y&+F&X=!MzlqbAV!1jq335JY?a<m-Wda>^k7I#jeR@FEvDhg#py!gJkx#{li
zj*E-q+%XW%%E}6Z;KCUY5TGgu9WV~slarIo0S$0vo0^*H@9%GEX-P>*5kj!xGvnpu
z<%oy~q1I?)^k1AVm?xb<W@aYygyf-9Z*FelNtu}`e^18<4GzGLuzGrW4i67?0>WDx
ygD}YoRk7pEhVUIDo*ho_l3kk&O&R!4GVmKs-}PM~Xi32U0000<MNUMnLSTZ(**Fvc

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_to_archive@3x.png b/Telegram/Resources/icons/info/info_stories_to_archive@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc2fc30af1a0e8102a5e46ace761759182954d49
GIT binary patch
literal 1869
zcmV-T2eSByP)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS>w@E}nRA>e5T3JX{PZ&?l(riZ~
z$iOmF$-PoY(<l-_6wyL`kwVZA6qg4DgRqD4tp^uKXcLzf%@P%T&?l2jMNP#G(e_oz
zg|uA$|6kod<2dKsIp^MU@72ry+^275zVA2RH*@Z{%*>`u>Q7w(bp_NFP**@*0d)m7
z+zMD2Pa``!J6Bg%H#fIKhYs!BxpT*k9pAovD=sen^5u*1CN(0mkB`sw>(^hrc(J&+
z_>bT>GBUDd%N8R}Z*XX9YwMJhl-}N6fh}x@udlDcXEzq$g$ox(M@Lx;#M-m7vwQaJ
zVf4U9`|R1X`T6<Q)>eOie`a)jnx{^kYHMo~GlA{<@#DwKmoLwsKQB}?FE5X9FE1|_
z6&3B;wM)omq8<wiiyJp?5QP{5JM-kplS`K_g@lB-xVUWFwvD6Ky?gf<@sA%r#>U2S
zM2s~AMXRf;8Ew#n!+7xEK}1A^jg5``xr2j)fB*h1L`zRkmk(k9&I1PybaizJ^{A<-
zIePS{eiNKHaiY7so536y7|;(*A1MT-4<9}-8o<uer%&qxN6A=OS!HHs;u|L5jg5`e
zxG_n%FQG#(D=XW&b*rJKJ$Ue7Wo0Ei<f~V&jvP5+C|Dj2yaoILSxyiuD=Sy8UggP{
z#Taa$t%6^Ui;FW0&3Q_mIdca4LmQTplfx4*i!pqJ9UUFCRSzFNGz-lol)QNHB5f7?
zGC$yo!ks&J?&;H~B_$<}j*g-X{7(4H$jHc;m>7fQ(3>}J9v&X14Lf=ABwq>PsI#;4
zuV243Acze@2Faf1&!20=Q8z#(Nq%XDq@*O;s;a6gX&C{><;$08kiC2N3Rrj@SQlD6
zK0cnuEpv?CuBD|V6pk_lX}09#WLmTR`}fnMk~9heIAMZ-v|Ca{f&oPYfdg^p&K(H>
zEyD^818P%K6RnAdhliG_{_D2n4>OxyT3X8IH{R&|`}b*ZcoDj(Gu_wMN7iX_auTjd
zXNbQ7%6H7x))o~VQ3jrwuL*C*92^{u9Xm$k8yXt$p_8VG31p9tA3r9OqQW4wD8Vxf
zZXKbSh}G8C5_(;TTeog4EG%fIe)Q-OUk6@5z~LD!jO2rEnw}mU91I8uK(;VGJ}&uh
z=NZOSjh{Y!GQBa(jIaOxm`>3q?^DEojL1w)O(At4&5cj+48z$iF<>KX7g0tI!844`
zba6T@2TmIg<4$qFwtM$(ZOjJg=NU#^5N1-SM^8@=t!{L5Gy)Mk0o`dj8mCSqAb$S*
zS;@qfmX<h~A!r%`M{ZeJS#(5EGKCJZ$P9@p_?8qF3%GDWBqV@@sH>~9x3||pE#<5z
z{>k(pB!j)288b36NMT06-@kv4GgCR@etv!x6&1)EnNTE0c9jSJ1Tr^>(9qD;F$EM7
znz6Al8W}VSA{-Xsi{$|0e@@cHw{PF#euu-%8**}TqNgt?DByAHGKM<`Bz`owhR)B=
z=fGUKa)lOQ5+jbF$k4>Z1i^aq=8Xu4f-WeE)Ry3b;9qvOw6qXr2qNFtfAYI`?-DE!
z-rnB&V~I(6d3g;D4Us9@+uOys{`BqJw-28y;Y8i3jZ;>CSXdar0)cxYSz@{{;n=*e
zun;L*XJ;o}B{3MxcbJxz#xUsCL}iHxMr^~40i8hVhNX;~4iyN1l4|h05Hvyfx_$dL
zQEB`3?dx?|Pv3jMNQEihABzxJfkHM7Q3&3Wm})WtXY6xxb5H}y=yy^kqAG3{iD4kH
zVC#?v{q-Mb&z@D7wXQJ_p8<S5ZQHeL*Vb^DZzCZgfi?^`FHuoZ=4;Lx&CAQnY1?LI
zX7C7S4TqUF5a|(W7~8_b!_Cy3HEQF67+DW(8{z>H_BBk38^~{vzvEejySuw$Fxdb>
zK|!djY1=^KFbpY=Yz(c8cpU<_MCuyk2K4cSRxv{j1O^6v{rZ)$4S%`Z+*~DzDZCL5
z3>nzt$B!FIZCN;Y?a|-g&nST=N=9UHdX%JiQz1k{EH>$mX-r2(g42AVHt0bxz<aRl
z>}*d@Pvp3gONFp2#KG>NUY3+K3IkH))YMcu;S%bHp6TgnoK814H{;ap@ZrNkRp44_
zLHx&1aUw4ja+<UUTX*y3O{Qd|6{YES@7@Xjhqy1tOGOPEz5=|KGpPam$q)%}s`c~d
zPudJoTJ!_M=HmgN!3$Zw&N_@@_YxBm>+9?3w_db)C@8_B1ERjHtJh!pB8bc32njFs
z;5=~TLSAuy|Nb5CpgHxwIF{P0u7J7%>I$eUpsv9GZUz1YpfhfT!e^t800000NkvXX
Hu0mjfcdban

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_to_profile.png b/Telegram/Resources/icons/info/info_stories_to_profile.png
new file mode 100644
index 0000000000000000000000000000000000000000..cfe465c5b341a4f2b44c1ebf2c0b167dc98278f3
GIT binary patch
literal 770
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfhot+#WBP}
z@M(yzM^d0f?JTaes0U7*syWIXZj-pWwhA5$xFFWJQ^X}kbjS1-v9+-~F6>>T<mi#1
zutQ+c-Xl370$Y#xE|^sRKxS8AsoUYiFy><M{?BuZ-<9wGzW2Rd+t2^gqSo@YAMWq(
zpXzn=_1AyC%w^@}!7H!in9cTEe*XOVmnFAy%#<hkEq`5NbueMV;fHI^ufCcUy*6zB
zeg0Obq>UAY%>f#FcJDsSXg5E9Vtiw&(aqPdv-ie@uME+C;(sc^K;d!uEAjc~pWnZK
zf2!A0Dfz=2Zss&PY`9fcRMfOdUtfP^&-E*>zustWbrJ~D6yR78Ut>4ls92!;XaHaT
zapqR_RnBp1<mY!MOgK_dQ^WJ@(W9h&^A|;}ZR2-2w0iaG{(k=5ck?zzcqlQnD0rUK
z5!0T0(n5xB*I#Z2iC#Bzb8{6xwaH6^IullgXifCs(3^gG0>{6%C)EtynomD{<RD}w
z#oOz)dGqGx4{zV{zGV>TbYW*_znV3*Vqb09?z<T#UMV|c?!Er{?*03hZ*p^TQZ`3U
zkPv+IvBE~;qUcMm)<dUHbBp@<`>Rjpbd#B?6st7xz=GW|dPxo&Gg#jVd_B*&Ps6U|
zV#bmyH*ZSb;pgYy8IxxtH-G;8;FB*)4%L*DxXiS?apz9X_S^pc{$ENLM6SJ-wz<<g
zdGpO@VQn{W-Evyz`b@mXxX{l=?l?;j<K2Uc9?ow92Je$Yuiw0VtNY&4#zsf1yKaAd
zWYob2Mf_~d4+}c{9{aZhs)Q%j*UP^>ZZcv2sjj)FMd~~!da&p^mqu*gzWu|3^1SWU
sKyY(^+rb^$B3zSCwgko`{}q2a-;9O#h__t&K2ZAeboFyt=akR{016;T7XSbN

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_to_profile@2x.png b/Telegram/Resources/icons/info/info_stories_to_profile@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e9990c2faa715e771ef297cee1833faa1118a65
GIT binary patch
literal 1482
zcmV;*1vUDKP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NHG)Y83R9Fe^SXn4`YZNw5ndh-k
zW|>L(QD#wy+_*R)aw8WfE=(87bSoJWl0u4H5HejLQ=|+@<}ourGCSw_z4iXPz4y0$
z_WAyslYQ~E_FB)g-nHLht@VC#a^KJQ41CYPpPd1D(e9m{or8jc%+1X$EG$e+OeQ8K
zDk>^OdlOG|babq&tXyAT|A+Iqxw(n=D}rfiYFb}k|N8pMiR72!^z>9;U!S@S2nc{?
zZf?%s-=8XFDP(13-QM0ZL%F`bZf|cdDk@4(PmhX<Vn&03VIttSx3}8bT3uaTnMI?h
zs90TH%?zcluP-t(Qbk2YU<@B0pPxT}Qr6($paA#_CRJ6{fq?<)cX4qsG&EF7zNMvQ
zcXv18Zf<T$$@&D=+}uo~X>Dyqe4h|F=7@-hs;VkqUtf;p>x!J592!k=aq-t3$duvX
z;Xw~nU0t0_eT#C|-``KBXMB8ISy@?>4<3?+h6XZ%f`S5%O+XbK9898ld3mw7w--Qu
z)ig3P0?$Yal8F4JV`yk7EG!J1hK7dB%uE&;TRSc;&ehfR`T2Q!d%LTv>+$gs7<F~^
z#Kc4!8yjSV<>lp`o}Q<tCngse<nHcHO-&6-2L}hFd!|*Qtf!}kiNSXur=d=<aDIM%
zJ3BjM{>bme#l_zr&*kMMQx6!xEP7{WC&84Kma<F|UsF<22nV}|1#)q5xxc?>jw&nz
z3<}dpOiT=+?CtHz%k!@f6aP!@=;$Dv+}vE24JDnJW@l%Ul9FO$W1E_qFp)$Fp|P<M
zL&5|AQL?^DNlA%JFjiUGvx0O?e0h0!F$2P?6Ft(dj*bpBB_t$Zui%M~kEcoi03&!1
zsUCv2w>K6pBqW5X6p~L)PKYC8V`C;RIXRi=%gV}_ddPo$JS=5-c{#y@$TA7MDzR*s
zQX?ZHD3KM<&d!)toDuT+@bJLYLvCqlVQKJsb#+CQSW2!{l7^k2Fx6zQ-Q3*Z`<OIZ
zAW_MgdPy=}l-=E3Ntn;|$f5MiW44$Cm<|sQ5hcEud!Cs{$VkWU@9zNPGo(FDEEn|k
z;NXC)JvRwuXJ-==hA}iWq!gB5U|_JZu|bfi&@3xn&CJYLU(3b()}ZeszJC1p@mu>L
z!OPUt6tT|C%xGz8u>g40)YP1uoFtg}`FXnYC^9WA4OZj@WOk|)63{pkcf85e+S-~P
zLkz5~t<Cg>Xpnda9X&O`Oe<up`_a)60pQ-kvhu&uWV*Dp#EZbDCR-kUF`ZZ-0z=K2
zo}MOyq2uOl$^rue2>{zs+P#J1N9%*TyE|`8xJ%$v6W6P&tLRp7LBj{oXe8}=bdLlB
zf!8y?vaPL+(9qBGBjn^fJw55#?eFiSHW3O|3MNUc;o)Hd!&FJa@aS<Hr0pWEr#y%c
zm7Sg4*47pofc&CP3HMKkbzxzlq@+ZWPl<YTLSzhdJam_E(Kx_po(K}n7dKOIDIzs+
zO?S!Y=%{eo02tdy<Ve!fqNAf}Yk{^9?XdLRKZVT8%cENaSx&@?golTtmn|$Tlv#-Q
zuY<;f?1B)bye=YodRpNDy|&C^N=;2g|3$;XQOeKHcXDzP7yt+G{QR6!*4Eaf?GFW<
zGEJ!7D=RB>03zVpfaR#EsVOKZU}Zvd{S=HlIO(yN4rL&tOlD+c93LOkk(dImL{?T-
zbQqM8-rnA^u`zCirWn}>IBRH0u{mftBVpk@+S=Ow_c7!}yTEM_|KVZhqKe_qHMAx8
k<3_Z%Z{qh1{C{WQU*g;$z-B#3YXATM07*qoM6N<$f)M1Wxc~qF

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/info/info_stories_to_profile@3x.png b/Telegram/Resources/icons/info/info_stories_to_profile@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..414fa60a26a9a46289991f3b1c2cf71b03b406ca
GIT binary patch
literal 2200
zcmV;J2xs?+P)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS@0!c(cRA>e5nq{a}TM)<Zwfowg
zSWissRzwU`6csVSM0vI%iXT)g6!C)?7<l0#VxSlZ28w~AC<ZEaVq<sf`{C9(!`gAq
z*>|6VJofo=X3v`W&#bjq%*@&;Dd8_1fp7%E5eP>h9D#5I{<;xJO`J!iN|pNO*RMp0
z68ZDz&z(E>$&)8DGBSSt{FykTi9*hiBS+7kJ=d&RbL-YEI$;0Jo;^EJwiAR~zI^#*
z%a%QV{@mUf%{hPme1aSZ9yTp4ZNY*CA3uK79dM)9uU~KXpk>RJ*REZA{rdIt<;x2e
zENB-<HYa=b?Bm9bd-m*^TQ`LN{rmU(_wR4mu%TG7Vs<a@+_@u?j~_qo*RP*lCV8CF
zrAwbWbxIsEh?_TWE?&I2M~@zj8Z|0YrcBnXS=~IN>Fd|8hWfsJ`>Ir_;-->7e*OCO
zc>{(wJl?x^Z`Q0?b?erR%l68ZEA3D(U%sqVsZv}xnHBEUtJnMY?+sVpym>Qy`gHbm
zALyy6spH3w2f@IcGG&SnG@mLXM~?jQ<A?Tu#}zA96fa)fC*mKqvSrI|-MUpH_wC#F
zPh*1r+qP|MtcMRDKJ@9+CqT1ZyLRPBc=zrdJEX_T4A5dq#flZ5K7Fc3^x?yY><vNM
zNIq7cJbBKYJEwbf_3Bk7Px3tp*kXEmy6zQ+Y4z&W1GbTTxH@&}aKcK*pu2SGl6*IU
zx43K9F6k8n+j{Ueq9Kwi3>q{j$R*vXRV&@A3l}bA$&w|S8v(ahu3TBd>(Qe}2ZMg>
z*fHI)Zr!>CgBuSD$&UE&;>8PgvUs$eWScc>CWsJ6j~;cBitvcdi6M6H-hJuPr5iVH
z96We%&YU^5YuAoQq@=K7=gysb=+Gf@wkubz?Afzt;>3xC3Kh~a22H4~-5^LX1mb?=
zl+=i=TetRr7(RSB!>Q$k-nMNU#~{D$+qVl-6DXqY22L2Qcdyf@PrFIR#Ahw(6Z!V-
z+a78Q7cSI}+G&E?SFc{#`I<vs=VpQzt0fsDH_7OHLU@g}WXTdYogqVpXvxgbKsj0=
zNz<?sCQNV?i-|dT@??$GzI}T~HNxMwZ{KQ3mKJ;0m@#8kuU<_sshcdC<Y3jRRsa6_
zA*=<6@g3voT)A@T7rtoGB1dOpVs6^BNlXGtzU-(rZQ3*ugP1X6h88PQq=+$#c!a5C
zX3m_c5vER^s#}F7)3d|~#G5v4D*U(!9ULA$eArF@?Af#8)|M?>>|)unWuw$#$w2UE
zm(d9efOY|kGho1g`Sa&DZrs>SASS-fCO2-}=%~X6qsusM)0MZ)l;h|2+EJ9z0WkA1
zfWzQf9mtzEuRaIKP#inP!Nu&CCeALQHNoVe-UtUm^M&S26B<AYti;*E@j<wD3}_Aa
z=+PtnC^Y67X>xmuQ=mWrX)*}5dxN7N^@hP4%Ve)Wc8&UYW7;qWntl|TbMwK22l7Z@
zDRf*!yLRnF-feb>KrDo7#~3!P?@`BOmlMgcW5@bf9u&q$j2Iyx2M!z%I(ULfC94^N
zz{1UjUiV14ZPsq;@Hrv0tsrv~yHy7M%a<==Z{50eb{XO54B+$U&n;TCkfx{vClV@R
zh$3uMmCOnddW*gN`}cdmHE-Vh+qZ8b%z})uj!Q}O$&)9d$ck;!q=|>2QKLrf+_{qj
z-R;=$(2X?h*s()QvY=(_i%2Mn^hAUq?%usyp+W`K?m(6-S(1v7*768P(H}f`un2pE
z%MO+dmV_`CT*HP94FUZ~JVA;jh9M~5bGM{rG)=01`SN8gkGl7JIC0{H$U|$i_~<rm
z+6WLt)H^$pMLl~_;k}X7+pW^!>({Sqa2|~S($GRhSD?8WH3Z^#%$YN%j;aV9qbgHr
zH10B)!!>Kx)QVi+a(AJ770bMxG-;An>EFM<#*CZhMoYuRQcENSH2Ow%Oo_p=&3x9$
zE8yg>vD~U0KYm<1AucOgv}oL&(F)WL1rB2K=FPg*jV6;f)(>yKQ>RXxct+;N=25O(
zIX4{PGfVY6q`n|cJ_)u2;#%n&=nWb)hy{zA%QI)rh#~~K{4|I3M2CnTu<SHiF_=mj
zcR3oAyG9QWbQ7@)d5YH6^pPV+xV+Z#-t^L?OD%QIG4F%N8z<5ViZS-Rd-sksds1va
zq@KDz7R?E%lO0ctNFx`egek%F%0lhfv7>g5byBNVt%yNSiqMR*p)4&fzPP7M(#4mj
zW4{}TxOLw+d<z#Y9E+kQ!xb)}EkB*S#;%?~Ne(DJRM34Rdh{`!66m6DXkvC9hSN#T
z7QU164fq!+k`C&Z22IdFEj~aO*&Buq9m+{Xq)GxBAaWEaKA1B4#>Oyg*f1Z7v)xj)
zYSlPS1fMP5E|Vw@Bg$Qg*cO7Tel}0vR`cb{M<!xS^@|rT`Udo=IB?*=PoF+XuY}mU
zcQ4;F#Jj0fty-0cQ9y*?^AVr^Wk!v-RwuvW4Fr>usFG_0_EN{XuVL5vI#VM+Q(*V^
zosR+T=aFC+pNt65@Q-Ytnj|+`L>t_4IHr<}g4QBA=-$1%ABTd~saLNaXPWlRgXWVs
zyLD>uR%pW23>B+*XR;kINKWU@optK&);I3v9jy}Pcj(Z;@oPp$`D8Q6j_1vrN6_px
zKi95Zn;f5h*W3d)Mrz3JQlmx<l5VO|+ypSes2TE&2v>W7n+pNM5eP>h9D#5I!V&m?
ajlh5YJ1iXjyv^PK0000<MNUMnLSTZ$)h1K`

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/archive_open.png b/Telegram/Resources/icons/menu/archive_open.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e3090130c73ec24625663cd4d2ecfd3c8bb3dc7
GIT binary patch
literal 473
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf%-Pe$F~maf
zZLnjXtAl{4k&22^l429{4UaW3jeD1_j@a2Oy@T0&!?r@^FeAavR*vA$(L8I9`K!%5
zd;ZI!($KwCf8*Q@&-}0Y#*}Y;;PW1hu8Mj0jlZ{N&f27-eR|S?h))kK)@g8^-t=yE
zf9PeC&o;~TubJe=h95TQyYPGSy4M$&tQ&K?rlmH{NZtNn9@E-=liwUqUYdE!gtu$A
znxpsq@3Ge>UQ$b(_uPP|_Wp9)cuqOV23AJ~-49PpT0Z$qKmSp5Po(wXeM*tjTmzSU
zdTh}otL5o5hhxh2Tx|}ftIRH4ixkQv*wmRcI^0DU>J-TPv9NCVFqzv+jkEs$<A9QS
z2bVM6lU!n3+gQqK*1vz*-nMJq>Q!BjD<s&q%vWht^%Oc@m~;DB;=0#N;ysTpvl=kS
rNBVVnIEG&R?a1i<yF+$o+}A&xX9{LYFPikW1Qe{Eu6{1-oD!M<c}%Tf

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/archive_open@2x.png b/Telegram/Resources/icons/menu/archive_open@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..c95bd9f6628fd34aafbd0815ac240af8b843481b
GIT binary patch
literal 654
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2GuF_QTV~F(iWX
zZTQ}GjE)jp?*wf>+2-n`8m;MJ(>l-TC8x=6Hm}y8oj1h;yi{BkEfSlX7OvvedTOtF
z<jqI3Bdu@jyfgdahre^*zxkPVEJtgpofqTMhM)&~xiSLZ7VnH%7|_wR@5T4p{{<Eu
zO(NY#k0yQ8{kb_t&wJ@4f#pAI^k$ynm75dcRFcw`DAVu$gty)Kdg;w37n`|$%kRHu
zK4o%t#?7l)TN~;(-}G5xF1{pgJ$vkjW|bwKwR_{3`<DfN^jM}cS%~T5f?z9O=SdO*
z5x2g+<*F5IdMK2={r2?JtuB*|Hm+dSkn#99<MPWd+w@iI3%A?0$J@>S-`ZTbHL8%`
z?O)iTyqNXZ6>h&R+kRWO{p)7E)0<?DJC-iKcq7M5hbPuqg*j(Os*0}T<ae7`7S3<}
zW#MF~oIKxX_Svx2q1VoTeN(pk^UpK9u9ou3EM~v|#$EraVe!a*uHRIzr;2{{H7OO>
zigx~SR6gu-!1I;kheGAxmJ@a<K1&6ZISVyfI~00OhHLDJe6+Vyv+|@MQ=6{KL!0x5
zdCQDuc8GO6Wfa)J(C^j%r&zYvt@+@Bt67XmHgfiV9DJNJF2DZD(7|zFX2l%k$tTaG
z8B1QTIlyGI&R)OI==Qeeo2rL+0vPt3Z>vA4{^f(5ke-{6kh$TP4@>>`c%^EwPGR~V
a-zcA%bLpJt{Vf|nX~EOg&t;ucLK6Th)C(E_

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/archive_open@3x.png b/Telegram/Resources/icons/menu/archive_open@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..74687f9ec2864f74e9f4da3b3afd809b2279f2c5
GIT binary patch
literal 952
zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|Wxo#^NA%Cx&(BWL^R}E~ycoX}-P;
zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz1|Sip>6gA`6MbbAj}uH@<B7?Q#I
zHfnE{k)ud&ppc=&2Ei?x1a*CQ!Y0}@-TEyNm8Bdp<5>6x4wgxW{8oQaixp8mqR=#T
zfx-{wF2}RwiHB1+r<L73dqj3$%<A2nzdxT-oL+W4k&o|%JJ$rJB@CJlFzSyXtIn*N
z<c$$Z6D_Q*t?lftmHf+X|Ni~^^y$;HwnnWC>AAisYVG#z+vT19y?ggA$85G^-p!ma
zkD&PrigwQ7GJF=oVyiZ}^YFt7P1bYIl~{#(ZkZpweEIUa{qd`|#^^m1Qd_?7((~uf
zZ>&q&xI+Jj`QG*G*9%6d2qjLnO1b-?!lvT&$B%-!mwThuwn^-X)4#>l?3uIq=7QG`
z6)*GFx){&sSa`SNYEq(}>leYZ2||neI<Kwrx2a#B{I^cNy0Emgb=%7}nI4%XOcI}0
zGVu3Y%y?3?b4f$qc5AtQ(SyJDKl#wacHw1-RD3Iw)T{X+S&I(8ULB&v>A|UxS;u0$
zT*>HMiczbR<1Ukr-GO$T3&Vfb$O%fBJo;#1#+%a|Stqkhob7Yr|6~<4b%i7~bBFTE
zS2OZX`tTX-lHbI7`(=sS;){zHFJ5cCoA1E<KR{34%-NRRuu5J&>A<R+IlpS`u14K5
zuih({<5}?VSbFcldxy_QGUj_M+SI#Hx93bV|Hi7ld`YJ-Ntq~~{QP<I!3XOTYnCa!
zIn#Rh;px+-|L68LMjUwi@ZrPNS!r<}XLh9-C0tQU;z?|gUDn5WTZ+x#m4vfIV*ASE
zL)+RId#sKLxZOy6+qd?5woCq_PL9e<OO+$b47POgwmbJUUY5&ve{;fgIUY8dRrBY}
z(Gla$Jip~`-tD(#e4eGt8Wsga9b!oMpwoQLB>LUcB1!Y+{JgwxXLwIJ2C^_DY-6Z1
zUa+^E-*I8Uii;Ts9Az~R-oAbN?_XP)zm1c{?k{;_&hnZoEx~9e4+~RJSVXM2xcJ(z
z?&ygjTCKBYW-##NO)(HuiCr6(wz28b{zG1i*o%dhS=f1ha~1-cuhA>?KIzStV<Mdo
zEG2o^?&g`Vvub5t?;R2NVN+#o?cDRv_Y?)m1vQ8$uzD~;sK&XXKiH>T{w|&Tq2U22
O*Lb@6xvX<aXaWEdZId(r

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/channel.png b/Telegram/Resources/icons/menu/channel.png
index df2823788ff692575706e313d6689af80d8ffe11..d6caf71311d55353eeaf1d87e462c0c0b9831f65 100644
GIT binary patch
delta 494
zcmV<K0TKS<1my&ffq&0QL_t(I5#>`oj{#8>WoBg&Ux_HF6cULg643~qk47O9i9~d$
zM5ofBP^<g}ogn@KjY6Z+_>MxvM}&~zWQR9nk<BP~U-9P5x#ym_^X`2D!Jl0;-#?Sf
z<>7Exuh;i_z4Q6ZH~p%2I-TS3csiYKHk(qZ^rlNXojx26g?~bUrfG-6vE6PZ5()A8
zAcz4voz7yhh(w|vd?Jx>xm>|uu+eBB)bIDDQmIO%+U<6X^3Euq&u223#bR+h9=S#`
znQStd*lf1JV8G>{W1r85YEkoJxLU1dvso^$)oO9l*a=Ay<n?;%^?Iw-A`CCU8YvWt
z)oR5nnJZ?ExPR;$m^GTu=NgUX4GSiPHLBHWe!o8wiJ(WH_lu$^p-{-<8}WD?Ba3!C
zuGws6x7&%=W7kON3e0Bn>2$&<e0?trAxu3UPqA3s@At`MlF|Kc1YNV)#AGrFXeEpc
z1_NG0p%B_yE|--`Wgrm1(2B)kM)rJ<#bTq;2<L(@Zf6sWOePZ}g=4$j#<f7zxm@mf
zK4Ed#%jGhaN`X<U)i5#|jU)<SwOVlkT;?T?Mx&^_+wBgA!%nAz>;6)D3%qP9s&KpA
kxP6sMWxZbC^#AdB0W{3S+?bKzHUIzs07*qoM6N<$g5jg~CIA2c

delta 491
zcmV<H0Tlk_1mXmcfq%?NL_t(I5#>`mj{#v6wPs}zuS6773W>xLiD<-hLZc9gL?Sv=
zqEqQms8#-gP7r^AMxjw@ydnw_j|d@QPxj-R@yhNfcfVqsIrrS-&b{9iiT><ignpS)
zsSJn1Mx(LU>zz&~q3LJ6+wC5Y$CJrqy<V3}r59b&>GXcTFMkvY48yowuFYm6lgX&p
zJ4FN-3<jIcCXq;P_(USXvTQIIY&04O_4|FfT&~e*w%aXHUK!={`AjA=pU)461K&uY
zP~eLk4#!|H;Pa2M&*ww6sQErztyZ(yET7lw_2@L7loV6s^?K{|daKo<j3B@nsZ^@v
zaw#a8FXoK+?0*ZGGn&n2I-TwX3zEVa)oL}r-yexYFr$y_BNlVAU;R@&9$&3i7{~pY
zPNz<%le*kbjfT#^YPBAZM_h%^?}MFysmJ3f7K^*xE}2Xc-OonQHJeS`Op<_6!pLMY
z;WZQrVXWnHS*=zF0s&lFsZ>g2kDswvY&05?zF?bRWMr{eh!l?Pb{lJfs&l#A<9y1Z
z*^9*@l}drpYPB#jo6R%|V7J@R0Y38-N25_x-tBgW!(pe>!MZ<{UIK3yaQllY&|_?0
hrBcBtZ^QrT{R;-)#LYJI=Nte4002ovPDHLkV1j<Y^C18L

diff --git a/Telegram/Resources/icons/menu/channel@2x.png b/Telegram/Resources/icons/menu/channel@2x.png
index 68e65eadeaa29ba64f182f566bf3875115f60891..c68e6a6b0826f3ec261dfa9eabe9c8219c822d1b 100644
GIT binary patch
delta 984
zcmV;}11J2-2+IhNfq%70L_t(o3GG-*D0NX3#=Yc`S6uSSqZmkeCh87Q9(jZjd5kDg
z2I$@aB?AKk32_GuWM)Jr6p18Vc?~E<Qj`HMMatX%)vDcb?9MsY-S;1zJJ_wg*4p2<
z&N^qWz4rC+(0_Uc^bF`35T1dYoSeD2xxKx;t*xz|o*q9xKY!sAS^<50e8$GczJLDg
zc6(S@m{tgZe(~|~J3BiR%=Pv4`1ttJ(h^N)W@ZEuX@aV(th~R!C;IvM`QYGSS~fK`
z1?9)bhp(@%CLq_H3<kr{(9qY{7l_~9-nzQFq?m<;g%o94TAEbvE;%|ndUJD=C@(KB
z^Yim%OxfAlWPgPqGttz@)iNars;jG$lam7i0+>=ksHv%mh=_p0^73+XbF;(YP;hq6
z<mu_z+1dI0{7es|eZ=?C*Vl*TT)w|&XnK0OrKN>Hudc3cZ*RE-Oo^S0ZG~xZaq;^4
zI@600s;a6eC2MPINl8gsKoLDH@$&Lg@c(0muuTAZa({ACUS9tC`l>)6+{AgB$jHdl
z)KoByj*kB4Xh6ydC@Lz7p8M$NNO%HN0XYGove7b>HP!XP9XJ6A=Wq&84@fOtoD99Z
zywIwkpdhv7;_aM(P?tYHKS7n3mnWW34UrSj<KrXMeyi0Q8yl;JAlS}7?QyIg9v+5-
zgzWF{_kZ^GZftC*o@H+Dlv`U{QHQi2^2~E!U_jwGQ$5R+5}uivd2n#Rn6wKkD=Q|G
zi3_b75dW1C7T#J`R+i0XYj1BymxJ-?=}AU|k8F5&7|Q<s{?pUbwzf7jspvqkw!Xd|
z_a2$QfGOG!YLI2!F+r!Kq)_Gx3JRdXu`@V0NPqtC@9!3iMb@dQW=K=S=!(F4e0==d
zJ0T%~0xBsfVb-#;vb(!G(l0J9hK7bR6Ou>z#Kc6VbXsU=Xdsl|l?hEZnJOwOq_}Z$
zaogM5WV*PxFq_S=qA_1xU4_!sUK@9)hld9Mp|6#)LdM5SthKeZGLyHr_sGZySuviU
zpMN6`ns#@0%bcB_PE1Sy3pG=A{lE>7IO^)^xS$C1=H`ZcFyix?oSams3tSM65+#5@
zJ32a~W`BQw(l<3VN%c}WFfb516pw@&o12>}HMu0CUV|HH&&bGNK<p-3R8+*2t_b|w
z6BjW(Jsp3k7>!0W+6xN{XJ=>O;o*&qjZtU{aE-^V!3*UsbfdDgv=prLj@XELgBs{Y
zR%Pgzm>874-Q8VmW|ZdH+1cN<Kp8=|>KV{8pl9Gm2L1tr#HtUi+<6WF0000<MNUMn
GLSTZJI`Ieq

delta 983
zcmV;|11S8<2+9bMfq%3~L_t(o3GG-*D11>E_ZafXD~!DID6^3AY@#es9*q%JgjtOe
zWr0{wvaqm_5E~Y<vmzUcMAAfF3rdj`W#KhN%G>`}U!AV&bnhK==D)hLIDO~)zVrKi
z_q+Fe=R4!&rT_E{=o!#6AUp$kd3kelb9;MxTU%RQU0s2Jfq%j&v;zA3`;U!{egFJz
zZ*NCNMrwr+=$Dj~w6n8A!CYTokB^TpEiKV>W@bhpktQgs)%x)8K=kwT^I>6Ov}|f>
z3d)a<kAQ#xO+fBD`TF_}4Gn#LeS!Gx?X9DuLyB2kTuf1BWMoM7o|5C@;x;!oiSqLD
zvY?<q#*~|zOMg}fG80V=TrE?Apt`y`EiElLIG8CFgqoTfqtOV5<>h6Y&31o(ui)&O
z$=lnzy}kYU`I#O_`-tzOudffwxqN@m(Dd}Q-EJq)tE;Qq+gmOHQ(`A$TVYySTDrcz
z&h%o0s;VkV$=cdlYHF$$P()8le0+Qq{QsCCY!iT<oPV5HESA^TR|Nv$CeG8u#Kfeh
zr-Ny9bo4(*15!>vv9YoA+($=8!V{nh$O#CQjh3OTsje69zzIk=hf9EZKx*mYWa#DP
zg;s@zhN>+WZ|4Mry8QY1399`3eDQ>8h@5~PA0Mgq_w@86BqXRI2)6T2dmO8Whlk<e
z;rsjhy??#E8yg#{XPL(vuIA=u)FJJMJo6kF7*IIQRL?S{glA`G9~>MoChfw?%1U%}
zG#6SmApR>OEWEXxoSezY$+osObUCf9txr!+G8%kj!^6W+_V@Rno}RX}w4g~v2ZFWr
z_4T;-$ovIN(SA^aEbER5+GH|O=8B4npuw>-IDa@u{_pSa-QC@?PE9pKnj%J51lHr@
z<KN!N$;lK@Sy>sgR#a5n-QAIXad9ysB7&KaJkqD6q%ft+LPJ9Xq5Q5)Xu8Q%Sy?H?
zO-xL5I2>fUxVR`REQA$}`ReK_l<xN0*wRj?6F}%|rL2(g@e*roZLQ4Y=jS&vGD232
z=YQwth=Zn`ot-jgm!}gG6Tm{vlwCh?10;^Rx;ic>0=>DpAs>wRys%3Y>H-&pqeKZH
zQ0zFVIVdQI^i54oQoU3T2?@at#Ur7{n$2dZ$t@Z68r(>GR#p}RVmHx}k`ktLN8sO{
zxQLmVnfOb^U@)N3URYQ-J3EVtifU|ZL{L+JYdm%hUMP2=2bJaJ<zS_E#75K`)Ibli
zDnrM|$D{P^?(SkUqcqRX&i<|i$_Tnu&w!o*Jp(^7@DE|?stne@DaHT*002ovPDHLk
FV1niZ^aKC^

diff --git a/Telegram/Resources/icons/menu/channel@3x.png b/Telegram/Resources/icons/menu/channel@3x.png
index e3f8e50d3672e3d15187295bcbf0387b2e44f570..0ce3d46a9c4d71259a2a33eab7a1bd104b62778c 100644
GIT binary patch
delta 1539
zcmV+e2K@Q)4Dt++fq&ddL_t(&1?^f{EL~j`ZmGG58qyLqB!;F5;Zf+5A?Alhh$%vZ
z7Y~Azm_iysG{ln_qM8QLqzP?Pga;uWgz{i0O{t+`NX!js@&8}4)0K12z4x4Z?vEUQ
zju&gM{hhVHwf5d?PxsCp<xh!#5&<OwN(7V$C=pO1a8nVudw)|6Np8o)#KgzP=h34_
zhlht>zI-`8K9+1kZXevgf4`=tW`BSGpTB><fB$~__N{}1gWRY};N$P_|NZ;-zaFXl
zhu*`(LxL^&K(n*6&(6+NyiqOe?(W*#+slWrI1>!N&dyG*7e_}&)z#I{pFb}sDA?H8
zpsI$3261yTL4Q1b`gCDofv7{6nwoNVcPDy|j*jHp>FMdcd-teHzCvnh>aSnFxL!3i
zH6bPP7{7S&f-v6R-n?3Q6-`Y|TU%R+E`*bllf=YCjVxnhV+k1%5up(y3nlW~%*+gN
zg)loiE6^2DQBeejEDImhNyNp)Vc8|35IQ<K%+1Z!k$<F->X@`yXlrYWG!-5m?&|7_
zw0rliJ{uQ&h5ZCEMt;x9$-$T;jYdJgwltbjQ5qT=78Moo!psf1e)7`NlCQ6?N=sS_
zQhQ8`8CA8VrDcDAzkbvOzIylWot2f9YExQEQXLZ?iUtM-U0q#aVPV`hfBg7?mFME(
zf~ypjjem`eLqkIuY+zsjGfngpy#eBmVFE-(Mp6UK&CMkxCD_`KMt1?za%X3U;U_02
zb^lNgq=;kK>6Dh1l3gq^=shaRSuhYLhXjaB$S`l-ytyr2fv|{UG$jD7s;Ux>f~;yq
z9DDruF{6vUHd3>!s0))3ag4?xJNyaTl2xsUV}Hc;^74}8TOsJ`*sXAeOHy%+8;#o_
zD?N4_dWp+Xq{sgJ`NPl_78cyk<S&ajwzajze85Rw_=GDrS4A9~pPy%panOAB?3vuy
z3-A$fthcw9K5lGmG&3`!qRa&m$0jBw#>dB*4;YQz-QBdwlDT6d=P|I)&CMMf8#6L8
zf`1$n6EinAS6^QbhrfRPD)j`-4GIqCl0tJWmpPf4nF3cA`c?h>`P0S4MR(t%-g)R0
zww9I_oSkt`raKQ&B(CG9r>8+z6nX6c+$G`=E&R+QwfAsnz>|tDj|@_KO!AE<uHtaw
zeE9Go0*Qx4%r{!ZHHOyB%`GV@iB-LS|9_sXA1Ns*+;KQMI+~D>u(`RZ(E?LOM@RSd
z^^yGX<3|Coo>j%g#ndxtX=%b%f`WnwoSdA@zz-fg`0(Ka=^<=yZwChl3z(AHV>-<-
zUVE=!zovYck&(fx<rUGZq`|!+llS)akYEc73&{@NmvPfrUS2LWTaxjs5k7tTM1S%2
z^70Ze>gVT2$gHd^9uobD^hf{*gM)(_A7e3|2^JC(!n31Z)ThT_rLC=vY^A5CldLXq
zXCU<$!7#hv`S9>CX+Rp;GB7ZZG)&+Q)b;gsUhS0?v0md&mxT};Wr4Kj>FG(4&dbZw
z$N<ByqM`!Bl8^`oo-oNQW-{_8tAEAqx<(^crmU;0BOZ8e)Mgip1i>(=w85Z9Us15w
zCu?hK$b`J->G=3~0V7vX_2kJDVoZrx-L8|96T!;L%G3c|4Zs+jgjW}OdV09{>yoiQ
zW^JFJpL5^xxa}bm5Wkl%UvjH7${2otsc!=?P+z@zg=cGxUtO^hxt>UK-+wN+%~)Go
z6Q;JdmRlttJ3BjL>Br_96ZqS=Z}`1YKtO;1{)(&c7K7hzG2XZXXWJ288+LYf<XB~8
zC9n4Sih8DdMhL%NpiaP6>+|Q&*xK^AVj(6CoySRYeS+k@B996NJ?LUX$n*yX2kq_c
zBO@aaFbFYzaGYby7v>>S|9r~I3eSRki&#wI28}mu0;axvebB}XKZaUeUDf6i^g}~K
zn2_2&m$lx;#wI^Me|dSC=N>i+c(cCwt?hrEW4vI)0W92DyKudMNwv7Rh^rAEMky*0
pP$Hm2K#7170VM)T1paRk_!puwHq*#dT>Jn4002ovPDHLkV1kvT^|b&1

delta 1538
zcmV+d2L1W+4Dk$*fq&acL_t(&1?^f{EL~j`uA$~3#!xXPrbviB3Z7_Veh|^r5Fx^g
z2SE^1hzLT6r>2M!L5PPI(Mmlu#6uIqL(MS{p-q$`t?~cA{3l&G_uPBWx##}$KmHsq
z)?WKNYkzC)z1N=Zqet?e90553as=cE$PthuAV=U&ML_3IHGd?!9YaGye}8{RN5_+s
zlbM;B)6-MYCZzU(nVDH#UETNZ-~amkcX@f)-Q8_(Z!a~fBKSOg`gCn=?YBn?|DpHv
z^b}!BI?(Lw>|eirDR`q;`1b9aot>R@2n#d8@ayU6VSDl8$B)|D+W7eRf`WqG-Ce3`
zZf+JfClQ37pMT%t;v!LpFg-o(;o(8_o;-O%zWx09Q(s@7s-!EVrly{spR>Js_39N;
zB8PEdVIg6Be0(^y(kdDm8MU>w5nTvpXJ;>8zEsIFE-sFck&%%qF_KUszs=3f5myLb
zzI@^7is<NQ0z;OB56UDG5)!cN5>W`9ot-8oCdx?Ch<|lV+$^-Qu|b-Oh=_1=b3@wI
z(a~n(g0HZjAjZh=IXO8PlcZ58XxElXQwmBwJ-y=MVosRZA=ge`Sy>4P2vBHAO+jpr
zsWGFdHa9na|Ngyp)Oo%d7#OgyuuyDDZAq+S!b4G4SGTvf_xbbZ>^3(yH?i{E+}yC0
zg0g{u!GE)7&lngYg_$P!iPivN$1nk+qN1pQ*RNlfmX=~`LmJHmOv|rdzcP3%!J2=l
z1yaB<>~zY?%E&Gj8T1|%r7Y<3lS2YTCS)*OU0n~wD-afNjHU#jH8nN-QIJ%vfMYH$
zE(~4lwUL@7MV+6FfMYZcnZqByElJf1I7VD=Z+~w|eh`ADjy(ux*d%4g*wJ_hvfN`2
zp_kYk1$yl2>WV>|nwqjdlfEqA*ulX8;{#6e{3l$gxhmk;!omW>7zfSJ&`_zd=iwvZ
z*xR>n>Eo7`7Gq;$DoR`saP0Hv&r?%Vj1L%%eSLkj$&$EZ0_QQX&&|!9oSc08_%Y<z
z*nimh`T3@%COEvbv?TTf%?^s#%NenFjpCk}naOi?p<NZOtX*AQHERAJkk~s9fuYDW
z+}hfTvor3=H0L3RbaHZ<ot*_;LFByyaF>WfH2*V?*xtjT0Z%HLJTi#wG0``kxCL2V
zU3GA9Kp-zJF7T*Bi@3_rA{@!d$xPMY;C~?QwjigZq_D@~#Kgpl7cchr_f=Y8lrb?e
z@7}#5d3bo32drgPNl6LyOj=qRzm<@X5CW5j*4EZTLqnv8aCmqa78b^1N^Fm5G{-pY
zy?OJ7@?l0s2B(%&#Pcy}aIeV7M@L6Uuth~hWC!odA3l7jsHng-CgJ}BVPs^4;(zV!
z?ajj=FffpiSy@>eB>Ho2Z;t>F#>U1}KE@^`B@rw<Je*@kxu|7$+S}X7R(g6m$;twI
z22zg^46_TKkB^U&2BeWKgM))f!wBqwy0f#xslBrz)@$79G9ko9nJ2Azd3jN!^YZdk
zGQjYwtgOVaBqYLtCrmPnnT-6&)PLf3U8RvbQ#Ld-5Dz>zs<Vr%NH9z)buj4BR}?Jf
zlkM$oWJ1pKbYfy6kC8j5^7Zv4#*~Pa?P4J&Sb2H5GJvfC7=x4W%0hpCKO28vGWN$z
z+t=6E?6*92d&mUDue!RLU8PdS@B>VF8-Rh@*w~0?Yn5N!u@bqSNVDHA*niDfT3QmO
zzP_Ga#UndAJ7cSj%{L}6_5=96QBY725B`p;unWO&w;0~I1824)oHlH2ZOO5!swz(H
z{S~!L_Y5KYdVx9tub!i$qxhu>hbtCh($IOFH1{Wn-YasbV9<lEySqE1KR!P0=;-+L
z=@SGDLX00A=a}US^AM?jV|!zRV?nw_ET(XS#+y0;Q(wA1sAGm7Lv3wssdEYXj~_o`
zLaO^*(t0Z^tNi@@_4Rd*d)O%8&HB%8ZU5~Y;{+QHVByBvh3gGWs^#TnT#axra#4<e
o90553as=cE$Pthu@PCWI-%pV?&^=Ag0RR9107*qoM6N<$f+h9<cmMzZ

diff --git a/Telegram/Resources/icons/menu/groups.png b/Telegram/Resources/icons/menu/groups.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fc454a3642438a1bdadc85d721c9b133ce0bb03
GIT binary patch
literal 685
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfl1uc#WBP}
z@NLNH?!rQeV<)`wq)mgwt~}W^X>RL{%uOkdSJtE{-qf5P5jo+|ss9OYue}LSvEF{B
zL*u$rigU_5eJj4j%g%rMUDtj2v)%ird-gxCJ-;yE-+XqF4G}s^gEIHVUC-L;pl~3;
zKu*Jnv;FYutEZk8Wtz;|zJ2@p_3M*1{`m6cOZnCit*d3bL$#*5NX<U`?)z`Q<)4oL
zwQs)procjki&e0geaGE*IcC+5KUD3V>BF`pLPw5=?QWj9?YlDTMZYxXx%oXV+Ijlv
zq%#c74+|_9Hb(H=@>CJp8kK7x;k0mqhsrKayV-7w3vJ{U*Ke|(e){O+LW2u_tFH>_
zok%yD$?=)%!pkpSOFw=5s3^kqFu`c%3np1k7N!|z(=O+2pD$Elb1!YPV|Mt@l*d1R
zT2A$n?LU4oW6S;b6MH!4Fy)v@PqLNkznZl+Y;~(squcJFwwUKVg+H%f58oM6XIybd
zb^iJ5ufJxPR4qO*SF1lTM9Eie^4|UX_wU(rr=*c#O_0${pXHYi1o&!AW!iH4t&39Q
zJ3rBL+TDc<(tCKUKmGjK*(tF6@=J%W2?iAwG7pYw>4<g5t#7yOa8Y_%Bssfu)z{}Y
zGfe)}{XhDzvBXMt+Jy7ZC)?W1Uw<_#R^mjP;X_{WS8}ubmRIea_vYX_Wx@G^CqDlX
zFq!Fd!qNU?fyI%xC;sG^%|8Epg{xZq@$&!bA+J(n_D}zDTzMZOr_8&&WrF&*KuN{Z
L)z4*}Q$iB}3d1PA

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/groups@2x.png b/Telegram/Resources/icons/menu/groups@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..986bc696db49038555c1e4b32a2c7eeee8e3d286
GIT binary patch
literal 1315
zcmV+;1>E|HP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGjY&j7R9Fe^S6L{nQ4~JMJcNh{
znL?2gq9~#$N~An^6fbx~Nhy*C<0JB5C?!f1nNma%N<w6MKx7P=XYS{&+GqRE`3JYt
zE%pBor?uDm*4pc=Y45GksGk}EH3Dh`en$j+eSNE{sumX)FD@<~A0KyjcYAtzGBYzx
zOicdwA22gBtE;Pfet!Nm^sTI{gocJ1(I>66ElW$wp`oF~#6-ZJo}NZVMi8X?`+FZB
zpVZXUfPet-A08euGBU=;$E8gfN7~WRL2;RxnF$OG<c*q|nwFH5AbkK_UtbHPJkn@%
zY-}ufGdMUXRx5FFaqsW%fFXEB+gQxdz`y`@IzK;mb#)cPfL~i%OMV9j2Mb^^F){u9
z{U;|UD8aqGy|%VCKR-VKM(NVZ%8JraUS2M(V`pcF@`6vLrKJpMVPVnG&_KZ>cj@=;
z?k+DckHLOGpeD#fLPCP9+Wh=H!0qkr3<+;Y{qXRxv9YnZxESSldwWaFtgI}CR7ylf
zMv`(^SeUHN*w`4reSLj|M5ZV<NV<cA15?K^TU}iR62r{R&5iLD5nf(i<WO2#nykY5
z`Z~bP&CP^F-C&8>@ZOLpBqZeJ<pt8RvNBn--vDSU6eBcSTU);g^cMd9{(#}j&CMkt
zDI!yP*e?d<cX)W%pj345e|&sU3bM1akphFtU~0A6mX;O}=p#%Af;l=mg23@YOTjPk
z2_q26EImCvF@^EhcXoEXy}k9(@TRh|@^5rLQN;|fzrPPAs+e(jg35>$950fmkp<ch
z4U(Uqk7i~%K|w*&)6*nfUS77fwPhmA8}OlmDrQU~l9Q83rJ$fdAX2(S$Fgy9b8|C0
zJBwp$dwZMg;&*9j$-~2gH;3tmc>@uGfhuMqcyoGs3jWsCmbJAt<Nuh5kB`T$%mYRs
zq1DyZHa0f;77<Q@p{G#A-QC@kDKOvO-lC(U^;L)?bJ{aCluJ}pR7y$;vH%azM+*xJ
z=#;CgD~1sen0`%7O-@cuJRhM&m(0!0@l2jp>JYD^oW}G+6{1PeQ^&{06B84tbeu@a
zEvQJn2#BT(LkP#JySux&xw)Q}0S1Z*2MF$Qs99c&2E=_5J%tM$K1h#_j;5xj4h{}@
zm=uiz2$^QB^dkZzF)1mDw>~*JY0yGvXQ!fJjT<|BXC^S*Fgawuv$HcCHB5`2+C@c0
zmzS5g<Klz|jNL#0MySHW!+Gz8G_Le(YiodF64uw(;|`B*7lK6A_V)I;_T!Ktx9~zT
z2!uf##x*rH$Q_WIo10P-5-%@la|ZX|nHW(xMqmWNkj_YYdOGceHx_R}N5glF3V?ch
zdv)BOQ^*4Cl#`Pqt=-kt1tjk5{0l;N{XkpMEe;PcX+zRd95VQ0z(hnuNFzr_M*(nl
zcE;8WsdESd;%;ngF!8?;WWmM7MOqz4ECJZoSn3>N_51qzDs4zsik|o~E%)pvn5Z=j
zK-42Xg_BU!kVaEcQGwzpEG(43K!m17knn+!MjAn?@KJ#%$_CO1-l!(j2&fTIBk-Fe
Z@CRb6SJ0@!iPr!C002ovPDHLkV1h2nJ~99R

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/groups@3x.png b/Telegram/Resources/icons/menu/groups@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..28ed4085f27c04d10d3ec4668687c8d5e78670bb
GIT binary patch
literal 1946
zcmV;L2W9w)P)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS?1W80eRA>e5T4_jCTNHLE%}TQz
zP;qEb8q^#r(g@K+2&B>!v<MRNhoIt*CPRN%L<2!T#70zbN-Y#Bh#<luGY3>K(Iz!b
zO(;p4v%&Yhw`sBOKKGvCUhVlF=l(dZz4w~-I%}`J);d~Q=|9~9x&?F#=oZi|pj$w<
zfNlZZ0>&+1ZG2kwY<PJ1_U+qaVq(0#y*)iWzkU1m`t|F+zP?+xZr!|jb9i`IHK$pF
z7#kbg*48#<`uFqa&&tZmWy_Y$8kLn4ad2?Ba^;HY9T}|P9cE@`DzTge>mD8+9UUD8
zx=&5Lef###ojVm36<4obefaPpHX|ThzI@rn#%30%Z$bsUtJc<5UTN$H2h7&imZxxZ
zblkglFHRbddFs?D9%pvJWo2cg>!(kjQd3i9^qV_(Zed{|;R8rXNs+<-*Onq9BT388
zpFc-LM5s~a$dMyNg&>J{u7=8T90M1UXIE8K<>%+eaQ5xnhk!1R&sG~78&|Db6%-VN
zp_7x-hYufc2xSQ2)tfhOj*E-ix^*k4+}zwoM@PSZ|1Lvf(Gs>qhKH`2czJnw!EYIP
zcXzib+9gYtoH=u5e0-b)F#z6yYh%TV6{ZL+n$O9}A$dD@?$pYwq@;w%0|El1<akBW
zEfEQT$;rtXxi;X3Ow8A>U+2%CFV#$b2H6Q|xqJ6+0U1H%$B!R`AOoB@aYCTAblA|)
zz~nu9_RI`kcuS=2fddD44q;(oa5h@~`0?YhW5+ga+O%}(QY^t!UcGuns{r=w*~61t
z3O|1Qm~ods`liG<J3Etd!iCn<)I>`U9z0mSe7V46(V|7=<>iF)=FJ-fApyty;k|qJ
z80#lbo|wS}N1rr1bm$Oqz|o@lfq{XA3m4Lo0NlQPn=o^8bA?suP;Hunn9GO}9_P)Q
zCuJl*vvTE1LKqntAvifXnP6NY@Ix93Kze#Qt_n8EQweCAhx_;MCxdO<woNPj#fuk-
zeD&(pMBdZW!{*gE0tGOTm=VB8r~$zH(%rjv4c3$fxMbhIe{XaH6B83`zP7ejiH+64
zg9jNho{HC+T319y+tSj)WN+EBB`GOMQ4c~mefl)gVm5@;X&wObNj6lITi{9{S|X$w
z^MUB-XfhbwJtQPV4X^C%Y@!+)8xz9A^XJdmDvAb0ZWk_GAXW&Kiuf83u3fuEtl>rR
z@$oXmD8P;%KTh}n($dlde0U&3`1R`-A7BJD3}F#89T@V97cZm;7R`WrS63IWYh7L4
zx^?RWrgnCAiHV7H^)dXSq9OqeLli#5?a`x0LS4t>pFe+|Fp*O7OVb1U`ug_v_L2++
z3=Iv{*Vmsrcdod&7#9wO3PP){t~RPPjtb0AXqPTslGZiW*4BKc`sK?P=rBE-5xC&u
zM+b_ukq6MyK}jZX;D>w~306;^KHafn2fHd+2}9W&6I4)8z>`luoRE+}w~(p5UB7-k
zFfdS2_R*t91r9iJNcIsbh4~vdZv2Vf2`q|GdG_qt&dyGxaL@*-AZNXQ|Nh~_hf!ZD
zZix_(8ROjw+YpAOrKLE%MDphV92lGjv*m5rutD%T+8A5|4x)*(`Fdy)qaNAc-ybO(
z${crh_jm8!4Gs<>8nw5#W0UHc=$N_%W{(9loH18dSM-}Goz=Pr91=uu)OA!=nc}c`
z@nQ%<$er`g4*+5kN;RZZh`pw$G|vYI2P2(9uajbwg#cRr`T6;1wi#6gNfsJXrbM7f
zMQNup|2E41?@45aXqzcsFQ$}`pGU+);xCnrJ5ZhwtGF=H1u-+hM?NNGv5NA{K!AQi
zWp(o8NtJw%vdWa3#?KfIQgd_jnl)?4qW%5-NM}$!$+RU1*(iW>;^N{G8XAh^kT*6G
z-;9h5A^{q5MIRp@cnT^@Jn_R;7c5x7t~fji8TxG8xDhX(W)uLll*w%P*p-);hhwZj
zqk0)PFG>O84`A1>T`H*&NYLa$f=}eAn^e+TtzW+$PRuCL_eG|omQo8IT4<!)%a<?h
z?d{c5uU)&AJ_djZK4Pe%<-3#UpQxhIjs%zEGefPP&a$$y$SyTCHOlhga`bTkUq)uM
zM!?Z0*FYtztQ>~{yU-V7)V0#AU+hxhBR}0Ra9;@KxJ@&N&KHzpOr`w=d0}{6IH(K<
zp-%e03F5}^zZ%35#Vo-y1ds@%8d^p0EgdPjxG}!pk5{Z!m{xoefoQJP?fHub<br5%
zW5V)(cPMU*&ro@IhIWXwka1@14NX!I@d=-YY0?0|4ahoBcmg(9n}I=8R22Jf7u+#g
z*&`CN#1c<wR5Z>_EuL{|ZL|0;h3}UL-`dE2W22b*4eEb&3+NWmEudRKw}5T|-2%D=
gbPMPf_+MJ!FNcR(B(lCtRsaA107*qoM6N<$f*@6QT>t<8

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/night_mode.png b/Telegram/Resources/icons/menu/night_mode.png
new file mode 100644
index 0000000000000000000000000000000000000000..531195e736bb99a2e4729cb1fcde1badb1fd74c2
GIT binary patch
literal 656
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf?3bsDV~B;|
z-4NRo8v`Z2`KL^1RWZDgY9Q&eh{tQ9=Sd|o)l?IW$p`hg4QEcBp8`~)v&7&B@6iYE
z_wIYY_Op%c`_C`t-Z#GceAoKo(sQ-(-yeSb_vTUYUZ15wKWprE-?f{4)@bIL*Iyqc
z)g206dF5%5rCk5fq>lv_6*lwybFR(tQPZA&diULP@4tI54cZ!2yEiV^Oj>)Uum}gs
z+p=mKx!FF;@`cu!N%dOr7pP7;`Q&BEr3{mn3pr+&Uz*(fr#0d9)7x*&CVE`B*?dqz
zhVQcAx(Ej$zvZ9*)`@emHYrH(wTpGP_7xnRB4Bm*Wy!nm)^l%Vm^2++5Vm@0^BP0d
zC6`}{b+a~ytPT79w{C4%a>l~`Mh6ESG2wk3F8#-yRTgby;cs<X8kA`?Gek>t3Xj{@
zs=Z;Wzn1M@xbtStx7nU|Gj_&E^|}?!W0`&S*xNGq#euC(g55`B^uo8_ZgtrkXFmIE
ziyN!Px1Tj_oSGfhOuYO0U6gc0I$btJt$p;-!uuUN%l+iG4`<|O{%6qHeE)st+MAv>
zavU=hXUyyV!0$eL+2xn)x<BMvkK5D*Ir2Gk>)zqcPr1T<w(Y&4^V%?OlQx0h^)2R)
z_=B{jS{Tni|B$Eo;DZ8-A2oG)(@#JDeE#|8a3_U@-z++pUVq){^ss90yRTISR&}Cn
dN9NzJV|3~{nzVCD(M3>d@O1TaS?83{1OV*!5|{u0

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/night_mode@2x.png b/Telegram/Resources/icons/menu/night_mode@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..1742270a361a72d7f56a1bca57a294b582d43239
GIT binary patch
literal 1261
zcmV<J1QPp+P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGS4l)cR9Fe^mq{qKQ5eUsDP$fN
zrUj9(u`q6k44JYptqdD1l`Uo1D5We=iY+NbB14qOkW!LDrYH$XgiN{L&v&}_yS{tx
zJKTG}zRi6X_rC8r|L1@H=Q-y*=Xo?5`(yWj-2?yD15OsxJ2*JR$H!-8W(EWVoS&at
z%wq$??Ck83k&&C58?EkHUS9U_@UQ{3T8fgAlI`tnos1v<ZEbC8*{s6*`}>cMj_M^`
zTwHW@b>-*h`}z4TEG*Ezy1HuB0IG~rQc^B2FNK<egM;eoY8Mw5!Qkxde0q9H7jji`
zS`JQ4O?`NHV92kpFTCRF>T1YUSy?HB78Vv7w*Ra5goFg4=H=xjFE7t{n9a>i1nunX
z7<bveg@bc+bi{bi&(9oh;{jq~VuX9@>+6ksZQn9DI4H&|EiE<4-q_d(;N#=N$H&Jc
zz?O|{WT9qYV89&j(9jSVR?Qs1mhF?1lMHryd+X`xX^wYpZVt@&__#TM1?`R|ZQ|nM
zii?Zs>hA7-dU`U6q4DnSj^Dt*Kyv^K+D!{8FE8h+;$-#p^+}Ktoal{?js`3Nvq9JT
z`Z{YdJv}YWRZvg>j3+rcSsH0oueZ1N=jSKGG&D3w6A>gIA0I&u4-ZQtt?DJI_@1>^
z%R!SR>gebINf1v@PnQK-2*}ON{jEnE9v&`BK?D+c$o2JgL_~xv-V#7nRh1Cr?(VL{
zo*+(a!~{*!ej<^Yn3&kx+w1A+@$&LgLjH}ZsHhNvl*iwini`>rwPQaeB_$c2J5qz9
zqN0tB4M8h@7Z(?m3Q9{$6E?B2u}W<;8vJCKLtb57kw)T=-QC@{w>P~MlHIAPDe*}j
zp@=^+GEx|1Wo0Qc(1GwMdt975y#oEq$;nA=Z7tp+kc!5#vNE0NpMU2+uevwF9dqJH
zGlQk2C0(=pKZ#0HQ&W3;J2T11$OsJ$W$gR=d!)d#va&)nfh939QE(j}AIp;p#LUbL
zli1qYQfi<EN0tXUx1`}dlOZ6#!G;|g78WK0RoTswk&i=8-J-%YC@4srn%>@C6=n*c
z<m0H}$;ru405^d2_xEG={{9{n6=l$D)hqjQe}5lZYAMx3IElmq<Js9+M@Q31fm%|r
zd*GzLQ)~N;Z*6VGn5VO|(-QliHq;1gO4!E6#(wrF{&I41nxCKlElFEfSEty{1dy;v
zb_EqNg__d@LSd7yukYdEq0oe%EL%Gkh`I$50+o^sLPi^bTd#@zq-u|ok5eOv`JhJW
zT_K}Lqrp%5wd33o3uNOH*9cKtf4foNQFF=;qyoTxYH4W^=Z+b(J&EJZ&CT2r+`$Y_
zu0sCAijZfL`-(LZzM;O8C|y*DFoqHfh)*JBioqm)5lt;+gH2<s<F6KEakp^ay}rI0
z3*rV{TU#S!P)q$~sN~bC^2N=~jVnDQBqTUEm?%uf$a8jfMh2o}Y&*v80lNqOiwAxI
XLF@-_ApQ~(00000NkvXXu0mjfv{FDt

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/night_mode@3x.png b/Telegram/Resources/icons/menu/night_mode@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..601217161b8112e614ec4d8a97f0c0b5526032d4
GIT binary patch
literal 1834
zcmV+_2i5qAP)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS>lu1NERA>e5T1iM%Ul`A+94cy2
zj+E)b2vQSKa1jP#CfY(lDYx39h=NiQv0OMO;*disD1oAu3qeJ*l_SoI&_+>2G!2}~
z3Df_N|M~NEyZ3pA_wMuQzxx)C^UnFsH=XZ%(|M;(X&+4kng%otXd2KoplRSQ(}0DA
zg{7tC|3qjrGczwQuNN;~w6wJR`0?Z5-~iu6MMX|dPA65u%a<>wrlx-X{vE;yKNAxZ
zj*gBeOg1++x8maBt*tGgJlvVu+S(H%n3a`PMn=Zo-X2!~Hs0LaY;SKbC@7#a+}_^4
zaN)uUknP5e8^goHJdu`{m$S38@7%d_=FAx;Lt<hg!Cb$7{kRME<jE7B?aj>0Jb(V&
z&d!d<dUJC#6KsEfANP>Adc1xnCnwV(U;rD7kB`^s+2J7xva_@Ec!|N|=FOWo#4844
zV`DdO-sHj1&$_xg0*;7?p!s7rNJ&W{MlmQWD?5Gq^kG~&I5?1B`TF%MLfm1HCZ31N
zxl+A*_fDMGgoFfwkByBrG53d&N9wS&v_y>N=jR^=N%Q05;|zXdW8>`EvowE<1~{27
zU%rr3xH*!{A<rQA4<9~A!kY%*>C>mgCEU84IQ#J7L)!A-!2{FqEl$<i+InGOfpPik
z*RR{RZ;QjTRj%{AdiAQDEk_OW`0-=XJyIl@<V#9Q*e=9nogq#p9%F%lr|Icw#^TP-
zj<d6~OiFLxz9n^>KYw24F15hSB@snON4vVZGO((us=2v2$t?_Eetw=HkG>d_7L3vb
z+^(#wEaFviz!eO2jU@b$0-&~EU0r1s)z#Idh&FP6GDTF-iuM{EI3OT^m_y>Llpd;P
zwtaSXRy;>hidhm~!W4spg9$}jTN{mK4jLO98KjGgOH@>p%w0zTMx_+WM{{!tEeIL%
z_V$LaXNNX9If-&x2xFR=!NEbs*3i(9ejL=))Q~Z!r>E<OMEza38;mXV()7cA?b<a&
zKa%aZ<W|H}Wo4y)f??OISFcDgbQAQG4GIbj4D|5u(3_3l8!ukGNDO6WX8vAKKB}&+
zCR7-}CgtBZcxDt9bXPk%I<8!~qMQZJ4z6HwsxQt~{t#(&cXu~eHuw_cG~zM9&(H7E
zr%&8nY>ezjPI<v+f_@`QB&7BA^)#>F0J8DU4ajka5xYY}Lt$g*&Ygqfy>#gkjwmT9
ziToDvz#S;cfpjp;n(x4<H0{uV>)$EdH@df6M%kF`O}Nz4)AQ)jqi^57v8DC(b@9_a
zjDi2=(Sc%!rlzJOKxHo@Z$u0bk%1=_x<C}%b8~ZP6*omTO#vV*kRd|n5Vj;(*M9=B
zJ0~ZHaxP+*25W0;A3uH!3JT)V`26`Z1Niy#C-PYC5^pTg4?}mFqH1_}cvDjoZ@=L`
zkt=(8dZMmDj|A5Yjeb<?qobq!{rza&aq**PzOu4nV`GEK*4Ea0_wI35m5u%V{Yi!8
z<>ksYs|}6!0AdF3Zw8^~mMx2mi+ZGs29f`BGdv*)221{PQTeZmiV8v}vyG#ii1ZG;
zbx>Cm=4y~jWYi!G35E<nng!!PJbU(x*houDQ-KI{GcYhfNblags{&GmD3B_knaOxS
zwyD}%6!+G`U#VaQ3I%)l@+D~&Z^8<9i-MzUA=GhkaiXv)FXD{~QIt_f08qIvgiwPn
z6&VA<fXeJ2f(~y~l>LCZfklRHBBiCJbZBU;$?u%%Mkj9;?{7qOWMo9-ElwUjT~G-8
zy}i9e2HCf*t*tIV)#eHd3yB<j267bDQ1cMsg_4>DAPL!wcW!xkc_bAEc+2N8qw-H5
zA0PaIgGj<ARljd2BDpWup!NFoYn8>&iSGXW`&`*zB=D1J#Q@|36tbB5@EoO+4Wmrr
z+_*Lg4iMiAjNPzmyit+wWm4fS-zWlzAjN&-`Vz(maxSP`X|9aW^#1*OUS*BUFfuYy
zgaIQi!A~Lv@N^6bQ3s&L2@4AosZAj~dNp_tK+}Q#GQqTiZz#w>w<J+$%Fzm9Hwuc7
zkPsm-4gubUK1d*t=HaOA?d{Pg)LGj@Dadj0M+=B2L}-E;<dfW7nRG9IfOk{835y)E
z38Z2q;TUnjV1y{(<;#Qjw}ys>n3x!K->FqFMWG^5cl-MK;tX%yx`qGoxx2g5U*dLm
zcTpz7-tfT*qdC*p*N0mwL}K8KQ3Mn6gwsHy2J<*f;i@*HX+YC}rU6X@ng%ot{3ROr
Y2S*QX%mi+;(EtDd07*qoM6N<$g3Y>JA^-pY

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/profile.png b/Telegram/Resources/icons/menu/profile.png
index 962419484ce8c255ebcf99618b75c67eaefc4071..99861b419c3ef81f6b8ade583935df847ea3f3aa 100644
GIT binary patch
delta 484
zcmV<A0UQ3e1=<9VfPVqRNkl<ZI1$a1J&FQB5QQBNU}j{ZfhdB9P&UyC1rZ}L*ehrz
zo?#*b17i=MU@%@_Phc#9fw+i&iSD=4L)qiBGb}92R^qFw_sW{;>P{s7n^_SJC6mc~
zKA*{C;H=l{)oOLS-6HsgR5qI(jYiw;_I|&AdIrEHNXj5EhJX6~{`GpD&1Q{8Bb7=i
z4gi-Rsa~&V6b6Fh@p!k})oQgsRCDU}Is|B1EClHVQn6T+OfHvu_Y5FG2+&^k#mCdO
z-b<&`xm+$Eo&j*B7JWfB&x=7a0-@3Z;#RArA^=>yFUUxQ?*mSnLt&hf;DcyITmUXX
zi9kkzP(}P7_J1XvPA?V<;l%>TNDu~?)oPV>(L=GH1{n!LU6lwIA{{YoC8kiNQVA3)
z6bkIFte@oQD}4Ma3mFMQA%BDLumfq6^EB)SML&MavfAzTa=F}WHtZe-a9y|AY?4x%
z`Fw6TesXFd*)%W=#Dl@Ww(U310K?&Mzu!BK10iZ5NnNWe!cY!}L#b3E@?_<5`FK2b
zyIua~<rPsHZsX~6noK57CB&Uhr|0uI=F%#5>@xyE>c#-(#h8Bdu=z(|Ec^>kt+DPO
a1-}4Oj55Nl!(Lbb0000<MNUMnLSTa8tKIJa

delta 578
zcmV-I0=@m(1h)l{fPVrZNkl<ZI1$a1&8ulq7{)g#Qc5YssiP!P3}kjp=nlw4DJ3HV
zb2CHv2TV|uWNKh+BI%TW;0(-63@8IR<s%G^`#Wdf*4>+ZoJ?+a4R-JQyw9`N+V97b
zr2lQ{?p#Q(*Lyr3r_%{%xm?a>vyYFDJA93h%jIgdTC3ISa(}tJe=Q7LKtdV<O{i2V
z9goMsV31CyEf$N+!N3J11VK}%0^9BOdc9605-L>T5P|?rf!&xK1<B|0iHOx|&E;~%
zVlf;J3lIXdZ|}v=b8Y2aLZQ%Nu^`8<udjrv)vDMF8i;njNF*r`RE+6#+GsSW9d<An
z+-|qYWKs|yBYzR@9|JZ;A)O-d{QOL%Y&M%fI-O3vUKcK8B=RwUpQo|t7rWg)91dld
z@A-V@Ycv`KVltUzP(~mlK`02HMl2SiT~v@HO(v6mzi&31!Q=7x>2!)jB1#Stgi1t;
zMx(bIx!vx!x3}GHH=obhh1#h!l7s}I3iNutWJI)|h<_OjhL@L@R4V0gI0PsgkdYu%
z;kU_Fg|(ckWdEfo%dgYvvf1o-JZ4tuJr>4jG|FT$LZDrK!peqhHk*jc<uZ-)>uX_D
zDwWM<Q>)b=-)l(1@`w})1$xi#_lv`Y5eNkK`+Yv2=a_FtL}oLMP`ll(5MLR+Uhi-?
z{4lh#hC8E}BT_&*MI9f0M&!N77rB4vVf*>1eR_IgcljB3_^G|h%RhC012D}sSs29>
QzyJUM07*qoM6N<$f(OeHCIA2c

diff --git a/Telegram/Resources/icons/menu/profile@2x.png b/Telegram/Resources/icons/menu/profile@2x.png
index fe742a4124a1f47348e941648fae58851ca17421..14faac7683f2cafa71ee93f00d76efa4a451c56b 100644
GIT binary patch
delta 1194
zcmV;b1XcT%3z`a$fPVypNkl<ZSPAWzO(<<q6vtg(MHE9H1I3KOz{iNlzyt$D22y5*
zMq;36LWZVB%8-F5A?0JB7$A9~6a!Hv<b@IM{j%QO&Ruu!bI-lJ=k>a0aL)etum4(m
z@3q%n`=q2KpX3buXEP8=cs$wJ*>!bw#l^*ig@pwL1@PS5+<#nOUte8a?e6YAJv}AN
z9G_r*etv&{|Kj2zDt|g2x&QzJ93OjJ=&Y=);o;%?`+H@^-QC^yw}*o20*nZPaVZJ}
z?(XhpEsVbX{r%C=(dz1IrHm?#bOA761Q8FUCI~1sHFa`w(y%WrEmc%h1Q7%V*a$)b
zRS-cxOioUYA%8tPJ8NiY@I%}B8XFrC6tkd$(1r@u#(<19D=RB`d3nL)Gzvk)jZKNq
zlUb?h>FKnzG_5C|I^vLsP0+?88ph&=BG2Gl*5u~qj*X2S931?3dniz)G0YM1g~ZHL
zS65d=U(5=dMfMcS?^>zm=H|=GOEKE}rjahI3aG@NAb(fe(~5s@BzTApmlvb*_4O4D
z@8#v?Z*LC;)9A7ZLSaV5yFULC;Z>p^9&JUkC^9z;luhKtP?f0#;95J`D5wY;#|il|
z2v@wpQDknEHDVc!GEcy~wwf}u!jB@;=vUwvLV!@~0v#S60$_n$0J;F+<1T>2aTG-Z
zbm-&^Z+~fN0k^QQ5RI0W($Z22dwY8>fOM6WmAL>yjv~}4j5esgzFz#_<v>b4v8s@x
z2z81<8&p(OB#6_~Qw25E^Ye4cwY9aTMHQq=V5&>1hTNz#Fl`Xo7JY1i=jUewQUzK2
z*49>+Q`3cr)kTB~a#T<jb&5ZtIyE(gKa$==w0}lcKDh0L3>7FsT_kr(x~c<GEzwoZ
z33lxI`Z^Q7v9ZB{AYRN$F*l)52oeCmqGeS}Y;{9JLx9Nrlw@=@fuqAqBIzaSyp5J}
zZ*MOK@dP<*SUaL0VJXPr>Qx50XhEH~-BNC9Y7!`!sHOXpnjjY~s8f`BmvMuU{nggi
zrhlNHSQT;<p)NLU%%dYVMJ$BR(V=%4RK#^gz^rl#9v>e^lam*-iE!$Yl9K2n{o&!^
z;^KlmI5RV2y)Hs4{@`;uMJV-2KQc2j2L}glZ*Ro|{NG$;2L=Yx)AgAB(+~Sqn(S$O
zd>k{0#oOE4VsA!9#{B$zdwV-oOyMv;Ie$6f?o8PXh#<t}y1cx+va(Vd=jP^mdU{@7
zUZhHbwzaiId1Pc{Vq(Gw#wf5YRSL^h0c&JR*4Eals;c}cYHDgWH#Y^(+F>a8#8X&=
zRgzCyF6^_jvjOihWc6kY9UUE1S65esB*BZU_ZoC`bOaZ>!*v6(P8dEXXV%Wn&VNU*
zK`8KE6lG+cot;+2K8<~SeIhGwJUZY6+c~{gX%Yr&zG0UPw-Oy5A0Lf1AG%5lCj&BT
z2QNc(CI4X?et&_<4i{@6`Ow@bcHB_*ov`CfU-mo3SVk-$KCy6OlX-v54=ZC9ah!95
z^X?D*(z8cuqu!_B{pl&GF;Sa`l_UMDKRta+Om-z_;GdX*UnkH_$0EB(lK=n!07*qo
IM6N<$f>ZD_Z2$lO

delta 1319
zcmV+?1=#wU3YH6yfPV!5Nkl<ZSPAWzTPSU76vwyw{gR}}gS??oN)!?0K@{>Jr6|P{
zFD@m|idTvkMNy<kijpLk7uSbVNXf44l3bHZ6zBZfW3SKj&AH}Y3+J>uSr2P{V~qd!
zk1@Z?_{Ln{zI}bZX5fD|10L^wo`8UW^z`)D*x2ak=$M!ocz=$Mk55ic4i67kS63e%
z9^TFDo@01;cy)F4#Kgqi-QC|#K9~Ri1MD8VTWEiO|CW}PtE($5jf;zmzkYrgXePk8
z;OJH$m%!!a<v)J>Q1RB+*W26MQ&Lj={QQjmh$t}uP+%N!H^M$N_VDoN=;%=FlarH)
ziHWW-0t2iZA%B6&6-PTvP*9K}-Pzg6%F41sTiI~IQPP46LMxPOI!{kemCf|@bXZuJ
zYkoQkN8D9T@1G~NQoX&sB&Wx5N5&lz$q8C_Tpd`pq4@J~HreFo=Z}w%Q+^&FAE`hi
zBO@6Z84iv&zmTYTa(j<+P~}Xq{LWS?I5@bkuTPRTet$ndKX-O^YNt<9gG%-j<nGl1
ziigCo*~PQ7vx3>*-|z13ZftBMKUErrhlk(Hk3)b^ya?gY{qC3YD%Fo2?F?I2S0}tZ
zJw3)}4Gj$)8X6K)Sy`E-yBrikxLU_4`NHU|-uU|ZUSD5>nVXw49~mDXpUurptlz(X
zw@e6H;eRKPB|ukl4dJl1wx$~#a&B&}Fy`dsICO7*)qzM#N_t~(2#~`-Acx}hS3k_$
z;ROW+*lpG;E-nrLj2jypuS|xK(hJgH`85=B0ufHL>Iu<N1O^5Inwpx@ph|ptdSc9C
zIV-yyKmiL<IjVt>6Nqq1DFZzr`WrFq?(S+(C4VYYc6PQ>8VBlZDx}fMSm{+b4e%;9
zH#f5uG!m~WtI^)x9xx8Pd6FU{BTr9Hv97GFSh@bvNv}dU4WrKx?H>qwKMkjewY9Z`
zjE|3>pPx@iNKn?aw6ukVg{Y_~7OTF#Ug^yP2_fN(Ry`r3&1Sr~xY*Uz1%OQ;-5E!Q
zDu2xJk(1Td)`s4O!BvxsOD1#`6&2^_=S-FHb98i6SXlVmc(P<oO-&loTa#rbBO)TW
zjJPi~t0r=lvSg%{nVE^p+1c6h@-j-wriRo+$ibuJR@O|qNUf8{r4!X{pC>~uQOu=P
z-$T5?+~405y12Ob^AgaRgq%Qx(`{y1u73`?bhGKRH1Y~yd3ES}h>+Cvl9Cb*Ft_0L
z_O>|*YEMl~O<rCeH$zBBhz7X1xj8sE;NBe^9JG90gcfoMr1@HlHth#Dc|${k)IH<L
zvK({V=u*7AytFX(?T7Vy3EdN2A4FUZ92{Zv_Vykd8)Nbi$$%S_`6H?^9v2+90e__^
zH8oW_lhM)9%F4=@mluI?t1K-oF>Y;bl}cuGcRDt=uyn*w%S_D7%p@l#J9YkPU{R<D
zg75F|m*n2x!osO~8fo&CI503E?Dh#^WwF#V6crVHsL7XL#@C>&tt}px(_!-lVos4g
z#|45T%k<ILAlR7+*?=<he3zD%nt!9(YpSZMlsf|mU7TQRT28pi>GJYYH9FR4D=oG7
z_V!kCg38>kSUK71SR=J|@`%owe~kv1dKerbacj9XHeF;>t9LHgX*2Bqj*&KACI&~`
zb;gg&)B@_$pYn&5dLHUH?^9eu|I%N2)|Fa?w;8BEJ^9B|I#HS%+NuBgPasd56u(Sg
dGw?^uz&{O*Kwhn6{15;D002ovPDHLkV1jG4iP-=E

diff --git a/Telegram/Resources/icons/menu/profile@3x.png b/Telegram/Resources/icons/menu/profile@3x.png
index e2cf0366701129d76e51c5ae124840c5e42dbe13..f760db92b79e12bd5f38308fa60261afd5b14a45 100644
GIT binary patch
delta 1853
zcmV-D2g3M@5vdQ5fqxT8L_t(&1?`&KOIKMG$D2uw#E}rYXrxjxQA80$35rPck`eUa
zLn-tkMS=7(59WUm!VpB!<tagY2#Q3|=s~&(Jrq<>;U%#|i6V$5vb;3&8TYVQ>~r?{
z?ep`TiQ@Nr__5C3>+;>}?7jBdYo8@ca=)AhavsQeAm@Rc2Y-HC5B!o6^!4l4*Vfi<
z-n@C!rcD(U6~8U~u=Ur%@Ad1~LqkI^Uc9(}|Nhw6SV|a@R9jP1bK$~;@$vD$y?-zx
zf?$$3lG3$w>C%>#meJ8s@8N_nB90PNC4wq}sd(VPfzzi?Z`raX6ws$npI*Lv`QgI{
z<{%7A$rUSBY=79Wp{S@Rl>hnj=ci7c>h0|f?fst`ckSAB_wL=G^FMz4c=hVlrlzK~
zYuCD<&;<i*Fb6fD7UeG4blY27TW4oyHKw__xz5hcef#z;Tei&KHq3}1h`82<a!mN^
zNq{pyKmXFDOBw|9jT<+Jfs4_OI7%$F7{c6Q)cVSnmVcJszI|JpfAHWzf-wzMDAf8f
zhf!aA+3{guWMo8JJ$v?SQgKWmp%As&3Pw{L8&g8RtC^XZqeqVln#2i|V?u_9>9ppg
zURUqmzptySOMEbvLQG&t#s&b3A%RB%lo-H~u|~xfclhw(KNfxvt{pEFC5H5SOE^xl
zrRd1+Ie!C*<BY{xlg2wV(&f1;U`~7rj3oCry$iFfLtSe%?%usSW5x&rY?ff?qmCtj
z3FV31NX8@%BM|$<z{bYLFJHchR0IJV%o#CZRPO;m$|#b70gca|JriU;`xP%ikBZ52
z`SRt4hKAo4eh@tBi4C*Qd&9KA0FsY?qsB(MEr04r3!+#T`dBu427+Mc&YeLi8nwR=
z@mX&=F9BFkZHyK<iIB+oWK4tvaRktpFJJC5MwcENf-X*J2Y}?00F(CEgns)ZsXp=&
z1DR~0?@B9yLFOcay30iaLmcO`F2eP2uoW#N;NeWFl?_VxSxBnKNx~)~KKmOmgdccL
zy?@aivRXJY`K$ppU$IJ%h*oZTVG|L=Gl~F1&Tqhr;Q!UtM>VFhveNY4ym|BM*RL*4
zX)iA?7us_TXf`b_dMpAMfH9501Ej?ukIsQnV-hBxcqq(i6B>*LY1wrSOvk`GckWnX
zp^sBjQ_{Y1<3^z)2+gL&BSbV94bl>E4u6b#pkbvNGdVeFiG@BsdGbWs4<0-ybOfQ<
zw0MLFV1Q&81Y`ui-SzO{L$j<OKYncYxu4Sx_r@}i3^qWzusGisbuuGE<;*1<Y`=K%
zqG`;_%VW=p-k@QC&G3t0P-n13APLAIkF$jtZ@dz##FuJHC0K!Euz4;^5&W0PZhv41
z`Xs@U8G+9;3D)2iH83s>j8`Z&Xr(@M;>Q-tsFYp+X?10O%r1C{Uf!D|F2+H@e;9lY
zETU5gpWq?PHgWy>bpyrJ-0g0UXVzOHrGcO+AQ^0c4ECCq|AJDmTi!Ca2h`a;w=%7V
z^ykloAKC5p?b}Vp#L{KI;^skf#($y#BNrM1GKPjDM~=wk;r1YgWXi0~&CLP~kg=_i
zUy3|;BC+LD699|=i%5c_?91x+JhnvXN~kY^V+%<7`}++ZHuybz_9TRDQEGr8*@nOi
z)#wZ!aCdce9Y22Dh#fj~C|Z$dFDom<JO>I^`O1|mT?KdQyYJq;o0yomcYp8Rz`y{s
zXow(yA-%v8IyMojm{VO{eeK#comI$Uh$a|hcX#)W9Xq1&C05MAjA!|+xVV_=#kX(Y
zq6ZM&J3l{v=FFMG!a~>ZVwK-AT&`1>eERgMlv$WygM)*X;dSfQ_4V~_-MZCkfAi)I
z6O6ZEwVOx6026if>eW^+8Gq0I{rlg(eQUK-lj5Tg)5tcsZQC{+Ve!O!j4pAnS+j<(
zQS_^L_3G8JW5;OsF~d6K0!0#Tqi1+{xTK^+izi>=Hrid65(NbXx}?s*_T<Tvyb6uU
zhi3Y<x3^DEPm6<Y-MXcV*r{AffXsD`)YjG}P(FYDTwh=BGYl6GrGGem6?Wpp3B#G<
zH4+Ab7k{h}Nk&Nv_wL>6LhRqpc}IdO>ywz2?!})$W?j{uKYw0cSN28Oz@X2aJ11&8
zIyzFnss(^`xh}z-`g>JXRRCd@4XakIk_jm$7;1`_>mUt&4uQ!lSFU6^2!~itPmjDb
zLs0xm@pA|R0|Rn0H-A<iJ$jV=#YbRlsAA{T1H)YEqcSj<lX|@w5TpeWb=d&^kOstc
zZei47$dzq|s7R!VI4QSV1_S|(QsEE$k(l^}{Y~%PGJiO~efQ7$GUSRfh6v&({18({
zOZvk(K+ZVw>S7UyqeN#?qW}97#56W*sVVpj3}!?UjH|{A2{uf{P<K*h;fzC|`$;}e
r4;^H#G3SAt2XY?Bc_8P3v^?++HEl+72`ft`00000NkvXXu0mjf6TOn!

delta 2080
zcmV+*2;cXq4~h|xfq(Q#L_t(&1?`$gh*e7vhG)!Tj+hssGm1EhSwu{TsEDp~kP#6X
z7<D%yx)9yyLJ$+`O5GSSfG$)pA|eJ*M8Sjs%wo>j_dZ@TMR{&_pT75=^WGEgUCi|9
zuJCttpX#dWQ&d#=6)aG&K*0h93luEy=USjRCFotcbQwN;cz@5HJxfbVJ9qBfv}w~O
zO`4$V{rmUt-o1P9;K9wCH?Lp6e){z3`}gmsgds_>g9i^@zkdClJ9qvH{y_{2j7j21
zN>;5}wPwzo$sz<#$3h4@LJ*Y*ssy&;`0?XctXR>zcW)~oHs0;qx8J;Z!zP4)4cWYT
z^KRX`u{o{uSAVZwUAS=J;lqcmqyJK8|Ni|?oH&tbe5SE;=gw)<rnPI=&IN@b2q1$v
zQvzZU?&3|izpAS0`}gk}6OWrWZyq~#Y~8wb!*xRp3ycwI-@biAMYx;<DC^Xzvth#q
zJtf+E_wFqzDal7V><Ed7MG@-q5gW?evSrKT$B%3EXMfI|88T!@D1RIsq7bO%qYkBU
zeEE%|;Kq#`TI;G+t7_M-ouAYwHv$o>wV*V`xv?emT7CWcb=Is|QBcV}BODbnJyfT)
zC-r;v{Q2`?!-gd{3%ev#ph)Hh06Seiy*MaQfFZM*6_zHvVg(j0S~Pd=+{u$Cw`$cY
z5Dbd+x_>3?Cpl8IFAm><99Ee5)vH%x6w~<e;|E)7)TmJ*(?gxG10%`vO`pQN)*)!1
znLT^<hYueFleMvD&z{DO8^;RNz63C_^g#p@CQJ}aIO0GbNRTC5{su8rn>KCyP*AGR
z0D?F_ef%gP>6I&21X<vDHD}HoVWIK#=~LdW0e=Gqbm-8bM~@!Ur%yk5@}wppd8t>g
zo*xaWg#}3Y#>#}-B95@YuiK1v?b=np?z?vFvbqY~0AaBRU%Ys6tZ*Hd04$c?M<AI9
zZ*5@2w0-+_fxBhP7LW9iBS-%F^~(^yef!orf6Wd6@nHbdL)o?U>Jy}fFq4ePa*-N0
zYk$_v&$V{#TCqZiD+h+q=boFA*oqVaa6d#dI(6z)VZkfLvfvUwfBtM(4jw!hOAic@
z54>1<JAq{Qq)dUC$dV;XL@}<yF8BA|BS(%Hmb-WF{yt)P0*2&oz_U=)bUE{Isxi0$
zLw4xUAvSjni|>E?_U+9u^iQ5V@sZYyNq;d%cxy&1{}BrqGXOk5T8RGXa!!mONO%q$
zI3R6*gMDQP2%`cQxj};lhDCV&Dl`}kGQ#DY7?m?2J$34owEd08j~^R?;^N|7y?XhH
z&8QShgxAld!Dx^XF6YFk2O3fwF(+(<&GkrpAVV`|%#gOL(Wg(Jva&KWs(D@X^nU{e
zNQOW_dg!A==g*%v@ASos7o%hDX95CVEyGBpJ7~}#H*FEy72)D+!UHAYI(P1zN4|aN
z+O=y&<*{SO>=cgP>gsC2O0C!B8!^G^)vJZ_^5x6nQ4)|0BOo0p+}%{64fFo}``uKQ
zk^1%P-@0{65R<c1R8&Ohx_9rcgMV7$k1=D$L})yZKr-?H=^{(M07T?LD#<1>CD9fw
zT5R3Al~52f;~wbj*RLPJl=hj7lAgIBCfqMuF*lXOJMyy0>SZ=qtEEeqW(yriMpQ0a
zF*oO-A$IE2Y2UtmnUj$oO5c+vP0}oZ?Yx`^xvfD!2otE!N;Yudz;)}^5r6*t{Q1)e
zeEs@0FA0Z%-i=mXS10b?hytXG#@WAY*)lP$rv&fh&ln&JMoyzfjWR~E>p+MA1Eh=D
zc|hZ-g=fTw5z@}N!HRLVe*E|`X9<Q;gV7*E#S&kNVRr1;5z3Z69TzkL%o7CrfaH+2
z_{1eKmO$D947q(H5$5Dc0)JS(GAKn#Rxa?YLVXAHwr$(y&6{V~%FD}-9zALm5$SB(
zw(aQAqp9HH?y0M}Tp#E7ix)2l9Vk&zw0imSWkinkk-I|a=iHZe){U%+Ie7NnyLU^y
zMc#zi29pG(g!wL%i4}8D5LkZWW_jbrjZ$)oor#rzY+&Wel?@v<bbn1OtMXgS<vI~J
zaku1xLx&E{%oI9y>`1yEu{Y2kJ$l4u<HpqrlvtUKni>0FxNu?O#ECFQxG0&OJ$u%4
zQatY4w=Z^#&}8>Wnj{7$B#)H1d86*#yC(!Rc%(iRTeUVIIBh8#KYaL5OW<_k%L0uu
z`4YF0?z)t~G3e}rFMl7nC31NS4K&hb`z=_o;M1p1GCe$=PR~-glmG-bH|!|UD%VK!
z=g*f;E>=9HmB>u2kK@LTlVU4kD=RAvnZ8B>Mv~V^D9F6{qarQA=$9{F0x!F)Q<Bap
zQ>NfTjIo|Qdqz1m-HSh?IC531tJAp`Wo1q^jWui5$SzpEe1CcBSGDM+_97V`x+M)^
zd*m3i)l5eJZ{51JY)DaoI#dHdQzqHY{2T(6n>KAqQb$pdKCjH`rTaOALC@Uux}lg{
zJMcm<Wd4Y{bm@|rkNJzb6hHFNnAj5feKYDYbKnJ<7Ny&;XRZ~BQrv-By|nR@k+6`*
z1V0i(TF8x=;D1T7!I7fRZrMMSNytq@KI?-_u7!l124eF(uVP}w5Ht8c#N@0aVWoh>
z363<5CMu2p_YW~;+Jryk0g~fr+lRMdM~IH3ME-x@z^8FoOHCnURuIFIpkFmsNMI{k
z&B>8X=x9Dqr%l4f56VCF^K|PZ3!McE6f97%K*0h93mK$kfxiJd?JRh{46JDY0000<
KMNUMnLSTaM5(e}D

diff --git a/Telegram/Resources/icons/menu/saved_messages.png b/Telegram/Resources/icons/menu/saved_messages.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b8ba1eb7e084458e9598f0afbecf8dc3c34ad6e
GIT binary patch
literal 469
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf%-++*F~mYJ
zIYDAugW%cpn3$NWSFfHscW%#~JsCj)@7~|vuOGYX%8eTp-`~Ziu)bi+y}vIuEUfI-
zmdyF~^=@J8>6<rgs;aKOee>qa7cchI{1npbJ9Oc~g&)7aub(nSq)Wr>+)U&2uCA^L
zsWT>6dU<)}-`i99?TzFng)p-V<qk(r1Zl7_s{Z@`zh9@fn>Xaxi4Gy17i<qNc?Q&6
zQD|~JVzB7&iCed<6crtPe0VJ9TnJJQ;EI>{`Rms_i^8P$_x5hxy7l|}`|n@A6ciV4
zmuR2KxIyB`ks~5<a&;dc9esLwy1ZqPOJHDNWTd5uiO7t{@9*wz&ym?A9e5)yF7E!m
z+TE3(pZ)&+e)g<cEv>DAOC%*4xcuf?ef|3S`tg2w|CvUtzG4TbPME+j&0SPfw5Fzp
pBalbz!j7}E&8@Ahl1}jWGcveGW-V(wqQMUeQcqVumvv4FO#u5*xC;OP

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/saved_messages@2x.png b/Telegram/Resources/icons/menu/saved_messages@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..b89a0923115d5af330f366328cce81904c3f9a25
GIT binary patch
literal 758
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2Neyz?9(W;usRa
z`8HzjX~#s7?^7FnpU4|1xS9oei7oXIbd^+D+#@!b=hQQPg8;V3HD;=w6Kjqw$YE<f
z_~3D^zwOT0b-!)CSM9xB%zn=HdHK1|yZ2Ym+x2{+#|u}{1zdmDGX*I=eOgqhAW*RT
zZq?p6waJ{GiOJ`le@@$+xi#wZ&pB`ZUoe~L12pXU`RiY+bj7;8mI|G3UNGTcf<*uE
ztgTY3Wcb)Cj+}nlwEjeImWfpN(WVt!ZoduCIC7NZ(}CMBzg8)neOcnABGgn=;c#H(
z%^anvk3U-QcRl%5wReS!)>Ng$lZ6>a9u{!0hOZWt{3{lyA)+836|^v54{Jy1vdfuY
zTB6oY(-+^K8X%P;@WOpxP2j($MV%k4=Jx3ex^uk!@cZw%={ncyci#=@3OTiVW<kr5
zb>Bs8R9z-b)N&D3(sby$EVqbjqKLxLO#Z-D53UK1wzRKsQepM@sQ+cc`RA*zzutV4
zr|oHm$*+TJe3gE7gs%>r5v4Zy<l&EB_~l<2H21kFo7^<m-+fd`tb6a$rzKW`XL8K!
zuO{8CStQVtX;7==`KHV|<o4ULtx>!-3DUjC+!iyM$?INwrR2NL$zcBB4cqVLN!lh{
z@lu<dF^T2up@$zztWp>0XK@B{9G~dxu{0=k^Uc3?=XKcH1txm5{LV7i5~C;naq2|Q
zwbx(Q*zvo|^GvPV|Ni^$!kI!RqobY{{p4<ukV`1p9ov7rdHJEfqRfR^1<#JOac(u9
z$#Zzw<(C#Rd<$ou-yt4#{IQ_a14*Sfe0%{#J7bmxZH!ub$f!VLfhZ4~_tHsc(+)q(
h=+SZz{gKXE$5=RTyWEw<_YQ&5oTsaw%Q~loCICjbKw<y@

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/saved_messages@3x.png b/Telegram/Resources/icons/menu/saved_messages@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..6fa7627de4c722a75e51ddfe45ee702c2a9fd117
GIT binary patch
literal 1076
zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|Wxo#^NA%Cx&(BWL^R}E~ycoX}-P;
zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz1|Sip>6gA`6MbbAj}KF`y|F(iZa
zZIq!$QJ{$4X^#ynCNOy_hHykH@hCNIYinzZ%F?<usfCR*X46I~<!G5%Sz1v-J&U45
zq?9_^xK@^Uv~zi_oU-Nrf$r0fQ~z79e}36oX8G&t=XK9(?ti}*YO#D(m)#NuO$V+C
zOhD?uT&Am8T1GQ{nwy*7zJ2TK>wER;ss%?<Yiet4ZEcS}{y2H^<Pt00eM^7t+P&La
zgsZl$&PniA?WUV=u3ejUHtq79{s{-3e}4M8^6d|g3HEk&YU=8y&$sQ_qvIJfZ@sax
zan}ybYQtm4j&0k%J+f)_>eXMre%-uz^Y7o)3y!Z_9rL2Zs{QcA+qbRF&C|_hvzk=A
zVP|9W^Y;&J62BQ29$sEve*6z73lmV2>m#pn&T~uV&Xt{JDAYNHFS^CV%IZ{+jJ*8x
z4-z7(6ZIUeEA%4n&6_8e+4AeXw6wHN<K>qj{2eY5a&l>vf9vdvrZ;WKFws(}sHq9r
z;9HuJoSeKcKp^Pw%l;#m6(1|C*}659W8a=VN8Sg?|NQl9mWK+9Ojxtd8m6SJTemXF
z{K-4@Xqs6=YKrNJN7@Q%-A6+i1RtN`b1{4(;iR(8>4f3BHx44seTpuRv|3~+lGrD$
zk-B2#;>FCjS`C;A>gxXe`Lm|f?X0}(ix+)8Jr0*YeEU|Gpa1^D2ZNbDd3kwdWo21s
zdAs#{*c{Re>+9t=FJHDSt)}MBqeo4qQAfFFh%|mvnRM^#*R31G(@G6iPCfdd^X1oH
zFJHc#JzLsv@}g{&y{CkYC3t{wav}1sCv(lb88bdq*ldjODGHw!r`l<1VshsE`Q<^H
zo)<5O-@kjebZ5+)j}2w7uUwg;R5exc+NDdIcI@C_pW7mI=F$Phi&w86)#w!OdNFl&
zM0E7+Q>VCm?s;lB>f1>0+<IFEvF?-L=BTyV+1c;kzZde#&(8;@34?<XQ;y|}r@oxp
zdE@kHZ@uZ01DY~>mM&HOBXi7u(*K3WFJ%_p=~h2-WKDupNn#=++pfB6zuL9;UlcZy
zla>9;7@X#9AtUy=(ezLIhpN49iv!b^f4aT@VzZ{x(}W`HU%!7p6cyTWuz&0Ax7VuH
z_&f1Wiqa9=X|(^nw)7E}I|~;sY;SLGZf3SPJJB+v?O^=oef#!3dzMyl&}e3lm&q+Z
gNIn5&7txRh@@KT>C955AjR56XPgg&ebxsLQ0F<TNQUCw|

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/settings.png b/Telegram/Resources/icons/menu/settings.png
index 4453866e9baa53cc1b74cc0c15d796e633700f66..27b0cbcd623e1dea9150ed2dfe56813a64754302 100644
GIT binary patch
delta 581
zcmV-L0=oUT1iA%~fPVrcNkl<ZI1$a1JFjU$6vsEmvm=B=B1(@+B3g}zFCdXfC=?Q+
z6A^_<q0xHeq80HSG}NL)bRrZY9*t*k|L&Y@%eLL*bY}|wv(|6Tp0#GpaNKVjlK*T-
zqtPT135&%7W;&gg%jNBM`-%U!kW?z|_xnnvve)Y&*yHgWkAFv(%XPcmKCLBgcQ_pP
z``u_XGB*UKFpAS0_p&3i*__Mev|8<8Fo?(F`~BW(wepdp(MY9IRVo#?+r8Or^7;I1
zHsifdfh>l@VYl0@R;wUVsgy#Yc)B?bKsKA5&*uP;FJJ-i4QDbLR8*~2)8J1U7+{15
z0hvs;TCKw2uz$c22n7E88xR5)V1yu$*`@4HE|;S;i^YP{&1SP!tKpb1ATXs~zz9Jg
zH9j=Z<#HK`M3^HO46fH})<m#CU<xDidc9u2*ejd9{GxFRg#v2Htc^yaR4P#dQy7^A
zWT(?X2oF3JGf^K8y$h5E`M%p9mLH|&^ZAe(LWFDKIDhd4WKJEZE*cMAg8?BZHsI^^
z%D2mRYM09ewKSPbbfHiv6pO`>ECvLo)C(A^Baj*&nl_}!p}yiD8xk13BD>wLUauPr
z1{yArNTSi`WHP~Zd%q&jQ#+kbe!qV_9_w_vbUJOf+i^?jLb!&-VzJd~>GgWJ0H4oi
z3W#f)%|7=2j5wW6aa!<<PzVM7(ZlvdgQ>=L+U+(9hX)sS<dsY&4~GL~ez*P$G-4xU
TL4Yo(00000NkvXXu0mjfi+vHV

delta 449
zcmV;y0Y3h^1-Ar{fPVp@Nkl<ZI1$}cyNbd<6iqfDD5SI4uwZG5h)sfElhTjy6Jldy
zB}wTg1oQ(0Tk`?2u~B>@5*xA7La-A-h<k8gOhyt@*z8Vq&bjB#%$>P29QSVrA75Fi
zR4Nn-@T^v=@pv3>j*Ql7wcG7Bo6Y9)IRI#+H73CG{BpUh*MIB80{{v!CM6WAR4R+b
z0##rmr_)K(G!j--6?8NjAp`?#p)`&QtsqH~=XthmUoIC>6kDy<`Fy4c*=)AiY(mLo
zGKOKuvV1rkXfy;mosR3eA&KLD9)l5KYq#5s_@w*&zFMtPp3CKi!{Plg06=L7Hkc`8
zLATr8Y&PX`nSTV3-0%0l9|HiC=x7MoU}p0vPp8wpUJu6%1qER%n1n<DKp}!!mNlJD
zVWx=yUqM3f+dCEn!T0^w#HW(5v|_RNrnHY8cJk{V3<k{gM>eFQD5hy{w_92{`moVx
z;K)fs=JWaE@kpaApijhcoa^<f>pF4Z%6GdR`abH3usRb3`D8K)KHBwqol2#GhNPH&
rBj~KR-$>|;URU_c$M!n!pHhASvY7p-G<uub00000NkvXXu0mjfj)>Sa

diff --git a/Telegram/Resources/icons/menu/settings@2x.png b/Telegram/Resources/icons/menu/settings@2x.png
index e255fb46a70d25847d08592593c0ff4965f82587..76e0fbbe4befb934e1f37ac0c4d4c468ac01abdd 100644
GIT binary patch
delta 1142
zcmV-+1d02l3Frxsfqzm-L_t(o3GJ9mD0NX3$FJ*=kUYQgijkp`fjsh_Fr;MgWgvu<
zD2gHjWg;11Vqj!wNK(qgz(64(6HG`V<WZjAkCknA&OPVed+x`lK4)<6+H3vSf9<pP
z+Iz2kT&|zzX9NNpfuO+LUGeep>FMb~L9aY7FE59Ohi7MJ0e^BjbT2P2-`?J)6NU#R
zCWoYs-D+xTo}Zr$T~SR;0GyFKnX-t8h=YTJ#Kc4<sJFLwZ*Nb<&dkgl9v)^APft%X
zGBR#&Z<Xm2g~Gx@8H~KVJaZV7Vg+WldeykSBFup7>}*l4uCAJ`N~vIGjB^e1QeR)+
z(a}*<ROEKMS$|VwV`KOC_lyCFV#O=z1#D<&C@LxnN8BNS3PLL{@A{seo|~H+m8xXS
z#l?j*udc4Ty1J|=+S}U!2=e6QgoINh5O#NWTRGb4o12@3<>~3^`ubWi(wv^2PEJm?
zb9T8>Q&VSWXO$NMB>0M^sj11{-DrhQI<dC4hRnWKS$|o%wY9adu#lCNWkg%IN$}<6
zWgKybBrPosLg`#eN{Y3g$ucuDgBwY}GFC>}j#bllL3e~uQjie7GUVsy%fOC|jQHx~
z4-Q)4Cy-Bm^YimKkc`pM(Ow-wLPFSYe!o2kG`+e&3ppX?=H|SDT`@5+GNhfIo%U$<
z>%qan^MCVmd0qLXi4v2&FDn#0fe2@hd|e$vT~bnF10_8-Ha7IG=rSe%HtwaRrGh7%
z4eZqv9v*&ocL#cMa?%PK85y~`xhafGOG{N%RlnXI1e&4;0L+R3a$p1^oE7FnKR!M#
zq$~uFi<A9V0R8>_`U--ACQ7ja`yhRnAe1ExhJQfsYTN&~xVV*-6(MCeYHe-R;bCE6
zmzS4V7Z(?Gy)I({01U9MM;2eKgdmW?YT0BYM@L5`yYbX<Wo2d3*NTb?U9ZcS1VgUq
zdT2$@Ao(;|x-;^`#036igRXCAXb{ra*jQb!%a{ZMSl1^cB#6Gcx?0!kvR(PX(E~I%
zIDc4o2t-yh$k<!)c<zu)SxZX`Zk*4$Ge@^Hb8>QYy)G-${{FtMXIV+Jwzk%fDKjK>
zTU(n>8c0&JkE_DLkEGU@y~i>mkYx{{f6Jbc{9YTQIyww1R-ZNGuRA)_;c8Q%O6NEk
zzT$8t7*zqg#TS$x)H#NM_uTw;1sdIyg@3^IrI)bmkT$!s=a(~Sos_VUklG%Ld@eTw
z0|Pd7g`opUNl8;vQ|s&Nffo7jT3v%kujAw6y1F_;Y(q0NG?Z(<ey!sTonM25-EdLl
z(2#Ol4oI7nEGQ`8>Laj+hliu1Ba!)aeo>S@Q3>i-X_Upq#XCDYP}^LkA0Hp32X7Qc
zB+XyDN*iWDH5(fnB_5HKQVA%<%6A#1^j+}4=+$|5cULgExw&SrQu=huG5H7d!HTTv
z>+AdXAFOz*&-<ij8Zw_1pY+kw$&~4R{zjjL^=|#h|MLj^0r8e>ehnRwtN;K207*qo
IM6N<$g8lI=zW@LL

delta 1075
zcmV-31kC&B38e{;fqxH4L_t(o3GJ9mD0NX3$M3zw#Pcg7!a%~%z@vns%rKR{!kDDY
z7%2>t$H<t(#K1EnVj!;~$}5zTC{mO{_`aXJvOAq~?mqV()GhZ6So^=$`tQBhUVHDg
zug4SlA`ys0;Fm?f`<3~Ym6g@i)#c^oMMp=&^ZNRFaBwg&F@LeLvho#Lm%bex9q;e&
z|12LMDwm`lkIM)Z6cntjt@(Vu^YioN<z-^S2#8a8d3k1LCSxitF5cYSbUmz)9lN``
z3C-2jRYF37(U~S7Dh!Nx@Uky>Q)L+&8(UghN^^UA`{Ch1;RrMV5ip2}i7~2zls}SD
z|D~j)w6(Pr6@L{m@0ppIySuxtt}fOEueY~1tFM41B_+GNyPch#X=!QI)zx0FcYS?*
zU|`_p=Emw00+ODdetCImN!$-$YHA9tl`Azh6_u?ERJJZw5_^PcZEbA{_2lH_+}xaO
ztDT*l%F0T6pHH-=rsnAA2wz-q#9esEbZ9@VLj2#<)PE!_&CSi?T3A?EUtiDW3Wg~v
zDypHOfwdN6NQ4dAVBF0%@$vE3*Vj;NY;3rnlYtcys33$Ea)Tx~kwnIsfD;vs2i<Tc
z{q=tc`k_N5iw?Pl(_Y2#&w!?<r|s2#l)Zg&a)O!qXv&EY$O}S@HFrzEY>?~e{{B7?
zHTMH&bbnS>meGJFpyuijLLoOgI?F>tLrjGTvl>=w!XUvvJ3Fhbt^MQw5NL{*ZJeMI
zuff4VE2jg(fp&X)D{Ha1xR{%ps~kuaSt+_Vr6)}p69%Sy;!qVfXdzc@Aqd&o+57wZ
z!ZJKOtUO6x0SSo$l>jJO%oK?5`1lBZcz6h{5q}R&X0bWSAQ~GRQ=UX#x=c<^R-h69
zMQdDZjxGTk8yl6Stq?G&l{G}joN@}$3#OG4W0l1(FE4s`-GGQK7o<d<fGeWo<71Jj
z7!{}lmCnix72_bC5!iq(m&}yZ)TydVcS*glupq1EGO0t__T&xU&u;tG)m0`mIy$NX
zV}B9|z8oqtZ}k^Yb1*`PCFCYH)eM+JQYI%RCMr{tK#@uXeR_Io#AyO*>er;CB<zLU
z=;$nm-Q^l~<p(jeEztV=`!g~!oFnYFsHmt=)k{lDi{zQh0u4g8w;a<D8B5rwr>DNY
zzPPwJ&9HKEa%7*A<KI@o(x0Ztzqq)ls(-4oj)_MS&ywfoXSMH?@YNz;I^!AC+uOUe
zw8Wj?+S<ysKQb~Reaz0zS_d>fJ}z2HpZ4~4TyVs_r>6%htNr&N^3a)|pSRKhGN8S^
zJ>ElBts@ff9MXGImSB*dpKpCi1EL~$ERdeWR_pSK#Eq-}v4PhQZ#vs9e+qPWbu2iJ
t#$|+rH@tqZa-3_#AQFK{1i~`{{{hF<d5QQg0_Ok#002ovPDHLkV1mld6&?Tp

diff --git a/Telegram/Resources/icons/menu/settings@3x.png b/Telegram/Resources/icons/menu/settings@3x.png
index 79b2cf6d2b06d849c2403c761e86764f7cecdd78..5135a603e8d2730da6b01384152a71f69e9df44f 100644
GIT binary patch
delta 1902
zcmV-!2a)*X4cQNnfPV)=Nkl<ZXa((?S*T837{`xuWUgbLPe)|9Ktfz7Q(U;ATqtAb
zPFzS_o{piYPcmJ|l%YhX3*1O3PD*475pEDZ^N?BizF*e&{w;g2z2CLBeRzAn_q{lt
z{jC4<JpZ-!8lJV*R#sN}m0F<G0;LuxwZQ+N1sXM69fuAbI)822wEq43w{6=t5aq>-
z7uT*`+r4}D`Sa%kMFsuXym|9AYu0@I`t_gK9{^y03%W8Q*SK-xvuDr7*661~z?jH9
zE1@DQNAgowS2un7bUnU$_3HZd>kl73{5$u1^ym?9sadmT2GgxuH(uSTQ>SE7h(c9W
z)#uNj%^P|5?tdL)tWhJ!Fh)t`0T?7E7#3oE#A(i)IqGNg=FOAdDF6@{LnkJ+vOKk=
z)3A5%-qP>ew@<QDumQHA6P#3;WyR=VJ$m#o40sicrFb>~fEi46b#*+r!AmPDD*E>A
zJAC-?UcGwR!C19UpFXXw_3hg?yEIA$CX*8<P6XZ?A%6%&ID$rb$UxtsMT-Rs7BIhw
z%7X_FYHMp*f_LuRX;TF7{P}aAka&<cgioJ7tzNyF*N$7eh@gaqfRE2)nC{)X-@SX+
zCJ`l5!yP+zbnMvCCqSDvZMJUR8s#dWhX6Eu-1=fFD=V*Fy&4GPe7tbs!iW(geEsHd
zqeqXvd4Kb!(_ugf4anr|e)-?Md-o~;3+~F5D`(H1y?OKIH*elZPjk+iHOskb`NVJ9
zwCTcy3&|nn5a)y-5a9?aJ!E_w=Esa0HA+sOJb7Zz8C|+`q1T~=6U_4a%7@@>EM2<v
z<;$12#kt@FC5mW_963@&DTVU#^2?Vm%j1+OQ-8vaGM}=XsUx99<I<%|5DPO&prI$o
z-5*cqARsajOHhpfv~}y&PoF+DzQ|Jt4H^^x{N_gpXe$IU=%pmEeED*<(8i4$Q+mmn
zF|+f_J*E72)22<Lv~1b3G+yY$&z?OqUKq{o+qX~QrC+~(Oa>P(Ui|RkgK>qxBT9-=
zIDhQWp#x-$E9j*eX#)lfP}{6syEZ9_&Ye4N+qO+TN3JAL1OQl4&+FE$lM|Ajq~>K~
z$Bvc#j2SZ$*$*2w%ua~pCoBL1oXGjinKR{Q%$PAzwvl^s)<oIZYnPN!0%_~XlP9%7
z21ed^{P^)ZckUpQ2aX#zu3fu!#=L9Su7AvRWF8z47){>4HksG2U+2XK0Wecy!Q$w-
z!-Xg(0BTW^*P5D|z(U6(N;{dx1j8T$*E2M<QbW&fJ{LSfM@A!x^qEPMCPe^$s7do>
zLf&X+4?jTPC5iwDj9~x}euRMC!<iw&7%xqyPMsb<eyrFC^r)Ee%6y~E%l-TJ^M6hZ
z0Du|)7-Akpi|$8gn+e3A7lqHS$K$PAw``AJzI>TGcdpZN#flX|a*X4|1eCyJf_b1+
zk8IBbLx57xo;_8XQedDTI&{ePyKC1jr>`3~ZWtKj$1}|broo1}(=AH7$qpVosJSN!
zpFq!4_V(>t!~5{z!(mIN1g%=zw|{RB1EMs5ja$qE(~L~kuU~I4^cN}P1>YQ*F3#L#
ze6?!TD(sXVuROra>|u;kBQWMs!$uKFT%1nAO5ciMsn&9CE5fcb)M#24hrG^xD@Ir3
zEZVqx|Ned0FVhq=o6+kKE({B61jZaEVE~$sA3qv>mNyvT$>YF*1J3XbH-C9-*s#Hj
zG5#`>`Ai;p-Wb#UnKNf5PMoNkFvRQY>-A_0np9YPr%s(}7e&dyw1~?*3?|8t5;9P-
z6Dt+@3gHF%*s)`OWP$F}rw^g^(hz=WfiBLJs%Vqo!Gj0u)h}7H#JBsFm9cHne%W|n
ztGj2<p1;2T5VSEjrGPQ<_kRx=5rd?^;eSZ>aI$A_m&m>lHky&MBF8|g%#a~NB>M>G
z&Yi=ATuCci*oq<mzyOC!V>C7yUVaSHT~xG(IaJp^eoMcr%e7nP$8XTf`A+z6(H@x6
z4#j!IO~n{#+yH2LbY4nu^x33IlZ3q?F+Af-3F32Sql$4tWG88)d4JsZFu!p*#=(>J
zV~JT?2II$%=L0JyOy!Ui5>$s%kjda0$wc(I_#+<IjT0tJut+8o2M!!~^ypDFV${VS
zOMEiFF=+p*T4?Rxzu)taP!U_UY`JRHDmIa-Rg{qNRV@r+F4sBe)oF(z!~t#8NtP-j
zfo;gWd-tMhV(YWNTz@wtqCSV<K!RR$<j4`5L`=@1Dk1QCZ7bv}SFTXi-}gC$*l>{K
z{D1|(<jIqDzU^5t`&Bcj=FgvRA5<}CkRLbr2P~>vjId?P7V`!?D+bKqIquH(8;-~%
zIEH;pk@oPMg!&wD>s*yP7mmAQJSQ#6xMH_&-xdSsYOYcQ8)sk}+L%I-i}piI8hG*I
z#b(%;zg!tJen=+t<5Bm>2D50<qC$ByF+2+7W9Jx#>5B^C^K{3T$muKevt<$fhk~Cg
oSb8e8K&b^vEl_HKKi>lX0uCfU{ijndr2qf`07*qoM6N<$f(8Av4*&oF

delta 1655
zcmV--28j9D59AGyfPV%~Nkl<ZXa((BODLva80T-~e!p8Nmt2c%WTB)OsaZ@-u~3YJ
z6iZ1`l$}e-#>Ti?2-!?7$;K2r5s9Ly8FwM$GLzx||Mfrh^muRI`+eX0jq&xqtLL2a
zd#>l4=iFWw7xObCU`D`<fEfWZ0%iov2>huCxH?*qudi=ZRDaZ)H*ZMM@87?Vj*f0_
zZm8y!5!u<<U%q^Ke0=;@_Ms9@0K77nU~XGm8+u0O(X$Ew0DDcoefaR<nFpE$fF0qR
z@pBB5YJYz}EG$fzJ3BkOxVU(Ke^1tXd3ojM=NA<fiJG&sv$(jpS1{#xclGr2kUtUv
zP1KQ&=(K+%`hV;%CM6}2klx;2q9p`x;w%!_-%<<;3R+lLpsw)q^HZ}7O$3{tpZE9o
zS3}xXZ{r+GPfsr^D+><~_wn)B*w`2v8ag;QsHv%GZEXz+2_aKpz1rkK)AjW=ArB7^
z8yg$P$H!x0V#>?QA^-OF_Vo00cz77{3Fbfo22D>-KYu+vQM^GQaew>vjn{x)US4*0
zcULn7OE&LY!MnG&Cs`z*2<o6zO^z}3um;0M;()w}NLN%;xVl;%4j(^$JU>5|OOVt+
zDc`#o;|!P5)z!sSe*gac>gtNu{QC6^n83h5&ZSLbVd(Ggmvj`wLO7S)+1aTr$M)8g
zl$3{u2Y;aqGpC@S0J1RHDl03GkB>2C#>U2CV`FVokeZr0GcyC>5Cwr^48MK*7Bfv~
zglZUIn{W$Ub8~YP!rIzejz&&POH1Q5w$T|G8Itq}MWhr4*d|ts>)pF|q+or0-ObHS
z3vFB*6hR%Sh8f1mc5%VP-QC?K>3Mm1MoPCN3xDcJHOw$Fi<Fd<km$+DNlTVIYlj`M
ztf{F9TMc|5P_))KC?x}6%rgb{6h=o!g<NEa`1p98qC7o48yXrEvpF`tXaYc|C@7_s
zb7W*hCzoZ|=g*&oW82%?meKzsuwmca+@vro5&!_EQy^3eAuvV{i>^)Z#l?k?6%i4k
zCx6T^cXf4Dk|%L$#>Z^}05CniK`B%VUNVM@vXRCvjzZqo*QcElVFd3hth^l^9ayHo
zg%1RZd^rYKJ6Pa?QZfL>*yx6wEg&G^^74{IBmZh7Mob<;;>4Mil_eL5N;DA+EOLyf
zfg+ynVFrv*1KUzxU0qGnR~R%kH5C~dDSwfLbce(s0O%G)uLKL4sNcY%m?z)Ae^1VX
zfB{uiRT7N`6pM?CfByW*HQ{_Ak&6Hm07FjFDk%a$V8tO&S64>>sD=R&2^vLR$wTl@
zOiW~EW@4jF;^d=(->3}?3=mS0-~i4@U|gRNotv8r1E^?`u);COLe9l`goJX%qJJ_v
zI$97&fIB-ogj6H|pgc*89LeSDUulE{>l_0QCI;zNoEX$<KjGowK|*jKB3ch&ontsS
z@P!{|3W*M!@I?)Bw8bkL000wsV&nobuc(N1f@nR26^>!<uC1*V78a7+A3uH&EeI^Q
zRKo`?RdRv@I3r>1?(T?QSy{0vd4CY21wt|1%O{4h>AtkIl(HfS<I}y<F3?W9r9#3G
zLjoI6?R}jX(Ad}FqKc0q&`78FB$a^a+yq0l;N{s%WB%w$9>KxELImvKnMzGA_JLS%
zx3;!8011$Cpr{7`Fn~Esk%m&J7ENS~BGS4_>M=}TYY&>Vb>w~Zw;VKI{C{cy8*Yk=
zkyisq;?8-QgMsUu*KcA*YG0V*W|L0u*3Dps4h;>JygxYn!W7!IuedP>aPN)Ba*1Wk
zY%S8;+q<Qu1rJO7iW|4+{E9m{Iay1DJ#7{rCE(u4$%*^`s;v}TCp@oFdI0f9A0=QM
zvWT8%Bv`hzw3L{bsO=A06MqsC<gaAhLqR+I840GSJ^bOaR`R5TKLzlEfSPZ3J;SXq
zFx(9cX36dZE2nJ_f0p0o6fKiK)qef@6_2#-?d^E5r6)f`LFZE~-23$DQ*(1OV(~Ih
zwTgc7%I@xN>I98Mf+m7tk<7`-;X>@Eb?p_cQiIES!fPx5ylHT75OBX%5tku^@s9C3
zocj8DH7j`&XHmmjTW{kW(@^8IzZ}75{?`#rBsIbj{~Kw<-QFhW{H;$!1&7EN{1Mam
zIVS0AOgU%!bynh`Su`VHM!<}K838i_W(3R#{0Hn3mqU$joml_?002ovPDHLkV1o4z
B2POaj

diff --git a/Telegram/Resources/icons/menu/stories_archive.png b/Telegram/Resources/icons/menu/stories_archive.png
index d86b5dc87df7c9354b102987cacf282e43efe99b..a98e8583cb2206f57959c777e5adeb62591be291 100644
GIT binary patch
delta 626
zcmdna*UdUXrJk`k$lZxy-8q?;3=9k|sS%!OzP=1vKsE;hV|yk83y{SK#8N=az`(qK
zk%1XVGlIkxFu`Rd7BC~&Acc=iOV<LGef4y446zWr8{)m%F;L{0jL0iD&q`j8lZz#0
zdwLlfO*$h{&mqh#7|i@sU1R1CMz$^<6=PLTCBq4g?wXn>9p1f~ckbSt<9kcpw^yq@
zkG}tX?bUnPZ}+k`)&HJYV?EhxX`_R}M2{nnKOTQ<IRE_5IU@cWQkxWlR$fWlyz*+6
zRBzhWsI}L#H%wHy`TDEUL=NlL=831Do=y8)U~$07MM?1RL?5+!{psE&QmG%#1Zapj
zO?_FCWgsy_N>*EhYh#3uyUqU+t6Cemo1rQ~oc?U$x|2^X@X`@W?uc72e(i+9*)-!V
z|4TD>+^pL#f9Gq}+=-t*el<Mtwrq9S>gIzAj|vs%pTC~BJ@JyqZ?-${zt`B++s&WI
zex5bTScY%n2jvxEtJllzQL2A&@oic4RgFC{dJj!fH%HFUjCSSG+j9HumMC2zff;Ah
z76xccKfN_dcdA!w#q6_b8zTZb9MvXE_PSZrxmWG2Gmt2;l6Cq1`>)f&1}7dLHQ|z5
zFH4eM$sW8Ks5MoDU7X=e+3vr0o!oof7T<nbcc1%~_Jg%dd~^M_-+zDlsZr7YY`GnG
z^DJZwbRPO`Tz#O=YiW>$Oq#b$&($p1{^QFpuMApQVI#NrVuzXaOdqlC*3(ZPc|2aS
zVEu=82llML{`&Jz9kK2yXCD^qxu<_FCQeB0Xx)s8yY(JFSd_jj@akQ9FBy~=JYD@<
J);T3K0RXrT3HSg2

literal 1847
zcmbVNZEVzJ9Pf<{2V@R{FchPuB#Vdi?Rq!v8cue*-q^^BxLe@(BJ0|2*W6uO+TPoS
zun`cKL13C8*_@~(V1R(a7hnvQK%$XNKt%W;L0%$akco^T6NXQ3w+k4QNRzft+u!s1
z{omWXXyN?4@h^>sASkcAjHv|oY37-m1+L1JoqNG;oLaV2hoA|Q&C>$4e*_TOH>Ik8
z5nwB5UWs6wpwx(1BBBB`1kIh7P&qy%8gPwRC&_N4zw0OhOM)9&;$R6@^@>5MtX>oS
z^$V-``Vj9Dka=&wa}zWmh=>LUCn8~4rxR`@#Y=;=`5H&y6vPO*5sxVd2iQfhSJ6b+
zg^?&v5F|Xug>g>7KF2}LhHZq6!U+nuQ7GZ09Yr)X2TmUdkkf=(x{@hPi-9LM5;P2z
z#_@POj>RcV(duy0<#OSK4Y%1)fI#)6Y;Xxw)@P0~Frv<Dl4?kb44aHxjS@B72rzZT
zhlrY?mGyL-K*aC_r{W|=m_DU|0-wRDQ7xPj7kFF@ixE*abbuu@ST(2^iXK${g*x;3
zF#!-;md$89(UwRgBcU6<7;qyUkSC(`s-!C7m7=ahHD2_^fSEH*Z&cc=iJYNmRf-ZG
z&D5e%lVO`3D}rBJBFTah*R79i5E;%8-H4erlqf<eqKdT9b{lP@UMK7{L5x6IMUZNf
zPlQr5VSfw?YDVA;?x|pbr)w20!U2b+2v;ZKs$7S_nK05`C9G&b7+6Q8tYBHXT-FUv
z=EZWxjR2o9NfKx$!8@I`B!N1_A`T_(_8Qbl)Q~7a)YRGt&fy|Ca&$kV@KH0fqx*&b
zx&KW~0<Fb`pP9#OII|?^GD!!?N~Sx)FD_57!V;XS0-EE^>UJZ%nO0Fi(yygwj=%_Q
zJSYOv|48=;Ojl}+IH!r8IuNa=mJ<$)$IU(*@dN*-i>Ysq^z0}c3<7hR8A0G7GsHw0
zOfC(Ks5KQN13{Mla>i5ToWxg8t8ZMp))xqDX>M+=crY|H<lkO#aMi=%i$_;I1jk=@
z`$OmUL}Rg8R_mltc0s|p!^zvj_wVgr`C#b$`GD8!&B@8}`_Bvv40Jc#9Ui)~tE;P{
zt*x!mT2fMS?c2_-75DZvHCYzt3=R$!*WJ8$bZbY)^c~jT-rk168!qi?E{@}`oM0FR
zd;9PK`2t;9T6$wvrQg5o$O<clJzrpH4~9Z5*C)AL2k-V@obU5-WKQt@w)q=1U2pX@
zuDpNi%2z<xh(=F;|6yC(h3<xe7SGhFQ|GN3W(Hq4CH4GSxp?u%j?fR+PHr7DW=!kZ
z3Awq@w(pJ~&%W@<<;$0kt;?U;Qnp}0R$kWo#j#kd=v4cmL-bYERX-5qxEY5$-QC?M
zHlxSHi4&bPeI$9ikfJ8<fj&R7W5;u!av%;5TcJ%YXW?WrS=}BE279E5`S~Y*?(3Mb
z9eS}C+wSQ(&~WG1v+w3j|7qX(?y(h~2i1^JU5}z@-n8PrzP_(5Yp5^2EPQWW{;OI3
zo^yMbfq*t|Sh*Ye<>l(|&Rx4c*aTmKoz5+tot>Y}&a%EE|9D{e(166U>>m`G{rjz3
zmMc>N>vu2RUszZ;HbAuc*Y`BFww@liex_+N$lb=?)f>L?{nluK#$C!M-pXYi=C7^X
Lw~*OWQoZ_bTX&P*

diff --git a/Telegram/Resources/icons/menu/stories_archive@2x.png b/Telegram/Resources/icons/menu/stories_archive@2x.png
index f2c59be33802eaa0d1781a5f0b985d2794cc3840..5c56025698df7450116ad533cc0a292b071791b3 100644
GIT binary patch
delta 1168
zcmV;B1aJG>5~K-`B!2;OQb$4nuFf3k0000)WmrjOO-%qQ0000800D<-00aO400961
z02%-Q00003paB2_0000100961paK8{000010000mpaTE|000010000m00000+c#_s
z000CINkl<ZSPAV|Nhqyh7(R|M%5cn=B(ZP|A(E7-Y?L7^6o2Xz8y1EP*;t4aN+e4|
zNF)ogAreYtWkZRS*oX|tJlv=I=G^1|zW<zmIHy~;^DX%Q_kG^yd%o%Y-v4Mc_GX^}
z`wZB21{~DT`}p{Tg@yh4(+&?0x3{;|uqls?jErn)X+ig&pPygf?d|Q&-``(3sj^5&
znVXyY*6{cL$A8Di%F2qPqoXNpQ&UrKZ?COXy1BUx3=F)!zLE+&KtWASO=@atXlSUD
zlar|wJbQV05%T)_y1KgB!9msD5Rhwvf`WE;cd3Jihlh@i4(uKwh<R^sZ-0M(r>ODq
zamKII5EmDBc6LU*)6>(z!NJm*5)%_?B>VgO(xc2F(SQBL#Rb%0mtZ5CgNpdHT5VQV
zRzN_2h+mG`#l;0jiPV&ol*kFS4R&yFkXB0B+*nyuR8$mF3k1!~%t)b@wYQ`Y3Nf>@
zvrvOG6&M&O^fB)^{{Z*&^e`TwAwE9-<m9BQt4ruG|E|~TDGPUYcA7_$cVc1!g0{A{
zEP|`5s(*mQ+>4D<%cUqSEk!DOdV1>X>*E3x8<&@tF;pbd^73*n313h_K>^TfYisxS
z_uMeW#`E)Y&?4MmFmOpYf^c~!B_#p9xVXrTQE40=9tMM_rze+3cn1L#9UV<q&-(g0
zH%6s#VPPRRH}~}Pl*_{rgc%|zax^zay>WGQRey|xvt*P}Zf|d~sl~!<XXgm=_Vz}9
z5G1`<l8$RQFE3AelvHG6V<V+RsR0^IMn(pubsTAA3TJ0$>9LlPWo2a$geG<nxD@H>
z>BcH$XJ;ooJe&(tV#IJs5SBnzyiTV>rl8oHo15a|Vs1FH;?&gC*w`4?%Q2$*q*ArH
zxqq4Cwq(MJzr4JVtjfwt7H(VtKR-X#%eUZIf^l<mlkc`<O-@e64GM<t?rs)4KR*xG
z!NCFR61CLT)gcBIF+RIQL1rBu9v<1**=+F-`{Uyy$WfS^CHUzK2?=?Aen!~n=;%+6
z+P8>^2+-m%#>B+P<LdA4r%A$1A}`Vs8h`hEQBjd46ywK^ySuxCCY)hcS69aWFAVkd
z^$>(6<A~Z?=j7xhX_8%Z+DI9`p!fFnNE51kyvpqel?*auety2(KvBG}ukXgj25G_v
zXiiR!D3o!;#>S$kqFC#ED^eHuV#N6;Ui@8JTFT7K6#Uk~h|ypR(v6D{l&x0&y?+7y
zRa8_MuTS)Gd3iZ9GE!SxTUc0_l9Ixn#G|7lz);3CG&Haok{y5&3-(Z3Tieyu6?zgh
zl$Y-A?xv(^X=$ykt=3#AQ+Cs53>Q0TXlUs8_}KW15oSVGxnfFa&1dWk%t1mz0%{Ly
i^4huVGw}b+z;Ay&K6%8}xRw9_00{s|MNUMnLSTXe8YpD|

literal 2395
zcmbVO3s4hx8jm(0<&9XBR}D)C0ZDd~5CSXV87O#!3e5qb)p5x#$(AG=vx^B2tpr+%
zZMj1cEa)iM6VV<Bh^Hb#rM=>`M*tP^C|ElR(xbI_AS$A_;Wh0|d!6gf?CyVefB)b2
zeZTK9Te2nbRXb}pYX*a17au21rtgsXv|34D2e)f_>D!CiI2p-c*f^Wd6^yDw>lh5?
z54bdqN|PkOazf2PP$C247}Q!C&0vIv8?=Z#3!_*Wm;%>`fctG701HP&K&n6jNwm?J
z5|7K(VXx;VO69p(@(>gV4`YQIV46UUQ3%VRR%uAsAOhxjVR~(T4Farr2$dxQBFzR_
zX_76hXhMgvLO5Ku9D=wkVF(8aMgxTcegKOH@%SLb2YGxp6buW3V7`#GcmT9H9hwOz
zi(?m!(N7{kNl{uD1oe77N6+UFIt9oL2?+rq9?0XdX#|_h(NKtits%Xa7{nMU*Wp?U
zCp0WGBa%U6QzC$Nbs+||c9~W~F6u-p1{x48$mKxhnC5|~d>N<B)~V)=qjC^aVQNf6
zku;XOjMXX$iXfH5uTU?4{+R(<TZv@Z#xMG!Rxg_%shFK~7>k1ZBAS%uXfZGuBZ+LC
z9E;gWyXkF?Mhi#lFoYs>Qi4z|WopZk%Pd|XCy3>nifd3pPx}32113f&Oaz#-#)g8}
zd`QaW!GS!O$KMDA!Vt6ol@KVNne!`9!Ox*|&7cT{{3aNc!<mFmjnDz(YD9s7T8#o=
zEz1Z;6DmSS8>YSE&wC+}!0{TALNszLUMvFWm^nC(!U&(w%Lt^)1cM+UJA@m^X9sin
zLbf0?1I2=bkbox;EbSK)@@#Wvm-eIo=l<7pINe%^>UZ;)8_rx3a2!t3$;w&mh}W?l
ziz^k*ny&&Fk(;Yq1jx;4#ZX}JHU7I5SfJG_F`D#$q<aBI5}A}9(P5DaTCLwKCy;g?
zH22{`9N>S4IREV@J-Y;_2Z4FHJc8(l<spV?=*gv{M-*!8rROBmIbIwoP3!tT?GL_F
zNo!^*X6}4cZUOLf!uP$pX5V2Y@a}<t*s^37z}?g+{%qxkzFy@Meoa`k?4a-c8AHde
zTVUNYN#--tN8DQ(yH7oTK0=!2hDnoX?CUIrqR=jAT|t7+`=zD!HZ~+lw%EU**U!|}
z0w2b<w6)DnO--db2!+Dx>gugqw~mjG+uPf_)U9@NgTum}6*Ap>dV12|ctaE#8qf-g
zL?V;P^i}@s>}+9S;q>(M=D+axd_D*^Ha9od*W<alPaZz3aM~mi{qfZG#Kgo=)VuP_
z)^xVy#|IDkhlbqOuKmL6{=~$gBS$(;pYH5Um&qn)W`qvamoG<Ue(vn-JSey=zuMoQ
zlA4N7jErnksXlQX%+Jq{@O*mw%o(ZCGB0oT?Af!8jiB%Owzl_6O3HeC{QWoAU5)I!
zawT4;Yxc9*$l>hH&ws^aV{2PpdwFOmPoY@l|4p|~GZI{Ocwk`Q8?^QdJw^47oRa#J
zo~5Isnf)a#ei~G(g=B`e-r5@-;OAH0-rnBQ!Ve4Ec69XaUF<~r#fwvulTMLJrP8Wv
zWMt%WSJzGS<jIrWw+d)ADk@yOs+uhS85|Jp>z<l=qqun02P@b?u1t8Z#RbCguQzYX
z(3iH&orYbn^z|j(OR9S@IXQU{U1M!=<<97+rG>?(uZ^!Q`9Qek?wvb-hQgRmZZE(5
zE^Ut+WmSAP-Y@LOe|`Hc4~93h$HvBFGMUE{WZl74`{w56j8=X4dRN!mJ1ura6<xS+
zVP;0JyHr)>R(iX;*KufM#6|p`$BCMn$N(?KCPQ|1%+|PeRY+@7Q`t&kxH|}f$*CzR
zJ_U|@3eUH;#<bIEj4iILtlYn+u5N=vO?P+P>VNd~RK&iQ(t7S(w^!#Q!Et`A=ifUU
zTCU6P@y@siy^hw_8kyGC)?(+)5=nbUhr+vv)AetY#LCZDyS}-(Z*SS#Upme@I5^nd
zJldyB-|k)1Y1L8FKo?F?5pz7zbM<QD>EkBL4&|C3qOD5Y-QBNUx^$^@5B^<K@rufd
zia77E;L<%_Ugg(Md{@6irLwKrCxjkOPfxDTi+ldqQePXNknr*Odg;lAhTXe%eGc}s
z3ET_Lwu+31VE7%bu8yve+`4h&#<{$M;peTHC+=<X@Gu@r;nqKOw0iSU(veR8w2c>6
zeA;WYsCtLhUewA=U_Wr}RL~;F3_GxOcEF>@kCSfa@7Pf=)N}yc%WO@T)UQ1ddMNeU
zlds-B;De&5ho@)RwsK;4c=+74^zWMr_Iy@U6!DUO<f-fSwzk&>1}e(SS;Wzlr|o0w
zES;R3_V3^S<cwkcCy%nCt`;0TF7+3<*?nzH9{ke=(Zo0?8h3rCv9a+@&fi9l)!79-
b?Rd0mLt_MWZ1Q7A^Y2f5Orp3UYP<120HT}I

diff --git a/Telegram/Resources/icons/menu/stories_archive@3x.png b/Telegram/Resources/icons/menu/stories_archive@3x.png
index f6369c74cd52818298a5dcf01c0280391ccde761..715f241bae049b810aa947e4530be9ffb2a2f45e 100644
GIT binary patch
delta 1791
zcmV<b1_1ep7mp5*B!2;OQb$4nuFf3k0000)WmrjOO-%qQ0000800D<-00aO400961
z02%-Q00003paB2_0000100961paK8{000010000;paTE|000010000;00000kTFK@
z000JgNkl<ZXa((BSx8k~6i(C1G_4d-P|Kl2GZR8c96&9hG=K4-#D`KbFd;|~Qt>U7
z!a^*N1VJM~5TPI&Gy)5JXd*RpNKF&LBpaMkbMAx9zc}~a<C*R`_vX*J54z7jYkh0j
z(^_kvEnD=T?g8Bcx(9R*=pN8L@XvW*s|7ULv17-cJ$pPoJ=fRQ|NQx50f}0PaB*=-
zPENjc>sD)P>wnj;UttgXEiNveJ$qIwsoLRg-@ZLHHTB7pCo3x}|Jh>vAEs)jfcnU`
zwzlc%>7%2g#&(!ImzI|DiVq75!>bb~PVl}}&}pN9$B5<l@#8md-h^gG{doNL?ORVz
z&%nUIj~_o~XJ^rP`0yc&HKN1{92psLc6LVp{rmTGbANNk#>R|jEc)o`>iX!>Ba=~C
z@9yp{DJeO6^r%E_4jnqg1(}(dfo)s2T4H14(J&||==0~#T!mIwS8Ho)4<0<oLoL=-
zQ&Yo*Li_dW*K7>MSXF(WkdQDnHN{os*|TS%p`qpoNlHq>gydpoW@efLD4X}<#f#j1
z59{aDsee<lQTP}f9UX7nxUsyv%*>09j^?A%rY|uu5mS!*+S}Xf@9(d!sNmpWEYXuE
zPpS(mgccYW$en?hemIMT7&gT_*wEkKPh-{8)MT}dOI?3)cXrG`D{NWn+QZPWv}mj*
zCnpaaIG}|TOJgEp`tacc8K!s-jEIO(eWx+Y7k}r@og=F-A(dg>xpM~~I46|(<;amE
zh|UlasCEWy+qP|Zc$gW6jsEcA!^#Z%_3IY{L3E-3;lYCkj2-4lVYd|}N=r-g_4Q?B
z_4V~1KYkQt;U$2BSU<8GUIO9HiHQkDfKx6!JY1Mq(%a|HpVM%Wg_1Z8iGKh7&2Zoy
z!+$rDo?>ER7(4z}R#r;l5g_vL@Sr&3)vH$ml%lW9G|a}vW@u=L+4l42Pa6M?<cds1
z<XYGg88c4gjZiGm50mQZ>X@Ltd-t9`eOg$kh+(m@vBYm(!=MHXgLzD!fPer}Oi2S<
z5F33=110<%9y6yRLn<#XUt3#4cH`sY!+*h+*+eLax)L)C8M{nwV{%mt11RbUc6N5g
z=qArX6#Mt@r!_i0K5oKmoel(aQUnXFF?V-&f<SqY+MA*wWZ14<yU2!rXT(S);f9c5
zJ9q9R=I`IXQ(IXBQ6ghJcI+4mLCQd+a|s#7t+Vgmy<0jcFJ)(EC+qj~^LzL1U4KSK
z1}~W<IwK<^*_MdESFT(!QzXiab8~Y9hz8CjRBz0LRl<%5LwZzHR49Qk<UwYD)SEET
z`1I)$@)1KS?Vk{aQ=r(;(4dMy4AI@Yce%DLEG*>Y<VXicEnS5rN|{(kM~8ITqLk;)
zpGPU5YhGhxqm+VIVPPTTGPjlrd4D9{%gaktW9bB_<Y7A@a5Rt|Bbg9DK)`{`nXx>6
z{8)ezuY`6*yngZG1@Vgp)I=$zfl40s{i>=eB1Sm`BT0{@4MgN_Zf>mq{rmT(1TuPX
z!$3}LZf=&rDVg-lnKM*`MJ-H#ue!RLlnDt55ulXxs=d9P!Qog#04j+`CV!E)w|7xd
zk?@Q_L{9Utudh$01U~Y-ygc&L_3PL9NG#EZ;uNV-TwJVzNQ@E!Y6glE7MK4jfXau6
zi;E+}a8W1<AbxAe0xQ%AfMsuQ@8skpx+957RY36N%NKlBfn%_&tW0rj(^#&9g99#w
znZs~-rNY8BmSDZ-?Cfls34eowgXYJ?)-%OWG7iVCt}e2zrKN?cSB8X}@CgSfm8oGN
z?o>CS5gVwC4@|hX&#>+PGj7~t4daFnQnbjaP@u<e1NUmyjR21p8ybjZQO{CK8e)80
z$1r?Z>5MuG<mcy8ss<(I=jX3oyJqI)EvoZbSy}i>iM;mm<x6$cm4AhT5BI1B%2dHk
z&!tP3aEQyoVZ?~G-@JK4Cb7ol%a;{EslbA)o-$F=2|MS#d-qaOQgEzGmw^wA2muX%
zShLy#y%FXV6ckY2!Bq}{CB9w6&UxX&1=txMAHQ$kzKu+!O9ISrq$iYjZA5ROe&})g
z_U)ymC9YwnZ77XZ#CWSyR8-X4w{M?5eJcH%IbIx1I&p#{LZztE)G+jLQ*up|&<+$C
zA+A-gUcK7Z);2voZDby*G1`gdBCGI)DHqPPjr<Ka*EpSU6Mzc<SZhk7_vjwbJ)nC)
h_kivJ-2;Xm_zOJq_faMK>CpfH002ovPDHLkV1k(tP{IHJ

literal 2952
zcmbVO2~-o;8V)`{1kox|u(E`>fJ^p-kdZ|sNKhk70z?bNAu}N%kcmlvB%&xNi-H2B
zE>%#Z;!y;Ja}=uxNCN^Xh;0?csI?aQEC{F=K(KuiL{8h|dp)l?XEJy0eE0j;?@zLW
zHw9Q*eqxEiV5|e#tWfmMHeMDp(C_YpOFlwxvt;be3Jk`|#(0@vzC3P^!I%z+d3+_G
zvk?#=2?P*AVqk(QL589+7*8LS3=~LUB{l|*6-&KvgV*YDSTW><i=cBz9GM?160<dO
zn5)^u6KEs?CWQ0x#(Js%lpq0Cf>>2Tyi@_Gyl|7e0J=7A6LHu{h*IK(^EDd8@;Sj+
zKST~=nFKOkKq8T`3?>2efM^UlbrqIEqELw>Dv?6PlRN;rJ3wV%r#?8;oE#DYp{#XN
z#?U7(oJgsZ0YqYQaxx*AN<ieXL^6}fB$6mZ3I&fM@Cvn5399f?#p3r2ELb6si)BhN
zBE=dR!5Ab_>4igGeHTN5Y?@Z8n938HF`^2T5y=FSF{Vi%B$&p@66Nue#vuU_j)xOq
zsZxPr$<tVw2vH&m5%OQCr(gfc0GeA4XWGUGeMv}|Hla}ZC!t|XCFFx>1y3!5iJ`Cp
zNt6p<|0LAS#l~o4fS()&m57{&Ao1@j75v_1EQLmJ$GSy`r4W*=SpJ6tFbh<|UN~db
z@FaIUmBb@c02&3LP*;*@0EzSt%0VEpQ2jwD6`+&<1VwuW0+rz3f*}DQMC1t|8n8G4
zjD?9ZX)F#qokqY9iAUt9VbnY7q!%0x5GYkBL8$-^WO?Dxm<eJr1d!<<iSEvX@e~Gy
zhNs2QAUu=GbjLI39t<c(K#oBd@6WRkL87s;@6SX3=lLMH7?l<n|Brc$f-|-Rz!ode
zVyUMT!G*U?t>VSl$u0mufw8;2Z~|jlVF)+1E&j(Dct@Kof>F}{QSNsz1tL@?gL2q6
z7R}b*wi6L`o@mtJyEuq{4RP|_A2j<Ojt&Cja(V=z57R>omZFnOj*h4t>dg@h#(Yg6
z%a<p5^n8oNtr)Rqxa&&V#r6rWe7_6+`N+KH489vjM{;q=Jn}SU%qGUYbVuiw7rE_O
zZ2sjt2in2xd-BNq+~dmxlF{G0xyi|cL*9c5RrIf8J37bf9*tkU@OWH5lK8f=ukOwd
zOW1i>-!#)%Gkv+-@7vninwolE_UKGZOq`sY3<g74Sy@0qPHrwIE-r4v2Ax)`J>ip@
zG}74J-F@?*b9s5WpP%3B;o&)SXNBTbT3T9FRaKPPi}8`c%a<<?4Gld-L4@_Atva2K
z!C)khfhj2yZ(jG^JmARnI{mq8kPwFPc>LSG9)lq%IyyQkDr)D>osp3ap3Ud3sb2}@
za=E|1nbYlwiHYK2lI5b%FV5>9K8zI#A$_Alf*=PE9%PWoOqEA*PI<XREPmF~(o#PR
z06<Po4zFPa0B9tV{YkJ|J!W|R0Nr*tzxmTo8Sd@_{r#QhXUfX@`}+$60;8fFSMM`m
zW)D8PUVh=it?#Q#OP{{_<(632*VhMv;QDUfJ*Qzk^0>|`^|#txTvKy%0)a4pT$PhN
zpr2P{pJvj2uCC6BXLh2gu`xy@$|`mvlS5eH&z?O~cIsMN=OKjtJF&NlZ-3o!^JYNd
zQ8XCQ!fkzb@l_Es8+%Iw4z}d5R~$NYXvVE=Pa4gD%@6BSqnW!cxBT3+-wyh$<r6e1
zF0LYx2zz(CMnj{~3P0XsJ$v>^c1wR#Q&ZZ}B$*7mP^i&-nackquV?4P=napHRaKx+
zxGd*kWhLQdx<89`(P8hIFZ1(v*ABn#>x#JW+0vy;U#L4eVQnT_AFGD82M><%9qjG5
zo(-<D@lF}VmKMAjAHP+!`ewT|9$#_){^sU6G4?AK*J(eiO9}M6rg^<W7jo&+rR|pb
z!N>K{SC!evT+za7H*vW(kGHau><-!JLeLIbLdO1HtA5qnbs<{oTARV@UDlJ4nVCHg
z?U@1X%a<>+{M@|mmG7jipi*s4crcmyd$lLa$};>m&&ubl`s%B%WUhucuO+y9KOb^2
zy>a6Pq^T8p)9CaaBLj}@6*V<>C6O+vc_&+k1_q?`j`1&6xv*v!{x!0s*+R7NEc%-&
zmPpF;A|N_$KoS@CGB?xA%xof?yk^eH(+e#>&&&JB(42p=m49AM(B`SH9Q8P2FY9Tu
z<(*c7p?rSJcUKM{wp(~4FRx)QKP=3aToP<iup#F@zO=M-xH{#{@bbp9kx!eOn;|v~
zFSUzlZ*Tv0^v<2Iz2?;}=Es4=bJwpI^=4=rgb&VFRJ>>tTC8R{@AJ(G<iIdYW7K{j
zIibw+Fkd&nXkHcz*ZgC1?joPm-=N<Y7c^MR?MbmM|4Db4*uVN%dV2cIAN=>tPxr_;
z57zhFVlx_9w<{|$Gc&Q;VAI=0XGaDfTh%Ub_Ea?#hb1On!ftr+Q`3Isyjf_IS~`1%
za=9a`9b6jhhUmVAdzU0#?6DU9rVSfxR_ec*%ijH#|0FuzFndvqn?p@aO>0+pc=*KI
zw@sW*RQQzj$4$Jo^`im)?BJ{EGg5w=xZl>ceqlkIUO(~unV@04YX97^%<a5Rw{OI{
zqN1WRK_|kCE!NRot1-j{ahm4=vwenYkjJ8MTU*<f+xoUcXfGast-jNxrk03ombtlQ
zZ44@0{VVV4fk%41QLdXyTCLsP-Jv&k?}qat=Q?{A-mmQF=r|W|E&C?h#<jP%*I-q_
zzJUrUY#TL`dg|0E1JpF8TsLZ16x-sUMkRSl-V<-Vw6?*T>%D6ZYfno@M|1SQ`~A9p
z3dgfN3xi!)KI;mWKSJeKDC<3ylkPLa(Xl8ydsdm%U|-)#r<Fa9cPKK2qSVSdOG%bm
zEFYIeJs;eizR{+nqJraMyVCC5*K<tP=sE*-H#9b`4G;fV?iQpwSKF`_`ZOdY|6A*l
zu`%_6F8!XSk}Gmnx#}vH6Zgb5sCZ4%ai(IOv{R9oh+n>(8F^T1-!Q&?WFWut$NH`}
z!K)&|!g_jpo3+}lA&$9{9j3v3bUK}K_{jl-|Hh-$^%pOG6gKPOX=ZmsEWGRb?rO9B
zGL1&#QxNd1`c?7P$}@}EYsySbO;s9=)04hu^WTkg%8-xCi(`xTbk-REVPgXQH?b<$
HZcY0S{h81^

diff --git a/Telegram/Resources/icons/menu/stories_archive_section.png b/Telegram/Resources/icons/menu/stories_archive_section.png
new file mode 100644
index 0000000000000000000000000000000000000000..d86b5dc87df7c9354b102987cacf282e43efe99b
GIT binary patch
literal 1847
zcmbVNZEVzJ9Pf<{2V@R{FchPuB#Vdi?Rq!v8cue*-q^^BxLe@(BJ0|2*W6uO+TPoS
zun`cKL13C8*_@~(V1R(a7hnvQK%$XNKt%W;L0%$akco^T6NXQ3w+k4QNRzft+u!s1
z{omWXXyN?4@h^>sASkcAjHv|oY37-m1+L1JoqNG;oLaV2hoA|Q&C>$4e*_TOH>Ik8
z5nwB5UWs6wpwx(1BBBB`1kIh7P&qy%8gPwRC&_N4zw0OhOM)9&;$R6@^@>5MtX>oS
z^$V-``Vj9Dka=&wa}zWmh=>LUCn8~4rxR`@#Y=;=`5H&y6vPO*5sxVd2iQfhSJ6b+
zg^?&v5F|Xug>g>7KF2}LhHZq6!U+nuQ7GZ09Yr)X2TmUdkkf=(x{@hPi-9LM5;P2z
z#_@POj>RcV(duy0<#OSK4Y%1)fI#)6Y;Xxw)@P0~Frv<Dl4?kb44aHxjS@B72rzZT
zhlrY?mGyL-K*aC_r{W|=m_DU|0-wRDQ7xPj7kFF@ixE*abbuu@ST(2^iXK${g*x;3
zF#!-;md$89(UwRgBcU6<7;qyUkSC(`s-!C7m7=ahHD2_^fSEH*Z&cc=iJYNmRf-ZG
z&D5e%lVO`3D}rBJBFTah*R79i5E;%8-H4erlqf<eqKdT9b{lP@UMK7{L5x6IMUZNf
zPlQr5VSfw?YDVA;?x|pbr)w20!U2b+2v;ZKs$7S_nK05`C9G&b7+6Q8tYBHXT-FUv
z=EZWxjR2o9NfKx$!8@I`B!N1_A`T_(_8Qbl)Q~7a)YRGt&fy|Ca&$kV@KH0fqx*&b
zx&KW~0<Fb`pP9#OII|?^GD!!?N~Sx)FD_57!V;XS0-EE^>UJZ%nO0Fi(yygwj=%_Q
zJSYOv|48=;Ojl}+IH!r8IuNa=mJ<$)$IU(*@dN*-i>Ysq^z0}c3<7hR8A0G7GsHw0
zOfC(Ks5KQN13{Mla>i5ToWxg8t8ZMp))xqDX>M+=crY|H<lkO#aMi=%i$_;I1jk=@
z`$OmUL}Rg8R_mltc0s|p!^zvj_wVgr`C#b$`GD8!&B@8}`_Bvv40Jc#9Ui)~tE;P{
zt*x!mT2fMS?c2_-75DZvHCYzt3=R$!*WJ8$bZbY)^c~jT-rk168!qi?E{@}`oM0FR
zd;9PK`2t;9T6$wvrQg5o$O<clJzrpH4~9Z5*C)AL2k-V@obU5-WKQt@w)q=1U2pX@
zuDpNi%2z<xh(=F;|6yC(h3<xe7SGhFQ|GN3W(Hq4CH4GSxp?u%j?fR+PHr7DW=!kZ
z3Awq@w(pJ~&%W@<<;$0kt;?U;Qnp}0R$kWo#j#kd=v4cmL-bYERX-5qxEY5$-QC?M
zHlxSHi4&bPeI$9ikfJ8<fj&R7W5;u!av%;5TcJ%YXW?WrS=}BE279E5`S~Y*?(3Mb
z9eS}C+wSQ(&~WG1v+w3j|7qX(?y(h~2i1^JU5}z@-n8PrzP_(5Yp5^2EPQWW{;OI3
zo^yMbfq*t|Sh*Ye<>l(|&Rx4c*aTmKoz5+tot>Y}&a%EE|9D{e(166U>>m`G{rjz3
zmMc>N>vu2RUszZ;HbAuc*Y`BFww@liex_+N$lb=?)f>L?{nluK#$C!M-pXYi=C7^X
Lw~*OWQoZ_bTX&P*

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_archive_section@2x.png b/Telegram/Resources/icons/menu/stories_archive_section@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2c59be33802eaa0d1781a5f0b985d2794cc3840
GIT binary patch
literal 2395
zcmbVO3s4hx8jm(0<&9XBR}D)C0ZDd~5CSXV87O#!3e5qb)p5x#$(AG=vx^B2tpr+%
zZMj1cEa)iM6VV<Bh^Hb#rM=>`M*tP^C|ElR(xbI_AS$A_;Wh0|d!6gf?CyVefB)b2
zeZTK9Te2nbRXb}pYX*a17au21rtgsXv|34D2e)f_>D!CiI2p-c*f^Wd6^yDw>lh5?
z54bdqN|PkOazf2PP$C247}Q!C&0vIv8?=Z#3!_*Wm;%>`fctG701HP&K&n6jNwm?J
z5|7K(VXx;VO69p(@(>gV4`YQIV46UUQ3%VRR%uAsAOhxjVR~(T4Farr2$dxQBFzR_
zX_76hXhMgvLO5Ku9D=wkVF(8aMgxTcegKOH@%SLb2YGxp6buW3V7`#GcmT9H9hwOz
zi(?m!(N7{kNl{uD1oe77N6+UFIt9oL2?+rq9?0XdX#|_h(NKtits%Xa7{nMU*Wp?U
zCp0WGBa%U6QzC$Nbs+||c9~W~F6u-p1{x48$mKxhnC5|~d>N<B)~V)=qjC^aVQNf6
zku;XOjMXX$iXfH5uTU?4{+R(<TZv@Z#xMG!Rxg_%shFK~7>k1ZBAS%uXfZGuBZ+LC
z9E;gWyXkF?Mhi#lFoYs>Qi4z|WopZk%Pd|XCy3>nifd3pPx}32113f&Oaz#-#)g8}
zd`QaW!GS!O$KMDA!Vt6ol@KVNne!`9!Ox*|&7cT{{3aNc!<mFmjnDz(YD9s7T8#o=
zEz1Z;6DmSS8>YSE&wC+}!0{TALNszLUMvFWm^nC(!U&(w%Lt^)1cM+UJA@m^X9sin
zLbf0?1I2=bkbox;EbSK)@@#Wvm-eIo=l<7pINe%^>UZ;)8_rx3a2!t3$;w&mh}W?l
ziz^k*ny&&Fk(;Yq1jx;4#ZX}JHU7I5SfJG_F`D#$q<aBI5}A}9(P5DaTCLwKCy;g?
zH22{`9N>S4IREV@J-Y;_2Z4FHJc8(l<spV?=*gv{M-*!8rROBmIbIwoP3!tT?GL_F
zNo!^*X6}4cZUOLf!uP$pX5V2Y@a}<t*s^37z}?g+{%qxkzFy@Meoa`k?4a-c8AHde
zTVUNYN#--tN8DQ(yH7oTK0=!2hDnoX?CUIrqR=jAT|t7+`=zD!HZ~+lw%EU**U!|}
z0w2b<w6)DnO--db2!+Dx>gugqw~mjG+uPf_)U9@NgTum}6*Ap>dV12|ctaE#8qf-g
zL?V;P^i}@s>}+9S;q>(M=D+axd_D*^Ha9od*W<alPaZz3aM~mi{qfZG#Kgo=)VuP_
z)^xVy#|IDkhlbqOuKmL6{=~$gBS$(;pYH5Um&qn)W`qvamoG<Ue(vn-JSey=zuMoQ
zlA4N7jErnksXlQX%+Jq{@O*mw%o(ZCGB0oT?Af!8jiB%Owzl_6O3HeC{QWoAU5)I!
zawT4;Yxc9*$l>hH&ws^aV{2PpdwFOmPoY@l|4p|~GZI{Ocwk`Q8?^QdJw^47oRa#J
zo~5Isnf)a#ei~G(g=B`e-r5@-;OAH0-rnBQ!Ve4Ec69XaUF<~r#fwvulTMLJrP8Wv
zWMt%WSJzGS<jIrWw+d)ADk@yOs+uhS85|Jp>z<l=qqun02P@b?u1t8Z#RbCguQzYX
z(3iH&orYbn^z|j(OR9S@IXQU{U1M!=<<97+rG>?(uZ^!Q`9Qek?wvb-hQgRmZZE(5
zE^Ut+WmSAP-Y@LOe|`Hc4~93h$HvBFGMUE{WZl74`{w56j8=X4dRN!mJ1ura6<xS+
zVP;0JyHr)>R(iX;*KufM#6|p`$BCMn$N(?KCPQ|1%+|PeRY+@7Q`t&kxH|}f$*CzR
zJ_U|@3eUH;#<bIEj4iILtlYn+u5N=vO?P+P>VNd~RK&iQ(t7S(w^!#Q!Et`A=ifUU
zTCU6P@y@siy^hw_8kyGC)?(+)5=nbUhr+vv)AetY#LCZDyS}-(Z*SS#Upme@I5^nd
zJldyB-|k)1Y1L8FKo?F?5pz7zbM<QD>EkBL4&|C3qOD5Y-QBNUx^$^@5B^<K@rufd
zia77E;L<%_Ugg(Md{@6irLwKrCxjkOPfxDTi+ldqQePXNknr*Odg;lAhTXe%eGc}s
z3ET_Lwu+31VE7%bu8yve+`4h&#<{$M;peTHC+=<X@Gu@r;nqKOw0iSU(veR8w2c>6
zeA;WYsCtLhUewA=U_Wr}RL~;F3_GxOcEF>@kCSfa@7Pf=)N}yc%WO@T)UQ1ddMNeU
zlds-B;De&5ho@)RwsK;4c=+74^zWMr_Iy@U6!DUO<f-fSwzk&>1}e(SS;Wzlr|o0w
zES;R3_V3^S<cwkcCy%nCt`;0TF7+3<*?nzH9{ke=(Zo0?8h3rCv9a+@&fi9l)!79-
b?Rd0mLt_MWZ1Q7A^Y2f5Orp3UYP<120HT}I

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_archive_section@3x.png b/Telegram/Resources/icons/menu/stories_archive_section@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..f6369c74cd52818298a5dcf01c0280391ccde761
GIT binary patch
literal 2952
zcmbVO2~-o;8V)`{1kox|u(E`>fJ^p-kdZ|sNKhk70z?bNAu}N%kcmlvB%&xNi-H2B
zE>%#Z;!y;Ja}=uxNCN^Xh;0?csI?aQEC{F=K(KuiL{8h|dp)l?XEJy0eE0j;?@zLW
zHw9Q*eqxEiV5|e#tWfmMHeMDp(C_YpOFlwxvt;be3Jk`|#(0@vzC3P^!I%z+d3+_G
zvk?#=2?P*AVqk(QL589+7*8LS3=~LUB{l|*6-&KvgV*YDSTW><i=cBz9GM?160<dO
zn5)^u6KEs?CWQ0x#(Js%lpq0Cf>>2Tyi@_Gyl|7e0J=7A6LHu{h*IK(^EDd8@;Sj+
zKST~=nFKOkKq8T`3?>2efM^UlbrqIEqELw>Dv?6PlRN;rJ3wV%r#?8;oE#DYp{#XN
z#?U7(oJgsZ0YqYQaxx*AN<ieXL^6}fB$6mZ3I&fM@Cvn5399f?#p3r2ELb6si)BhN
zBE=dR!5Ab_>4igGeHTN5Y?@Z8n938HF`^2T5y=FSF{Vi%B$&p@66Nue#vuU_j)xOq
zsZxPr$<tVw2vH&m5%OQCr(gfc0GeA4XWGUGeMv}|Hla}ZC!t|XCFFx>1y3!5iJ`Cp
zNt6p<|0LAS#l~o4fS()&m57{&Ao1@j75v_1EQLmJ$GSy`r4W*=SpJ6tFbh<|UN~db
z@FaIUmBb@c02&3LP*;*@0EzSt%0VEpQ2jwD6`+&<1VwuW0+rz3f*}DQMC1t|8n8G4
zjD?9ZX)F#qokqY9iAUt9VbnY7q!%0x5GYkBL8$-^WO?Dxm<eJr1d!<<iSEvX@e~Gy
zhNs2QAUu=GbjLI39t<c(K#oBd@6WRkL87s;@6SX3=lLMH7?l<n|Brc$f-|-Rz!ode
zVyUMT!G*U?t>VSl$u0mufw8;2Z~|jlVF)+1E&j(Dct@Kof>F}{QSNsz1tL@?gL2q6
z7R}b*wi6L`o@mtJyEuq{4RP|_A2j<Ojt&Cja(V=z57R>omZFnOj*h4t>dg@h#(Yg6
z%a<p5^n8oNtr)Rqxa&&V#r6rWe7_6+`N+KH489vjM{;q=Jn}SU%qGUYbVuiw7rE_O
zZ2sjt2in2xd-BNq+~dmxlF{G0xyi|cL*9c5RrIf8J37bf9*tkU@OWH5lK8f=ukOwd
zOW1i>-!#)%Gkv+-@7vninwolE_UKGZOq`sY3<g74Sy@0qPHrwIE-r4v2Ax)`J>ip@
zG}74J-F@?*b9s5WpP%3B;o&)SXNBTbT3T9FRaKPPi}8`c%a<<?4Gld-L4@_Atva2K
z!C)khfhj2yZ(jG^JmARnI{mq8kPwFPc>LSG9)lq%IyyQkDr)D>osp3ap3Ud3sb2}@
za=E|1nbYlwiHYK2lI5b%FV5>9K8zI#A$_Alf*=PE9%PWoOqEA*PI<XREPmF~(o#PR
z06<Po4zFPa0B9tV{YkJ|J!W|R0Nr*tzxmTo8Sd@_{r#QhXUfX@`}+$60;8fFSMM`m
zW)D8PUVh=it?#Q#OP{{_<(632*VhMv;QDUfJ*Qzk^0>|`^|#txTvKy%0)a4pT$PhN
zpr2P{pJvj2uCC6BXLh2gu`xy@$|`mvlS5eH&z?O~cIsMN=OKjtJF&NlZ-3o!^JYNd
zQ8XCQ!fkzb@l_Es8+%Iw4z}d5R~$NYXvVE=Pa4gD%@6BSqnW!cxBT3+-wyh$<r6e1
zF0LYx2zz(CMnj{~3P0XsJ$v>^c1wR#Q&ZZ}B$*7mP^i&-nackquV?4P=napHRaKx+
zxGd*kWhLQdx<89`(P8hIFZ1(v*ABn#>x#JW+0vy;U#L4eVQnT_AFGD82M><%9qjG5
zo(-<D@lF}VmKMAjAHP+!`ewT|9$#_){^sU6G4?AK*J(eiO9}M6rg^<W7jo&+rR|pb
z!N>K{SC!evT+za7H*vW(kGHau><-!JLeLIbLdO1HtA5qnbs<{oTARV@UDlJ4nVCHg
z?U@1X%a<>+{M@|mmG7jipi*s4crcmyd$lLa$};>m&&ubl`s%B%WUhucuO+y9KOb^2
zy>a6Pq^T8p)9CaaBLj}@6*V<>C6O+vc_&+k1_q?`j`1&6xv*v!{x!0s*+R7NEc%-&
zmPpF;A|N_$KoS@CGB?xA%xof?yk^eH(+e#>&&&JB(42p=m49AM(B`SH9Q8P2FY9Tu
z<(*c7p?rSJcUKM{wp(~4FRx)QKP=3aToP<iup#F@zO=M-xH{#{@bbp9kx!eOn;|v~
zFSUzlZ*Tv0^v<2Iz2?;}=Es4=bJwpI^=4=rgb&VFRJ>>tTC8R{@AJ(G<iIdYW7K{j
zIibw+Fkd&nXkHcz*ZgC1?joPm-=N<Y7c^MR?MbmM|4Db4*uVN%dV2cIAN=>tPxr_;
z57zhFVlx_9w<{|$Gc&Q;VAI=0XGaDfTh%Ub_Ea?#hb1On!ftr+Q`3Isyjf_IS~`1%
za=9a`9b6jhhUmVAdzU0#?6DU9rVSfxR_ec*%ijH#|0FuzFndvqn?p@aO>0+pc=*KI
zw@sW*RQQzj$4$Jo^`im)?BJ{EGg5w=xZl>ceqlkIUO(~unV@04YX97^%<a5Rw{OI{
zqN1WRK_|kCE!NRot1-j{ahm4=vwenYkjJ8MTU*<f+xoUcXfGast-jNxrk03ombtlQ
zZ44@0{VVV4fk%41QLdXyTCLsP-Jv&k?}qat=Q?{A-mmQF=r|W|E&C?h#<jP%*I-q_
zzJUrUY#TL`dg|0E1JpF8TsLZ16x-sUMkRSl-V<-Vw6?*T>%D6ZYfno@M|1SQ`~A9p
z3dgfN3xi!)KI;mWKSJeKDC<3ylkPLa(Xl8ydsdm%U|-)#r<Fa9cPKK2qSVSdOG%bm
zEFYIeJs;eizR{+nqJraMyVCC5*K<tP=sE*-H#9b`4G;fV?iQpwSKF`_`ZOdY|6A*l
zu`%_6F8!XSk}Gmnx#}vH6Zgb5sCZ4%ai(IOv{R9oh+n>(8F^T1-!Q&?WFWut$NH`}
z!K)&|!g_jpo3+}lA&$9{9j3v3bUK}K_{jl-|Hh-$^%pOG6gKPOX=ZmsEWGRb?rO9B
zGL1&#QxNd1`c?7P$}@}EYsySbO;s9=)04hu^WTkg%8-xCi(`xTbk-REVPgXQH?b<$
HZcY0S{h81^

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_save.png b/Telegram/Resources/icons/menu/stories_save.png
new file mode 100644
index 0000000000000000000000000000000000000000..68d2a12f43ea6df9c2eed221de4d6bf723cfa913
GIT binary patch
literal 729
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfyvp^#WBP}
z@NS5I$IC#Gc@rxuT3wWsv$+?#OjO#qahAPy>a{gfY*QDmaP*qEX<I<pHhnRTuBmF>
zfy)-&$e4BVv--z<cV0e!{_~}8{^wVBua@t7Kd1Qo-sg9!3qRgpce+?zn1dy6`|YHS
zC!T%^(F$FE{d}d+yi%=Vi#Y)rCUX7q^70eky?mKzB6ZMLYwEQu)9I&6r4Ai9u={2X
zA6xV7+qX}jUR)Ka6Q&{*xi&0p^;E@4euX>ll<n@#byX6ayF9tm<<o7plCQ##b^g}H
zYls+S#4lgBe*N^*OAkL>;FMw{>5;_r;F{*iyzPq%PT4RWOxSSytq|AI=S4f;mhHa%
zw(gBM!<ig2?{lrTX0v<a)-Ml|+4Zc%N_#4o;Jf$l-@kjeC2DQd+JD>P8LZ~^efTib
zN9|!j#ooB!PQC@#U)$Q)2v|?_5b1Vp+W%hs;L;$?nLct!87~3^`;SNKh`Cf%RvJq1
zgolS4)V~vFn0?kvs`vc)^IHnkZe*A+H8KSLsIhA^n0)erY#_IXM~lf!AFkG>cON=0
zzbx4qvyA1=iQm7gZRFH_Or-w)_~GC><#2(;mkOIHl?-OHdD|VVHMpwJrERXU>*w3I
zckj%zX<mo!CLcL}PB$)XVZeetj3!dP%P${{SkPP|=iCre@{HqfwcY&RfBz|ed*3sA
zw)B~D9WiHNf8&m$NhJ))wOWP-Kbk-MijR-~c&%}&mulgm??!X|qSsFITAIX>yu{#)
z-Td`YYah9NwAd4;U+Ap;|IZgmW2HCV|Jjm1*sIkuityiD+0b}c36!inUHx3vIVCg!
E02IwMNdN!<

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_save@2x.png b/Telegram/Resources/icons/menu/stories_save@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..327054159704b82183b222c48723f6fbe254f219
GIT binary patch
literal 1408
zcmV-`1%LX9P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NG>PbXFR9Fe^SXn4;Q4}`Mga_ef
z9z!xjbft_%8A9SkhEmF#SIO`oL&idp;sFolcrj#nP{s#ShGfc^v4l(9`(@vD$3Evj
z{^`0e|9SY^d#!JMYn^rWS!<nNO-*&FGN8)9zs-QUBJ|eQ);~Tj3k!?!@$voreMQ*h
zv{hGE4-O8lsHoW4+4;@6R##W$WK_f~EG%q!d6^T&-{sTOQ(0LVm)X$Ju(r0gprAlo
zTbnCa+S=CEc4TCPhH`&@-__NXmX_w@<Adjgv@xFI<Kqc<b8|B^G?dXQx(x~nIzK-r
z4vvnFGBPrBbacc?b#--zhK7jA`}=!#cD6WP1})NnCj#o%*Vn?rLVbOG>5WrTQcg}z
zVB65pAib3s5{CwHfbsVBHZn3&3@SlqWMmW_9sOlSC0KtJJ2*I8U0op>JTG^5_s`02
z-)U%QtgNgcnwOWCkdTmX_s?^fnVE@baOLIY@s!^#z&mnyc!+2g7Z;W5JS!^;%yDsX
zve6<dAKCdG5fLG1iQ|=!kPs6S<Lc_l1-QAn#m2@aCMG&MI&$T7-Pzd*=C!poDv@e2
zIXMZlxw$z$+RV(Xudk1UOMa)Pr+<E2Sa9Wl0n9L@r>7H)zrR1DlDIWBH6^b)3O@rf
zFfhQ)alMlDuaAo_2N)(?5|pvKyE`Cds7$DYgxbZ$h0BF?Wo0E%H8(fg+uK`NS>@*D
zUR+!dAr{zLT3XuK*#SU=Fkwj0<Dly3=wRd$w`h|vK!s%xnwpw7H#blX3=D8pI4%bV
z2beu@u()!-0EP(+xpF-{y`-chV`F2kP;A}S)&`>a`FRH9<m3c7uH@uoM&;@0>EYqQ
z2mvE_7&0o6trlO0u?xbpvol6TZ}jo;kx^}IY%mgRaaM@%?d^@Pl_b&ND{!bGq;wQ;
ze0==;{EV%yuP+sfEXdS>LVtRCDgygjH8?nkRF961@}Wcp2P9}dfJ)5G%`Y!6!PC>z
zLxplK$P0o-#<vd$2*7XyH~szn{Gp8Eq3Z4J1q|T8z(6XOb5T@O1g`7rYeARQ)z#4A
zs;jG$<bYO(#>PfI#>U2GYHBJ!KcBA>mZ0_%m&gYnfysteWPXhSzP!Agn9t122#!H5
zEiC}7udnmTg(c{$4-XHpVdi#<#sMOe2{0(QG?7@;)YOn@mX?<IeFF@5Rms_EYHFf{
zUoS|v+S=N}R3wPTnZ+PLe4}4#X=%YzdwaWhG@uF(4<{JRqJqrALR!->&kG~e)J#lF
z@K6Z*#Kgqd*ce&Q&dx$5$qLgxU?_65s!2kAk|X)ZIV{l=1kV9MqY04yKtjRq@bIws
zR{+@1v;F-1KKYYo6BQLj+=#qQ;K7X+7Z+pbMrSQ8Eyc5whFneziVcYg<E)$%$~8kn
z#&;)TLdTGK=u~bfl+D@M8DC_G2@R4zuThxD0^emAlDxgWMKH3d&`Qx($+N=|Mu^he
zBd>dVd$2{OrMLPTiOFzbVS&Vi?p<GB&!2u67995O?rvg&UmSb}py(Dab4f`Fso=!T
z_V#vFRaH(-4!%9Syu26(7^(<tcXt=Ph|wx-i&V9?w&IM_Tc-<5CX5?=VBqwKlraV`
z{}yjwYHBK;2nsHlMi3SnxcrPtv_vJuIDwC>fAvq95@S_SsWR|?GVliyLs;TDjFTJy
O0000<MNUMnLSTaNXMhj@

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_save@3x.png b/Telegram/Resources/icons/menu/stories_save@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..e97dac0abd2bce4a30d6e3f66ef1d63ade31d358
GIT binary patch
literal 2128
zcmV-W2(R~vP)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS?x=BPqRA>e5nt6ziT@=S>?1mR1
z`#Pj-iHt}{QMN1@4W+zDwrp9_Xe6>EBwMlzkwKI-dnmi?`!bVdG}-s{eLuOc)AcO(
zd7k<Ge#7)Wzkhz`x%Yd{J?Adxo^zfjE-w6qGZ4-|I0NAfgfkG%z#lgQ@wuSUUq60D
zix$nu$oTs8Yc7l!t08LFu0440;Pva*pF4N%&6_v>{QTX%eY<SgvauR9c5<s!sgjzS
zdgI28pN2U6_v_a$b_zsaGV8E#;lj_KKRcMBH36t!zrKC=`1p7XPD@LxTD7XZ_xH3E
zDN<zO#EGw7z0&mmkNo!S+qG-gwr$%sckbNDlP8ZFHL5{_298<|7%)Kbuy4nV8IwPM
zen(jDHMVQlE;BRJV$!EipLXxwJ!sIN^5x6>RAu(;*}9Mm7cMkw*32hw^kGAX4*l}w
zi>}bqr%#6uA6}wFiJ*hks8Qq2ojbZHR-5A*R8FLLw#tG93v@l+zkfe{`gFW{jzuRV
zB+QyMOFyO1g$fnQvB1ELXs~zhUfrtw`}gAy0}FCu;1C`^eq0IXZosKHp<>gvapOka
zswGR76ev(2ibG*cN=nL<DO0LfuO3CQ9-?r7VihsOh!G<^81h7Sr%s(X(_&cf-o5jL
z+8#<KCMLdo`BJRHRd_JumF~@(H;Yv}ckaxWFJE5K+@xpIrcJ+p|E>(<!pXr+Ws?3I
z`*7&8T*=AFzeSUA<HivH?Ao=fSg~T!QGmEftU7)AbaaFUlv1ixDe+t5$B*~J?R^6t
zI&`RQ+qOy;MjkwP&@7kus6&SiHEY%+ZXl98di3b|^XDxr?Cmyf+ElAnjqu{$y?fmF
z_wV1gVD!g=1q(iV_Dn@K1lEozf{yp52`S`s9X@>6%m8@PrcHvEbtXgW)2EN(g}8b1
zrgSL+78uRp%gU82g)1p3$*hTYFY7MiNQw;%jT<+9{P?ke?Zo!&+js2PVQ-ff*b(A3
zMC;bA1-WR^A~%@dc5xd_YURq6-PkHrsF0nVEd%G6KnS87M<LX@d-txA%(jduK8BgF
zUCBCo_N?Q0s!?AGCZ3#<eMPX>v13PaGmD+f5zdWs$Qfg@Mvfe*FEygv1|nM(QT~%X
z2Wf0&5rj|&#E+&rb?ON0#fumAlg+sz`&1YRId38m78pec0;NkQC_r4lejV8A)vNXP
zz;xffePyt&UAwyR6Nw9~XV0FFu=efS;{f^LR~-Rdk0Jztju;w=c*>M1<JRl9ed^Sy
zG8hipjcv=8Eeb~E!%(jH`^V2s2#l{^zgBV(=mzuOZc3%fl`AKLo;-OXw1ucxv7&0>
z*5Vqq^vdI!HEV>n6EHq}_^@u>x^(H|+L1n6OtGw_`W$z{88?YU{i;jwZb);x*pro&
zWk&kxH5o<~K#AaF8%om>G~fm+RjTA?R6B;XlEd@p(IYz`Xp70Pj~_oOaV1NZG?X9q
z?Aaru4jVR1=s?7fAwvYeZ{I$lV?r!nzFeG#>=XM))J)kt3i<BcI|}!9jJbv7lKX%c
z7g0&}Oe@9(FA9l=kQO0W*qJkDsz-M3h7B7qReC*%!i5WSFG(apxWtk6<cbI|aNs~$
zF=7aVER?%^`LckiWEr-uMGg}oj90H-C6KoO^he%Bkrt4}=#7EtR;^mekfTSBHuE=Z
z*pQ_Y{Nlxn&3M&IkyKdNjm?`kH*itsyL9Oicy@k7Ar<ahir5DZ954*2y88F;FIqy(
zn>WwQGHu#49S^%1->q9W!T0Lb%MAADJ$dq^l0`IxZ}dRN#aVRaz_{&6%Zms(rG_DR
zlPHE!H8UW+yC}1XET4tM`Sa%s3&g5bt1NLAPH*Ybr8>q>e#J4CmsPwd^;X@C;N*w~
zRN%~D>9yF#D>Bqlz4!)_WswO1SQrfiQMhnXkTxLwc3r%9QAs46t5>g{Pd3Y*q;SHC
zrNG4HEBZmqm@&hrK)p0jt5z+aTqYPVQAHKfdlU56J_=^sNWx1hg*TT+hzS!W{K{c^
zTDx|w;yrWbj0xwzk0WvA$`w(lMT-{xdC647uZjc+B2O+!|8hVQh?0y`^J>(nQ6NSW
zSl{2gzp^+36K7mpmo8n1vvjLIeE874dv`Ne?)9EHaYB@vGiOeqfpN_^9&S}~ACkx%
z1e1Y=3kGHfvVX)jss_RM|70{DDu`ihCquE|PqxBYH*rl^NbFBM>Ws^Ct>h!8PMwl<
zBhCuy-29l#B~JWZx^!tj;HY6Jaaz8~z?YbFjX+~l%v4`vQ5DYZFOVos9654C)(zs;
zty?{M^ax~t9ZWrR*|KHwx(KnJ@xEP1q%ADbiWMt#+n|pfJ7!L(45Jd?Jb(UN7f1z(
zOWD8@!>+Mo$8yYcZJ-HL2o#$%Y2triY94y<g%Hvuw?emnu@ND;ZPU}!Ew-u02M->s
zTeoh~q)8O(J9qBfvSrKS#fv)*#?2uTxWw6kZdsfodkh=7zV*;Vw6qYsRdFjkQm?0K
z!UuqU(%BeV;wYLgT()lADupzQRqBzj*pW96G;;s&F%<rdJjD{)zkT~Q*8)ZFm+O(t
zCC-zikeVb}CYL)dffpB%2n>0{83<<}oPlr#!WsB~%)q}(a#YeK$b41+0000<MNUMn
GLSTZXp!?bY

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_saved.png b/Telegram/Resources/icons/menu/stories_saved.png
deleted file mode 100644
index 727dc35f301f8aa11736e35bbed1c896f5cb5f02..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1935
zcmbVN3ry5z96u4A!jKtaU~04}qKMwL_vEg69*jE$-62B~Krm9;@9rvh*Rw6&LnqE?
zHW`b17%@(VY;lQ;GB;!>Xg~#2Hs4G^nat@qVo+ibO*G)X!g0|>XJ(VOU)$gJ`~BbD
zl99eBB5ZP)L?VeuOEqS~dyIIF3xQX5(honv+jv)ME-#UUM~UYs$<AF65qh7=$`NwR
z?-La3kdZW72xK0I3!){G`38@Rq>6xm6ap*b)T6!i4Jg9UdNf;Y#?7u2U}I8!99ZT{
z&!T)ql#WIX3y}F90unfYKq4N8-N_RkJvzioz_s`qLy;kfP^3qbML{IToPngU96)q3
zxs<|jIg+T8ky=`ns8-HJ6u3f(;Yv)Ql;T=Kts#_&NZ>%B97kJ-Oyk0U7<|&BHbHO^
z80L1nWp1U6<*b-or_*7$0#hiY5FzEgPJ#4DoqX&FgAwo)$G8NBbs{1oS;!U(dK8*E
z?1RG<q;>LvIKhZv9@2%$Ww_|m5Rj&VI9D-e9}=f24A_AKI0YVJ<w2~=#tJNNV_!iX
z{QQCdjIG%m)Oe{a4o6Ue7ffrQ8-akl6wPOOT>#4jJX_3Bz_bRM87q3@B2qX&3M`k!
zvi6ZoWsI1NC{!{H5}(aDY1Ylper^LWk^<19V%DU%MykZK<O)KiA>^7lxQf8>VW^p<
z8H@K7P=)pdD6AQp6v)?tX^OD0oP&f8GY--UFqhMcBEc{cDXg94pfI#fIb?;|Or$w^
zfpk(J&8SDA&oYLg2})(r=zvZmRV8W@r7F3~BGt)Na;aQSYk)?fRO<>ABm0dkRV-$9
zWIz2s_b=rb*jl9hjd{d|6H9_fWq6n@Z=fTVfz^SPok51GfFLQcy7ed}rWMd=;5GBc
z5g4X*+W;i}k8}^ic-A7gNe(1iVYFUbP8c*E6Z>%35A5GA4t;yBXGh?05Qxj*2!ap6
zAqJdqa&d4(S<C*MFOh^SN;4*BX(v(zF^Z(z+#`n%doF%!s(1JPQd2XgG&ME#bZhIE
z1)J*Xp0s>6<=L}mC69VKwnWX^zJ2@R#fw9l^Oi4PKk!8DzMnsB+O!kjZ`!>1!>5lN
zXLqVsty*QvEGsLktgKx7sAtqQW8=2&?(Pah$;`O8iH9P)7ka(k9?s|UCDk^aIu&(m
zprxe+s&X7x_{H^_qG;T9F*G6~!hicVJ}EprykY%7T~*bD6(`!;V`4tH`8HII4jUXC
zm|`>F8!jBuYPE;!>%W`Q*4g=XnMIYnXY{P4-`H6;^3b|J>WYn~D>sg<os*DoA+&sM
z?xm|&-=gE=<MWSt9$ebHq_nj3bQrwsKXBlKhxrZb^)qLVEl*BrJbU)ontRvsYMnnF
zJlHtN-tlvsQW<4BfA4zj$&)7^#Ds?S_xDd<u&w}r&7bbiKcYxXj5eLGO!+u0Jbd-L
zP0h_Uh0Uez-W$4k^J3>*XlvUv!xt(Un}DmS$!%R-k-x3{``qq&Uw;Drc;W<mbaeUo
z3PWFCUu^gtzh7T6I5h@*t#SP2_nR&kTCL?36<eL}L`7Zp|5~1xmnT<7<6RPPv8}qg
zx~tyJ-fi2LGu40d=7U?OA|G})?XK-=D8W#4<BG{S8ApBnt(~2__9Q1or!84>Yunf-
zd#kJao<8nvY&vuN_(%Java{X&cYoh8d&0bNVPPS6ddjht=XU9o8qJk6XU5wM8B=r8
z2M3-;@5oT8RG&y^bhNi`s61%zP!#MR{ngfn2Ez<>Zf<VG?$!{=^tkT!F?RnA@t2uq
LN;g)$SFrIP<9Nc5

diff --git a/Telegram/Resources/icons/menu/stories_saved@2x.png b/Telegram/Resources/icons/menu/stories_saved@2x.png
deleted file mode 100644
index 3e68c618230e69169da1a5630e61bec71deda642..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2521
zcmbVO2~-nj9*>H0xN%i1CBiOY)RhWJCMPgWbTL7Wbp$nXxo*udftX|_Bm)T%+GjX~
zbVWghRiV$KrLG0kS`>6ww~4kKUSJh;mAX_^+=}7>5-sc(uGj8sw{QEsH~GGq`ThU@
z-~XHx#jlE@dd>HuP$<-xXvrF|!>-F?D){Z(h+hGlr#?E_NTJO1c3o2_dn<e?6t_OL
zG>J??S0V~R$CN1vImR^W^Z-qv2$z}lGKB^s>2fSpjf<eir_VxkwNeBn@=+Moi?K9y
zw8em}wXBjVEE+|)5?Z#DE;J)Rfes^ObhA#28xgYz8r6${v+FSnqK`sIjR=Zx8Kftn
z@pLg^!06#jHbVi!Y<g%oQx>M=h4Q%}bPmknvS2QY!)3r>2w#A3L+RrS0_F@#6|zRM
zV%!+G6G3StsYh5WlgY$1ahZf6mBkJZ4`;y~7Kg(C2!=5WCuL>^Zd@>-L4p|-2DP44
z6FA+aQ6?ucND&06j`^U|PpZX@<8cBJW0_@o7MlsXd>RER6_Yr9hCw@OT&ZATT1<!G
zq!D1*lURKkK@!F^;wMy3zW#v$5L*<TwDF@{bh=3sMlv!JxG^4(A4MCbS$d4M1~U>F
z1_c(G31}{Gd80?f224g01}Q;kCo&a3L7C3sF$MIXL^ZA?OvbmqCxA(0BqoAfS!2Kg
z1{apHIS5aHum!;|4}sw^C`u^Rs;r-Y^7uc1f|^mvNZHSVl?p^f7<4k=uv#Ze#aMbg
z6{1gu5fKww!T<~dcHB`GD2l}3MpA|=uo#I50zNa<Y9+#E!+d@i#%IXG_$r2+$CER{
z*aAL-s|*Vls1))r6;ClCFCi2euFOuzEB{aVID;CrmQ4GL^|%_&RT4<F+6a=BHQo_x
zvGnniR!twR0z{^8RksLIxYCL#q4CG+UmSrkwWc%-DE%Ml9)lSP6={+gu!vL;t)DF?
z79h`Z_2HNwtp9d#^xgM*b^;Ctf$KOqg22t>5W{dVxeQ=L)xY#*fyq}ABZ-hEIUXe?
z2kB(<1eZCjLRDdLQ*L8k9_OHWhmc+x2sv{1K+wX-d!;MS?|h3EV2;0=|Erh}7mCB~
zZ9dy~>d`sTt8c&2HvanhRoa>QVPa&&+2OoiRx)%f;1%;~TU-B=CxykumztV_f`h$1
z+`e`=>~<(PScBtzLqkIye(N`E$eH;Eu;Stp6B9?4FA)d?xw*M#mv%ZFj=nzJVBq9F
z9~h`Ud+E|8o6W}K^Rud}YHM$Ib{6KV_mr2HpFe-TqQW1@=H%ogC*Lrcr%lVPto%(u
z|Ikq5>C<l$T7Q54zdQhQaNW9fNpUshHe0Mpb+4^$PIy9M;xotZf`T4&cU!HQCeyyX
zdt>j`9Xz;e*RH(Vd=9633o4PEyL#38kVn3~p`n2|a}AK_=;+vJv{)YY_BJPW0;ttG
zH8L`?`4*Xxk)HmKdjkVLds^T0(`YovccrCsn}zN(W;AgHg0e3+0J6)3LP_VVSDV4A
z!@)Xo<cMCAmUc(vZFe|&SFcWKbgo3v)%{o6+vQQtp`mNnuO}oVRPprpc?c3)k)54=
z?!Xj?#Bt`^bMs3Ca|d3&EL`z<$&R9;=B}=;o}Q0tYirG<(a7DMEfoyZd~B!Qy?Bw@
zwuvC_n{%YYzKfmi?(WiQH)O-Z!xB3PT}#U*g(CM1lfj_PpZ~Nhy>M0ffvT#`lEA>g
zJYHz%mx<(yl9D;j>4SwQW4H8)Z?CV>nxi}leAcI=#3tuIe8by&!HG{cHeR_BFNjA`
zs{PU9$M1hMTr!*XeE!Zg4LJe%6vZLEbVq0DUr(NVbAL}yPn>s^@ncC|{fQGc8>KSF
zH_fl4^}>Y<6t++pLJjnlw7-5WoU$Jtp-r(CQ!Ew>RQ`DvrLCi5@uI+_%uK<Q_`wm+
z!tL7^Q~p_du8YDJi5S#CUX9afahf)7ULO&$MKpEatpkS}>g!*qmq!;iH^&rgtE{X%
z<k8a70(KNdwXO4fe4yD2yYG91BZ$@QyICpP8%xx|J{eC!^6Yl|l@A_7`O{_<Z?zvf
zeAtV&y)Y=<-!nNSrCamvyGvKhIUMZi>6!g-cYlBSxef2Ule!grR@p7@X|;b|8W0em
zDC_yu3s2DNk5*JvnA7BPD01+{3oMEm67siwfBZ1G;ppnMYY#}hKhY0Y_3!`vk(!#L
z3j)86KUP;)2T$La^v#mRN@YuHYcCBhH5#H(bkWwPrs%FkA%^1id5`k{&E3DNt?Yw}
zifDvp<A3#AOUh?5&s$TcP3!IX;^xiygU9d4zhb7QrY;)DQ&vZTj9N4Q_5SRKJ_Q;o
zb=&2z!ROCm`>xiR1+37|{8aM1P0yY_b>3>-dFs@ulf?Fe_1)ca)vYc5cX+2?v$@>k
z#l^E)A|-d8)xUwe*{ED=#AWftql2p&Jo1-UYX=4gpJiu9J8U+uLnaj!7H53nd$YaW
z`ll7>wqr#_p4C=&H@EC{2{b=H@hLH!lYRAN-KR6%tS3$gnr+*7GkSKHmv>ydHvRVT
l#>OH%7cF%SjDT-L-JqJeYu5jLyX((aOynv_-SUmr{{S3>=r#ZV

diff --git a/Telegram/Resources/icons/menu/stories_saved@3x.png b/Telegram/Resources/icons/menu/stories_saved@3x.png
deleted file mode 100644
index 9fc3db0d68c9d57901062163af4889d99e93d545..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3136
zcmbVO2~-o;8V+DU5y2`_5YQMvWXVES2n3!53K3b$5<roV3`q>hq{#pQ1S{AEMG$2d
zl%-{J305oM!;QKHrR=3FBBw4?<Ozx(MG-qe<h1X!ujh5nnYnl7e)nI$|DSun&&Okx
z_Ihmu0<ntW=@tNwo~nDfI{X|sY`z*ER){@Ar3l2zwW?bUaje7;fmk-q4`f12@9i`;
zD8#ZjU=)Cr3B@oPfpBt>iCJs`0HLA)E?-1PPc&Ucqxc*;I)v<v_ZGVXJice51o$k`
zCy<>eU^{ToF3u<?84VU71RxelCX5kDX)-!`K`srRt3Kn<s09clprf~{6rz~kekfN^
z0-zkQb{IAuZ-=5fU|IGY5|vD}MG^1>A`VZ)5r`PPJ&jDE5vi!f3k|E2aH44eZl5kH
zgWu?A9t4SLI9z;uJT{()1tnaZor8k|4o|=l2pAZFktT>BmJB13nthPq21wZwz8K<z
zB9uxZD+-K-=xEr~_dW>4OJYUR#W=wc!^v1;oE;Xg@@WCcVK3ptv67et<s3E+hyjFv
z2$I5ByCtlc2ST8f2mXcVrR6^;fMe_Jy`<w$Z4nBWR7fHBIM|KFfcz<18kiskZ~=f6
zjFqqf_c+*0GnF@DnyUn0L7*fM1Y<sA%I|~8C;|yfL2U`)i#TAs)arK|05=u{(9x=_
zVek|T5g%wrpphsvJBkgSM8o6XL%l%`KRV$rphWT?pfG1REQs~DU=EuW4N8P8*kQhq
z#RYI;5f_bG3M0)Gi~%LEV%R$3f)(E0G=@kDu|#Zu;YLTpK4bZO4viG;V8<cDwmT5*
z2pBs8iHwONvj7a9NQ60QPr~B?;KP15kR7Ya?1%lF|8u{ugb$Y%E9M`|Q5Bqu5;RY~
z6i!ycVnuuge7-n~;iDE<Kx460?53mHs<Z+e^x|j!KaRkAvGF_r7WzNZ{T?O-qoH_~
z1lY=jqxH9R!okMlRCV~?58QvdxUlxSn*9KW8-Z%N)Pmrbr6vZ5;Lat1TNJ6?JO+W#
z{FULhHIR4zSy+N+NcigB9$T;QN)lDy%G!%$fw%5@Add`->ieX;a$j!ca_ikl3(q54
zcYlOLYKzJ}S02bW+~;mlA6fr%Fw6L2IClEV%sAWrUg_P-zeG$VJEy#qm)T249Jy_+
zRH>q<h_O9tO-`Yk&9~>yO-@Zs&CKZO=;-L`Mx2do*4I1_jdnFQHg<J&RaI4$mzTR7
zf*`1GaM0JzNuf}Tj*f!&FU=Aa`}glp*I%W1{XDmQeE8~BaM)QO7<;$+o1jy{dAYfV
z4jq~+@*Ef(%>4Sro0*9zy<J!Kv?!H@WI=vgc1cMIVsvI=_?F>Ua(Ss!!qL*w()|4T
zz3=8t9pjcEk^TMs2W**4=G3cK2O*b~IYU!Zp-80AFh6}PJG-O1+ibG-)TyyMc{b@(
zwhp^xe5eV&G8;BKI))1ZDHO_2b-O;E?Hm|bziu5_I<j3it?x@SkE~0{Z}Q2%j&@sm
z`(r}SJsRz<KCN~9`0<vO7GiX(AJ2$oWNe(#YE|fND3IT2Z`U(sWQ&{HN?qzivF9#a
zKwc^=G<@u65kwIV^b8OC*1UfG`upqGtDWTH_;~KiNx9y}J$v?4Ii2i3&%L^5_NX5{
zWv;Ked8?OJ@9EI)zP@yV;YS~pM@QpuILkog&X*%Ubj!E`f#Ay@wKO$@PczH2I@enI
zthUxKd3?7(u#pfH6!gf|)%E(+Xm?)N)Z`=@ong`#I~Kfi{QK9_W1%T&mxD2ysM}el
z-7ik0b(~#c^f-68w_Z4?>}`sl=-4s3JZ64wwoLh`{l`-wv%*bpj+85vo)@30sj0cU
zyYo0=o5{&Z6I0W5QG@5Xdb2Obe!{h7Y8qMEOixcsF2-%!cChA%T87r`ko0G5#~o8I
z<mVgstbO94tj2`bE9s}c9XOw3>1#deVSwEsd-1@1nfkf)hv!Qiq<(hGLOOz5f^fFB
z=H~6CE*^VtMxCmuL2T;zrK-Zq(xt;s8C0#N=J#_5bfbcz<)mritrdwxA_JQYFk$8)
zZwDK|RNC{^$qp%V_Td@VoHR5vE@*0Ic>yO}hYSl*@G@6RZOdCY+P%7Y#l^;FlE?2k
ztlPIDr*vc_nqupp;IL&)SZqfb?DgFaJ42oCo+@@W^&mHM>W>{;Z!nmqrN==!sW&MU
zJ{@_@GY=m=<TIvLp43QVhJ{5miuc{@?d{!sx%uVu=ct>-RB&6YjiY06Pxg$ZMj=W|
zZ-a@63CE}mX6ucMEBP%opSn@ya`~*N)-6(9$TdQRF-%7GzdCc~O#f(MV8g+T4E`Lc
z|61#OQKs3<(XWjuG1=#qWrft|^x)XI*)KXFH*LIfpN7_QnCgZ4d8ICU1L(O=gHuwR
zk(V_s<EEbY-*9@d54fAp2u*F|d3eYJ8?F)f?`&;tH|rTHv<Ivy)CUHhJ5sC0>LM;5
zecD_5SvAjs%l(C+UN3tQb<#cc-sPl_*Ed|~^qrnyXJ@CAPT}D;@8vp=k1&f-@Bt)X
za+3`fi|sQt(iZ%rsGQa{rF}!gpEZiBI22*n;<{3iE-M?fr9Y`e6jOv@!NCSeez1)Z
z4Wi?5PK8ybOeQ9>57)8LASgMRK5RNt&|U4@J(<4!;?rGMQ`SNLYrecHOH4#3A6QeC
znVG4pv27AXr53!o_BjNTurUy>>&T6zaF@w#yghU2S8!KHSJx+PEy>TEr6aA(iLs%f
zp>4NXzJMm~)BBozePl7@yGo@W;U1@2p<SxqCtsFpv0*K5Rd)Jli^)*AZ7h7cdi8k4
z&mn$(h-=#N=I5Le-%r1}0~b?X>+*ly7Kh~I<gnT75XFw#xsmgkQEax^L9Oad|Dl<#
z@B6N}xVR|g!Ni2`k3sj{A_H4mTg}7uHaho=<)BRFGd{*Cb36`juzGt5S@~6$*`t9A
zf`21iXIcXALF#WN49gU{sjnDGku?+g^4-qe&xV=`-g@4N#PbegNTly7D$rzdo(uPo
zHMjkQMNC>++WHrt3l$&$R6a4o=23ldMUL?bUTV@<FXl?~!~Le_Cx_vRyKp@k0B!~g
z%j*tgWaJFaWf75EMP-!She)p@=H}+<8*Gb@9W&4R;TEf*57n9E9vWKI%Q7-Cx!2!s
z^`J2}HrC(Y-=r=toyP9cNvv)6;ALfHk%-Zib#)WBGL5&yU%7H6AdHceloXfh>wChO
zD(WGA4R_uHRlJIwpg^FZaZ;hcVlbxUjrNX?8CYRdR7F@pFjCe44o5SdG9Ns9M&DgP
z#^ayAo)+vN&X3C`Z!|Y=va+hJug_0ZXB!FpYTK!UCOrvoqf4*xD!`V@K^{-`Zax{-
n84+>62N4PXfo_<;bDUk7e>?K5%S9`>>Wt5D_i<Bv5}EoNc7hCC

diff --git a/Telegram/Resources/icons/menu/stories_saved_section.png b/Telegram/Resources/icons/menu/stories_saved_section.png
new file mode 100644
index 0000000000000000000000000000000000000000..31a29d96b181bd09de6973730957e6121a0671b5
GIT binary patch
literal 774
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfvLdL#WBP}
z@M_3@?^TH+X6scvA1&xmQx#g#-Q>d3r7HMROvO?|$)erq-J%)wjU5&*9y}0f%*zyX
zwctz=;>)sN32I_pG)3ypa*@i~{W<%-&puZ<@AL0H&yzR)o1I@<d-wV0r9mre?D9>d
z0=K&;MaIU?z0NS(b4AE1AGP51*WZ@?e*HRIZ+bJ&=Esj8yDgsRp)zgy^qFxcNt<u}
z`up$Z&6z<fm+WY5ZC$*0@#)jvDT_3sN-bpM<>m9X&sLMz6{0ou^i!T6(igI}t~mO%
z=&#TDMF$fEa`kj|&mOujmDYGMsKsx2baeD$hZRl>H{8wp{kLw`ts6Hac-W@!uVS{4
z;hQMWyr{-UZSu{WWe#B)B8O&aim<vVO`JPdR>j`l{(o)c<gc&4{#w0STT5W}SvN~_
zbMx7=XIF%^Iyo*3m|&)^uFlq+*yF>rcFh_aTic0lOYY>DO<WnaIyNSzz+#T(*4?|i
zMbEM@^*#yaUUuR2*LUyVA5Ai})M>c9WAcZ!Vcf2Ue~)wYANN+9{QLLsu+>w~h;y*q
zyLa!H+2m`do_^XAZ6x#Q@xFKO-kC`8s=1!s7`1j0)1`S^w{Hgq8!HoIXO)fIjl|TO
zIn&NRpDgm^?c3b-*S#BcbXEkdY&o1b$-&Y>?+$N-Q2UvuMZf@Cd{Lvdex+FV)4zZJ
z9%q|*ZAb1O2^S^7$-kWZ{QcwO;<jzuR=12(MD%2e5mUln#i_t(QRzIIG=Kj5qe+D(
zQWJGjdSu=zc&!XM<fAz)jGv#s@aFmR=Ra25d0F!4>Oo<*AO1=Q-@aWNwRTzUvzOi7
z-7}o5=GIkIyeP3^XJ_AVB>T|sdCxz+e^}3WUhMsS^PTFSL21y_)z4*}Q$iB}6o^b6

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_saved_section@2x.png b/Telegram/Resources/icons/menu/stories_saved_section@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..02a9f4687e8b66db0e7c1f7ec8f08225009d6de3
GIT binary patch
literal 1492
zcmV;_1uOcAP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NHK1oDDR9Fe^SXn5wT@*IYGK<gf
z88SpflDUNN$&}(ka)Ag@Qid{QDpLrh$Pgu&!W9uA7hEXA4H;7?WQfn~{~z{$-t#)%
z^Pcw{=f86<&e><J=UIF0{jR;%+OLev&*x_czPAIi64P~bbZl*HO-xLF{raV^uRk&}
zvazutu{Ftw=H}+v+1azRvo9|%|Ij|m%gc15>FMd2nVIV9>U5!$$5vKW0|NszAO0IY
zK0fa3?4+cmQ1f|vdlTK+*;!#>p{AxLrIOA-TU)!au@MuKX<lDn7Zw(Ba&lZ;T=1}D
zWd)zRfq}vH_BQ4E#l?l2o11i8ffYMDJIeaszke$$D>0u)>nbWLSy@>}M@Pi1s;VlH
z6i$GkprFgkOJcOUyBi_QiN*sdDJdl<Cu4VwjEs1)Z~R6^Mm|43!<+m2`}p|yZ&;EB
z)zQ)M=H>?8oSvR~dU{I3<hza3)YSa^d__e?@eJzf>f+$Qo4dO^M6P&b;b==sOY-zf
zN=k&Im^A(U{e&487ssTRP$n-ge|UHZ%MT9^NUB_|xVpN&y}g0f(9pn@!x<M55kZ!U
z)XJ4KH8lkel82U-7CXn!&#$JYrlO((sev6y*P_S}pL~3L=yK*UKm?teo6C&gNs$=I
zMgsrz^n^me(}?&*?c3emy}G)xwzd`lg}S-9851DoBeSuf5%nZ-19^LUOV-fd-p=v(
zz&N{o{?z;X`>>82$QLn<{Qdoj6VA@g78Vw3Yis21;^LyAp&=7PA{nd`yV23n>;^(Z
zLkZH%%nV3W_2J=RBEdP5TBbntfl)jQk!^#3L+}YDwzRaAL>01EY-}w1l2BDJ-Jd^y
z*kIMw)o^HUZ;uW1q5J9(p~S?*P$7lW_q7qAEF;5VE+iy@V3MN{7#JHH<NNsdm@X7+
zJUTjxX6WGH0IZ^-qSn?{VRRB|%F4=Y4&gw8e{ymn%nBFz`ue)NyZik7Jir+l8R#$>
zS=bvQNBX7%KJAdIsw#A-?R10$<MZ?Lv9U2i)YH>rw7|(BvNzxCU9jT%`g-(0@B^Kl
zu+AS&6GstAbdMG2e~3fm!O`X1$7puZcZY<8fC6vA!^3eGV5HB@&2@Ek;RGPHG6LR(
z1qB807cHVt1W&<u%Ixj!x3;!m0iVgqNjS%YOZrQdVibIK8($p?3kySeBHrMU34|)U
zHKAGrHQIO>MT!#&`cO~~{r>$Mg1o#uTtz@cs3s;RvUA1Lj*pMS?&|6)JI%qtfouk<
z9&!&EBY}B%c(4<R7D*^LI9N{Z(-~(bCM6{i>%P9e%m|(onT8<AH*T&RPlKA88VNO#
z9nSkyKx3hpV`*uLH#PGw+PB``-s|gY+yZb}6G*tsoDXSPSs7UauH-B*zA7y(jS!-u
zqWDrNjSzDrOws3XU4w#x0@+feC2?<tBHixn>?Es285RlV0|EmB$;*wEl@+e*;Ir9A
z)6>&%Xl7=H4aBHJ%S@IsI5>#jg%Klx5DgtJ*BTlc5^!aDdU}XYXiT}hV31@Eu{dZ}
zs2d}?HZLzPvFHSh8&X|e9sTW*1V%nlii?Y>>(0>75U0D#hXGSl(~gb~5{s}P&DT~q
z5lA-V@!<-Fem)@~fxUKug1(AS@ZHzf_i2^i|AR0rFE2;;PR#+&kezW2ZE0yiEh9yZ
znrLETf*^qj2ne9$QXZh`M(e)0xk<T24Foc~lao_hTbp>v=EGUR*Ml5aU}Iy0JdP5J
u`h^ePG&VIg)z;Q>r2oKvcHn>9fqwzyfmkWzWnJ?C0000<MNUMnLSTYgVYll5

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_saved_section@3x.png b/Telegram/Resources/icons/menu/stories_saved_section@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..8de09fb323c02b8875d8a464b6417403d3aa5d6f
GIT binary patch
literal 2223
zcmV;g2vGNlP)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS@8A(JzRA>e5n|EjzOAyCnY_V%p
z>;**xJ2nJGMMXrg7i?HSMZ}JvVg(f}s9+Zb1qCaL6-BWIV#kIZ>|*b|z4v)<UncwZ
z?tZ!3yI(~3<&R`;Z|1u*b9*~wFG)#>pM(bz9!Pi~;ekvK<j5rBK;_PzyK?2q#flXx
zRH#tiym?QZI`#79%Q(!FEs)i#S06rn_|~mkuV26Z{rmU7w4WzWo|G(EGF!BWZjwrs
zDovR(<>t+snjtP^hYlT#Ioq{sck$xI<HwH=7%+gRYTW&umv!pY*|KHJ&!0bC?D+rC
zg9i_a7cXw;%HkBRXV0EZojSEZfdU4P#bu~>tbF<M3l}cz-o1N{93~ESUbt}K+O=ye
zNLJwc_wQf5dUg8r>38qm>00K@ne*-2w}vH;A3w&twQJX6u+E$vG?t7VI~H}t*@{5R
zHEY(e-U=5k>{E}vefwgqfx{&(^NA1{TmJm{w{6>IsB-V#y+MNp<;$1PVj8?Kdi3a9
zw{Gc}JU0s{dy#VG%AGrRPS@bgn>Q0COh`#du?PKEl4Ut`=up<J0$;y=eMrCn;*A<L
z!jWRtuV23wFJA224+0=MSc!O;H)P0=@SbvDdknN{)#}ryPh!=_j~}~r>*m4x=L}b=
zQbpgFA3l6ozkdBcM|}@X3l=PR<;oR#R%gzfDO0A52X9=48#Zis_3Bl^V2>U>BAbAn
zeaDU+VikKAxnE@H{(z1hJEj!){P}aunl=3iWyLjP#tgBFfK#JJjjSZ1zkl%HLD7M@
zpC?bAuyfR|U0V`a_P~xEJBCHHl5gI;IkSsx_}U6a6OuIm=(K6mv^&{NlF*1@Y{-OD
zdkwmD=^|LSZ{Mc4vd0Q7nVOnPiY)q&N!mlQ`w395UcG_>HEr5-)22-nGQ1VbmMtq=
zwro%YgTG(DexlFUuU{=4-KI?&K{<c^yfc3{W;HG+PMl~IG9NyC=-IQU!4~v#|Ni|-
ztGjpa29^DfpAt(9YumPMAQYaJVU<A5oH;Y7kMyZalqj)c#R?VzyP7`kmo8GI2oXbR
zbN1|6zhW7D8#itgOD<iy^ytwexrErcb0-O525<KMrAil7C@6wXE)4>#LY6wxD09eT
z$kBqrM!>&e!v<v-(W6wUQb7@4ym+C+u}8_W{H9NzuE!!W&(^J52SpCcPmZfJV?(pR
zavG*Ux_0f#79wkb&_8nINDF*R5jue~_o79MEKs}*1B%YiojZ2~AA-nhks?v5lLpnT
zTQ`7u^uuK0K7INGkj!9>VHhmzY)6h9ktrb3($WGgcouegdb&agknfKgHA-fsUdYfk
zz~9)00hB9OuI0;@%PbK0@89=d;8m?!Rb=<<+2g8NvU8NBD1#I!LPtpa`0;~o@vvdT
zc+C`Sg$fl;oH#-2!OtvTzI+k3(xpoatrISWN#~3pOFK>Hj+S1vY8Cm-^XJbMEX}X&
z+qd&Dvz&z$Av|6^a~H!Tda{Fwe4Ge6+dL&&+zOu#ELlsIbCD?0eNk-W60ZC%hRGiF
z?b|n3nJfnbB~3IWmo8l@m_EyMdUZ;KWhJsY;bNFPD5LYNi^ZmSvtu*pcgKtwvvTE1
zHG&{5as93rEvdb{r3Pm#!&vm<7(*<QOU_m(s|b}ci#mMr11?MP_3PKJ{4R#+XLI3;
zDA!pTWH&o{^yt8W1BK!6;luPv<uW!gckWymScmiH&yUTSc~{SX?b5j{;~3#lvNjcw
zmMmG~XFtr;GAXHI#fn~gz0Q_NH)+yD!rzDyBQkIJreYgLHlufx+2RHd9_$-C1S4^b
z0IF=;ym_+)l9yr0$;or(%n`3aJbn7KS+iyqQ&@^fw}Xi-AY2W@U!*^}RdgIkI4tn(
zMdr<$CmOIag94FEC~;cIO14_HYMgEgCIoANT{kFlSbiFmqJ$njed5Xk=h2Z#@KUiU
z3n1m@<jIqLLb<Zp9WVX^Me0BsNU4HyK_fL=u$W;GG|9uXlQD^?04BdJ&6_oAmgvM$
zx2xy8294&X;Lvpqd8D8pBoLOo6o@rW51EZv=|qlCbr_4=agSb&e)7cfqmP2Y4#ofj
zh@*+P$@$5MU#!)uR||?okAOspca`I5j$Uiks^xc9Z$7>w5Pdi<BY1f8#=L{q<)KmA
z=>=#^xwzbH+_<qsSIi605pY#!_Uzf>q?0F4{;p4gc=(quUlvXH*ep<G<YF43Mz%+A
z*R5OU!W93Z7A;zcRV<=N7IqSAfOROiluz+D#GHZnF6%JLW4iwR`-@>rK!47hIsVt3
zI(4GyKWWk=`#r-|$ps4*h;6hCvMC>TRVCU%Uf%-;4v1|KY`zgYFItWLqZKV$)ac;u
z-MiPTN#q~a$w>uC^y}TbxARuKMrfhvQJd#RT|?_LXz}Z%AK`?(LCR2z6D%)eU*mkK
zXdGD5TI$@nvoB!}CNf$aWSm#fKlNab+i?5#?fIHdf90S#DBzPt>9_iyoK>fP5ujj6
zFMrQRXf&`W77g3D<RgsTyLS`r8Z>C&r<9~e%{FV`kU+O=+0wsjqUWUfE+;yMa$Mqd
zj#0>f4jno~&(L*0pgW)o^3skQH!iA|Hmt#zw{PD*Ne*;H4Z2>vdd|tHvN+4XapMNB
znPvIt1dVWnZ~exPAJ11xQWF?fap~2l44c$C%rHMIcQDut@M_WO;8P6#Yl1Y70+Ahs
xvyNT6cG0NBJR8u2Ou_>R4<tO0@IVL;{0&@(Z)HI&Wy$~m002ovPDHLkV1ndK8zKMz

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_to_chats.png b/Telegram/Resources/icons/menu/stories_to_chats.png
new file mode 100644
index 0000000000000000000000000000000000000000..b5129521c857d6d123bf9d5c46eb00d1489357eb
GIT binary patch
literal 793
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfvMTk#WBP}
zFgZb@s$uTjx%21G_bE3vHm<I$TpPLh*~`nz|Nr~@`~Uy{+w<@9$=S@9KmYvU!_I3K
zW@Tk9_nZ6c$H&KOA~*l}^QWY=bn4WpZ{NIm_3BmL-CbWF9qs<`q2N%OrG-V!-(RIK
zFD+$aVw$lkG9sd=sHpDGkBb*BG-!4UT)c9n=KsH1UteA)4(ZdkZb@ZCdU<h~d2Wb^
ziHRvMH{WD(;RVnQ`8PK$-L`Go?Ag+KqN1X#Cf-3oLV8X8iCI}(Uyi=Iy1H>;G0=u?
zaeby_fprfrEOZuD_iHH*l&GqznmBP{!NR+{%l+rsNFI9n<jJ1u?|D^KRxxHuN{?=C
zPVedIDG+2ieCpJz=jZ1;@H@3M6!Q0&?)Y-3m3!h`1q}@jAF)-dRu!;n^t^bHu^@8Z
zym<~6V|SM=+^M0f%NzL>sOjz7w~9(S4q1m<IN8|P3Pkzk?P7M9t?laa3Jtyb@Nj#h
zV%5)2PaiyZpdY`FL6$YV?HGgPmh$)aj`zzutIU`&<I&^CjcK(%J}^qKw6(QWR#vL1
zsU1*w|N1qvmDsv?4-bwRry8>!C~5Rmetu?K^~J-@jm=d=nms;&Ir#YP?fH$F>?;h-
z%*uX$dt3hQ4x^hG%N&csMQhjc&bxkLk-X}sA3uIHNXouAyKbFc!j5m>zsK*dla(+P
zVDIeeTDEN2rcIleYNbR(Sk81F(5U|R$I|S1)6s@Y`|JO+Epzh`m64HgSiIbKwwpq?
zL&>zMQ}@>V3<Ac_Wrc)gcXyS(zQ4boGbO6SPg!|!@$++x#_R^u8HKGTbjS4UsrtHV
z(<Y(Id~=izd}4{%aCc9o@C)CHgJA&y4E#BBtO`9DoTvX;BsP8F7XfyLE?bq%<1e<{
P03}OLS3j3^P6<r_PHb1b

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_to_chats@2x.png b/Telegram/Resources/icons/menu/stories_to_chats@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..575fcc811a8eb29b71f4211668ff24f3dec87d5e
GIT binary patch
literal 1579
zcmV+`2Gse9P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NHl}SWFR9Fe^SZOF{YZ$lAScXBy
zXv{=eiXtJ|CygoF2g3(DA)kaoQTQM=mXIW-6p}5<ZlWmB9VE&!*|O_4Mn;TvX1Mp)
zee`xZ|NlAv#qHkP&G~S8-{<{3&-<L`eYW?gs60Fm7Wki8K=q#Aqo$_z?Af!YPoHXP
zYJU0h<>=_>o&uF&hY~(MJ{1)eQ&Ust=jVTLp6l!D?d|Q3j*jT)XhTCoWuz;_qo=2r
znVGq}yUWo*aQO4*&;I^?M@L76wou^6-oAa?(9mFJW=0!aTwHwo_;Gf2c4cMd`1lyx
zo0^)wc=5u*!Xh9bz|PK&V9>X<wKXp<50+C%GcmB1mR4nD<?Zb)Sz~f?GC4UJ))L{d
zva-s}&HeW68?l_6oCF64i{KSldiCnn=g*%>?#|ARudlEC%mxMq#l^*!mzQ9_xw*;7
z$&sH|IuhZtw6sLpG&eV6Djg?Eb#`{%+uI`{B_$=ISkXm9^62O&2}(&x5yi?_eE$47
z4w^*8$H&V+3oSP`Hj;?<@81hy<fp^BaDNEAy1H_AcbA{>4ymiFOJ3a4(xR#=7K{k1
zq@<+${QUIvbWcxDWGSJ|;OXS%a03yT5W7lRT3TUY;iE^7gb+`jJo)h916h&1=jP@d
z92|sL!o$M}nV6U;gi(@?gtWD_#q>ZbVtE1iIw~qkNbdCXG`RNn_qDaPg)qv{hlYkA
z7fMuBRUtJ%1Ts@hOw8foA?T;4r{?D7QstuT&}YC}QUQWQxQ<YqM@B}H(2R@>9>2A<
zH5ItNzCPX(o35^|g1xP+jZx$pzy^qj*w|P}bWcwYBrY#6OZClyf&#3fnCR>4OY(w(
zYy<$(w}^vEVf^&z6Uhw>4CF0C6)e@&)$!;wee>oGR`Cc`R)jqP5S2U$f*}eVG8h~}
zLqnspv(wGZjmL?pcsc+8yGn9_g0Lq#IUg4zrqKHOI_mb>*_n=x&cMKct*tHNgcD%w
z<>e*y$X>sGja3{O!jQ3W2j9PchgeY19dPM#jnQ==jPb2#gI>OTNjwn|5oGhMtSp`s
z8yg#P>fYX7-V%38eqB~p#wB8`L38Yc9#1GSpw9{o4TX8(<~TciTzh*v4%FY@zqGUz
z;SITH1mMCUAtC4_VNryAYHBKPGX<tyU0w9WGT(7=aU>UsadUH%;Sn<2#E85gzCBJ2
zypmbtjtl%78ymk1JO>8{KYsiGKi;er1Sd*IO-&8cgOo!LX>V^Yt_@NsfpPTWNGYpm
zvKAH=z>kfQh$Rs?G885>si=L3XQ^(^<MQzEAcZ8uo`>X5(Y%h2j}x<?dHGDT=Sn6<
zb#?Xb?rzcs&MSK#xe+3z<1Wgiih>e|8Z0$6HDzaKGYs50EG*2$#l^(L1WGeH8=&Bz
zXB`<C85|r$%3yRFAWI=NV2t|1TaqzFVuX1h6v>*P7@%p#eq=V8Z(MzXf`WM4h_@eN
ztsqj4tE(#t7Oo8tib4qG{QUfA14Jb%0OF3fAErh|MovynbaRE$=*MeoYYB#y133(1
zG@14F^^&{_P^zn|$s}WAW0sbdGC6UqU%!3@%dcO*yuH1FXD#gF;-WmkD7FO(8RSAb
zZmTkDDiefu8z~!HGcz-2t*A%9i7V6OGoduV#RGCtq}WxO2N0U#yugTOetw<?EY}OM
z$VI1YgtuI14hSDWdeHk`7e$U(75w;tM6^drT&1bD;QcpTrI}9X(b$I$4-db4_m1TP
zhT#r)KO4{+B_t$p0YYQ3u;=k0V#~|RnGeBczy0-~3&R+$`p^B2VVdDOi;iq<ZH-w=
zWB{uu@xzMkrN97T4^Jv8Dw>#>pkFOat^5DdQ{c#?s!Vq`V`JmTk01YQKRu;t_CS2F
dz<<R8e*&JJNyHuN_KW}k002ovPDHLkV1jKT-LL=v

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/menu/stories_to_chats@3x.png b/Telegram/Resources/icons/menu/stories_to_chats@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..00f6b959b4a327ff51ebf6d8133cffb42a02694b
GIT binary patch
literal 2274
zcmV<82p#u{P)<h;3K|Lk000e1NJLTq002k;002k`0ssI2+K(g<00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IS@Oi4sRRA>e5nq|lpO%#Cb?r!W3
zRM5{r5d#cFvBmlnQGX}~*o7@9w!$l7w}OS;9Y1Uo8xdQvJHCf^@i6b~-n+AVcl{LZ
z{d3REoO9-!*_l%_$&$r?@fC=#Kzs$_D-d6Szjp<ahb?;Q)T#65&z~*vmoa0;_wV1o
zef#$L^XCs9Jox$ZXLz%RqFBL#1zWakS+i!%DpjhKFJC@QnlyF=K79Ca^XAR7XU`rw
zbm-v0gHN73vE%$HJ$#iRLx!9_efr0bAAAk--}mp|4;(nqqeqVn88ZAS%2;v~DN<zC
ztXb?ab7dScA3l88r%#_$sZzzRUaVU&ZQ8UWMvNFfd^mt64d3m?jT;v)Uc7qs>WddI
zUcY|L0ppuw&YYQrC{w0PwQALh6)UDu(S&N`$dQ{jZ?+Hprk(~38gNduwSV{S-G&Vt
zI(6!lHEUK^IVx7HIBL`=PMUr9o;r1^Xwjms9D+v}Hf-3pZ{Jj{9zA-*N@vfW-N$5w
z3Kd$oaN*alUsZ<Bo;|ByzrGJ{K1tH2Prq^FMpc53A3u&BJ=(q-y-rZ7R4Kl;`t`ni
z`O>#<U$1=p;HOKMZr{Frs#X}Z;>C;m;T3@fKXK>I9d(KY3l=0#9;-QsFk50bTJ71h
zN1bxov}q|*ri@18Yb<BZocK3&qDhk``Ql_ow07-UnT+#*g)pP@gN4!OL&<aiVNKV+
ze}9<<v(c_yJ3sPzq!~MQtW3&Du2G|gM+R=_aO51z99_G1b)yJueD&(pGA|yS$TKk3
zFj~2CrEs4!XO36aDN>}sDb=oByH1@tIAxmV95~o#&YY3imo8muPV0^-TjKcf;{@#P
z1ivuKl`FS+@nT|JkqqGJ)2B<8EU8eT!Y?eBf4&mm%Hlv^S*cPb7XtHPOkcl#{cy@A
z@r1>epnmS$xtbGK)k8#_vZ43u)~%b+?%K7>T=IZ12?&oJJ0_w;0ZyJgX|h=C-U}Bl
z2%m-x8wM<~2|I+?v15k_0dVWqt*KL|cJJPug`YBIiWXt+-o4wz88&IsM0m0sCXxh<
z!=@Dp*otVP<(f1nOqifXh64r+FcCrxddLq<NCLwll2lDa$BrE%v2N3*jp$Fjn<-PK
zNSM-(j~0^2lP61mOaPJl)~#EcHf<W6OY7FHW$tzB)<s8Fy-54^?V}?z%4rmLdinBY
zSA2H_5tsebym|8|1Q>Tw%r&7RTaj3P*|KF(*vgOus}SbNlSleBz|>KSQ6_{Vv#3?8
zmc~e$k`&*)d-vYGd(!U+P^-d_SVKbSh$)?0wrsIr;{AH{>Pf!_m@2ku(<W+37BT5P
zMi0-c@v@q^a^<oBj*TON)+uO2^xBYPbN~MR(oYFYU=1);4CBUbJb(VYMV})_4z>gv
zNn=NonxWMhWuK^2f<|3L?T-YLs&UM$(XwUBMndz}pEqyb+qZAa5u!?CF9akcBnX1*
z?kZfkuppZUf!D8Jx2R5>IN?o!DA*-Rlu)fXtg-JZeydikMAr4|*Q-NzSdHAx^B}yl
zHN~Y%m+U<K_fR%_^X836zkU05wGxP)j~_pl(axPaN16#u1dXtm0(c}$tzQfy_DLI(
zW|Gn*XNC`AQe7B7e*E<5(=B~kv}nQh>)ErX#gufA(M5|ES<MwIR#dKBdEB^hpFe+=
zS&2O889H>R^h}&Mk*cFKg9NCyt1*tOr6JI}cW;q~SaI97Z7Lxro`v`~@kg12&`}-T
zxpSvUtSJ1wZN)Wf)~J(th4BY6F#ys1kt0V;n2_LQ$&y8-b%nhd53+p|IU?Z<9z0kR
zh<tSR?AcLv0WM_Tym?xDtTC}S`L^t8OAJ1S9l`2JQmm_5wW^_Kz>bd@Glrzy;)4Ca
z%aJ(Rry&MMXZCr%eEDP)yUJeW9^=}zYtm^5P*kP3Zy|F~$r20+-O);xEU8+2_L64t
z?JO`zTtxhn_yh1%^U%1dIZ@lTZM9MVO%`7D(O0fqvHNKM+zlwj)<+M54LB(%oN}Tn
zIcE3o-*=3v6pDaBHEjvZI}_j&2a8t3gpuOlfVENBnJ1b_9JIng2RS6GR;}Vf%o^YZ
z8Ncj!^mFIV-MDdMGBIjqt`-mb#^n>)&ECCxO@d$v0}un(uS@Bptww-Y@Mg`L*_qU>
zTbG*`Y@>Wta{$wiZc)-arVFK*BYS`&OQlKEiZCxm|1L|geED)m8yi;n4YxRd{`_<2
z&QU_sv?7;LEsBWf8(<d6R%Nc=T`<L>NaSW|a`fm?gy4+19x#Lk2VYMJN&2HM#Noq-
z)k1oRC2m<c4VW=wh6u#9dWQ}jGz9Zzmo8nfNy1I4YvEo>ILgOok+AL4r%zm_IHH+%
za+NOCHI(Fq5437B8vHeqqi{24&XffnG-!|`j@M3JjR;Rl$a2d=XDAncj`B%tMHS8D
zL^(3iI@x#<h`xOJQj`P;>#A0hfPEsfl6`2Su{70^t4!hba^1B2W*4+TfdbM=^dNBT
zz^hlUG$vf5ktJ!JVKphW%W@Kna30L7F_C1l9<T^o0671q2P`5Jep!p2IB8s5s0pPY
zy8>hh_9sVZ)u>S;SK8nau+G{O8f%TIe^6R8x!#ZRNQ`KPl9_LzlavO0a3MYzIB+0F
z+3|J@wTVke+lbsrbErnjv%b=$OINR6-LX{s|3B-Kg}{&yN%A65Jsv*b03TwCYykim
zk6yic@%YfIN}ScepU%d*6&=@uat*ne_2V&zW9cUB$IhWBrfQ2;RC`E6_)$egA7W+h
w#KL}_uF4RH<0}wff%po<S0KIuf7=TD2kP{7QG~8vUjP6A07*qoM6N<$f~T7|egFUf

literal 0
HcmV?d00001

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index 253b789ce..243c36b43 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -477,10 +477,10 @@ void FillSourceMenu(
 			controller->showSection(Info::Stories::Make(
 				peer,
 				Info::Stories::Tab::Archive));
-		}, &st::menuIconStoriesArchive);
+		}, &st::menuIconStoriesArchiveSection);
 		add(tr::lng_stories_my_title(tr::now), [=] {
 			controller->showSection(Info::Stories::Make(peer));
-		}, &st::menuIconStoriesSaved);
+		}, &st::menuIconStoriesSavedSection);
 	} else {
 		add(tr::lng_profile_send_message(tr::now), [=] {
 			controller->showPeerHistory(peer);
@@ -508,7 +508,7 @@ void FillSourceMenu(
 		if (in(Data::StorySourcesList::Hidden)) {
 			add(tr::lng_stories_show_in_chats(tr::now), [=] {
 				toggle(true);
-			}, &st::menuIconAddToFolder);
+			}, &st::menuIconStoriesToChats);
 		}
 	}
 }
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index 21687e562..eb746c3c5 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -176,12 +176,12 @@ infoTopBarDelete: IconButton(infoTopBarForward) {
 	iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }};
 }
 infoTopBarSaveStories: IconButton(infoTopBarForward) {
-	icon: icon {{ "menu/stories_saved", boxTitleCloseFg }};
-	iconOver: icon {{ "menu/stories_saved", boxTitleCloseFgOver }};
+	icon: icon {{ "info/info_stories_to_profile", boxTitleCloseFg }};
+	iconOver: icon {{ "info/info_stories_to_profile", boxTitleCloseFgOver }};
 }
 infoTopBarArchiveStories: IconButton(infoTopBarForward) {
-	icon: icon {{ "menu/archive", boxTitleCloseFg }};
-	iconOver: icon {{ "menu/archive", boxTitleCloseFgOver }};
+	icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }};
+	iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }};
 }
 infoTopBar: InfoTopBar {
 	height: infoTopBarHeight;
@@ -251,12 +251,12 @@ infoLayerTopBarDelete: IconButton(infoLayerTopBarForward) {
 	iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }};
 }
 infoLayerTopBarSaveStories: IconButton(infoLayerTopBarForward) {
-	icon: icon {{ "menu/stories_saved", boxTitleCloseFg }};
-	iconOver: icon {{ "menu/stories_saved", boxTitleCloseFgOver }};
+	icon: icon {{ "info/info_stories_to_profile", boxTitleCloseFg }};
+	iconOver: icon {{ "info/info_stories_to_profile", boxTitleCloseFgOver }};
 }
 infoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) {
-	icon: icon {{ "menu/archive", boxTitleCloseFg }};
-	iconOver: icon {{ "menu/archive", boxTitleCloseFgOver }};
+	icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }};
+	iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }};
 }
 infoLayerTopBar: InfoTopBar(infoTopBar) {
 	height: infoLayerTopBarHeight;
diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
index 7fb64b9d2..8a06efa5c 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -1029,7 +1029,9 @@ void ListWidget::showContextMenu(
 					? tr::lng_mediaview_save_to_profile
 					: tr::lng_archived_add)(tr::now),
 				crl::guard(this, [this] { toggleStoryPinSelected(); }),
-				(pin ? &st::menuIconStoriesSaved : &st::menuIconArchive));
+				(pin
+					? &st::menuIconStoriesSave
+					: &st::menuIconStoriesArchive));
 		}
 		if (canForwardAll()) {
 			_contextMenu->addAction(
@@ -1070,7 +1072,9 @@ void ListWidget::showContextMenu(
 					crl::guard(this, [=] {
 						toggleStoryPin({ 1, globalId.itemId });
 					}),
-					(pin ? &st::menuIconStoriesSaved : &st::menuIconArchive));
+					(pin
+						? &st::menuIconStoriesSave
+						: &st::menuIconStoriesArchive));
 			}
 			if (selectionData.canForward) {
 				_contextMenu->addAction(
diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style
index 853af9e43..2fd6327a4 100644
--- a/Telegram/SourceFiles/settings/settings.style
+++ b/Telegram/SourceFiles/settings/settings.style
@@ -93,13 +93,7 @@ settingsIconThemes: icon {{ "settings/palette", settingsIconFg }};
 settingsIconGroup: icon {{ "settings/group", settingsIconFg }};
 settingsIconChannel: icon {{ "settings/channel", settingsIconFg }};
 settingsIconUser: icon {{ "settings/user", settingsIconFg }};
-settingsIconSavedMessages: icon {{ "settings/saved_messages", settingsIconFg }};
-settingsIconStories: icon {{ "settings/stories", settingsIconFg }};
 settingsIconKey: icon {{ "settings/key", settingsIconFg }};
-settingsIconReload: icon {{ "settings/reload", settingsIconFg }};
-settingsIconNight: icon {{ "settings/night", settingsIconFg }};
-settingsIconSettings: icon {{ "settings/settings", settingsIconFg }};
-settingsIconArchive: icon {{ "settings/archive", settingsIconFg }};
 settingsIconPlus: icon {{ "settings/plus", settingsIconFg }};
 settingsIconMinus: icon {{ "settings/minus", settingsIconFg }};
 settingsIconTimer: icon {{ "settings/timer", settingsIconFg }};
diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style
index 3c697f316..183026881 100644
--- a/Telegram/SourceFiles/ui/menu_icons.style
+++ b/Telegram/SourceFiles/ui/menu_icons.style
@@ -104,8 +104,15 @@ menuIconNewWindow: icon {{ "menu/new_window", menuIconColor }};
 menuIconChatBubble: icon {{ "menu/chat_bubble", menuIconColor }};
 menuIconPhone: icon {{ "menu/phone", menuIconColor }};
 menuIconChannel: icon {{ "menu/channel", menuIconColor }};
-menuIconStoriesSaved: icon {{ "menu/stories_saved", menuIconColor }};
+menuIconStoriesSavedSection: icon {{ "menu/stories_saved_section", menuIconColor }};
+menuIconStoriesArchiveSection: icon {{ "menu/stories_archive_section", menuIconColor }};
+menuIconStoriesSave: icon {{ "menu/stories_save", menuIconColor }};
 menuIconStoriesArchive: icon {{ "menu/stories_archive", menuIconColor }};
+menuIconArchiveOpen: icon {{ "menu/archive_open", menuIconColor }};
+menuIconGroups: icon {{ "menu/groups", menuIconColor }};
+menuIconSavedMessages: icon {{ "menu/saved_messages", menuIconColor }};
+menuIconNightMode: icon {{ "menu/night_mode", menuIconColor }};
+menuIconStoriesToChats: icon {{ "menu/stories_to_chats", menuIconColor }};
 
 menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
 menuIconTTLAnyTextPosition: point(11px, 22px);
@@ -126,8 +133,8 @@ mediaMenuIconDelete: icon {{ "menu/delete", mediaviewMenuFg }};
 mediaMenuIconShowAll: icon {{ "menu/all_media", mediaviewMenuFg }};
 mediaMenuIconProfile: icon {{ "menu/profile", mediaviewMenuFg }};
 mediaMenuIconReport: icon {{ "menu/report", mediaviewMenuFg }};
-mediaMenuIconSaveStory: icon {{ "menu/stories_saved", mediaviewMenuFg }};
-mediaMenuIconArchiveStory: icon {{ "menu/archive", mediaviewMenuFg }};
+mediaMenuIconSaveStory: icon {{ "menu/stories_save", mediaviewMenuFg }};
+mediaMenuIconArchiveStory: icon {{ "menu/stories_archive", mediaviewMenuFg }};
 
 menuIconDeleteAttention: icon {{ "menu/delete", menuIconAttentionColor }};
 menuIconLeaveAttention: icon {{ "menu/leave", menuIconAttentionColor }};
diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp
index 13f4e45bf..14f9a1cd1 100644
--- a/Telegram/SourceFiles/window/window_main_menu.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu.cpp
@@ -587,7 +587,7 @@ void MainMenu::setupArchive() {
 		inner,
 		tr::lng_archived_name(),
 		st::mainMenuButton,
-		{ &st::settingsIconArchive, kIconGray });
+		{ &st::menuIconArchiveOpen });
 	inner->add(
 		object_ptr<Ui::PlainShadow>(inner),
 		{ 0, st::mainMenuSkip, 0, st::mainMenuSkip });
@@ -734,13 +734,13 @@ void MainMenu::setupMenu() {
 	if (!_controller->session().supportMode()) {
 		addAction(
 			tr::lng_create_group_title(),
-			{ &st::settingsIconGroup, kIconLightBlue }
+			{ &st::menuIconGroups }
 		)->setClickedCallback([=] {
 			controller->showNewGroup();
 		});
 		addAction(
 			tr::lng_create_channel_title(),
-			{ &st::settingsIconChannel, kIconLightOrange }
+			{ &st::menuIconChannel }
 		)->setClickedCallback([=] {
 			controller->showNewChannel();
 		});
@@ -752,10 +752,7 @@ void MainMenu::setupMenu() {
 					_menu,
 					tr::lng_menu_my_stories(),
 					st::mainMenuButton,
-					IconDescriptor{
-						&st::settingsIconStories,
-						kIconDarkBlue
-					})));
+					IconDescriptor{ &st::menuIconStoriesSavedSection })));
 		const auto stories = &controller->session().data().stories();
 		if (stories->archiveCount() > 0) {
 			wrap->toggle(true, anim::type::instant);
@@ -776,32 +773,32 @@ void MainMenu::setupMenu() {
 
 		addAction(
 			tr::lng_menu_contacts(),
-			{ &st::settingsIconUser, kIconRed }
+			{ &st::menuIconProfile }
 		)->setClickedCallback([=] {
 			controller->show(PrepareContactsBox(controller));
 		});
 		addAction(
 			tr::lng_menu_calls(),
-			{ &st::settingsIconCalls, kIconGreen }
+			{ &st::menuIconPhone }
 		)->setClickedCallback([=] {
 			ShowCallsBox(controller);
 		});
 		addAction(
 			tr::lng_saved_messages(),
-			{ &st::settingsIconSavedMessages, kIconLightBlue }
+			{ &st::menuIconSavedMessages }
 		)->setClickedCallback([=] {
 			controller->showPeerHistory(controller->session().user());
 		});
 	} else {
 		addAction(
 			tr::lng_profile_add_contact(),
-			{ &st::settingsIconUser, kIconRed }
+			{ &st::menuIconProfile }
 		)->setClickedCallback([=] {
 			controller->showAddContact();
 		});
 		addAction(
 			rpl::single(u"Fix chats order"_q),
-			{ &st::settingsIconPin, kIconGreen }
+			{ &st::menuIconPin }
 		)->toggleOn(rpl::single(
 			_controller->session().settings().supportFixChatsOrder()
 		))->toggledChanges(
@@ -811,21 +808,21 @@ void MainMenu::setupMenu() {
 		}, _menu->lifetime());
 		addAction(
 			rpl::single(u"Reload templates"_q),
-			{ &st::settingsIconReload, kIconLightBlue }
+			{ &st::menuIconRestore }
 		)->setClickedCallback([=] {
 			_controller->session().supportTemplates().reload();
 		});
 	}
 	addAction(
 		tr::lng_menu_settings(),
-		{ &st::settingsIconSettings, kIconPurple }
+		{ &st::menuIconSettings }
 	)->setClickedCallback([=] {
 		controller->showSettings();
 	});
 
 	_nightThemeToggle = addAction(
 		tr::lng_menu_night_mode(),
-		{ &st::settingsIconNight, kIconDarkBlue }
+		{ &st::menuIconNightMode }
 	)->toggleOn(_nightThemeSwitches.events_starting_with(
 		Window::Theme::IsNightMode()
 	));

From a79deb89cebaaed08ea862591796926714df10c6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 3 Jul 2023 11:40:29 +0400
Subject: [PATCH 135/259] Update API scheme on laydr 160.

---
 Telegram/SourceFiles/boxes/reactions_settings_box.cpp | 3 ++-
 Telegram/SourceFiles/data/data_session.cpp            | 3 ++-
 Telegram/SourceFiles/data/data_stories.cpp            | 2 +-
 Telegram/SourceFiles/main/main_account.cpp            | 3 ++-
 Telegram/SourceFiles/mtproto/scheme/api.tl            | 3 ++-
 5 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
index 7ddcd57ab..f0282306b 100644
--- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
+++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
@@ -62,7 +62,8 @@ PeerId GenerateUser(not_null<History*> history, const QString &name) {
 		MTPstring(), // bot placeholder
 		MTPstring(), // lang code
 		MTPEmojiStatus(),
-		MTPVector<MTPUsername>()));
+		MTPVector<MTPUsername>(),
+		MTPint())); // stories_max_id
 	return peerId;
 }
 
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 02437c67c..ea176d2bf 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -4258,7 +4258,8 @@ void Session::serviceNotification(
 			MTPstring(), // bot_inline_placeholder
 			MTPstring(), // lang_code
 			MTPEmojiStatus(),
-			MTPVector<MTPUsername>()));
+			MTPVector<MTPUsername>(),
+			MTPint())); // stories_max_id
 	}
 	const auto history = this->history(PeerData::kServiceNotificationsId);
 	if (!history->folderKnown()) {
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index dfb5129e5..b5dee6659 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -610,7 +610,7 @@ void Stories::finalizeResolve(FullStoryId id) {
 void Stories::applyDeleted(FullStoryId id) {
 	applyRemovedFromActive(id);
 
-	_deleted.emplace(id).second;
+	_deleted.emplace(id);
 	const auto i = _stories.find(id.peer);
 	if (i != end(_stories)) {
 		const auto j = i->second.find(id.story);
diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp
index adef5d1c3..0eae6e91b 100644
--- a/Telegram/SourceFiles/main/main_account.cpp
+++ b/Telegram/SourceFiles/main/main_account.cpp
@@ -171,7 +171,8 @@ void Account::createSession(
 			MTPstring(), // bot_inline_placeholder
 			MTPstring(), // lang_code
 			MTPEmojiStatus(),
-			MTPVector<MTPUsername>()),
+			MTPVector<MTPUsername>(),
+			MTPint()), // stories_max_id
 		serialized,
 		streamVersion,
 		std::move(settings));
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 20a0dd4f4..b8771b61c 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -81,7 +81,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 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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> = User;
+user#abb5f120 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_hidden: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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> stories_max_id:flags2.5?int = 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;
@@ -2097,6 +2097,7 @@ stories.getPinnedStories#b471137 user_id:InputUser offset_id:int limit:int = sto
 stories.getStoriesArchive#1f5bc5d2 offset_id:int limit:int = stories.Stories;
 stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector<int> = stories.Stories;
 stories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool;
+stories.getAllReadUserStories#729c562c = Updates;
 stories.readStories#edc5105b user_id:InputUser max_id:int = Vector<int>;
 stories.incrementStoryViews#22126127 user_id:InputUser id:Vector<int> = Bool;
 stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList;

From 451c4e3101c0eefac461a679ce2aa728f4a88f35 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 4 Jul 2023 00:05:11 +0400
Subject: [PATCH 136/259] Implement vertical list of hidden story sources.

---
 Telegram/Resources/langs/lang.strings         |   3 +
 Telegram/SourceFiles/boxes/boxes.style        |  10 +
 Telegram/SourceFiles/boxes/peer_list_box.cpp  |   9 +-
 Telegram/SourceFiles/boxes/peer_list_box.h    |   3 +
 .../boxes/peer_list_controllers.cpp           | 404 +++++++++++++-----
 Telegram/SourceFiles/data/data_stories.cpp    |  21 +-
 Telegram/SourceFiles/data/data_stories.h      |   7 +-
 .../dialogs/ui/dialogs_stories_content.cpp    |   8 +-
 .../dialogs/ui/dialogs_stories_list.cpp       |  19 +-
 .../dialogs/ui/dialogs_stories_list.h         |   5 +-
 .../stories/info_stories_inner_widget.cpp     |   2 +-
 .../SourceFiles/ui/effects/round_checkbox.cpp |  56 ++-
 .../SourceFiles/ui/effects/round_checkbox.h   |  10 +-
 13 files changed, 417 insertions(+), 140 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index fa47c0a99..a816e9883 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2407,6 +2407,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_add_contact" = "Create";
 "lng_add_contact_button" = "New contact";
 "lng_contacts_header" = "Contacts";
+"lng_contacts_by_online" = "Sorted by last seen time";
+"lng_contacts_by_name" = "Sorted by name";
+"lng_contacts_hidden_stories" = "Hidden Stories";
 "lng_contact_not_joined" = "Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\n\nWe will notify you about any of your contacts who join Telegram.";
 "lng_try_other_contact" = "Try someone else";
 "lng_create_group_link" = "Link";
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index 4dba7e723..4ba5c01b5 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -942,6 +942,16 @@ requestsBoxList: PeerList(peerListBox) {
 	padding: margins(0px, 12px, 0px, 12px);
 	item: requestsBoxItem;
 }
+contactsWithStories: PeerList(peerListBox) {
+	item: PeerListItem(peerListBoxItem) {
+		checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
+			check: RoundCheckbox(defaultPeerListCheck) {
+				size: 0px;
+			}
+		}
+		nameFgChecked: contactsNameFg;
+	}
+}
 requestsAcceptButton: RoundButton(defaultActiveButton) {
 	width: -28px;
 	height: 30px;
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index 2ceba422b..95920addc 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -33,8 +33,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/style_dialogs.h"
 #include "styles/style_widgets.h"
 
-#include <rpl/range.h>
-
 PaintRoundImageCallback PaintUserpicCallback(
 		not_null<PeerData*> peer,
 		bool respectSavedMessagesChat) {
@@ -887,6 +885,13 @@ void PeerListRow::setCheckedInternal(bool checked, anim::type animated) {
 	_checkbox->setChecked(checked, animated);
 }
 
+void PeerListRow::setCustomizedCheckSegments(
+		std::vector<Ui::RoundImageCheckboxSegment> segments) {
+	Expects(_checkbox != nullptr);
+
+	_checkbox->setCustomizedSegments(std::move(segments));
+}
+
 void PeerListRow::finishCheckedAnimation() {
 	_checkbox->setChecked(_checkbox->checked(), anim::type::instant);
 }
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h
index 28cb69534..cfa5b66bc 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.h
+++ b/Telegram/SourceFiles/boxes/peer_list_box.h
@@ -36,6 +36,7 @@ class SlideWrap;
 class FlatLabel;
 struct ScrollToRequest;
 class PopupMenu;
+struct RoundImageCheckboxSegment;
 } // namespace Ui
 
 using PaintRoundImageCallback = Fn<void(
@@ -202,6 +203,8 @@ public:
 		}
 		setCheckedInternal(checked, animated);
 	}
+	void setCustomizedCheckSegments(
+		std::vector<Ui::RoundImageCheckboxSegment> segments);
 	void setHidden(bool hidden) {
 		_hidden = hidden;
 	}
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 2bf72ee92..6c0343b80 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -9,8 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "api/api_chat_participants.h"
 #include "base/random.h"
+#include "boxes/filters/edit_filter_chats_list.h"
 #include "ui/boxes/confirm_box.h"
+#include "ui/effects/round_checkbox.h"
+#include "ui/widgets/menu/menu_add_action_callback_factory.h"
 #include "ui/widgets/checkbox.h"
+#include "ui/widgets/popup_menu.h"
 #include "ui/wrap/padding_wrap.h"
 #include "ui/painter.h"
 #include "ui/ui_utility.h"
@@ -32,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/history.h"
 #include "history/history_item.h"
 #include "dialogs/dialogs_main_list.h"
+#include "ui/wrap/slide_wrap.h"
 #include "window/window_session_controller.h" // showAddContact()
 #include "base/unixtime.h"
 #include "styles/style_boxes.h"
@@ -42,12 +47,302 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/ui/dialogs_stories_content.h"
 #include "dialogs/ui/dialogs_stories_list.h"
 
-
 namespace {
 
 constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
 constexpr auto kSearchPerPage = 50;
 
+class StoriesRow final : public PeerListRow {
+public:
+	StoriesRow(
+		not_null<Main::Session*> session,
+		const QBrush &unread,
+		const Dialogs::Stories::Element &element,
+		int position);
+
+	void applySegments(const Dialogs::Stories::Element &element);
+	void updateGradient(QBrush unread);
+
+	int position = 0;
+
+private:
+	void refreshSegments();
+
+	QBrush _unread;
+	int _count = 0;
+	int _unreadCount = 0;
+
+};
+
+class StoriesController final
+	: public PeerListController
+	, public base::has_weak_ptr {
+public:
+	using Content = Dialogs::Stories::Content;
+	using Row = StoriesRow;
+
+	StoriesController(
+		not_null<Window::SessionController*> window,
+		rpl::producer<Content> content,
+		Fn<void(uint64)> open,
+		Fn<void()> loadMore);
+
+	Main::Session &session() const override;
+	void prepare() override;
+	void loadMoreRows() override;
+	void rowClicked(not_null<PeerListRow*> row) override;
+	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
+		QWidget *parent,
+		not_null<PeerListRow*> row) override;
+
+private:
+	void refresh(const Content &content);
+
+	const not_null<Window::SessionController*> _window;
+	QBrush _unread;
+	rpl::producer<Content> _content;
+	Fn<void(uint64)> _open;
+	Fn<void()> _loadMore;
+	bool _positionPositive = false;
+
+	rpl::lifetime _lifetime;
+
+};
+
+StoriesRow::StoriesRow(
+	not_null<Main::Session*> session,
+	const QBrush &unread,
+	const Dialogs::Stories::Element &element,
+	int position)
+: PeerListRow(session->data().peer(PeerId(element.id)))
+, position(position)
+, _unread(unread) {
+}
+
+void StoriesRow::applySegments(const Dialogs::Stories::Element &element) {
+	Expects(element.unreadCount <= element.count);
+
+	_count = std::max(element.count, 1);
+	_unreadCount = element.unreadCount;
+	refreshSegments();
+}
+
+void StoriesRow::updateGradient(QBrush unread) {
+	_unread = std::move(unread);
+	refreshSegments();
+}
+
+void StoriesRow::refreshSegments() {
+	Expects(_unreadCount <= _count);
+	Expects(_count > 0);
+
+	auto segments = std::vector<Ui::RoundImageCheckboxSegment>();
+	const auto add = [&](bool unread) {
+		segments.push_back({
+			.brush = unread ? _unread : st::dialogsUnreadBgMuted->b,
+			.width = (unread
+				? st::dialogsStoriesFull.lineTwice / 2.
+				: st::dialogsStoriesFull.lineReadTwice / 2.),
+		});
+	};
+	segments.reserve(_count);
+	for (auto i = 0, count = _count - _unreadCount; i != count; ++i) {
+		add(false);
+	}
+	for (auto i = 0; i != _unreadCount; ++i) {
+		add(true);
+	}
+	setCustomizedCheckSegments(std::move(segments));
+}
+
+StoriesController::StoriesController(
+	not_null<Window::SessionController*> window,
+	rpl::producer<Content> content,
+	Fn<void(uint64)> open,
+	Fn<void()> loadMore)
+: _window(window)
+, _content(std::move(content))
+, _open(std::move(open))
+, _loadMore(std::move(loadMore)) {
+	const auto createGradient = [=] {
+		auto gradient = QLinearGradient(
+			QPoint(10, 0),
+			QPoint(0, 10));
+		gradient.setStops({
+			{ 0., st::groupCallLive1->c },
+			{ 1., st::groupCallMuted1->c },
+		});
+		_unread = QBrush(gradient);
+	};
+	createGradient();
+	style::PaletteChanged(
+	) | rpl::start_with_next([=] {
+		createGradient();
+		for (auto i = 0, count = int(delegate()->peerListFullRowsCount())
+			; i != count
+			; ++i) {
+			const auto row = delegate()->peerListRowAt(i).get();
+			static_cast<Row*>(row)->updateGradient(_unread);
+		}
+	}, _lifetime);
+}
+
+Main::Session &StoriesController::session() const {
+	return _window->session();
+}
+
+void StoriesController::prepare() {
+	if (_loadMore) {
+		_loadMore();
+	}
+	std::move(
+		_content
+	) | rpl::start_with_next([=](Content content) {
+		refresh(content);
+	}, _lifetime);
+}
+
+void StoriesController::loadMoreRows() {
+	if (_loadMore) {
+		_loadMore();
+	}
+}
+
+void StoriesController::rowClicked(not_null<PeerListRow*> row) {
+	if (_open) {
+		_open(row->id());
+	}
+}
+
+base::unique_qptr<Ui::PopupMenu> StoriesController::rowContextMenu(
+		QWidget *parent,
+		not_null<PeerListRow*> row) {
+	auto result = base::make_unique_q<Ui::PopupMenu>(parent);
+
+	Dialogs::Stories::FillSourceMenu(_window, {
+		.id = row->id(),
+		.callback = Ui::Menu::CreateAddActionCallback(result.get()),
+	});
+
+	if (result->empty()) {
+		return nullptr;
+	}
+	return result;
+}
+
+void StoriesController::refresh(const Content &content) {
+	const auto session = &_window->session();
+	const auto positive = _positionPositive = !_positionPositive;
+	auto position = positive ? 1 : -int(content.elements.size());
+	for (const auto &element : content.elements) {
+		if (const auto row = delegate()->peerListFindRow(element.id)) {
+			static_cast<Row*>(row)->position = position;
+			static_cast<Row*>(row)->applySegments(element);
+		} else {
+			auto added = std::make_unique<Row>(
+				session,
+				_unread,
+				element,
+				position);
+			const auto raw = added.get();
+			delegate()->peerListAppendRow(std::move(added));
+			delegate()->peerListSetRowChecked(raw, true);
+			raw->applySegments(element);
+		}
+		++position;
+	}
+	auto count = delegate()->peerListFullRowsCount();
+	for (auto i = 0; i != count;) {
+		const auto row = delegate()->peerListRowAt(i);
+		const auto position = static_cast<Row*>(row.get())->position;
+		if (positive ? (position > 0) : (position < 0)) {
+			++i;
+		} else {
+			delegate()->peerListRemoveRow(row);
+			--count;
+		}
+	}
+	delegate()->peerListSortRows([](
+			const PeerListRow &a,
+			const PeerListRow &b) {
+		return static_cast<const Row&>(a).position
+			< static_cast<const Row&>(b).position;
+	});
+	delegate()->peerListRefreshRows();
+}
+
+[[nodiscard]] object_ptr<Ui::RpWidget> PrepareHiddenStoriesList(
+		not_null<PeerListBox*> box,
+		not_null<Window::SessionController*> sessionController,
+		rpl::producer<ContactsBoxController::SortMode> mode) {
+	auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
+		box,
+		object_ptr<Ui::VerticalLayout>(box));
+	const auto container = result->entity();
+	const auto stories = &sessionController->session().data().stories();
+
+	container->add(CreatePeerListSectionSubtitle(
+		container,
+		tr::lng_contacts_hidden_stories()));
+
+	auto &lifetime = container->lifetime();
+	auto list = Dialogs::Stories::ContentForSession(
+		&sessionController->session(),
+		Data::StorySourcesList::Hidden
+	) | rpl::start_spawning(lifetime);
+	const auto delegate = lifetime.make_state<
+		PeerListContentDelegateSimple
+	>();
+	const auto open = [=](uint64 id) {
+		sessionController->openPeerStories(
+			PeerId(int64(id)),
+			Data::StorySourcesList::Hidden);
+	};
+	const auto loadMore = [=] {
+		stories->loadMore(Data::StorySourcesList::Hidden);
+	};
+	const auto controller = lifetime.make_state<StoriesController>(
+		sessionController,
+		rpl::duplicate(
+			list
+		) | rpl::filter([](const Dialogs::Stories::Content &list) {
+			return !list.elements.empty();
+		}),
+		open,
+		loadMore);
+	controller->setStyleOverrides(&st::contactsWithStories);
+	const auto content = container->add(object_ptr<PeerListContent>(
+		container,
+		controller));
+	delegate->setContent(content);
+	controller->setDelegate(delegate);
+
+	container->add(CreatePeerListSectionSubtitle(
+		container,
+		rpl::conditional(
+			std::move(
+				mode
+			) | rpl::map(
+				rpl::mappers::_1 == ContactsBoxController::SortMode::Online
+			),
+			tr::lng_contacts_by_online(),
+			tr::lng_contacts_by_name())));
+
+	stories->incrementPreloadingHiddenSources();
+	lifetime.add([=] {
+		stories->decrementPreloadingHiddenSources();
+	});
+
+	result->toggleOn(rpl::duplicate(
+		list
+	) | rpl::map([](const Dialogs::Stories::Content &list) {
+		return !list.elements.empty();
+	}));
+	result->finishAnimating();
+
+	return result;
+}
+
 } // namespace
 
 object_ptr<Ui::BoxContent> PrepareContactsBox(
@@ -55,14 +350,14 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 	using Mode = ContactsBoxController::SortMode;
 	auto controller = std::make_unique<ContactsBoxController>(
 		&sessionController->session());
+	controller->setStyleOverrides(&st::contactsWithStories);
 	const auto raw = controller.get();
 	auto init = [=](not_null<PeerListBox*> box) {
 		using namespace Dialogs::Stories;
 
 		struct State {
-			List *stories = nullptr;
 			QPointer<::Ui::IconButton> toggleSort;
-			Mode mode = ContactsBoxController::SortMode::Online;
+			rpl::variable<Mode> mode = Mode::Online;
 			::Ui::Animations::Simple scrollAnimation;
 		};
 
@@ -73,109 +368,20 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 			tr::lng_profile_add_contact(),
 			[=] { sessionController->showAddContact(); });
 		state->toggleSort = box->addTopButton(st::contactsSortButton, [=] {
-			const auto online = (state->mode == Mode::Online);
-			state->mode = online ? Mode::Alphabet : Mode::Online;
-			raw->setSortMode(state->mode);
+			const auto online = (state->mode.current() == Mode::Online);
+			const auto mode = online ? Mode::Alphabet : Mode::Online;
+			state->mode = mode;
+			raw->setSortMode(mode);
 			state->toggleSort->setIconOverride(
 				online ? &st::contactsSortOnlineIcon : nullptr,
 				online ? &st::contactsSortOnlineIconOver : nullptr);
 		});
 		raw->setSortMode(Mode::Online);
 
-		auto list = object_ptr<List>(
+		box->peerListSetAboveWidget(PrepareHiddenStoriesList(
 			box,
-			st::dialogsStoriesList,
-			ContentForSession(
-				&sessionController->session(),
-				Data::StorySourcesList::Hidden),
-			[=] { return state->stories->height() - box->scrollTop(); });
-		const auto raw = state->stories = list.data();
-		box->peerListSetAboveWidget(object_ptr<::Ui::PaddingWrap<>>(
-			box,
-			std::move(list),
-			style::margins(0, st::membersMarginTop, 0, 0)));
-
-		raw->clicks(
-		) | rpl::start_with_next([=](uint64 id) {
-			sessionController->openPeerStories(
-				PeerId(int64(id)),
-				Data::StorySourcesList::Hidden);
-		}, raw->lifetime());
-
-		raw->showMenuRequests(
-		) | rpl::start_with_next([=](const ShowMenuRequest &request) {
-			FillSourceMenu(sessionController, request);
-		}, raw->lifetime());
-
-		raw->loadMoreRequests(
-		) | rpl::start_with_next([=] {
-			stories->loadMore(Data::StorySourcesList::Hidden);
-		}, raw->lifetime());
-
-		const auto defaultScrollTop = [=] {
-			return std::max(raw->height() - st::dialogsStories.height, 0);
-		};
-		const auto scrollToDefault = [=](bool verytop) {
-			if (state->scrollAnimation.animating()) {
-				return;
-			}
-			if (verytop) {
-				//_scroll->verticalScrollBar()->setMinimum(0);
-			}
-			state->scrollAnimation.stop();
-			auto scrollTop = box->scrollTop();
-			const auto scrollTo = verytop ? 0 : defaultScrollTop();
-			if (scrollTop == scrollTo) {
-				return;
-			}
-			const auto maxAnimatedDelta = box->height();
-			if (scrollTo + maxAnimatedDelta < scrollTop) {
-				scrollTop = scrollTo + maxAnimatedDelta;
-				box->scrollToY(scrollTop);
-			}
-
-			const auto scroll = [=] {
-				const auto animated = qRound(
-					state->scrollAnimation.value(scrollTo));
-				const auto animatedDelta = animated - scrollTo;
-				const auto realDelta = box->scrollTop() - scrollTo;
-				if (realDelta * animatedDelta < 0) {
-					// We scrolled manually to the other side of target 'scrollTo'.
-					state->scrollAnimation.stop();
-				} else if (std::abs(realDelta) > std::abs(animatedDelta)) {
-					// We scroll by animation only if it gets us closer to target.
-					box->scrollToY(animated);
-				}
-			};
-
-			state->scrollAnimation.start(
-				scroll,
-				scrollTop,
-				scrollTo,
-				st::slideDuration,
-				anim::sineInOut);
-		};
-		const auto top = box->scrollTop();
-		raw->toggleExpandedRequests(
-		) | rpl::start_with_next([=](bool expanded) {
-			if (expanded || box->scrollTop() < defaultScrollTop()) {
-				scrollToDefault(expanded);
-			}
-		}, raw->lifetime());
-
-		raw->heightValue(
-		) | rpl::filter([=] {
-			return (box->scrollHeight() > 0)
-				&& (defaultScrollTop() > box->scrollTop());
-		}) | rpl::start_with_next([=] {
-			//refreshForDefaultScroll();
-			box->scrollToY(defaultScrollTop());
-		}, raw->lifetime());
-
-		stories->incrementPreloadingHiddenSources();
-		raw->lifetime().add([=] {
-			stories->decrementPreloadingHiddenSources();
-		});
+			sessionController,
+			state->mode.value()));
 	};
 	return Box<PeerListBox>(std::move(controller), std::move(init));
 }
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index b5dee6659..8f27352a0 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -37,6 +37,7 @@ constexpr auto kSavedFirstPerPage = 30;
 constexpr auto kSavedPerPage = 100;
 constexpr auto kMaxPreloadSources = 10;
 constexpr auto kStillPreloadFromFirst = 3;
+constexpr auto kMaxSegmentsCount = 180;
 
 using UpdateFlag = StoryUpdate::Flag;
 
@@ -74,13 +75,15 @@ StoriesSourceInfo StoriesSource::info() const {
 	return {
 		.id = user->id,
 		.last = ids.empty() ? 0 : ids.back().date,
-		.unread = unread(),
-		.premium = user->isPremium(),
+		.count = std::min(int(ids.size()), kMaxSegmentsCount),
+		.unreadCount = std::min(unreadCount(), kMaxSegmentsCount),
+		.premium = user->isPremium() ? 1 : 0,
 	};
 }
 
-bool StoriesSource::unread() const {
-	return !ids.empty() && readTill < ids.back().id;
+int StoriesSource::unreadCount() const {
+	const auto i = ids.lower_bound(StoryIdDates{ .id = readTill + 1 });
+	return int(end(ids) - i);
 }
 
 StoryIdDates StoriesSource::toOpen() const {
@@ -724,7 +727,7 @@ void Stories::sort(StorySourcesList list) {
 		const auto key = int64(info.last)
 			+ (info.premium ? (int64(1) << 47) : 0)
 			+ ((info.id == changelogSenderId) ? (int64(1) << 47) : 0)
-			+ (info.unread ? (int64(1) << 49) : 0)
+			+ ((info.unreadCount > 0) ? (int64(1) << 49) : 0)
 			+ ((info.id == self) ? (int64(1) << 50) : 0);
 		return std::make_pair(key, info.id);
 	};
@@ -895,10 +898,10 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 		sendMarkAsReadRequests();
 	}
 	_markReadPending.emplace(id.peer);
-	const auto wasUnread = i->second.unread();
+	const auto wasUnreadCount = i->second.unreadCount();
 	i->second.readTill = id.story;
-	const auto nowUnread = i->second.unread();
-	if (wasUnread != nowUnread) {
+	const auto nowUnreadCount = i->second.unreadCount();
+	if (wasUnreadCount != nowUnreadCount) {
 		const auto refreshInList = [&](StorySourcesList list) {
 			auto &sources = _sources[static_cast<int>(list)];
 			const auto i = ranges::find(
@@ -906,7 +909,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 				id.peer,
 				&StoriesSourceInfo::id);
 			if (i != end(sources)) {
-				i->unread = nowUnread;
+				i->unreadCount = nowUnreadCount;
 				sort(list);
 			}
 		};
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 6cb415977..437a26a7b 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -41,8 +41,9 @@ struct StoriesIds {
 struct StoriesSourceInfo {
 	PeerId id = 0;
 	TimeId last = 0;
-	bool unread = false;
-	bool premium = false;
+	int count : 15 = 0;
+	int unreadCount : 15 = 0;
+	int premium : 1 = 0;
 
 	friend inline bool operator==(
 		StoriesSourceInfo,
@@ -56,7 +57,7 @@ struct StoriesSource {
 	bool hidden = false;
 
 	[[nodiscard]] StoriesSourceInfo info() const;
-	[[nodiscard]] bool unread() const;
+	[[nodiscard]] int unreadCount() const;
 	[[nodiscard]] StoryIdDates toOpen() const;
 
 	friend inline bool operator==(StoriesSource, StoriesSource) = default;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index 243c36b43..329f61296 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -351,8 +351,9 @@ Content State::next() {
 				? tr::lng_stories_my_name(tr::now)
 				: user->shortName()),
 			.thumbnail = std::move(userpic),
-			.unread = info.unread,
-			.skipSmall = user->isSelf(),
+			.count = info.count,
+			.unreadCount = info.unreadCount,
+			.skipSmall = user->isSelf() ? 1 : 0,
 		});
 	}
 	return result;
@@ -423,7 +424,8 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 							result.elements.push_back({
 								.id = uint64(id),
 								.thumbnail = MakeStoryThumbnail(*maybe),
-								.unread = (id > readTill),
+								.count = 1,
+								.unreadCount = (id > readTill) ? 1 : 0,
 							});
 						}
 					} else if (maybe.error() == Data::NoStory::Unknown) {
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index b94ef43b1..23da7e9c8 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -108,7 +108,8 @@ void List::showContent(Content &&content) {
 				item.element.name = element.name;
 				item.nameCache = QImage();
 			}
-			item.element.unread = element.unread;
+			item.element.count = element.count;
+			item.element.unreadCount = element.unreadCount;
 		} else {
 			_data.items.emplace_back(Item{ .element = element });
 		}
@@ -129,7 +130,7 @@ List::Summaries List::ComposeSummaries(Data &data) {
 	auto unreadInFirst = 0;
 	auto unreadTotal = 0;
 	for (auto i = skip; i != total; ++i) {
-		if (data.items[i].element.unread) {
+		if (data.items[i].element.unreadCount > 0) {
 			++unreadTotal;
 			if (i < skip + kSmallThumbsShown) {
 				++unreadInFirst;
@@ -162,7 +163,7 @@ List::Summaries List::ComposeSummaries(Data &data) {
 	}
 	if (unreadInFirst > 0 && unreadInFirst == unreadTotal) {
 		for (auto i = skip; i != total; ++i) {
-			if (data.items[i].element.unread) {
+			if (data.items[i].element.unreadCount > 0) {
 				append(result.unreadNames.string, i, !--unreadTotal);
 			}
 		}
@@ -449,8 +450,8 @@ void List::paintEvent(QPaintEvent *e) {
 		return Single{ x, indexSmall, small, indexFull, full, y };
 	};
 	const auto hasUnread = [&](const Single &single) {
-		return (single.itemSmall && single.itemSmall->element.unread)
-			|| (single.itemFull && single.itemFull->element.unread);
+		return (single.itemSmall && single.itemSmall->element.unreadCount)
+			|| (single.itemFull && single.itemFull->element.unreadCount);
 	};
 	const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
 		auto nextGradientPainted = false;
@@ -513,8 +514,8 @@ void List::paintEvent(QPaintEvent *e) {
 			photo);
 		const auto small = single.itemSmall;
 		const auto itemFull = single.itemFull;
-		const auto smallUnread = small && small->element.unread;
-		const auto fullUnread = itemFull && itemFull->element.unread;
+		const auto smallUnread = small && small->element.unreadCount;
+		const auto fullUnread = itemFull && itemFull->element.unreadCount;
 		const auto unreadOpacity = (smallUnread && fullUnread)
 			? 1.
 			: smallUnread
@@ -550,8 +551,8 @@ void List::paintEvent(QPaintEvent *e) {
 			photo);
 		const auto small = single.itemSmall;
 		const auto itemFull = single.itemFull;
-		const auto smallUnread = small && small->element.unread;
-		const auto fullUnread = itemFull && itemFull->element.unread;
+		const auto smallUnread = small && small->element.unreadCount;
+		const auto fullUnread = itemFull && itemFull->element.unreadCount;
 
 		// White circle with possible read gray line.
 		const auto hasReadLine = (itemFull && !fullUnread);
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 49391432c..9bbca05b0 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -36,8 +36,9 @@ struct Element {
 	uint64 id = 0;
 	QString name;
 	std::shared_ptr<Thumbnail> thumbnail;
-	bool unread : 1 = false;
-	bool skipSmall : 1 = false;
+	int count : 15 = 0;
+	int unreadCount : 15 = 0;
+	int skipSmall : 1 = 0;
 
 	friend inline bool operator==(
 		const Element &a,
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index 15473babc..b6fc6254f 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -160,7 +160,7 @@ void InnerWidget::createButtons() {
 		self
 	) | rpl::map([=](Content &&content) {
 		for (auto &element : content.elements) {
-			element.unread = false;
+			element.unreadCount = 0;
 		}
 		return std::move(content);
 	}) | rpl::start_spawning(recentWrap->lifetime());
diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
index 74c51e144..4d2a03f77 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
@@ -389,26 +389,49 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 	}
 
 	if (selectionLevel > 0) {
-		const auto radius = _roundingRadius
-			? _roundingRadius(_st.imageRadius * 2)
-			: std::optional<int>();
 		PainterHighQualityEnabler hq(p);
 		p.setOpacity(std::clamp(selectionLevel, 0., 1.));
 		p.setBrush(Qt::NoBrush);
-		const auto pen = QPen(
-			_fgOverride ? (*_fgOverride) : _st.selectFg->b,
-			_st.selectWidth);
-		p.setPen(pen);
+		const auto segments = int(_segments.size());
 		const auto rect = style::rtlrect(
 			x,
 			y,
 			_st.imageRadius * 2,
 			_st.imageRadius * 2,
 			outerWidth);
-		if (!radius) {
-			p.drawEllipse(rect);
+		if (segments < 2) {
+			const auto radius = _roundingRadius
+				? _roundingRadius(_st.imageRadius * 2)
+				: std::optional<int>();
+			const auto pen = QPen(
+				segments ? _segments.front().brush : _st.selectFg->b,
+				segments ? _segments.front().width : _st.selectWidth);
+			p.setPen(pen);
+			if (!radius) {
+				p.drawEllipse(rect);
+			} else {
+				p.drawRoundedRect(rect, *radius, *radius);
+			}
 		} else {
-			p.drawRoundedRect(rect, *radius, *radius);
+			const auto small = 160;
+			const auto full = arc::kFullLength;
+			const auto separator = (full > 2 * small * segments)
+				? small
+				: full / (segments * 2);
+			const auto left = full - (separator * segments);
+			const auto length = left / float64(segments);
+			auto start = 0. + (arc::kQuarterLength + (separator / 2));
+			for (const auto &segment : ranges::views::reverse(_segments)) {
+				p.setPen(QPen(
+					segment.brush,
+					segment.width,
+					Qt::SolidLine,
+					Qt::RoundCap));
+				const auto from = int(base::SafeRound(start));
+				const auto till = int(base::SafeRound(start + length));
+				p.drawArc(rect, from, till - from);
+				start += length + separator;
+			}
 		}
 		p.setOpacity(1.);
 	}
@@ -474,7 +497,18 @@ void RoundImageCheckbox::prepareWideCache() {
 }
 
 void RoundImageCheckbox::setColorOverride(std::optional<QBrush> fg) {
-	_fgOverride = fg;
+	if (fg) {
+		setCustomizedSegments({
+			{ .brush = *fg, .width = float64(_st.selectWidth) }
+		});
+	} else {
+		setCustomizedSegments({});
+	}
+}
+
+void RoundImageCheckbox::setCustomizedSegments(
+		std::vector<Segment> segments) {
+	_segments = std::move(segments);
 }
 
 } // namespace Ui
diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.h b/Telegram/SourceFiles/ui/effects/round_checkbox.h
index 461244f95..9247c226e 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.h
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.h
@@ -45,8 +45,14 @@ private:
 
 };
 
+struct RoundImageCheckboxSegment {
+	QBrush brush;
+	float64 width = 0.;
+};
+
 class RoundImageCheckbox {
 public:
+	using Segment = RoundImageCheckboxSegment;
 	using PaintRoundImage = Fn<void(Painter &p, int x, int y, int outerWidth, int size)>;
 	RoundImageCheckbox(
 		const style::RoundImageCheckbox &st,
@@ -58,6 +64,7 @@ public:
 	float64 checkedAnimationRatio() const;
 
 	void setColorOverride(std::optional<QBrush> fg);
+	void setCustomizedSegments(std::vector<Segment> segments);
 
 	bool checked() const {
 		return _check.checked();
@@ -83,7 +90,8 @@ private:
 
 	RoundCheckbox _check;
 
-	std::optional<QBrush> _fgOverride;
+	//std::optional<QBrush> _fgOverride;
+	std::vector<Segment> _segments;
 
 };
 

From 9a29807276323d37b52126cbd60642905b463f3b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 4 Jul 2023 12:29:40 +0400
Subject: [PATCH 137/259] Show stories segments in contacts list.

---
 Telegram/SourceFiles/boxes/peer_list_box.cpp  |  34 +++-
 Telegram/SourceFiles/boxes/peer_list_box.h    |   8 +
 .../boxes/peer_list_controllers.cpp           | 171 ++++++++++++++----
 .../SourceFiles/boxes/peer_list_controllers.h |  19 ++
 4 files changed, 197 insertions(+), 35 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index 95920addc..777e952b9 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -261,15 +261,23 @@ void PeerListBox::peerListSetRowChecked(
 		not_null<PeerListRow*> row,
 		bool checked) {
 	if (checked) {
-		addSelectItem(row, anim::type::normal);
+		if (_controller->trackSelectedList()) {
+			addSelectItem(row, anim::type::normal);
+		}
 		PeerListContentDelegate::peerListSetRowChecked(row, checked);
 		peerListUpdateRow(row);
 
 		// This call deletes row from _searchRows.
-		_select->entity()->clearQuery();
+		if (_select) {
+			_select->entity()->clearQuery();
+		}
 	} else {
 		// The itemRemovedCallback will call changeCheckState() here.
-		_select->entity()->removeItem(row->id());
+		if (_select) {
+			_select->entity()->removeItem(row->id());
+		} else {
+			PeerListContentDelegate::peerListSetRowChecked(row, checked);
+		}
 		peerListUpdateRow(row);
 	}
 }
@@ -1131,6 +1139,24 @@ PeerListRow *PeerListContent::findRow(PeerListRowId id) {
 	return (it == _rowsById.cend()) ? nullptr : it->second.get();
 }
 
+std::optional<QPoint> PeerListContent::lastRowMousePosition() const {
+	if (!_lastMousePosition) {
+		return std::nullopt;
+	}
+	const auto point = mapFromGlobal(*_lastMousePosition);
+	auto in = parentWidget()->rect().contains(
+		parentWidget()->mapFromGlobal(*_lastMousePosition));
+	auto rowsPointY = point.y() - rowsTop();
+	const auto index = (in
+		&& rowsPointY >= 0
+		&& rowsPointY < shownRowsCount() * _rowHeight)
+		? (rowsPointY / _rowHeight)
+		: -1;
+	return (index >= 0 && index == _selected.index.value)
+		? QPoint(point.x(), rowsPointY)
+		: std::optional<QPoint>();
+}
+
 void PeerListContent::removeRow(not_null<PeerListRow*> row) {
 	auto index = row->absoluteIndex();
 	auto isSearchResult = row->isSearchResult();
@@ -1998,10 +2024,12 @@ void PeerListContent::setSearchQuery(
 
 bool PeerListContent::submitted() {
 	if (const auto row = getRow(_selected.index)) {
+		_lastMousePosition = std::nullopt;
 		_controller->rowClicked(row);
 		return true;
 	} else if (showingSearch()) {
 		if (const auto row = getRow(RowIndex(0))) {
+			_lastMousePosition = std::nullopt;
 			_controller->rowClicked(row);
 			return true;
 		}
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h
index cfa5b66bc..4252b5fa2 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.h
+++ b/Telegram/SourceFiles/boxes/peer_list_box.h
@@ -327,6 +327,7 @@ public:
 	virtual void peerListScrollToTop() = 0;
 	virtual int peerListFullRowsCount() = 0;
 	virtual PeerListRow *peerListFindRow(PeerListRowId id) = 0;
+	virtual std::optional<QPoint> peerListLastRowMousePosition() = 0;
 	virtual void peerListSortRows(Fn<bool(const PeerListRow &a, const PeerListRow &b)> compare) = 0;
 	virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;
 	virtual void peerListShowBox(
@@ -503,6 +504,9 @@ public:
 		return delegate()->peerListIsRowChecked(row);
 	}
 
+	virtual bool trackSelectedList() {
+		return true;
+	}
 	virtual bool searchInLocal() {
 		return true;
 	}
@@ -612,6 +616,7 @@ public:
 	void prependRow(std::unique_ptr<PeerListRow> row);
 	void prependRowFromSearchResult(not_null<PeerListRow*> row);
 	PeerListRow *findRow(PeerListRowId id);
+	std::optional<QPoint> lastRowMousePosition() const;
 	void updateRow(not_null<PeerListRow*> row) {
 		updateRow(row, RowIndex());
 	}
@@ -866,6 +871,9 @@ public:
 	PeerListRow *peerListFindRow(PeerListRowId id) override {
 		return _content->findRow(id);
 	}
+	std::optional<QPoint> peerListLastRowMousePosition() override {
+		return _content->lastRowMousePosition();
+	}
 	void peerListUpdateRow(not_null<PeerListRow*> row) override {
 		_content->updateRow(row);
 	}
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 6c0343b80..745e5eef8 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/ui_utility.h"
 #include "main/main_session.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_channel.h"
 #include "data/data_chat.h"
 #include "data/data_user.h"
@@ -109,6 +110,46 @@ private:
 
 };
 
+[[nodiscard]] std::vector<Ui::RoundImageCheckboxSegment> PrepareSegments(
+		int count,
+		int unread,
+		const QBrush &unreadBrush) {
+	Expects(unread <= count);
+	Expects(count > 0);
+
+	auto result = std::vector<Ui::RoundImageCheckboxSegment>();
+	const auto add = [&](bool unread) {
+		result.push_back({
+			.brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b,
+			.width = (unread
+				? st::dialogsStoriesFull.lineTwice / 2.
+				: st::dialogsStoriesFull.lineReadTwice / 2.),
+		});
+	};
+	result.reserve(count);
+	for (auto i = 0, till = count - unread; i != till; ++i) {
+		add(false);
+	}
+	for (auto i = 0; i != unread; ++i) {
+		add(true);
+	}
+	return result;
+}
+
+[[nodiscard]] QBrush CreateStoriesGradient() {
+	const auto &st = st::contactsWithStories.item;
+	const auto left = st.photoPosition.x();
+	const auto top = st.photoPosition.y();
+	auto gradient = QLinearGradient(
+		QPoint(left + st.photoSize, top),
+		QPoint(left, top + st.photoSize));
+	gradient.setStops({
+		{ 0., st::groupCallLive1->c },
+		{ 1., st::groupCallMuted1->c },
+	});
+	return QBrush(gradient);
+}
+
 StoriesRow::StoriesRow(
 	not_null<Main::Session*> session,
 	const QBrush &unread,
@@ -133,26 +174,8 @@ void StoriesRow::updateGradient(QBrush unread) {
 }
 
 void StoriesRow::refreshSegments() {
-	Expects(_unreadCount <= _count);
-	Expects(_count > 0);
-
-	auto segments = std::vector<Ui::RoundImageCheckboxSegment>();
-	const auto add = [&](bool unread) {
-		segments.push_back({
-			.brush = unread ? _unread : st::dialogsUnreadBgMuted->b,
-			.width = (unread
-				? st::dialogsStoriesFull.lineTwice / 2.
-				: st::dialogsStoriesFull.lineReadTwice / 2.),
-		});
-	};
-	segments.reserve(_count);
-	for (auto i = 0, count = _count - _unreadCount; i != count; ++i) {
-		add(false);
-	}
-	for (auto i = 0; i != _unreadCount; ++i) {
-		add(true);
-	}
-	setCustomizedCheckSegments(std::move(segments));
+	setCustomizedCheckSegments(
+		PrepareSegments(_count, _unreadCount, _unread));
 }
 
 StoriesController::StoriesController(
@@ -164,20 +187,10 @@ StoriesController::StoriesController(
 , _content(std::move(content))
 , _open(std::move(open))
 , _loadMore(std::move(loadMore)) {
-	const auto createGradient = [=] {
-		auto gradient = QLinearGradient(
-			QPoint(10, 0),
-			QPoint(0, 10));
-		gradient.setStops({
-			{ 0., st::groupCallLive1->c },
-			{ 1., st::groupCallMuted1->c },
-		});
-		_unread = QBrush(gradient);
-	};
-	createGradient();
+	_unread = CreateStoriesGradient();
 	style::PaletteChanged(
 	) | rpl::start_with_next([=] {
-		createGradient();
+		_unread = CreateStoriesGradient();
 		for (auto i = 0, count = int(delegate()->peerListFullRowsCount())
 			; i != count
 			; ++i) {
@@ -248,6 +261,7 @@ void StoriesController::refresh(const Content &content) {
 			delegate()->peerListAppendRow(std::move(added));
 			delegate()->peerListSetRowChecked(raw, true);
 			raw->applySegments(element);
+			raw->finishCheckedAnimation();
 		}
 		++position;
 	}
@@ -351,6 +365,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 	auto controller = std::make_unique<ContactsBoxController>(
 		&sessionController->session());
 	controller->setStyleOverrides(&st::contactsWithStories);
+	controller->setStoriesShown(true);
 	const auto raw = controller.get();
 	auto init = [=](not_null<PeerListBox*> box) {
 		using namespace Dialogs::Stories;
@@ -647,6 +662,33 @@ void ContactsBoxController::prepare() {
 
 	prepareViewHook();
 
+	if (_storiesShown) {
+		_storiesUnread = CreateStoriesGradient();
+		style::PaletteChanged() | rpl::start_with_next([=] {
+			_storiesUnread = CreateStoriesGradient();
+			for (auto &entry : _storiesCounts) {
+				entry.second.count = entry.second.unread = -1;
+			}
+			updateStories();
+		}, lifetime());
+
+		const auto stories = &session().data().stories();
+		rpl::merge(
+			rpl::single(rpl::empty),
+			stories->sourcesChanged(Data::StorySourcesList::NotHidden),
+			stories->sourcesChanged(Data::StorySourcesList::Hidden)
+		) | rpl::start_with_next([=] {
+			updateStories();
+		}, lifetime());
+		stories->sourceChanged() | rpl::start_with_next([=](PeerId id) {
+			const auto source = stories->source(id);
+			const auto info = source
+				? source->info()
+				: Data::StoriesSourceInfo();
+			updateStoriesFor(id.value, info.count, info.unreadCount);
+		}, lifetime());
+	}
+
 	session().data().contactsLoaded().value(
 	) | rpl::start_with_next([=] {
 		rebuildRows();
@@ -692,6 +734,14 @@ std::unique_ptr<PeerListRow> ContactsBoxController::createSearchRow(
 void ContactsBoxController::rowClicked(not_null<PeerListRow*> row) {
 	const auto peer = row->peer();
 	if (const auto window = peer->session().tryResolveWindow()) {
+		if (_storiesShown) {
+			const auto point = delegate()->peerListLastRowMousePosition();
+			const auto &st = st::contactsWithStories.item;
+			if (point && point->x() < st.photoPosition.x() + st.photoSize) {
+				window->openPeerStories(peer->id);
+				return;
+			}
+		}
 		window->showPeerHistory(row->peer());
 	}
 }
@@ -717,6 +767,55 @@ void ContactsBoxController::setSortMode(SortMode mode) {
 	}
 }
 
+void ContactsBoxController::setStoriesShown(bool shown) {
+	_storiesShown = shown;
+}
+
+void ContactsBoxController::updateStories() {
+	const auto stories = &_session->data().stories();
+	const auto &a = stories->sources(Data::StorySourcesList::NotHidden);
+	const auto &b = stories->sources(Data::StorySourcesList::Hidden);
+	auto checked = base::flat_set<PeerListRowId>();
+	for (const auto &info : ranges::views::concat(a, b)) {
+		const auto id = info.id.value;
+		checked.emplace(id);
+		updateStoriesFor(id, info.count, info.unreadCount);
+	}
+	for (auto i = begin(_storiesCounts); i != end(_storiesCounts); ++i) {
+		if (i->second.count && !checked.contains(i->first)) {
+			updateStoriesFor(i->first, 0, 0);
+		}
+	}
+}
+
+void ContactsBoxController::updateStoriesFor(
+		uint64 id,
+		int count,
+		int unread) {
+	if (const auto row = delegate()->peerListFindRow(id)) {
+		applyRowStories(row, count, unread);
+		delegate()->peerListUpdateRow(row);
+	}
+}
+
+void ContactsBoxController::applyRowStories(
+		not_null<PeerListRow*> row,
+		int count,
+		int unread,
+		bool force) {
+	auto &counts = _storiesCounts[row->id()];
+	if (!force && counts.count == count && counts.unread == unread) {
+		return;
+	}
+	counts.count = count;
+	counts.unread = unread;
+	delegate()->peerListSetRowChecked(row, count > 0);
+	if (count > 0) {
+		row->setCustomizedCheckSegments(
+			PrepareSegments(count, unread, _storiesUnread));
+	}
+}
+
 void ContactsBoxController::sort() {
 	switch (_sortMode) {
 	case SortMode::Alphabet: sortByName(); break;
@@ -762,7 +861,15 @@ bool ContactsBoxController::appendRow(not_null<UserData*> user) {
 		return false;
 	}
 	if (auto row = createRow(user)) {
+		const auto raw = row.get();
 		delegate()->peerListAppendRow(std::move(row));
+		if (_storiesShown) {
+			const auto stories = &session().data().stories();
+			if (const auto source = stories->source(user->id)) {
+				const auto info = source->info();
+				applyRowStories(raw, info.count, info.unreadCount, true);
+			}
+		}
 		return true;
 	}
 	return false;
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h
index 00106c392..46d1a184c 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.h
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h
@@ -128,12 +128,16 @@ public:
 	[[nodiscard]] std::unique_ptr<PeerListRow> createSearchRow(
 		not_null<PeerData*> peer) override final;
 	void rowClicked(not_null<PeerListRow*> row) override;
+	bool trackSelectedList() override {
+		return !_storiesShown;
+	}
 
 	enum class SortMode {
 		Alphabet,
 		Online,
 	};
 	void setSortMode(SortMode mode);
+	void setStoriesShown(bool shown);
 
 protected:
 	virtual std::unique_ptr<PeerListRow> createRow(not_null<UserData*> user);
@@ -143,18 +147,33 @@ protected:
 	}
 
 private:
+	struct StoriesCount {
+		int count = 0;
+		int unread = 0;
+	};
 	void sort();
 	void sortByName();
 	void sortByOnline();
 	void rebuildRows();
+	void updateStories();
 	void checkForEmptyRows();
 	bool appendRow(not_null<UserData*> user);
+	void updateStoriesFor(uint64 id, int count, int unread);
+	void applyRowStories(
+		not_null<PeerListRow*> row,
+		int count,
+		int unread,
+		bool force = false);
 
 	const not_null<Main::Session*> _session;
 	SortMode _sortMode = SortMode::Alphabet;
 	base::Timer _sortByOnlineTimer;
 	rpl::lifetime _sortByOnlineLifetime;
 
+	QBrush _storiesUnread;
+	base::flat_map<uint64, StoriesCount> _storiesCounts;
+	bool _storiesShown = false;
+
 };
 
 class ChooseRecipientBoxController

From d7d8847c1d185cc2c215a1d891a88f3753f174a5 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 4 Jul 2023 20:13:56 +0400
Subject: [PATCH 138/259] Show stories in chats list userpics.

---
 Telegram/SourceFiles/api/api_updates.cpp      |  4 +
 Telegram/SourceFiles/data/data_session.cpp    | 14 ++++
 Telegram/SourceFiles/data/data_stories.cpp    | 84 +++++++++++++++++--
 Telegram/SourceFiles/data/data_stories.h      | 16 ++++
 Telegram/SourceFiles/data/data_user.cpp       | 33 ++++++++
 Telegram/SourceFiles/data/data_user.h         | 12 +++
 .../dialogs/dialogs_inner_widget.cpp          | 26 +++---
 .../dialogs/dialogs_inner_widget.h            |  6 +-
 Telegram/SourceFiles/dialogs/dialogs_row.cpp  | 80 +++++++++++++++---
 Telegram/SourceFiles/dialogs/dialogs_row.h    |  9 +-
 .../SourceFiles/dialogs/dialogs_widget.cpp    |  8 ++
 11 files changed, 259 insertions(+), 33 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index 55a15d2a7..3d7584a64 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -2524,6 +2524,10 @@ void Updates::feedUpdate(const MTPUpdate &update) {
 		_session->data().stories().apply(update.c_updateStory());
 	} break;
 
+	case mtpc_updateReadStories: {
+		_session->data().stories().apply(update.c_updateReadStories());
+	} break;
+
 	}
 }
 
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index ea176d2bf..ca39658a6 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -525,6 +525,13 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
 				| Flag::DiscardMinPhoto
 				| Flag::StoriesHidden
 				: Flag());
+		const auto storiesState = minimal
+			? std::optional<Data::Stories::PeerSourceState>()
+			: data.is_stories_unavailable()
+			? Data::Stories::PeerSourceState()
+			: !data.vstories_max_id()
+			? std::optional<Data::Stories::PeerSourceState>()
+			: stories().peerSourceState(result, data.vstories_max_id()->v);
 		const auto flagsSet = (data.is_deleted() ? Flag::Deleted : Flag())
 			| (data.is_verified() ? Flag::Verified : Flag())
 			| (data.is_scam() ? Flag::Scam : Flag())
@@ -551,6 +558,13 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
 					MTP_long(data.vaccess_hash().value_or_empty()));
 			}
 		} else {
+			if (storiesState) {
+				result->setStoriesState(!storiesState->maxId
+					? UserData::StoriesState::None
+					: (storiesState->maxId > storiesState->readTill)
+					? UserData::StoriesState::HasUnread
+					: UserData::StoriesState::HasRead);
+			}
 			if (data.is_self()) {
 				result->input = MTP_inputPeerSelf();
 				result->inputUser = MTP_inputUserSelf();
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 8f27352a0..5b265f8a7 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -136,7 +136,7 @@ void Stories::apply(const MTPDupdateStory &data) {
 	i->second.ids.emplace(idDates);
 	const auto nowInfo = i->second.info();
 	if (user->isSelf() && i->second.readTill < idDates.id) {
-		i->second.readTill = idDates.id;
+		_readTill[user->id] = i->second.readTill = idDates.id;
 	}
 	if (wasInfo == nowInfo) {
 		return;
@@ -158,6 +158,11 @@ void Stories::apply(const MTPDupdateStory &data) {
 		refreshInList(StorySourcesList::NotHidden);
 	}
 	_sourceChanged.fire_copy(peerId);
+	updateUserStoriesState(user);
+}
+
+void Stories::apply(const MTPDupdateReadStories &data) {
+	bumpReadTill(peerFromUser(data.vuser_id()), data.vmax_id().v);
 }
 
 void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {
@@ -166,6 +171,7 @@ void Stories::apply(not_null<PeerData*> peer, const MTPUserStories *data) {
 		applyDeletedFromSources(peer->id, StorySourcesList::Hidden);
 		_all.erase(peer->id);
 		_sourceChanged.fire_copy(peer->id);
+		updateUserStoriesState(peer);
 	} else {
 		parseAndApply(*data);
 	}
@@ -281,6 +287,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 	} else if (user->isSelf()) {
 		result.readTill = result.ids.back().id;
 	}
+	_readTill[peerId] = result.readTill;
 	const auto info = result.info();
 	const auto i = _all.find(peerId);
 	if (i != end(_all)) {
@@ -317,6 +324,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 		applyDeletedFromSources(peerId, StorySourcesList::Hidden);
 	}
 	_sourceChanged.fire_copy(peerId);
+	updateUserStoriesState(result.user);
 }
 
 Story *Stories::parseAndApply(
@@ -684,12 +692,14 @@ void Stories::applyRemovedFromActive(FullStoryId id) {
 		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);
+			const auto user = i->second.user;
 			if (i->second.ids.empty()) {
 				_all.erase(i);
 				removeFromList(StorySourcesList::NotHidden);
 				removeFromList(StorySourcesList::Hidden);
 			}
 			_sourceChanged.fire_copy(id.peer);
+			updateUserStoriesState(user);
 		}
 	}
 }
@@ -891,22 +901,35 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 			_incrementViewsTimer.callOnce(kIncrementViewsDelay);
 		}
 	}
-	const auto i = _all.find(id.peer);
-	if (i == end(_all) || i->second.readTill >= id.story) {
+	if (!bumpReadTill(id.peer, id.story)) {
 		return;
-	} else if (!_markReadPending.contains(id.peer)) {
+	}
+	if (!_markReadPending.contains(id.peer)) {
 		sendMarkAsReadRequests();
 	}
 	_markReadPending.emplace(id.peer);
+	_markReadTimer.callOnce(kMarkAsReadDelay);
+}
+
+bool Stories::bumpReadTill(PeerId peerId, StoryId maxReadTill) {
+	auto &till = _readTill[peerId];
+	if (till < maxReadTill) {
+		till = maxReadTill;
+		updateUserStoriesState(_owner->peer(peerId));
+	}
+	const auto i = _all.find(peerId);
+	if (i == end(_all) || i->second.readTill >= maxReadTill) {
+		return false;
+	}
 	const auto wasUnreadCount = i->second.unreadCount();
-	i->second.readTill = id.story;
+	i->second.readTill = maxReadTill;
 	const auto nowUnreadCount = i->second.unreadCount();
 	if (wasUnreadCount != nowUnreadCount) {
 		const auto refreshInList = [&](StorySourcesList list) {
 			auto &sources = _sources[static_cast<int>(list)];
 			const auto i = ranges::find(
 				sources,
-				id.peer,
+				peerId,
 				&StoriesSourceInfo::id);
 			if (i != end(sources)) {
 				i->unreadCount = nowUnreadCount;
@@ -916,7 +939,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 		refreshInList(StorySourcesList::NotHidden);
 		refreshInList(StorySourcesList::Hidden);
 	}
-	_markReadTimer.callOnce(kMarkAsReadDelay);
+	return true;
 }
 
 void Stories::toggleHidden(
@@ -1408,6 +1431,53 @@ void Stories::setPreloadingInViewer(std::vector<FullStoryId> ids) {
 	}
 }
 
+std::optional<Stories::PeerSourceState> Stories::peerSourceState(
+		not_null<PeerData*> peer,
+		StoryId storyMaxId) {
+	const auto i = _readTill.find(peer->id);
+	if (_readTillReceived || (i != end(_readTill))) {
+		return PeerSourceState{
+			.maxId = storyMaxId,
+			.readTill = std::min(
+				storyMaxId,
+				(i != end(_readTill)) ? i->second : 0),
+		};
+	}
+	if (!_readTillsRequestId) {
+		const auto api = &_owner->session().api();
+		_readTillsRequestId = api->request(MTPstories_GetAllReadUserStories(
+		)).done([=](const MTPUpdates &result) {
+			_readTillReceived = true;
+			api->applyUpdates(result);
+			for (auto &[peer, maxId] : base::take(_pendingUserStateMaxId)) {
+				updateUserStoriesState(peer);
+			}
+		}).send();
+	}
+	_pendingUserStateMaxId[peer] = storyMaxId;
+	return std::nullopt;
+}
+
+void Stories::updateUserStoriesState(not_null<PeerData*> peer) {
+	const auto till = _readTill.find(peer->id);
+	const auto readTill = (till != end(_readTill)) ? till->second : 0;
+	const auto pendingMaxId = [&] {
+		const auto j = _pendingUserStateMaxId.find(peer);
+		return (j != end(_pendingUserStateMaxId)) ? j->second : 0;
+	};
+	const auto i = _all.find(peer->id);
+	const auto max = (i != end(_all))
+		? (i->second.ids.empty() ? 0 : i->second.ids.back().id)
+		: pendingMaxId();
+	if (const auto user = peer->asUser()) {
+		user->setStoriesState(!max
+			? UserData::StoriesState::None
+			: (max <= readTill)
+			? UserData::StoriesState::HasRead
+			: UserData::StoriesState::HasUnread);
+	}
+}
+
 void Stories::preloadSourcesChanged(StorySourcesList list) {
 	if (rebuildPreloadSources(list)) {
 		continuePreloading();
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 437a26a7b..c15b1fc6b 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -137,6 +137,7 @@ public:
 
 	void loadMore(StorySourcesList list);
 	void apply(const MTPDupdateStory &data);
+	void apply(const MTPDupdateReadStories &data);
 	void apply(not_null<PeerData*> peer, const MTPUserStories *data);
 	Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story);
 	void loadAround(FullStoryId id, StoriesContext context);
@@ -199,6 +200,14 @@ public:
 	void decrementPreloadingHiddenSources();
 	void setPreloadingInViewer(std::vector<FullStoryId> ids);
 
+	struct PeerSourceState {
+		StoryId maxId = 0;
+		StoryId readTill = 0;
+	};
+	[[nodiscard]] std::optional<PeerSourceState> peerSourceState(
+		not_null<PeerData*> peer,
+		StoryId storyMaxId);
+
 private:
 	struct Saved {
 		StoriesIds ids;
@@ -222,6 +231,7 @@ private:
 		const QVector<MTPStoryItem> &list);
 	void sendResolveRequests();
 	void finalizeResolve(FullStoryId id);
+	void updateUserStoriesState(not_null<PeerData*> peer);
 
 	void applyDeleted(FullStoryId id);
 	void applyExpired(FullStoryId id);
@@ -230,6 +240,7 @@ private:
 	void removeDependencyStory(not_null<Story*> story);
 	void savedStateUpdated(not_null<Story*> story);
 	void sort(StorySourcesList list);
+	bool bumpReadTill(PeerId peerId, StoryId maxReadTill);
 
 	[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
 		not_null<Story*> story);
@@ -317,6 +328,11 @@ private:
 	int _preloadingHiddenSourcesCounter = 0;
 	int _preloadingMainSourcesCounter = 0;
 
+	base::flat_map<PeerId, StoryId> _readTill;
+	base::flat_map<not_null<PeerData*>, StoryId> _pendingUserStateMaxId;
+	mtpRequestId _readTillsRequestId = 0;
+	bool _readTillReceived = false;
+
 };
 
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp
index be4a9fa6f..d25f37780 100644
--- a/Telegram/SourceFiles/data/data_user.cpp
+++ b/Telegram/SourceFiles/data/data_user.cpp
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_user_names.h"
 #include "data/data_wall_paper.h"
 #include "data/notify/data_notify_settings.h"
+#include "history/history.h"
 #include "api/api_peer_photo.h"
 #include "apiwrap.h"
 #include "ui/text/text_options.h"
@@ -125,6 +126,38 @@ void UserData::setPrivateForwardName(const QString &name) {
 	_privateForwardName = name;
 }
 
+bool UserData::hasActiveStories() const {
+	return flags() & UserDataFlag::HasActiveStories;
+}
+
+bool UserData::hasUnreadStories() const {
+	return flags() & UserDataFlag::HasUnreadStories;
+}
+
+void UserData::setStoriesState(StoriesState state) {
+	Expects(state != StoriesState::Unknown);
+
+	const auto was = flags();
+	using Flag = UserDataFlag;
+	switch (state) {
+	case StoriesState::None:
+		_flags.remove(Flag::HasActiveStories | Flag::HasUnreadStories);
+		break;
+	case StoriesState::HasRead:
+		_flags.set(
+			(flags() & ~Flag::HasUnreadStories) | Flag::HasActiveStories);
+		break;
+	case StoriesState::HasUnread:
+		_flags.add(Flag::HasActiveStories | Flag::HasUnreadStories);
+		break;
+	}
+	if (flags() != was) {
+		if (const auto history = owner().historyLoaded(this)) {
+			history->updateChatListEntryPostponed();
+		}
+	}
+}
+
 void UserData::setName(const QString &newFirstName, const QString &newLastName, const QString &newPhoneName, const QString &newUsername) {
 	bool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty();
 
diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h
index 37005d124..ad6d0b7ae 100644
--- a/Telegram/SourceFiles/data/data_user.h
+++ b/Telegram/SourceFiles/data/data_user.h
@@ -63,6 +63,8 @@ enum class UserDataFlag {
 	VoiceMessagesForbidden = (1 << 16),
 	PersonalPhoto = (1 << 17),
 	StoriesHidden = (1 << 18),
+	HasActiveStories = (1 << 19),
+	HasUnreadStories = (1 << 20),
 };
 inline constexpr bool is_flag_type(UserDataFlag) { return true; };
 using UserDataFlags = base::flags<UserDataFlag>;
@@ -174,6 +176,16 @@ public:
 	[[nodiscard]] QString privateForwardName() const;
 	void setPrivateForwardName(const QString &name);
 
+	enum class StoriesState {
+		Unknown,
+		None,
+		HasRead,
+		HasUnread,
+	};
+	[[nodiscard]] bool hasActiveStories() const;
+	[[nodiscard]] bool hasUnreadStories() const;
+	void setStoriesState(StoriesState state);
+
 private:
 	auto unavailableReasons() const
 		-> const std::vector<Data::UnavailableReason> & override;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 091ebaf16..6b2b7efe7 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -1294,6 +1294,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
 	}
 	_mouseSelection = true;
 	_lastMousePosition = globalPosition;
+	_lastRowLocalMouseX = local.x();
 
 	const auto w = width();
 	const auto mouseY = local.y();
@@ -2209,6 +2210,7 @@ FilterId InnerWidget::filterId() const {
 void InnerWidget::clearSelection() {
 	_mouseSelection = false;
 	_lastMousePosition = std::nullopt;
+	_lastRowLocalMouseX = -1;
 	if (isSelected()) {
 		updateSelectedRow();
 		_collapsedSelected = -1;
@@ -2907,6 +2909,7 @@ void InnerWidget::resizeEmptyLabel() {
 void InnerWidget::clearMouseSelection(bool clearSelection) {
 	_mouseSelection = false;
 	_lastMousePosition = std::nullopt;
+	_lastRowLocalMouseX = -1;
 	if (clearSelection) {
 		if (_state == WidgetState::Default) {
 			_collapsedSelected = -1;
@@ -3375,29 +3378,30 @@ ChosenRow InnerWidget::computeChosenRow() const {
 	if (_state == WidgetState::Default) {
 		if (_selected) {
 			return {
-				_selected->key(),
-				Data::UnreadMessagePosition
+				.key = _selected->key(),
+				.message = Data::UnreadMessagePosition,
 			};
 		}
 	} else if (_state == WidgetState::Filtered) {
 		if (base::in_range(_filteredSelected, 0, _filterResults.size())) {
 			return {
-				_filterResults[_filteredSelected].key(),
-				Data::UnreadMessagePosition,
-				true
+				.key = _filterResults[_filteredSelected].key(),
+				.message = Data::UnreadMessagePosition,
+				.filteredRow = true,
 			};
 		} else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) {
+			const auto peer = _peerSearchResults[_peerSearchSelected]->peer;
 			return {
-				session().data().history(_peerSearchResults[_peerSearchSelected]->peer),
-				Data::UnreadMessagePosition
+				.key = session().data().history(peer),
+				.message = Data::UnreadMessagePosition
 			};
 		} else if (base::in_range(_searchedSelected, 0, _searchResults.size())) {
 			const auto result = _searchResults[_searchedSelected].get();
 			const auto topic = result->topic();
 			const auto item = result->item();
 			return {
-				(topic ? (Entry*)topic : (Entry*)item->history()),
-				item->position()
+				.key = (topic ? (Entry*)topic : (Entry*)item->history()),
+				.message = item->position()
 			};
 		}
 	}
@@ -3413,10 +3417,12 @@ bool InnerWidget::chooseRow(
 	} else if (chooseHashtag()) {
 		return true;
 	}
-	const auto modifyChosenRow = [](
+	const auto modifyChosenRow = [&](
 			ChosenRow row,
 			Qt::KeyboardModifiers modifiers) {
 		row.newWindow = (modifiers & Qt::ControlModifier);
+		row.userpicClick = (_lastRowLocalMouseX >= 0)
+			&& (_lastRowLocalMouseX < _st->nameLeft);
 		return row;
 	};
 	auto chosen = modifyChosenRow(computeChosenRow(), modifiers);
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index b23aefa0d..c72d9667f 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -65,8 +65,9 @@ class IndexedList;
 struct ChosenRow {
 	Key key;
 	Data::MessagePosition message;
-	bool filteredRow = false;
-	bool newWindow = false;
+	bool userpicClick : 1 = false;
+	bool filteredRow : 1 = false;
+	bool newWindow : 1 = false;
 };
 
 enum class SearchRequestType {
@@ -416,6 +417,7 @@ private:
 	FilterId _filterId = 0;
 	bool _mouseSelection = false;
 	std::optional<QPoint> _lastMousePosition;
+	int _lastRowLocalMouseX = -1;
 	Qt::MouseButton _pressButton = Qt::LeftButton;
 
 	Data::Folder *_openedFolder = nullptr;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index c5145c368..77f9aca72 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_forum.h"
 #include "data/data_session.h"
 #include "data/data_peer_values.h"
+#include "data/data_user.h"
 #include "history/history.h"
 #include "history/history_item.h"
 #include "lang/lang_keys.h"
@@ -330,6 +331,7 @@ void Row::ensureCornerBadgeUserpic() const {
 
 void Row::PaintCornerBadgeFrame(
 		not_null<CornerBadgeUserpic*> data,
+		int framePadding,
 		not_null<PeerData*> peer,
 		Ui::VideoUserpic *videoUserpic,
 		Ui::PeerUserpicView &view,
@@ -337,6 +339,19 @@ void Row::PaintCornerBadgeFrame(
 	data->frame.fill(Qt::transparent);
 
 	Painter q(&data->frame);
+	q.translate(framePadding, framePadding);
+	auto hq = std::optional<PainterHighQualityEnabler>();
+	if (data->storiesShown) {
+		hq.emplace(q);
+		const auto line = st::dialogsStoriesFull.lineTwice / 2.;
+		const auto skip = line * 3 / 2.;
+		const auto scale = 1. - (2 * skip / context.st->photoSize);
+		const auto center = context.st->photoSize / 2.;
+		q.save();
+		q.translate(center, center);
+		q.scale(scale, scale);
+		q.translate(-center, -center);
+	}
 	PaintUserpic(
 		q,
 		peer,
@@ -347,9 +362,37 @@ void Row::PaintCornerBadgeFrame(
 		data->frame.width() / data->frame.devicePixelRatio(),
 		context.st->photoSize,
 		context.paused);
+	if (data->storiesShown) {
+		q.restore();
+
+		const auto st = context.st;
+		const auto storiesUnreadBrush = [&] {
+			const auto left = st->padding.left();
+			const auto top = st->padding.top();
+			auto gradient = QLinearGradient(
+				QPoint(left + st->photoSize, top),
+				QPoint(left, top + st->photoSize));
+			gradient.setStops({
+				{ 0., st::groupCallLive1->c },
+				{ 1., st::groupCallMuted1->c },
+			});
+			return QBrush(gradient);
+		};
+		const auto storiesBrush = data->storiesUnread
+			? storiesUnreadBrush()
+			: context.active
+			? st::dialogsUnreadBgMutedActive->b
+			: st::dialogsUnreadBgMuted->b;
+		const auto storiesLine = data->storiesUnread
+			? (st::dialogsStoriesFull.lineTwice / 2.)
+			: (st::dialogsStoriesFull.lineReadTwice / 2.);
+		const auto pen = QPen(storiesBrush, storiesLine);
+		q.setPen(pen);
+		q.drawEllipse(0, 0, st->photoSize, st->photoSize);
+	}
 
 	const auto &manager = data->layersManager;
-	if (const auto p = manager.progressForLayer(kBottomLayer); p) {
+	if (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) {
 		const auto size = context.st->photoSize;
 		if (data->cacheTTL.isNull() && peer->messagesTTL()) {
 			data->cacheTTL = CornerBadgeTTL(peer, view, size);
@@ -364,7 +407,9 @@ void Row::PaintCornerBadgeFrame(
 		return;
 	}
 
-	PainterHighQualityEnabler hq(q);
+	if (!hq) {
+		hq.emplace(q);
+	}
 	q.setCompositionMode(QPainter::CompositionMode_Source);
 
 	const auto size = peer->isUser()
@@ -401,7 +446,14 @@ void Row::paintUserpic(
 	const auto cornerBadgeShown = !_cornerBadgeUserpic
 		? _cornerBadgeShown
 		: !_cornerBadgeUserpic->layersManager.isDisplayedNone();
-	if (!historyForCornerBadge || !cornerBadgeShown) {
+	const auto storiesUser = historyForCornerBadge
+		? historyForCornerBadge->peer->asUser()
+		: nullptr;
+	const auto storiesShown = (storiesUser
+		&& storiesUser->hasActiveStories()) ? 1 : 0;
+	const auto storiesUnread = (storiesShown
+		&& storiesUser->hasUnreadStories()) ? 1 : 0;
+	if (!historyForCornerBadge || (!cornerBadgeShown && !storiesShown)) {
 		BasicRow::paintUserpic(
 			p,
 			peer,
@@ -415,12 +467,12 @@ void Row::paintUserpic(
 	}
 	ensureCornerBadgeUserpic();
 	const auto ratio = style::DevicePixelRatio();
-	const auto added = std::max({
+	const auto framePadding = std::max({
 		-st::dialogsCallBadgeSkip.x(),
 		-st::dialogsCallBadgeSkip.y(),
-		0 });
-	const auto frameSide = (context.st->photoSize + added)
-		* style::DevicePixelRatio();
+		st::lineWidth * 2 });
+	const auto frameSide = (2 * framePadding + context.st->photoSize)
+		* ratio;
 	const auto frameSize = QSize(frameSide, frameSide);
 	if (_cornerBadgeUserpic->frame.size() != frameSize) {
 		_cornerBadgeUserpic->frame = QImage(
@@ -432,6 +484,7 @@ void Row::paintUserpic(
 	key.first += peer->messagesTTL();
 	const auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1;
 	const auto paletteVersion = style::PaletteVersion();
+	const auto active = context.active ? 1 : 0;
 	const auto keyChanged = (_cornerBadgeUserpic->key != key)
 		|| (_cornerBadgeUserpic->paletteVersion != paletteVersion);
 	if (keyChanged) {
@@ -439,24 +492,29 @@ void Row::paintUserpic(
 	}
 	if (keyChanged
 		|| !_cornerBadgeUserpic->layersManager.isFinished()
-		|| _cornerBadgeUserpic->active != context.active
+		|| _cornerBadgeUserpic->active != active
 		|| _cornerBadgeUserpic->frameIndex != frameIndex
+		|| _cornerBadgeUserpic->storiesShown != storiesShown
+		|| _cornerBadgeUserpic->storiesUnread != storiesUnread
 		|| videoUserpic) {
 		_cornerBadgeUserpic->key = key;
 		_cornerBadgeUserpic->paletteVersion = paletteVersion;
-		_cornerBadgeUserpic->active = context.active;
+		_cornerBadgeUserpic->active = active;
+		_cornerBadgeUserpic->storiesShown = storiesShown;
+		_cornerBadgeUserpic->storiesUnread = storiesUnread;
 		_cornerBadgeUserpic->frameIndex = frameIndex;
 		_cornerBadgeUserpic->layersManager.markFrameShown();
 		PaintCornerBadgeFrame(
 			_cornerBadgeUserpic.get(),
+			framePadding,
 			peer,
 			videoUserpic,
 			userpicView(),
 			context);
 	}
 	p.drawImage(
-		context.st->padding.left(),
-		context.st->padding.top(),
+		context.st->padding.left() - framePadding,
+		context.st->padding.top() - framePadding,
 		_cornerBadgeUserpic->frame);
 	if (historyForCornerBadge->peer->isUser()) {
 		return;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h
index d79c2efdf..735b03004 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.h
@@ -167,11 +167,13 @@ private:
 	struct CornerBadgeUserpic {
 		InMemoryKey key;
 		CornerLayersManager layersManager;
-		int paletteVersion = 0;
-		int frameIndex = -1;
-		bool active = false;
 		QImage frame;
 		QImage cacheTTL;
+		int frameIndex = -1;
+		int paletteVersion : 24 = 0;
+		int storiesShown : 1 = 0;
+		int storiesUnread : 1 = 0;
+		int active : 1 = 0;
 	};
 
 	void setCornerBadgeShown(
@@ -180,6 +182,7 @@ private:
 	void ensureCornerBadgeUserpic() const;
 	static void PaintCornerBadgeFrame(
 		not_null<CornerBadgeUserpic*> data,
+		int framePadding,
 		not_null<PeerData*> peer,
 		Ui::VideoUserpic *videoUserpic,
 		Ui::PeerUserpicView &view,
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 6c3e2e059..72e8b9ea9 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -503,6 +503,14 @@ void Widget::chosenRow(const ChosenRow &row) {
 		return;
 	} else if (history) {
 		const auto peer = history->peer;
+		if (const auto user = history->peer->asUser()) {
+			if (row.message.fullId.msg == ShowAtUnreadMsgId) {
+				if (row.userpicClick && user->hasActiveStories()) {
+					controller()->openPeerStories(user->id);
+					return;
+				}
+			}
+		}
 		const auto showAtMsgId = controller()->uniqueChatsInSearchResults()
 			? ShowAtUnreadMsgId
 			: row.message.fullId.msg;

From a0ffa1588597db999dcdf49f532b03a822883141 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 11:55:16 +0400
Subject: [PATCH 139/259] Update story mention layout, add outline.

Also use uint32 for bool-bitfields, otherwise:

int a : 1 = 0;
...
const auto test = true;
const auto b = test ? 1 : 0;
if (a != b) {
    a = b;
    ...
}
Assert(a == b); // Violation, because a == -1, not 1 (after a = b).
---
 .../boxes/peer_list_controllers.cpp           |  2 +-
 Telegram/SourceFiles/data/data_stories.cpp    | 63 ++++++++++++++-----
 Telegram/SourceFiles/data/data_stories.h      |  9 ++-
 Telegram/SourceFiles/dialogs/dialogs_row.cpp  |  3 +
 Telegram/SourceFiles/dialogs/dialogs_row.h    | 14 ++---
 .../dialogs/ui/dialogs_stories_content.cpp    |  6 +-
 .../dialogs/ui/dialogs_stories_list.h         |  6 +-
 .../history/view/media/history_view_photo.cpp |  4 +-
 .../history/view/media/history_view_photo.h   |  8 +--
 .../view/media/history_view_service_box.cpp   |  8 ++-
 .../view/media/history_view_service_box.h     |  4 ++
 .../view/media/history_view_story_mention.cpp | 55 +++++++++++++---
 .../view/media/history_view_story_mention.h   |  8 ++-
 Telegram/SourceFiles/ui/chat/chat.style       |  9 ++-
 Telegram/SourceFiles/ui/userpic_view.h        |  4 +-
 15 files changed, 151 insertions(+), 52 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 745e5eef8..5137b5c99 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -163,7 +163,7 @@ StoriesRow::StoriesRow(
 void StoriesRow::applySegments(const Dialogs::Stories::Element &element) {
 	Expects(element.unreadCount <= element.count);
 
-	_count = std::max(element.count, 1);
+	_count = int(std::max(element.count, 1U));
 	_unreadCount = element.unreadCount;
 	refreshSegments();
 }
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 5b265f8a7..6c6991bd5 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -75,9 +75,9 @@ StoriesSourceInfo StoriesSource::info() const {
 	return {
 		.id = user->id,
 		.last = ids.empty() ? 0 : ids.back().date,
-		.count = std::min(int(ids.size()), kMaxSegmentsCount),
-		.unreadCount = std::min(unreadCount(), kMaxSegmentsCount),
-		.premium = user->isPremium() ? 1 : 0,
+		.count = uint32(std::min(int(ids.size()), kMaxSegmentsCount)),
+		.unreadCount = uint32(std::min(unreadCount(), kMaxSegmentsCount)),
+		.premium = user->isPremium() ? 1U : 0U,
 	};
 }
 
@@ -913,9 +913,25 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
 
 bool Stories::bumpReadTill(PeerId peerId, StoryId maxReadTill) {
 	auto &till = _readTill[peerId];
+	auto refreshItems = std::vector<StoryId>();
+	const auto guard = gsl::finally([&] {
+		for (const auto id : refreshItems) {
+			_owner->refreshStoryItemViews({ peerId, id });
+		}
+	});
 	if (till < maxReadTill) {
+		const auto from = till;
 		till = maxReadTill;
 		updateUserStoriesState(_owner->peer(peerId));
+		const auto i = _stories.find(peerId);
+		if (i != end(_stories)) {
+			refreshItems = ranges::make_subrange(
+				i->second.lower_bound(from + 1),
+				i->second.lower_bound(till + 1)
+			) | ranges::views::transform([](const auto &pair) {
+				return pair.first;
+			}) | ranges::to_vector;
+		}
 	}
 	const auto i = _all.find(peerId);
 	if (i == end(_all) || i->second.readTill >= maxReadTill) {
@@ -1443,21 +1459,40 @@ std::optional<Stories::PeerSourceState> Stories::peerSourceState(
 				(i != end(_readTill)) ? i->second : 0),
 		};
 	}
-	if (!_readTillsRequestId) {
-		const auto api = &_owner->session().api();
-		_readTillsRequestId = api->request(MTPstories_GetAllReadUserStories(
-		)).done([=](const MTPUpdates &result) {
-			_readTillReceived = true;
-			api->applyUpdates(result);
-			for (auto &[peer, maxId] : base::take(_pendingUserStateMaxId)) {
-				updateUserStoriesState(peer);
-			}
-		}).send();
-	}
+	requestReadTills();
 	_pendingUserStateMaxId[peer] = storyMaxId;
 	return std::nullopt;
 }
 
+void Stories::requestReadTills() {
+	if (_readTillReceived || _readTillsRequestId) {
+		return;
+	}
+	const auto api = &_owner->session().api();
+	_readTillsRequestId = api->request(MTPstories_GetAllReadUserStories(
+	)).done([=](const MTPUpdates &result) {
+		_readTillReceived = true;
+		api->applyUpdates(result);
+		for (auto &[peer, maxId] : base::take(_pendingUserStateMaxId)) {
+			updateUserStoriesState(peer);
+		}
+		for (const auto &storyId : base::take(_pendingReadTillItems)) {
+			_owner->refreshStoryItemViews(storyId);
+		}
+	}).send();
+}
+
+bool Stories::isUnread(not_null<Story*> story) {
+	const auto till = _readTill.find(story->peer()->id);
+	if (till == end(_readTill) && !_readTillReceived) {
+		requestReadTills();
+		_pendingReadTillItems.emplace(story->fullId());
+		return false;
+	}
+	const auto readTill = (till != end(_readTill)) ? till->second : 0;
+	return (story->id() > readTill);
+}
+
 void Stories::updateUserStoriesState(not_null<PeerData*> peer) {
 	const auto till = _readTill.find(peer->id);
 	const auto readTill = (till != end(_readTill)) ? till->second : 0;
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index c15b1fc6b..e1bb0dcb6 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -41,9 +41,9 @@ struct StoriesIds {
 struct StoriesSourceInfo {
 	PeerId id = 0;
 	TimeId last = 0;
-	int count : 15 = 0;
-	int unreadCount : 15 = 0;
-	int premium : 1 = 0;
+	uint32 count : 15 = 0;
+	uint32 unreadCount : 15 = 0;
+	uint32 premium : 1 = 0;
 
 	friend inline bool operator==(
 		StoriesSourceInfo,
@@ -207,6 +207,7 @@ public:
 	[[nodiscard]] std::optional<PeerSourceState> peerSourceState(
 		not_null<PeerData*> peer,
 		StoryId storyMaxId);
+	[[nodiscard]] bool isUnread(not_null<Story*> story);
 
 private:
 	struct Saved {
@@ -241,6 +242,7 @@ private:
 	void savedStateUpdated(not_null<Story*> story);
 	void sort(StorySourcesList list);
 	bool bumpReadTill(PeerId peerId, StoryId maxReadTill);
+	void requestReadTills();
 
 	[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
 		not_null<Story*> story);
@@ -329,6 +331,7 @@ private:
 	int _preloadingMainSourcesCounter = 0;
 
 	base::flat_map<PeerId, StoryId> _readTill;
+	base::flat_set<FullStoryId> _pendingReadTillItems;
 	base::flat_map<not_null<PeerData*>, StoryId> _pendingUserStateMaxId;
 	mtpRequestId _readTillsRequestId = 0;
 	bool _readTillReceived = false;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index 77f9aca72..326ec6db4 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -367,6 +367,9 @@ void Row::PaintCornerBadgeFrame(
 
 		const auto st = context.st;
 		const auto storiesUnreadBrush = [&] {
+			if (context.active) {
+				return st::dialogsUnreadBgActive->b;
+			}
 			const auto left = st->padding.left();
 			const auto top = st->padding.top();
 			auto gradient = QLinearGradient(
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h
index 735b03004..78e638093 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.h
@@ -170,10 +170,10 @@ private:
 		QImage frame;
 		QImage cacheTTL;
 		int frameIndex = -1;
-		int paletteVersion : 24 = 0;
-		int storiesShown : 1 = 0;
-		int storiesUnread : 1 = 0;
-		int active : 1 = 0;
+		uint32 paletteVersion : 24 = 0;
+		uint32 storiesShown : 1 = 0;
+		uint32 storiesUnread : 1 = 0;
+		uint32 active : 1 = 0;
 	};
 
 	void setCornerBadgeShown(
@@ -192,9 +192,9 @@ private:
 	mutable std::unique_ptr<CornerBadgeUserpic> _cornerBadgeUserpic;
 	int _top = 0;
 	int _height = 0;
-	int _index : 30 = 0;
-	int _cornerBadgeShown : 1 = 0;
-	int _topicJumpRipple : 1 = 0;
+	uint32 _index : 30 = 0;
+	uint32 _cornerBadgeShown : 1 = 0;
+	uint32 _topicJumpRipple : 1 = 0;
 
 };
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index 329f61296..b882842ca 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -353,7 +353,7 @@ Content State::next() {
 			.thumbnail = std::move(userpic),
 			.count = info.count,
 			.unreadCount = info.unreadCount,
-			.skipSmall = user->isSelf() ? 1 : 0,
+			.skipSmall = user->isSelf() ? 1U : 0U,
 		});
 	}
 	return result;
@@ -424,8 +424,8 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 							result.elements.push_back({
 								.id = uint64(id),
 								.thumbnail = MakeStoryThumbnail(*maybe),
-								.count = 1,
-								.unreadCount = (id > readTill) ? 1 : 0,
+								.count = 1U,
+								.unreadCount = (id > readTill) ? 1U : 0U,
 							});
 						}
 					} else if (maybe.error() == Data::NoStory::Unknown) {
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 9bbca05b0..a3b44d04a 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -36,9 +36,9 @@ struct Element {
 	uint64 id = 0;
 	QString name;
 	std::shared_ptr<Thumbnail> thumbnail;
-	int count : 15 = 0;
-	int unreadCount : 15 = 0;
-	int skipSmall : 1 = 0;
+	uint32 count : 15 = 0;
+	uint32 unreadCount : 15 = 0;
+	uint32 skipSmall : 1 = 0;
 
 	friend inline bool operator==(
 		const Element &a,
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
index 960a4cd16..a7f08a50d 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
@@ -164,7 +164,7 @@ void Photo::unloadHeavyPart() {
 
 QSize Photo::countOptimalSize() {
 	if (_serviceWidth > 0) {
-		return { _serviceWidth, _serviceWidth };
+		return { int(_serviceWidth), int(_serviceWidth) };
 	}
 
 	if (_parent->media() != this) {
@@ -209,7 +209,7 @@ QSize Photo::countOptimalSize() {
 
 QSize Photo::countCurrentSize(int newWidth) {
 	if (_serviceWidth) {
-		return { _serviceWidth, _serviceWidth };
+		return { int(_serviceWidth), int(_serviceWidth) };
 	}
 	const auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize);
 	const auto minWidth = std::clamp(
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h
index 31d2b08bc..f6d7062f0 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h
@@ -167,10 +167,10 @@ private:
 	const std::unique_ptr<MediaSpoiler> _spoiler;
 	mutable QImage _imageCache;
 	mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
-	int _serviceWidth : 29 = 0;
-	mutable int _imageCacheForum : 1 = 0;
-	mutable int _imageCacheBlurred : 1 = 0;
-	mutable int _story : 1 = 0;
+	uint32 _serviceWidth : 29 = 0;
+	mutable uint32 _imageCacheForum : 1 = 0;
+	mutable uint32 _imageCacheBlurred : 1 = 0;
+	mutable uint32 _story : 1 = 0;
 
 };
 
diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp
index e425bdf65..57369d1fa 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp
@@ -81,8 +81,7 @@ ServiceBox::ServiceBox(
 		+ _subtitle.countHeight(_maxWidth)
 		+ (_button.empty()
 			? 0
-			: (st::msgServiceGiftBoxButtonMargins.top()
-				+ _button.size.height()))
+			: (_content->buttonSkip() + _button.size.height()))
 		+ st::msgServiceGiftBoxButtonMargins.bottom()))
 , _innerSize(_size - QSize(0, st::msgServiceGiftBoxTopSkip)) {
 }
@@ -165,6 +164,11 @@ TextState ServiceBox::textState(QPoint point, StateRequest request) const {
 		if (rect.contains(point)) {
 			result.link = _button.link;
 			_button.lastPoint = point - rect.topLeft();
+		} else if (contentRect().contains(point)) {
+			if (!_contentLink) {
+				_contentLink = _content->createViewLink();
+			}
+			result.link = _contentLink;
 		}
 	}
 	return result;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h
index 94347d401..1ae0c30ea 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h
@@ -23,6 +23,9 @@ public:
 	[[nodiscard]] virtual QSize size() = 0;
 	[[nodiscard]] virtual QString title() = 0;
 	[[nodiscard]] virtual TextWithEntities subtitle() = 0;
+	[[nodiscard]] virtual int buttonSkip() {
+		return top();
+	}
 	[[nodiscard]] virtual QString button() = 0;
 	virtual void draw(
 		Painter &p,
@@ -84,6 +87,7 @@ private:
 
 	const not_null<Element*> _parent;
 	const std::unique_ptr<ServiceBoxContent> _content;
+	mutable ClickHandlerPtr _contentLink;
 
 	struct Button {
 		void drawBg(QPainter &p) const;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
index fb9f62f29..a4d255fd2 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "main/main_session.h"
 #include "window/window_session_controller.h"
 #include "ui/boxes/confirm_box.h"
+#include "ui/chat/chat_style.h"
 #include "ui/text/text_utilities.h"
 #include "ui/toast/toast.h"
 #include "ui/painter.h"
@@ -39,13 +40,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace HistoryView {
 namespace {
 
+constexpr auto kReadOutlineAlpha = 0.5;
+
 } // namespace
 
 StoryMention::StoryMention(
 	not_null<Element*> parent,
 	not_null<Data::Story*> story)
 : _parent(parent)
-, _story(story) {
+, _story(story)
+, _unread(story->owner().stories().isUnread(story) ? 1 : 0) {
 }
 
 StoryMention::~StoryMention() = default;
@@ -62,6 +66,10 @@ QString StoryMention::title() {
 	return QString();
 }
 
+int StoryMention::buttonSkip() {
+	return st::storyMentionButtonSkip;
+}
+
 QString StoryMention::button() {
 	return tr::lng_action_story_mention_button(tr::now);
 }
@@ -86,7 +94,7 @@ void StoryMention::draw(
 		Painter &p,
 		const PaintContext &context,
 		const QRect &geometry) {
-	const auto showStory = !_story->forbidsForward();
+	const auto showStory = _story->forbidsForward() ? 0 : 1;
 	if (!_thumbnail || _thumbnailFromStory != showStory) {
 		using namespace Dialogs::Stories;
 		const auto item = _parent->data();
@@ -97,18 +105,49 @@ void StoryMention::draw(
 				? history->session().user()
 				: history->peer);
 		_thumbnailFromStory = showStory;
-		_subscribed = false;
+		_subscribed = 0;
 	}
 	if (!_subscribed) {
 		_thumbnail->subscribeToUpdates([=] {
 			_parent->data()->history()->owner().requestViewRepaint(_parent);
 		});
-		_subscribed = true;
+		_subscribed = 1;
 	}
 
+	const auto padding = (geometry.width() - st::storyMentionSize) / 2;
+	const auto size = geometry.width() - 2 * padding;
 	p.drawImage(
-		geometry.topLeft(),
-		_thumbnail->image(st::msgServicePhotoWidth));
+		geometry.topLeft() + QPoint(padding, padding),
+		_thumbnail->image(size));
+
+	const auto thumbnail = geometry.marginsRemoved(
+		QMargins(padding, padding, padding, padding));
+	const auto added = 0.5 * (_unread
+		? st::storyMentionUnreadSkipTwice
+		: st::storyMentionReadSkipTwice);
+	const auto outline = thumbnail.marginsAdded(
+		QMargins(added, added, added, added));
+	if (_unread && _paletteVersion != style::PaletteVersion()) {
+		_paletteVersion = style::PaletteVersion();
+		auto gradient = QLinearGradient(
+			outline.topRight(),
+			outline.bottomLeft());
+		gradient.setStops({
+			{ 0., st::groupCallLive1->c },
+			{ 1., st::groupCallMuted1->c },
+		});
+		_unreadBrush = QBrush(gradient);
+	}
+	auto readColor = context.st->msgServiceFg()->c;
+	readColor.setAlphaF(std::min(readColor.alphaF(), kReadOutlineAlpha));
+	p.setPen(QPen(
+		_unread ? _unreadBrush : QBrush(readColor),
+		0.5 * (_unread
+			? st::storyMentionUnreadStrokeTwice
+			: st::storyMentionReadStrokeTwice)));
+	p.setBrush(Qt::NoBrush);
+	auto hq = PainterHighQualityEnabler(p);
+	p.drawEllipse(outline);
 }
 
 void StoryMention::stickerClearLoopPlayed() {
@@ -121,12 +160,12 @@ std::unique_ptr<StickerPlayer> StoryMention::stickerTakePlayer(
 }
 
 bool StoryMention::hasHeavyPart() {
-	return _subscribed;
+	return _subscribed != 0;
 }
 
 void StoryMention::unloadHeavyPart() {
 	if (_subscribed) {
-		_subscribed = false;
+		_subscribed = 0;
 		_thumbnail->subscribeToUpdates(nullptr);
 	}
 }
diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.h b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
index 9cdacebcb..fea24787f 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
@@ -32,6 +32,7 @@ public:
 	QSize size() override;
 	QString title() override;
 	TextWithEntities subtitle() override;
+	int buttonSkip() override;
 	QString button() override;
 	void draw(
 		Painter &p,
@@ -57,8 +58,11 @@ private:
 	const not_null<Element*> _parent;
 	const not_null<Data::Story*> _story;
 	std::shared_ptr<Thumbnail> _thumbnail;
-	bool _thumbnailFromStory = false;
-	bool _subscribed = false;
+	QBrush _unreadBrush;
+	uint32 _paletteVersion : 29 = 0;
+	uint32 _thumbnailFromStory : 1 = 0;
+	uint32 _subscribed : 1 = 0;
+	uint32 _unread : 1 = 0;
 
 };
 
diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style
index 56f84a8e6..4119099de 100644
--- a/Telegram/SourceFiles/ui/chat/chat.style
+++ b/Telegram/SourceFiles/ui/chat/chat.style
@@ -841,7 +841,7 @@ searchInChatPeerList: PeerList(defaultPeerList) {
 }
 
 msgServiceGiftBoxSize: size(206px, 231px); // Plus msgServiceGiftBoxTopSkip.
-msgServiceGiftBoxRadius: 12px;
+msgServiceGiftBoxRadius: 20px;
 msgServiceGiftBoxTopSkip: 4px;
 msgServiceGiftBoxButtonHeight: 32px;
 msgServiceGiftBoxButtonPadding: margins(2px, 0px, 2px, 0px);
@@ -915,3 +915,10 @@ backgroundSwitchToLight: IconButton(backgroundSwitchToDark) {
 	icon: icon {{ "menu/header_mode_day", boxTitleCloseFg }};
 	iconOver: icon {{ "menu/header_mode_day", boxTitleCloseFgOver }};
 }
+
+storyMentionSize: 80px;
+storyMentionUnreadSkipTwice: 8px;
+storyMentionUnreadStrokeTwice: 6px;
+storyMentionReadSkipTwice: 7px;
+storyMentionReadStrokeTwice: 3px;
+storyMentionButtonSkip: 5px;
diff --git a/Telegram/SourceFiles/ui/userpic_view.h b/Telegram/SourceFiles/ui/userpic_view.h
index e05162f95..6e50127d7 100644
--- a/Telegram/SourceFiles/ui/userpic_view.h
+++ b/Telegram/SourceFiles/ui/userpic_view.h
@@ -25,8 +25,8 @@ struct PeerUserpicView {
 	QImage cached;
 	std::shared_ptr<QImage> cloud;
 	base::weak_ptr<const EmptyUserpic> empty;
-	int paletteVersion : 31 = 0;
-	int forum : 1 = 0;
+	uint32 paletteVersion : 31 = 0;
+	uint32 forum : 1 = 0;
 };
 
 [[nodiscard]] bool PeerUserpicLoading(const PeerUserpicView &view);

From e41dba5fb259abc441215a1d22e04f800c490080 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 11:58:59 +0400
Subject: [PATCH 140/259] Cache title controls layout.

---
 Telegram/lib_ui | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index c3aab1bd1..6abad69ca 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit c3aab1bd141101c32393edc07032113ae4f1122a
+Subproject commit 6abad69ca6de8b592d23902b7bc2bf4190af2dcb

From 57b7391f534211f18c1b9e41140fc19c6f6e5c97 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 12:55:22 +0400
Subject: [PATCH 141/259] Improve design of contacts according to mockups.

---
 Telegram/SourceFiles/boxes/boxes.style                | 11 +++++++++++
 .../boxes/filters/edit_filter_chats_list.cpp          |  2 +-
 Telegram/SourceFiles/boxes/peer_list_controllers.cpp  |  4 +++-
 Telegram/SourceFiles/dialogs/dialogs_row.cpp          |  2 +-
 Telegram/SourceFiles/ui/effects/round_checkbox.cpp    |  9 ++++++---
 Telegram/SourceFiles/window/window.style              |  7 ++++---
 Telegram/lib_ui                                       |  2 +-
 7 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index 4ba5c01b5..3758d8a61 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -943,8 +943,17 @@ requestsBoxList: PeerList(peerListBox) {
 	item: requestsBoxItem;
 }
 contactsWithStories: PeerList(peerListBox) {
+	padding: margins(0px, 0px, 0px, 0px);
 	item: PeerListItem(peerListBoxItem) {
+		height: 52px;
+		photoPosition: point(18px, 5px);
+		namePosition: point(70px, 7px);
+		statusPosition: point(70px, 27px);
+
 		checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
+			selectExtendTwice: 1px;
+			imageRadius: 21px;
+			imageSmallRadius: 19px;
 			check: RoundCheckbox(defaultPeerListCheck) {
 				size: 0px;
 			}
@@ -952,6 +961,8 @@ contactsWithStories: PeerList(peerListBox) {
 		nameFgChecked: contactsNameFg;
 	}
 }
+storiesReadLineTwice: 2px;
+storiesUnreadLineTwice: 4px;
 requestsAcceptButton: RoundButton(defaultActiveButton) {
 	width: -28px;
 	height: 30px;
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
index 8e393c583..6cce77d60 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
@@ -309,7 +309,7 @@ object_ptr<Ui::RpWidget> CreatePeerListSectionSubtitle(
 		rpl::producer<QString> text) {
 	auto result = object_ptr<Ui::FixedHeightWidget>(
 		parent,
-		st::searchedBarHeight);
+		st::windowFilterChatsSectionSubtitleHeight);
 
 	const auto raw = result.data();
 	raw->paintRequest(
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 5137b5c99..3ea891511 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -230,7 +230,9 @@ void StoriesController::rowClicked(not_null<PeerListRow*> row) {
 base::unique_qptr<Ui::PopupMenu> StoriesController::rowContextMenu(
 		QWidget *parent,
 		not_null<PeerListRow*> row) {
-	auto result = base::make_unique_q<Ui::PopupMenu>(parent);
+	auto result = base::make_unique_q<Ui::PopupMenu>(
+		parent,
+		st::popupMenuWithIcons);
 
 	Dialogs::Stories::FillSourceMenu(_window, {
 		.id = row->id(),
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index 326ec6db4..102ad6dd5 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -368,7 +368,7 @@ void Row::PaintCornerBadgeFrame(
 		const auto st = context.st;
 		const auto storiesUnreadBrush = [&] {
 			if (context.active) {
-				return st::dialogsUnreadBgActive->b;
+				return st::dialogsUnreadBgMutedActive->b;
 			}
 			const auto left = st->padding.left();
 			const auto top = st->padding.top();
diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
index 4d2a03f77..8a6b7f4c2 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
@@ -399,6 +399,9 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 			_st.imageRadius * 2,
 			_st.imageRadius * 2,
 			outerWidth);
+		const auto add = _st.selectExtendTwice / 2.;
+		const auto outline = QRectF(rect).marginsAdded({
+			add, add, add, add });
 		if (segments < 2) {
 			const auto radius = _roundingRadius
 				? _roundingRadius(_st.imageRadius * 2)
@@ -408,9 +411,9 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 				segments ? _segments.front().width : _st.selectWidth);
 			p.setPen(pen);
 			if (!radius) {
-				p.drawEllipse(rect);
+				p.drawEllipse(outline);
 			} else {
-				p.drawRoundedRect(rect, *radius, *radius);
+				p.drawRoundedRect(outline, *radius, *radius);
 			}
 		} else {
 			const auto small = 160;
@@ -429,7 +432,7 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 					Qt::RoundCap));
 				const auto from = int(base::SafeRound(start));
 				const auto till = int(base::SafeRound(start + length));
-				p.drawArc(rect, from, till - from);
+				p.drawArc(outline, from, till - from);
 				start += length + separator;
 			}
 		}
diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style
index b2acd7367..2d4abf372 100644
--- a/Telegram/SourceFiles/window/window.style
+++ b/Telegram/SourceFiles/window/window.style
@@ -165,7 +165,7 @@ mainMenuVersionLabel: FlatLabel(mainMenuTelegramLabel) {
 mainMenuVersionBottom: 17px;
 
 mainMenuToggleSize: 6px;
-mainMenuToggleFourStrokes: 4px;
+mainMenuToggleFourStrokes: 3px;
 mainMenuTogglePosition: point(30px, 30px);
 
 themeEditorSampleSize: size(90px, 51px);
@@ -305,13 +305,14 @@ windowFilterTypeBots: icon {{ "folders/folders_type_bots", historyPeerUserpicFg
 windowFilterTypeNoMuted: icon {{ "folders/folders_type_muted", historyPeerUserpicFg }};
 windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeerUserpicFg }};
 windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }};
+windowFilterChatsSectionSubtitleHeight: 28px;
 windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) {
 	style: TextStyle(defaultTextStyle) {
-		font: searchedBarFont;
+		font: semiboldFont;
 	}
 	textFg: searchedBarFg;
 }
-windowFilterChatsSectionSubtitlePadding: margins(17px, 7px, 17px, 7px);
+windowFilterChatsSectionSubtitlePadding: margins(22px, 5px, 22px, 5px);
 
 windowArchiveToast: Toast(defaultToast) {
 	minWidth: boxWideWidth;
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 6abad69ca..d431d803c 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 6abad69ca6de8b592d23902b7bc2bf4190af2dcb
+Subproject commit d431d803c8b347c192d1bc9b6a206d2622a10748

From ee507722bada71de17a3afe87f92cec8e2b5118b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 13:12:38 +0400
Subject: [PATCH 142/259] Add stories counter to hidden stories sources.

---
 Telegram/Resources/langs/lang.strings                | 4 ++++
 Telegram/SourceFiles/boxes/peer_list_controllers.cpp | 6 ++++++
 2 files changed, 10 insertions(+)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index a816e9883..3130497ee 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2410,6 +2410,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_contacts_by_online" = "Sorted by last seen time";
 "lng_contacts_by_name" = "Sorted by name";
 "lng_contacts_hidden_stories" = "Hidden Stories";
+"lng_contacts_stories_status#one" = "{count} story";
+"lng_contacts_stories_status#other" = "{count} stories";
+"lng_contacts_stories_status_new#one" = "{count} new story";
+"lng_contacts_stories_status_new#other" = "{count} new stories";
 "lng_contact_not_joined" = "Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\n\nWe will notify you about any of your contacts who join Telegram.";
 "lng_try_other_contact" = "Try someone else";
 "lng_create_group_link" = "Link";
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 3ea891511..7090a018d 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -166,6 +166,12 @@ void StoriesRow::applySegments(const Dialogs::Stories::Element &element) {
 	_count = int(std::max(element.count, 1U));
 	_unreadCount = element.unreadCount;
 	refreshSegments();
+	setCustomStatus(_unreadCount
+		? tr::lng_contacts_stories_status_new(
+			tr::now,
+			lt_count,
+			_unreadCount)
+		: tr::lng_contacts_stories_status(tr::now, lt_count, _count));
 }
 
 void StoriesRow::updateGradient(QBrush unread) {

From 12fe0a836a5216e0932534dd677f0c2134b38bf9 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 16:18:55 +0400
Subject: [PATCH 143/259] Apply editions in realtime, show badge.

---
 Telegram/SourceFiles/data/data_story.cpp      |  7 +++++++
 Telegram/SourceFiles/data/data_story.h        |  2 ++
 .../stories/media_stories_controller.cpp      | 20 +++++++++++++++----
 .../media/stories/media_stories_delegate.h    |  2 ++
 .../media/stories/media_stories_header.cpp    | 13 ++++++++++--
 .../media/stories/media_stories_header.h      |  1 +
 .../media/view/media_view_overlay_widget.cpp  | 14 +++++++++++++
 .../media/view/media_view_overlay_widget.h    |  1 +
 8 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index dd11bcc0e..af8b50c8a 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -260,6 +260,10 @@ bool Story::forbidsForward() const {
 	return _noForwards;
 }
 
+bool Story::edited() const {
+	return _edited;
+}
+
 bool Story::canDownload() const {
 	return !forbidsForward() || _peer->isSelf();
 }
@@ -369,6 +373,7 @@ void Story::applyViewsSlice(
 
 bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 	const auto pinned = data.is_pinned();
+	const auto edited = data.is_edited();
 	const auto isPublic = data.is_public();
 	const auto closeFriends = data.is_close_friends();
 	const auto noForwards = data.is_noforwards();
@@ -395,6 +400,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 
 	const auto changed = (_media != media)
 		|| (_pinned != pinned)
+		|| (_edited != edited)
 		|| (_isPublic != isPublic)
 		|| (_closeFriends != closeFriends)
 		|| (_noForwards != noForwards)
@@ -405,6 +411,7 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 		return false;
 	}
 	_media = std::move(media);
+	_edited = edited;
 	_pinned = pinned;
 	_isPublic = isPublic;
 	_closeFriends = closeFriends;
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
index 183e46808..d80272e67 100644
--- a/Telegram/SourceFiles/data/data_story.h
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -89,6 +89,7 @@ public:
 	[[nodiscard]] bool isPublic() const;
 	[[nodiscard]] bool closeFriends() const;
 	[[nodiscard]] bool forbidsForward() const;
+	[[nodiscard]] bool edited() const;
 
 	[[nodiscard]] bool canDownload() const;
 	[[nodiscard]] bool canShare() const;
@@ -128,6 +129,7 @@ private:
 	bool _isPublic : 1 = false;
 	bool _closeFriends : 1 = false;
 	bool _noForwards : 1 = false;
+	bool _edited : 1 = false;
 
 };
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index cf411bc7b..e1e6cc875 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -741,20 +741,24 @@ void Controller::show(
 		_slider->raise();
 	}
 
-	if (_shown == storyId) {
+	_captionText = story->caption();
+	_captionFullView = nullptr;
+	_header->show({
+		.user = user,
+		.date = story->date(),
+		.edited = story->edited(),
+	});
+	if (_shown == storyId && _session == &story->session()) {
 		return;
 	}
 	_shown = storyId;
 	_viewed = false;
-	_captionText = story->caption();
-	_captionFullView = nullptr;
 	invalidate_weak_ptrs(&_viewsLoadGuard);
 	_reactions->hide();
 	if (_replyFocused) {
 		unfocusReply();
 	}
 
-	_header->show({ .user = user, .date = story->date() });
 	_replyArea->show({
 		.user = unsupported ? nullptr : user,
 		.id = story->id(),
@@ -781,6 +785,14 @@ void Controller::show(
 				checkWaitingFor();
 			}
 		}, _sessionLifetime);
+		session->changes().storyUpdates(
+			Data::StoryUpdate::Flag::Edited
+		) | rpl::filter([=](const Data::StoryUpdate &update) {
+			return (update.story == this->story());
+		}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
+			show(update.story, _context);
+			_delegate->storiesRedisplay(update.story);
+		}, _sessionLifetime);
 		_sessionLifetime.add([=] {
 			session->data().stories().setPreloadingInViewer({});
 		});
diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h
index 6dfbf3acd..f42e3c463 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h
@@ -13,6 +13,7 @@ struct FileChosen;
 } // namespace ChatHelpers
 
 namespace Data {
+class Story;
 struct StoriesContext;
 } // namespace Data
 
@@ -49,6 +50,7 @@ public:
 		-> rpl::producer<ChatHelpers::FileChosen> = 0;
 	[[nodiscard]] virtual auto storiesCachedReactionIconFactory()
 		-> HistoryView::Reactions::CachedIconFactory & = 0;
+	virtual void storiesRedisplay(not_null<Data::Story*> story) = 0;
 	virtual void storiesJumpTo(
 		not_null<Main::Session*> session,
 		FullStoryId id,
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index dc33da5f4..fd8f21b33 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -75,6 +75,15 @@ struct Timestamp {
 	return { Ui::FormatDateTime(whenFull) };
 }
 
+[[nodiscard]] Timestamp ComposeDetails(HeaderData data, TimeId now) {
+	auto result = ComposeTimestamp(data.date, now);
+	if (data.edited) {
+		result.text.append(
+			QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now));
+	}
+	return result;
+}
+
 } // namespace
 
 Header::Header(not_null<Controller*> controller)
@@ -123,7 +132,7 @@ void Header::show(HeaderData data) {
 			raw->setGeometry(layout.header);
 		}, raw->lifetime());
 	}
-	auto timestamp = ComposeTimestamp(data.date, base::unixtime::now());
+	auto timestamp = ComposeDetails(data, base::unixtime::now());
 	_date = std::make_unique<Ui::FlatLabel>(
 		_widget.get(),
 		std::move(timestamp.text),
@@ -148,7 +157,7 @@ void Header::updateDateText() {
 	if (!_date || !_data || !_data->date) {
 		return;
 	}
-	auto timestamp = ComposeTimestamp(_data->date, base::unixtime::now());
+	auto timestamp = ComposeDetails(*_data, base::unixtime::now());
 	_date->setText(timestamp.text);
 	if (timestamp.changes > 0) {
 		_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index 97d62b250..cab943464 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -22,6 +22,7 @@ class Controller;
 struct HeaderData {
 	not_null<UserData*> user;
 	TimeId date = 0;
+	bool edited = false;
 
 	friend inline auto operator<=>(HeaderData, HeaderData) = default;
 	friend inline bool operator==(HeaderData, HeaderData) = default;
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 12b6e0332..9690f150e 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -4167,6 +4167,20 @@ void OverlayWidget::storiesJumpTo(
 	});
 }
 
+void OverlayWidget::storiesRedisplay(not_null<Data::Story*> story) {
+	Expects(_stories != nullptr);
+
+	clearStreaming();
+	_streamingStartPaused = false;
+	v::match(story->media().data, [&](not_null<PhotoData*> photo) {
+		displayPhoto(photo, anim::activation::background);
+	}, [&](not_null<DocumentData*> document) {
+		displayDocument(document, anim::activation::background);
+	}, [&](v::null_t) {
+		displayDocument(nullptr, anim::activation::background);
+	});
+}
+
 void OverlayWidget::storiesClose() {
 	close();
 }
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index d90cf8a02..b5be1b16c 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -250,6 +250,7 @@ private:
 		-> rpl::producer<ChatHelpers::FileChosen> override;
 	auto storiesCachedReactionIconFactory()
 		-> HistoryView::Reactions::CachedIconFactory & override;
+	void storiesRedisplay(not_null<Data::Story*> story) override;
 	void storiesJumpTo(
 		not_null<Main::Session*> session,
 		FullStoryId id,

From 5ccb97668c7acd4c76802238ae06f80823d250cc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 19:52:22 +0400
Subject: [PATCH 144/259] Add short-polling of stories.

---
 Telegram/SourceFiles/data/data_stories.cpp    | 108 ++++++++++++++++--
 Telegram/SourceFiles/data/data_stories.h      |  27 ++++-
 Telegram/SourceFiles/data/data_story.cpp      |  11 +-
 Telegram/SourceFiles/data/data_story.h        |   7 +-
 .../history/view/media/history_view_gif.cpp   |  20 ++++
 .../history/view/media/history_view_gif.h     |   3 +
 .../history/view/media/history_view_photo.cpp |  21 ++++
 .../history/view/media/history_view_photo.h   |   5 +-
 .../view/media/history_view_story_mention.cpp |  29 ++++-
 .../view/media/history_view_story_mention.h   |   2 +
 .../info/stories/info_stories_provider.cpp    |  24 +++-
 .../info/stories/info_stories_provider.h      |   2 +
 .../stories/media_stories_controller.cpp      |  99 ++++++++++------
 .../media/stories/media_stories_controller.h  |   2 +
 14 files changed, 308 insertions(+), 52 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 6c6991bd5..e0ff8ca26 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -38,6 +38,8 @@ constexpr auto kSavedPerPage = 100;
 constexpr auto kMaxPreloadSources = 10;
 constexpr auto kStillPreloadFromFirst = 3;
 constexpr auto kMaxSegmentsCount = 180;
+constexpr auto kPollingIntervalChat = 5 * TimeId(60);
+constexpr auto kPollingIntervalViewer = 1 * TimeId(60);
 
 using UpdateFlag = StoryUpdate::Flag;
 
@@ -98,10 +100,12 @@ Stories::Stories(not_null<Session*> owner)
 : _owner(owner)
 , _expireTimer([=] { processExpired(); })
 , _markReadTimer([=] { sendMarkAsReadRequests(); })
-, _incrementViewsTimer([=] { sendIncrementViewsRequests(); }) {
+, _incrementViewsTimer([=] { sendIncrementViewsRequests(); })
+, _pollingTimer([=] { sendPollingRequests(); }) {
 }
 
 Stories::~Stories() {
+	Expects(_pollingSettings.empty());
 }
 
 Session &Stories::owner() const {
@@ -348,7 +352,7 @@ Story *Stories::parseAndApply(
 		const auto result = i->second.get();
 		const auto pinned = result->pinned();
 		const auto mediaChanged = (result->media() != *media);
-		if (result->applyChanges(*media, data)) {
+		if (result->applyChanges(*media, data, now)) {
 			if (result->pinned() != pinned) {
 				savedStateUpdated(result);
 			}
@@ -358,6 +362,11 @@ Story *Stories::parseAndApply(
 			if (const auto item = lookupItem(result)) {
 				item->applyChanges(result);
 			}
+			_owner->refreshStoryItemViews(fullId);
+		}
+		const auto j = _pollingSettings.find(result);
+		if (j != end(_pollingSettings)) {
+			maybeSchedulePolling(result, j->second, now);
 		}
 		if (mediaChanged) {
 			_preloaded.remove(fullId);
@@ -378,7 +387,7 @@ Story *Stories::parseAndApply(
 		StoryMedia{ *media },
 		data.vdate().v,
 		data.vexpire_date().v)).first->second.get();
-	result->applyChanges(*media, data);
+	result->applyChanges(*media, data, now);
 	if (result->pinned()) {
 		savedStateUpdated(result);
 	}
@@ -656,6 +665,7 @@ void Stories::applyDeleted(FullStoryId id) {
 				preloadFinished(id);
 			}
 			_owner->refreshStoryItemViews(id);
+			Assert(!_pollingSettings.contains(story.get()));
 			if (i->second.empty()) {
 				_stories.erase(i);
 			}
@@ -818,13 +828,15 @@ base::expected<not_null<Story*>, NoStory> Stories::lookup(
 		_deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown);
 }
 
-void Stories::resolve(FullStoryId id, Fn<void()> done) {
-	const auto already = lookup(id);
-	if (already.has_value() || already.error() != NoStory::Unknown) {
-		if (done) {
-			done();
+void Stories::resolve(FullStoryId id, Fn<void()> done, bool force) {
+	if (!force) {
+		const auto already = lookup(id);
+		if (already.has_value() || already.error() != NoStory::Unknown) {
+			if (done) {
+				done();
+			}
+			return;
 		}
-		return;
 	}
 	if (const auto i = _resolveSent.find(id.peer); i != end(_resolveSent)) {
 		if (const auto j = i->second.find(id.story); j != end(i->second)) {
@@ -1493,6 +1505,84 @@ bool Stories::isUnread(not_null<Story*> story) {
 	return (story->id() > readTill);
 }
 
+void Stories::registerPolling(not_null<Story*> story, Polling polling) {
+	auto &settings = _pollingSettings[story];
+	switch (polling) {
+	case Polling::Chat: ++settings.chat; break;
+	case Polling::Viewer: ++settings.viewer; break;
+	}
+	maybeSchedulePolling(story, settings, base::unixtime::now());
+}
+
+void Stories::unregisterPolling(not_null<Story*> story, Polling polling) {
+	const auto i = _pollingSettings.find(story);
+	Assert(i != end(_pollingSettings));
+
+	switch (polling) {
+	case Polling::Chat:
+		Assert(i->second.chat > 0);
+		--i->second.chat;
+		break;
+	case Polling::Viewer:
+		Assert(i->second.viewer > 0);
+		--i->second.viewer;
+		break;
+	}
+	if (!i->second.chat && !i->second.viewer) {
+		_pollingSettings.erase(i);
+	}
+}
+
+bool Stories::registerPolling(FullStoryId id, Polling polling) {
+	if (const auto maybeStory = lookup(id)) {
+		registerPolling(*maybeStory, polling);
+		return true;
+	}
+	return false;
+}
+
+void Stories::unregisterPolling(FullStoryId id, Polling polling) {
+	const auto maybeStory = lookup(id);
+	Assert(maybeStory.has_value());
+	unregisterPolling(*maybeStory, polling);
+}
+
+int Stories::pollingInterval(const PollingSettings &settings) const {
+	return settings.viewer ? kPollingIntervalViewer : kPollingIntervalChat;
+}
+
+void Stories::maybeSchedulePolling(
+		not_null<Story*> story,
+		const PollingSettings &settings,
+		TimeId now) {
+	const auto last = story->lastUpdateTime();
+	const auto next = last + pollingInterval(settings);
+	const auto left = std::max(next - now, 0) * crl::time(1000) + 1;
+	if (!_pollingTimer.isActive() || _pollingTimer.remainingTime() > left) {
+		_pollingTimer.callOnce(left);
+	}
+}
+
+void Stories::sendPollingRequests() {
+	auto min = 0;
+	const auto now = base::unixtime::now();
+	for (const auto &[story, settings] : _pollingSettings) {
+		const auto last = story->lastUpdateTime();
+		const auto next = last + pollingInterval(settings);
+		if (now >= next) {
+			resolve(story->fullId(), nullptr, true);
+		} else {
+			const auto left = (next - now) * crl::time(1000) + 1;
+			if (!min || left < min) {
+				min = left;
+			}
+		}
+	}
+	if (min > 0) {
+		_pollingTimer.callOnce(min);
+	}
+}
+
 void Stories::updateUserStoriesState(not_null<PeerData*> peer) {
 	const auto till = _readTill.find(peer->id);
 	const auto readTill = (till != end(_readTill)) ? till->second : 0;
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index e1bb0dcb6..579ec7b89 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -153,7 +153,7 @@ public:
 
 	[[nodiscard]] base::expected<not_null<Story*>, NoStory> lookup(
 		FullStoryId id) const;
-	void resolve(FullStoryId id, Fn<void()> done);
+	void resolve(FullStoryId id, Fn<void()> done, bool force = false);
 	[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(FullStoryId id);
 	[[nodiscard]] std::shared_ptr<HistoryItem> resolveItem(
 		not_null<Story*> story);
@@ -209,6 +209,16 @@ public:
 		StoryId storyMaxId);
 	[[nodiscard]] bool isUnread(not_null<Story*> story);
 
+	enum class Polling {
+		Chat,
+		Viewer,
+	};
+	void registerPolling(not_null<Story*> story, Polling polling);
+	void unregisterPolling(not_null<Story*> story, Polling polling);
+
+	bool registerPolling(FullStoryId id, Polling polling);
+	void unregisterPolling(FullStoryId id, Polling polling);
+
 private:
 	struct Saved {
 		StoriesIds ids;
@@ -217,6 +227,10 @@ private:
 		bool loaded = false;
 		mtpRequestId requestId = 0;
 	};
+	struct PollingSettings {
+		int chat = 0;
+		int viewer = 0;
+	};
 
 	void parseAndApply(const MTPUserStories &stories);
 	[[nodiscard]] Story *parseAndApply(
@@ -265,6 +279,14 @@ private:
 	void startPreloading(not_null<Story*> story);
 	void preloadFinished(FullStoryId id, bool markAsPreloaded = false);
 
+	[[nodiscard]] int pollingInterval(
+		const PollingSettings &settings) const;
+	void maybeSchedulePolling(
+		not_null<Story*> story,
+		const PollingSettings &settings,
+		TimeId now);
+	void sendPollingRequests();
+
 	const not_null<Session*> _owner;
 	std::unordered_map<
 		PeerId,
@@ -336,6 +358,9 @@ private:
 	mtpRequestId _readTillsRequestId = 0;
 	bool _readTillReceived = false;
 
+	base::flat_map<not_null<Story*>, PollingSettings> _pollingSettings;
+	base::Timer _pollingTimer;
+
 };
 
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index af8b50c8a..3a57400c0 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -371,7 +371,12 @@ void Story::applyViewsSlice(
 	}
 }
 
-bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
+bool Story::applyChanges(
+		StoryMedia media,
+		const MTPDstoryItem &data,
+		TimeId now) {
+	_lastUpdateTime = now;
+
 	const auto pinned = data.is_pinned();
 	const auto edited = data.is_edited();
 	const auto isPublic = data.is_public();
@@ -424,6 +429,10 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
 	return true;
 }
 
+TimeId Story::lastUpdateTime() const {
+	return _lastUpdateTime;
+}
+
 StoryPreload::StoryPreload(not_null<Story*> story, Fn<void()> done)
 : _story(story)
 , _done(std::move(done)) {
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
index d80272e67..ed6254a09 100644
--- a/Telegram/SourceFiles/data/data_story.h
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -113,7 +113,11 @@ public:
 		const std::vector<StoryView> &slice,
 		int total);
 
-	bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
+	bool applyChanges(
+		StoryMedia media,
+		const MTPDstoryItem &data,
+		TimeId now);
+	[[nodiscard]] TimeId lastUpdateTime() const;
 
 private:
 	const StoryId _id = 0;
@@ -125,6 +129,7 @@ private:
 	int _views = 0;
 	const TimeId _date = 0;
 	const TimeId _expires = 0;
+	TimeId _lastUpdateTime = 0;
 	bool _pinned : 1 = false;
 	bool _isPublic : 1 = false;
 	bool _closeFriends : 1 = false;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
index e1c78f8b5..d3040ce07 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
@@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/path_shift_gradient.h"
 #include "ui/effects/spoiler_mess.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_streaming.h"
 #include "data/data_document.h"
 #include "data/data_file_click_handler.h"
@@ -134,6 +135,7 @@ Gif::~Gif() {
 			_parent->checkHeavyPart();
 		}
 	}
+	togglePollingStory(false);
 }
 
 bool Gif::CanPlayInline(not_null<DocumentData*> document) {
@@ -1432,6 +1434,22 @@ void Gif::dataMediaCreated() const {
 		_dataMedia->videoThumbnailWanted(_realParent->fullId());
 	}
 	history()->owner().registerHeavyViewPart(_parent);
+	togglePollingStory(true);
+}
+
+void Gif::togglePollingStory(bool enabled) const {
+	if (!_story || _pollingStory == enabled) {
+		return;
+	}
+	const auto polling = Data::Stories::Polling::Chat;
+	const auto media = _parent->data()->media();
+	const auto id = media ? media->storyId() : FullStoryId();
+	if (!enabled) {
+		_data->owner().stories().unregisterPolling(id, polling);
+	} else if (!_data->owner().stories().registerPolling(id, polling)) {
+		return;
+	}
+	_pollingStory = enabled;
 }
 
 bool Gif::uploading() const {
@@ -1686,6 +1704,7 @@ void Gif::unloadHeavyPart() {
 	_thumbCache = QImage();
 	_videoThumbnailFrame = nullptr;
 	_caption.unloadPersistentAnimation();
+	togglePollingStory(false);
 }
 
 void Gif::refreshParentId(not_null<HistoryItem*> realParent) {
@@ -1815,6 +1834,7 @@ void Gif::setStreamed(std::unique_ptr<Streamed> value) {
 	_streamed = std::move(value);
 	if (set) {
 		history()->owner().registerHeavyViewPart(_parent);
+		togglePollingStory(true);
 	} else if (removed) {
 		_parent->checkHeavyPart();
 	}
diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h
index 6a8fca3b5..50c5583ac 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_gif.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h
@@ -208,6 +208,8 @@ private:
 		StateRequest request,
 		QPoint position) const;
 
+	void togglePollingStory(bool enabled) const;
+
 	const not_null<DocumentData*> _data;
 	Ui::Text::String _caption;
 	std::unique_ptr<Streamed> _streamed;
@@ -222,6 +224,7 @@ private:
 	mutable bool _thumbCacheBlurred : 1 = false;
 	mutable bool _thumbIsEllipse : 1 = false;
 	mutable bool _story : 1 = false;
+	mutable bool _pollingStory : 1 = false;
 
 };
 
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
index a7f08a50d..ed5c49ac9 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/painter.h"
 #include "ui/power_saving.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_streaming.h"
 #include "data/data_photo.h"
 #include "data/data_photo_media.h"
@@ -101,6 +102,7 @@ Photo::~Photo() {
 			_parent->checkHeavyPart();
 		}
 	}
+	togglePollingStory(false);
 }
 
 void Photo::create(FullMsgId contextId, PeerData *chat) {
@@ -145,6 +147,7 @@ void Photo::dataMediaCreated() const {
 		_dataMedia->wanted(PhotoSize::Small, _realParent->fullId());
 	}
 	history()->owner().registerHeavyViewPart(_parent);
+	togglePollingStory(true);
 }
 
 bool Photo::hasHeavyPart() const {
@@ -160,6 +163,23 @@ void Photo::unloadHeavyPart() {
 	}
 	_imageCache = QImage();
 	_caption.unloadPersistentAnimation();
+	togglePollingStory(false);
+}
+
+void Photo::togglePollingStory(bool enabled) const {
+	const auto pollingStory = (enabled ? 1 : 0);
+	if (!_story || _pollingStory == pollingStory) {
+		return;
+	}
+	const auto polling = Data::Stories::Polling::Chat;
+	const auto media = _parent->data()->media();
+	const auto id = media ? media->storyId() : FullStoryId();
+	if (!enabled) {
+		_data->owner().stories().unregisterPolling(id, polling);
+	} else if (!_data->owner().stories().registerPolling(id, polling)) {
+		return;
+	}
+	_pollingStory = pollingStory;
 }
 
 QSize Photo::countOptimalSize() {
@@ -926,6 +946,7 @@ void Photo::setStreamed(std::unique_ptr<Streamed> value) {
 	_streamed = std::move(value);
 	if (set) {
 		history()->owner().registerHeavyViewPart(_parent);
+		togglePollingStory(true);
 	} else if (removed) {
 		_parent->checkHeavyPart();
 	}
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h
index f6d7062f0..94404ddbe 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h
@@ -160,6 +160,8 @@ private:
 
 	[[nodiscard]] QSize photoSize() const;
 
+	void togglePollingStory(bool enabled) const;
+
 	const not_null<PhotoData*> _data;
 	Ui::Text::String _caption;
 	mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
@@ -167,10 +169,11 @@ private:
 	const std::unique_ptr<MediaSpoiler> _spoiler;
 	mutable QImage _imageCache;
 	mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
-	uint32 _serviceWidth : 29 = 0;
+	uint32 _serviceWidth : 28 = 0;
 	mutable uint32 _imageCacheForum : 1 = 0;
 	mutable uint32 _imageCacheBlurred : 1 = 0;
 	mutable uint32 _story : 1 = 0;
+	mutable uint32 _pollingStory : 1 = 0;
 
 };
 
diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
index a4d255fd2..15d8925b5 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
@@ -52,7 +52,9 @@ StoryMention::StoryMention(
 , _unread(story->owner().stories().isUnread(story) ? 1 : 0) {
 }
 
-StoryMention::~StoryMention() = default;
+StoryMention::~StoryMention() {
+	changeSubscribedTo(0);
+}
 
 int StoryMention::top() {
 	return st::msgServiceGiftBoxButtonMargins.top();
@@ -105,13 +107,12 @@ void StoryMention::draw(
 				? history->session().user()
 				: history->peer);
 		_thumbnailFromStory = showStory;
-		_subscribed = 0;
+		changeSubscribedTo(0);
 	}
-	if (!_subscribed) {
+	if (changeSubscribedTo(1)) {
 		_thumbnail->subscribeToUpdates([=] {
 			_parent->data()->history()->owner().requestViewRepaint(_parent);
 		});
-		_subscribed = 1;
 	}
 
 	const auto padding = (geometry.width() - st::storyMentionSize) / 2;
@@ -164,10 +165,26 @@ bool StoryMention::hasHeavyPart() {
 }
 
 void StoryMention::unloadHeavyPart() {
-	if (_subscribed) {
-		_subscribed = 0;
+	if (changeSubscribedTo(0)) {
 		_thumbnail->subscribeToUpdates(nullptr);
 	}
 }
 
+bool StoryMention::changeSubscribedTo(uint32 value) {
+	Expects(value == 0 || value == 1);
+
+	if (_subscribed == value) {
+		return false;
+	}
+	_subscribed = value;
+	const auto stories = &_parent->history()->owner().stories();
+	if (value) {
+		_parent->history()->owner().registerHeavyViewPart(_parent);
+		stories->registerPolling(_story, Data::Stories::Polling::Chat);
+	} else {
+		stories->unregisterPolling(_story, Data::Stories::Polling::Chat);
+	}
+	return true;
+}
+
 } // namespace HistoryView
diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.h b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
index fea24787f..51704d440 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h
@@ -55,6 +55,8 @@ public:
 private:
 	using Thumbnail = Dialogs::Stories::Thumbnail;
 
+	bool changeSubscribedTo(uint32 value);
+
 	const not_null<Element*> _parent;
 	const not_null<Data::Story*> _story;
 	std::shared_ptr<Thumbnail> _thumbnail;
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
index b4cf680c6..fbe81ebc5 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
@@ -56,6 +56,10 @@ Provider::Provider(not_null<AbstractController*> controller)
 	}, _lifetime);
 }
 
+Provider::~Provider() {
+	clear();
+}
+
 Type Provider::type() {
 	return Type::PhotoVideo;
 }
@@ -90,11 +94,20 @@ std::optional<int> Provider::fullCount() {
 	return _slice.fullCount();
 }
 
-void Provider::restart() {
+void Provider::clear() {
+	for (const auto &[storyId, _] : _layouts) {
+		_peer->owner().stories().unregisterPolling(
+			{ _peer->id, storyId },
+			Data::Stories::Polling::Chat);
+	}
 	_layouts.clear();
 	_aroundId = kDefaultAroundId;
 	_idsLimit = kMinimalIdsLimit;
 	_slice = Data::StoriesIdsSlice();
+}
+
+void Provider::restart() {
+	clear();
 	refreshViewer();
 }
 
@@ -210,6 +223,9 @@ void Provider::markLayoutsStale() {
 void Provider::clearStaleLayouts() {
 	for (auto i = _layouts.begin(); i != _layouts.end();) {
 		if (i->second.stale) {
+			_peer->owner().stories().unregisterPolling(
+				{ _peer->id, i->first },
+				Data::Stories::Polling::Chat);
 			_layoutRemoved.fire(i->second.item.get());
 			const auto taken = _items.take(i->first);
 			i = _layouts.erase(i);
@@ -240,6 +256,9 @@ bool Provider::isAfter(
 void Provider::itemRemoved(not_null<const HistoryItem*> item) {
 	const auto id = StoryIdFromMsgId(item->id);
 	if (const auto i = _layouts.find(id); i != end(_layouts)) {
+		_peer->owner().stories().unregisterPolling(
+			{ _peer->id, id },
+			Data::Stories::Polling::Chat);
 		_layoutRemoved.fire(i->second.item.get());
 		_layouts.erase(i);
 	}
@@ -253,6 +272,9 @@ BaseLayout *Provider::getLayout(
 		if (auto layout = createLayout(id, delegate)) {
 			layout->initDimensions();
 			it = _layouts.emplace(id, std::move(layout)).first;
+			_peer->owner().stories().registerPolling(
+				{ _peer->id, id },
+				Data::Stories::Polling::Chat);
 		} else {
 			return nullptr;
 		}
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.h b/Telegram/SourceFiles/info/stories/info_stories_provider.h
index f1fb535d3..390234128 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.h
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.h
@@ -30,6 +30,7 @@ class Provider final
 	, public base::has_weak_ptr {
 public:
 	explicit Provider(not_null<AbstractController*> controller);
+	~Provider();
 
 	Media::Type type() override;
 	bool hasSelectRestriction() override;
@@ -99,6 +100,7 @@ private:
 	void itemRemoved(not_null<const HistoryItem*> item);
 	void markLayoutsStale();
 	void clearStaleLayouts();
+	void clear();
 
 	[[nodiscard]] HistoryItem *ensureItem(StoryId id);
 	[[nodiscard]] Media::BaseLayout *getLayout(
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index e1e6cc875..d05aa292c 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -302,7 +302,9 @@ Controller::Controller(not_null<Delegate*> delegate)
 	_contentFadeAnimation.stop();
 }
 
-Controller::~Controller() = default;
+Controller::~Controller() {
+	changeShown(nullptr);
+}
 
 void Controller::updateContentFaded() {
 	if (_contentFaded == _replyActive) {
@@ -480,6 +482,9 @@ void Controller::initLayout() {
 }
 
 Data::Story *Controller::story() const {
+	if (!_session) {
+		return nullptr;
+	}
 	const auto maybeStory = _session->data().stories().lookup(_shown);
 	return maybeStory ? maybeStory->get() : nullptr;
 }
@@ -748,10 +753,9 @@ void Controller::show(
 		.date = story->date(),
 		.edited = story->edited(),
 	});
-	if (_shown == storyId && _session == &story->session()) {
+	if (!changeShown(story)) {
 		return;
 	}
-	_shown = storyId;
 	_viewed = false;
 	invalidate_weak_ptrs(&_viewsLoadGuard);
 	_reactions->hide();
@@ -769,41 +773,72 @@ void Controller::show(
 		.valid = user->isSelf(),
 	});
 
-	const auto session = &story->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();
-			}
-		});
-		session->data().stories().itemsChanged(
-		) | rpl::start_with_next([=](PeerId peerId) {
-			if (_waitingForId.peer == peerId) {
-				checkWaitingFor();
-			}
-		}, _sessionLifetime);
-		session->changes().storyUpdates(
-			Data::StoryUpdate::Flag::Edited
-		) | rpl::filter([=](const Data::StoryUpdate &update) {
-			return (update.story == this->story());
-		}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
-			show(update.story, _context);
-			_delegate->storiesRedisplay(update.story);
-		}, _sessionLifetime);
-		_sessionLifetime.add([=] {
-			session->data().stories().setPreloadingInViewer({});
-		});
-	}
-
 	stories.loadAround(storyId, context);
 
 	updatePlayingAllowed();
 	user->updateFull();
 }
 
+bool Controller::changeShown(Data::Story *story) {
+	const auto id = story ? story->fullId() : FullStoryId();
+	const auto session = story ? &story->session() : nullptr;
+	const auto sessionChanged = (_session != session);
+	if (_shown == id && !sessionChanged) {
+		return false;
+	}
+	if (const auto now = this->story()) {
+		now->owner().stories().unregisterPolling(
+			now,
+			Data::Stories::Polling::Viewer);
+	}
+	if (sessionChanged) {
+		_sessionLifetime.destroy();
+	}
+	_shown = id;
+	_session = session;
+	if (sessionChanged) {
+		subscribeToSession();
+	}
+	if (story) {
+		story->owner().stories().registerPolling(
+			story,
+			Data::Stories::Polling::Viewer);
+	}
+	return true;
+}
+
+void Controller::subscribeToSession() {
+	Expects(!_sessionLifetime);
+
+	if (!_session) {
+		return;
+	}
+	_session->changes().storyUpdates(
+		Data::StoryUpdate::Flag::Destroyed
+	) | rpl::start_with_next([=](Data::StoryUpdate update) {
+		if (update.story->fullId() == _shown) {
+			_delegate->storiesClose();
+		}
+	}, _sessionLifetime);
+	_session->data().stories().itemsChanged(
+	) | rpl::start_with_next([=](PeerId peerId) {
+		if (_waitingForId.peer == peerId) {
+			checkWaitingFor();
+		}
+	}, _sessionLifetime);
+	_session->changes().storyUpdates(
+		Data::StoryUpdate::Flag::Edited
+	) | rpl::filter([=](const Data::StoryUpdate &update) {
+		return (update.story == this->story());
+	}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
+		show(update.story, _context);
+		_delegate->storiesRedisplay(update.story);
+	}, _sessionLifetime);
+	_sessionLifetime.add([=] {
+		_session->data().stories().setPreloadingInViewer({});
+	});
+}
+
 void Controller::updatePlayingAllowed() {
 	if (!_shown) {
 		return;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 5463cdc83..37d7b60fb 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -164,6 +164,8 @@ private:
 	class Unsupported;
 
 	void initLayout();
+	bool changeShown(Data::Story *story);
+	void subscribeToSession();
 	void updatePhotoPlayback(const Player::TrackState &state);
 	void updatePlayback(const Player::TrackState &state);
 	void updatePowerSaveBlocker(const Player::TrackState &state);

From 6a118888523d33349554d75c33214080ef641acc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 21:02:57 +0400
Subject: [PATCH 145/259] Poll views for my story that is viewed.

---
 Telegram/SourceFiles/data/data_changes.h      |   3 +-
 Telegram/SourceFiles/data/data_stories.cpp    |  68 ++++++----
 Telegram/SourceFiles/data/data_stories.h      |  11 +-
 Telegram/SourceFiles/data/data_story.cpp      | 123 +++++++++++++-----
 Telegram/SourceFiles/data/data_story.h        |  15 ++-
 .../stories/media_stories_controller.cpp      |  13 +-
 6 files changed, 161 insertions(+), 72 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h
index ee8ff6075..754f12ef3 100644
--- a/Telegram/SourceFiles/data/data_changes.h
+++ b/Telegram/SourceFiles/data/data_changes.h
@@ -223,8 +223,9 @@ struct StoryUpdate {
 		Edited = (1U << 0),
 		Destroyed = (1U << 1),
 		NewAdded = (1U << 2),
+		ViewsAdded = (1U << 3),
 
-		LastUsedBit = (1U << 2),
+		LastUsedBit = (1U << 3),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index e0ff8ca26..fa3ff6b58 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -40,6 +40,8 @@ constexpr auto kStillPreloadFromFirst = 3;
 constexpr auto kMaxSegmentsCount = 180;
 constexpr auto kPollingIntervalChat = 5 * TimeId(60);
 constexpr auto kPollingIntervalViewer = 1 * TimeId(60);
+constexpr auto kPollViewsInterval = 10 * crl::time(1000);
+constexpr auto kPollingViewsPerPage = Story::kRecentViewersMax;
 
 using UpdateFlag = StoryUpdate::Flag;
 
@@ -101,11 +103,13 @@ Stories::Stories(not_null<Session*> owner)
 , _expireTimer([=] { processExpired(); })
 , _markReadTimer([=] { sendMarkAsReadRequests(); })
 , _incrementViewsTimer([=] { sendIncrementViewsRequests(); })
-, _pollingTimer([=] { sendPollingRequests(); }) {
+, _pollingTimer([=] { sendPollingRequests(); })
+, _pollingViewsTimer([=] { sendPollingViewsRequests(); }) {
 }
 
 Stories::~Stories() {
 	Expects(_pollingSettings.empty());
+	Expects(_pollingViews.empty());
 }
 
 Session &Stories::owner() const {
@@ -350,20 +354,9 @@ Story *Stories::parseAndApply(
 	const auto i = stories.find(id);
 	if (i != end(stories)) {
 		const auto result = i->second.get();
-		const auto pinned = result->pinned();
 		const auto mediaChanged = (result->media() != *media);
-		if (result->applyChanges(*media, data, now)) {
-			if (result->pinned() != pinned) {
-				savedStateUpdated(result);
-			}
-			session().changes().storyUpdated(
-				result,
-				UpdateFlag::Edited);
-			if (const auto item = lookupItem(result)) {
-				item->applyChanges(result);
-			}
-			_owner->refreshStoryItemViews(fullId);
-		}
+		const auto pinned = result->pinned();
+		result->applyChanges(*media, data, now);
 		const auto j = _pollingSettings.find(result);
 		if (j != end(_pollingSettings)) {
 			maybeSchedulePolling(result, j->second, now);
@@ -385,12 +378,9 @@ Story *Stories::parseAndApply(
 		id,
 		peer,
 		StoryMedia{ *media },
-		data.vdate().v,
-		data.vexpire_date().v)).first->second.get();
-	result->applyChanges(*media, data, now);
-	if (result->pinned()) {
-		savedStateUpdated(result);
-	}
+		data,
+		now
+	)).first->second.get();
 
 	if (peer->isSelf()) {
 		const auto added = _archive.list.emplace(id).second;
@@ -479,7 +469,7 @@ void Stories::unregisterDependentMessage(
 	}
 }
 
-void Stories::savedStateUpdated(not_null<Story*> story) {
+void Stories::savedStateChanged(not_null<Story*> story) {
 	const auto id = story->id();
 	const auto peer = story->peer()->id;
 	const auto pinned = story->pinned();
@@ -1132,14 +1122,20 @@ void Stories::loadViewsSlice(
 		StoryId id,
 		std::optional<StoryView> offset,
 		Fn<void(std::vector<StoryView>)> done) {
-	_viewsDone = std::move(done);
-	if (_viewsStoryId == id && _viewsOffset == offset) {
+	if (_viewsStoryId == id
+		&& _viewsOffset == offset
+		&& (offset || _viewsRequestId)) {
+		if (_viewsRequestId) {
+			_viewsDone = std::move(done);
+		}
 		return;
 	}
 	_viewsStoryId = id;
 	_viewsOffset = offset;
+	_viewsDone = std::move(done);
 
 	const auto api = &_owner->session().api();
+	const auto perPage = _viewsDone ? kViewsPerPage : kPollingViewsPerPage;
 	api->request(_viewsRequestId).cancel();
 	_viewsRequestId = api->request(MTPstories_GetStoryViewsList(
 		MTP_int(id),
@@ -1509,7 +1505,13 @@ void Stories::registerPolling(not_null<Story*> story, Polling polling) {
 	auto &settings = _pollingSettings[story];
 	switch (polling) {
 	case Polling::Chat: ++settings.chat; break;
-	case Polling::Viewer: ++settings.viewer; break;
+	case Polling::Viewer:
+		++settings.viewer;
+		if (story->peer()->isSelf()
+			&& _pollingViews.emplace(story).second) {
+			sendPollingViewsRequests();
+		}
+		break;
 	}
 	maybeSchedulePolling(story, settings, base::unixtime::now());
 }
@@ -1525,7 +1527,12 @@ void Stories::unregisterPolling(not_null<Story*> story, Polling polling) {
 		break;
 	case Polling::Viewer:
 		Assert(i->second.viewer > 0);
-		--i->second.viewer;
+		if (!--i->second.viewer) {
+			_pollingViews.remove(story);
+			if (_pollingViews.empty()) {
+				_pollingViewsTimer.cancel();
+			}
+		}
 		break;
 	}
 	if (!i->second.chat && !i->second.viewer) {
@@ -1583,6 +1590,17 @@ void Stories::sendPollingRequests() {
 	}
 }
 
+void Stories::sendPollingViewsRequests() {
+	if (_pollingViews.empty()) {
+		return;
+	} else if (!_viewsRequestId) {
+		Assert(_viewsDone == nullptr);
+		const auto one = _pollingViews.front();
+		loadViewsSlice(_pollingViews.front()->id(), std::nullopt, nullptr);
+	}
+	_pollingViewsTimer.callOnce(kPollViewsInterval);
+}
+
 void Stories::updateUserStoriesState(not_null<PeerData*> peer) {
 	const auto till = _readTill.find(peer->id);
 	const auto readTill = (till != end(_readTill)) ? till->second : 0;
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 579ec7b89..2bdd27d1c 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -219,6 +219,10 @@ public:
 	bool registerPolling(FullStoryId id, Polling polling);
 	void unregisterPolling(FullStoryId id, Polling polling);
 
+	void savedStateChanged(not_null<Story*> story);
+	[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
+		not_null<Story*> story);
+
 private:
 	struct Saved {
 		StoriesIds ids;
@@ -253,14 +257,10 @@ private:
 	void applyRemovedFromActive(FullStoryId id);
 	void applyDeletedFromSources(PeerId id, StorySourcesList list);
 	void removeDependencyStory(not_null<Story*> story);
-	void savedStateUpdated(not_null<Story*> story);
 	void sort(StorySourcesList list);
 	bool bumpReadTill(PeerId peerId, StoryId maxReadTill);
 	void requestReadTills();
 
-	[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
-		not_null<Story*> story);
-
 	void sendMarkAsReadRequests();
 	void sendMarkAsReadRequest(not_null<PeerData*> peer, StoryId tillId);
 	void sendIncrementViewsRequests();
@@ -286,6 +286,7 @@ private:
 		const PollingSettings &settings,
 		TimeId now);
 	void sendPollingRequests();
+	void sendPollingViewsRequests();
 
 	const not_null<Session*> _owner;
 	std::unordered_map<
@@ -359,7 +360,9 @@ private:
 	bool _readTillReceived = false;
 
 	base::flat_map<not_null<Story*>, PollingSettings> _pollingSettings;
+	base::flat_set<not_null<Story*>> _pollingViews;
 	base::Timer _pollingTimer;
+	base::Timer _pollingViewsTimer;
 
 };
 
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index 3a57400c0..59f672a11 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -10,12 +10,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/unixtime.h"
 #include "api/api_text_entities.h"
 #include "data/data_document.h"
+#include "data/data_changes.h"
 #include "data/data_file_origin.h"
 #include "data/data_photo.h"
 #include "data/data_photo_media.h"
 #include "data/data_user.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_thread.h"
+#include "history/history_item.h"
 #include "lang/lang_keys.h"
 #include "main/main_session.h"
 #include "media/streaming/media_streaming_reader.h"
@@ -23,6 +26,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text/text_utilities.h"
 
 namespace Data {
+namespace {
+
+using UpdateFlag = StoryUpdate::Flag;
+
+} // namespace
 
 class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask {
 public:
@@ -103,7 +111,7 @@ bool StoryPreload::LoadTask::feedPart(
 		&& _requestedOffsets.empty()) {
 		_finished = true;
 		removeFromQueue();
-		auto result = Media::Streaming::SerializeComplexPartsMap(_parts);
+		auto result = ::Media::Streaming::SerializeComplexPartsMap(_parts);
 		if (result.size() == _full) {
 			// Make sure it is parsed as a complex map.
 			result.push_back(char(0));
@@ -130,13 +138,13 @@ Story::Story(
 	StoryId id,
 	not_null<PeerData*> peer,
 	StoryMedia media,
-	TimeId date,
-	TimeId expires)
+	const MTPDstoryItem &data,
+	TimeId now)
 : _id(id)
 , _peer(peer)
-, _media(std::move(media))
-, _date(date)
-, _expires(expires) {
+, _date(data.vdate().v)
+, _expires(data.vexpire_date().v) {
+	applyFields(std::move(media), data, now, true);
 }
 
 Session &Story::owner() const {
@@ -318,13 +326,6 @@ const TextWithEntities &Story::caption() const {
 	return unsupported() ? empty : _caption;
 }
 
-void Story::setViewsData(
-		std::vector<not_null<PeerData*>> recent,
-		int total) {
-	_recentViewers = std::move(recent);
-	_views = total;
-}
-
 const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
 	return _recentViewers;
 }
@@ -341,6 +342,7 @@ void Story::applyViewsSlice(
 		const std::optional<StoryView> &offset,
 		const std::vector<StoryView> &slice,
 		int total) {
+	const auto changed = (_views != total);
 	_views = total;
 	if (!offset) {
 		const auto i = _viewsList.empty()
@@ -369,12 +371,42 @@ void Story::applyViewsSlice(
 			}
 		}
 	}
+	const auto known = int(_viewsList.size());
+	if (known >= _recentViewers.size()) {
+		const auto take = std::min(known, kRecentViewersMax);
+		auto viewers = _viewsList
+			| ranges::views::take(take)
+			| ranges::views::transform(&StoryView::peer)
+			| ranges::to_vector;
+		if (_recentViewers != viewers) {
+			_recentViewers = std::move(viewers);
+			if (!changed) {
+				// Count not changed, but list of recent viewers changed.
+				_peer->session().changes().storyUpdated(
+					this,
+					UpdateFlag::ViewsAdded);
+			}
+		}
+	}
+	if (changed) {
+		_peer->session().changes().storyUpdated(
+			this,
+			UpdateFlag::ViewsAdded);
+	}
 }
 
-bool Story::applyChanges(
+void Story::applyChanges(
 		StoryMedia media,
 		const MTPDstoryItem &data,
 		TimeId now) {
+	applyFields(std::move(media), data, now, false);
+}
+
+void Story::applyFields(
+		StoryMedia media,
+		const MTPDstoryItem &data,
+		TimeId now,
+		bool initial) {
 	_lastUpdateTime = now;
 
 	const auto pinned = data.is_pinned();
@@ -388,45 +420,64 @@ bool Story::applyChanges(
 			&owner().session(),
 			data.ventities().value_or_empty()),
 	};
-	auto views = -1;
-	auto recent = std::vector<not_null<PeerData*>>();
+	auto views = _views;
+	auto viewers = std::vector<not_null<PeerData*>>();
 	if (!data.is_min()) {
 		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());
+				viewers.reserve(list->v.size());
 				auto &owner = _peer->owner();
-				for (const auto &id : list->v) {
-					recent.push_back(owner.peer(peerFromUser(id)));
+				auto &&cut = list->v
+					| ranges::views::take(kRecentViewersMax);
+				for (const auto &id : cut) {
+					viewers.push_back(owner.peer(peerFromUser(id)));
 				}
 			}
 		}
 	}
 
-	const auto changed = (_media != media)
-		|| (_pinned != pinned)
-		|| (_edited != edited)
-		|| (_isPublic != isPublic)
-		|| (_closeFriends != closeFriends)
-		|| (_noForwards != noForwards)
-		|| (_caption != caption)
-		|| (views >= 0 && _views != views)
-		|| (_recentViewers != recent);
-	if (!changed) {
-		return false;
-	}
-	_media = std::move(media);
+	const auto pinnedChanged = (_pinned != pinned);
+	const auto editedChanged = (_edited != edited);
+	const auto mediaChanged = (_media != media);
+	const auto captionChanged = (_caption != caption);
+	const auto viewsChanged = (_views != views)
+		|| (_recentViewers != viewers);
+
+	_isPublic = isPublic;
+	_closeFriends = closeFriends;
+	_noForwards = noForwards;
 	_edited = edited;
 	_pinned = pinned;
 	_isPublic = isPublic;
 	_closeFriends = closeFriends;
 	_noForwards = noForwards;
-	_caption = std::move(caption);
-	if (views >= 0) {
+	if (viewsChanged) {
 		_views = views;
+		_recentViewers = std::move(viewers);
+	}
+	if (mediaChanged) {
+		_media = std::move(media);
+	}
+	if (captionChanged) {
+		_caption = std::move(caption);
+	}
+
+	const auto changed = (editedChanged || captionChanged || mediaChanged);
+	if (!initial && (changed || viewsChanged)) {
+		_peer->session().changes().storyUpdated(this, UpdateFlag()
+			| (changed ? UpdateFlag::Edited : UpdateFlag())
+			| (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag()));
+	}
+	if (!initial && (captionChanged || mediaChanged)) {
+		if (const auto item = _peer->owner().stories().lookupItem(this)) {
+			item->applyChanges(this);
+		}
+		_peer->owner().refreshStoryItemViews(fullId());
+	}
+	if (pinnedChanged) {
+		_peer->owner().stories().savedStateChanged(this);
 	}
-	_recentViewers = std::move(recent);
-	return true;
 }
 
 TimeId Story::lastUpdateTime() const {
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
index ed6254a09..d95c4dd40 100644
--- a/Telegram/SourceFiles/data/data_story.h
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -61,8 +61,10 @@ public:
 		StoryId id,
 		not_null<PeerData*> peer,
 		StoryMedia media,
-		TimeId date,
-		TimeId expires);
+		const MTPDstoryItem &data,
+		TimeId now);
+
+	static constexpr int kRecentViewersMax = 3;
 
 	[[nodiscard]] Session &owner() const;
 	[[nodiscard]] Main::Session &session() const;
@@ -103,7 +105,6 @@ public:
 	void setCaption(TextWithEntities &&caption);
 	[[nodiscard]] const TextWithEntities &caption() const;
 
-	void setViewsData(std::vector<not_null<PeerData*>> recent, int total);
 	[[nodiscard]] auto recentViewers() const
 		-> const std::vector<not_null<PeerData*>> &;
 	[[nodiscard]] const std::vector<StoryView> &viewsList() const;
@@ -113,13 +114,19 @@ public:
 		const std::vector<StoryView> &slice,
 		int total);
 
-	bool applyChanges(
+	void applyChanges(
 		StoryMedia media,
 		const MTPDstoryItem &data,
 		TimeId now);
 	[[nodiscard]] TimeId lastUpdateTime() const;
 
 private:
+	void applyFields(
+		StoryMedia media,
+		const MTPDstoryItem &data,
+		TimeId now,
+		bool initial);
+
 	const StoryId _id = 0;
 	const not_null<PeerData*> _peer;
 	StoryMedia _media;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index d05aa292c..788806bdd 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -828,11 +828,20 @@ void Controller::subscribeToSession() {
 	}, _sessionLifetime);
 	_session->changes().storyUpdates(
 		Data::StoryUpdate::Flag::Edited
+		| Data::StoryUpdate::Flag::ViewsAdded
 	) | rpl::filter([=](const Data::StoryUpdate &update) {
 		return (update.story == this->story());
 	}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
-		show(update.story, _context);
-		_delegate->storiesRedisplay(update.story);
+		if (update.flags & Data::StoryUpdate::Flag::Edited) {
+			show(update.story, _context);
+			_delegate->storiesRedisplay(update.story);
+		} else {
+			_recentViews->show({
+				.list = update.story->recentViewers(),
+				.total = update.story->views(),
+				.valid = update.story->peer()->isSelf(),
+			});
+		}
 	}, _sessionLifetime);
 	_sessionLifetime.add([=] {
 		_session->data().stories().setPreloadingInViewer({});

From 1c914e40bb4e75c6b8d992786e959b148eecbf5a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 5 Jul 2023 21:05:32 +0400
Subject: [PATCH 146/259] Closed alpha version 4.8.4.1.

---
 Telegram/Resources/uwp/AppX/AppxManifest.xml |  2 +-
 Telegram/Resources/winrc/Telegram.rc         |  8 ++++----
 Telegram/Resources/winrc/Updater.rc          |  8 ++++----
 Telegram/SourceFiles/core/version.h          |  2 +-
 Telegram/build/updates.py                    | 18 +++++++++---------
 Telegram/build/version                       |  4 ++--
 6 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 475e3ee7e..e9ff08323 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.4.0" />
+    Version="4.8.4.1" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 6f39c1168..c90e04576 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,0
- PRODUCTVERSION 4,8,4,0
+ FILEVERSION 4,8,4,1
+ PRODUCTVERSION 4,8,4,1
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.4.0"
+            VALUE "FileVersion", "4.8.4.1"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.0"
+            VALUE "ProductVersion", "4.8.4.1"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 732642486..1292b3215 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,0
- PRODUCTVERSION 4,8,4,0
+ FILEVERSION 4,8,4,1
+ PRODUCTVERSION 4,8,4,1
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.4.0"
+            VALUE "FileVersion", "4.8.4.1"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.0"
+            VALUE "ProductVersion", "4.8.4.1"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 0058d08cf..9e9f695db 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/const_string.h"
 
-#define TDESKTOP_REQUESTED_ALPHA_VERSION (0ULL)
+#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004001ULL)
 
 #ifdef TDESKTOP_ALLOW_CLOSED_ALPHA
 #define TDESKTOP_ALPHA_VERSION TDESKTOP_REQUESTED_ALPHA_VERSION
diff --git a/Telegram/build/updates.py b/Telegram/build/updates.py
index 3f5cd81a5..85dbda59a 100644
--- a/Telegram/build/updates.py
+++ b/Telegram/build/updates.py
@@ -53,9 +53,9 @@ outputFolder = 'updates/' + today
 archive = 'tdesktop_macOS_' + today + '.zip'
 
 if building:
-    print('Building debug version for OS X 10.12+..')
+    print('Building Release version for OS X 10.12+..')
 
-    if os.path.exists('../out/Debug/' + outputFolder):
+    if os.path.exists('../out/Release/' + outputFolder):
         finish(1, 'Todays updates version exists.')
 
     if uuid == '':
@@ -65,11 +65,11 @@ if building:
 
     os.chdir('../out')
     if uuid == '':
-        result = subprocess.call('cmake --build . --config Debug --target Telegram', shell=True)
+        result = subprocess.call('cmake --build . --config Release --target Telegram', shell=True)
         if result != 0:
             finish(1, 'While building Telegram.')
 
-    os.chdir('Debug')
+    os.chdir('Release')
     if uuid == '':
         if not os.path.exists('Telegram.app'):
             finish(1, 'Telegram.app not found.')
@@ -106,7 +106,7 @@ if building:
             finish(1, 'Adding tdesktop to archive.')
 
         print('Beginning notarization process.')
-        lines = subprocess.check_output('xcrun altool --notarize-app --primary-bundle-id "com.tdesktop.TelegramDebug" --username "' + username + '" --password "@keychain:AC_PASSWORD" --file "' + archive + '"', stderr=subprocess.STDOUT, shell=True).decode('utf-8')
+        lines = subprocess.check_output('xcrun altool --notarize-app --primary-bundle-id "com.tdesktop.Telegram" --username "' + username + '" --password "@keychain:AC_PASSWORD" --file "' + archive + '"', stderr=subprocess.STDOUT, shell=True).decode('utf-8')
         print('Response received.')
         uuid = ''
         for line in lines.split('\n'):
@@ -193,7 +193,7 @@ if building:
         print('NB! Notarization log not found.')
     finish(0)
 
-commandPath = scriptPath + '/../../out/Debug/' + outputFolder + '/command.txt'
+commandPath = scriptPath + '/../../out/Release/' + outputFolder + '/command.txt'
 
 if composing:
     templatePath = scriptPath + '/../../../DesktopPrivate/updates_template.txt'
@@ -235,7 +235,7 @@ if composing:
             for line in template:
                 if line.startswith('//'):
                     continue
-                line = line.replace('{path}', scriptPath + '/../../out/Debug/' + outputFolder + '/' + archive)
+                line = line.replace('{path}', scriptPath + '/../../out/Release/' + outputFolder + '/' + archive)
                 line = line.replace('{caption}', 'TDesktop at ' + today.replace('_', '.') + ':\n\n' + changelog)
                 f.write(line)
     print('\n\nEdit:\n')
@@ -262,9 +262,9 @@ if len(caption) > 1024:
     print('vi ' + commandPath)
     finish(1, 'Too large.')
 
-if not os.path.exists('../out/Debug/' + outputFolder + '/' + archive):
+if not os.path.exists('../out/Release/' + outputFolder + '/' + archive):
     finish(1, 'Not built yet.')
 
-subprocess.call(scriptPath + '/../../out/Debug/Telegram.app/Contents/MacOS/Telegram -sendpath interpret://' + scriptPath + '/../../out/Debug/' + outputFolder + '/command.txt', shell=True)
+subprocess.call(scriptPath + '/../../out/Release/Telegram.app/Contents/MacOS/Telegram -sendpath interpret://' + scriptPath + '/../../out/Release/' + outputFolder + '/command.txt', shell=True)
 
 finish(0)
diff --git a/Telegram/build/version b/Telegram/build/version
index e74e8988d..ac52ec62e 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -3,5 +3,5 @@ AppVersionStrMajor 4.8
 AppVersionStrSmall 4.8.4
 AppVersionStr      4.8.4
 BetaChannel        0
-AlphaVersion       0
-AppVersionOriginal 4.8.4
+AlphaVersion       4008004001
+AppVersionOriginal 4.8.4.1

From 08ee386b2817d438263496e33344a3b028d0d0cb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 6 Jul 2023 00:05:34 +0400
Subject: [PATCH 147/259] Fix build for macOS.

---
 .../history/view/media/history_view_story_mention.cpp           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
index 15d8925b5..8615aec4b 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
@@ -140,7 +140,7 @@ void StoryMention::draw(
 		_unreadBrush = QBrush(gradient);
 	}
 	auto readColor = context.st->msgServiceFg()->c;
-	readColor.setAlphaF(std::min(readColor.alphaF(), kReadOutlineAlpha));
+	readColor.setAlphaF(std::min(1. * readColor.alphaF(), kReadOutlineAlpha));
 	p.setPen(QPen(
 		_unread ? _unreadBrush : QBrush(readColor),
 		0.5 * (_unread

From dc806d42868de6bd55599eea2de94f94b4e0e685 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 7 Jul 2023 22:49:30 +0400
Subject: [PATCH 148/259] Try ElasticScroll in Dialogs::Widget.

---
 .../dialogs/dialogs_inner_widget.cpp          |  1 +
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 30 ++++++++++++-------
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  4 +--
 Telegram/lib_ui                               |  2 +-
 4 files changed, 23 insertions(+), 14 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 6b2b7efe7..2c0ff4141 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/application.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/popup_menu.h"
+#include "ui/widgets/scroll_area.h"
 #include "ui/text/text_utilities.h"
 #include "ui/text/text_options.h"
 #include "ui/painter.h"
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 72e8b9ea9..8fc7250ee 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/view/history_view_group_call_bar.h"
 #include "boxes/peers/edit_peer_requests_box.h"
 #include "ui/widgets/buttons.h"
+#include "ui/widgets/elastic_scroll.h"
 #include "ui/widgets/input_fields.h"
 #include "ui/wrap/fade_wrap.h"
 #include "ui/effects/radial_animation.h"
@@ -211,7 +212,7 @@ Widget::Widget(
 , _lockUnlock(_searchControls, st::dialogsLock)
 , _scroll(this)
 , _allowStoriesExpandTimer([=] {
-	_scroll->verticalScrollBar()->setMinimum(0);
+	//_scroll->verticalScrollBar()->setMinimum(0);
 })
 , _scrollToTop(_scroll, st::dialogsToUp)
 , _searchTimer([=] { searchMessages(); })
@@ -269,7 +270,9 @@ Widget::Widget(
 	_inner->storiesExpandedRequests(
 	) | rpl::start_with_next([=](bool expanded) {
 		if (expanded || _scroll->scrollTop() < _inner->defaultScrollTop()) {
-			scrollToDefaultChecked(expanded);
+			if (_scroll->scrollTop() > 0) {
+				scrollToDefaultChecked(expanded);
+			}
 		}
 	}, lifetime());
 
@@ -427,6 +430,7 @@ Widget::Widget(
 	setupSupportMode();
 	setupScrollUpButton();
 
+	_scroll->setOverscrollBg(st::dialogsBg->c);
 	if (_layout != Layout::Child) {
 		setupConnectingWidget();
 
@@ -449,7 +453,11 @@ Widget::Widget(
 		}, lifetime());
 
 		_childListShown.changes(
-		) | rpl::start_with_next([=] {
+		) | rpl::start_with_next([=](float64 value) {
+			_scroll->setOverscrollBg(anim::color(
+				st::dialogsBg,
+				st::dialogsBgOver,
+				value));
 			updateControlsGeometry();
 		}, lifetime());
 
@@ -867,7 +875,7 @@ void Widget::changeOpenedSubsection(
 		}
 		oldContentCache = grabForFolderSlideAnimation();
 	}
-	_scroll->verticalScrollBar()->setMinimum(0);
+	//_scroll->verticalScrollBar()->setMinimum(0);
 	_showAnimation = nullptr;
 	destroyChildListCanvas();
 	change();
@@ -1143,7 +1151,7 @@ void Widget::jumpToTop(bool belowPinned) {
 
 void Widget::scrollToDefault(bool verytop) {
 	if (verytop) {
-		_scroll->verticalScrollBar()->setMinimum(0);
+		//_scroll->verticalScrollBar()->setMinimum(0);
 	}
 	_scrollToAnimation.stop();
 	auto scrollTop = _scroll->scrollTop();
@@ -2397,28 +2405,28 @@ bool Widget::customWheelProcess(not_null<QWheelEvent*> e) {
 void Widget::customScrollProcess(Qt::ScrollPhase phase) {
 	const auto now = _scroll->scrollTop();
 	const auto def = _inner->defaultScrollTop();
-	const auto bar = _scroll->verticalScrollBar();
+	//const auto bar = _scroll->verticalScrollBar();
 	if (phase == Qt::ScrollBegin || phase == Qt::ScrollUpdate) {
 		_allowStoriesExpandTimer.cancel();
 		_inner->setTouchScrollActive(true);
-		bar->setMinimum(0);
+		//bar->setMinimum(0);
 	} else if (phase == Qt::ScrollEnd || phase == Qt::ScrollMomentum) {
 		_allowStoriesExpandTimer.cancel();
 		_inner->setTouchScrollActive(false);
 		if (def > 0 && now >= def) {
-			bar->setMinimum(def);
+			//bar->setMinimum(def);
 		} else {
-			bar->setMinimum(0);
+			//bar->setMinimum(0);
 		}
 	} else {
 		const auto allow = (def <= 0)
 			|| (now < def)
 			|| (now == def && !_allowStoriesExpandTimer.isActive());
 		if (allow) {
-			_scroll->verticalScrollBar()->setMinimum(0);
+			//_scroll->verticalScrollBar()->setMinimum(0);
 			_allowStoriesExpandTimer.cancel();
 		} else {
-			bar->setMinimum(def);
+			//bar->setMinimum(def);
 			_allowStoriesExpandTimer.callOnce(kWaitTillAllowStoriesExpand);
 		}
 	}
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 686ce1b23..ee3861fdb 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/dialogs_key.h"
 #include "window/section_widget.h"
 #include "ui/effects/animations.h"
-#include "ui/widgets/scroll_area.h"
 #include "mtproto/sender.h"
 #include "api/api_single_message_search.h"
 
@@ -46,6 +45,7 @@ class GroupCallBar;
 class RequestsBar;
 class MoreChatsBar;
 class JumpDownButton;
+class ElasticScroll;
 template <typename Widget>
 class FadeWrapScaled;
 } // namespace Ui
@@ -247,7 +247,7 @@ private:
 	std::unique_ptr<Ui::RequestsBar> _forumRequestsBar;
 	std::unique_ptr<HistoryView::ContactStatus> _forumReportBar;
 
-	object_ptr<Ui::ScrollArea> _scroll;
+	object_ptr<Ui::ElasticScroll> _scroll;
 	base::Timer _allowStoriesExpandTimer;
 	QPointer<InnerWidget> _inner;
 	class BottomButton;
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index d431d803c..7135f3ed8 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit d431d803c8b347c192d1bc9b6a206d2622a10748
+Subproject commit 7135f3ed87ee1a6d89cebe6510a2250ffedfb1a9

From e0c10e7cc2112ab1fb89418486570ae0f5794ab0 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 10 Jul 2023 11:07:21 +0400
Subject: [PATCH 149/259] Fix scroll-to-top in chats list visibility.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 8fc7250ee..687aa38cc 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -227,6 +227,7 @@ Widget::Widget(
 			_childListPeerId.value(),
 			_childListShown.value(),
 			makeChildListShown)));
+	_scrollToTop->raise();
 
 	_inner->updated(
 	) | rpl::start_with_next([=] {

From 85795aa376cd9d69e4af2a7e8322f0f1a8299d43 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 11:15:32 +0400
Subject: [PATCH 150/259] Working stories in overscroll.

---
 Telegram/SourceFiles/dialogs/dialogs.style    |  13 +-
 .../dialogs/dialogs_inner_widget.cpp          |  95 +---------
 .../dialogs/dialogs_inner_widget.h            |  15 --
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 170 +++++++++---------
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  13 +-
 .../dialogs/ui/dialogs_stories_list.cpp       | 150 +++++++++++-----
 .../dialogs/ui/dialogs_stories_list.h         |  32 ++--
 Telegram/SourceFiles/info/info_top_bar.cpp    |   3 +-
 .../stories/info_stories_inner_widget.cpp     |   3 +-
 Telegram/lib_ui                               |   2 +-
 10 files changed, 232 insertions(+), 264 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index 8e9f09232..6a20b6cf7 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -246,7 +246,7 @@ dialogsSearchForNarrowFilters: IconButton(dialogsMenuToggle) {
 dialogsFilter: InputField(defaultInputField) {
 	textBg: filterInputInactiveBg;
 	textBgActive: filterInputActiveBg;
-	textMargins: margins(12px, 7px, 30px, 3px);
+	textMargins: margins(12px, 8px, 30px, 5px);
 
 	placeholderFg: placeholderFg;
 	placeholderFgActive: placeholderFgActive;
@@ -257,16 +257,16 @@ dialogsFilter: InputField(defaultInputField) {
 	placeholderFont: normalFont;
 
 	borderFg: filterInputInactiveBg;
-	borderFgActive: filterInputBorderFg;
+	borderFgActive: windowBgRipple;
 	borderFgError: activeLineFgError;
 
 	border: 2px;
 	borderActive: 2px;
-	borderRadius: roundRadiusSmall;
+	borderRadius: 18px;
 
 	font: normalFont;
 
-	heightMin: 32px;
+	heightMin: 35px;
 }
 dialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) {
 	icon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }};
@@ -511,8 +511,9 @@ DialogsStoriesList {
 dialogsStories: DialogsStories {
 	left: 4px;
 	height: 35px;
-	photo: 24px;
-	photoLeft: 10px;
+	photo: 21px;
+	photoTop: 4px;
+	photoLeft: 4px;
 	shift: 16px;
 	lineTwice: 3px;
 	lineReadTwice: 0px;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 2c0ff4141..0a01ddda7 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -141,13 +141,6 @@ InnerWidget::InnerWidget(
 	rpl::producer<ChildListShown> childListShown)
 : RpWidget(parent)
 , _controller(controller)
-, _stories(std::make_unique<Stories::List>(
-	this,
-	st::dialogsStoriesList,
-	Stories::ContentForSession(
-		&controller->session(),
-		Data::StorySourcesList::NotHidden),
-	[=] { return _stories->height() - _visibleTop; }))
 , _shownList(controller->session().data().chatsList()->indexed())
 , _st(&st::defaultDialogRow)
 , _pinnedShiftAnimation([=](crl::time now) {
@@ -328,37 +321,6 @@ InnerWidget::InnerWidget(
 		switchToFilter(filterId);
 	}, lifetime());
 
-	_stories->heightValue(
-	) | rpl::filter([=] {
-		return (_viewportHeight > 0) && (defaultScrollTop() > _visibleTop);
-	}) | rpl::start_with_next([=] {
-		refreshForDefaultScroll();
-		jumpToTop();
-	}, lifetime());
-
-	_stories->entered(
-	) | rpl::start_with_next([=] {
-		clearSelection();
-	}, lifetime());
-
-	_stories->clicks(
-	) | rpl::start_with_next([=](uint64 id) {
-		_controller->openPeerStories(
-			PeerId(int64(id)),
-			Data::StorySourcesList::NotHidden);
-	}, lifetime());
-
-	_stories->showMenuRequests(
-	) | rpl::start_with_next([=](const Stories::ShowMenuRequest &request) {
-		FillSourceMenu(_controller, request);
-	}, lifetime());
-
-	_stories->loadMoreRequests(
-	) | rpl::start_with_next([=] {
-		session().data().stories().loadMore(
-			Data::StorySourcesList::NotHidden);
-	}, lifetime());
-
 	session().data().stories().incrementPreloadingMainSources();
 
 	handleChatListEntryRefreshes();
@@ -450,14 +412,8 @@ int InnerWidget::skipTopHeight() const {
 		: 0;
 }
 
-bool InnerWidget::storiesShown() const {
-	return (_state == WidgetState::Default)
-		&& !_openedFolder
-		&& !_openedForum;
-}
-
 int InnerWidget::collapsedRowsOffset() const {
-	return storiesShown() ? _stories->height() : 0;
+	return 0;
 }
 
 int InnerWidget::dialogsOffset() const {
@@ -466,22 +422,6 @@ int InnerWidget::dialogsOffset() const {
 		- skipTopHeight();
 }
 
-rpl::producer<bool> InnerWidget::storiesExpandedRequests() const {
-	return rpl::merge(
-		_stories->toggleExpandedRequests(),
-		_storiesExpandedRequests.events());
-}
-
-void InnerWidget::setTouchScrollActive(bool active) {
-	_stories->setTouchScrollActive(active);
-}
-
-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) {
@@ -564,7 +504,6 @@ void InnerWidget::changeOpenedFolder(Data::Folder *folder) {
 	stopReorderPinned();
 	clearSelection();
 	_openedFolder = folder;
-	_stories->setVisible(storiesShown());
 	refreshShownList();
 	refreshWithCollapsedRows(true);
 	if (_loadMoreCallback) {
@@ -591,7 +530,6 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) {
 	}
 	_openedForum = forum;
 	_st = forum ? &st::forumTopicRow : &st::defaultDialogRow;
-	_stories->setVisible(storiesShown());
 	refreshShownList();
 
 	_openedForumLifetime.destroy();
@@ -642,7 +580,6 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
 		.paused = videoPaused,
 		.narrow = (fullWidth < st::columnMinimalWidthLeft / 2),
 	};
-	_stories->setBgOverride(context.currentBg);
 	const auto fillGuard = gsl::finally([&] {
 		// We translate painter down, but it'll be cropped below rect.
 		p.fillRect(rect(), context.currentBg);
@@ -1759,13 +1696,6 @@ void InnerWidget::mousePressReleased(
 	}
 }
 
-void InnerWidget::setViewportHeight(int viewportHeight) {
-	if (_viewportHeight != viewportHeight) {
-		_viewportHeight = viewportHeight;
-		refreshForDefaultScroll();
-	}
-}
-
 void InnerWidget::setCollapsedPressed(int pressed) {
 	if (_collapsedPressed != pressed) {
 		if (_collapsedPressed >= 0) {
@@ -1836,7 +1766,6 @@ void InnerWidget::setSearchedPressed(int pressed) {
 }
 
 void InnerWidget::resizeEvent(QResizeEvent *e) {
-	_stories->resizeToWidth(width());
 	resizeEmptyLabel();
 	moveCancelSearchButtons();
 }
@@ -2597,7 +2526,6 @@ void InnerWidget::visibleTopBottomUpdated(
 		int visibleBottom) {
 	_visibleTop = visibleTop;
 	_visibleBottom = visibleBottom;
-	_stories->update();
 	preloadRowsData();
 	const auto loadTill = _visibleTop
 		+ PreloadHeightsCount * (_visibleBottom - _visibleTop);
@@ -2793,12 +2721,6 @@ void InnerWidget::editOpenedFilter() {
 	}
 }
 
-void InnerWidget::refreshForDefaultScroll() {
-	if (height() < defaultScrollTop() + _viewportHeight) {
-		refresh();
-	}
-}
-
 void InnerWidget::refresh(bool toTop) {
 	if (!_geometryInited) {
 		return;
@@ -2820,9 +2742,6 @@ 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();
@@ -3033,10 +2952,7 @@ void InnerWidget::clearFilter() {
 }
 
 void InnerWidget::setState(WidgetState state) {
-	if (_state != state) {
-		_state = state;
-		_stories->setVisible(storiesShown());
-	}
+	_state = state;
 }
 
 void InnerWidget::selectSkip(int32 direction) {
@@ -3331,8 +3247,7 @@ void InnerWidget::switchToFilter(FilterId filterId) {
 }
 
 void InnerWidget::jumpToTop() {
-	const auto to = defaultScrollTop();
-	_mustScrollTo.fire({ to, -1 });
+	_mustScrollTo.fire({ 0, -1 });
 }
 
 void InnerWidget::saveChatsFilterScrollState(FilterId filterId) {
@@ -3342,8 +3257,7 @@ void InnerWidget::saveChatsFilterScrollState(FilterId filterId) {
 void InnerWidget::restoreChatsFilterScrollState(FilterId filterId) {
 	const auto it = _chatsFilterScrollStates.find(filterId);
 	if (it != end(_chatsFilterScrollStates)) {
-		const auto top = std::max(it->second, defaultScrollTop());
-		_mustScrollTo.fire({ top, -1 });
+		_mustScrollTo.fire({ std::max(it->second, 0), -1 });
 	}
 }
 
@@ -3412,7 +3326,6 @@ ChosenRow InnerWidget::computeChosenRow() const {
 bool InnerWidget::chooseRow(
 		Qt::KeyboardModifiers modifiers,
 		MsgId pressedTopicRootId) {
-	_storiesExpandedRequests.fire(false);
 	if (chooseCollapsedRow()) {
 		return true;
 	} else if (chooseHashtag()) {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index c72d9667f..608513c85 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -52,10 +52,6 @@ struct PaintContext;
 struct TopicJumpCache;
 } // namespace Dialogs::Ui
 
-namespace Dialogs::Stories {
-class List;
-} // namespace Dialogs::Stories
-
 namespace Dialogs {
 
 class Row;
@@ -105,11 +101,6 @@ public:
 		const QVector<MTPPeer> &my,
 		const QVector<MTPPeer> &result);
 
-	void setTouchScrollActive(bool active);
-	[[nodiscard]] rpl::producer<bool> storiesExpandedRequests() const;
-	[[nodiscard]] int defaultScrollTop() const;
-	void setViewportHeight(int viewportHeight);
-
 	[[nodiscard]] FilterId filterId() const;
 
 	void clearSelection();
@@ -124,7 +115,6 @@ public:
 
 	void clearFilter();
 	void refresh(bool toTop = false);
-	void refreshForDefaultScroll();
 	void refreshEmptyLabel();
 	void resizeEmptyLabel();
 
@@ -322,7 +312,6 @@ private:
 	void fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu);
 
 	void refreshShownList();
-	[[nodiscard]] bool storiesShown() const;
 	[[nodiscard]] int skipTopHeight() const;
 	[[nodiscard]] int collapsedRowsOffset() const;
 	[[nodiscard]] int dialogsOffset() const;
@@ -409,10 +398,6 @@ private:
 
 	const not_null<Window::SessionController*> _controller;
 
-	const std::unique_ptr<Stories::List> _stories;
-	rpl::event_stream<bool> _storiesExpandedRequests;
-	int _viewportHeight = 0;
-
 	not_null<IndexedList*> _shownList;
 	FilterId _filterId = 0;
 	bool _mouseSelection = false;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 687aa38cc..10046fa6e 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "dialogs/dialogs_widget.h"
 
+#include "dialogs/ui/dialogs_stories_content.h"
+#include "dialogs/ui/dialogs_stories_list.h"
 #include "dialogs/dialogs_inner_widget.h"
 #include "dialogs/dialogs_search_from_controllers.h"
 #include "dialogs/dialogs_key.h"
@@ -64,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_changes.h"
 #include "data/data_download_manager.h"
 #include "data/data_chat_filters.h"
+#include "data/data_stories.h"
 #include "info/downloads/info_downloads_widget.h"
 #include "info/info_memento.h"
 #include "styles/style_dialogs.h"
@@ -211,15 +214,21 @@ Widget::Widget(
 , _cancelSearch(_searchControls, st::dialogsCancelSearch)
 , _lockUnlock(_searchControls, st::dialogsLock)
 , _scroll(this)
-, _allowStoriesExpandTimer([=] {
-	//_scroll->verticalScrollBar()->setMinimum(0);
-})
 , _scrollToTop(_scroll, st::dialogsToUp)
+, _stories(std::make_unique<Stories::List>(
+	this,
+	st::dialogsStoriesList,
+	Stories::ContentForSession(
+		&controller->session(),
+		Data::StorySourcesList::NotHidden)))
 , _searchTimer([=] { searchMessages(); })
 , _singleMessageSearch(&controller->session()) {
 	const auto makeChildListShown = [](PeerId peerId, float64 shown) {
 		return InnerWidget::ChildListShown{ peerId, shown };
 	};
+	_scroll->setOverscrollTypes(
+		Ui::ElasticScroll::OverscrollType::Virtual,
+		Ui::ElasticScroll::OverscrollType::Real);
 	_inner = _scroll->setOwnedWidget(object_ptr<InnerWidget>(
 		this,
 		controller,
@@ -228,6 +237,22 @@ Widget::Widget(
 			_childListShown.value(),
 			makeChildListShown)));
 	_scrollToTop->raise();
+	rpl::combine(
+		_scroll->positionValue(),
+		_scroll->movementValue()
+	) | rpl::start_with_next([=](
+			Ui::ElasticScrollPosition position,
+			Ui::ElasticScrollMovement movement) {
+		const auto overscrollTop = std::max(-position.overscroll, 0);
+		if (_overscrollTop != overscrollTop) {
+			_overscrollTop = overscrollTop;
+			updateControlsGeometry();
+		}
+		using Phase = Ui::ElasticScrollMovement;
+		_stories->setExpandedHeight(
+			_overscrollTop,
+			(movement == Phase::Momentum || movement == Phase::Returning));
+	}, lifetime());
 
 	_inner->updated(
 	) | rpl::start_with_next([=] {
@@ -268,15 +293,6 @@ Widget::Widget(
 		}
 	}, lifetime());
 
-	_inner->storiesExpandedRequests(
-	) | rpl::start_with_next([=](bool expanded) {
-		if (expanded || _scroll->scrollTop() < _inner->defaultScrollTop()) {
-			if (_scroll->scrollTop() > 0) {
-				scrollToDefaultChecked(expanded);
-			}
-		}
-	}, lifetime());
-
 	_inner->mustScrollTo(
 	) | rpl::start_with_next([=](const Ui::ScrollToRequest &data) {
 		if (_scroll) {
@@ -328,12 +344,6 @@ Widget::Widget(
 	) | rpl::start_with_next([=] {
 		listScrollUpdated();
 	}, lifetime());
-	_scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
-		return customWheelProcess(e);
-	});
-	_scroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
-		return customTouchProcess(e);
-	});
 
 	session().data().chatsListChanges(
 	) | rpl::filter([=](Data::Folder *folder) {
@@ -383,6 +393,7 @@ Widget::Widget(
 
 	setupMainMenuToggle();
 	setupShortcuts();
+	setupStories();
 
 	_searchForNarrowFilters->setClickedCallback([=] {
 		_filter->setFocusFast();
@@ -589,7 +600,7 @@ void Widget::setupMoreChatsBar() {
 	}
 	controller()->activeChatsFilter(
 	) | rpl::start_with_next([=](FilterId id) {
-		if (!id) {
+		if (!id && false) { // #TODO stories testing
 			_moreChatsBar = nullptr;
 			updateControlsGeometry();
 			return;
@@ -770,6 +781,38 @@ void Widget::setupMainMenuToggle() {
 	}, _mainMenu.toggle->lifetime());
 }
 
+void Widget::setupStories() {
+	_stories->clicks(
+	) | rpl::start_with_next([=](uint64 id) {
+		controller()->openPeerStories(
+			PeerId(int64(id)),
+			Data::StorySourcesList::NotHidden);
+	}, lifetime());
+
+	_stories->showMenuRequests(
+	) | rpl::start_with_next([=](const Stories::ShowMenuRequest &request) {
+		FillSourceMenu(controller(), request);
+	}, lifetime());
+
+	_stories->loadMoreRequests(
+	) | rpl::start_with_next([=] {
+		session().data().stories().loadMore(
+			Data::StorySourcesList::NotHidden);
+	}, lifetime());
+
+	_stories->toggleExpandedRequests(
+	) | rpl::start_with_next([=](bool expanded) {
+		if (expanded) {
+			if (!_scrollToAnimation.animating() || _scrollAnimationTo) {
+				scrollToDefault();
+			}
+		}
+		_scroll->setOverscrollDefaults(
+			expanded ? -st::dialogsStoriesFull.height : 0,
+			0);
+	}, lifetime());
+}
+
 void Widget::setupShortcuts() {
 	Shortcuts::Requests(
 	) | rpl::filter([=] {
@@ -1128,7 +1171,7 @@ void Widget::jumpToTop(bool belowPinned) {
 		return;
 	}
 	if ((currentSearchQuery().trimmed().isEmpty() && !_searchInChat)) {
-		auto to = _inner->defaultScrollTop();
+		auto to = 0;
 		if (belowPinned) {
 			const auto list = _openedForum
 				? _openedForum->topicsList()
@@ -1156,7 +1199,7 @@ void Widget::scrollToDefault(bool verytop) {
 	}
 	_scrollToAnimation.stop();
 	auto scrollTop = _scroll->scrollTop();
-	const auto scrollTo = verytop ? 0 : _inner->defaultScrollTop();
+	const auto scrollTo = 0;
 	if (scrollTop == scrollTo) {
 		return;
 	}
@@ -1172,7 +1215,7 @@ void Widget::scrollToDefault(bool verytop) {
 		const auto animated = qRound(_scrollToAnimation.value(scrollTo));
 		const auto animatedDelta = animated - scrollTo;
 		const auto realDelta = _scroll->scrollTop() - scrollTo;
-		if (realDelta * animatedDelta < 0) {
+		if (base::OppositeSigns(realDelta, animatedDelta)) {
 			// We scrolled manually to the other side of target 'scrollTo'.
 			_scrollToAnimation.stop();
 		} else if (std::abs(realDelta) > std::abs(animatedDelta)) {
@@ -1181,6 +1224,7 @@ void Widget::scrollToDefault(bool verytop) {
 		}
 	};
 
+	_scrollAnimationTo = scrollTo;
 	_scrollToAnimation.start(
 		scroll,
 		scrollTop,
@@ -2398,55 +2442,6 @@ void Widget::completeHashtag(QString tag) {
 	applyFilterUpdate(true);
 }
 
-bool Widget::customWheelProcess(not_null<QWheelEvent*> e) {
-	customScrollProcess(e->phase());
-	return false;
-}
-
-void Widget::customScrollProcess(Qt::ScrollPhase phase) {
-	const auto now = _scroll->scrollTop();
-	const auto def = _inner->defaultScrollTop();
-	//const auto bar = _scroll->verticalScrollBar();
-	if (phase == Qt::ScrollBegin || phase == Qt::ScrollUpdate) {
-		_allowStoriesExpandTimer.cancel();
-		_inner->setTouchScrollActive(true);
-		//bar->setMinimum(0);
-	} else if (phase == Qt::ScrollEnd || phase == Qt::ScrollMomentum) {
-		_allowStoriesExpandTimer.cancel();
-		_inner->setTouchScrollActive(false);
-		if (def > 0 && now >= def) {
-			//bar->setMinimum(def);
-		} else {
-			//bar->setMinimum(0);
-		}
-	} else {
-		const auto allow = (def <= 0)
-			|| (now < def)
-			|| (now == def && !_allowStoriesExpandTimer.isActive());
-		if (allow) {
-			//_scroll->verticalScrollBar()->setMinimum(0);
-			_allowStoriesExpandTimer.cancel();
-		} else {
-			//bar->setMinimum(def);
-			_allowStoriesExpandTimer.callOnce(kWaitTillAllowStoriesExpand);
-		}
-	}
-}
-
-bool Widget::customTouchProcess(not_null<QTouchEvent*> e) {
-	const auto type = e->type();
-	customScrollProcess([&] {
-		switch (e->type()) {
-		case QEvent::TouchBegin: return Qt::ScrollBegin;
-		case QEvent::TouchUpdate: return Qt::ScrollUpdate;
-		case QEvent::TouchEnd:
-		case QEvent::TouchCancel: return Qt::ScrollEnd;
-		}
-		Unexpected("Touch event type in Widdget::customTouchProcess.");
-	}());
-	return false;
-}
-
 void Widget::resizeEvent(QResizeEvent *e) {
 	updateControlsGeometry();
 }
@@ -2526,11 +2521,9 @@ void Widget::updateControlsGeometry() {
 		? st::dialogsFilterSkip
 		: (st::dialogsFilterPadding.x() + _mainMenu.toggle->width()))
 		+ st::dialogsFilterPadding.x();
-	auto filterRight = (session().domain().local().hasLocalPasscode()
-		? (st::dialogsFilterPadding.x() + _lockUnlock->width())
-		: st::dialogsFilterSkip) + st::dialogsFilterPadding.x();
-	auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight;
-	auto filterAreaHeight = st::topBarHeight;
+	const auto filterRight = st::dialogsFilterSkip + st::dialogsFilterPadding.x();
+	const auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight;
+	const auto filterAreaHeight = st::topBarHeight;
 	_searchControls->setGeometry(0, filterAreaTop, ratiow, filterAreaHeight);
 	if (_subsectionTopBar) {
 		_subsectionTopBar->setGeometryWithNarrowRatio(
@@ -2542,6 +2535,7 @@ void Widget::updateControlsGeometry() {
 	auto filterTop = (filterAreaHeight - _filter->height()) / 2;
 	filterLeft = anim::interpolate(filterLeft, _narrowWidth, narrowRatio);
 	_filter->setGeometryToLeft(filterLeft, filterTop, filterWidth, _filter->height());
+
 	auto mainMenuLeft = anim::interpolate(
 		st::dialogsFilterPadding.x(),
 		(_narrowWidth - _mainMenu.toggle->width()) / 2,
@@ -2567,14 +2561,21 @@ void Widget::updateControlsGeometry() {
 	right -= _chooseFromUser->width(); _chooseFromUser->moveToLeft(right, _filter->y());
 
 	const auto barw = width();
+	const auto expandedStoriesTop = filterAreaTop + filterAreaHeight;
+	const auto storiesHeight = 2 * st::dialogsStories.photoTop
+		+ st::dialogsStories.photo;
+	const auto added = (st::dialogsFilter.heightMin - storiesHeight) / 2;
+	_stories->setLayoutConstraints(
+		{ filterLeft + filterWidth, filterTop + added },
+		{ 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height });
 	if (_forumTopShadow) {
 		_forumTopShadow->setGeometry(
 			0,
-			filterAreaTop + filterAreaHeight,
+			expandedStoriesTop,
 			barw,
 			st::lineWidth);
 	}
-	const auto moreChatsBarTop = filterAreaTop + filterAreaHeight;
+	const auto moreChatsBarTop = expandedStoriesTop + _overscrollTop;
 	if (_moreChatsBar) {
 		_moreChatsBar->move(0, moreChatsBarTop);
 		_moreChatsBar->resizeToWidth(barw);
@@ -2599,8 +2600,7 @@ void Widget::updateControlsGeometry() {
 	auto scrollTop = forumReportTop
 		+ (_forumReportBar ? _forumReportBar->bar().height() : 0);
 	const auto wasScrollTop = _scroll->scrollTop();
-	const auto newScrollTop = (_topDelta < 0
-		&& wasScrollTop <= _inner->defaultScrollTop())
+	const auto newScrollTop = (_topDelta < 0 && wasScrollTop <= 0)
 		? wasScrollTop
 		: (wasScrollTop + _topDelta);
 	auto scrollHeight = height() - scrollTop;
@@ -2626,19 +2626,13 @@ 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();
 	}
-	const auto startWithTop = _inner->defaultScrollTop();
+	const auto startWithTop = 0;
 	if (wasScrollHeight < startWithTop && scrollHeight >= startWithTop) {
 		_scroll->scrollToY(startWithTop);
 	} else if (newScrollTop != wasScrollTop) {
@@ -2718,7 +2712,7 @@ void Widget::paintEvent(QPaintEvent *e) {
 		p.fillRect(above.intersected(r), bg);
 	}
 
-	auto belowTop = _scroll->y() + qMin(_scroll->height(), _inner->height());
+	auto belowTop = _scroll->y() + _scroll->height();
 	if (!_widthAnimationCache.isNull()) {
 		p.drawPixmapLeft(0, _scroll->y(), width(), _widthAnimationCache);
 		belowTop = _scroll->y() + (_widthAnimationCache.height() / cIntRetinaFactor());
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index ee3861fdb..13ca38398 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -56,6 +56,10 @@ class ConnectionState;
 struct SectionShow;
 } // namespace Window
 
+namespace Dialogs::Stories {
+class List;
+} // namespace Dialogs::Stories
+
 namespace Dialogs {
 
 struct RowDescriptor;
@@ -136,9 +140,6 @@ private:
 	void cancelSearchInChat();
 	void filterCursorMoved();
 	void completeHashtag(QString tag);
-	bool customWheelProcess(not_null<QWheelEvent*> e);
-	bool customTouchProcess(not_null<QTouchEvent*> e);
-	void customScrollProcess(Qt::ScrollPhase phase);
 
 	[[nodiscard]] QString currentSearchQuery() const;
 	void clearSearchField();
@@ -165,6 +166,7 @@ private:
 	void setupMoreChatsBar();
 	void setupDownloadBar();
 	void setupShortcuts();
+	void setupStories();
 	[[nodiscard]] bool searchForPeersRequired(const QString &query) const;
 	[[nodiscard]] bool searchForTopicsRequired(const QString &query) const;
 	bool setSearchInChat(Key chat, PeerData *from = nullptr);
@@ -248,7 +250,6 @@ private:
 	std::unique_ptr<HistoryView::ContactStatus> _forumReportBar;
 
 	object_ptr<Ui::ElasticScroll> _scroll;
-	base::Timer _allowStoriesExpandTimer;
 	QPointer<InnerWidget> _inner;
 	class BottomButton;
 	object_ptr<BottomButton> _updateTelegram = { nullptr };
@@ -257,6 +258,7 @@ private:
 	std::unique_ptr<Window::ConnectionState> _connecting;
 
 	Ui::Animations::Simple _scrollToAnimation;
+	int _scrollAnimationTo = 0;
 	std::unique_ptr<Window::SlideAnimation> _showAnimation;
 	rpl::variable<float64> _shownProgressValue;
 
@@ -265,6 +267,9 @@ private:
 	bool _scrollToTopIsShown = false;
 	bool _forumSearchRequested = false;
 
+	int _overscrollTop = 0;
+	std::unique_ptr<Stories::List> _stories;
+
 	Data::Folder *_openedFolder = nullptr;
 	Data::Forum *_openedForum = nullptr;
 	Dialogs::Key _searchInChat;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 23da7e9c8..d7d62b416 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -24,7 +24,7 @@ constexpr auto kPreloadPages = 2;
 constexpr auto kExpandAfterRatio = 0.85;
 constexpr auto kCollapseAfterRatio = 0.72;
 constexpr auto kFrictionRatio = 0.15;
-constexpr auto kSnapExpandedTimeout = crl::time(200);
+constexpr auto kExpandCatchUpDuration = crl::time(200);
 
 [[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) {
 	const auto &full = st.full;
@@ -37,7 +37,6 @@ constexpr auto kSnapExpandedTimeout = crl::time(200);
 
 struct List::Layout {
 	int itemsCount = 0;
-	int shownHeight = 0;
 	float64 expandedRatio = 0.;
 	float64 ratio = 0.;
 	float64 thumbnailLeft = 0.;
@@ -58,12 +57,9 @@ struct List::Layout {
 List::List(
 	not_null<QWidget*> parent,
 	const style::DialogsStoriesList &st,
-	rpl::producer<Content> content,
-	Fn<int()> shownHeight)
+	rpl::producer<Content> content)
 : RpWidget(parent)
-, _st(st)
-, _shownHeight(shownHeight)
-, _snapExpandedTimer([=] { requestExpanded(_expanded); }) {
+, _st(st) {
 	setCursor(style::cur_default);
 
 	std::move(content) | rpl::start_with_next([=](Content &&content) {
@@ -87,6 +83,7 @@ void List::showContent(Content &&content) {
 		return;
 	}
 	const auto hidden = _content.elements.empty();
+	const auto wasCount = int((hidden ? _hidingData : _data).items.size());
 	_content = std::move(content);
 	auto items = base::take(
 		_data.items.empty() ? _hidingData.items : _data.items);
@@ -114,6 +111,9 @@ void List::showContent(Content &&content) {
 			_data.items.emplace_back(Item{ .element = element });
 		}
 	}
+	if (int(_data.items.size()) != wasCount) {
+		updateGeometry();
+	}
 	updateScrollMax();
 	updateSummary(_data);
 	update();
@@ -253,7 +253,6 @@ rpl::producer<> List::loadMoreRequests() const {
 }
 
 void List::requestExpanded(bool expanded) {
-	_snapExpandedTimer.cancel();
 	if (_expanded != expanded) {
 		_expanded = expanded;
 		_expandedAnimation.start(
@@ -274,12 +273,12 @@ void List::resizeEvent(QResizeEvent *e) {
 	updateScrollMax();
 }
 
-void List::updateExpanding(int minHeight, int shownHeight, int fullHeight) {
-	Expects(shownHeight == minHeight || fullHeight > minHeight);
+void List::updateExpanding(int expandingHeight, int expandedHeight) {
+	Expects(!expandingHeight || expandedHeight > 0);
 
-	const auto ratio = (shownHeight == minHeight)
+	const auto ratio = !expandingHeight
 		? 0.
-		: (float64(shownHeight - minHeight) / (fullHeight - minHeight));
+		: (float64(expandingHeight) / expandedHeight);
 	if (_lastRatio == ratio) {
 		return;
 	}
@@ -296,20 +295,12 @@ void List::updateExpanding(int minHeight, int shownHeight, int fullHeight) {
 List::Layout List::computeLayout() {
 	const auto &st = _st.small;
 	const auto &full = _st.full;
-	const auto shownHeight = std::max(_shownHeight(), st.height);
-	if (_lastHeight != shownHeight) {
-		_lastHeight = shownHeight;
-		if (_lastHeight == st.height || _lastHeight == full.height) {
-			_snapExpandedTimer.cancel();
-		} else if (!_touchScrollActive) {
-			_snapExpandedTimer.callOnce(kSnapExpandedTimeout);
-		}
-	}
-	updateExpanding(st.height, shownHeight, full.height);
+	const auto use = _lastExpandedHeight
+		* _expandCatchUpAnimation.value(1.);
+	updateExpanding(use, full.height);
 
 	const auto expanded = _expandedAnimation.value(_expanded ? 1. : 0.);
-	const auto expandedRatio = float64(shownHeight - st.height)
-		/ (full.height - st.height);
+	const auto expandedRatio = _lastRatio;
 	const auto collapsedRatio = expandedRatio * kFrictionRatio;
 	const auto ratio = expandedRatio * expanded
 		+ collapsedRatio * (1. - expanded);
@@ -323,7 +314,7 @@ List::Layout List::computeLayout() {
 	const auto narrowWidth = st::defaultDialogRow.padding.left()
 		+ st::defaultDialogRow.photoSize
 		+ st::defaultDialogRow.padding.left();
-	const auto narrow = (width() <= narrowWidth);
+	const auto narrow = false;// (width() <= narrowWidth);
 	const auto smallSkip = (itemsCount > 1
 		&& rendering.items[0].element.skipSmall)
 		? 1
@@ -352,7 +343,6 @@ List::Layout List::computeLayout() {
 	const auto photoLeft = lerp(st.photoLeft, full.photoLeft);
 	return Layout{
 		.itemsCount = itemsCount,
-		.shownHeight = shownHeight,
 		.expandedRatio = expandedRatio,
 		.ratio = ratio,
 		.thumbnailLeft = thumbnailLeft,
@@ -391,14 +381,14 @@ void List::paintEvent(QPaintEvent *e) {
 	auto &rendering = _data.empty() ? _hidingData : _data;
 	const auto line = elerp(st.lineTwice, full.lineTwice) / 2.;
 	const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.;
-	const auto photoTopSmall = (st.height - st.photo) / 2.;
+	const auto photoTopSmall = st.photoTop;
 	const auto photoTop = photoTopSmall
 		+ (full.photoTop - photoTopSmall) * layout.expandedRatio;
 	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 nameScale = _lastRatio;
 	const auto nameTop = nameScale * full.nameTop;
 	const auto nameWidth = nameScale * AvailableNameWidth(_st);
 	const auto nameHeight = nameScale * full.nameStyle.font->height;
@@ -407,8 +397,18 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.);
 
 	auto p = QPainter(this);
-	p.fillRect(e->rect(), _bgOverride.value_or(_st.bg));
-	p.translate(0, height() - layout.shownHeight);
+
+	if (_state == State::Changing) {
+		const auto left = anim::interpolate(
+			_changingGeometryFrom.x(),
+			_geometryFull.x(),
+			layout.expandedRatio);
+		const auto top = anim::interpolate(
+			_changingGeometryFrom.y(),
+			_geometryFull.y(),
+			layout.expandedRatio);
+		p.translate(QPoint(left, top) - pos());
+	}
 
 	const auto drawSmall = (expandRatio < 1.);
 	const auto drawFull = (expandRatio > 0.);
@@ -691,6 +691,7 @@ void List::paintSummary(
 		Data &data,
 		float64 summaryTop,
 		float64 hidden) {
+#if 0 // #TODO stories to-remove
 	const auto total = int(data.items.size());
 	auto &summary = ChooseSummary(
 		_st.small,
@@ -723,6 +724,7 @@ void List::paintSummary(
 	p.drawImage(
 		QRectF(left, summaryTop, summaryWidth, summaryHeight),
 		summary.cache);
+#endif
 }
 
 void List::wheelEvent(QWheelEvent *e) {
@@ -767,12 +769,9 @@ void List::mouseMoveEvent(QMouseEvent *e) {
 	_lastMousePosition = e->globalPos();
 	updateSelected();
 
-	if (!_dragging && _mouseDownPosition) {
+	if (!_dragging && _mouseDownPosition && _state == State::Full) {
 		if ((_lastMousePosition - *_mouseDownPosition).manhattanLength()
 			>= QApplication::startDragDistance()) {
-			if (_shownHeight() < _st.full.height) {
-				requestExpanded(true);
-			}
 			_dragging = true;
 			_startDraggingLeft = _scrollLeft;
 		}
@@ -822,19 +821,82 @@ void List::mouseReleaseEvent(QMouseEvent *e) {
 	}
 }
 
-void List::setBgOverride(QBrush brush) {
-	_bgOverride = std::move(brush);
+void List::setExpandedHeight(int height, bool momentum) {
+	height = std::clamp(height, 0, _st.full.height);
+	if (_lastExpandedHeight == height) {
+		return;
+	} else if (momentum && _expandIgnored) {
+		return;
+	} else if (momentum && height > 0 && !_lastExpandedHeight) {
+		_expandIgnored = true;
+		return;
+	} else if (!momentum && _expandIgnored && height > 0) {
+		_expandIgnored = false;
+		_expandCatchUpAnimation.start([=] {
+			update();
+			if (!_expandCatchUpAnimation.animating()
+				&& _lastExpandedHeight == _st.full.height) {
+				setState(State::Full);
+			}
+		}, 0., 1., kExpandCatchUpDuration);
+	} else if (!height && _expandCatchUpAnimation.animating()) {
+		_expandCatchUpAnimation.stop();
+	}
+	_lastExpandedHeight = height;
+	setState(!height
+		? State::Small
+		: (height < _st.full.height
+			|| _expandCatchUpAnimation.animating()
+			|| _expandedAnimation.animating())
+		? State::Changing
+		: State::Full);
+	update();
 }
 
-void List::setTouchScrollActive(bool active) {
-	if (_touchScrollActive != active) {
-		_touchScrollActive = active;
-		if (active) {
-			_snapExpandedTimer.cancel();
-		} else {
-			requestExpanded(_expanded);
-		}
+void List::setLayoutConstraints(QPoint topRightSmall, QRect geometryFull) {
+	_topRightSmall = topRightSmall;
+	_geometryFull = geometryFull;
+	updateGeometry();
+	update();
+}
+
+void List::updateGeometry() {
+	switch (_state) {
+	case State::Small: setGeometry(countSmallGeometry()); break;
+	case State::Changing: {
+		_changingGeometryFrom = countSmallGeometry();
+		setGeometry(_geometryFull.united(_changingGeometryFrom));
+	} break;
+	case State::Full: setGeometry(_geometryFull);
 	}
+	update();
+}
+
+QRect List::countSmallGeometry() const {
+	const auto &st = _st.small;
+	const auto layout = const_cast<List*>(this)->computeLayout();
+	const auto count = layout.endIndexSmall - layout.startIndexSmall;
+	const auto width = st.left
+		+ st.photoLeft
+		+ st.photo + (count - 1) * st.shift
+		+ st.photoLeft
+		+ st.left;
+	return QRect(
+		_topRightSmall.x() - width,
+		_topRightSmall.y(),
+		width,
+		st.photoTop + st.photo + st.photoTop);
+}
+
+void List::setState(State state) {
+	if (_state == state) {
+		return;
+	}
+	_state = state;
+	setAttribute(
+		Qt::WA_TransparentForMouseEvents,
+		state == State::Changing);
+	updateGeometry();
 }
 
 void List::contextMenuEvent(QContextMenuEvent *e) {
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index a3b44d04a..0bf355bdb 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -63,11 +63,10 @@ public:
 	List(
 		not_null<QWidget*> parent,
 		const style::DialogsStoriesList &st,
-		rpl::producer<Content> content,
-		Fn<int()> shownHeight);
+		rpl::producer<Content> content);
 
-	void setBgOverride(QBrush brush);
-	void setTouchScrollActive(bool active);
+	void setExpandedHeight(int height, bool momentum = false);
+	void setLayoutConstraints(QPoint topRightSmall, QRect geometryFull);
 
 	[[nodiscard]] rpl::producer<uint64> clicks() const;
 	[[nodiscard]] rpl::producer<ShowMenuRequest> showMenuRequests() const;
@@ -77,6 +76,11 @@ public:
 
 private:
 	struct Layout;
+	enum class State {
+		Small,
+		Changing,
+		Full,
+	};
 	struct Item {
 		Element element;
 		QImage nameCache;
@@ -149,7 +153,10 @@ private:
 	void checkLoadMore();
 	void requestExpanded(bool expanded);
 
-	void updateExpanding(int minHeight, int shownHeight, int fullHeight);
+	void setState(State state);
+	void updateGeometry();
+	[[nodiscard]] QRect countSmallGeometry() const;
+	void updateExpanding(int expandingHeight, int expandedHeight);
 	void updateHeight();
 	void toggleAnimated(bool shown);
 	void paintSummary(
@@ -164,14 +171,17 @@ private:
 	Content _content;
 	Data _data;
 	Data _hidingData;
-	Fn<int()> _shownHeight = 0;
 	rpl::event_stream<uint64> _clicks;
 	rpl::event_stream<ShowMenuRequest> _showMenuRequests;
 	rpl::event_stream<bool> _toggleExpandedRequests;
 	rpl::event_stream<> _entered;
 	rpl::event_stream<> _loadMoreRequests;
 
-	std::optional<QBrush> _bgOverride;
+	QPoint _topRightSmall;
+	QRect _geometryFull;
+	QRect _changingGeometryFrom;
+	State _state = State::Small;
+
 	Ui::Animations::Simple _shownAnimation;
 
 	QPoint _lastMousePosition;
@@ -182,11 +192,11 @@ private:
 	bool _dragging = false;
 
 	Ui::Animations::Simple _expandedAnimation;
-	base::Timer _snapExpandedTimer;
+	Ui::Animations::Simple _expandCatchUpAnimation;
 	float64 _lastRatio = 0.;
-	int _lastHeight = 0;
-	bool _expanded = false;
-	bool _touchScrollActive = false;
+	int _lastExpandedHeight = 0;
+	bool _expandIgnored : 1 = false;
+	bool _expanded : 1 = false;
 
 	int _selected = -1;
 	int _pressed = -1;
diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp
index 68aae3228..81f4dd685 100644
--- a/Telegram/SourceFiles/info/info_top_bar.cpp
+++ b/Telegram/SourceFiles/info/info_top_bar.cpp
@@ -465,8 +465,7 @@ void TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) {
 					last
 				) | rpl::filter([](const Content &content) {
 					return !content.elements.empty();
-				}),
-				[] { return st::dialogsStories.height; }),
+				})),
 				st::infoTopBarScale);
 		registerToggleControlCallback(
 			stories,
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index b6fc6254f..8abc39d8d 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -170,8 +170,7 @@ void InnerWidget::createButtons() {
 		st::dialogsStoriesListMine,
 		rpl::duplicate(last) | rpl::filter([](const Content &content) {
 			return !content.elements.empty();
-		}),
-		[] { return st::dialogsStories.height; });
+		}));
 	thumbs->show();
 	rpl::combine(
 		recent->sizeValue(),
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 7135f3ed8..80308cea4 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 7135f3ed87ee1a6d89cebe6510a2250ffedfb1a9
+Subproject commit 80308cea4f57fd13e73abec121325f174ba21883

From 61b8aac7c422474ab6d85733bf9a859f1d381b7b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 12:21:50 +0400
Subject: [PATCH 151/259] Allow manually toggle stories expand state.

---
 .../SourceFiles/data/data_chat_filters.cpp    |   1 +
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 167 ++++++++++++------
 Telegram/SourceFiles/dialogs/dialogs_widget.h |   8 +-
 .../dialogs/ui/dialogs_stories_list.cpp       |   4 +-
 Telegram/lib_ui                               |   2 +-
 5 files changed, 121 insertions(+), 61 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp
index 26c6b6287..90ef0bebe 100644
--- a/Telegram/SourceFiles/data/data_chat_filters.cpp
+++ b/Telegram/SourceFiles/data/data_chat_filters.cpp
@@ -905,6 +905,7 @@ rpl::producer<> ChatFilters::suggestedUpdated() const {
 
 rpl::producer<Ui::MoreChatsBarContent> ChatFilters::moreChatsContent(
 		FilterId id) {
+	return rpl::single(Ui::MoreChatsBarContent{ .count = 10 }); // #TODO stories testing
 	if (!id) {
 		return rpl::single(Ui::MoreChatsBarContent{ .count = 0 });
 	}
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 10046fa6e..74fbf9e6c 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -83,7 +83,7 @@ namespace Dialogs {
 namespace {
 
 constexpr auto kSearchPerPage = 50;
-constexpr auto kWaitTillAllowStoriesExpand = crl::time(200);
+constexpr auto kStoriesExpandDuration = crl::time(200);
 
 } // namespace
 
@@ -239,19 +239,43 @@ Widget::Widget(
 	_scrollToTop->raise();
 	rpl::combine(
 		_scroll->positionValue(),
-		_scroll->movementValue()
+		_scroll->movementValue(),
+		_storiesExplicitExpandValue.value()
 	) | rpl::start_with_next([=](
 			Ui::ElasticScrollPosition position,
-			Ui::ElasticScrollMovement movement) {
+			Ui::ElasticScrollMovement movement,
+			int explicitlyExpanded) {
 		const auto overscrollTop = std::max(-position.overscroll, 0);
-		if (_overscrollTop != overscrollTop) {
-			_overscrollTop = overscrollTop;
-			updateControlsGeometry();
+		if (overscrollTop > 0 && _storiesExplicitExpand) {
+			_scroll->setOverscrollDefaults(
+				-st::dialogsStoriesFull.height,
+				0,
+				true);
+		}
+		if (explicitlyExpanded > 0 && explicitlyExpanded < overscrollTop) {
+			_storiesExplicitExpandAnimation.stop();
+			_storiesExplicitExpand = false;
+			_storiesExplicitExpandValue = 0;
+			return;
+		}
+		const auto above = std::max(explicitlyExpanded, overscrollTop);
+		if (_aboveScrollAdded != above) {
+			_aboveScrollAdded = above;
+			if (_updateScrollGeometryCached) {
+				_updateScrollGeometryCached();
+			}
 		}
 		using Phase = Ui::ElasticScrollMovement;
 		_stories->setExpandedHeight(
-			_overscrollTop,
-			(movement == Phase::Momentum || movement == Phase::Returning));
+			_aboveScrollAdded,
+			(movement == Phase::Momentum || movement == Phase::Returning)
+				&& (explicitlyExpanded < above));
+		if (position.overscroll > 0
+			|| (position.value
+				> (_storiesExplicitExpandScrollTop
+					+ st::dialogsRowHeight))) {
+			storiesToggleExplicitExpand(false);
+		}
 	}, lifetime());
 
 	_inner->updated(
@@ -487,6 +511,8 @@ Widget::Widget(
 }
 
 void Widget::chosenRow(const ChosenRow &row) {
+	storiesToggleExplicitExpand(false);
+
 	const auto history = row.key.history();
 	const auto topicJump = history
 		? history->peer->forumTopicFor(row.message.fullId.msg)
@@ -600,6 +626,8 @@ void Widget::setupMoreChatsBar() {
 	}
 	controller()->activeChatsFilter(
 	) | rpl::start_with_next([=](FilterId id) {
+		storiesToggleExplicitExpand(false);
+
 		if (!id && false) { // #TODO stories testing
 			_moreChatsBar = nullptr;
 			updateControlsGeometry();
@@ -802,17 +830,36 @@ void Widget::setupStories() {
 
 	_stories->toggleExpandedRequests(
 	) | rpl::start_with_next([=](bool expanded) {
-		if (expanded) {
-			if (!_scrollToAnimation.animating() || _scrollAnimationTo) {
-				scrollToDefault();
-			}
+		const auto position = _scroll->position();
+		if (!expanded) {
+			_scroll->setOverscrollDefaults(0, 0);
+		} else if (position.value > 0 || position.overscroll >= 0) {
+			storiesToggleExplicitExpand(true);
+			_scroll->setOverscrollDefaults(0, 0);
+		} else {
+			_scroll->setOverscrollDefaults(
+				-st::dialogsStoriesFull.height,
+				0);
 		}
-		_scroll->setOverscrollDefaults(
-			expanded ? -st::dialogsStoriesFull.height : 0,
-			0);
 	}, lifetime());
 }
 
+void Widget::storiesToggleExplicitExpand(bool expand) {
+	if (_storiesExplicitExpand == expand) {
+		return;
+	}
+	_storiesExplicitExpand = expand;
+	if (!expand) {
+		_scroll->setOverscrollDefaults(0, 0, true);
+	}
+	const auto height = st::dialogsStoriesFull.height;
+	const auto duration = kStoriesExpandDuration;
+	_storiesExplicitExpandScrollTop = _scroll->position().value;
+	_storiesExplicitExpandAnimation.start([=](float64 value) {
+		_storiesExplicitExpandValue = int(base::SafeRound(value));
+	}, expand ? 0 : height, expand ? height : 0, duration, anim::sineInOut);
+}
+
 void Widget::setupShortcuts() {
 	Shortcuts::Requests(
 	) | rpl::filter([=] {
@@ -957,6 +1004,7 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) {
 		controller()->closeForum();
 		_openedFolder = folder;
 		_inner->changeOpenedFolder(folder);
+		storiesToggleExplicitExpand(false);
 	}, (folder != nullptr), animated);
 }
 
@@ -970,6 +1018,7 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) {
 		_openedForum = forum;
 		_api.request(base::take(_topicSearchRequest)).cancel();
 		_inner->changeOpenedForum(forum);
+		storiesToggleExplicitExpand(false);
 	}, (forum != nullptr), animated);
 }
 
@@ -2575,42 +2624,15 @@ void Widget::updateControlsGeometry() {
 			barw,
 			st::lineWidth);
 	}
-	const auto moreChatsBarTop = expandedStoriesTop + _overscrollTop;
-	if (_moreChatsBar) {
-		_moreChatsBar->move(0, moreChatsBarTop);
-		_moreChatsBar->resizeToWidth(barw);
-	}
-	const auto forumGroupCallTop = moreChatsBarTop
-		+ (_moreChatsBar ? _moreChatsBar->height() : 0);
-	if (_forumGroupCallBar) {
-		_forumGroupCallBar->move(0, forumGroupCallTop);
-		_forumGroupCallBar->resizeToWidth(barw);
-	}
-	const auto forumRequestsTop = forumGroupCallTop
-		+ (_forumGroupCallBar ? _forumGroupCallBar->height() : 0);
-	if (_forumRequestsBar) {
-		_forumRequestsBar->move(0, forumRequestsTop);
-		_forumRequestsBar->resizeToWidth(barw);
-	}
-	const auto forumReportTop = forumRequestsTop
-		+ (_forumRequestsBar ? _forumRequestsBar->height() : 0);
-	if (_forumReportBar) {
-		_forumReportBar->bar().move(0, forumReportTop);
-	}
-	auto scrollTop = forumReportTop
-		+ (_forumReportBar ? _forumReportBar->bar().height() : 0);
-	const auto wasScrollTop = _scroll->scrollTop();
-	const auto newScrollTop = (_topDelta < 0 && wasScrollTop <= 0)
-		? wasScrollTop
-		: (wasScrollTop + _topDelta);
-	auto scrollHeight = height() - scrollTop;
+
+	auto bottomSkip = 0;
 	const auto putBottomButton = [&](auto &button) {
 		if (button && !button->isHidden()) {
 			const auto buttonHeight = button->height();
-			scrollHeight -= buttonHeight;
+			bottomSkip += buttonHeight;
 			button->setGeometry(
 				0,
-				scrollTop + scrollHeight,
+				height() - bottomSkip,
 				barw,
 				buttonHeight);
 		}
@@ -2618,24 +2640,55 @@ void Widget::updateControlsGeometry() {
 	putBottomButton(_updateTelegram);
 	putBottomButton(_downloadBar);
 	putBottomButton(_loadMoreChats);
-	const auto bottomSkip = (height() - scrollTop) - scrollHeight;
 	if (_connecting) {
 		_connecting->setBottomSkip(bottomSkip);
 	}
 	controller()->setConnectingBottomSkip(bottomSkip);
 
+	const auto wasScrollTop = _scroll->scrollTop();
+	const auto newScrollTop = (_topDelta < 0 && wasScrollTop <= 0)
+		? wasScrollTop
+		: (wasScrollTop + _topDelta);
+
 	const auto scrollw = _childList ? _narrowWidth : barw;
-	const auto wasScrollHeight = _scroll->height();
-	_scroll->setGeometry(0, scrollTop, scrollw, scrollHeight);
+	_updateScrollGeometryCached = [=] {
+		const auto moreChatsBarTop = expandedStoriesTop + _aboveScrollAdded;
+		if (_moreChatsBar) {
+			_moreChatsBar->move(0, moreChatsBarTop);
+			_moreChatsBar->resizeToWidth(barw);
+		}
+		const auto forumGroupCallTop = moreChatsBarTop
+			+ (_moreChatsBar ? _moreChatsBar->height() : 0);
+		if (_forumGroupCallBar) {
+			_forumGroupCallBar->move(0, forumGroupCallTop);
+			_forumGroupCallBar->resizeToWidth(barw);
+		}
+		const auto forumRequestsTop = forumGroupCallTop
+			+ (_forumGroupCallBar ? _forumGroupCallBar->height() : 0);
+		if (_forumRequestsBar) {
+			_forumRequestsBar->move(0, forumRequestsTop);
+			_forumRequestsBar->resizeToWidth(barw);
+		}
+		const auto forumReportTop = forumRequestsTop
+			+ (_forumRequestsBar ? _forumRequestsBar->height() : 0);
+		if (_forumReportBar) {
+			_forumReportBar->bar().move(0, forumReportTop);
+		}
+		auto scrollTop = forumReportTop
+			+ (_forumReportBar ? _forumReportBar->bar().height() : 0);
+		auto scrollHeight = height() - scrollTop;
+
+		const auto wasScrollHeight = _scroll->height();
+		_scroll->setGeometry(0, scrollTop, scrollw, scrollHeight);
+		if (scrollHeight != wasScrollHeight) {
+			controller()->floatPlayerAreaUpdated();
+		}
+	};
+	_updateScrollGeometryCached();
+
 	_inner->resize(scrollw, _inner->height());
 	_inner->setNarrowRatio(narrowRatio);
-	if (scrollHeight != wasScrollHeight) {
-		controller()->floatPlayerAreaUpdated();
-	}
-	const auto startWithTop = 0;
-	if (wasScrollHeight < startWithTop && scrollHeight >= startWithTop) {
-		_scroll->scrollToY(startWithTop);
-	} else if (newScrollTop != wasScrollTop) {
+	if (newScrollTop != wasScrollTop) {
 		_scroll->scrollToY(newScrollTop);
 	} else {
 		listScrollUpdated();
@@ -2646,7 +2699,7 @@ void Widget::updateControlsGeometry() {
 
 	if (_childList) {
 		const auto childw = std::max(_narrowWidth, width() - scrollw);
-		const auto childh = scrollTop + scrollHeight;
+		const auto childh = _scroll->y() + _scroll->height();
 		const auto childx = width() - childw;
 		_childList->setGeometryWithTopMoved(
 			{ childx, 0, childw, childh },
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 13ca38398..ca83fefa1 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -167,6 +167,7 @@ private:
 	void setupDownloadBar();
 	void setupShortcuts();
 	void setupStories();
+	void storiesToggleExplicitExpand(bool expand);
 	[[nodiscard]] bool searchForPeersRequired(const QString &query) const;
 	[[nodiscard]] bool searchForTopicsRequired(const QString &query) const;
 	bool setSearchInChat(Key chat, PeerData *from = nullptr);
@@ -267,8 +268,13 @@ private:
 	bool _scrollToTopIsShown = false;
 	bool _forumSearchRequested = false;
 
-	int _overscrollTop = 0;
+	Fn<void()> _updateScrollGeometryCached;
 	std::unique_ptr<Stories::List> _stories;
+	Ui::Animations::Simple _storiesExplicitExpandAnimation;
+	rpl::variable<int> _storiesExplicitExpandValue = 0;
+	int _storiesExplicitExpandScrollTop = 0;
+	int _aboveScrollAdded = 0;
+	bool _storiesExplicitExpand = false;
 
 	Data::Folder *_openedFolder = nullptr;
 	Data::Forum *_openedForum = nullptr;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index d7d62b416..2d3ca0d76 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -813,7 +813,7 @@ void List::mouseReleaseEvent(QMouseEvent *e) {
 	}
 	updateSelected();
 	if (_selected == pressed) {
-		if (_selected < 0) {
+		if (!_expanded) {
 			requestExpanded(true);
 		} else if (_selected < _data.items.size()) {
 			_clicks.fire_copy(_data.items[_selected].element.id);
@@ -906,7 +906,7 @@ void List::contextMenuEvent(QContextMenuEvent *e) {
 		_lastMousePosition = e->globalPos();
 		updateSelected();
 	}
-	if (_selected < 0 || _data.empty()) {
+	if (_selected < 0 || _data.empty() || !_expanded) {
 		return;
 	}
 	_menu = base::make_unique_q<Ui::PopupMenu>(
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 80308cea4..763b3a37c 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 80308cea4f57fd13e73abec121325f174ba21883
+Subproject commit 763b3a37c34fd7819f9d553b94387c4fe16554ff

From 70ca3d4f1ac78f25020dde44fd0425fd80575bdb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 19:22:18 +0400
Subject: [PATCH 152/259] Show hidden stories in archive.

---
 Telegram/Resources/langs/lang.strings         |   8 +-
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 233 ++++++++++++------
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  18 +-
 .../dialogs/ui/dialogs_stories_content.cpp    |   8 +-
 .../dialogs/ui/dialogs_stories_list.cpp       | 113 ++++-----
 .../dialogs/ui/dialogs_stories_list.h         |  13 +-
 Telegram/SourceFiles/ui/chat/more_chats_bar.h |   4 +
 Telegram/SourceFiles/ui/menu_icons.style      |   1 -
 Telegram/lib_ui                               |   2 +-
 9 files changed, 241 insertions(+), 159 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 3130497ee..d360b8957 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3823,8 +3823,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji";
 
 "lng_stories_my_name" = "My Story";
-"lng_stories_hide_to_contacts" = "Hide";
-"lng_stories_show_in_chats" = "Show in Chats";
+"lng_stories_archive" = "Archive";
+"lng_stories_unarchive" = "Unarchive";
 "lng_stories_row_count#one" = "{count} Story";
 "lng_stories_row_count#other" = "{count} Stories";
 "lng_stories_row_unread_and_one" = "{accumulated}, {user}";
@@ -3841,8 +3841,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_archive_title" = "Stories Archive";
 "lng_stories_archive_about" = "Only you can see archived stories unless you choose to save them to your profile.";
 "lng_stories_reply_sent" = "Message Sent";
-"lng_stories_hidden_to_contacts" = "Stories of {user} were moved to **Contacts**.";
-"lng_stories_shown_in_chats" = "Stories of {user} were moved above the **Chats List**.";
+"lng_stories_hidden_to_contacts" = "Stories of {user} were moved to **Archive**.";
+"lng_stories_shown_in_chats" = "Stories of {user} were moved to the **Chats List**.";
 "lng_stories_delete_one_sure" = "Are you sure you want to delete this story?";
 "lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?";
 "lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?";
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 74fbf9e6c..fb92cbf1e 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -215,12 +215,12 @@ Widget::Widget(
 , _lockUnlock(_searchControls, st::dialogsLock)
 , _scroll(this)
 , _scrollToTop(_scroll, st::dialogsToUp)
-, _stories(std::make_unique<Stories::List>(
-	this,
-	st::dialogsStoriesList,
-	Stories::ContentForSession(
-		&controller->session(),
-		Data::StorySourcesList::NotHidden)))
+, _stories((_layout != Layout::Child)
+	? std::make_unique<Stories::List>(
+		this,
+		st::dialogsStoriesList,
+		_storiesContents.events() | rpl::flatten_latest())
+	: nullptr)
 , _searchTimer([=] { searchMessages(); })
 , _singleMessageSearch(&controller->session()) {
 	const auto makeChildListShown = [](PeerId peerId, float64 shown) {
@@ -237,46 +237,6 @@ Widget::Widget(
 			_childListShown.value(),
 			makeChildListShown)));
 	_scrollToTop->raise();
-	rpl::combine(
-		_scroll->positionValue(),
-		_scroll->movementValue(),
-		_storiesExplicitExpandValue.value()
-	) | rpl::start_with_next([=](
-			Ui::ElasticScrollPosition position,
-			Ui::ElasticScrollMovement movement,
-			int explicitlyExpanded) {
-		const auto overscrollTop = std::max(-position.overscroll, 0);
-		if (overscrollTop > 0 && _storiesExplicitExpand) {
-			_scroll->setOverscrollDefaults(
-				-st::dialogsStoriesFull.height,
-				0,
-				true);
-		}
-		if (explicitlyExpanded > 0 && explicitlyExpanded < overscrollTop) {
-			_storiesExplicitExpandAnimation.stop();
-			_storiesExplicitExpand = false;
-			_storiesExplicitExpandValue = 0;
-			return;
-		}
-		const auto above = std::max(explicitlyExpanded, overscrollTop);
-		if (_aboveScrollAdded != above) {
-			_aboveScrollAdded = above;
-			if (_updateScrollGeometryCached) {
-				_updateScrollGeometryCached();
-			}
-		}
-		using Phase = Ui::ElasticScrollMovement;
-		_stories->setExpandedHeight(
-			_aboveScrollAdded,
-			(movement == Phase::Momentum || movement == Phase::Returning)
-				&& (explicitlyExpanded < above));
-		if (position.overscroll > 0
-			|| (position.value
-				> (_storiesExplicitExpandScrollTop
-					+ st::dialogsRowHeight))) {
-			storiesToggleExplicitExpand(false);
-		}
-	}, lifetime());
 
 	_inner->updated(
 	) | rpl::start_with_next([=] {
@@ -417,7 +377,9 @@ Widget::Widget(
 
 	setupMainMenuToggle();
 	setupShortcuts();
-	setupStories();
+	if (_stories) {
+		setupStories();
+	}
 
 	_searchForNarrowFilters->setClickedCallback([=] {
 		_filter->setFocusFast();
@@ -609,14 +571,8 @@ void Widget::scrollToDefaultChecked(bool verytop) {
 
 void Widget::setupScrollUpButton() {
 	_scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); });
-	base::install_event_filter(_scrollToTop, [=](not_null<QEvent*> event) {
-		if (event->type() != QEvent::Wheel) {
-			return base::EventFilterResult::Continue;
-		}
-		return _scroll->viewportEvent(event)
-			? base::EventFilterResult::Cancel
-			: base::EventFilterResult::Continue;
-	});
+	trackScroll(_scrollToTop);
+	trackScroll(this);
 	updateScrollUpVisibility();
 }
 
@@ -628,7 +584,7 @@ void Widget::setupMoreChatsBar() {
 	) | rpl::start_with_next([=](FilterId id) {
 		storiesToggleExplicitExpand(false);
 
-		if (!id && false) { // #TODO stories testing
+		if (!id) {
 			_moreChatsBar = nullptr;
 			updateControlsGeometry();
 			return;
@@ -638,6 +594,8 @@ void Widget::setupMoreChatsBar() {
 			this,
 			filters->moreChatsContent(id));
 
+		trackScroll(_moreChatsBar->wrap());
+
 		_moreChatsBar->barClicks(
 		) | rpl::start_with_next([=] {
 			if (const auto missing = filters->moreChats(id)
@@ -810,11 +768,64 @@ void Widget::setupMainMenuToggle() {
 }
 
 void Widget::setupStories() {
+	trackScroll(_stories.get());
+
+	_storiesContents.fire(Stories::ContentForSession(
+		&controller()->session(),
+		Data::StorySourcesList::NotHidden));
+
+	const auto currentSource = [=] {
+		using List = Data::StorySourcesList;
+		return _openedFolder ? List::Hidden : List::NotHidden;
+	};
+
+	rpl::combine(
+		_scroll->positionValue(),
+		_scroll->movementValue(),
+		_storiesExplicitExpandValue.value()
+	) | rpl::start_with_next([=](
+			Ui::ElasticScrollPosition position,
+			Ui::ElasticScrollMovement movement,
+			int explicitlyExpanded) {
+		if (_stories->isHidden()) {
+			return;
+		}
+		const auto overscrollTop = std::max(-position.overscroll, 0);
+		if (overscrollTop > 0 && _storiesExplicitExpand) {
+			_scroll->setOverscrollDefaults(
+				-st::dialogsStoriesFull.height,
+				0,
+				true);
+		}
+		if (explicitlyExpanded > 0 && explicitlyExpanded < overscrollTop) {
+			_storiesExplicitExpandAnimation.stop();
+			_storiesExplicitExpand = false;
+			_storiesExplicitExpandValue = 0;
+			return;
+		}
+		const auto above = std::max(explicitlyExpanded, overscrollTop);
+		if (_aboveScrollAdded != above) {
+			_aboveScrollAdded = above;
+			if (_updateScrollGeometryCached) {
+				_updateScrollGeometryCached();
+			}
+		}
+		using Phase = Ui::ElasticScrollMovement;
+		_stories->setExpandedHeight(
+			_aboveScrollAdded,
+			(movement == Phase::Momentum || movement == Phase::Returning)
+				&& (explicitlyExpanded < above));
+		if (position.overscroll > 0
+			|| (position.value
+				> (_storiesExplicitExpandScrollTop
+					+ st::dialogsRowHeight))) {
+			storiesToggleExplicitExpand(false);
+		}
+	}, lifetime());
+
 	_stories->clicks(
 	) | rpl::start_with_next([=](uint64 id) {
-		controller()->openPeerStories(
-			PeerId(int64(id)),
-			Data::StorySourcesList::NotHidden);
+		controller()->openPeerStories(PeerId(int64(id)), currentSource());
 	}, lifetime());
 
 	_stories->showMenuRequests(
@@ -824,8 +835,7 @@ void Widget::setupStories() {
 
 	_stories->loadMoreRequests(
 	) | rpl::start_with_next([=] {
-		session().data().stories().loadMore(
-			Data::StorySourcesList::NotHidden);
+		session().data().stories().loadMore(currentSource());
 	}, lifetime());
 
 	_stories->toggleExpandedRequests(
@@ -860,6 +870,20 @@ void Widget::storiesToggleExplicitExpand(bool expand) {
 	}, expand ? 0 : height, expand ? height : 0, duration, anim::sineInOut);
 }
 
+void Widget::trackScroll(not_null<Ui::RpWidget*> widget) {
+	widget->events(
+	) | rpl::start_with_next([=](not_null<QEvent*> e) {
+		const auto type = e->type();
+		if (type == QEvent::TouchBegin
+			|| type == QEvent::TouchUpdate
+			|| type == QEvent::TouchEnd
+			|| type == QEvent::TouchCancel
+			|| type == QEvent::Wheel) {
+			_scroll->viewportEvent(e);
+		}
+	}, widget->lifetime());
+}
+
 void Widget::setupShortcuts() {
 	Shortcuts::Requests(
 	) | rpl::filter([=] {
@@ -902,6 +926,7 @@ void Widget::fullSearchRefreshOn(rpl::producer<> events) {
 void Widget::updateControlsVisibility(bool fast) {
 	updateLoadMoreChatsVisibility();
 	_scroll->show();
+	updateStoriesVisibility();
 	if ((_openedFolder || _openedForum) && _filter->hasFocus()) {
 		setInnerFocus();
 	}
@@ -1002,9 +1027,16 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) {
 		cancelSearch();
 		closeChildList(anim::type::instant);
 		controller()->closeForum();
+		const auto was = (_openedFolder != nullptr);
 		_openedFolder = folder;
 		_inner->changeOpenedFolder(folder);
 		storiesToggleExplicitExpand(false);
+		if (was != (_openedFolder != nullptr)) {
+			using List = Data::StorySourcesList;
+			_storiesContents.fire(Stories::ContentForSession(
+				&controller()->session(),
+				folder ? List::Hidden : List::NotHidden));
+		}
 	}, (folder != nullptr), animated);
 }
 
@@ -1019,6 +1051,7 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) {
 		_api.request(base::take(_topicSearchRequest)).cancel();
 		_inner->changeOpenedForum(forum);
 		storiesToggleExplicitExpand(false);
+		updateStoriesVisibility();
 	}, (forum != nullptr), animated);
 }
 
@@ -1032,6 +1065,9 @@ void Widget::refreshTopBars() {
 	if (_openedFolder || _openedForum) {
 		if (!_subsectionTopBar) {
 			_subsectionTopBar.create(this, controller());
+			if (_stories) {
+				_stories->raise();
+			}
 			_subsectionTopBar->searchCancelled(
 			) | rpl::start_with_next([=] {
 				escape();
@@ -1306,6 +1342,7 @@ void Widget::startWidthAnimation() {
 	_widthAnimationCache = Ui::PixmapFromImage(std::move(image));
 	_scroll->setGeometry(scrollGeometry);
 	_scroll->hide();
+	updateStoriesVisibility();
 }
 
 void Widget::stopWidthAnimation() {
@@ -1313,9 +1350,41 @@ void Widget::stopWidthAnimation() {
 	if (!_showAnimation) {
 		_scroll->show();
 	}
+	updateStoriesVisibility();
 	update();
 }
 
+void Widget::updateStoriesVisibility() {
+	if (!_stories) {
+		return;
+	}
+	const auto hidden = (_showAnimation != nullptr)
+		|| _openedForum
+		|| !_widthAnimationCache.isNull()
+		|| _childList
+		|| !_filter->getLastText().isEmpty()
+		|| _searchInChat;
+	if (_stories->isHidden() != hidden) {
+		_stories->setVisible(!hidden);
+		using Type = Ui::ElasticScroll::OverscrollType;
+		if (hidden) {
+			_scroll->setOverscrollDefaults(0, 0);
+			_scroll->setOverscrollTypes(Type::Real, Type::Real);
+			if (_scroll->position().overscroll < 0) {
+				_scroll->scrollToY(0);
+			}
+		} else {
+			_scroll->setOverscrollDefaults(0, 0);
+			_scroll->setOverscrollTypes(Type::Virtual, Type::Real);
+			_storiesExplicitExpandValue.force_assign(
+				_storiesExplicitExpandValue.current());
+		}
+		if (_aboveScrollAdded > 0 && _updateScrollGeometryCached) {
+			_updateScrollGeometryCached();
+		}
+	}
+}
+
 void Widget::showFast() {
 	if (isHidden()) {
 		_inner->clearSelection();
@@ -1358,6 +1427,9 @@ void Widget::startSlideAnimation(
 		QPixmap newContentCache,
 		Window::SlideDirection direction) {
 	_scroll->hide();
+	if (_stories) {
+		_stories->hide();
+	}
 	_searchControls->hide();
 	if (_subsectionTopBar) {
 		_subsectionTopBar->hide();
@@ -2166,6 +2238,7 @@ void Widget::applyFilterUpdate(bool force) {
 		return;
 	}
 
+	updateStoriesVisibility();
 	const auto filterText = currentSearchQuery();
 	_inner->applyFilterUpdate(filterText, force);
 	if (filterText.isEmpty() && !_searchFromAuthor) {
@@ -2319,6 +2392,7 @@ void Widget::closeChildList(anim::type animated) {
 	} else {
 		_childListShadow = nullptr;
 	}
+	updateStoriesVisibility();
 }
 
 void Widget::searchInChat(Key chat) {
@@ -2372,6 +2446,7 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
 		_searchInChat = chat;
 		controller()->searchInChat = _searchInChat;
 		updateJumpToDateVisibility();
+		updateStoriesVisibility();
 	}
 	if (searchFromUpdated) {
 		updateSearchFromVisibility();
@@ -2614,9 +2689,11 @@ void Widget::updateControlsGeometry() {
 	const auto storiesHeight = 2 * st::dialogsStories.photoTop
 		+ st::dialogsStories.photo;
 	const auto added = (st::dialogsFilter.heightMin - storiesHeight) / 2;
-	_stories->setLayoutConstraints(
-		{ filterLeft + filterWidth, filterTop + added },
-		{ 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height });
+	if (_stories) {
+		_stories->setLayoutConstraints(
+			{ filterLeft + filterWidth, filterTop + added },
+			{ 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height });
+	}
 	if (_forumTopShadow) {
 		_forumTopShadow->setGeometry(
 			0,
@@ -2650,43 +2727,49 @@ void Widget::updateControlsGeometry() {
 		? wasScrollTop
 		: (wasScrollTop + _topDelta);
 
-	const auto scrollw = _childList ? _narrowWidth : barw;
+	const auto scrollWidth = _childList ? _narrowWidth : barw;
+	if (_moreChatsBar) {
+		_moreChatsBar->resizeToWidth(barw);
+	}
+	if (_forumGroupCallBar) {
+		_forumGroupCallBar->resizeToWidth(barw);
+	}
+	if (_forumRequestsBar) {
+		_forumRequestsBar->resizeToWidth(barw);
+	}
 	_updateScrollGeometryCached = [=] {
-		const auto moreChatsBarTop = expandedStoriesTop + _aboveScrollAdded;
+		const auto moreChatsBarTop = expandedStoriesTop
+			+ ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded);
 		if (_moreChatsBar) {
 			_moreChatsBar->move(0, moreChatsBarTop);
-			_moreChatsBar->resizeToWidth(barw);
 		}
 		const auto forumGroupCallTop = moreChatsBarTop
 			+ (_moreChatsBar ? _moreChatsBar->height() : 0);
 		if (_forumGroupCallBar) {
 			_forumGroupCallBar->move(0, forumGroupCallTop);
-			_forumGroupCallBar->resizeToWidth(barw);
 		}
 		const auto forumRequestsTop = forumGroupCallTop
 			+ (_forumGroupCallBar ? _forumGroupCallBar->height() : 0);
 		if (_forumRequestsBar) {
 			_forumRequestsBar->move(0, forumRequestsTop);
-			_forumRequestsBar->resizeToWidth(barw);
 		}
 		const auto forumReportTop = forumRequestsTop
 			+ (_forumRequestsBar ? _forumRequestsBar->height() : 0);
 		if (_forumReportBar) {
 			_forumReportBar->bar().move(0, forumReportTop);
 		}
-		auto scrollTop = forumReportTop
+		const auto scrollTop = forumReportTop
 			+ (_forumReportBar ? _forumReportBar->bar().height() : 0);
-		auto scrollHeight = height() - scrollTop;
-
+		const auto scrollHeight = height() - scrollTop;
 		const auto wasScrollHeight = _scroll->height();
-		_scroll->setGeometry(0, scrollTop, scrollw, scrollHeight);
+		_scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight);
 		if (scrollHeight != wasScrollHeight) {
 			controller()->floatPlayerAreaUpdated();
 		}
 	};
 	_updateScrollGeometryCached();
 
-	_inner->resize(scrollw, _inner->height());
+	_inner->resize(scrollWidth, _inner->height());
 	_inner->setNarrowRatio(narrowRatio);
 	if (newScrollTop != wasScrollTop) {
 		_scroll->scrollToY(newScrollTop);
@@ -2698,7 +2781,7 @@ void Widget::updateControlsGeometry() {
 	}
 
 	if (_childList) {
-		const auto childw = std::max(_narrowWidth, width() - scrollw);
+		const auto childw = std::max(_narrowWidth, width() - scrollWidth);
 		const auto childh = _scroll->y() + _scroll->height();
 		const auto childx = width() - childw;
 		_childList->setGeometryWithTopMoved(
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index ca83fefa1..245998392 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -58,6 +58,7 @@ struct SectionShow;
 
 namespace Dialogs::Stories {
 class List;
+struct Content;
 } // namespace Dialogs::Stories
 
 namespace Dialogs {
@@ -168,6 +169,7 @@ private:
 	void setupShortcuts();
 	void setupStories();
 	void storiesToggleExplicitExpand(bool expand);
+	void trackScroll(not_null<Ui::RpWidget*> widget);
 	[[nodiscard]] bool searchForPeersRequired(const QString &query) const;
 	[[nodiscard]] bool searchForTopicsRequired(const QString &query) const;
 	bool setSearchInChat(Key chat, PeerData *from = nullptr);
@@ -179,6 +181,7 @@ private:
 	void updateControlsVisibility(bool fast = false);
 	void updateLockUnlockVisibility();
 	void updateLoadMoreChatsVisibility();
+	void updateStoriesVisibility();
 	void updateJumpToDateVisibility(bool fast = false);
 	void updateSearchFromVisibility(bool fast = false);
 	void updateControlsGeometry();
@@ -268,6 +271,14 @@ private:
 	bool _scrollToTopIsShown = false;
 	bool _forumSearchRequested = false;
 
+	Data::Folder *_openedFolder = nullptr;
+	Data::Forum *_openedForum = nullptr;
+	Dialogs::Key _searchInChat;
+	History *_searchInMigrated = nullptr;
+	PeerData *_searchFromAuthor = nullptr;
+	QString _lastFilterText;
+
+	rpl::event_stream<rpl::producer<Stories::Content>> _storiesContents;
 	Fn<void()> _updateScrollGeometryCached;
 	std::unique_ptr<Stories::List> _stories;
 	Ui::Animations::Simple _storiesExplicitExpandAnimation;
@@ -276,13 +287,6 @@ private:
 	int _aboveScrollAdded = 0;
 	bool _storiesExplicitExpand = false;
 
-	Data::Folder *_openedFolder = nullptr;
-	Data::Forum *_openedForum = nullptr;
-	Dialogs::Key _searchInChat;
-	History *_searchInMigrated = nullptr;
-	PeerData *_searchFromAuthor = nullptr;
-	QString _lastFilterText;
-
 	base::Timer _searchTimer;
 
 	QString _peerSearchQuery;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index b882842ca..2f6c55dd1 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -503,14 +503,14 @@ void FillSourceMenu(
 				controller->uiShow());
 		};
 		if (in(Data::StorySourcesList::NotHidden)) {
-			add(tr::lng_stories_hide_to_contacts(tr::now), [=] {
+			add(tr::lng_stories_archive(tr::now), [=] {
 				toggle(false);
-			}, &st::menuIconCancel);
+			}, &st::menuIconArchive);
 		}
 		if (in(Data::StorySourcesList::Hidden)) {
-			add(tr::lng_stories_show_in_chats(tr::now), [=] {
+			add(tr::lng_stories_unarchive(tr::now), [=] {
 				toggle(true);
-			}, &st::menuIconStoriesToChats);
+			}, &st::menuIconUnarchive);
 		}
 	}
 }
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 2d3ca0d76..dc6384a8d 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -21,8 +21,8 @@ namespace {
 constexpr auto kSmallThumbsShown = 3;
 constexpr auto kSummaryExpandLeft = 1;
 constexpr auto kPreloadPages = 2;
-constexpr auto kExpandAfterRatio = 0.85;
-constexpr auto kCollapseAfterRatio = 0.72;
+constexpr auto kExpandAfterRatio = 0.72;
+constexpr auto kCollapseAfterRatio = 0.68;
 constexpr auto kFrictionRatio = 0.15;
 constexpr auto kExpandCatchUpDuration = crl::time(200);
 
@@ -66,7 +66,6 @@ List::List(
 		showContent(std::move(content));
 	}, lifetime());
 
-	_shownAnimation.stop();
 	setMouseTracking(true);
 	resize(0, _data.empty() ? 0 : st.full.height);
 }
@@ -76,18 +75,13 @@ void List::showContent(Content &&content) {
 		return;
 	}
 	if (content.elements.empty()) {
-		_hidingData = base::take(_data);
-		if (!_hidingData.empty()) {
-			toggleAnimated(false);
-		}
+		_data = {};
+		_empty = true;
 		return;
 	}
-	const auto hidden = _content.elements.empty();
-	const auto wasCount = int((hidden ? _hidingData : _data).items.size());
+	const auto wasCount = int(_data.items.size());
 	_content = std::move(content);
-	auto items = base::take(
-		_data.items.empty() ? _hidingData.items : _data.items);
-	_hidingData = {};
+	auto items = base::take(_data.items);
 	_data.items.reserve(_content.elements.size());
 	for (const auto &element : _content.elements) {
 		const auto id = element.id;
@@ -117,8 +111,8 @@ void List::showContent(Content &&content) {
 	updateScrollMax();
 	updateSummary(_data);
 	update();
-	if (hidden) {
-		toggleAnimated(true);
+	if (!wasCount) {
+		_empty = false;
 	}
 }
 
@@ -204,24 +198,6 @@ void List::updateSummary(Data &data) {
 	Populate(_st.small, data.summaries);
 }
 
-void List::toggleAnimated(bool shown) {
-	_shownAnimation.start(
-		[=] { updateHeight(); },
-		shown ? 0. : 1.,
-		shown ? 1. : 0.,
-		st::slideWrapDuration);
-}
-
-void List::updateHeight() {
-	const auto shown = _shownAnimation.value(_data.empty() ? 0. : 1.);
-	resize(
-		width(),
-		anim::interpolate(0, _st.full.height, shown));
-	if (_data.empty() && shown == 0.) {
-		_hidingData = {};
-	}
-}
-
 void List::updateScrollMax() {
 	const auto &full = _st.full;
 	const auto singleFull = full.photoLeft * 2 + full.photo;
@@ -256,7 +232,7 @@ void List::requestExpanded(bool expanded) {
 	if (_expanded != expanded) {
 		_expanded = expanded;
 		_expandedAnimation.start(
-			[=] { update(); },
+			[=] { checkForFullState(); update(); },
 			_expanded ? 0. : 1.,
 			_expanded ? 1. : 0.,
 			st::slideWrapDuration,
@@ -308,15 +284,14 @@ List::Layout List::computeLayout() {
 	const auto lerp = [&](float64 a, float64 b) {
 		return a + (b - a) * ratio;
 	};
-	auto &rendering = _data.empty() ? _hidingData : _data;
 	const auto singleFull = full.photoLeft * 2 + full.photo;
-	const auto itemsCount = int(rendering.items.size());
+	const auto itemsCount = int(_data.items.size());
 	const auto narrowWidth = st::defaultDialogRow.padding.left()
 		+ st::defaultDialogRow.photoSize
 		+ st::defaultDialogRow.padding.left();
 	const auto narrow = false;// (width() <= narrowWidth);
 	const auto smallSkip = (itemsCount > 1
-		&& rendering.items[0].element.skipSmall)
+		&& _data.items[0].element.skipSmall)
 		? 1
 		: 0;
 	const auto smallCount = std::min(
@@ -378,7 +353,6 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto elerp = [&](float64 a, float64 b) {
 		return a + (b - a) * expandRatio;
 	};
-	auto &rendering = _data.empty() ? _hidingData : _data;
 	const auto line = elerp(st.lineTwice, full.lineTwice) / 2.;
 	const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.;
 	const auto photoTopSmall = st.photoTop;
@@ -389,7 +363,8 @@ void List::paintEvent(QPaintEvent *e) {
 		- (st.photoTop + (st.photo / 2.))
 		+ (photoTop + (photo / 2.));
 	const auto nameScale = _lastRatio;
-	const auto nameTop = nameScale * full.nameTop;
+	const auto nameTop = full.nameTop
+		+ (photoTop + photo - full.photoTop - full.photo);
 	const auto nameWidth = nameScale * AvailableNameWidth(_st);
 	const auto nameHeight = nameScale * full.nameStyle.font->height;
 	const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;
@@ -402,11 +377,11 @@ void List::paintEvent(QPaintEvent *e) {
 		const auto left = anim::interpolate(
 			_changingGeometryFrom.x(),
 			_geometryFull.x(),
-			layout.expandedRatio);
+			layout.ratio);
 		const auto top = anim::interpolate(
 			_changingGeometryFrom.y(),
 			_geometryFull.y(),
-			layout.expandedRatio);
+			layout.ratio);
 		p.translate(QPoint(left, top) - pos());
 	}
 
@@ -433,18 +408,19 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto lookup = [&](int index) {
 		const auto indexSmall = layout.startIndexSmall + index;
 		const auto indexFull = layout.startIndexFull + index;
+		const auto k = (photoTop - photoTopSmall);
 		const auto ySmall = photoTopSmall
-			+ ((indexSmall - layout.smallSkip + 1)
-				* (photoTop - photoTopSmall) / 3.);
+			+ ((photoTop - photoTopSmall)
+				* (kSmallThumbsShown - indexSmall + layout.smallSkip) / 0.5);
 		const auto y = elerp(ySmall, photoTop);
 
 		const auto small = (drawSmall
 			&& indexSmall < layout.endIndexSmall
 			&& indexSmall >= layout.smallSkip)
-			? &rendering.items[indexSmall]
+			? &_data.items[indexSmall]
 			: nullptr;
 		const auto full = (drawFull && indexFull < layout.endIndexFull)
-			? &rendering.items[indexFull]
+			? &_data.items[indexFull]
 			: nullptr;
 		const auto x = layout.left + layout.single * index;
 		return Single{ x, indexSmall, small, indexFull, full, y };
@@ -556,9 +532,16 @@ void List::paintEvent(QPaintEvent *e) {
 
 		// White circle with possible read gray line.
 		const auto hasReadLine = (itemFull && !fullUnread);
+		p.setOpacity((small && itemFull)
+			? 1.
+			: small
+			? (1. - expandRatio)
+			: expandRatio);
 		if (hasReadLine) {
 			auto color = st::dialogsUnreadBgMuted->c;
-			color.setAlphaF(color.alphaF() * expandRatio);
+			if (small) {
+				color.setAlphaF(color.alphaF() * expandRatio);
+			}
 			auto pen = QPen(color);
 			pen.setWidthF(lineRead);
 			p.setPen(pen);
@@ -601,7 +584,7 @@ void List::paintEvent(QPaintEvent *e) {
 		p.setOpacity(1.);
 	});
 
-	paintSummary(p, rendering, summaryTop, ratio);
+	paintSummary(p, _data, summaryTop, ratio);
 }
 
 void List::validateThumbnail(not_null<Item*> item) {
@@ -729,7 +712,7 @@ void List::paintSummary(
 
 void List::wheelEvent(QWheelEvent *e) {
 	const auto horizontal = (e->angleDelta().x() != 0);
-	if (!horizontal) {
+	if (!horizontal || _state == State::Small) {
 		e->ignore();
 		return;
 	}
@@ -757,6 +740,10 @@ void List::wheelEvent(QWheelEvent *e) {
 void List::mousePressEvent(QMouseEvent *e) {
 	if (e->button() != Qt::LeftButton) {
 		return;
+	} else if (_state == State::Small) {
+		requestExpanded(true);
+	} else if (_state != State::Full) {
+		return;
 	}
 	_lastMousePosition = e->globalPos();
 	updateSelected();
@@ -834,25 +821,28 @@ void List::setExpandedHeight(int height, bool momentum) {
 		_expandIgnored = false;
 		_expandCatchUpAnimation.start([=] {
 			update();
-			if (!_expandCatchUpAnimation.animating()
-				&& _lastExpandedHeight == _st.full.height) {
-				setState(State::Full);
-			}
+			checkForFullState();
 		}, 0., 1., kExpandCatchUpDuration);
 	} else if (!height && _expandCatchUpAnimation.animating()) {
 		_expandCatchUpAnimation.stop();
 	}
 	_lastExpandedHeight = height;
-	setState(!height
-		? State::Small
-		: (height < _st.full.height
-			|| _expandCatchUpAnimation.animating()
-			|| _expandedAnimation.animating())
-		? State::Changing
-		: State::Full);
+	if (!checkForFullState()) {
+		setState(!height ? State::Small : State::Changing);
+	}
 	update();
 }
 
+bool List::checkForFullState() {
+	if (_expandCatchUpAnimation.animating()
+		|| _expandedAnimation.animating()
+		|| _lastExpandedHeight < _st.full.height) {
+		return false;
+	}
+	setState(State::Full);
+	return true;
+}
+
 void List::setLayoutConstraints(QPoint topRightSmall, QRect geometryFull) {
 	_topRightSmall = topRightSmall;
 	_geometryFull = geometryFull;
@@ -875,7 +865,9 @@ void List::updateGeometry() {
 QRect List::countSmallGeometry() const {
 	const auto &st = _st.small;
 	const auto layout = const_cast<List*>(this)->computeLayout();
-	const auto count = layout.endIndexSmall - layout.startIndexSmall;
+	const auto count = layout.endIndexSmall
+		- layout.startIndexSmall
+		- layout.smallSkip;
 	const auto width = st.left
 		+ st.photoLeft
 		+ st.photo + (count - 1) * st.shift
@@ -893,9 +885,6 @@ void List::setState(State state) {
 		return;
 	}
 	_state = state;
-	setAttribute(
-		Qt::WA_TransparentForMouseEvents,
-		state == State::Changing);
 	updateGeometry();
 }
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 0bf355bdb..7a02db40f 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -68,6 +68,12 @@ public:
 	void setExpandedHeight(int height, bool momentum = false);
 	void setLayoutConstraints(QPoint topRightSmall, QRect geometryFull);
 
+	[[nodiscard]] bool empty() const {
+		return _empty.current();
+	}
+	[[nodiscard]] rpl::producer<bool> emptyValue() const {
+		return _empty.value();
+	}
 	[[nodiscard]] rpl::producer<uint64> clicks() const;
 	[[nodiscard]] rpl::producer<ShowMenuRequest> showMenuRequests() const;
 	[[nodiscard]] rpl::producer<bool> toggleExpandedRequests() const;
@@ -153,12 +159,11 @@ private:
 	void checkLoadMore();
 	void requestExpanded(bool expanded);
 
+	bool checkForFullState();
 	void setState(State state);
 	void updateGeometry();
 	[[nodiscard]] QRect countSmallGeometry() const;
 	void updateExpanding(int expandingHeight, int expandedHeight);
-	void updateHeight();
-	void toggleAnimated(bool shown);
 	void paintSummary(
 		QPainter &p,
 		Data &data,
@@ -170,7 +175,6 @@ private:
 	const style::DialogsStoriesList &_st;
 	Content _content;
 	Data _data;
-	Data _hidingData;
 	rpl::event_stream<uint64> _clicks;
 	rpl::event_stream<ShowMenuRequest> _showMenuRequests;
 	rpl::event_stream<bool> _toggleExpandedRequests;
@@ -181,8 +185,7 @@ private:
 	QRect _geometryFull;
 	QRect _changingGeometryFrom;
 	State _state = State::Small;
-
-	Ui::Animations::Simple _shownAnimation;
+	rpl::variable<bool> _empty = true;
 
 	QPoint _lastMousePosition;
 	std::optional<QPoint> _mouseDownPosition;
diff --git a/Telegram/SourceFiles/ui/chat/more_chats_bar.h b/Telegram/SourceFiles/ui/chat/more_chats_bar.h
index 5536349c8..f920e4832 100644
--- a/Telegram/SourceFiles/ui/chat/more_chats_bar.h
+++ b/Telegram/SourceFiles/ui/chat/more_chats_bar.h
@@ -31,6 +31,10 @@ public:
 		rpl::producer<MoreChatsBarContent> content);
 	~MoreChatsBar();
 
+	[[nodiscard]] not_null<RpWidget*> wrap() {
+		return &_wrap;
+	}
+
 	void show();
 	void hide();
 	void raise();
diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style
index 183026881..f1b1a2029 100644
--- a/Telegram/SourceFiles/ui/menu_icons.style
+++ b/Telegram/SourceFiles/ui/menu_icons.style
@@ -112,7 +112,6 @@ menuIconArchiveOpen: icon {{ "menu/archive_open", menuIconColor }};
 menuIconGroups: icon {{ "menu/groups", menuIconColor }};
 menuIconSavedMessages: icon {{ "menu/saved_messages", menuIconColor }};
 menuIconNightMode: icon {{ "menu/night_mode", menuIconColor }};
-menuIconStoriesToChats: icon {{ "menu/stories_to_chats", menuIconColor }};
 
 menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
 menuIconTTLAnyTextPosition: point(11px, 22px);
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 763b3a37c..427fc4c8f 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 763b3a37c34fd7819f9d553b94387c4fe16554ff
+Subproject commit 427fc4c8f7c8b92cca900e0270718f83fcb16e35

From 7b911897fc5dd89ed1b59cf639105fce13fbfefc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 19:41:04 +0400
Subject: [PATCH 153/259] Remove legacy summaries from stories list.

---
 Telegram/Resources/langs/lang.strings         |   2 -
 .../dialogs/ui/dialogs_stories_list.cpp       | 177 ------------------
 .../dialogs/ui/dialogs_stories_list.h         |  44 -----
 3 files changed, 223 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index d360b8957..81a75e5a6 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3827,8 +3827,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_unarchive" = "Unarchive";
 "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}";
 "lng_stories_views#one" = "{count} view";
 "lng_stories_views#other" = "{count} views";
 "lng_stories_no_views" = "No views";
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index dc6384a8d..0fd70644d 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -109,95 +109,12 @@ void List::showContent(Content &&content) {
 		updateGeometry();
 	}
 	updateScrollMax();
-	updateSummary(_data);
 	update();
 	if (!wasCount) {
 		_empty = false;
 	}
 }
 
-List::Summaries List::ComposeSummaries(Data &data) {
-	const auto total = int(data.items.size());
-	const auto skip = (total > 1 && data.items[0].element.skipSmall)
-		? 1
-		: 0;
-	auto unreadInFirst = 0;
-	auto unreadTotal = 0;
-	for (auto i = skip; i != total; ++i) {
-		if (data.items[i].element.unreadCount > 0) {
-			++unreadTotal;
-			if (i < skip + kSmallThumbsShown) {
-				++unreadInFirst;
-			}
-		}
-	}
-	auto result = Summaries{ .skipOne = (skip > 0) };
-	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].element.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].element.name);
-		}
-	};
-	if (!total) {
-		return result;
-	} else if (total <= skip + kSmallThumbsShown) {
-		for (auto i = skip; i != total; ++i) {
-			append(result.allNames.string, i, i == total - 1);
-		}
-	}
-	if (unreadInFirst > 0 && unreadInFirst == unreadTotal) {
-		for (auto i = skip; i != total; ++i) {
-			if (data.items[i].element.unreadCount > 0) {
-				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(
-		const style::DialogsStories &st,
-		Summary &summary) {
-	if (summary.empty()) {
-		return;
-	}
-	summary.cache = QImage();
-	summary.text = Ui::Text::String(st.nameStyle, summary.string);
-}
-
-void List::Populate(
-		const style::DialogsStories &st,
-		Summaries &summaries) {
-	Populate(st, summaries.total);
-	Populate(st, summaries.allNames);
-	Populate(st, summaries.unreadNames);
-}
-
-void List::updateSummary(Data &data) {
-	auto summaries = ComposeSummaries(data);
-	if (StringsEqual(summaries, data.summaries)) {
-		return;
-	}
-	data.summaries = std::move(summaries);
-	Populate(_st.small, data.summaries);
-}
-
 void List::updateScrollMax() {
 	const auto &full = _st.full;
 	const auto singleFull = full.photoLeft * 2 + full.photo;
@@ -583,8 +500,6 @@ void List::paintEvent(QPaintEvent *e) {
 		}
 		p.setOpacity(1.);
 	});
-
-	paintSummary(p, _data, summaryTop, ratio);
 }
 
 void List::validateThumbnail(not_null<Item*> item) {
@@ -618,98 +533,6 @@ void List::validateName(not_null<Item*> item) {
 	text.drawElided(p, 0, 0, available, 1, style::al_top);
 }
 
-List::Summary &List::ChooseSummary(
-		const style::DialogsStories &st,
-		Summaries &summaries,
-		int totalItems,
-		int fullWidth) {
-	const auto used = std::min(
-		totalItems - (summaries.skipOne ? 1 : 0),
-		kSmallThumbsShown);
-	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(
-		const style::DialogsStories &st,
-		Summary &summary) {
-	if (!summary.cache.isNull()
-		&& summary.cacheForWidth == summary.available
-		&& summary.cacheColor == st::dialogsNameFg->c) {
-		return;
-	}
-	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) {
-#if 0 // #TODO stories to-remove
-	const auto total = int(data.items.size());
-	auto &summary = ChooseSummary(
-		_st.small,
-		data.summaries,
-		total,
-		width());
-	PrerenderSummary(_st.small, summary);
-	const auto lerp = [&](float64 from, float64 to) {
-		return from + (to - from) * hidden;
-	};
-	const auto &st = _st.small;
-	const auto &full = _st.full;
-	const auto used = std::min(
-		total - (data.summaries.skipOne ? 1 : 0),
-		kSmallThumbsShown);
-	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);
-#endif
-}
-
 void List::wheelEvent(QWheelEvent *e) {
 	const auto horizontal = (e->angleDelta().x() != 0);
 	if (!horizontal || _state == State::Small) {
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 7a02db40f..7e0028822 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -93,52 +93,14 @@ 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;
-		bool skipOne = false;
-	};
 	struct Data {
 		std::vector<Item> 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(
-		const style::DialogsStories &st,
-		Summary &summary);
-	static void Populate(
-		const style::DialogsStories &st,
-		Summaries &summaries);
-	[[nodiscard]] static Summary &ChooseSummary(
-		const style::DialogsStories &st,
-		Summaries &summaries,
-		int totalItems,
-		int fullWidth);
-	static void PrerenderSummary(
-		const style::DialogsStories &st,
-		Summary &summary);
-
 	void showContent(Content &&content);
 	void enterEventHook(QEnterEvent *e) override;
 	void resizeEvent(QResizeEvent *e) override;
@@ -152,7 +114,6 @@ private:
 	void validateThumbnail(not_null<Item*> item);
 	void validateName(not_null<Item*> item);
 	void updateScrollMax();
-	void updateSummary(Data &data);
 	void updateSelected();
 	void checkDragging();
 	bool finishDragging();
@@ -164,11 +125,6 @@ private:
 	void updateGeometry();
 	[[nodiscard]] QRect countSmallGeometry() const;
 	void updateExpanding(int expandingHeight, int expandedHeight);
-	void paintSummary(
-		QPainter &p,
-		Data &data,
-		float64 summaryTop,
-		float64 hidden);
 
 	[[nodiscard]] Layout computeLayout();
 

From 06469270d029be0528e094f2a83e8bf37c8bb202 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 19:41:23 +0400
Subject: [PATCH 154/259] Remove hidden stories from contacts box.

---
 .../boxes/peer_list_controllers.cpp           | 278 ------------------
 1 file changed, 278 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 7090a018d..2019f0c50 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -53,63 +53,6 @@ namespace {
 constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
 constexpr auto kSearchPerPage = 50;
 
-class StoriesRow final : public PeerListRow {
-public:
-	StoriesRow(
-		not_null<Main::Session*> session,
-		const QBrush &unread,
-		const Dialogs::Stories::Element &element,
-		int position);
-
-	void applySegments(const Dialogs::Stories::Element &element);
-	void updateGradient(QBrush unread);
-
-	int position = 0;
-
-private:
-	void refreshSegments();
-
-	QBrush _unread;
-	int _count = 0;
-	int _unreadCount = 0;
-
-};
-
-class StoriesController final
-	: public PeerListController
-	, public base::has_weak_ptr {
-public:
-	using Content = Dialogs::Stories::Content;
-	using Row = StoriesRow;
-
-	StoriesController(
-		not_null<Window::SessionController*> window,
-		rpl::producer<Content> content,
-		Fn<void(uint64)> open,
-		Fn<void()> loadMore);
-
-	Main::Session &session() const override;
-	void prepare() override;
-	void loadMoreRows() override;
-	void rowClicked(not_null<PeerListRow*> row) override;
-	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
-		QWidget *parent,
-		not_null<PeerListRow*> row) override;
-
-private:
-	void refresh(const Content &content);
-
-	const not_null<Window::SessionController*> _window;
-	QBrush _unread;
-	rpl::producer<Content> _content;
-	Fn<void(uint64)> _open;
-	Fn<void()> _loadMore;
-	bool _positionPositive = false;
-
-	rpl::lifetime _lifetime;
-
-};
-
 [[nodiscard]] std::vector<Ui::RoundImageCheckboxSegment> PrepareSegments(
 		int count,
 		int unread,
@@ -150,221 +93,6 @@ private:
 	return QBrush(gradient);
 }
 
-StoriesRow::StoriesRow(
-	not_null<Main::Session*> session,
-	const QBrush &unread,
-	const Dialogs::Stories::Element &element,
-	int position)
-: PeerListRow(session->data().peer(PeerId(element.id)))
-, position(position)
-, _unread(unread) {
-}
-
-void StoriesRow::applySegments(const Dialogs::Stories::Element &element) {
-	Expects(element.unreadCount <= element.count);
-
-	_count = int(std::max(element.count, 1U));
-	_unreadCount = element.unreadCount;
-	refreshSegments();
-	setCustomStatus(_unreadCount
-		? tr::lng_contacts_stories_status_new(
-			tr::now,
-			lt_count,
-			_unreadCount)
-		: tr::lng_contacts_stories_status(tr::now, lt_count, _count));
-}
-
-void StoriesRow::updateGradient(QBrush unread) {
-	_unread = std::move(unread);
-	refreshSegments();
-}
-
-void StoriesRow::refreshSegments() {
-	setCustomizedCheckSegments(
-		PrepareSegments(_count, _unreadCount, _unread));
-}
-
-StoriesController::StoriesController(
-	not_null<Window::SessionController*> window,
-	rpl::producer<Content> content,
-	Fn<void(uint64)> open,
-	Fn<void()> loadMore)
-: _window(window)
-, _content(std::move(content))
-, _open(std::move(open))
-, _loadMore(std::move(loadMore)) {
-	_unread = CreateStoriesGradient();
-	style::PaletteChanged(
-	) | rpl::start_with_next([=] {
-		_unread = CreateStoriesGradient();
-		for (auto i = 0, count = int(delegate()->peerListFullRowsCount())
-			; i != count
-			; ++i) {
-			const auto row = delegate()->peerListRowAt(i).get();
-			static_cast<Row*>(row)->updateGradient(_unread);
-		}
-	}, _lifetime);
-}
-
-Main::Session &StoriesController::session() const {
-	return _window->session();
-}
-
-void StoriesController::prepare() {
-	if (_loadMore) {
-		_loadMore();
-	}
-	std::move(
-		_content
-	) | rpl::start_with_next([=](Content content) {
-		refresh(content);
-	}, _lifetime);
-}
-
-void StoriesController::loadMoreRows() {
-	if (_loadMore) {
-		_loadMore();
-	}
-}
-
-void StoriesController::rowClicked(not_null<PeerListRow*> row) {
-	if (_open) {
-		_open(row->id());
-	}
-}
-
-base::unique_qptr<Ui::PopupMenu> StoriesController::rowContextMenu(
-		QWidget *parent,
-		not_null<PeerListRow*> row) {
-	auto result = base::make_unique_q<Ui::PopupMenu>(
-		parent,
-		st::popupMenuWithIcons);
-
-	Dialogs::Stories::FillSourceMenu(_window, {
-		.id = row->id(),
-		.callback = Ui::Menu::CreateAddActionCallback(result.get()),
-	});
-
-	if (result->empty()) {
-		return nullptr;
-	}
-	return result;
-}
-
-void StoriesController::refresh(const Content &content) {
-	const auto session = &_window->session();
-	const auto positive = _positionPositive = !_positionPositive;
-	auto position = positive ? 1 : -int(content.elements.size());
-	for (const auto &element : content.elements) {
-		if (const auto row = delegate()->peerListFindRow(element.id)) {
-			static_cast<Row*>(row)->position = position;
-			static_cast<Row*>(row)->applySegments(element);
-		} else {
-			auto added = std::make_unique<Row>(
-				session,
-				_unread,
-				element,
-				position);
-			const auto raw = added.get();
-			delegate()->peerListAppendRow(std::move(added));
-			delegate()->peerListSetRowChecked(raw, true);
-			raw->applySegments(element);
-			raw->finishCheckedAnimation();
-		}
-		++position;
-	}
-	auto count = delegate()->peerListFullRowsCount();
-	for (auto i = 0; i != count;) {
-		const auto row = delegate()->peerListRowAt(i);
-		const auto position = static_cast<Row*>(row.get())->position;
-		if (positive ? (position > 0) : (position < 0)) {
-			++i;
-		} else {
-			delegate()->peerListRemoveRow(row);
-			--count;
-		}
-	}
-	delegate()->peerListSortRows([](
-			const PeerListRow &a,
-			const PeerListRow &b) {
-		return static_cast<const Row&>(a).position
-			< static_cast<const Row&>(b).position;
-	});
-	delegate()->peerListRefreshRows();
-}
-
-[[nodiscard]] object_ptr<Ui::RpWidget> PrepareHiddenStoriesList(
-		not_null<PeerListBox*> box,
-		not_null<Window::SessionController*> sessionController,
-		rpl::producer<ContactsBoxController::SortMode> mode) {
-	auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
-		box,
-		object_ptr<Ui::VerticalLayout>(box));
-	const auto container = result->entity();
-	const auto stories = &sessionController->session().data().stories();
-
-	container->add(CreatePeerListSectionSubtitle(
-		container,
-		tr::lng_contacts_hidden_stories()));
-
-	auto &lifetime = container->lifetime();
-	auto list = Dialogs::Stories::ContentForSession(
-		&sessionController->session(),
-		Data::StorySourcesList::Hidden
-	) | rpl::start_spawning(lifetime);
-	const auto delegate = lifetime.make_state<
-		PeerListContentDelegateSimple
-	>();
-	const auto open = [=](uint64 id) {
-		sessionController->openPeerStories(
-			PeerId(int64(id)),
-			Data::StorySourcesList::Hidden);
-	};
-	const auto loadMore = [=] {
-		stories->loadMore(Data::StorySourcesList::Hidden);
-	};
-	const auto controller = lifetime.make_state<StoriesController>(
-		sessionController,
-		rpl::duplicate(
-			list
-		) | rpl::filter([](const Dialogs::Stories::Content &list) {
-			return !list.elements.empty();
-		}),
-		open,
-		loadMore);
-	controller->setStyleOverrides(&st::contactsWithStories);
-	const auto content = container->add(object_ptr<PeerListContent>(
-		container,
-		controller));
-	delegate->setContent(content);
-	controller->setDelegate(delegate);
-
-	container->add(CreatePeerListSectionSubtitle(
-		container,
-		rpl::conditional(
-			std::move(
-				mode
-			) | rpl::map(
-				rpl::mappers::_1 == ContactsBoxController::SortMode::Online
-			),
-			tr::lng_contacts_by_online(),
-			tr::lng_contacts_by_name())));
-
-	stories->incrementPreloadingHiddenSources();
-	lifetime.add([=] {
-		stories->decrementPreloadingHiddenSources();
-	});
-
-	result->toggleOn(rpl::duplicate(
-		list
-	) | rpl::map([](const Dialogs::Stories::Content &list) {
-		return !list.elements.empty();
-	}));
-	result->finishAnimating();
-
-	return result;
-}
-
 } // namespace
 
 object_ptr<Ui::BoxContent> PrepareContactsBox(
@@ -384,7 +112,6 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 			::Ui::Animations::Simple scrollAnimation;
 		};
 
-		const auto stories = &sessionController->session().data().stories();
 		const auto state = box->lifetime().make_state<State>();
 		box->addButton(tr::lng_close(), [=] { box->closeBox(); });
 		box->addLeftButton(
@@ -400,11 +127,6 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 				online ? &st::contactsSortOnlineIconOver : nullptr);
 		});
 		raw->setSortMode(Mode::Online);
-
-		box->peerListSetAboveWidget(PrepareHiddenStoriesList(
-			box,
-			sessionController,
-			state->mode.value()));
 	};
 	return Box<PeerListBox>(std::move(controller), std::move(init));
 }

From dfd1aa5cd6cde38c0da6b78ec6685132b3232e2e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 19:41:33 +0400
Subject: [PATCH 155/259] Fix empty stories list hiding.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index fb92cbf1e..a994d4571 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -852,6 +852,10 @@ void Widget::setupStories() {
 				0);
 		}
 	}, lifetime());
+
+	_stories->emptyValue() | rpl::skip(1) | rpl::start_with_next([=] {
+		updateStoriesVisibility();
+	}, lifetime());
 }
 
 void Widget::storiesToggleExplicitExpand(bool expand) {
@@ -1363,7 +1367,8 @@ void Widget::updateStoriesVisibility() {
 		|| !_widthAnimationCache.isNull()
 		|| _childList
 		|| !_filter->getLastText().isEmpty()
-		|| _searchInChat;
+		|| _searchInChat
+		|| _stories->empty();
 	if (_stories->isHidden() != hidden) {
 		_stories->setVisible(!hidden);
 		using Type = Ui::ElasticScroll::OverscrollType;

From 05cf8d034ec9ffc705307ea4fa069c985242f59e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 21:42:08 +0400
Subject: [PATCH 156/259] Improve touch-screen stories overscroll.

---
 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp | 3 +--
 Telegram/lib_ui                                          | 2 +-
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 0fd70644d..2141540a1 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -689,8 +689,7 @@ QRect List::countSmallGeometry() const {
 	const auto &st = _st.small;
 	const auto layout = const_cast<List*>(this)->computeLayout();
 	const auto count = layout.endIndexSmall
-		- layout.startIndexSmall
-		- layout.smallSkip;
+		- std::max(layout.startIndexSmall, layout.smallSkip);
 	const auto width = st.left
 		+ st.photoLeft
 		+ st.photo + (count - 1) * st.shift
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 427fc4c8f..6af98c080 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 427fc4c8f7c8b92cca900e0270718f83fcb16e35
+Subproject commit 6af98c08025027cc3e7833e068db47670aa34dab

From 35e34541b0050ff9784a690447e8615849a44db7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 22:05:42 +0400
Subject: [PATCH 157/259] Closed alpha version 4.8.4.2.

---
 Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +-
 Telegram/Resources/winrc/Telegram.rc         | 8 ++++----
 Telegram/Resources/winrc/Updater.rc          | 8 ++++----
 Telegram/SourceFiles/core/version.h          | 2 +-
 Telegram/build/version                       | 4 ++--
 5 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index e9ff08323..6f1bf2f9b 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.4.1" />
+    Version="4.8.4.2" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index c90e04576..03e4f6943 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,1
- PRODUCTVERSION 4,8,4,1
+ FILEVERSION 4,8,4,2
+ PRODUCTVERSION 4,8,4,2
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.4.1"
+            VALUE "FileVersion", "4.8.4.2"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.1"
+            VALUE "ProductVersion", "4.8.4.2"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 1292b3215..dd8f3c39a 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,1
- PRODUCTVERSION 4,8,4,1
+ FILEVERSION 4,8,4,2
+ PRODUCTVERSION 4,8,4,2
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.4.1"
+            VALUE "FileVersion", "4.8.4.2"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.1"
+            VALUE "ProductVersion", "4.8.4.2"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 9e9f695db..b3dccc6e6 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/const_string.h"
 
-#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004001ULL)
+#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004002ULL)
 
 #ifdef TDESKTOP_ALLOW_CLOSED_ALPHA
 #define TDESKTOP_ALPHA_VERSION TDESKTOP_REQUESTED_ALPHA_VERSION
diff --git a/Telegram/build/version b/Telegram/build/version
index ac52ec62e..3cbfb66f9 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -3,5 +3,5 @@ AppVersionStrMajor 4.8
 AppVersionStrSmall 4.8.4
 AppVersionStr      4.8.4
 BetaChannel        0
-AlphaVersion       4008004001
-AppVersionOriginal 4.8.4.1
+AlphaVersion       4008004002
+AppVersionOriginal 4.8.4.2

From 641a03c98887c5d2633f7ded8de7888cb7456abc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 10:21:22 +0400
Subject: [PATCH 158/259] Fix crash on mention story opening.

---
 .../history/view/media/history_view_story_mention.cpp        | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
index 8615aec4b..8aefb319b 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
@@ -53,7 +53,10 @@ StoryMention::StoryMention(
 }
 
 StoryMention::~StoryMention() {
-	changeSubscribedTo(0);
+	if (_subscribed) {
+		changeSubscribedTo(0);
+		_parent->checkHeavyPart();
+	}
 }
 
 int StoryMention::top() {

From 8726884b214548f61512b9c2c99db916277358bf Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 10:28:38 +0400
Subject: [PATCH 159/259] Fix media rotation animation.

---
 .../SourceFiles/media/view/media_view_overlay_opengl.cpp  | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
index 96da6d8ff..04a9d025f 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
@@ -526,7 +526,13 @@ void OverlayWidget::RendererGL::paintTransformedContent(
 		geometry.controlsOpacity,
 		1.f - float(geometry.fade)));
 	if (!fillTransparentBackground) {
-		program->setUniformValue("roundRect", Uniform(rect));
+		program->setUniformValue(
+			"roundRect",
+			geometry.roundRadius ? Uniform(rect) : QVector4D(
+				0,
+				0,
+				_uniformViewport.x(),
+				_uniformViewport.y()));
 		program->setUniformValue(
 			"roundRadius",
 			GLfloat(geometry.roundRadius * _factor));

From b0403553fa0df05c1f109022b30a111d90039f87 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 12:21:20 +0400
Subject: [PATCH 160/259] Remove testing more chats bar.

---
 Telegram/SourceFiles/data/data_chat_filters.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp
index 90ef0bebe..26c6b6287 100644
--- a/Telegram/SourceFiles/data/data_chat_filters.cpp
+++ b/Telegram/SourceFiles/data/data_chat_filters.cpp
@@ -905,7 +905,6 @@ rpl::producer<> ChatFilters::suggestedUpdated() const {
 
 rpl::producer<Ui::MoreChatsBarContent> ChatFilters::moreChatsContent(
 		FilterId id) {
-	return rpl::single(Ui::MoreChatsBarContent{ .count = 10 }); // #TODO stories testing
 	if (!id) {
 		return rpl::single(Ui::MoreChatsBarContent{ .count = 0 });
 	}

From f7cbac1f469c77a960ed180dac655dd799f45e1e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 12:21:30 +0400
Subject: [PATCH 161/259] Try different function in overscroll.

---
 Telegram/lib_ui | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 6af98c080..d04a38e15 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 6af98c08025027cc3e7833e068db47670aa34dab
+Subproject commit d04a38e15d957ab0dacfe825396c199541097443

From 6607848abc8921dd03d3eed7e0faaf5c80279fcd Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 11 Jul 2023 22:22:40 +0400
Subject: [PATCH 162/259] Fix build with GCC.

---
 Telegram/SourceFiles/data/data_message_reactions.cpp   |  1 -
 Telegram/SourceFiles/data/data_poll.cpp                |  3 ---
 Telegram/SourceFiles/data/data_stories.cpp             |  8 ++------
 Telegram/SourceFiles/data/data_story.cpp               |  1 -
 .../SourceFiles/dialogs/ui/dialogs_stories_list.cpp    |  1 -
 Telegram/SourceFiles/export/data/export_data_types.cpp |  1 -
 .../SourceFiles/info/media/info_media_list_widget.cpp  |  1 -
 .../media/stories/media_stories_controller.cpp         |  1 -
 .../media/stories/media_stories_reactions.cpp          |  1 -
 .../SourceFiles/media/stories/media_stories_share.cpp  | 10 ----------
 .../media/stories/media_stories_sibling.cpp            |  1 -
 .../media/view/media_view_overlay_widget.cpp           |  1 -
 Telegram/SourceFiles/overview/overview_layout.cpp      |  1 -
 13 files changed, 2 insertions(+), 29 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp
index c0e7d9b87..e4279cbb3 100644
--- a/Telegram/SourceFiles/data/data_message_reactions.cpp
+++ b/Telegram/SourceFiles/data/data_message_reactions.cpp
@@ -1194,7 +1194,6 @@ bool MessageReactions::change(
 			}
 		}
 	}
-	const auto selfId = owner.session().userPeerId();
 	auto parsed = base::flat_map<ReactionId, std::vector<RecentReaction>>();
 	for (const auto &reaction : recent) {
 		reaction.match([&](const MTPDmessagePeerReaction &data) {
diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp
index efe928960..f781f95bc 100644
--- a/Telegram/SourceFiles/data/data_poll.cpp
+++ b/Telegram/SourceFiles/data/data_poll.cpp
@@ -132,9 +132,6 @@ bool PollData::applyResults(const MTPPollResults &results) {
 			}
 		}
 		if (const auto recent = results.vrecent_voters()) {
-			const auto bareProj = [](not_null<UserData*> user) {
-				return peerToUser(user->id).bare;
-			};
 			const auto recentChanged = !ranges::equal(
 				recentVoters,
 				recent->v,
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index fa3ff6b58..add13494f 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -273,7 +273,6 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 	const auto &data = stories.data();
 	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 = user,
@@ -355,7 +354,6 @@ Story *Stories::parseAndApply(
 	if (i != end(stories)) {
 		const auto result = i->second.get();
 		const auto mediaChanged = (result->media() != *media);
-		const auto pinned = result->pinned();
 		result->applyChanges(*media, data, now);
 		const auto j = _pollingSettings.find(result);
 		if (j != end(_pollingSettings)) {
@@ -577,7 +575,7 @@ void Stories::sendResolveRequests() {
 			finish(peerId);
 			continue;
 		}
-		const auto requestId = api->request(MTPstories_GetStoriesByID(
+		api->request(MTPstories_GetStoriesByID(
 			user->inputUser,
 			MTP_vector<MTPint>(prepared)
 		)).done([=](const MTPstories_Stories &result) {
@@ -1083,7 +1081,6 @@ void Stories::sendIncrementViewsRequests() {
 		return;
 	}
 	auto ids = QVector<MTPint>();
-	auto peer = PeerId();
 	struct Prepared {
 		PeerId peer = 0;
 		QVector<MTPint> ids;
@@ -1141,7 +1138,7 @@ void Stories::loadViewsSlice(
 		MTP_int(id),
 		MTP_int(offset ? offset->date : 0),
 		MTP_long(offset ? peerToUser(offset->peer->id).bare : 0),
-		MTP_int(kViewsPerPage)
+		MTP_int(perPage)
 	)).done([=](const MTPstories_StoryViewsList &result) {
 		_viewsRequestId = 0;
 
@@ -1595,7 +1592,6 @@ void Stories::sendPollingViewsRequests() {
 		return;
 	} else if (!_viewsRequestId) {
 		Assert(_viewsDone == nullptr);
-		const auto one = _pollingViews.front();
 		loadViewsSlice(_pollingViews.front()->id(), std::nullopt, nullptr);
 	}
 	_pollingViewsTimer.callOnce(kPollViewsInterval);
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index 59f672a11..210e4a8f4 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -104,7 +104,6 @@ bool StoryPreload::LoadTask::feedPart(
 	Expects(bytes.size() <= Storage::kDownloadPartSize);
 
 	const auto part = Storage::kDownloadPartSize;
-	const auto index = offset / part;
 	_requestedOffsets.remove(int(offset));
 	_parts[offset] = bytes;
 	if ((_nextRequestOffset + part >= _parts.size() * part)
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 2141540a1..14e37d4d9 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -761,7 +761,6 @@ void List::updateSelected() {
 		return;
 	}
 	const auto &st = _st.small;
-	const auto &full = _st.full;
 	const auto p = mapFromGlobal(_lastMousePosition);
 	const auto layout = computeLayout();
 	const auto firstRightFull = layout.leftFull
diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp
index e2bc9fde2..6b9fc2997 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.cpp
+++ b/Telegram/SourceFiles/export/data/export_data_types.cpp
@@ -623,7 +623,6 @@ StoriesSlice ParseStoriesSlice(
 		++result.skipped;
 		story.match([&](const MTPDstoryItem &data) {
 			const auto date = data.vdate().v;
-			const auto expires = data.vexpire_date().v;
 			auto media = Media();
 			data.vmedia().match([&](const MTPDmessageMediaPhoto &data) {
 				const auto suggestedPath = "stories/"
diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
index 8a06efa5c..459f1408c 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -1219,7 +1219,6 @@ void ListWidget::toggleStoryPin(
 			confirmed();
 		}
 	};
-	const auto session = &_controller->session();
 	const auto onePhrase = pin
 		? tr::lng_stories_save_sure
 		: tr::lng_stories_archive_sure;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 788806bdd..973473de1 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -454,7 +454,6 @@ void Controller::initLayout() {
 		const auto nameBoundingRect = [&](QRect geometry, bool left) {
 			const auto skipSmall = nameFontSize;
 			const auto skipBig = skipSmall - std::min(xLeft, 0);
-			const auto top = userpic(geometry).y() + innerHeight;
 			return QRect(
 				left ? skipBig : skipSmall,
 				(geometry.height() - innerHeight) / 2,
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
index fb94bc026..9d4265758 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -131,7 +131,6 @@ void Reactions::create() {
 		}
 	}, _parent->lifetime());
 
-	const auto withSearch = reactions.customAllowed;
 	_selector = std::make_unique<HistoryView::Reactions::Selector>(
 		_parent.get(),
 		st::storiesReactionsPan,
diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp
index 0a9b87df5..413fbf3d1 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp
@@ -105,12 +105,8 @@ namespace Media::Stories {
 			return;
 		}
 
-		const auto generateRandom = [&] {
-			return base::RandomValue<MTPlong>();
-		};
 		const auto api = &story->owner().session().api();
 		auto &histories = story->owner().histories();
-		const auto requestType = Data::Histories::RequestType::Send;
 		for (const auto thread : result) {
 			const auto action = Api::SendAction(thread, options);
 			if (!comment.text.isEmpty()) {
@@ -119,11 +115,6 @@ namespace Media::Stories {
 				message.action.clearDraft = false;
 				api->sendMessage(std::move(message));
 			}
-			const auto topicRootId = thread->topicRootId();
-			const auto kGeneralId = Data::ForumTopic::kGeneralId;
-			const auto topMsgId = (topicRootId == kGeneralId)
-				? MsgId(0)
-				: topicRootId;
 			const auto peer = thread->peer();
 			const auto threadHistory = thread->owningHistory();
 			const auto randomId = base::RandomValue<uint64>();
@@ -131,7 +122,6 @@ namespace Media::Stories {
 			if (action.replyTo) {
 				sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
 			}
-			const auto anonymousPost = peer->amAnonymous();
 			const auto silentPost = ShouldSendSilent(peer, action.options);
 			if (silentPost) {
 				sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
index c95674a0b..b8eda7d4a 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
@@ -252,7 +252,6 @@ void Sibling::checkStory() {
 		return;
 	}
 	const auto story = *maybeStory;
-	const auto &data = story->media().data;
 	const auto origin = Data::FileOrigin();
 	v::match(story->media().data, [&](not_null<PhotoData*> photo) {
 		_loader = std::make_unique<LoaderPhoto>(photo, origin, [=] {
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 9690f150e..379c341c0 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -673,7 +673,6 @@ void OverlayWidget::showSaveMsgToastWith(
 		+ st::mediaviewSaveMsgPadding.top()
 		+ st::mediaviewSaveMsgPadding.bottom();
 	_saveMsg = QRect((width() - w) / 2, (height() - h) / 2, w, h);
-	const auto toIn = 1.;
 	const auto callback = [=](float64 value) {
 		updateSaveMsg();
 		if (!_saveMsgAnimation.animating()) {
diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp
index 603963621..2f8609ace 100644
--- a/Telegram/SourceFiles/overview/overview_layout.cpp
+++ b/Telegram/SourceFiles/overview/overview_layout.cpp
@@ -517,7 +517,6 @@ void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const
 	if ((blurred || thumbnail || good)
 		&& ((_pix.width() != _width * cIntRetinaFactor())
 			|| (_pixBlurred && (thumbnail || good)))) {
-		auto size = _width * cIntRetinaFactor();
 		auto img = good
 			? good->original()
 			: thumbnail

From bbe431201724ea284106cf9a4a7827526e87dd3a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 21:59:18 +0400
Subject: [PATCH 163/259] Use GSL from a desktop-app fork.

---
 .gitmodules | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitmodules b/.gitmodules
index 2de5f5b1f..d3d445255 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -3,7 +3,7 @@
 	url = https://github.com/telegramdesktop/libtgvoip
 [submodule "Telegram/ThirdParty/GSL"]
 	path = Telegram/ThirdParty/GSL
-	url = https://github.com/Microsoft/GSL.git
+	url = https://github.com/desktop-app/GSL.git
 [submodule "Telegram/ThirdParty/xxHash"]
 	path = Telegram/ThirdParty/xxHash
 	url = https://github.com/Cyan4973/xxHash.git

From 66532aaac549e7ae6c6d022c32fe767017516d94 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 22:03:10 +0400
Subject: [PATCH 164/259] Workaround for std::variant bug in libstdc++.

---
 Telegram/ThirdParty/GSL | 2 +-
 cmake                   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/ThirdParty/GSL b/Telegram/ThirdParty/GSL
index a35345671..09938e870 160000
--- a/Telegram/ThirdParty/GSL
+++ b/Telegram/ThirdParty/GSL
@@ -1 +1 @@
-Subproject commit a3534567187d2edc428efd3f13466ff75fe5805c
+Subproject commit 09938e870420b69a01f55c755207c871bc20b4e5
diff --git a/cmake b/cmake
index f20e647e2..982546b16 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit f20e647e297b6dd006e77f288465e6a1cdab10e9
+Subproject commit 982546b169df3d479e6511425870327559b38a89

From 196447ac19017ed32f1b0a3c540fe4bc446d8387 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 22:03:26 +0400
Subject: [PATCH 165/259] Fix build with GCC.

---
 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 14e37d4d9..3b3917807 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -19,7 +19,6 @@ namespace Dialogs::Stories {
 namespace {
 
 constexpr auto kSmallThumbsShown = 3;
-constexpr auto kSummaryExpandLeft = 1;
 constexpr auto kPreloadPages = 2;
 constexpr auto kExpandAfterRatio = 0.72;
 constexpr auto kCollapseAfterRatio = 0.68;
@@ -276,9 +275,6 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto photoTop = photoTopSmall
 		+ (full.photoTop - photoTopSmall) * layout.expandedRatio;
 	const auto photo = lerp(st.photo, full.photo);
-	const auto summaryTop = st.nameTop
-		- (st.photoTop + (st.photo / 2.))
-		+ (photoTop + (photo / 2.));
 	const auto nameScale = _lastRatio;
 	const auto nameTop = full.nameTop
 		+ (photoTop + photo - full.photoTop - full.photo);
@@ -325,7 +321,6 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto lookup = [&](int index) {
 		const auto indexSmall = layout.startIndexSmall + index;
 		const auto indexFull = layout.startIndexFull + index;
-		const auto k = (photoTop - photoTopSmall);
 		const auto ySmall = photoTopSmall
 			+ ((photoTop - photoTopSmall)
 				* (kSmallThumbsShown - indexSmall + layout.smallSkip) / 0.5);

From 51027a0bc25e10e22244dedd0349ed939188f190 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 22:05:58 +0400
Subject: [PATCH 166/259] Workaround Wayland popup menu bug.

When hiding a child popup first the app receives ApplicationDeactivate
event and in a short time (a couple of ms) ApplicationActivate.

But the first event hides all popups, so the parent popup gets closed too.

Delay handling of ApplicationDeactivate event in this specific case.
---
 Telegram/SourceFiles/core/sandbox.cpp | 12 +++++++++++-
 Telegram/lib_ui                       |  2 +-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp
index 6c332bbc1..d477101f4 100644
--- a/Telegram/SourceFiles/core/sandbox.cpp
+++ b/Telegram/SourceFiles/core/sandbox.cpp
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/qthelp_regex.h"
 #include "ui/ui_utility.h"
 #include "ui/effects/animations.h"
+#include "ui/platform/ui_platform_utility.h"
 
 #include <QtCore/QLockFile>
 #include <QtGui/QSessionManager>
@@ -580,9 +581,18 @@ void Sandbox::registerEnterFromEventLoop() {
 }
 
 bool Sandbox::notifyOrInvoke(QObject *receiver, QEvent *e) {
-	if (e->type() == base::InvokeQueuedEvent::kType) {
+	const auto type = e->type();
+	if (type == base::InvokeQueuedEvent::kType) {
 		static_cast<base::InvokeQueuedEvent*>(e)->invoke();
 		return true;
+	} else if (receiver == this) {
+		if (type == QEvent::ApplicationDeactivate) {
+			if (Ui::Platform::SkipApplicationDeactivateEvent()) {
+				return true;
+			}
+		} else if (type == QEvent::ApplicationActivate) {
+			Ui::Platform::GotApplicationActivateEvent();
+		}
 	}
 	return QApplication::notify(receiver, e);
 }
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index d04a38e15..8db6dcf12 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit d04a38e15d957ab0dacfe825396c199541097443
+Subproject commit 8db6dcf125da5c767d36a696794d4a51d82c7955

From 2664d984d9786db268a1d440ad4059a4e24443c3 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 23:01:27 +0400
Subject: [PATCH 167/259] Ignore wrong wheel direction in ElasticScroll.

---
 Telegram/lib_ui | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 8db6dcf12..b6cb6cb77 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 8db6dcf125da5c767d36a696794d4a51d82c7955
+Subproject commit b6cb6cb770236a49bffdf572427faa477f00ccc0

From ebd8380019e7befd922bd77fe36c5935179939d8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 12 Jul 2023 23:17:08 +0400
Subject: [PATCH 168/259] Use correct reply placeholder in stories view.

---
 Telegram/Resources/langs/lang.strings                    | 1 +
 .../view/controls/history_view_compose_controls.cpp      | 9 +++++++--
 .../view/controls/history_view_compose_controls.h        | 2 ++
 .../SourceFiles/media/stories/media_stories_reply.cpp    | 1 +
 4 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 81a75e5a6..68a00520e 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2044,6 +2044,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_broadcast_ph" = "Broadcast a message...";
 "lng_broadcast_silent_ph" = "Silent broadcast...";
 "lng_send_anonymous_ph" = "Send anonymously...";
+"lng_story_reply_ph" = "Reply privately...";
 "lng_send_text_no" = "Text not allowed.";
 "lng_send_text_no_about" = "The admins of this group only allow sending {types}.";
 "lng_send_text_type_and_last" = "{types} and {last}";
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 089767c2c..e1b32aa7d 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -971,12 +971,15 @@ ComposeControls::ComposeControls(
 , _tabbedSelectorToggle(Ui::CreateChild<Ui::EmojiButton>(
 	_wrap.get(),
 	_st.emoji))
+, _fieldCustomPlaceholder(std::move(descriptor.customPlaceholder))
 , _field(
 	Ui::CreateChild<Ui::InputField>(
 		_wrap.get(),
 		_st.field,
 		Ui::InputField::Mode::MultiLine,
-		tr::lng_message_ph()))
+		(_fieldCustomPlaceholder
+			? rpl::duplicate(_fieldCustomPlaceholder)
+			: tr::lng_message_ph())))
 , _botCommandStart(_features.botCommandSend
 	? Ui::CreateChild<Ui::IconButton>(
 		_wrap.get(),
@@ -1816,7 +1819,9 @@ void ComposeControls::updateFieldPlaceholder() {
 	}
 
 	_field->setPlaceholder([&] {
-		if (isEditingMessage()) {
+		if (_fieldCustomPlaceholder) {
+			return rpl::duplicate(_fieldCustomPlaceholder);
+		} else if (isEditingMessage()) {
 			return tr::lng_edit_message_text();
 		} else if (!_history) {
 			return tr::lng_message_ph();
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 fa63a8b3d..518782954 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<ChatHelpers::FileChosen> stickerOrEmojiChosen;
+	rpl::producer<QString> customPlaceholder;
 	bool voiceLockFromBottom = false;
 	ChatHelpers::ComposeFeatures features;
 };
@@ -353,6 +354,7 @@ private:
 	const not_null<Ui::IconButton*> _attachToggle;
 	std::unique_ptr<Ui::IconButton> _replaceMedia;
 	const not_null<Ui::EmojiButton*> _tabbedSelectorToggle;
+	rpl::producer<QString> _fieldCustomPlaceholder;
 	const not_null<Ui::InputField*> _field;
 	Ui::IconButton * const _botCommandStart = nullptr;
 	std::unique_ptr<Ui::SendAsButton> _sendAs;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index d032533b3..9fa53b529 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -85,6 +85,7 @@ ReplyArea::ReplyArea(not_null<Controller*> controller)
 		.mode = HistoryView::ComposeControlsMode::Normal,
 		.sendMenuType = SendMenu::Type::SilentOnly,
 		.stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(),
+		.customPlaceholder = tr::lng_story_reply_ph(),
 		.voiceLockFromBottom = true,
 		.features = {
 			.sendAs = false,

From 8a974273b9f6b5377059423882a24325258a7ecd Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 09:12:05 +0400
Subject: [PATCH 169/259] Fix possible crash in click handling.

---
 Telegram/SourceFiles/data/data_sponsored_messages.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp
index bbf6273c5..fa21af469 100644
--- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp
+++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp
@@ -385,7 +385,7 @@ const SponsoredMessages::Entry *SponsoredMessages::find(
 	}
 	auto &list = it->second;
 	const auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) {
-		return e.item->fullId() == fullId;
+		return e.item && e.item->fullId() == fullId;
 	});
 	if (entryIt == end(list.entries)) {
 		return nullptr;

From 91cc5f5284de4ccde0d63d42f7a95f58fc97a2cc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 11:15:37 +0400
Subject: [PATCH 170/259] Fix small stories thumbnails in profile / my stories.

---
 .../SourceFiles/boxes/peer_list_controllers.cpp |  2 --
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp |  1 +
 .../dialogs/ui/dialogs_stories_list.cpp         | 17 +++++++++++++----
 .../dialogs/ui/dialogs_stories_list.h           |  8 ++++++--
 Telegram/SourceFiles/info/info_top_bar.cpp      |  5 ++---
 .../info/stories/info_stories_inner_widget.cpp  | 16 +++++++---------
 6 files changed, 29 insertions(+), 20 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 2019f0c50..18d2d514f 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -104,8 +104,6 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 	controller->setStoriesShown(true);
 	const auto raw = controller.get();
 	auto init = [=](not_null<PeerListBox*> box) {
-		using namespace Dialogs::Stories;
-
 		struct State {
 			QPointer<::Ui::IconButton> toggleSort;
 			rpl::variable<Mode> mode = Mode::Online;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index a994d4571..957589a90 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -2697,6 +2697,7 @@ void Widget::updateControlsGeometry() {
 	if (_stories) {
 		_stories->setLayoutConstraints(
 			{ filterLeft + filterWidth, filterTop + added },
+			style::al_right,
 			{ 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height });
 	}
 	if (_forumTopShadow) {
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 3b3917807..e7c0a6bfb 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -661,8 +661,12 @@ bool List::checkForFullState() {
 	return true;
 }
 
-void List::setLayoutConstraints(QPoint topRightSmall, QRect geometryFull) {
-	_topRightSmall = topRightSmall;
+void List::setLayoutConstraints(
+		QPoint positionSmall,
+		style::align alignSmall,
+		QRect geometryFull) {
+	_positionSmall = positionSmall;
+	_alignSmall = alignSmall;
 	_geometryFull = geometryFull;
 	updateGeometry();
 	update();
@@ -690,9 +694,14 @@ QRect List::countSmallGeometry() const {
 		+ st.photo + (count - 1) * st.shift
 		+ st.photoLeft
 		+ st.left;
+	const auto left = ((_alignSmall & Qt::AlignRight) == Qt::AlignRight)
+		? (_positionSmall.x() - width)
+		: ((_alignSmall & Qt::AlignCenter) == Qt::AlignCenter)
+		? (_positionSmall.x() - (width / 2))
+		: _positionSmall.x();
 	return QRect(
-		_topRightSmall.x() - width,
-		_topRightSmall.y(),
+		left,
+		_positionSmall.y(),
 		width,
 		st.photoTop + st.photo + st.photoTop);
 }
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 7e0028822..85d7fd5e8 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -66,7 +66,10 @@ public:
 		rpl::producer<Content> content);
 
 	void setExpandedHeight(int height, bool momentum = false);
-	void setLayoutConstraints(QPoint topRightSmall, QRect geometryFull);
+	void setLayoutConstraints(
+		QPoint positionSmall,
+		style::align alignSmall,
+		QRect geometryFull = QRect());
 
 	[[nodiscard]] bool empty() const {
 		return _empty.current();
@@ -137,7 +140,8 @@ private:
 	rpl::event_stream<> _entered;
 	rpl::event_stream<> _loadMoreRequests;
 
-	QPoint _topRightSmall;
+	QPoint _positionSmall;
+	style::align _alignSmall = {};
 	QRect _geometryFull;
 	QRect _changingGeometryFrom;
 	State _state = State::Small;
diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp
index 81f4dd685..7dcb56ab3 100644
--- a/Telegram/SourceFiles/info/info_top_bar.cpp
+++ b/Telegram/SourceFiles/info/info_top_bar.cpp
@@ -389,11 +389,10 @@ void TopBar::updateStoriesGeometry(int newWidth) {
 	}
 	const auto left = (_back ? _st.back.width : _st.titlePosition.x())
 		- st::dialogsStories.left - st::dialogsStories.photoLeft;
-	const auto top = st::dialogsStories.height
-		- st::dialogsStoriesFull.height
-		+ (_st.height - st::dialogsStories.height) / 2;
+	const auto top = (_st.height - st::dialogsStories.height) / 2;
 	_stories->resizeToWidth(newWidth - left - right);
 	_stories->moveToLeft(left, top, newWidth);
+	_stories->entity()->setLayoutConstraints({ 0, 0 }, style::al_left);
 }
 
 void TopBar::paintEvent(QPaintEvent *e) {
diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index 8abc39d8d..49bb41022 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -179,19 +179,17 @@ void InnerWidget::createButtons() {
 		if (content.elements.empty()) {
 			return;
 		}
-		const auto width = st::defaultDialogRow.padding.left()
-			+ st::defaultDialogRow.photoSize
-			+ st::defaultDialogRow.padding.left();
 		const auto &small = st::dialogsStories;
 		const auto count = int(content.elements.size());
 		const auto smallWidth = small.photo + (count - 1) * small.shift;
 		const auto real = smallWidth;
-		const auto top = st::dialogsStories.height
-			- st::dialogsStoriesFull.height
-			+ (size.height() - st::dialogsStories.height) / 2;
-		const auto right = st::settingsButtonRightSkip - (width - real) / 2;
-		thumbs->resizeToWidth(width);
-		thumbs->moveToRight(right, top);
+		const auto height = small.photo + 2 * small.photoTop;
+		const auto top = (size.height() - height) / 2;
+		const auto right = st::settingsButtonRightSkip
+			- small.left
+			- small.photoLeft;
+		const auto left = size.width() - right;
+		thumbs->setLayoutConstraints({ left, top }, style::al_right);
 	}, thumbs->lifetime());
 	thumbs->setAttribute(Qt::WA_TransparentForMouseEvents);
 	recent->addClickHandler([=] {

From 21a5e26250957ea4c3ededfd801062eeb6a4fd08 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 12:39:18 +0400
Subject: [PATCH 171/259] Add current stories label to profile top bar.

---
 Telegram/SourceFiles/info/info_top_bar.cpp | 93 ++++++++++++++++------
 Telegram/SourceFiles/info/info_top_bar.h   |  4 +-
 2 files changed, 71 insertions(+), 26 deletions(-)

diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp
index 7dcb56ab3..f15a6b549 100644
--- a/Telegram/SourceFiles/info/info_top_bar.cpp
+++ b/Telegram/SourceFiles/info/info_top_bar.cpp
@@ -124,6 +124,9 @@ void TopBar::enableBackButton() {
 	if (_title) {
 		_title->setAttribute(Qt::WA_TransparentForMouseEvents);
 	}
+	if (_storiesWrap) {
+		_storiesWrap->raise();
+	}
 	updateControlsGeometry(width());
 }
 
@@ -387,12 +390,16 @@ void TopBar::updateStoriesGeometry(int newWidth) {
 		button->moveToRight(right, 0, newWidth);
 		right += button->width();
 	}
-	const auto left = (_back ? _st.back.width : _st.titlePosition.x())
-		- st::dialogsStories.left - st::dialogsStories.photoLeft;
-	const auto top = (_st.height - st::dialogsStories.height) / 2;
-	_stories->resizeToWidth(newWidth - left - right);
-	_stories->moveToLeft(left, top, newWidth);
-	_stories->entity()->setLayoutConstraints({ 0, 0 }, style::al_left);
+	const auto &small = st::dialogsStories;
+	const auto wrapLeft = (_back ? _st.back.width : 0);
+	const auto left = _back
+		? 0
+		: (_st.titlePosition.x() - small.left - small.photoLeft);
+	const auto height = small.photo + 2 * small.photoTop;
+	const auto top = _st.titlePosition.y()
+		+ (_st.title.style.font->height - height) / 2;
+	_stories->setLayoutConstraints({ left, top }, style::al_left);
+	_storiesWrap->move(wrapLeft, 0);
 }
 
 void TopBar::paintEvent(QPaintEvent *e) {
@@ -447,36 +454,61 @@ void TopBar::updateControlsVisibility(anim::type animated) {
 
 void TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) {
 	_storiesLifetime.destroy();
+	delete _storiesWrap.data();
 	if (content) {
 		using namespace Dialogs::Stories;
 
 		auto last = std::move(
 			content
 		) | rpl::start_spawning(_storiesLifetime);
-		delete _stories;
 
-		const auto stories = Ui::CreateChild<Ui::FadeWrap<List>>(
-			this,
-			object_ptr<List>(
-				this,
-				st::dialogsStoriesListInfo,
-				rpl::duplicate(
-					last
-				) | rpl::filter([](const Content &content) {
-					return !content.elements.empty();
-				})),
-				st::infoTopBarScale);
+		_storiesWrap = _storiesLifetime.make_state<
+			Ui::FadeWrap<Ui::AbstractButton>
+		>(this, object_ptr<Ui::AbstractButton>(this), st::infoTopBarScale);
 		registerToggleControlCallback(
-			stories,
+			_storiesWrap.data(),
 			[this] { return _storiesCount > 0; });
-		stories->toggle(false, anim::type::instant);
-		stories->setDuration(st::infoTopBarDuration);
+		_storiesWrap->toggle(false, anim::type::instant);
+		_storiesWrap->setDuration(st::infoTopBarDuration);
+
+		const auto button = _storiesWrap->entity();
+		const auto stories = Ui::CreateChild<List>(
+			button,
+			st::dialogsStoriesListInfo,
+			rpl::duplicate(
+				last
+			) | rpl::filter([](const Content &content) {
+				return !content.elements.empty();
+			}));
+		const auto label = Ui::CreateChild<Ui::FlatLabel>(
+			button,
+			QString(),
+			_st.title);
+		stories->setAttribute(Qt::WA_TransparentForMouseEvents);
+		label->setAttribute(Qt::WA_TransparentForMouseEvents);
+		stories->geometryValue(
+		) | rpl::start_with_next([=](QRect geometry) {
+			const auto skip = _st.title.style.font->spacew;
+			label->move(
+				geometry.x() + geometry.width() + skip,
+				_st.titlePosition.y());
+		}, label->lifetime());
+		rpl::combine(
+			_storiesWrap->positionValue(),
+			label->geometryValue()
+		) | rpl::start_with_next([=] {
+			button->resize(
+				label->x() + label->width() + _st.titlePosition.x(),
+				_st.height);
+		}, button->lifetime());
+
 		_stories = stories;
-		_stories->entity()->clicks(
+		_stories->clicks(
 		) | rpl::start_to_stream(_storyClicks, _stories->lifetime());
-		if (_back) {
-			_back->raise();
-		}
+
+		button->setClickedCallback([=] {
+			_storyClicks.fire({});
+		});
 
 		rpl::duplicate(
 			last
@@ -489,9 +521,20 @@ void TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) {
 				if (was != now) {
 					updateControlsVisibility(anim::type::normal);
 				}
+				if (now) {
+					label->setText(
+						tr::lng_contacts_stories_status(
+							tr::now,
+							lt_count,
+							_storiesCount));
+				}
 				updateControlsGeometry(width());
 			}
 		}, _storiesLifetime);
+
+		_storiesLifetime.add([weak = QPointer<QWidget>(label)] {
+			delete weak.data();
+		});
 	} else {
 		_storiesCount = 0;
 	}
diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h
index c41125904..496166954 100644
--- a/Telegram/SourceFiles/info/info_top_bar.h
+++ b/Telegram/SourceFiles/info/info_top_bar.h
@@ -28,6 +28,7 @@ class SessionNavigation;
 } // namespace Window
 
 namespace Ui {
+class AbstractButton;
 class IconButton;
 class FlatLabel;
 class InputField;
@@ -175,7 +176,8 @@ private:
 	QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryPin;
 	rpl::event_stream<SelectionAction> _selectionActionRequests;
 
-	QPointer<Ui::FadeWrap<Dialogs::Stories::List>> _stories;
+	QPointer<Ui::FadeWrap<Ui::AbstractButton>> _storiesWrap;
+	QPointer<Dialogs::Stories::List> _stories;
 	rpl::lifetime _storiesLifetime;
 	int _storiesCount = 0;
 

From 7d98acb4a38948dfb040b892dbb629ef4d04cdc0 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 13:37:13 +0400
Subject: [PATCH 172/259] Closed alpha version 4.8.4.3.

---
 Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +-
 Telegram/Resources/winrc/Telegram.rc         | 8 ++++----
 Telegram/Resources/winrc/Updater.rc          | 8 ++++----
 Telegram/SourceFiles/core/version.h          | 2 +-
 Telegram/build/version                       | 4 ++--
 5 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 6f1bf2f9b..d47902e9b 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.4.2" />
+    Version="4.8.4.3" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 03e4f6943..f3aa8d4f8 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,2
- PRODUCTVERSION 4,8,4,2
+ FILEVERSION 4,8,4,3
+ PRODUCTVERSION 4,8,4,3
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.4.2"
+            VALUE "FileVersion", "4.8.4.3"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.2"
+            VALUE "ProductVersion", "4.8.4.3"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index dd8f3c39a..4820d2c10 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,2
- PRODUCTVERSION 4,8,4,2
+ FILEVERSION 4,8,4,3
+ PRODUCTVERSION 4,8,4,3
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.4.2"
+            VALUE "FileVersion", "4.8.4.3"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.2"
+            VALUE "ProductVersion", "4.8.4.3"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index b3dccc6e6..caff68b46 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/const_string.h"
 
-#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004002ULL)
+#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004003ULL)
 
 #ifdef TDESKTOP_ALLOW_CLOSED_ALPHA
 #define TDESKTOP_ALPHA_VERSION TDESKTOP_REQUESTED_ALPHA_VERSION
diff --git a/Telegram/build/version b/Telegram/build/version
index 3cbfb66f9..ed59e5ebe 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -3,5 +3,5 @@ AppVersionStrMajor 4.8
 AppVersionStrSmall 4.8.4
 AppVersionStr      4.8.4
 BetaChannel        0
-AlphaVersion       4008004002
-AppVersionOriginal 4.8.4.2
+AlphaVersion       4008004003
+AppVersionOriginal 4.8.4.3

From 39a1743e7e392362fff7938bb4beb0be4714bc4d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 16:54:18 +0400
Subject: [PATCH 173/259] Fix overscroll in topics list.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 957589a90..bcf81a3b4 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -226,9 +226,10 @@ Widget::Widget(
 	const auto makeChildListShown = [](PeerId peerId, float64 shown) {
 		return InnerWidget::ChildListShown{ peerId, shown };
 	};
+	using OverscrollType = Ui::ElasticScroll::OverscrollType;
 	_scroll->setOverscrollTypes(
-		Ui::ElasticScroll::OverscrollType::Virtual,
-		Ui::ElasticScroll::OverscrollType::Real);
+		_stories ? OverscrollType::Virtual : OverscrollType::Real,
+		OverscrollType::Real);
 	_inner = _scroll->setOwnedWidget(object_ptr<InnerWidget>(
 		this,
 		controller,

From 5d234d31035285b9e34a35c9d78103523827c87d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 19:33:57 +0400
Subject: [PATCH 174/259] Update API scheme on layer 160.

---
 .../SourceFiles/api/api_global_privacy.cpp    | 33 +++++++++++++++----
 Telegram/SourceFiles/api/api_global_privacy.h | 12 +++++++
 Telegram/SourceFiles/mtproto/scheme/api.tl    |  4 +--
 3 files changed, 41 insertions(+), 8 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp
index c940dbe86..89e1d57da 100644
--- a/Telegram/SourceFiles/api/api_global_privacy.cpp
+++ b/Telegram/SourceFiles/api/api_global_privacy.cpp
@@ -56,6 +56,15 @@ rpl::producer<bool> GlobalPrivacy::archiveAndMute() const {
 	return _archiveAndMute.value();
 }
 
+UnarchiveOnNewMessage GlobalPrivacy::unarchiveOnNewMessageCurrent() const {
+	return _unarchiveOnNewMessage.current();
+}
+
+auto GlobalPrivacy::unarchiveOnNewMessage() const
+-> rpl::producer<UnarchiveOnNewMessage> {
+	return _unarchiveOnNewMessage.value();
+}
+
 rpl::producer<bool> GlobalPrivacy::showArchiveAndMute() const {
 	using namespace rpl::mappers;
 
@@ -78,11 +87,20 @@ void GlobalPrivacy::dismissArchiveAndMuteSuggestion() {
 void GlobalPrivacy::update(bool archiveAndMute) {
 	using Flag = MTPDglobalPrivacySettings::Flag;
 
+	const auto unarchive = unarchiveOnNewMessageCurrent();
 	_api.request(_requestId).cancel();
+	const auto flags = Flag()
+		| (archiveAndMute
+			? Flag::f_archive_and_mute_new_noncontact_peers
+			: Flag())
+		| (unarchive == UnarchiveOnNewMessage::AnyUnmuted
+			? Flag::f_keep_archived_unmuted
+			: Flag())
+		| (unarchive != UnarchiveOnNewMessage::None
+			? Flag::f_keep_archived_folders
+			: Flag());
 	_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(
-		MTP_globalPrivacySettings(
-			MTP_flags(Flag::f_archive_and_mute_new_noncontact_peers),
-			MTP_bool(archiveAndMute))
+		MTP_globalPrivacySettings(MTP_flags(flags))
 	)).done([=](const MTPGlobalPrivacySettings &result) {
 		_requestId = 0;
 		apply(result);
@@ -94,9 +112,12 @@ void GlobalPrivacy::update(bool archiveAndMute) {
 
 void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) {
 	data.match([&](const MTPDglobalPrivacySettings &data) {
-		_archiveAndMute = data.varchive_and_mute_new_noncontact_peers()
-			? mtpIsTrue(*data.varchive_and_mute_new_noncontact_peers())
-			: false;
+		_archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
+		_unarchiveOnNewMessage = data.is_keep_archived_unmuted()
+			? UnarchiveOnNewMessage::AnyUnmuted
+			: data.is_keep_archived_folders()
+			? UnarchiveOnNewMessage::NotInFoldersUnmuted
+			: UnarchiveOnNewMessage::None;
 	});
 }
 
diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h
index a1a693499..f569951ca 100644
--- a/Telegram/SourceFiles/api/api_global_privacy.h
+++ b/Telegram/SourceFiles/api/api_global_privacy.h
@@ -17,6 +17,12 @@ class Session;
 
 namespace Api {
 
+enum class UnarchiveOnNewMessage {
+	None,
+	NotInFoldersUnmuted,
+	AnyUnmuted,
+};
+
 class GlobalPrivacy final {
 public:
 	explicit GlobalPrivacy(not_null<ApiWrap*> api);
@@ -26,6 +32,10 @@ public:
 
 	[[nodiscard]] bool archiveAndMuteCurrent() const;
 	[[nodiscard]] rpl::producer<bool> archiveAndMute() const;
+	[[nodiscard]] auto unarchiveOnNewMessageCurrent() const
+		-> UnarchiveOnNewMessage;
+	[[nodiscard]] auto unarchiveOnNewMessage() const
+		-> rpl::producer<UnarchiveOnNewMessage>;
 	[[nodiscard]] rpl::producer<bool> showArchiveAndMute() const;
 	[[nodiscard]] rpl::producer<> suggestArchiveAndMute() const;
 	void dismissArchiveAndMuteSuggestion();
@@ -37,6 +47,8 @@ private:
 	MTP::Sender _api;
 	mtpRequestId _requestId = 0;
 	rpl::variable<bool> _archiveAndMute = false;
+	rpl::variable<UnarchiveOnNewMessage> _unarchiveOnNewMessage
+		= UnarchiveOnNewMessage::None;
 	rpl::variable<bool> _showArchiveAndMute = false;
 	std::vector<Fn<void()>> _callbacks;
 
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index b8771b61c..18c9bcd5b 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -1238,7 +1238,7 @@ statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInvite
 
 stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector<StatsGroupTopPoster> top_admins:Vector<StatsGroupTopAdmin> top_inviters:Vector<StatsGroupTopInviter> users:Vector<User> = stats.MegagroupStats;
 
-globalPrivacySettings#bea2f424 flags:# archive_and_mute_new_noncontact_peers:flags.0?Bool = GlobalPrivacySettings;
+globalPrivacySettings#734c4ccb flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true = GlobalPrivacySettings;
 
 help.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector<string> patterns:flags.1?Vector<string> = help.CountryCode;
 
@@ -1529,7 +1529,7 @@ sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Phot
 storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector<long> = StoryViews;
 
 storyItemDeleted#51e6ee4f id:int = StoryItem;
-storyItemSkipped#693206a2 id:int date:int expire_date:int = StoryItem;
+storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem;
 storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
 
 userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = UserStories;

From 5368507259d3c2ee31a174b356964f3b1a54f2e7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 20:49:08 +0400
Subject: [PATCH 175/259] Improve lock/unlock icon behavior.

---
 Telegram/SourceFiles/dialogs/dialogs.style    | 17 ++---
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 49 +++++++++++--
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  1 +
 .../dialogs/ui/dialogs_stories_list.cpp       | 69 +++++++++++++------
 .../dialogs/ui/dialogs_stories_list.h         |  8 +++
 5 files changed, 111 insertions(+), 33 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index 6a20b6cf7..d8b405e7c 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -221,19 +221,20 @@ dialogsMenuToggleUnreadMuted: icon {
 dialogsLock: IconButton(dialogsMenuToggle) {
 	icon: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFg }};
 	iconOver: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFgOver }};
+	ripple: emptyRippleAnimation;
 }
 dialogsUnlockIcon: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFg }};
 dialogsUnlockIconOver: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFgOver }};
 dialogsCalendar: IconButton {
-	width: 29px;
-	height: 32px;
+	width: 32px;
+	height: 35px;
 
 	icon: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFg }};
 	iconOver: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFgOver }};
-	iconPosition: point(0px, 5px);
+	iconPosition: point(1px, 6px);
 }
 dialogsSearchFrom: IconButton(dialogsCalendar) {
-	width: 26px;
+	width: 29px;
 	icon: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFg }};
 	iconOver: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFgOver }};
 }
@@ -276,12 +277,12 @@ dialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) {
 	rippleAreaSize: 34px;
 }
 dialogsCancelSearch: CrossButton {
-	width: 32px;
-	height: 32px;
+	width: 35px;
+	height: 35px;
 
 	cross: CrossAnimation {
-		size: 32px;
-		skip: 10px;
+		size: 35px;
+		skip: 12px;
 		stroke: 1.5;
 		minScale: 0.3;
 	}
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index bcf81a3b4..984d5b647 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -814,14 +814,20 @@ void Widget::setupStories() {
 		using Phase = Ui::ElasticScrollMovement;
 		_stories->setExpandedHeight(
 			_aboveScrollAdded,
-			(movement == Phase::Momentum || movement == Phase::Returning)
-				&& (explicitlyExpanded < above));
+			((movement == Phase::Momentum || movement == Phase::Returning)
+				&& (explicitlyExpanded < above)));
 		if (position.overscroll > 0
 			|| (position.value
 				> (_storiesExplicitExpandScrollTop
 					+ st::dialogsRowHeight))) {
 			storiesToggleExplicitExpand(false);
 		}
+		updateLockUnlockPosition();
+	}, lifetime());
+
+	_stories->collapsedGeometryChanged(
+	) | rpl::start_with_next([=] {
+		updateLockUnlockPosition();
 	}, lifetime());
 
 	_stories->clicks(
@@ -857,6 +863,10 @@ void Widget::setupStories() {
 	_stories->emptyValue() | rpl::skip(1) | rpl::start_with_next([=] {
 		updateStoriesVisibility();
 	}, lifetime());
+
+	_stories->widthValue() | rpl::start_with_next([=] {
+		updateLockUnlockPosition();
+	}, lifetime());
 }
 
 void Widget::storiesToggleExplicitExpand(bool expand) {
@@ -977,6 +987,23 @@ void Widget::updateControlsVisibility(bool fast) {
 	if (_childList && _filter->hasFocus()) {
 		setInnerFocus();
 	}
+	updateLockUnlockPosition();
+}
+
+void Widget::updateLockUnlockPosition() {
+	if (_lockUnlock->isHidden()) {
+		return;
+	}
+	const auto stories = (_stories && !_stories->isHidden())
+		? _stories->collapsedGeometryCurrent()
+		: Stories::List::CollapsedGeometry();
+	const auto simple = _filter->x() + _filter->width();
+	const auto right = stories.geometry.isEmpty()
+		? simple
+		: anim::interpolate(stories.geometry.x(), simple, stories.expanded);
+	_lockUnlock->move(
+		right - _lockUnlock->width(),
+		st::dialogsFilterPadding.y());
 }
 
 void Widget::changeOpenedSubsection(
@@ -1388,6 +1415,8 @@ void Widget::updateStoriesVisibility() {
 		if (_aboveScrollAdded > 0 && _updateScrollGeometryCached) {
 			_updateScrollGeometryCached();
 		}
+		updateLockUnlockVisibility();
+		updateLockUnlockPosition();
 	}
 }
 
@@ -2253,6 +2282,7 @@ void Widget::applyFilterUpdate(bool force) {
 	_cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal);
 	updateLoadMoreChatsVisibility();
 	updateJumpToDateVisibility();
+	updateLockUnlockPosition();
 
 	if (filterText.isEmpty()) {
 		_peerSearchCache.clear();
@@ -2458,6 +2488,7 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
 		updateSearchFromVisibility();
 		clearSearchCache();
 	}
+	updateLockUnlockPosition();
 	if (_searchInChat && _layout == Layout::Main) {
 		controller()->closeFolder();
 	}
@@ -2580,9 +2611,18 @@ void Widget::updateLockUnlockVisibility() {
 	if (_showAnimation) {
 		return;
 	}
-	const auto hidden = !session().domain().local().hasLocalPasscode();
+	const auto hidden = !session().domain().local().hasLocalPasscode()
+		|| (_showAnimation != nullptr)
+		|| _openedForum
+		|| !_widthAnimationCache.isNull()
+		|| _childList
+		|| !_filter->getLastText().isEmpty()
+		|| _searchInChat;
 	if (_lockUnlock->isHidden() != hidden) {
 		_lockUnlock->setVisible(!hidden);
+		if (!hidden) {
+			updateLockUnlockPosition();
+		}
 		updateControlsGeometry();
 	}
 }
@@ -2685,7 +2725,6 @@ void Widget::updateControlsGeometry() {
 	_searchForNarrowFilters->moveToLeft(searchLeft, st::dialogsFilterPadding.y());
 
 	auto right = filterLeft + filterWidth;
-	_lockUnlock->moveToLeft(right + st::dialogsFilterPadding.x(), st::dialogsFilterPadding.y());
 	_cancelSearch->moveToLeft(right - _cancelSearch->width(), _filter->y());
 	right -= _jumpToDate->width(); _jumpToDate->moveToLeft(right, _filter->y());
 	right -= _chooseFromUser->width(); _chooseFromUser->moveToLeft(right, _filter->y());
@@ -2709,6 +2748,8 @@ void Widget::updateControlsGeometry() {
 			st::lineWidth);
 	}
 
+	updateLockUnlockPosition();
+
 	auto bottomSkip = 0;
 	const auto putBottomButton = [&](auto &button) {
 		if (button && !button->isHidden()) {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 245998392..48760b630 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -224,6 +224,7 @@ private:
 	void updateScrollUpVisibility();
 	void startScrollUpButtonAnimation(bool shown);
 	void updateScrollUpPosition();
+	void updateLockUnlockPosition();
 
 	MTP::Sender _api;
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index e7c0a6bfb..685cc767d 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include <QtWidgets/QApplication>
 
+#include "base/debug_log.h"
+
 namespace Dialogs::Stories {
 namespace {
 
@@ -36,6 +38,7 @@ constexpr auto kExpandCatchUpDuration = crl::time(200);
 
 struct List::Layout {
 	int itemsCount = 0;
+	QPointF geometryShift;
 	float64 expandedRatio = 0.;
 	float64 ratio = 0.;
 	float64 thumbnailLeft = 0.;
@@ -147,12 +150,13 @@ rpl::producer<> List::loadMoreRequests() const {
 void List::requestExpanded(bool expanded) {
 	if (_expanded != expanded) {
 		_expanded = expanded;
-		_expandedAnimation.start(
-			[=] { checkForFullState(); update(); },
-			_expanded ? 0. : 1.,
-			_expanded ? 1. : 0.,
-			st::slideWrapDuration,
-			anim::sineInOut);
+		const auto from = _expanded ? 0. : 1.;
+		const auto till = _expanded ? 1. : 0.;
+		_expandedAnimation.start([=] {
+			checkForFullState();
+			update();
+			_collapsedGeometryChanged.fire({});
+		}, from, till, st::slideWrapDuration, anim::sineInOut);
 	}
 	_toggleExpandedRequests.fire_copy(_expanded);
 }
@@ -185,13 +189,15 @@ void List::updateExpanding(int expandingHeight, int expandedHeight) {
 }
 
 List::Layout List::computeLayout() {
+	updateExpanding(
+		_lastExpandedHeight * _expandCatchUpAnimation.value(1.),
+		_st.full.height);
+	return computeLayout(_expandedAnimation.value(_expanded ? 1. : 0.));
+}
+
+List::Layout List::computeLayout(float64 expanded) const {
 	const auto &st = _st.small;
 	const auto &full = _st.full;
-	const auto use = _lastExpandedHeight
-		* _expandCatchUpAnimation.value(1.);
-	updateExpanding(use, full.height);
-
-	const auto expanded = _expandedAnimation.value(_expanded ? 1. : 0.);
 	const auto expandedRatio = _lastRatio;
 	const auto collapsedRatio = expandedRatio * kFrictionRatio;
 	const auto ratio = expandedRatio * expanded
@@ -234,6 +240,13 @@ List::Layout List::computeLayout() {
 	const auto photoLeft = lerp(st.photoLeft, full.photoLeft);
 	return Layout{
 		.itemsCount = itemsCount,
+		.geometryShift = QPointF(
+			(_state == State::Changing
+				? (lerp(_changingGeometryFrom.x(), _geometryFull.x()) - x())
+				: 0.),
+			(_state == State::Changing
+				? (lerp(_changingGeometryFrom.y(), _geometryFull.y()) - y())
+				: 0.)),
 		.expandedRatio = expandedRatio,
 		.ratio = ratio,
 		.thumbnailLeft = thumbnailLeft,
@@ -287,15 +300,7 @@ void List::paintEvent(QPaintEvent *e) {
 	auto p = QPainter(this);
 
 	if (_state == State::Changing) {
-		const auto left = anim::interpolate(
-			_changingGeometryFrom.x(),
-			_geometryFull.x(),
-			layout.ratio);
-		const auto top = anim::interpolate(
-			_changingGeometryFrom.y(),
-			_geometryFull.y(),
-			layout.ratio);
-		p.translate(QPoint(left, top) - pos());
+		p.translate(layout.geometryShift);
 	}
 
 	const auto drawSmall = (expandRatio < 1.);
@@ -672,6 +677,28 @@ void List::setLayoutConstraints(
 	update();
 }
 
+List::CollapsedGeometry List::collapsedGeometryCurrent() const {
+	const auto expanded = _expandedAnimation.value(_expanded ? 1. : 0.);
+	if (expanded == 1.) {
+		return { QRect(), 1. };
+	}
+	const auto layout = computeLayout(0.);
+	const auto small = countSmallGeometry();
+	const auto index = layout.smallSkip - layout.startIndexSmall;
+	const auto shift = x() + layout.geometryShift.x();
+	const auto left = int(base::SafeRound(
+		shift + layout.left + layout.single * index));
+	const auto width = small.x() + small.width() - left;
+	return {
+		QRect(left, small.y(), width, small.height()),
+		expanded,
+	};
+}
+
+rpl::producer<> List::collapsedGeometryChanged() const {
+	return _collapsedGeometryChanged.events();
+}
+
 void List::updateGeometry() {
 	switch (_state) {
 	case State::Small: setGeometry(countSmallGeometry()); break;
@@ -686,7 +713,7 @@ void List::updateGeometry() {
 
 QRect List::countSmallGeometry() const {
 	const auto &st = _st.small;
-	const auto layout = const_cast<List*>(this)->computeLayout();
+	const auto layout = computeLayout(0.);
 	const auto count = layout.endIndexSmall
 		- std::max(layout.startIndexSmall, layout.smallSkip);
 	const auto width = st.left
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index 85d7fd5e8..d0ecc1428 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -70,6 +70,12 @@ public:
 		QPoint positionSmall,
 		style::align alignSmall,
 		QRect geometryFull = QRect());
+	struct CollapsedGeometry {
+		QRect geometry;
+		float64 expanded = 0.;
+	};
+	[[nodiscard]] CollapsedGeometry collapsedGeometryCurrent() const;
+	[[nodiscard]] rpl::producer<> collapsedGeometryChanged() const;
 
 	[[nodiscard]] bool empty() const {
 		return _empty.current();
@@ -130,6 +136,7 @@ private:
 	void updateExpanding(int expandingHeight, int expandedHeight);
 
 	[[nodiscard]] Layout computeLayout();
+	[[nodiscard]] Layout computeLayout(float64 expanded) const;
 
 	const style::DialogsStoriesList &_st;
 	Content _content;
@@ -139,6 +146,7 @@ private:
 	rpl::event_stream<bool> _toggleExpandedRequests;
 	rpl::event_stream<> _entered;
 	rpl::event_stream<> _loadMoreRequests;
+	rpl::event_stream<> _collapsedGeometryChanged;
 
 	QPoint _positionSmall;
 	style::align _alignSmall = {};

From ac136638a4ebb2a204305039e1f85f01eda10317 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 20:51:14 +0400
Subject: [PATCH 176/259] Closed alpha version 4.8.4.4.

---
 Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +-
 Telegram/Resources/winrc/Telegram.rc         | 8 ++++----
 Telegram/Resources/winrc/Updater.rc          | 8 ++++----
 Telegram/SourceFiles/core/version.h          | 2 +-
 Telegram/build/version                       | 4 ++--
 5 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index d47902e9b..1553b95ad 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.4.3" />
+    Version="4.8.4.4" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index f3aa8d4f8..1c0a03179 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,3
- PRODUCTVERSION 4,8,4,3
+ FILEVERSION 4,8,4,4
+ PRODUCTVERSION 4,8,4,4
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.4.3"
+            VALUE "FileVersion", "4.8.4.4"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.3"
+            VALUE "ProductVersion", "4.8.4.4"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 4820d2c10..87a38fb0b 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,3
- PRODUCTVERSION 4,8,4,3
+ FILEVERSION 4,8,4,4
+ PRODUCTVERSION 4,8,4,4
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.4.3"
+            VALUE "FileVersion", "4.8.4.4"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.3"
+            VALUE "ProductVersion", "4.8.4.4"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index caff68b46..79690f889 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/const_string.h"
 
-#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004003ULL)
+#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004004ULL)
 
 #ifdef TDESKTOP_ALLOW_CLOSED_ALPHA
 #define TDESKTOP_ALPHA_VERSION TDESKTOP_REQUESTED_ALPHA_VERSION
diff --git a/Telegram/build/version b/Telegram/build/version
index ed59e5ebe..8b47580b7 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -3,5 +3,5 @@ AppVersionStrMajor 4.8
 AppVersionStrSmall 4.8.4
 AppVersionStr      4.8.4
 BetaChannel        0
-AlphaVersion       4008004003
-AppVersionOriginal 4.8.4.3
+AlphaVersion       4008004004
+AppVersionOriginal 4.8.4.4

From 74014d18a52b586d6e78f7dd1eff2e0251ba2d3b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 13 Jul 2023 21:23:30 +0400
Subject: [PATCH 177/259] Fix build with GCC.

---
 .../SourceFiles/info/stories/info_stories_inner_widget.cpp     | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
index 49bb41022..7c572746d 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp
@@ -180,9 +180,6 @@ void InnerWidget::createButtons() {
 			return;
 		}
 		const auto &small = st::dialogsStories;
-		const auto count = int(content.elements.size());
-		const auto smallWidth = small.photo + (count - 1) * small.shift;
-		const auto real = smallWidth;
 		const auto height = small.photo + 2 * small.photoTop;
 		const auto top = (size.height() - height) / 2;
 		const auto right = st::settingsButtonRightSkip

From 3c28e7b5851fdd808994df0e7564626fb4568960 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Jul 2023 11:24:13 +0400
Subject: [PATCH 178/259] Mark as read recent stories in profile top bar.

---
 Telegram/SourceFiles/data/data_changes.h      | 45 ++++++++++---------
 Telegram/SourceFiles/data/data_stories.cpp    |  5 ++-
 .../dialogs/ui/dialogs_stories_content.cpp    | 34 +++++++++++++-
 3 files changed, 59 insertions(+), 25 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h
index 754f12ef3..3f0e587dd 100644
--- a/Telegram/SourceFiles/data/data_changes.h
+++ b/Telegram/SourceFiles/data/data_changes.h
@@ -148,19 +148,19 @@ struct TopicUpdate {
 	enum class Flag : uint32 {
 		None = 0,
 
-		UnreadView = (1U << 1),
-		UnreadMentions = (1U << 2),
+		UnreadView      = (1U << 1),
+		UnreadMentions  = (1U << 2),
 		UnreadReactions = (1U << 3),
-		Notifications = (1U << 4),
-		Title = (1U << 5),
-		IconId = (1U << 6),
-		ColorId = (1U << 7),
-		CloudDraft = (1U << 8),
-		Closed = (1U << 9),
-		Creator = (1U << 10),
-		Destroyed = (1U << 11),
+		Notifications   = (1U << 4),
+		Title           = (1U << 5),
+		IconId          = (1U << 6),
+		ColorId         = (1U << 7),
+		CloudDraft      = (1U << 8),
+		Closed          = (1U << 9),
+		Creator         = (1U << 10),
+		Destroyed       = (1U << 11),
 
-		LastUsedBit = (1U << 11),
+		LastUsedBit     = (1U << 11),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
@@ -199,14 +199,14 @@ struct EntryUpdate {
 	enum class Flag : uint32 {
 		None = 0,
 
-		Repaint = (1U << 0),
+		Repaint           = (1U << 0),
 		HasPinnedMessages = (1U << 1),
-		ForwardDraft = (1U << 2),
-		LocalDraftSet = (1U << 3),
-		Height = (1U << 4),
-		Destroyed = (1U << 5),
+		ForwardDraft      = (1U << 2),
+		LocalDraftSet     = (1U << 3),
+		Height            = (1U << 4),
+		Destroyed         = (1U << 5),
 
-		LastUsedBit = (1U << 5),
+		LastUsedBit       = (1U << 5),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
@@ -220,12 +220,13 @@ struct StoryUpdate {
 	enum class Flag : uint32 {
 		None = 0,
 
-		Edited = (1U << 0),
-		Destroyed = (1U << 1),
-		NewAdded = (1U << 2),
-		ViewsAdded = (1U << 3),
+		Edited      = (1U << 0),
+		Destroyed   = (1U << 1),
+		NewAdded    = (1U << 2),
+		ViewsAdded  = (1U << 3),
+		MarkRead    = (1U << 4),
 
-		LastUsedBit = (1U << 3),
+		LastUsedBit = (1U << 4),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index add13494f..19ff73eb2 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -928,7 +928,10 @@ bool Stories::bumpReadTill(PeerId peerId, StoryId maxReadTill) {
 			refreshItems = ranges::make_subrange(
 				i->second.lower_bound(from + 1),
 				i->second.lower_bound(till + 1)
-			) | ranges::views::transform([](const auto &pair) {
+			) | ranges::views::transform([=](const auto &pair) {
+				_owner->session().changes().storyUpdated(
+					pair.second.get(),
+					StoryUpdate::Flag::MarkRead);
 				return pair.first;
 			}) | ranges::to_vector;
 		}
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
index 2f6c55dd1..ed723b55e 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
@@ -402,17 +402,25 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 		}
 		return rpl::make_producer<Content>([=](auto consumer) {
 			auto lifetime = rpl::lifetime();
+			if (ids.empty()) {
+				consumer.put_next(Content());
+				consumer.put_done();
+				return lifetime;
+			}
 
 			struct State {
 				Fn<void()> check;
 				base::has_weak_ptr guard;
+				int readTill = StoryId();
 				bool pushed = false;
 			};
 			const auto state = lifetime.make_state<State>();
+			state->readTill = readTill;
 			state->check = [=] {
 				if (state->pushed) {
 					return;
 				}
+				auto done = true;
 				auto resolving = false;
 				auto result = Content{};
 				for (const auto id : ids) {
@@ -420,13 +428,17 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 					const auto maybe = stories->lookup(storyId);
 					if (maybe) {
 						if (!resolving) {
+							const auto unread = (id > state->readTill);
 							result.elements.reserve(ids.size());
 							result.elements.push_back({
 								.id = uint64(id),
 								.thumbnail = MakeStoryThumbnail(*maybe),
 								.count = 1U,
-								.unreadCount = (id > readTill) ? 1U : 0U,
+								.unreadCount = unread ? 1U : 0U,
 							});
+							if (unread) {
+								done = false;
+							}
 						}
 					} else if (maybe.error() == Data::NoStory::Unknown) {
 						resolving = true;
@@ -440,12 +452,30 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
 				}
 				state->pushed = true;
 				consumer.put_next(std::move(result));
-				consumer.put_done();
+				if (done) {
+					consumer.put_done();
+				}
 			};
+
 			rpl::single(peerId) | rpl::then(
 				stories->itemsChanged() | rpl::filter(_1 == peerId)
 			) | rpl::start_with_next(state->check, lifetime);
 
+			stories->session().changes().storyUpdates(
+				Data::StoryUpdate::Flag::MarkRead
+			) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
+				if (update.story->peer()->id == peerId) {
+					if (update.story->id() > state->readTill) {
+						state->readTill = update.story->id();
+						if (ranges::contains(ids, state->readTill)
+							|| state->readTill > ids.front()) {
+							state->pushed = false;
+							state->check();
+						}
+					}
+				}
+			}, lifetime);
+
 			return lifetime;
 		});
 	}) | rpl::flatten_latest();

From 10f65c63e7f6aa3ef08f174b29f3fbfe68e035c3 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Jul 2023 12:02:04 +0400
Subject: [PATCH 179/259] Allow opening unknown stories from chats list.

---
 Telegram/SourceFiles/data/data_stories.cpp    | 22 +++++++++++++++----
 Telegram/SourceFiles/data/data_stories.h      |  8 +++++--
 .../window/window_session_controller.cpp      | 13 +++++++++--
 3 files changed, 35 insertions(+), 8 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 19ff73eb2..20db4a13a 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -196,21 +196,34 @@ Story *Stories::applyFromWebpage(PeerId peerId, const MTPstoryItem &story) {
 	return value ? value->get() : nullptr;
 }
 
-void Stories::requestUserStories(not_null<UserData*> user) {
-	if (!_requestingUserStories.emplace(user).second) {
+void Stories::requestUserStories(
+		not_null<UserData*> user,
+		Fn<void()> done) {
+	const auto [i, ok] = _requestingUserStories.emplace(user);
+	if (done) {
+		i->second.push_back(std::move(done));
+	}
+	if (!ok) {
 		return;
 	}
+	const auto finish = [=] {
+		if (const auto callbacks = _requestingUserStories.take(user)) {
+			for (const auto &callback : *callbacks) {
+				callback();
+			}
+		}
+	};
 	_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());
+		finish();
 	}).fail([=] {
-		_requestingUserStories.remove(user);
 		applyDeletedFromSources(user->id, StorySourcesList::NotHidden);
 		applyDeletedFromSources(user->id, StorySourcesList::Hidden);
+		finish();
 	}).send();
 }
 
@@ -290,6 +303,7 @@ void Stories::parseAndApply(const MTPUserStories &stories) {
 	if (result.ids.empty()) {
 		applyDeletedFromSources(peerId, StorySourcesList::NotHidden);
 		applyDeletedFromSources(peerId, StorySourcesList::Hidden);
+		user->setStoriesState(UserData::StoriesState::None);
 		return;
 	} else if (user->isSelf()) {
 		result.readTill = result.ids.back().id;
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 2bdd27d1c..848227a5d 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -218,6 +218,9 @@ public:
 
 	bool registerPolling(FullStoryId id, Polling polling);
 	void unregisterPolling(FullStoryId id, Polling polling);
+	void requestUserStories(
+		not_null<UserData*> user,
+		Fn<void()> done = nullptr);
 
 	void savedStateChanged(not_null<Story*> story);
 	[[nodiscard]] std::shared_ptr<HistoryItem> lookupItem(
@@ -266,7 +269,6 @@ private:
 	void sendIncrementViewsRequests();
 	void checkQuitPreventFinished();
 
-	void requestUserStories(not_null<UserData*> user);
 	void registerExpiring(TimeId expires, FullStoryId id);
 	void scheduleExpireTimer();
 	void processExpired();
@@ -335,7 +337,9 @@ private:
 	base::flat_set<PeerId> _markReadPending;
 	base::Timer _markReadTimer;
 	base::flat_set<PeerId> _markReadRequests;
-	base::flat_set<not_null<UserData*>> _requestingUserStories;
+	base::flat_map<
+		not_null<UserData*>,
+		std::vector<Fn<void()>>> _requestingUserStories;
 
 	base::flat_map<PeerId, base::flat_set<StoryId>> _incrementViewsPending;
 	base::Timer _incrementViewsTimer;
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index e35e83d03..1a89da1b1 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -2528,9 +2528,10 @@ void SessionController::openPeerStory(
 	if (from) {
 		window().openInMediaView(OpenRequest(this, *from, context));
 	} else if (from.error() == Data::NoStory::Unknown) {
-		stories.resolve({ peer->id, storyId }, crl::guard(&_storyOpenGuard, [=] {
+		const auto done = crl::guard(&_storyOpenGuard, [=] {
 			openPeerStory(peer, storyId, context);
-		}));
+		});
+		stories.resolve({ peer->id, storyId }, done);
 	}
 }
 
@@ -2540,6 +2541,7 @@ void SessionController::openPeerStories(
 	using namespace Media::View;
 	using namespace Data;
 
+	invalidate_weak_ptrs(&_storyOpenGuard);
 	auto &stories = session().data().stories();
 	if (const auto source = stories.source(peerId)) {
 		if (const auto idDates = source->toOpen()) {
@@ -2550,6 +2552,13 @@ void SessionController::openPeerStories(
 					? StoriesContext{ *list }
 					: StoriesContext{ StoriesContextPeer() }));
 		}
+	} else if (const auto userId = peerToUser(peerId)) {
+		if (const auto user = session().data().userLoaded(userId)) {
+			const auto done = crl::guard(&_storyOpenGuard, [=] {
+				openPeerStories(peerId, list);
+			});
+			stories.requestUserStories(user, done);
+		}
 	}
 }
 

From 94820f36ba5acda95b1b7b2e8ba08b70856e3e85 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Jul 2023 12:22:50 +0400
Subject: [PATCH 180/259] Improve expanded state of small amount of stories.

---
 .../dialogs/ui/dialogs_stories_list.cpp       | 24 ++++++++++---------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 685cc767d..ad39539dd 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -206,12 +206,18 @@ List::Layout List::computeLayout(float64 expanded) const {
 	const auto lerp = [&](float64 a, float64 b) {
 		return a + (b - a) * ratio;
 	};
-	const auto singleFull = full.photoLeft * 2 + full.photo;
+	const auto widthFull = width();
 	const auto itemsCount = int(_data.items.size());
-	const auto narrowWidth = st::defaultDialogRow.padding.left()
-		+ st::defaultDialogRow.photoSize
-		+ st::defaultDialogRow.padding.left();
-	const auto narrow = false;// (width() <= narrowWidth);
+	const auto leftFullMin = full.left;
+	const auto singleFullMin = full.photoLeft * 2 + full.photo;
+	const auto totalFull = leftFullMin + singleFullMin * itemsCount;
+	const auto skipSide = (totalFull < widthFull)
+		? (widthFull - totalFull) / (itemsCount + 1)
+		: 0;
+	const auto skipBetween = (totalFull < widthFull && itemsCount > 1)
+		? (widthFull - totalFull - 2 * skipSide) / (itemsCount - 1)
+		: skipSide;
+	const auto singleFull = singleFullMin + skipBetween;
 	const auto smallSkip = (itemsCount > 1
 		&& _data.items[0].element.skipSmall)
 		? 1
@@ -220,12 +226,8 @@ List::Layout List::computeLayout(float64 expanded) const {
 		kSmallThumbsShown,
 		itemsCount - smallSkip);
 	const auto smallWidth = st.photo + (smallCount - 1) * st.shift;
-	const auto leftSmall = (narrow
-		? ((narrowWidth - smallWidth) / 2 - st.photoLeft)
-		: st.left) - (smallSkip ? st.shift : 0);
-	const auto leftFull = (narrow
-		? ((narrowWidth - full.photo) / 2 - full.photoLeft)
-		: full.left) - _scrollLeft;
+	const auto leftSmall = st.left - (smallSkip ? st.shift : 0);
+	const auto leftFull = full.left - _scrollLeft + skipSide;
 	const auto startIndexFull = std::max(-leftFull, 0) / singleFull;
 	const auto cellLeftFull = leftFull + (startIndexFull * singleFull);
 	const auto endIndexFull = std::min(

From 5f5933c1b3e2b99ed75f6f704b4adde5b68fe898 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Jul 2023 16:09:14 +0400
Subject: [PATCH 181/259] Improve overscroll, revert cmake_helpers.

---
 Telegram/lib_ui | 2 +-
 cmake           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index b6cb6cb77..fd1752a5c 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit b6cb6cb770236a49bffdf572427faa477f00ccc0
+Subproject commit fd1752a5c2e71810e75c3b90eee3ff6fd9e0d71b
diff --git a/cmake b/cmake
index 982546b16..6d67365e5 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 982546b169df3d479e6511425870327559b38a89
+Subproject commit 6d67365e52480edbde2b2596844c26b56a8644dc

From 1f69c61d51c2ecc40ed05f3dafac4c30283d8652 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Jul 2023 14:58:06 +0400
Subject: [PATCH 182/259] Fix init glitch in ComposeControls.

---
 .../SourceFiles/chat_helpers/emoji_suggestions_widget.cpp  | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
index 7233c5a21..a97d1fe42 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
@@ -778,6 +778,13 @@ SuggestionsController::SuggestionsController(
 
 	updateForceHidden();
 
+	_container->shownValue(
+	) | rpl::filter([=](bool shown) {
+		return shown && !_shown;
+	}) | rpl::start_with_next([=] {
+		_container->hide();
+	}, _container->lifetime());
+
 	handleTextChange();
 }
 

From d46f974ab56df685fba907268b938060ed4f82dd Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Jul 2023 16:18:48 +0400
Subject: [PATCH 183/259] Fix mark-as-read of deleted accounts by Enter.

---
 Telegram/SourceFiles/history/history_widget.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index aaa9733fe..25abe7428 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -1804,7 +1804,9 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(
 }
 
 void HistoryWidget::tryProcessKeyInput(not_null<QKeyEvent*> e) {
-	if (_canSendTexts && _field->isVisible()) {
+	e->accept();
+	keyPressEvent(e);
+	if (!e->isAccepted() && _canSendTexts && _field->isVisible()) {
 		_field->setFocusFast();
 		QCoreApplication::sendEvent(_field->rawTextEdit(), e);
 	}

From 3851fa27d91d606cf830713ca3f165a692f8f651 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 14 Jul 2023 17:25:28 +0400
Subject: [PATCH 184/259] Update lock/unlock icon.

---
 .../Resources/icons/dialogs/dialogs_lock.png     | Bin 362 -> 0 bytes
 .../Resources/icons/dialogs/dialogs_lock@2x.png  | Bin 556 -> 0 bytes
 .../Resources/icons/dialogs/dialogs_lock@3x.png  | Bin 817 -> 0 bytes
 .../Resources/icons/dialogs/dialogs_lock_off.png | Bin 0 -> 327 bytes
 .../icons/dialogs/dialogs_lock_off@2x.png        | Bin 0 -> 541 bytes
 .../icons/dialogs/dialogs_lock_off@3x.png        | Bin 0 -> 703 bytes
 .../Resources/icons/dialogs/dialogs_lock_on.png  | Bin 0 -> 307 bytes
 .../icons/dialogs/dialogs_lock_on@2x.png         | Bin 0 -> 502 bytes
 .../icons/dialogs/dialogs_lock_on@3x.png         | Bin 0 -> 662 bytes
 .../Resources/icons/dialogs/dialogs_unlock.png   | Bin 357 -> 0 bytes
 .../icons/dialogs/dialogs_unlock@2x.png          | Bin 523 -> 0 bytes
 .../icons/dialogs/dialogs_unlock@3x.png          | Bin 769 -> 0 bytes
 Telegram/SourceFiles/dialogs/dialogs.style       |  15 ++++++++++-----
 13 files changed, 10 insertions(+), 5 deletions(-)
 delete mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock.png
 delete mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock@2x.png
 delete mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock@3x.png
 create mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock_off.png
 create mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png
 create mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png
 create mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock_on.png
 create mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png
 create mode 100644 Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png
 delete mode 100644 Telegram/Resources/icons/dialogs/dialogs_unlock.png
 delete mode 100644 Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png
 delete mode 100644 Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock.png b/Telegram/Resources/icons/dialogs/dialogs_lock.png
deleted file mode 100644
index cd953f753d8edb26955b385b97f617d4e0e47ce5..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 362
zcmeAS@N?(olHy`uVBq!ia0vp^qChOd!2~4VYCcQ>QjEnx?oJHr&dIz4a$Hg)JkxxA
z8MJ_G4hF{dOa>N^5+IfWVg?501&j>LK$;OGwtxvPE4qLg&K3bFG_AJY2c)iex;Tbd
z2%ZhH7HU=yXcf5-S;g7!konYcPn-Bdo_PmPNoMc5QpBRob!PU2!fOdk#tS)EKYy!@
zNJ)LO{iyL{r;dH^rv`mE|9Q>UvU90vk+Ij0&8WTq{yb}gP)c5%%<<gYx3=*zGE7m}
z{3$fF@_l>ag1jjTigz`tJ7+I@I7k1qQz($T^mCAx%KbR)DHASL-ZDGp`Z@Oc+Sgql
zFPQoY-%4WV6rJ}&Oka)fax+s~`!oOl@-iX6TRwhj5Bc)DKlt;(_^;cqspbDWTWncx
gb?C^S$!AYX%Vq9aeqK27cM-^+p00i_>zopr0C|0b8UO$Q

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png
deleted file mode 100644
index d29e0579f8e32422298666ad38bad5a0768d39f9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 556
zcmeAS@N?(olHy`uVBq!ia0vp^T0pG9!2~4PGyk&#DaPU;cPEB*=VV?2IWDOYo@u_m
zU{xFpjP02WEFdL7ECs|249p7{8JK}IBS>rk6I@nn0W+Mf0aCc`*0*e+vT2?!jv*18
zZ^M22TpUF5CVQ^Q$#Hqax1({ca)<Yhrs6ZKF$b?PMij6qisd;}?K?9&<ie^bubXE!
zytq>R{`bnNDVO?>*YDfXEtVe{Ccx77*kOO}?L``Gi91eYtG@nSH}Cmp8+&naCl%ZC
z(T-EuJtz5G-tr-qH;~C;=gaj9OJ+*XS+(ldubl=F=Uz^dT=4wN;!LTQ)1gW(lhoL6
zEZgPc)mND9DQmWC-TqYyC({@{Xgk<7Kc6$t^y8_Bb_O=rd-;cZ7IjSPyS+y<*NK6H
zlgmqf_NEw*-_mzB=^W+aaS$>6)|NOS`~LT&YBq%~sp^eO7gXKmo>g4>GS6=P;Ty9e
zr<KOK>vJ8<pS4V-iLtqn!>U^K&90hvX%ku2uFJk`QrWP*^7gjJ7JP><l*ZN+?m4=i
zVTMKY@so^8!Y;WtDcjmx=z4sV*?GTe!m~n^PqVh=m*m@Yrmb3Z=WO^@PtLi!e+u~q
zyFN6VUn@MXT*#yF?Z@dq_84lO@OXGY_V~vK3zZriU22{bi*rhy^Ln|M-{pep-kp<o
dan~<pzCU+{fpp%fvj(7;@^tlcS?83{1OR(!+;0E?

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png
deleted file mode 100644
index 7964279201cb4cc118f20f917583fd5b0f808ce1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 817
zcmV-11J3-3P)<h;3K|Lk000e1NJLTq002J#002A)0ssI2Ua9pa00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91KcE8u1ONa40RR91JOBUy0N6#c*Z=?mn@L1LRA>e5SkKBiQ5gQnhA5?o
z;u=_6*x0xN3tJX$fos7<5F28nELm78rGza}#P9cfskhS%<IM3ozQwV6oq7JvGh@um
z&yV?-22>jGsE|Iu@p`?nSS*o9KzqGjZ@1gq?e+m5R>@#6ST2{_?e^((`X_ww6^qye
z0IMOBn9t{{R;!!MM#%hN1OPCAnQS>^B9X{&ID8QH$r8Yza5%rMkxHc&i^Y?iPnSUf
z(PFOqR~rZf#^Z51ofdQ8EbsMt{eFM7T46nzOy=|XLZRUI`$gDvI?ZOY$Kz34)H=6X
zEla)4W-}g-+e2Uxn=D|^?MO{4nM?-PK?;PqQmHt517M**#F_G`t({JXqGPDlYBEms
zdR=7UN-D!*#)QM+%jH5jX0sXop=3M&1DJ>qfs_o96H_XcC;^6Iu_&(&u*ikfoh!BX
z@-_}g@q9g>&)sg<0c_a>FetQ0Qu@;0?_ur<mdoXSzo#@W2Y^8#7)j|%AB{%Iez)7v
zEIEi2lG2x6yjQtiuacn&CQ?XBUwX>HIGFT|WxA9y-McJ|5pKF?EYqcw>E2~wjBpp-
zdpsUID@-O6+;xa|e3El6m!sU6Ei7n48TxP`7~6p7w83CtvHZkWxIBDN9F0a~huOuB
zp$|RL@A=?mKLYCMaX@Lm^$c)y(XKNTXQZCc<N`<MNcXK$KN0+-G`~+g91fvS=;aPb
zrAH+%h1dc1Q{Qgk;)-b}e|^QI|K1@m-J9;!8Zc*$T5lXI(>+IvY8s|{wca>brhASS
z)ik8K$KAG?x-v`BQ7Zj>K3C46MwE1vO5bQS)Iun;I354OI|bz;eu-aypYv9FkZ}aw
vd6!1-3my)ieV6v`t{c8-z%*bQ_-)`nKQr+B6ZXCC00000NkvXXu0mjf3Pop5

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off.png
new file mode 100644
index 0000000000000000000000000000000000000000..2459e11373947bac31650a2acdc80cd38c85aebf
GIT binary patch
literal 327
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlfUZS-_;46zV=
z8@Q2|S&^q^6I1Af8!eifm?v*y5*6)XSmf^);UxHeflc5N{d=rd^J~)$ZkHXcG%v{i
zc=_*^Yv*>Rb2BXIE)zH|A+O6YVV%T7#~oZg9y|>qDc^UMny`Cmnk<>JLbruMVd<My
z`MtXu8ou9?XfA$m{7Tg9;$?gH&EFa}<=8vVbGs%ed#wrBpkt!rzeJ!XtI++oVZE8w
u(l_Zs#+rtc{P(x{%KtV!_Ir`vmPYZD`K=3uD(*f6dBW4x&t;ucLK6VMXJ@?t

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..33b77eb3fa06701d9ae8fef05941249fb820001f
GIT binary patch
literal 541
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2GuF*6!)z7!twx
zHq@}M*+772xp2syW+|_I2kIB>oh5#de}cw@jDReGdIp!G#nC6bbdD#SD+piHeZt!M
zo@{?|&F+s0>^B(88e|jLdt?fFZY)bVUU<lWr-_04{rBq2Cg-1D-qK-OCv!aZx?`gR
z2V+!rLWqX%<<i(z5ktm~DS?{YDK`|ZgoJ+mYHXkw=@I)%_@DWsB@0*-a|MN4mT<B(
zMjJT9D|IQo;__ayj-%Cof#&NPjf?jiN|pyK)Zo&n)@>C~P;C0<b>PlEh5O%E9=GTV
zGHziKG3iW>pK!&2t36R+$@^+e*N4l6Z26B&OpR>&l^UtFJ4W}#$BHMF>!uX3_)J=|
z?sev@7jiG_r>8PXdiRyw+RV1}E7!f$^OsuQ?#o_zd}G1+w$9`aw;nHvxqdf3%HVnB
zHyi(!U!9E#Hx{g4deUot4)5nz?<cr%EseM^&GpR$*Y<+y?W<OPcH`#lTaz1IdRX_%
zheN%G{=a+K5SMYtdc)^JR^BI@<u>ttw>ZnUf%y%C^#Q&=GnwyC{<byYGxr*4P}F$3
L`njxgN@xNAbvn{r

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..7653aa43105e854c1ab445eac2b0d15039750021
GIT binary patch
literal 703
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@+Kv;uw;_
z`ZmI_?~nrzOUsKF3p%GvGoSN=$+zmC!@>9;40{;vs`Irha0~Q$!0d4HZimOjlbbj`
z#F;s6W-Z#Y?eWaQ4zsON;*tu?DGfXm7>yj*PB6eY?L1dxuU(p+Wg-=SU6h4!)!XmC
z=fB<+qxb7koz|A9wHGr=EM)3>Su1WhHOxO}QqFt0VDn9hjy##yU!@Xf?G~BaUAFKf
zFUM)tLt4$e%xPJzjeqv(6&o##Hcb=a@1Mdxi-(Qb(s;+2mz^S#0dCU&8-pg_&Aa}x
z<c!<yyX%gyNq442blUu_i$68>vX@zxu4Jdn&S<M@8@cqiPP>l(^~|04!$M-WovH8r
zLz&&Huf8f=Z{(nMYf}wV_V(M6(G^8Tzt<@;r#!lwU{TAo`RkJ-@sbIilTsR%&b2Id
zOKtE~oBXq;&PMJdw>A6WJvp=16;=5y4;FDf|2<6d=)CqPMw_m`_FaDYZCQ77@VBh0
zs}5FO3#ObtXR@{J@I#<R(KcPVr2zq#`)9`pyb%cP5@Y@Q+HCgOoIBZnW_X$(*&oZw
z-^SLUE1r5kPt*5G-I;^?UH7ijO-f=&czV<2wy>4K?V?A4pT2H<Z^%?^6vryIFo|{2
z+b3ZmXE=L79I;LRU$U;%UmdE`m&~0qH99_wJ7;IiDuIYe;@z#O%Qe0qpLg8hLHfJ}
zmrpk2oywQ9_&4R<Z1=$6^S{xe5E7psP8^V*mVa*dl<-L>L5auH)z4*}Q$iB}GEEpm

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on.png
new file mode 100644
index 0000000000000000000000000000000000000000..8209991f3dacb1838126588c14e08b1bd481d120
GIT binary patch
literal 307
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlfU&GmF~46zV=
zJMkdz5d|I==Uo;mv%L62yp#i&I{v4&*xQ`(oG@pK`yYpIdb^EIZ`=EI0gK&QJ>4gB
zxD{)nFB<6GQtUI7Tbm&f-cup4IzcH(%I(0!>vl?Owgzab?o{Y)Se25o<la2Cz2CQn
zb?&rhT&VV@=-l3Y?-zzBFJ7b(8sm^Gay3`-@68ocjH+(&drkacnsu+Cc>l7B{TG^r
ZEEr4YG-$M(Sh^kLU{6;+mvv4FO#n^#UpW8(

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4e3d6b2532b70644477c6aa11ae93e3c4f872e1
GIT binary patch
literal 502
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2GuFmf-2)7!twx
zcB*4vvjLB*xA=xfGHVh~itgZlE!VBAu+br@c?Ux|L(7cyQJVw;mV0s=OIdbnZL6Pi
zUR!bdY(CWmEK?eU0+_;BY_z||y{U|oIUXpo@JeZH?Df-|-lU4BUNXtOEyh=&;PPqF
zTl3pbD(%i2M?d78q>+1MdHaC_d8anj*s!ama4+??e7N^vJ(r(=5QC4vBy|ppdaHRV
zo*Cf^H{WzkuylNu@buk=wsRt4Vy=woulYD8F5x+Sjp^~7qX80aSGK?Z{g-V{%FQ_C
zlo%e)w|4p~7oL|~XPpum7JAjhx8&55_!A1pCY845pSu`U_F+}kG<BPIt975+i0$J)
z_Gp`Y+rjUDzejH_k~yxxVKGHrtgXTEooa}=e2L@A<8ck2bu<_L_u*K6{Y%C1($%eE
z?*lHI9G9N8=^Qt2@Bcej8TWr^sb-j;^K;z-trUUzr4uDzWJYpoFoFW+z<R+Sj8Y{_
U_lmjb$$`S$)78&qol`;+07e0~3IG5A

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..67c7d7c71763a280ed477ec333ff05b99937072a
GIT binary patch
literal 662
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{KF`;uw;_
z`gVq6Uz3A?>(y54vK@wnuO-!D8kH;9#pOKv4W>)<PTX3!_TGV|#`cpYY?40dAbMlQ
z{ng7>YQ5Tj-yy-^fdZot26AR{J{+}lsoUa<*OPsg2eZWJO`m^!$Nl$9KiWpf^dCR{
z^i$Q|e^tz%3b+_;Cp_F@G5hQ$ozEh+x-xw1EtZ{*@{3|CKB+u9%6O=1F|YKQ?_AQK
zejEOo%X$CTM)kta9@P&#XDusget6-du%?m15zBdvmnMJyDRX?|{TC@l6K@%~FOnC#
z;5A9*lHPIcn;L#6JycF@K6o~Kb?8OCHLKtGS9KqG^<Y<euerlNv2(L;y#Idn?g@sO
z5p%zu-+b)QA`LCQ9_!^Zru(Hd_~`Ji+nu4a=H1iV-OD$8yRj{JX5ZGsre9Wl{#mm(
zPQL&6Bh_mOO(i0ZD+8|RiFMz8d(FW!lgn%2gWp>jH!NHgqs6uF{`RwJ!CY}WJ8XZZ
zU7qs&s`g)v)gsIO&TOtXI=paO$9l%5sq#j*=3QvKKka1KB=PtQYI-FREq$dNx1R)+
zoMB0BcYP7jzQI>-Y3Z7!;#>Bu3A^#K#E9{Y$i?Ybxo*EL^Yd402U@$t@3WHpZ(BB%
z`9E3bR`cHfR4@0V>tOBu1O>w#^>Syw8yb5yuy~-M1?DXO7=kwJwzt1LX&ER@c)I$z
JtaD0e0sylt5<36@

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock.png b/Telegram/Resources/icons/dialogs/dialogs_unlock.png
deleted file mode 100644
index 207f93bb99be3977e2d1ef9ab2b73ab46073a945..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 357
zcmeAS@N?(olHy`uVBq!ia0vp^qChOd!2~4VYCcQ>QjEnx?oJHr&dIz4a$Hg)JkxxA
z8MJ_G4hF{dOa>N^5+IfWVg?501&j>LK$;OGwtxvPE4qLg&K3bFG_AJY2c*t>x;Tbd
z2;QBvm$yYh=IBEysc#E59nhU|Q1ZqR(={=zM~)qnQd0<vy0f>XMVLi;l6GH{P2K<R
zKh&12y%-haWy#YlkaT-n=B#O{D=oQWvTNrp|9(%RSx#!n1MNlo-V4rnKC_sQxhg~8
z&59T2A6oQ97`Hg{O0n2%7y7ho@w(U7O7DKRowmU7fO6-e3F$qu#|32_8yeCD4m<tJ
zt5R6|GbL;3l&#-l^H&_2oU*R@dBF^^dD$vI>kDGCO?_XQU#<GL|CIIfta7oRlb3(}
d`&eneu>6v_n|ycAIpqiPqNl5$%Q~loCIIgme~|zH

diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png b/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png
deleted file mode 100644
index 64359ff67dda272585074f6992d6b970975cf913..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 523
zcmeAS@N?(olHy`uVBq!ia0vp^T0pG9!2~4PGyk&#DaPU;cPEB*=VV?2IWDOYo@u_m
zU{xFpjP02WEFdL7ECs|249p7{8JK}IBS>rk6I@nn0W+Mf0aCc`*0*e+vQkeM$B+ol
zx8b{cTMR^E^c0oU7IM63(0kETw~jGNVJU;h3dTl`7Ytj>t-Pm|^T^aaD%sA&uD$>H
z%u4y3Gj?*}a`V>wFyOgt(z}S`%evRAR!JQXRC{{NYtj;()2Wg3jx}(m%N);(TKB@{
z{PRk)NBev>vNWuI7HG4i)Wjug*0aym2}#esXiF}5Zsm4+o59&NiHBBBV(tm^T~g5J
zzWR#bvG29ftbTJD512W;J7^bPox9)Yu404Z#N*0o(^6NJ*rY`%2{W{^v`z~<+aIcL
zdG%-8=5^UoO2Nz~tqdu0T=Fc9i+B$^)EJpM1amk%TlnFK<;6XR=il%NUzu%QzdqVx
zw`<^++V%HT*6mkls{6otpt8ZtcXGCeG{2xHo5!7NlPVRC9{roqU7fd_W98a{+4rjg
zZa(7pGjF@xi(BmP*Trp^dT!C3v-YbeGtK>ftLE=TjTN05)<LtcKDIYr+2PxHMLvGV
t3Q@@i>qO#f6`J&RzByXEuQUHjqyG1~6^jl`<@N(bgr}>Y%Q~loCIDu|$j$%&

diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png b/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png
deleted file mode 100644
index c629423025405920a5f5e24ea4d2ccbb1832b314..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 769
zcmV+c1OEJpP)<h;3K|Lk000e1NJLTq002J#002A)0ssI2Ua9pa00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91KcE8u1ONa40RR91JOBUy0N6#c*Z=?mYe_^wRA>e5SiP$9KoB+GwGyoa
zMJo$Y8#`Y_EVcD5d;@(2JFWEv>;+3v3rjn}N<kFleg}kQvdJcsCD{<}TFuU$IdgLM
z=jJAtvp?&AjsrhBMU0wavDj!dTCG;0P<Xvw@AsQgls2T->rJQA^ZESi{vm@rX44j^
z=-+O)SF4p<v(E?^AQc=brH8}e^ZE1<>z4u&_>`1-+D;}DKan9hRC$;pC!EL??gF^D
zSS;MR-05^$uh;wi{&u^S%jIsj+wb=~osI+>jYiAmQc9W|LXfm!E|<$-Fz{7?JZ9;N
zpf=4jARW`)Zda{VeWibsm<5JD02l)B2~OeqqdMSguypYN(h8(=jm~B>s$R8Wy#4`6
z3&G>XB%Yyvl8(n?q<%Qo;sL-Dsg{EO(E&fki$Js46nNY)BIX2iyMqU8oGLFyQ+>T&
zr(C!k=`b{w@Df{Sxd3RYuT(060>SlqrNhuz!b@zS<pQ9oKA+DE3Iw0ehYmwy2`{mQ
zmJ5KUdQqQlVAZGRzxCdFujhc>IeNauQI&1)BRJqV&UifDZnyXQUA!U26@U}W;76AT
znwZd9t+v^0;?f$xgD&XBRK<e94>QugPlaA_t^gJ^z^x>v(2?aU2ZSA&gv0<&bmRzQ
z9T6NU?SFqbBD|Q#<54P=ym=|ZQV)})G6O@-*3b5RZ`B*vMv4~JdxbFu($;$eCM#N4
z?-j-vNL%j>n5<}Fy;m4xApHlt$J@36BWVj}N0#@8!+}y8h?yN(>gV&h0mZaMoE@|M
zO#!ZC_<PQGO|b%5{gy^IrH!ov)&c8)t^@x8z$nsV1*0Pm00000NkvXXu0mjfXxB)q

diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index d8b405e7c..418584f7b 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -218,13 +218,18 @@ dialogsMenuToggleUnreadMuted: icon {
 	{ "dialogs/dialogs_menu_unread_dot", dialogsMenuIconFg },
 };
 
-dialogsLock: IconButton(dialogsMenuToggle) {
-	icon: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFg }};
-	iconOver: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFgOver }};
+dialogsLock: IconButton {
+	width: 36px;
+	height: 38px;
+
+	icon: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFg }};
+	iconOver: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFgOver }};
+	iconPosition: point(-1px, -1px);
+
 	ripple: emptyRippleAnimation;
 }
-dialogsUnlockIcon: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFg }};
-dialogsUnlockIconOver: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFgOver }};
+dialogsUnlockIcon: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFg }};
+dialogsUnlockIconOver: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFgOver }};
 dialogsCalendar: IconButton {
 	width: 32px;
 	height: 35px;

From 6b4ccd29957e03f506a9f857886f68e785ee0618 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 11:41:12 +0400
Subject: [PATCH 185/259] Fix lock/unlock toggle without stories.

---
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 23 +++++++++++++------
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  5 ++--
 2 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 984d5b647..a1f570fac 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -212,7 +212,9 @@ Widget::Widget(
 	_searchControls,
 	object_ptr<Ui::IconButton>(this, st::dialogsCalendar))
 , _cancelSearch(_searchControls, st::dialogsCancelSearch)
-, _lockUnlock(_searchControls, st::dialogsLock)
+, _lockUnlock(
+	_searchControls,
+	object_ptr<Ui::IconButton>(this, st::dialogsLock))
 , _scroll(this)
 , _scrollToTop(_scroll, st::dialogsToUp)
 , _stories((_layout != Layout::Child)
@@ -370,10 +372,13 @@ Widget::Widget(
 	) | rpl::start_with_next([=] {
 		updateLockUnlockVisibility();
 	}, lifetime());
-	_lockUnlock->setClickedCallback([this] {
-		_lockUnlock->setIconOverride(&st::dialogsUnlockIcon, &st::dialogsUnlockIconOver);
+	const auto lockUnlock = _lockUnlock->entity();
+	lockUnlock->setClickedCallback([=] {
+		lockUnlock->setIconOverride(
+			&st::dialogsUnlockIcon,
+			&st::dialogsUnlockIconOver);
 		Core::App().maybeLockByPasscode();
-		_lockUnlock->setIconOverride(nullptr);
+		lockUnlock->setIconOverride(nullptr);
 	});
 
 	setupMainMenuToggle();
@@ -2273,6 +2278,7 @@ void Widget::applyFilterUpdate(bool force) {
 		return;
 	}
 
+	updateLockUnlockVisibility(anim::type::normal);
 	updateStoriesVisibility();
 	const auto filterText = currentSearchQuery();
 	_inner->applyFilterUpdate(filterText, force);
@@ -2607,7 +2613,7 @@ void Widget::resizeEvent(QResizeEvent *e) {
 	updateControlsGeometry();
 }
 
-void Widget::updateLockUnlockVisibility() {
+void Widget::updateLockUnlockVisibility(anim::type animated) {
 	if (_showAnimation) {
 		return;
 	}
@@ -2618,8 +2624,11 @@ void Widget::updateLockUnlockVisibility() {
 		|| _childList
 		|| !_filter->getLastText().isEmpty()
 		|| _searchInChat;
-	if (_lockUnlock->isHidden() != hidden) {
-		_lockUnlock->setVisible(!hidden);
+	if (_lockUnlock->toggled() == hidden) {
+		const auto stories = _stories && !_stories->empty();
+		_lockUnlock->toggle(
+			!hidden,
+			stories ? anim::type::instant : animated);
 		if (!hidden) {
 			updateLockUnlockPosition();
 		}
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 48760b630..83a8d70e6 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -179,7 +179,8 @@ private:
 	void clearSearchCache();
 	void setSearchQuery(const QString &query);
 	void updateControlsVisibility(bool fast = false);
-	void updateLockUnlockVisibility();
+	void updateLockUnlockVisibility(
+		anim::type animated = anim::type::instant);
 	void updateLoadMoreChatsVisibility();
 	void updateStoriesVisibility();
 	void updateJumpToDateVisibility(bool fast = false);
@@ -245,7 +246,7 @@ private:
 	object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _chooseFromUser;
 	object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _jumpToDate;
 	object_ptr<Ui::CrossButton> _cancelSearch;
-	object_ptr<Ui::IconButton> _lockUnlock;
+	object_ptr< Ui::FadeWrapScaled<Ui::IconButton>> _lockUnlock;
 
 	std::unique_ptr<Ui::MoreChatsBar> _moreChatsBar;
 

From ad8f06fd95cf725e2f46875c79bebf62f86d0759 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 11:41:26 +0400
Subject: [PATCH 186/259] Mark stories as read almost instantly.

---
 .../SourceFiles/media/stories/media_stories_controller.cpp    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 973473de1..bd6c19b64 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -72,8 +72,8 @@ constexpr auto kPreloadUsersCount = 3;
 constexpr auto kPreloadStoriesCount = 5;
 constexpr auto kPreloadNextMediaCount = 3;
 constexpr auto kPreloadPreviousMediaCount = 1;
-constexpr auto kMarkAsReadAfterSeconds = 1;
-constexpr auto kMarkAsReadAfterProgress = 0.2;
+constexpr auto kMarkAsReadAfterSeconds = 0.2;
+constexpr auto kMarkAsReadAfterProgress = 0.;
 
 } // namespace
 

From 3606e62515bd18d794cfef3409c78ad2598655c9 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 11:41:38 +0400
Subject: [PATCH 187/259] Apply interface scale multiplier in overscroll.

---
 Telegram/lib_ui | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index fd1752a5c..c7e0b7af3 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit fd1752a5c2e71810e75c3b90eee3ff6fd9e0d71b
+Subproject commit c7e0b7af37bce7ec77195ff717d70457e51e1e7a

From de0b79aee80a19332d032ba4a3165c120a4e72f2 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 11:48:27 +0400
Subject: [PATCH 188/259] Use larger skips in segments to allow up to 50.

---
 Telegram/SourceFiles/ui/effects/round_checkbox.cpp | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
index 8a6b7f4c2..a31eb708e 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
@@ -418,11 +418,12 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 		} else {
 			const auto small = 160;
 			const auto full = arc::kFullLength;
-			const auto separator = (full > 2 * small * segments)
+			const auto separator = (full > 1.1 * small * segments)
 				? small
-				: full / (segments * 2);
+				: full / (segments * 1.1);
 			const auto left = full - (separator * segments);
 			const auto length = left / float64(segments);
+
 			auto start = 0. + (arc::kQuarterLength + (separator / 2));
 			for (const auto &segment : ranges::views::reverse(_segments)) {
 				p.setPen(QPen(

From b46799f2c3a2fd6612d921a8db49fc3729b6e4fe Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 11:58:56 +0400
Subject: [PATCH 189/259] Fix premium gift message layout.

---
 .../history/view/media/history_view_premium_gift.cpp          | 4 ++++
 .../history/view/media/history_view_premium_gift.h            | 1 +
 2 files changed, 5 insertions(+)

diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
index ec292be93..7df009fde 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
@@ -76,6 +76,10 @@ ClickHandlerPtr PremiumGift::createViewLink() {
 	});
 }
 
+int PremiumGift::buttonSkip() {
+	return st::msgServiceGiftBoxButtonMargins.top();
+}
+
 void PremiumGift::draw(
 		Painter &p,
 		const PaintContext &context,
diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
index edb79ad52..f3839291d 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
@@ -28,6 +28,7 @@ public:
 	QString title() override;
 	TextWithEntities subtitle() override;
 	QString button() override;
+	int buttonSkip() override;
 	void draw(
 		Painter &p,
 		const PaintContext &context,

From 3b59f520280f05fd572b6973e14beffdcbf5e0a6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 12:23:23 +0400
Subject: [PATCH 190/259] Fix overscroll background on theme change.

---
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 20 +++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index a1f570fac..7c88ad701 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -434,7 +434,18 @@ Widget::Widget(
 	setupSupportMode();
 	setupScrollUpButton();
 
-	_scroll->setOverscrollBg(st::dialogsBg->c);
+	const auto overscrollBg = [=] {
+		return anim::color(
+			st::dialogsBg,
+			st::dialogsBgOver,
+			_childListShown.current());
+	};
+	_scroll->setOverscrollBg(overscrollBg());
+	style::PaletteChanged(
+	) | rpl::start_with_next([=] {
+		_scroll->setOverscrollBg(overscrollBg());
+	}, lifetime());
+
 	if (_layout != Layout::Child) {
 		setupConnectingWidget();
 
@@ -457,11 +468,8 @@ Widget::Widget(
 		}, lifetime());
 
 		_childListShown.changes(
-		) | rpl::start_with_next([=](float64 value) {
-			_scroll->setOverscrollBg(anim::color(
-				st::dialogsBg,
-				st::dialogsBgOver,
-				value));
+		) | rpl::start_with_next([=] {
+			_scroll->setOverscrollBg(overscrollBg());
 			updateControlsGeometry();
 		}, lifetime());
 

From 89bd3c10c5dc225e092487f7bbdbc0beab84f37c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 12:36:52 +0400
Subject: [PATCH 191/259] Fix crash in premium preview box.

---
 Telegram/SourceFiles/settings/settings_premium.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp
index 055b1296e..82d4e4801 100644
--- a/Telegram/SourceFiles/settings/settings_premium.cpp
+++ b/Telegram/SourceFiles/settings/settings_premium.cpp
@@ -1823,7 +1823,7 @@ not_null<Ui::GradientButton*> CreateSubscribeButton(
 	const auto &st = st::premiumPreviewBox.button;
 	result->resize(args.parent->width(), st.height);
 
-	const auto premium = &args.controller->session().api().premium();
+	const auto premium = &args.show->session().api().premium();
 	premium->reload();
 	const auto computeCost = [=] {
 		const auto amount = premium->monthlyAmount();

From 5f4dcc5eb662d06bde6da4affc7fe8b315362479 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 19:59:27 +0400
Subject: [PATCH 192/259] Show segments in expanded stories list.

---
 Telegram/Resources/langs/lang.strings         |   2 -
 Telegram/SourceFiles/boxes/peer_list_box.cpp  |   9 +-
 Telegram/SourceFiles/boxes/peer_list_box.h    |   4 +-
 .../boxes/peer_list_controllers.cpp           |   5 +-
 .../dialogs/ui/dialogs_stories_list.cpp       | 122 +++++++++++++-----
 .../dialogs/ui/dialogs_stories_list.h         |   8 ++
 .../ui/effects/outline_segments.cpp           |  73 +++++++++++
 .../SourceFiles/ui/effects/outline_segments.h |  23 ++++
 .../SourceFiles/ui/effects/round_checkbox.cpp |  28 +---
 .../SourceFiles/ui/effects/round_checkbox.h   |  14 +-
 Telegram/cmake/td_ui.cmake                    |   2 +
 11 files changed, 218 insertions(+), 72 deletions(-)
 create mode 100644 Telegram/SourceFiles/ui/effects/outline_segments.cpp
 create mode 100644 Telegram/SourceFiles/ui/effects/outline_segments.h

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 68a00520e..0fea4928d 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2408,8 +2408,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_add_contact" = "Create";
 "lng_add_contact_button" = "New contact";
 "lng_contacts_header" = "Contacts";
-"lng_contacts_by_online" = "Sorted by last seen time";
-"lng_contacts_by_name" = "Sorted by name";
 "lng_contacts_hidden_stories" = "Hidden Stories";
 "lng_contacts_stories_status#one" = "{count} story";
 "lng_contacts_stories_status#other" = "{count} stories";
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index 777e952b9..bbfb2acb1 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -10,13 +10,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "main/session/session_show.h"
 #include "main/main_session.h"
 #include "mainwidget.h"
+#include "ui/effects/loading_element.h"
+#include "ui/effects/outline_segments.h"
+#include "ui/effects/round_checkbox.h"
+#include "ui/effects/ripple_animation.h"
 #include "ui/widgets/multi_select.h"
 #include "ui/widgets/labels.h"
 #include "ui/widgets/scroll_area.h"
 #include "ui/widgets/popup_menu.h"
-#include "ui/effects/loading_element.h"
-#include "ui/effects/round_checkbox.h"
-#include "ui/effects/ripple_animation.h"
 #include "ui/empty_userpic.h"
 #include "ui/wrap/slide_wrap.h"
 #include "ui/text/text_options.h"
@@ -894,7 +895,7 @@ void PeerListRow::setCheckedInternal(bool checked, anim::type animated) {
 }
 
 void PeerListRow::setCustomizedCheckSegments(
-		std::vector<Ui::RoundImageCheckboxSegment> segments) {
+		std::vector<Ui::OutlineSegment> segments) {
 	Expects(_checkbox != nullptr);
 
 	_checkbox->setCustomizedSegments(std::move(segments));
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h
index 4252b5fa2..ac8ab554b 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.h
+++ b/Telegram/SourceFiles/boxes/peer_list_box.h
@@ -36,7 +36,7 @@ class SlideWrap;
 class FlatLabel;
 struct ScrollToRequest;
 class PopupMenu;
-struct RoundImageCheckboxSegment;
+struct OutlineSegment;
 } // namespace Ui
 
 using PaintRoundImageCallback = Fn<void(
@@ -204,7 +204,7 @@ public:
 		setCheckedInternal(checked, animated);
 	}
 	void setCustomizedCheckSegments(
-		std::vector<Ui::RoundImageCheckboxSegment> segments);
+		std::vector<Ui::OutlineSegment> segments);
 	void setHidden(bool hidden) {
 		_hidden = hidden;
 	}
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 18d2d514f..10e393b56 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/history.h"
 #include "history/history_item.h"
 #include "dialogs/dialogs_main_list.h"
+#include "ui/effects/outline_segments.h"
 #include "ui/wrap/slide_wrap.h"
 #include "window/window_session_controller.h" // showAddContact()
 #include "base/unixtime.h"
@@ -53,14 +54,14 @@ namespace {
 constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
 constexpr auto kSearchPerPage = 50;
 
-[[nodiscard]] std::vector<Ui::RoundImageCheckboxSegment> PrepareSegments(
+[[nodiscard]] std::vector<Ui::OutlineSegment> PrepareSegments(
 		int count,
 		int unread,
 		const QBrush &unreadBrush) {
 	Expects(unread <= count);
 	Expects(count > 0);
 
-	auto result = std::vector<Ui::RoundImageCheckboxSegment>();
+	auto result = std::vector<Ui::OutlineSegment>();
 	const auto add = [&](bool unread) {
 		result.push_back({
 			.brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b,
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index ad39539dd..6e89cb043 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/effects/outline_segments.h"
 #include "ui/widgets/menu/menu_add_action_callback_factory.h"
 #include "ui/widgets/popup_menu.h"
 #include "ui/painter.h"
@@ -41,6 +42,7 @@ struct List::Layout {
 	QPointF geometryShift;
 	float64 expandedRatio = 0.;
 	float64 ratio = 0.;
+	float64 segmentsSpinProgress = 0.;
 	float64 thumbnailLeft = 0.;
 	float64 photoLeft = 0.;
 	float64 left = 0.;
@@ -72,6 +74,8 @@ List::List(
 	resize(0, _data.empty() ? 0 : st.full.height);
 }
 
+List::~List() = default;
+
 void List::showContent(Content &&content) {
 	if (_content == content) {
 		return;
@@ -151,12 +155,13 @@ void List::requestExpanded(bool expanded) {
 	if (_expanded != expanded) {
 		_expanded = expanded;
 		const auto from = _expanded ? 0. : 1.;
-		const auto till = _expanded ? 1. : 0.;
+		const auto till = _expanded ? 2. : 0.;
+		const auto duration = (_expanded ? 2 : 1) * st::slideWrapDuration;
 		_expandedAnimation.start([=] {
 			checkForFullState();
 			update();
 			_collapsedGeometryChanged.fire({});
-		}, from, till, st::slideWrapDuration, anim::sineInOut);
+		}, from, till, duration, anim::sineInOut);
 	}
 	_toggleExpandedRequests.fire_copy(_expanded);
 }
@@ -192,10 +197,13 @@ List::Layout List::computeLayout() {
 	updateExpanding(
 		_lastExpandedHeight * _expandCatchUpAnimation.value(1.),
 		_st.full.height);
-	return computeLayout(_expandedAnimation.value(_expanded ? 1. : 0.));
+	return computeLayout(_expandedAnimation.value(_expanded ? 2. : 0.));
 }
 
 List::Layout List::computeLayout(float64 expanded) const {
+	const auto segmentsSpinProgress = expanded / 2.;
+	expanded = std::min(expanded, 1.);
+
 	const auto &st = _st.small;
 	const auto &full = _st.full;
 	const auto expandedRatio = _lastRatio;
@@ -251,6 +259,7 @@ List::Layout List::computeLayout(float64 expanded) const {
 				: 0.)),
 		.expandedRatio = expandedRatio,
 		.ratio = ratio,
+		.segmentsSpinProgress = segmentsSpinProgress,
 		.thumbnailLeft = thumbnailLeft,
 		.photoLeft = photoLeft,
 		.left = thumbnailLeft - photoLeft,
@@ -385,6 +394,11 @@ void List::paintEvent(QPaintEvent *e) {
 			}
 		}
 	};
+	auto gradient = QLinearGradient();
+	gradient.setStops({
+		{ 0., st::groupCallLive1->c },
+		{ 1., st::groupCallMuted1->c },
+	});
 	enumerate([&](Single single) {
 		// Name.
 		if (const auto full = single.itemFull) {
@@ -409,30 +423,35 @@ void List::paintEvent(QPaintEvent *e) {
 			photo);
 		const auto small = single.itemSmall;
 		const auto itemFull = single.itemFull;
-		const auto smallUnread = small && small->element.unreadCount;
-		const auto fullUnread = itemFull && itemFull->element.unreadCount;
-		const auto unreadOpacity = (smallUnread && fullUnread)
+		const auto smallUnread = (small && small->element.unreadCount);
+		const auto fullUnreadCount = itemFull
+			? itemFull->element.unreadCount
+			: 0;
+		const auto unreadOpacity = (smallUnread && fullUnreadCount)
 			? 1.
 			: smallUnread
 			? (1. - expandRatio)
-			: fullUnread
+			: fullUnreadCount
 			? expandRatio
 			: 0.;
 		if (unreadOpacity > 0.) {
 			p.setOpacity(unreadOpacity);
-			const auto outerAdd = 2 * line;
+			const auto outerAdd = 1.5 * 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);
+			gradient.setStart(userpic.topRight());
+			gradient.setFinalStop(userpic.bottomLeft());
+			if (!fullUnreadCount) {
+				p.setPen(QPen(gradient, line));
+				p.drawEllipse(outer);
+			} else {
+				validateSegments(itemFull, gradient, line, true);
+				Ui::PaintOutlineSegments(
+					p,
+					outer,
+					itemFull->segments,
+					layout.segmentsSpinProgress);
+			}
 		}
 		p.setOpacity(1.);
 	}, [&](Single single) {
@@ -447,30 +466,37 @@ void List::paintEvent(QPaintEvent *e) {
 		const auto small = single.itemSmall;
 		const auto itemFull = single.itemFull;
 		const auto smallUnread = small && small->element.unreadCount;
-		const auto fullUnread = itemFull && itemFull->element.unreadCount;
+		const auto fullUnreadCount = itemFull
+			? itemFull->element.unreadCount
+			: 0;
+		const auto fullCount = itemFull ? itemFull->element.count : 0;
 
 		// White circle with possible read gray line.
-		const auto hasReadLine = (itemFull && !fullUnread);
+		const auto hasReadLine = (itemFull && fullUnreadCount < fullCount);
 		p.setOpacity((small && itemFull)
 			? 1.
 			: small
 			? (1. - expandRatio)
 			: expandRatio);
-		if (hasReadLine) {
-			auto color = st::dialogsUnreadBgMuted->c;
-			if (small) {
-				color.setAlphaF(color.alphaF() * expandRatio);
-			}
-			auto pen = QPen(color);
-			pen.setWidthF(lineRead);
-			p.setPen(pen);
-		} else {
-			p.setPen(Qt::NoPen);
-		}
 		const auto add = line + (hasReadLine ? (lineRead / 2.) : 0.);
 		const auto rect = userpic.marginsAdded({ add, add, add, add });
+		p.setPen(Qt::NoPen);
 		p.setBrush(st::dialogsBg);
 		p.drawEllipse(rect);
+		if (hasReadLine) {
+			if (small && !small->element.unreadCount) {
+				p.setOpacity(expandRatio);
+			}
+			validateSegments(
+				itemFull,
+				st::dialogsUnreadBgMuted->b,
+				lineRead,
+				false);
+			Ui::PaintOutlineSegments(
+				p,
+				rect,
+				itemFull->segments);
+		}
 
 		// Userpic.
 		if (itemFull == small) {
@@ -514,6 +540,36 @@ void List::validateThumbnail(not_null<Item*> item) {
 	}
 }
 
+void List::validateSegments(
+		not_null<Item*> item,
+		const QBrush &brush,
+		float64 line,
+		bool forUnread) {
+	const auto count = item->element.count;
+	const auto unread = item->element.unreadCount;
+	if (int(item->segments.size()) != count) {
+		item->segments.resize(count);
+	}
+	auto i = 0;
+	if (forUnread) {
+		for (; i != count - unread; ++i) {
+			item->segments[i].width = 0.;
+		}
+		for (; i != count; ++i) {
+			item->segments[i].brush = brush;
+			item->segments[i].width = line;
+		}
+	} else {
+		for (; i != count - unread; ++i) {
+			item->segments[i].brush = brush;
+			item->segments[i].width = line;
+		}
+		for (; i != count; ++i) {
+			item->segments[i].width = 0.;
+		}
+	}
+}
+
 void List::validateName(not_null<Item*> item) {
 	const auto &color = st::dialogsNameFg;
 	if (!item->nameCache.isNull() && item->nameCacheColor == color->c) {
@@ -680,8 +736,8 @@ void List::setLayoutConstraints(
 }
 
 List::CollapsedGeometry List::collapsedGeometryCurrent() const {
-	const auto expanded = _expandedAnimation.value(_expanded ? 1. : 0.);
-	if (expanded == 1.) {
+	const auto expanded = _expandedAnimation.value(_expanded ? 2. : 0.);
+	if (expanded >= 1.) {
 		return { QRect(), 1. };
 	}
 	const auto layout = computeLayout(0.);
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index d0ecc1428..ef3b2917e 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -22,6 +22,7 @@ struct DialogsStoriesList;
 
 namespace Ui {
 class PopupMenu;
+struct OutlineSegment;
 } // namespace Ui
 
 namespace Dialogs::Stories {
@@ -64,6 +65,7 @@ public:
 		not_null<QWidget*> parent,
 		const style::DialogsStoriesList &st,
 		rpl::producer<Content> content);
+	~List();
 
 	void setExpandedHeight(int height, bool momentum = false);
 	void setLayoutConstraints(
@@ -100,6 +102,7 @@ private:
 		Element element;
 		QImage nameCache;
 		QColor nameCacheColor;
+		std::vector<Ui::OutlineSegment> segments;
 		bool subscribed = false;
 	};
 	struct Data {
@@ -134,6 +137,11 @@ private:
 	void updateGeometry();
 	[[nodiscard]] QRect countSmallGeometry() const;
 	void updateExpanding(int expandingHeight, int expandedHeight);
+	void validateSegments(
+		not_null<Item*> item,
+		const QBrush &brush,
+		float64 line,
+		bool forUnread);
 
 	[[nodiscard]] Layout computeLayout();
 	[[nodiscard]] Layout computeLayout(float64 expanded) const;
diff --git a/Telegram/SourceFiles/ui/effects/outline_segments.cpp b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
new file mode 100644
index 000000000..2482217e7
--- /dev/null
+++ b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
@@ -0,0 +1,73 @@
+/*
+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 "ui/effects/outline_segments.h"
+
+namespace Ui {
+
+void PaintOutlineSegments(
+		QPainter &p,
+		QRectF ellipse,
+		const std::vector<OutlineSegment> &segments,
+		float64 fromFullProgress) {
+	Expects(!segments.empty());
+
+	p.setBrush(Qt::NoBrush);
+	const auto count = int(segments.size());
+	if (count == 1) {
+		p.setPen(QPen(segments.front().brush, segments.front().width));
+		p.drawEllipse(ellipse);
+		return;
+	}
+	const auto small = 160;
+	const auto full = arc::kFullLength;
+	const auto separator = (full > 1.1 * small * count)
+		? small
+		: (full / (count * 1.1));
+	const auto left = full - (separator * count);
+	const auto length = left / float64(count);
+	const auto step = length + separator;
+	const auto spin = separator * (1. - fromFullProgress);
+
+	auto start = 0. + (arc::kQuarterLength + (separator / 2)) + (3. * spin);
+	auto pen = QPen(
+		segments.back().brush,
+		segments.back().width,
+		Qt::SolidLine,
+		Qt::RoundCap);
+	p.setPen(pen);
+	for (auto i = 0; i != count;) {
+		const auto &segment = segments[count - (++i)];
+		if (!segment.width) {
+			start += length + separator;
+			continue;
+		} else if (pen.brush() != segment.brush
+			|| pen.widthF() != segment.width) {
+			pen = QPen(
+				segment.brush,
+				segment.width,
+				Qt::SolidLine,
+				Qt::RoundCap);
+			p.setPen(pen);
+		}
+		const auto from = int(base::SafeRound(start));
+		const auto till = start + length;
+		auto added = spin;
+		for (; i != count;) {
+			start += length + separator;
+			const auto &next = segments[count - (++i)];
+			if (next.width) {
+				--i;
+				break;
+			}
+			added += (separator + length) * (1. - fromFullProgress);
+		}
+		p.drawArc(ellipse, from, int(base::SafeRound(till + added)) - from);
+	}
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/effects/outline_segments.h b/Telegram/SourceFiles/ui/effects/outline_segments.h
new file mode 100644
index 000000000..fd4835595
--- /dev/null
+++ b/Telegram/SourceFiles/ui/effects/outline_segments.h
@@ -0,0 +1,23 @@
+/*
+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 Ui {
+
+struct OutlineSegment {
+	QBrush brush;
+	float64 width = 0.;
+};
+
+void PaintOutlineSegments(
+	QPainter &p,
+	QRectF ellipse,
+	const std::vector<OutlineSegment> &segments,
+	float64 fromFullProgress = 1.);
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
index a31eb708e..78996a89c 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/rp_widget.h"
 #include "ui/ui_utility.h"
 #include "ui/painter.h"
+#include "ui/effects/outline_segments.h"
 #include "ui/image/image_prepare.h"
 
 #include <QtCore/QCoreApplication>
@@ -368,6 +369,10 @@ RoundImageCheckbox::RoundImageCheckbox(
 , _check(_st.check, _updateCallback) {
 }
 
+RoundImageCheckbox::RoundImageCheckbox(RoundImageCheckbox&&) = default;
+
+RoundImageCheckbox::~RoundImageCheckbox() = default;
+
 void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 	auto selectionLevel = _selection.value(checked() ? 1. : 0.);
 	if (_selection.animating()) {
@@ -416,26 +421,7 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 				p.drawRoundedRect(outline, *radius, *radius);
 			}
 		} else {
-			const auto small = 160;
-			const auto full = arc::kFullLength;
-			const auto separator = (full > 1.1 * small * segments)
-				? small
-				: full / (segments * 1.1);
-			const auto left = full - (separator * segments);
-			const auto length = left / float64(segments);
-
-			auto start = 0. + (arc::kQuarterLength + (separator / 2));
-			for (const auto &segment : ranges::views::reverse(_segments)) {
-				p.setPen(QPen(
-					segment.brush,
-					segment.width,
-					Qt::SolidLine,
-					Qt::RoundCap));
-				const auto from = int(base::SafeRound(start));
-				const auto till = int(base::SafeRound(start + length));
-				p.drawArc(outline, from, till - from);
-				start += length + separator;
-			}
+			PaintOutlineSegments(p, outline, _segments);
 		}
 		p.setOpacity(1.);
 	}
@@ -511,7 +497,7 @@ void RoundImageCheckbox::setColorOverride(std::optional<QBrush> fg) {
 }
 
 void RoundImageCheckbox::setCustomizedSegments(
-		std::vector<Segment> segments) {
+		std::vector<Ui::OutlineSegment> segments) {
 	_segments = std::move(segments);
 }
 
diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.h b/Telegram/SourceFiles/ui/effects/round_checkbox.h
index 9247c226e..ff24ac597 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.h
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.h
@@ -15,6 +15,8 @@ enum class ImageRoundRadius;
 
 namespace Ui {
 
+struct OutlineSegment;
+
 class RoundCheckbox {
 public:
 	RoundCheckbox(const style::RoundCheckbox &st, Fn<void()> updateCallback);
@@ -45,26 +47,22 @@ private:
 
 };
 
-struct RoundImageCheckboxSegment {
-	QBrush brush;
-	float64 width = 0.;
-};
-
 class RoundImageCheckbox {
 public:
-	using Segment = RoundImageCheckboxSegment;
 	using PaintRoundImage = Fn<void(Painter &p, int x, int y, int outerWidth, int size)>;
 	RoundImageCheckbox(
 		const style::RoundImageCheckbox &st,
 		Fn<void()> updateCallback,
 		PaintRoundImage &&paintRoundImage,
 		Fn<std::optional<int>(int size)> roundingRadius = nullptr);
+	RoundImageCheckbox(RoundImageCheckbox&&);
+	~RoundImageCheckbox();
 
 	void paint(Painter &p, int x, int y, int outerWidth) const;
 	float64 checkedAnimationRatio() const;
 
 	void setColorOverride(std::optional<QBrush> fg);
-	void setCustomizedSegments(std::vector<Segment> segments);
+	void setCustomizedSegments(std::vector<OutlineSegment> segments);
 
 	bool checked() const {
 		return _check.checked();
@@ -91,7 +89,7 @@ private:
 	RoundCheckbox _check;
 
 	//std::optional<QBrush> _fgOverride;
-	std::vector<Segment> _segments;
+	std::vector<OutlineSegment> _segments;
 
 };
 
diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake
index 08b9990c4..d5ea92dcb 100644
--- a/Telegram/cmake/td_ui.cmake
+++ b/Telegram/cmake/td_ui.cmake
@@ -267,6 +267,8 @@ PRIVATE
     ui/effects/glare.h
     ui/effects/loading_element.cpp
     ui/effects/loading_element.h
+    ui/effects/outline_segments.cpp
+    ui/effects/outline_segments.h
     ui/effects/premium_graphics.cpp
     ui/effects/premium_graphics.h
     ui/effects/premium_stars.cpp

From 04f1a7be02f268ec6f3c566c50849b3e2b7a8320 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 20:47:31 +0400
Subject: [PATCH 193/259] Preload not_hidden -> hidden -> archive.

---
 Telegram/SourceFiles/data/data_stories.h | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 848227a5d..4f81cd447 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -280,6 +280,7 @@ private:
 	[[nodiscard]] FullStoryId nextPreloadId() const;
 	void startPreloading(not_null<Story*> story);
 	void preloadFinished(FullStoryId id, bool markAsPreloaded = false);
+	void preloadListsMore();
 
 	[[nodiscard]] int pollingInterval(
 		const PollingSettings &settings) const;

From 734c5c6740c1f76f892602c442042bbb5a1d9f91 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 20:47:44 +0400
Subject: [PATCH 194/259] Use a single Ui::UnreadStoryOutlineGradient.

---
 .../boxes/peer_list_controllers.cpp           | 10 ++-----
 Telegram/SourceFiles/data/data_stories.cpp    | 26 +++++++++++++++++++
 Telegram/SourceFiles/dialogs/dialogs_row.cpp  | 13 +++++-----
 .../dialogs/ui/dialogs_stories_list.cpp       |  9 +++----
 .../view/media/history_view_story_mention.cpp | 16 ++++--------
 .../ui/effects/outline_segments.cpp           |  9 +++++++
 .../SourceFiles/ui/effects/outline_segments.h |  2 ++
 7 files changed, 53 insertions(+), 32 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 10e393b56..88f0fac9a 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -84,14 +84,8 @@ constexpr auto kSearchPerPage = 50;
 	const auto &st = st::contactsWithStories.item;
 	const auto left = st.photoPosition.x();
 	const auto top = st.photoPosition.y();
-	auto gradient = QLinearGradient(
-		QPoint(left + st.photoSize, top),
-		QPoint(left, top + st.photoSize));
-	gradient.setStops({
-		{ 0., st::groupCallLive1->c },
-		{ 1., st::groupCallMuted1->c },
-	});
-	return QBrush(gradient);
+	const auto size = st.photoSize;
+	return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size));
 }
 
 } // namespace
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 20db4a13a..d5ebe356c 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -531,11 +531,37 @@ void Stories::loadMore(StorySourcesList list) {
 			}
 		}, [](const MTPDstories_allStoriesNotModified &) {
 		});
+
+		preloadListsMore();
 	}).fail([=] {
 		_loadMoreRequestId[index] = 0;
 	}).send();
 }
 
+void Stories::preloadListsMore() {
+	if (_loadMoreRequestId[static_cast<int>(StorySourcesList::NotHidden)]
+		|| _loadMoreRequestId[static_cast<int>(StorySourcesList::Hidden)]) {
+		return;
+	}
+	const auto loading = [&](StorySourcesList list) {
+		return _loadMoreRequestId[static_cast<int>(list)] != 0;
+	};
+	const auto countLoaded = [&](StorySourcesList list) {
+		const auto index = static_cast<int>(list);
+		return _sourcesLoaded[index] || !_sourcesStates[index].isEmpty();
+	};
+	if (loading(StorySourcesList::NotHidden)
+		|| loading(StorySourcesList::Hidden)) {
+		return;
+	} else if (!countLoaded(StorySourcesList::NotHidden)) {
+		loadMore(StorySourcesList::NotHidden);
+	} else if (!countLoaded(StorySourcesList::Hidden)) {
+		loadMore(StorySourcesList::Hidden);
+	} else if (!archiveCountKnown()) {
+		archiveLoadMore();
+	}
+}
+
 void Stories::sendResolveRequests() {
 	if (!_resolveSent.empty()) {
 		return;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index 102ad6dd5..649fea059 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "ui/chat/chat_theme.h" // CountAverageColor.
 #include "ui/color_contrast.h"
+#include "ui/effects/outline_segments.h"
 #include "ui/effects/ripple_animation.h"
 #include "ui/image/image_prepare.h"
 #include "ui/text/format_values.h"
@@ -372,13 +373,11 @@ void Row::PaintCornerBadgeFrame(
 			}
 			const auto left = st->padding.left();
 			const auto top = st->padding.top();
-			auto gradient = QLinearGradient(
-				QPoint(left + st->photoSize, top),
-				QPoint(left, top + st->photoSize));
-			gradient.setStops({
-				{ 0., st::groupCallLive1->c },
-				{ 1., st::groupCallMuted1->c },
-			});
+			auto gradient = Ui::UnreadStoryOutlineGradient(QRectF(
+				st->padding.left(),
+				st->padding.top(),
+				st->photoSize,
+				st->photoSize));
 			return QBrush(gradient);
 		};
 		const auto storiesBrush = data->storiesUnread
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 6e89cb043..9b3e90027 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -394,11 +394,7 @@ void List::paintEvent(QPaintEvent *e) {
 			}
 		}
 	};
-	auto gradient = QLinearGradient();
-	gradient.setStops({
-		{ 0., st::groupCallLive1->c },
-		{ 1., st::groupCallMuted1->c },
-	});
+	auto gradient = Ui::UnreadStoryOutlineGradient();
 	enumerate([&](Single single) {
 		// Name.
 		if (const auto full = single.itemFull) {
@@ -495,7 +491,8 @@ void List::paintEvent(QPaintEvent *e) {
 			Ui::PaintOutlineSegments(
 				p,
 				rect,
-				itemFull->segments);
+				itemFull->segments,
+				layout.segmentsSpinProgress);
 		}
 
 		// Userpic.
diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
index 8aefb319b..07a469097 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp
@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "window/window_session_controller.h"
 #include "ui/boxes/confirm_box.h"
 #include "ui/chat/chat_style.h"
+#include "ui/effects/outline_segments.h"
 #include "ui/text/text_utilities.h"
 #include "ui/toast/toast.h"
 #include "ui/painter.h"
@@ -124,23 +125,16 @@ void StoryMention::draw(
 		geometry.topLeft() + QPoint(padding, padding),
 		_thumbnail->image(size));
 
-	const auto thumbnail = geometry.marginsRemoved(
-		QMargins(padding, padding, padding, padding));
+	const auto thumbnail = QRectF(geometry.marginsRemoved(
+		QMargins(padding, padding, padding, padding)));
 	const auto added = 0.5 * (_unread
 		? st::storyMentionUnreadSkipTwice
 		: st::storyMentionReadSkipTwice);
 	const auto outline = thumbnail.marginsAdded(
-		QMargins(added, added, added, added));
+		QMarginsF(added, added, added, added));
 	if (_unread && _paletteVersion != style::PaletteVersion()) {
 		_paletteVersion = style::PaletteVersion();
-		auto gradient = QLinearGradient(
-			outline.topRight(),
-			outline.bottomLeft());
-		gradient.setStops({
-			{ 0., st::groupCallLive1->c },
-			{ 1., st::groupCallMuted1->c },
-		});
-		_unreadBrush = QBrush(gradient);
+		_unreadBrush = QBrush(Ui::UnreadStoryOutlineGradient(outline));
 	}
 	auto readColor = context.st->msgServiceFg()->c;
 	readColor.setAlphaF(std::min(1. * readColor.alphaF(), kReadOutlineAlpha));
diff --git a/Telegram/SourceFiles/ui/effects/outline_segments.cpp b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
index 2482217e7..bb9befb8b 100644
--- a/Telegram/SourceFiles/ui/effects/outline_segments.cpp
+++ b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
@@ -70,4 +70,13 @@ void PaintOutlineSegments(
 	}
 }
 
+QLinearGradient UnreadStoryOutlineGradient(QRectF rect) {
+	auto result = QLinearGradient(rect.topRight(), rect.bottomLeft());
+	result.setStops({
+		{ 0., st::groupCallLive1->c },
+		{ 1., st::groupCallMuted1->c },
+	});
+	return result;
+}
+
 } // namespace Ui
diff --git a/Telegram/SourceFiles/ui/effects/outline_segments.h b/Telegram/SourceFiles/ui/effects/outline_segments.h
index fd4835595..c777827a4 100644
--- a/Telegram/SourceFiles/ui/effects/outline_segments.h
+++ b/Telegram/SourceFiles/ui/effects/outline_segments.h
@@ -20,4 +20,6 @@ void PaintOutlineSegments(
 	const std::vector<OutlineSegment> &segments,
 	float64 fromFullProgress = 1.);
 
+[[nodiscard]] QLinearGradient UnreadStoryOutlineGradient(QRectF rect = {});
+
 } // namespace Ui

From 88180121826eebd34580fbc415caec17be641c8a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Jul 2023 21:07:36 +0400
Subject: [PATCH 195/259] Show segments around chats list userpics.

---
 Telegram/SourceFiles/dialogs/dialogs_row.cpp | 87 ++++++++++++--------
 Telegram/SourceFiles/dialogs/dialogs_row.h   |  6 +-
 2 files changed, 56 insertions(+), 37 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index 649fea059..1a8778b90 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_folder.h"
 #include "data/data_forum.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_peer_values.h"
 #include "data/data_user.h"
 #include "history/history.h"
@@ -342,12 +343,14 @@ void Row::PaintCornerBadgeFrame(
 	Painter q(&data->frame);
 	q.translate(framePadding, framePadding);
 	auto hq = std::optional<PainterHighQualityEnabler>();
-	if (data->storiesShown) {
+	const auto photoSize = context.st->photoSize;
+	const auto storiesCount = data->storiesCount;
+	if (storiesCount) {
 		hq.emplace(q);
 		const auto line = st::dialogsStoriesFull.lineTwice / 2.;
 		const auto skip = line * 3 / 2.;
-		const auto scale = 1. - (2 * skip / context.st->photoSize);
-		const auto center = context.st->photoSize / 2.;
+		const auto scale = 1. - (2 * skip / photoSize);
+		const auto center = photoSize / 2.;
 		q.save();
 		q.translate(center, center);
 		q.scale(scale, scale);
@@ -361,41 +364,43 @@ void Row::PaintCornerBadgeFrame(
 		0,
 		0,
 		data->frame.width() / data->frame.devicePixelRatio(),
-		context.st->photoSize,
+		photoSize,
 		context.paused);
-	if (data->storiesShown) {
+	if (storiesCount) {
 		q.restore();
 
 		const auto st = context.st;
+		const auto outline = QRectF(0, 0, photoSize, photoSize);
+		const auto storiesUnreadCount = data->storiesUnreadCount;
 		const auto storiesUnreadBrush = [&] {
-			if (context.active) {
+			if (context.active || !storiesUnreadCount) {
 				return st::dialogsUnreadBgMutedActive->b;
 			}
 			const auto left = st->padding.left();
 			const auto top = st->padding.top();
-			auto gradient = Ui::UnreadStoryOutlineGradient(QRectF(
-				st->padding.left(),
-				st->padding.top(),
-				st->photoSize,
-				st->photoSize));
+			auto gradient = Ui::UnreadStoryOutlineGradient(outline);
 			return QBrush(gradient);
-		};
-		const auto storiesBrush = data->storiesUnread
-			? storiesUnreadBrush()
-			: context.active
+		}();
+		const auto storiesBrush = context.active
 			? st::dialogsUnreadBgMutedActive->b
 			: st::dialogsUnreadBgMuted->b;
-		const auto storiesLine = data->storiesUnread
-			? (st::dialogsStoriesFull.lineTwice / 2.)
-			: (st::dialogsStoriesFull.lineReadTwice / 2.);
-		const auto pen = QPen(storiesBrush, storiesLine);
-		q.setPen(pen);
-		q.drawEllipse(0, 0, st->photoSize, st->photoSize);
+		const auto storiesUnread = st::dialogsStoriesFull.lineTwice / 2.;
+		const auto storiesLine = st::dialogsStoriesFull.lineReadTwice / 2.;
+		auto segments = std::vector<Ui::OutlineSegment>();
+		segments.reserve(storiesCount);
+		const auto storiesReadCount = storiesCount - storiesUnreadCount;
+		for (auto i = 0; i != storiesReadCount; ++i) {
+			segments.push_back({ storiesBrush, storiesLine });
+		}
+		for (auto i = 0; i != storiesUnreadCount; ++i) {
+			segments.push_back({ storiesUnreadBrush, storiesUnread });
+		}
+		Ui::PaintOutlineSegments(q, outline, segments);
 	}
 
 	const auto &manager = data->layersManager;
 	if (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) {
-		const auto size = context.st->photoSize;
+		const auto size = photoSize;
 		if (data->cacheTTL.isNull() && peer->messagesTTL()) {
 			data->cacheTTL = CornerBadgeTTL(peer, view, size);
 		}
@@ -430,8 +435,8 @@ void Row::PaintCornerBadgeFrame(
 		? st::dialogsOnlineBadgeFgActive
 		: st::dialogsOnlineBadgeFg);
 	q.drawEllipse(QRectF(
-		context.st->photoSize - skip.x() - size,
-		context.st->photoSize - skip.y() - size,
+		photoSize - skip.x() - size,
+		photoSize - skip.y() - size,
 		size,
 		size
 	).marginsRemoved({ shrink, shrink, shrink, shrink }));
@@ -451,11 +456,8 @@ void Row::paintUserpic(
 	const auto storiesUser = historyForCornerBadge
 		? historyForCornerBadge->peer->asUser()
 		: nullptr;
-	const auto storiesShown = (storiesUser
-		&& storiesUser->hasActiveStories()) ? 1 : 0;
-	const auto storiesUnread = (storiesShown
-		&& storiesUser->hasUnreadStories()) ? 1 : 0;
-	if (!historyForCornerBadge || (!cornerBadgeShown && !storiesShown)) {
+	const auto storiesHas = storiesUser && storiesUser->hasActiveStories();
+	if (!historyForCornerBadge || (!cornerBadgeShown && !storiesHas)) {
 		BasicRow::paintUserpic(
 			p,
 			peer,
@@ -476,6 +478,22 @@ void Row::paintUserpic(
 	const auto frameSide = (2 * framePadding + context.st->photoSize)
 		* ratio;
 	const auto frameSize = QSize(frameSide, frameSide);
+	const auto storiesSource = storiesHas
+		? storiesUser->owner().stories().source(storiesUser->id)
+		: nullptr;
+	const auto storiesCountReal = storiesSource
+		? int(storiesSource->ids.size())
+		: storiesHas
+		? 1
+		: 0;
+	const auto storiesUnreadCountReal = storiesSource
+		? storiesSource->unreadCount()
+		: (storiesHas && storiesUser->hasUnreadStories())
+		? 1
+		: 0;
+	const auto limit = (1 << 7) - 1;
+	const auto storiesCount = std::min(storiesCountReal, limit);
+	const auto storiesUnreadCount = std::min(storiesUnreadCountReal, limit);
 	if (_cornerBadgeUserpic->frame.size() != frameSize) {
 		_cornerBadgeUserpic->frame = QImage(
 			frameSize,
@@ -485,7 +503,8 @@ void Row::paintUserpic(
 	auto key = peer->userpicUniqueKey(userpicView());
 	key.first += peer->messagesTTL();
 	const auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1;
-	const auto paletteVersion = style::PaletteVersion();
+	const auto paletteVersionReal = style::PaletteVersion();
+	const auto paletteVersion = (paletteVersionReal & ((1 << 17) - 1));
 	const auto active = context.active ? 1 : 0;
 	const auto keyChanged = (_cornerBadgeUserpic->key != key)
 		|| (_cornerBadgeUserpic->paletteVersion != paletteVersion);
@@ -496,14 +515,14 @@ void Row::paintUserpic(
 		|| !_cornerBadgeUserpic->layersManager.isFinished()
 		|| _cornerBadgeUserpic->active != active
 		|| _cornerBadgeUserpic->frameIndex != frameIndex
-		|| _cornerBadgeUserpic->storiesShown != storiesShown
-		|| _cornerBadgeUserpic->storiesUnread != storiesUnread
+		|| _cornerBadgeUserpic->storiesCount != storiesCount
+		|| _cornerBadgeUserpic->storiesUnreadCount != storiesUnreadCount
 		|| videoUserpic) {
 		_cornerBadgeUserpic->key = key;
 		_cornerBadgeUserpic->paletteVersion = paletteVersion;
 		_cornerBadgeUserpic->active = active;
-		_cornerBadgeUserpic->storiesShown = storiesShown;
-		_cornerBadgeUserpic->storiesUnread = storiesUnread;
+		_cornerBadgeUserpic->storiesCount = storiesCount;
+		_cornerBadgeUserpic->storiesUnreadCount = storiesUnreadCount;
 		_cornerBadgeUserpic->frameIndex = frameIndex;
 		_cornerBadgeUserpic->layersManager.markFrameShown();
 		PaintCornerBadgeFrame(
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h
index 78e638093..86120ec3d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.h
@@ -170,9 +170,9 @@ private:
 		QImage frame;
 		QImage cacheTTL;
 		int frameIndex = -1;
-		uint32 paletteVersion : 24 = 0;
-		uint32 storiesShown : 1 = 0;
-		uint32 storiesUnread : 1 = 0;
+		uint32 paletteVersion : 17 = 0;
+		uint32 storiesCount : 7 = 0;
+		uint32 storiesUnreadCount : 7 = 0;
 		uint32 active : 1 = 0;
 	};
 

From 35214d108e3e65d7178731dcce08956caaebb5f6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 09:37:37 +0400
Subject: [PATCH 196/259] Move Bio privacy up a bit.

---
 .../SourceFiles/settings/settings_privacy_security.cpp    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
index 92f8229fe..7554dba27 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
@@ -281,6 +281,10 @@ void SetupPrivacy(
 		tr::lng_settings_profile_photo_privacy(),
 		Key::ProfilePhoto,
 		[] { return std::make_unique<ProfilePhotoPrivacyController>(); });
+	add(
+		tr::lng_settings_bio_privacy(),
+		Key::About,
+		[] { return std::make_unique<AboutPrivacyController>(); });
 	add(
 		tr::lng_settings_forwards_privacy(),
 		Key::Forwards,
@@ -300,10 +304,6 @@ void SetupPrivacy(
 		tr::lng_settings_voices_privacy(),
 		Key::Voices,
 		[=] { return std::make_unique<VoicesPrivacyController>(session); });
-	add(
-		tr::lng_settings_bio_privacy(),
-		Key::About,
-		[] { return std::make_unique<AboutPrivacyController>(); });
 
 	session->api().userPrivacy().reload(Api::UserPrivacy::Key::AddedByPhone);
 

From 4402cce928de5995ef6ecebc0d6267e122e619b1 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 12:15:19 +0400
Subject: [PATCH 197/259] Allow archive with stories only.

---
 Telegram/Resources/langs/lang.strings         |  8 +-
 Telegram/SourceFiles/data/data_folder.cpp     | 44 +++++++++-
 Telegram/SourceFiles/data/data_folder.h       |  7 ++
 Telegram/SourceFiles/data/data_stories.cpp    | 36 ++++++--
 Telegram/SourceFiles/data/data_stories.h      |  5 ++
 Telegram/SourceFiles/dialogs/dialogs.style    |  2 +-
 Telegram/SourceFiles/dialogs/dialogs_row.cpp  | 84 +++++++++----------
 Telegram/SourceFiles/dialogs/dialogs_row.h    | 12 +--
 .../SourceFiles/dialogs/dialogs_widget.cpp    |  1 +
 .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 21 ++---
 .../dialogs/ui/dialogs_video_userpic.cpp      | 27 +++++-
 .../dialogs/ui/dialogs_video_userpic.h        | 14 ++++
 .../ui/effects/outline_segments.cpp           |  2 +-
 .../SourceFiles/ui/effects/outline_segments.h |  2 +
 .../SourceFiles/window/window_main_menu.cpp   | 15 ++--
 .../window/window_session_controller.cpp      |  3 +-
 Telegram/lib_ui                               |  2 +-
 17 files changed, 207 insertions(+), 78 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 0fea4928d..592cc337e 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3822,8 +3822,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji";
 
 "lng_stories_my_name" = "My Story";
-"lng_stories_archive" = "Archive";
-"lng_stories_unarchive" = "Unarchive";
+"lng_stories_archive" = "Hide Stories";
+"lng_stories_unarchive" = "Unhide Stories";
 "lng_stories_row_count#one" = "{count} Story";
 "lng_stories_row_count#other" = "{count} Stories";
 "lng_stories_views#one" = "{count} view";
@@ -3838,8 +3838,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stories_archive_title" = "Stories Archive";
 "lng_stories_archive_about" = "Only you can see archived stories unless you choose to save them to your profile.";
 "lng_stories_reply_sent" = "Message Sent";
-"lng_stories_hidden_to_contacts" = "Stories of {user} were moved to **Archive**.";
-"lng_stories_shown_in_chats" = "Stories of {user} were moved to the **Chats List**.";
+"lng_stories_hidden_to_contacts" = "Stories from {user} will now be shown in **Archived Chats**.";
+"lng_stories_shown_in_chats" = "Stories from {user} will now be shown in the **Chats List**.";
 "lng_stories_delete_one_sure" = "Are you sure you want to delete this story?";
 "lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?";
 "lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?";
diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp
index 4faa19f71..743487c85 100644
--- a/Telegram/SourceFiles/data/data_folder.cpp
+++ b/Telegram/SourceFiles/data/data_folder.cpp
@@ -39,6 +39,21 @@ constexpr auto kShowChatNamesCount = 8;
 		not_null<Folder*> folder) {
 	const auto &list = folder->lastHistories();
 	if (list.empty()) {
+		if (const auto storiesUnread = folder->storiesUnreadCount()) {
+			return {
+				tr::lng_contacts_stories_status_new(
+					tr::now,
+					lt_count,
+					storiesUnread),
+			};
+		} else if (const auto storiesCount = folder->storiesCount()) {
+			return {
+				tr::lng_contacts_stories_status(
+					tr::now,
+					lt_count,
+					storiesCount),
+			};
+		}
 		return {};
 	}
 
@@ -301,6 +316,33 @@ void Folder::validateListEntryCache() {
 		Ui::ItemTextDefaultOptions());
 }
 
+void Folder::updateStoriesCount(int count, int unread) {
+	if (_storiesCount == count && _storiesUnreadCount == unread) {
+		return;
+	}
+	const auto limit = (1 << 16) - 1;
+	const auto was = (_storiesCount > 0);
+	_storiesCount = std::min(count, limit);
+	_storiesUnreadCount = std::min(unread, limit);
+	const auto now = (_storiesCount > 0);
+	if (was == now) {
+		updateChatListEntryPostponed();
+	} else if (now) {
+		updateChatListSortPosition();
+	} else {
+		updateChatListExistence();
+	}
+	++_chatListViewVersion;
+}
+
+int Folder::storiesCount() const {
+	return _storiesCount;
+}
+
+int Folder::storiesUnreadCount() const {
+	return _storiesUnreadCount;
+}
+
 void Folder::requestChatListMessage() {
 	if (!chatListMessageKnown()) {
 		owner().histories().requestDialogEntry(this);
@@ -339,7 +381,7 @@ int Folder::fixedOnTopIndex() const {
 }
 
 bool Folder::shouldBeInChatList() const {
-	return !_chatsList.empty();
+	return !_chatsList.empty() || (_storiesCount > 0);
 }
 
 Dialogs::UnreadState Folder::chatListUnreadState() const {
diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h
index 9e62fcabe..7008adac0 100644
--- a/Telegram/SourceFiles/data/data_folder.h
+++ b/Telegram/SourceFiles/data/data_folder.h
@@ -75,6 +75,10 @@ public:
 		return _listEntryCache;
 	}
 
+	void updateStoriesCount(int count, int unread);
+	[[nodiscard]] int storiesCount() const;
+	[[nodiscard]] int storiesUnreadCount() const;
+
 private:
 	void indexNameParts();
 
@@ -104,6 +108,9 @@ private:
 	int _chatListViewVersion = 0;
 	//rpl::variable<MessagePosition> _unreadPosition;
 
+	uint16_t _storiesCount = 0;
+	uint16_t _storiesUnreadCount = 0;
+
 	rpl::lifetime _lifetime;
 
 };
diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index d5ebe356c..904813765 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/application.h"
 #include "data/data_changes.h"
 #include "data/data_document.h"
+#include "data/data_folder.h"
 #include "data/data_photo.h"
 #include "data/data_user.h"
 #include "data/data_session.h"
@@ -562,6 +563,31 @@ void Stories::preloadListsMore() {
 	}
 }
 
+void Stories::notifySourcesChanged(StorySourcesList list) {
+	_sourcesChanged[static_cast<int>(list)].fire({});
+	if (list == StorySourcesList::Hidden) {
+		pushHiddenCountsToFolder();
+	}
+}
+
+void Stories::pushHiddenCountsToFolder() {
+	const auto &list = sources(StorySourcesList::Hidden);
+	if (list.empty()) {
+		if (_folderForHidden) {
+			_folderForHidden->updateStoriesCount(0, 0);
+		}
+		return;
+	}
+	if (!_folderForHidden) {
+		_folderForHidden = _owner->folder(Folder::kId);
+	}
+	const auto count = int(list.size());
+	const auto unread = ranges::count_if(
+		list,
+		[](const StoriesSourceInfo &info) { return info.unreadCount > 0; });
+	_folderForHidden->updateStoriesCount(count, unread);
+}
+
 void Stories::sendResolveRequests() {
 	if (!_resolveSent.empty()) {
 		return;
@@ -722,7 +748,7 @@ void Stories::applyRemovedFromActive(FullStoryId id) {
 			&StoriesSourceInfo::id);
 		if (i != end(sources)) {
 			sources.erase(i);
-			_sourcesChanged[index].fire({});
+			notifySourcesChanged(list);
 		}
 	};
 	const auto i = _all.find(id.peer);
@@ -751,7 +777,7 @@ void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) {
 	if (i != end(sources)) {
 		sources.erase(i);
 	}
-	_sourcesChanged[static_cast<int>(list)].fire({});
+	notifySourcesChanged(list);
 }
 
 void Stories::removeDependencyStory(not_null<Story*> story) {
@@ -780,7 +806,7 @@ void Stories::sort(StorySourcesList list) {
 		return std::make_pair(key, info.id);
 	};
 	ranges::sort(sources, ranges::greater(), proj);
-	_sourcesChanged[index].fire({});
+	notifySourcesChanged(list);
 	preloadSourcesChanged(list);
 }
 
@@ -1044,7 +1070,7 @@ void Stories::toggleHidden(
 		const auto i = ranges::find(_sources[main], peerId, proj);
 		if (i != end(_sources[main])) {
 			_sources[main].erase(i);
-			_sourcesChanged[main].fire({});
+			notifySourcesChanged(StorySourcesList::NotHidden);
 			preloadSourcesChanged(StorySourcesList::NotHidden);
 		}
 		const auto j = ranges::find(_sources[other], peerId, proj);
@@ -1058,7 +1084,7 @@ void Stories::toggleHidden(
 		const auto i = ranges::find(_sources[other], peerId, proj);
 		if (i != end(_sources[other])) {
 			_sources[other].erase(i);
-			_sourcesChanged[other].fire({});
+			notifySourcesChanged(StorySourcesList::Hidden);
 			preloadSourcesChanged(StorySourcesList::Hidden);
 		}
 		const auto j = ranges::find(_sources[main], peerId, proj);
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 4f81cd447..27eb0f761 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -24,6 +24,7 @@ enum class ReportReason;
 
 namespace Data {
 
+class Folder;
 class Session;
 struct StoryView;
 struct StoryIdDates;
@@ -282,6 +283,9 @@ private:
 	void preloadFinished(FullStoryId id, bool markAsPreloaded = false);
 	void preloadListsMore();
 
+	void notifySourcesChanged(StorySourcesList list);
+	void pushHiddenCountsToFolder();
+
 	[[nodiscard]] int pollingInterval(
 		const PollingSettings &settings) const;
 	void maybeSchedulePolling(
@@ -319,6 +323,7 @@ private:
 	rpl::event_stream<> _sourcesChanged[kStorySourcesListCount];
 	bool _sourcesLoaded[kStorySourcesListCount] = { false };
 	QString _sourcesStates[kStorySourcesListCount];
+	Folder *_folderForHidden = nullptr;
 
 	mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 };
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index 418584f7b..69849adc8 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -257,7 +257,7 @@ dialogsFilter: InputField(defaultInputField) {
 	placeholderFg: placeholderFg;
 	placeholderFgActive: placeholderFgActive;
 	placeholderFgError: placeholderFgActive;
-	placeholderMargins: margins(2px, 0px, 2px, 0px);
+	placeholderMargins: margins(5px, 0px, 2px, 0px);
 	placeholderScale: 0.;
 	placeholderShift: -50px;
 	placeholderFont: normalFont;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index 1a8778b90..f8d748d0d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -233,20 +233,11 @@ void BasicRow::paintRipple(
 
 void BasicRow::paintUserpic(
 		Painter &p,
-		not_null<PeerData*> peer,
+		not_null<Entry*> entry,
+		PeerData *peer,
 		Ui::VideoUserpic *videoUserpic,
-		History *historyForCornerBadge,
 		const Ui::PaintContext &context) const {
-	PaintUserpic(
-		p,
-		peer,
-		videoUserpic,
-		_userpic,
-		context.st->padding.left(),
-		context.st->padding.top(),
-		context.width,
-		context.st->photoSize,
-		context.paused);
+	PaintUserpic(p, entry, peer, videoUserpic, _userpic, context);
 }
 
 Row::Row(Key key, int index, int top) : _id(key), _top(top), _index(index) {
@@ -334,7 +325,8 @@ void Row::ensureCornerBadgeUserpic() const {
 void Row::PaintCornerBadgeFrame(
 		not_null<CornerBadgeUserpic*> data,
 		int framePadding,
-		not_null<PeerData*> peer,
+		not_null<Entry*> entry,
+		PeerData *peer,
 		Ui::VideoUserpic *videoUserpic,
 		Ui::PeerUserpicView &view,
 		const Ui::PaintContext &context) {
@@ -356,16 +348,15 @@ void Row::PaintCornerBadgeFrame(
 		q.scale(scale, scale);
 		q.translate(-center, -center);
 	}
+	q.translate(-context.st->padding.left(), -context.st->padding.top());
 	PaintUserpic(
 		q,
+		entry,
 		peer,
 		videoUserpic,
 		view,
-		0,
-		0,
-		data->frame.width() / data->frame.devicePixelRatio(),
-		photoSize,
-		context.paused);
+		context);
+	q.translate(context.st->padding.left(), context.st->padding.top());
 	if (storiesCount) {
 		q.restore();
 
@@ -401,7 +392,7 @@ void Row::PaintCornerBadgeFrame(
 	const auto &manager = data->layersManager;
 	if (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) {
 		const auto size = photoSize;
-		if (data->cacheTTL.isNull() && peer->messagesTTL()) {
+		if (data->cacheTTL.isNull() && peer && peer->messagesTTL()) {
 			data->cacheTTL = CornerBadgeTTL(peer, view, size);
 		}
 		q.setOpacity(p);
@@ -419,11 +410,12 @@ void Row::PaintCornerBadgeFrame(
 	}
 	q.setCompositionMode(QPainter::CompositionMode_Source);
 
-	const auto size = peer->isUser()
+	const auto online = peer && peer->isUser();
+	const auto size = online
 		? st::dialogsOnlineBadgeSize
 		: st::dialogsCallBadgeSize;
 	const auto stroke = st::dialogsOnlineBadgeStroke;
-	const auto skip = peer->isUser()
+	const auto skip = online
 		? st::dialogsOnlineBadgeSkip
 		: st::dialogsCallBadgeSkip;
 	const auto shrink = (size / 2) * (1. - topLayerProgress);
@@ -444,27 +436,27 @@ void Row::PaintCornerBadgeFrame(
 
 void Row::paintUserpic(
 		Painter &p,
-		not_null<PeerData*> peer,
+		not_null<Entry*> entry,
+		PeerData *peer,
 		Ui::VideoUserpic *videoUserpic,
-		History *historyForCornerBadge,
 		const Ui::PaintContext &context) const {
-	updateCornerBadgeShown(peer);
+	if (peer) {
+		updateCornerBadgeShown(peer);
+	}
 
 	const auto cornerBadgeShown = !_cornerBadgeUserpic
 		? _cornerBadgeShown
 		: !_cornerBadgeUserpic->layersManager.isDisplayedNone();
-	const auto storiesUser = historyForCornerBadge
-		? historyForCornerBadge->peer->asUser()
-		: nullptr;
-	const auto storiesHas = storiesUser && storiesUser->hasActiveStories();
-	if (!historyForCornerBadge || (!cornerBadgeShown && !storiesHas)) {
-		BasicRow::paintUserpic(
-			p,
-			peer,
-			videoUserpic,
-			historyForCornerBadge,
-			context);
-		if (!historyForCornerBadge || !_cornerBadgeShown) {
+	const auto storiesUser = peer ? peer->asUser() : nullptr;
+	const auto storiesFolder = peer ? nullptr : _id.folder();
+	const auto storiesHas = storiesUser
+		? storiesUser->hasActiveStories()
+		: storiesFolder
+		? storiesFolder->storiesCount()
+		: false;
+	if (!cornerBadgeShown && !storiesHas) {
+		BasicRow::paintUserpic(p, entry, peer, videoUserpic, context);
+		if (!peer || !_cornerBadgeShown) {
 			_cornerBadgeUserpic = nullptr;
 		}
 		return;
@@ -478,20 +470,24 @@ void Row::paintUserpic(
 	const auto frameSide = (2 * framePadding + context.st->photoSize)
 		* ratio;
 	const auto frameSize = QSize(frameSide, frameSide);
-	const auto storiesSource = storiesHas
+	const auto storiesSource = (storiesHas && storiesUser)
 		? storiesUser->owner().stories().source(storiesUser->id)
 		: nullptr;
 	const auto storiesCountReal = storiesSource
 		? int(storiesSource->ids.size())
+		: storiesFolder
+		? storiesFolder->storiesCount()
 		: storiesHas
 		? 1
 		: 0;
 	const auto storiesUnreadCountReal = storiesSource
 		? storiesSource->unreadCount()
-		: (storiesHas && storiesUser->hasUnreadStories())
+		: storiesFolder
+		? storiesFolder->storiesUnreadCount()
+		: (storiesUser && storiesUser->hasUnreadStories())
 		? 1
 		: 0;
-	const auto limit = (1 << 7) - 1;
+	const auto limit = Ui::kOutlineSegmentsMax;
 	const auto storiesCount = std::min(storiesCountReal, limit);
 	const auto storiesUnreadCount = std::min(storiesUnreadCountReal, limit);
 	if (_cornerBadgeUserpic->frame.size() != frameSize) {
@@ -500,8 +496,8 @@ void Row::paintUserpic(
 			QImage::Format_ARGB32_Premultiplied);
 		_cornerBadgeUserpic->frame.setDevicePixelRatio(ratio);
 	}
-	auto key = peer->userpicUniqueKey(userpicView());
-	key.first += peer->messagesTTL();
+	auto key = peer ? peer->userpicUniqueKey(userpicView()) : InMemoryKey();
+	key.first += peer ? peer->messagesTTL() : 0;
 	const auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1;
 	const auto paletteVersionReal = style::PaletteVersion();
 	const auto paletteVersion = (paletteVersionReal & ((1 << 17) - 1));
@@ -528,6 +524,7 @@ void Row::paintUserpic(
 		PaintCornerBadgeFrame(
 			_cornerBadgeUserpic.get(),
 			framePadding,
+			_id.entry(),
 			peer,
 			videoUserpic,
 			userpicView(),
@@ -537,10 +534,11 @@ void Row::paintUserpic(
 		context.st->padding.left() - framePadding,
 		context.st->padding.top() - framePadding,
 		_cornerBadgeUserpic->frame);
-	if (historyForCornerBadge->peer->isUser()) {
+	const auto history = _id.history();
+	if (!history || history->peer->isUser()) {
 		return;
 	}
-	const auto actionPainter = historyForCornerBadge->sendActionPainter();
+	const auto actionPainter = history->sendActionPainter();
 	const auto bg = context.active
 		? st::dialogsBgActive
 		: st::dialogsBg;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h
index 86120ec3d..eecbd9c4f 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.h
@@ -35,6 +35,7 @@ struct TopicJumpCache;
 
 namespace Dialogs {
 
+class Entry;
 enum class SortMode;
 
 [[nodiscard]] QRect CornerBadgeTTLRect(int photoSize);
@@ -46,9 +47,9 @@ public:
 
 	virtual void paintUserpic(
 		Painter &p,
-		not_null<PeerData*> peer,
+		not_null<Entry*> entry,
+		PeerData *peer,
 		Ui::VideoUserpic *videoUserpic,
-		History *historyForCornerBadge,
 		const Ui::PaintContext &context) const;
 
 	void addRipple(QPoint origin, QSize size, Fn<void()> updateCallback);
@@ -99,9 +100,9 @@ public:
 		Fn<void()> updateCallback = nullptr) const;
 	void paintUserpic(
 		Painter &p,
-		not_null<PeerData*> peer,
+		not_null<Entry*> entry,
+		PeerData *peer,
 		Ui::VideoUserpic *videoUserpic,
-		History *historyForCornerBadge,
 		const Ui::PaintContext &context) const final override;
 
 	[[nodiscard]] bool lookupIsInTopicJump(int x, int y) const;
@@ -183,7 +184,8 @@ private:
 	static void PaintCornerBadgeFrame(
 		not_null<CornerBadgeUserpic*> data,
 		int framePadding,
-		not_null<PeerData*> peer,
+		not_null<Entry*> entry,
+		PeerData *peer,
 		Ui::VideoUserpic *videoUserpic,
 		Ui::PeerUserpicView &view,
 		const Ui::PaintContext &context);
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 7c88ad701..c25b31e5d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -1419,6 +1419,7 @@ void Widget::updateStoriesVisibility() {
 			if (_scroll->position().overscroll < 0) {
 				_scroll->scrollToY(0);
 			}
+			_scroll->update();
 		} else {
 			_scroll->setOverscrollDefaults(0, 0);
 			_scroll->setOverscrollTypes(Type::Virtual, Type::Real);
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
index 7e195ba94..91674bfed 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
@@ -331,22 +331,23 @@ void PaintRow(
 			context.st->padding.top(),
 			context.width,
 			context.st->photoSize);
-	} else if (from) {
-		row->paintUserpic(
-			p,
-			from,
-			videoUserpic,
-			(flags & Flag::AllowUserOnline) ? history : nullptr,
-			context);
-	} else if (hiddenSenderInfo) {
+	} else if (!from && hiddenSenderInfo) {
 		hiddenSenderInfo->emptyUserpic.paintCircle(
 			p,
 			context.st->padding.left(),
 			context.st->padding.top(),
 			context.width,
 			context.st->photoSize);
+	} else if (!(flags & Flag::AllowUserOnline)) {
+		PaintUserpic(
+			p,
+			entry,
+			from,
+			videoUserpic,
+			row->userpicView(),
+			context);
 	} else {
-		entry->paintUserpic(p, row->userpicView(), context);
+		row->paintUserpic(p, entry, from, videoUserpic, context);
 	}
 
 	auto nameleft = context.st->nameLeft;
@@ -785,7 +786,7 @@ void RowPainter::Paint(
 			? history->peer->migrateTo()
 			: history->peer.get())
 		: nullptr;
-	const auto allowUserOnline = !context.narrow || badgesState.empty();
+	const auto allowUserOnline = true;// !context.narrow || badgesState.empty();
 	const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0))
 		| (peer && peer->isSelf() ? Flag::SavedMessages : Flag(0))
 		| (peer && peer->isRepliesChat() ? Flag::RepliesMessages : Flag(0))
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
index cdebd9da4..bfc056243 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
@@ -7,13 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "dialogs/ui/dialogs_video_userpic.h"
 
-#include "ui/painter.h"
 #include "core/file_location.h"
 #include "data/data_peer.h"
 #include "data/data_photo.h"
 #include "data/data_photo_media.h"
 #include "data/data_file_origin.h"
 #include "data/data_session.h"
+#include "dialogs/ui/dialogs_layout.h"
+#include "ui/painter.h"
+#include "styles/style_dialogs.h"
 
 namespace Dialogs::Ui {
 
@@ -129,6 +131,29 @@ void VideoUserpic::clipCallback(Media::Clip::Notification notification) {
 	}
 }
 
+void PaintUserpic(
+		Painter &p,
+		not_null<Entry*> entry,
+		PeerData *peer,
+		VideoUserpic *videoUserpic,
+		PeerUserpicView &view,
+		const Ui::PaintContext &context) {
+	if (peer) {
+		PaintUserpic(
+			p,
+			peer,
+			videoUserpic,
+			view,
+			context.st->padding.left(),
+			context.st->padding.top(),
+			context.width,
+			context.st->photoSize,
+			context.paused);
+	} else {
+		entry->paintUserpic(p, view, context);
+	}
+}
+
 void PaintUserpic(
 		Painter &p,
 		not_null<PeerData*> peer,
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
index 6c285ce97..cc06a008c 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
@@ -19,10 +19,16 @@ namespace Ui {
 struct PeerUserpicView;
 } // namespace Ui
 
+namespace Dialogs {
+class Entry;
+} // namespace Dialogs
+
 namespace Dialogs::Ui {
 
 using namespace ::Ui;
 
+struct PaintContext;
+
 class VideoUserpic final {
 public:
 	VideoUserpic(not_null<PeerData*> peer, Fn<void()> repaint);
@@ -54,6 +60,14 @@ private:
 
 };
 
+void PaintUserpic(
+	Painter &p,
+	not_null<Entry*> entry,
+	PeerData *peer,
+	VideoUserpic *videoUserpic,
+	PeerUserpicView &view,
+	const Ui::PaintContext &context);
+
 void PaintUserpic(
 	Painter &p,
 	not_null<PeerData*> peer,
diff --git a/Telegram/SourceFiles/ui/effects/outline_segments.cpp b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
index bb9befb8b..5b302056b 100644
--- a/Telegram/SourceFiles/ui/effects/outline_segments.cpp
+++ b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
@@ -17,7 +17,7 @@ void PaintOutlineSegments(
 	Expects(!segments.empty());
 
 	p.setBrush(Qt::NoBrush);
-	const auto count = int(segments.size());
+	const auto count = std::min(int(segments.size()), kOutlineSegmentsMax);
 	if (count == 1) {
 		p.setPen(QPen(segments.front().brush, segments.front().width));
 		p.drawEllipse(ellipse);
diff --git a/Telegram/SourceFiles/ui/effects/outline_segments.h b/Telegram/SourceFiles/ui/effects/outline_segments.h
index c777827a4..9a38cf9ce 100644
--- a/Telegram/SourceFiles/ui/effects/outline_segments.h
+++ b/Telegram/SourceFiles/ui/effects/outline_segments.h
@@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace Ui {
 
+inline constexpr auto kOutlineSegmentsMax = 50;
+
 struct OutlineSegment {
 	QBrush brush;
 	float64 width = 0.;
diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp
index 14f9a1cd1..ba24fba90 100644
--- a/Telegram/SourceFiles/window/window_main_menu.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu.cpp
@@ -572,7 +572,7 @@ void MainMenu::setupArchive() {
 	const auto checkArchive = [=] {
 		const auto f = folder();
 		return f
-			&& !f->chatsList()->empty()
+			&& (!f->chatsList()->empty() || f->storiesCount() > 0)
 			&& controller->session().settings().archiveInMainMenu();
 	};
 
@@ -650,10 +650,15 @@ void MainMenu::setupArchive() {
 		return Badge::UnreadBadge{ state.unreadCounter, true };
 	}));
 
-	controller->session().data().chatsListChanges(
-	) | rpl::filter([](Data::Folder *folder) {
-		return folder && (folder->id() == Data::Folder::kId);
-	}) | rpl::start_with_next([=] {
+	rpl::merge(
+		controller->session().data().chatsListChanges(
+		) | rpl::filter([](Data::Folder *folder) {
+			return folder && (folder->id() == Data::Folder::kId);
+		}) | rpl::to_empty,
+		controller->session().data().stories().sourcesChanged(
+			Data::StorySourcesList::Hidden
+		)
+	) | rpl::start_with_next([=] {
 		const auto isArchiveVisible = checkArchive();
 		wrap->toggle(isArchiveVisible, anim::type::normal);
 		if (!isArchiveVisible) {
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index 1a89da1b1..657876a59 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -949,7 +949,8 @@ SessionController::SessionController(
 	) | rpl::filter([=](Data::Folder *folder) {
 		return (folder != nullptr)
 			&& (folder == _openedFolder.current())
-			&& folder->chatsList()->indexed()->empty();
+			&& folder->chatsList()->indexed()->empty()
+			&& !folder->storiesCount();
 	}) | rpl::start_with_next([=](Data::Folder *folder) {
 		folder->updateChatListSortPosition();
 		closeFolder();
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index c7e0b7af3..1c0889f78 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit c7e0b7af37bce7ec77195ff717d70457e51e1e7a
+Subproject commit 1c0889f78a74bb29e8cea6986ea8b18e7add7fb0

From fad05e8b353076997e51a476bbf451e903e0f502 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 18:15:44 +0400
Subject: [PATCH 198/259] Update tgcalls.

---
 Telegram/ThirdParty/tgcalls      | 2 +-
 Telegram/cmake/lib_tgcalls.cmake | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index 2e2797648..258cf9b35 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit 2e2797648aac2588e7fe479c2e8b4455ec65c5e6
+Subproject commit 258cf9b352ee2afde7e08e431f5887cd13119931
diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake
index 94cfdb856..8d730b142 100644
--- a/Telegram/cmake/lib_tgcalls.cmake
+++ b/Telegram/cmake/lib_tgcalls.cmake
@@ -28,6 +28,7 @@ PRIVATE
     CodecSelectHelper.h
     CryptoHelper.cpp
     CryptoHelper.h
+    DirectConnectionChannel.h
     EncryptedConnection.cpp
     EncryptedConnection.h
     FakeAudioDeviceModule.cpp
@@ -64,8 +65,11 @@ PRIVATE
 
     v2/ContentNegotiation.cpp
     v2/ContentNegotiation.h
+    v2/DirectNetworkingImpl.cpp
+    v2/DirectNetworkingImpl.h
     v2/ExternalSignalingConnection.cpp
     v2/ExternalSignalingConnection.h
+    v2/InstanceNetworking.h
     v2/InstanceV2ReferenceImpl.cpp
     v2/InstanceV2ReferenceImpl.h
     v2/InstanceV2Impl.cpp

From f31b40f6cece7cd212b0a6e8afe4bfc007fd3902 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 20:10:15 +0400
Subject: [PATCH 199/259] Add stories outline to group participants list.

---
 Telegram/SourceFiles/boxes/peer_list_box.cpp  |   6 +-
 .../boxes/peer_list_controllers.cpp           | 272 ++++++++++--------
 .../SourceFiles/boxes/peer_list_controllers.h |  61 +++-
 .../boxes/peers/edit_participants_box.cpp     |  55 +++-
 .../boxes/peers/edit_participants_box.h       |   8 +
 Telegram/SourceFiles/data/data_changes.h      |  29 +-
 Telegram/SourceFiles/data/data_user.cpp       |   1 +
 Telegram/SourceFiles/info/info.style          |   9 +
 .../info/profile/info_profile_members.cpp     |   2 +
 9 files changed, 287 insertions(+), 156 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index bbfb2acb1..6f27553d0 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -889,9 +889,11 @@ void PeerListRow::createCheckbox(
 }
 
 void PeerListRow::setCheckedInternal(bool checked, anim::type animated) {
-	Expects(_checkbox != nullptr);
+	Expects(!checked || _checkbox != nullptr);
 
-	_checkbox->setChecked(checked, animated);
+	if (_checkbox) {
+		_checkbox->setChecked(checked, animated);
+	}
 }
 
 void PeerListRow::setCustomizedCheckSegments(
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 88f0fac9a..5831ec0b5 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -54,40 +54,6 @@ namespace {
 constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
 constexpr auto kSearchPerPage = 50;
 
-[[nodiscard]] std::vector<Ui::OutlineSegment> PrepareSegments(
-		int count,
-		int unread,
-		const QBrush &unreadBrush) {
-	Expects(unread <= count);
-	Expects(count > 0);
-
-	auto result = std::vector<Ui::OutlineSegment>();
-	const auto add = [&](bool unread) {
-		result.push_back({
-			.brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b,
-			.width = (unread
-				? st::dialogsStoriesFull.lineTwice / 2.
-				: st::dialogsStoriesFull.lineReadTwice / 2.),
-		});
-	};
-	result.reserve(count);
-	for (auto i = 0, till = count - unread; i != till; ++i) {
-		add(false);
-	}
-	for (auto i = 0; i != unread; ++i) {
-		add(true);
-	}
-	return result;
-}
-
-[[nodiscard]] QBrush CreateStoriesGradient() {
-	const auto &st = st::contactsWithStories.item;
-	const auto left = st.photoPosition.x();
-	const auto top = st.photoPosition.y();
-	const auto size = st.photoSize;
-	return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size));
-}
-
 } // namespace
 
 object_ptr<Ui::BoxContent> PrepareContactsBox(
@@ -124,6 +90,39 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
 	return Box<PeerListBox>(std::move(controller), std::move(init));
 }
 
+QBrush PeerListStoriesGradient(const style::PeerList &st) {
+	const auto left = st.item.photoPosition.x();
+	const auto top = st.item.photoPosition.y();
+	const auto size = st.item.photoSize;
+	return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size));
+}
+
+std::vector<Ui::OutlineSegment> PeerListStoriesSegments(
+		int count,
+		int unread,
+		const QBrush &unreadBrush) {
+	Expects(unread <= count);
+	Expects(count > 0);
+
+	auto result = std::vector<Ui::OutlineSegment>();
+	const auto add = [&](bool unread) {
+		result.push_back({
+			.brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b,
+			.width = (unread
+				? st::dialogsStoriesFull.lineTwice / 2.
+				: st::dialogsStoriesFull.lineReadTwice / 2.),
+		});
+	};
+	result.reserve(count);
+	for (auto i = 0, till = count - unread; i != till; ++i) {
+		add(false);
+	}
+	for (auto i = 0; i != unread; ++i) {
+		add(true);
+	}
+	return result;
+}
+
 void PeerListRowWithLink::setActionLink(const QString &action) {
 	_action = action;
 	refreshActionLink();
@@ -359,6 +358,115 @@ bool ChatsListBoxController::appendRow(not_null<History*> history) {
 	return false;
 }
 
+PeerListStories::PeerListStories(
+	not_null<PeerListController*> controller,
+	not_null<Main::Session*> session)
+: _controller(controller)
+, _session(session) {
+}
+
+void PeerListStories::updateColors() {
+	for (auto i = begin(_counts); i != end(_counts); ++i) {
+		if (const auto row = _delegate->peerListFindRow(i->first)) {
+			if (i->second.count >= 0 && i->second.unread >= 0) {
+				applyForRow(row, i->second.count, i->second.unread, true);
+			}
+		}
+	}
+}
+
+void PeerListStories::updateFor(
+		uint64 id,
+		int count,
+		int unread) {
+	if (const auto row = _delegate->peerListFindRow(id)) {
+		applyForRow(row, count, unread);
+		_delegate->peerListUpdateRow(row);
+	}
+}
+
+void PeerListStories::process(not_null<PeerListRow*> row) {
+	const auto user = row->peer()->asUser();
+	if (!user) {
+		return;
+	}
+	const auto stories = &_session->data().stories();
+	const auto source = stories->source(user->id);
+	const auto count = source
+		? int(source->ids.size())
+		: user->hasActiveStories()
+		? 1
+		: 0;
+	const auto unread = source
+		? source->info().unreadCount
+		: user->hasUnreadStories()
+		? 1
+		: 0;
+	applyForRow(row, count, unread, true);
+}
+
+bool PeerListStories::handleClick(not_null<PeerData*> peer) {
+	const auto point = _delegate->peerListLastRowMousePosition();
+	const auto &st = _controller->listSt()->item;
+	if (point && point->x() < st.photoPosition.x() + st.photoSize) {
+		if (const auto window = peer->session().tryResolveWindow()) {
+			if (const auto user = peer->asUser()) {
+				if (user->hasActiveStories()) {
+					window->openPeerStories(peer->id);
+					return true;
+				}
+			}
+		}
+	}
+	return false;
+}
+
+void PeerListStories::prepare(not_null<PeerListDelegate*> delegate) {
+	_delegate = delegate;
+
+	_unreadBrush = PeerListStoriesGradient(*_controller->listSt());
+	style::PaletteChanged() | rpl::start_with_next([=] {
+		_unreadBrush = PeerListStoriesGradient(*_controller->listSt());
+		updateColors();
+	}, _lifetime);
+
+	_session->changes().peerUpdates(
+		Data::PeerUpdate::Flag::StoriesState
+	) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
+		const auto id = update.peer->id.value;
+		if (const auto row = _delegate->peerListFindRow(id)) {
+			process(row);
+		}
+	}, _lifetime);
+
+	const auto stories = &_session->data().stories();
+	stories->sourceChanged() | rpl::start_with_next([=](PeerId id) {
+		const auto source = stories->source(id);
+		const auto info = source
+			? source->info()
+			: Data::StoriesSourceInfo();
+		updateFor(id.value, info.count, info.unreadCount);
+	}, _lifetime);
+}
+
+void PeerListStories::applyForRow(
+		not_null<PeerListRow*> row,
+		int count,
+		int unread,
+		bool force) {
+	auto &counts = _counts[row->id()];
+	if (!force && counts.count == count && counts.unread == unread) {
+		return;
+	}
+	counts.count = count;
+	counts.unread = unread;
+	_delegate->peerListSetRowChecked(row, count > 0);
+	if (count > 0) {
+		row->setCustomizedCheckSegments(
+			PeerListStoriesSegments(count, unread, _unreadBrush));
+	}
+}
+
 ContactsBoxController::ContactsBoxController(
 	not_null<Main::Session*> session)
 : ContactsBoxController(
@@ -385,31 +493,8 @@ void ContactsBoxController::prepare() {
 
 	prepareViewHook();
 
-	if (_storiesShown) {
-		_storiesUnread = CreateStoriesGradient();
-		style::PaletteChanged() | rpl::start_with_next([=] {
-			_storiesUnread = CreateStoriesGradient();
-			for (auto &entry : _storiesCounts) {
-				entry.second.count = entry.second.unread = -1;
-			}
-			updateStories();
-		}, lifetime());
-
-		const auto stories = &session().data().stories();
-		rpl::merge(
-			rpl::single(rpl::empty),
-			stories->sourcesChanged(Data::StorySourcesList::NotHidden),
-			stories->sourcesChanged(Data::StorySourcesList::Hidden)
-		) | rpl::start_with_next([=] {
-			updateStories();
-		}, lifetime());
-		stories->sourceChanged() | rpl::start_with_next([=](PeerId id) {
-			const auto source = stories->source(id);
-			const auto info = source
-				? source->info()
-				: Data::StoriesSourceInfo();
-			updateStoriesFor(id.value, info.count, info.unreadCount);
-		}, lifetime());
+	if (_stories) {
+		_stories->prepare(delegate());
 	}
 
 	session().data().contactsLoaded().value(
@@ -456,16 +541,10 @@ std::unique_ptr<PeerListRow> ContactsBoxController::createSearchRow(
 
 void ContactsBoxController::rowClicked(not_null<PeerListRow*> row) {
 	const auto peer = row->peer();
-	if (const auto window = peer->session().tryResolveWindow()) {
-		if (_storiesShown) {
-			const auto point = delegate()->peerListLastRowMousePosition();
-			const auto &st = st::contactsWithStories.item;
-			if (point && point->x() < st.photoPosition.x() + st.photoSize) {
-				window->openPeerStories(peer->id);
-				return;
-			}
-		}
-		window->showPeerHistory(row->peer());
+	if (_stories && _stories->handleClick(peer)) {
+		return;
+	} else if (const auto window = peer->session().tryResolveWindow()) {
+		window->showPeerHistory(peer);
 	}
 }
 
@@ -491,52 +570,7 @@ void ContactsBoxController::setSortMode(SortMode mode) {
 }
 
 void ContactsBoxController::setStoriesShown(bool shown) {
-	_storiesShown = shown;
-}
-
-void ContactsBoxController::updateStories() {
-	const auto stories = &_session->data().stories();
-	const auto &a = stories->sources(Data::StorySourcesList::NotHidden);
-	const auto &b = stories->sources(Data::StorySourcesList::Hidden);
-	auto checked = base::flat_set<PeerListRowId>();
-	for (const auto &info : ranges::views::concat(a, b)) {
-		const auto id = info.id.value;
-		checked.emplace(id);
-		updateStoriesFor(id, info.count, info.unreadCount);
-	}
-	for (auto i = begin(_storiesCounts); i != end(_storiesCounts); ++i) {
-		if (i->second.count && !checked.contains(i->first)) {
-			updateStoriesFor(i->first, 0, 0);
-		}
-	}
-}
-
-void ContactsBoxController::updateStoriesFor(
-		uint64 id,
-		int count,
-		int unread) {
-	if (const auto row = delegate()->peerListFindRow(id)) {
-		applyRowStories(row, count, unread);
-		delegate()->peerListUpdateRow(row);
-	}
-}
-
-void ContactsBoxController::applyRowStories(
-		not_null<PeerListRow*> row,
-		int count,
-		int unread,
-		bool force) {
-	auto &counts = _storiesCounts[row->id()];
-	if (!force && counts.count == count && counts.unread == unread) {
-		return;
-	}
-	counts.count = count;
-	counts.unread = unread;
-	delegate()->peerListSetRowChecked(row, count > 0);
-	if (count > 0) {
-		row->setCustomizedCheckSegments(
-			PrepareSegments(count, unread, _storiesUnread));
-	}
+	_stories = std::make_unique<PeerListStories>(this, _session);
 }
 
 void ContactsBoxController::sort() {
@@ -586,12 +620,8 @@ bool ContactsBoxController::appendRow(not_null<UserData*> user) {
 	if (auto row = createRow(user)) {
 		const auto raw = row.get();
 		delegate()->peerListAppendRow(std::move(row));
-		if (_storiesShown) {
-			const auto stories = &session().data().stories();
-			if (const auto source = stories->source(user->id)) {
-				const auto info = source->info();
-				applyRowStories(raw, info.count, info.unreadCount, true);
-			}
+		if (_stories) {
+			_stories->process(raw);
 		}
 		return true;
 	}
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h
index 46d1a184c..0ba108f01 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.h
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h
@@ -20,12 +20,21 @@ class Forum;
 class ForumTopic;
 } // namespace Data
 
+namespace Ui {
+struct OutlineSegment;
+} // namespace Ui
+
 namespace Window {
 class SessionController;
 } // namespace Window
 
 [[nodiscard]] object_ptr<Ui::BoxContent> PrepareContactsBox(
 	not_null<Window::SessionController*> sessionController);
+[[nodiscard]] QBrush PeerListStoriesGradient(const style::PeerList &st);
+[[nodiscard]] std::vector<Ui::OutlineSegment> PeerListStoriesSegments(
+	int count,
+	int unread,
+	const QBrush &unreadBrush);
 
 class PeerListRowWithLink : public PeerListRow {
 public:
@@ -116,6 +125,41 @@ private:
 
 };
 
+class PeerListStories final {
+public:
+	PeerListStories(
+		not_null<PeerListController*> controller,
+		not_null<Main::Session*> session);
+
+	void prepare(not_null<PeerListDelegate*> delegate);
+
+	void process(not_null<PeerListRow*> row);
+	bool handleClick(not_null<PeerData*> peer);
+
+private:
+	struct Counts {
+		int count = 0;
+		int unread = 0;
+	};
+
+	void updateColors();
+	void updateFor(uint64 id, int count, int unread);
+	void applyForRow(
+		not_null<PeerListRow*> row,
+		int count,
+		int unread,
+		bool force = false);
+
+	const not_null<PeerListController*> _controller;
+	const not_null<Main::Session*> _session;
+	PeerListDelegate *_delegate = nullptr;
+
+	QBrush _unreadBrush;
+	base::flat_map<uint64, Counts> _counts;
+	rpl::lifetime _lifetime;
+
+};
+
 class ContactsBoxController : public PeerListController {
 public:
 	explicit ContactsBoxController(not_null<Main::Session*> session);
@@ -129,7 +173,7 @@ public:
 		not_null<PeerData*> peer) override final;
 	void rowClicked(not_null<PeerListRow*> row) override;
 	bool trackSelectedList() override {
-		return !_storiesShown;
+		return !_stories;
 	}
 
 	enum class SortMode {
@@ -147,32 +191,19 @@ protected:
 	}
 
 private:
-	struct StoriesCount {
-		int count = 0;
-		int unread = 0;
-	};
 	void sort();
 	void sortByName();
 	void sortByOnline();
 	void rebuildRows();
-	void updateStories();
 	void checkForEmptyRows();
 	bool appendRow(not_null<UserData*> user);
-	void updateStoriesFor(uint64 id, int count, int unread);
-	void applyRowStories(
-		not_null<PeerListRow*> row,
-		int count,
-		int unread,
-		bool force = false);
 
 	const not_null<Main::Session*> _session;
 	SortMode _sortMode = SortMode::Alphabet;
 	base::Timer _sortByOnlineTimer;
 	rpl::lifetime _sortByOnlineLifetime;
 
-	QBrush _storiesUnread;
-	base::flat_map<uint64, StoriesCount> _storiesCounts;
-	bool _storiesShown = false;
+	std::unique_ptr<PeerListStories> _stories;
 
 };
 
diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
index 94caec150..c35fc9fbb 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
@@ -25,11 +25,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/dialogs_indexed_list.h"
 #include "data/data_peer_values.h"
 #include "data/data_session.h"
+#include "data/data_stories.h"
 #include "data/data_channel.h"
 #include "data/data_chat.h"
 #include "data/data_user.h"
 #include "data/data_changes.h"
 #include "base/unixtime.h"
+#include "ui/effects/outline_segments.h"
 #include "ui/widgets/popup_menu.h"
 #include "ui/ui_utility.h"
 #include "info/profile/info_profile_values.h"
@@ -900,7 +902,11 @@ void ParticipantsBoxController::setupListChangeViewers() {
 				return (row.peer() == user);
 			});
 		} else if (auto row = createRow(user)) {
+			const auto raw = row.get();
 			delegate()->peerListPrependRow(std::move(row));
+			if (_stories) {
+				_stories->process(raw);
+			}
 			refreshRows();
 			if (_onlineSorter) {
 				_onlineSorter->sort();
@@ -1160,8 +1166,14 @@ void ParticipantsBoxController::restoreState(
 			loadMoreRows();
 		}
 		PeerListController::restoreState(std::move(state));
-		if (delegate()->peerListFullRowsCount() > 0 || _allLoaded) {
+		const auto count = delegate()->peerListFullRowsCount();
+		if (count > 0 || _allLoaded) {
 			refreshDescription();
+			if (_stories) {
+				for (auto i = 0; i != count; ++i) {
+					_stories->process(delegate()->peerListRowAt(i));
+				}
+			}
 		}
 		if (_onlineSorter) {
 			_onlineSorter->sort();
@@ -1177,14 +1189,21 @@ rpl::producer<int> ParticipantsBoxController::fullCountValue() const {
 	return _fullCountValue.value();
 }
 
+void ParticipantsBoxController::setStoriesShown(bool shown) {
+	_stories = std::make_unique<PeerListStories>(
+		this,
+		&_navigation->session());
+}
+
 void ParticipantsBoxController::prepare() {
 	auto title = [&] {
 		switch (_role) {
 		case Role::Admins: return tr::lng_channel_admins();
 		case Role::Profile:
-		case Role::Members: return (_peer->isChannel() && !_peer->isMegagroup()
-			? tr::lng_profile_subscribers_section()
-			: tr::lng_profile_participants_section());
+		case Role::Members:
+			return ((_peer->isChannel() && !_peer->isMegagroup())
+				? tr::lng_profile_subscribers_section()
+				: tr::lng_profile_participants_section());
 		case Role::Restricted: return tr::lng_exceptions_list_title();
 		case Role::Kicked: return tr::lng_removed_list_title();
 		}
@@ -1207,6 +1226,10 @@ void ParticipantsBoxController::prepare() {
 	setDescriptionText(tr::lng_contacts_loading(tr::now));
 	setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
 
+	if (_stories) {
+		_stories->prepare(delegate());
+	}
+
 	if (_role == Role::Profile) {
 		auto visible = _peer->isMegagroup()
 			? Info::Profile::CanViewParticipantsValue(_peer->asMegagroup())
@@ -1319,7 +1342,11 @@ void ParticipantsBoxController::rebuildChatParticipants(
 	}
 	for (const auto &user : participants) {
 		if (auto row = createRow(user)) {
+			const auto raw = row.get();
 			delegate()->peerListAppendRow(std::move(row));
+			if (_stories) {
+				_stories->process(raw);
+			}
 		}
 	}
 	_onlineSorter->sort();
@@ -1373,7 +1400,11 @@ void ParticipantsBoxController::rebuildChatAdmins(
 	}
 	for (const auto user : list) {
 		if (auto row = createRow(user)) {
+			const auto raw = row.get();
 			delegate()->peerListAppendRow(std::move(row));
+			if (_stories) {
+				_stories->process(raw);
+			}
 		}
 	}
 
@@ -1543,6 +1574,11 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() {
 void ParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {
 	const auto participant = row->peer();
 	const auto user = participant->asUser();
+
+	if (_stories && _stories->handleClick(participant)) {
+		return;
+	}
+
 	if (_role == Role::Admins) {
 		Assert(user != nullptr);
 		showAdmin(user);
@@ -1890,7 +1926,11 @@ bool ParticipantsBoxController::appendRow(not_null<PeerData*> participant) {
 		recomputeTypeFor(participant);
 		return false;
 	} else if (auto row = createRow(participant)) {
+		const auto raw = row.get();
 		delegate()->peerListAppendRow(std::move(row));
+		if (_stories) {
+			_stories->process(raw);
+		}
 		if (_role != Role::Kicked) {
 			setDescriptionText(QString());
 		}
@@ -1906,10 +1946,17 @@ bool ParticipantsBoxController::prependRow(not_null<PeerData*> participant) {
 		if (_role == Role::Admins) {
 			// Perhaps we've added a new admin from search.
 			delegate()->peerListPrependRowFromSearchResult(row);
+			if (_stories) {
+				_stories->process(row);
+			}
 		}
 		return false;
 	} else if (auto row = createRow(participant)) {
+		const auto raw = row.get();
 		delegate()->peerListPrependRow(std::move(row));
+		if (_stories) {
+			_stories->process(raw);
+		}
 		if (_role != Role::Kicked) {
 			setDescriptionText(QString());
 		}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h
index 30b445813..0882f214a 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h
+++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/weak_ptr.h"
 #include "info/profile/info_profile_members_controllers.h"
 
+class PeerListStories;
 struct ChatAdminRightsInfo;
 struct ChatRestrictionsInfo;
 
@@ -174,6 +175,9 @@ public:
 		QWidget *parent,
 		not_null<PeerListRow*> row) override;
 	void loadMoreRows() override;
+	bool trackSelectedList() override {
+		return !_stories;
+	}
 
 	void peerListSearchAddRow(not_null<PeerData*> peer) override;
 	std::unique_ptr<PeerListRow> createSearchRow(
@@ -187,6 +191,8 @@ public:
 	[[nodiscard]] rpl::producer<int> onlineCountValue() const;
 	[[nodiscard]] rpl::producer<int> fullCountValue() const;
 
+	void setStoriesShown(bool shown);
+
 protected:
 	// Allow child controllers not providing navigation.
 	// This is their responsibility to override all methods that use it.
@@ -288,6 +294,8 @@ private:
 	Ui::BoxPointer _addBox;
 	QPointer<Ui::BoxContent> _editParticipantBox;
 
+	std::unique_ptr<PeerListStories> _stories;
+
 };
 
 // Members, banned and restricted users server side search.
diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h
index 3f0e587dd..d4fee22c8 100644
--- a/Telegram/SourceFiles/data/data_changes.h
+++ b/Telegram/SourceFiles/data/data_changes.h
@@ -85,26 +85,27 @@ struct PeerUpdate {
 		SupportInfo         = (1ULL << 23),
 		IsBot               = (1ULL << 24),
 		EmojiStatus         = (1ULL << 25),
+		StoriesState        = (1ULL << 26),
 
 		// For chats and channels
-		InviteLinks         = (1ULL << 26),
-		Members             = (1ULL << 27),
-		Admins              = (1ULL << 28),
-		BannedUsers         = (1ULL << 29),
-		Rights              = (1ULL << 30),
-		PendingRequests     = (1ULL << 31),
-		Reactions           = (1ULL << 32),
+		InviteLinks         = (1ULL << 27),
+		Members             = (1ULL << 28),
+		Admins              = (1ULL << 29),
+		BannedUsers         = (1ULL << 30),
+		Rights              = (1ULL << 31),
+		PendingRequests     = (1ULL << 32),
+		Reactions           = (1ULL << 33),
 
 		// For channels
-		ChannelAmIn         = (1ULL << 33),
-		StickersSet         = (1ULL << 34),
-		ChannelLinkedChat   = (1ULL << 35),
-		ChannelLocation     = (1ULL << 36),
-		Slowmode            = (1ULL << 37),
-		GroupCall           = (1ULL << 38),
+		ChannelAmIn         = (1ULL << 34),
+		StickersSet         = (1ULL << 35),
+		ChannelLinkedChat   = (1ULL << 36),
+		ChannelLocation     = (1ULL << 37),
+		Slowmode            = (1ULL << 38),
+		GroupCall           = (1ULL << 39),
 
 		// For iteration
-		LastUsedBit         = (1ULL << 38),
+		LastUsedBit         = (1ULL << 39),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp
index d25f37780..09c66a922 100644
--- a/Telegram/SourceFiles/data/data_user.cpp
+++ b/Telegram/SourceFiles/data/data_user.cpp
@@ -155,6 +155,7 @@ void UserData::setStoriesState(StoriesState state) {
 		if (const auto history = owner().historyLoaded(this)) {
 			history->updateChatListEntryPostponed();
 		}
+		session().changes().peerUpdated(this, UpdateFlag::StoriesState);
 	}
 }
 
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index eb746c3c5..c5567dd79 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -488,6 +488,15 @@ infoMembersList: PeerList(defaultPeerList) {
 		photoPosition: point(18px, 6px);
 		namePosition: point(79px, 11px);
 		statusPosition: point(79px, 31px);
+		checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
+			selectExtendTwice: 1px;
+			imageRadius: 21px;
+			imageSmallRadius: 19px;
+			check: RoundCheckbox(defaultPeerListCheck) {
+				size: 0px;
+			}
+		}
+		nameFgChecked: contactsNameFg;
 	}
 }
 infoMembersButtonPosition: point(12px, 0px);
diff --git a/Telegram/SourceFiles/info/profile/info_profile_members.cpp b/Telegram/SourceFiles/info/profile/info_profile_members.cpp
index 605ebbad5..b226935c3 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_members.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_members.cpp
@@ -50,6 +50,7 @@ Members::Members(
 , _controller(controller)
 , _peer(_controller->key().peer())
 , _listController(CreateMembersController(controller, _peer)) {
+	_listController->setStoriesShown(true);
 	setupHeader();
 	setupList();
 	setContent(_list.data());
@@ -232,6 +233,7 @@ void Members::setupButtons() {
 void Members::setupList() {
 	auto topSkip = _header ? _header->height() : 0;
 	_listController->setStyleOverrides(&st::infoMembersList);
+	_listController->setStoriesShown(true);
 	_list = object_ptr<ListWidget>(
 		this,
 		_listController.get());

From b630e48a77dbf6f37b1b1fc01577adbf3b9bdd2d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 20:45:19 +0400
Subject: [PATCH 200/259] Improve chats list filter border.

---
 Telegram/SourceFiles/dialogs/dialogs.style | 3 ++-
 Telegram/lib_ui                            | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index 69849adc8..c824b8219 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -266,9 +266,10 @@ dialogsFilter: InputField(defaultInputField) {
 	borderFgActive: windowBgRipple;
 	borderFgError: activeLineFgError;
 
-	border: 2px;
+	border: 3px;
 	borderActive: 2px;
 	borderRadius: 18px;
+	borderDenominator: 2;
 
 	font: normalFont;
 
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 1c0889f78..048156ecd 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 1c0889f78a74bb29e8cea6986ea8b18e7add7fb0
+Subproject commit 048156ecda89b54ea48aafd5b0f97ccffcc922c9

From 961dd2a4a8a2e9827fda6cb7ff0d7916da265c3b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 22:27:12 +0400
Subject: [PATCH 201/259] Break large stories lists by days in viewer.

---
 .../stories/media_stories_controller.cpp      | 52 ++++++++++++++++++-
 .../media/stories/media_stories_controller.h  |  2 +
 .../media/stories/media_stories_header.cpp    | 20 +++++--
 .../media/stories/media_stories_header.h      |  2 +
 .../SourceFiles/media/view/media_view.style   |  2 +-
 5 files changed, 73 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index bd6c19b64..2bdb19098 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 "base/qt_signal_producer.h"
+#include "base/unixtime.h"
 #include "boxes/peers/prepare_short_info_box.h"
 #include "chat_helpers/compose/compose_show.h"
 #include "core/application.h"
@@ -75,6 +76,42 @@ constexpr auto kPreloadPreviousMediaCount = 1;
 constexpr auto kMarkAsReadAfterSeconds = 0.2;
 constexpr auto kMarkAsReadAfterProgress = 0.;
 
+struct SameDayRange {
+	int from = 0;
+	int till = 0;
+};
+[[nodiscard]] SameDayRange ComputeSameDayRange(
+		not_null<Data::Story*> story,
+		const Data::StoriesIds &ids,
+		int index) {
+	Expects(index >= 0 && index < ids.list.size());
+
+	auto result = SameDayRange{ .from = index, .till = index };
+	const auto peerId = story->peer()->id;
+	const auto stories = &story->owner().stories();
+	const auto now = base::unixtime::parse(story->date());
+	const auto b = begin(ids.list);
+	for (auto i = b + index; i != b;) {
+		if (const auto maybeStory = stories->lookup({ peerId, *--i })) {
+			const auto day = base::unixtime::parse((*maybeStory)->date());
+			if (day.date() != now.date()) {
+				break;
+			}
+		}
+		--result.from;
+	}
+	for (auto i = b + index + 1, e = end(ids.list); i != e; ++i) {
+		if (const auto maybeStory = stories->lookup({ peerId, *i })) {
+			const auto day = base::unixtime::parse((*maybeStory)->date());
+			if (day.date() != now.date()) {
+				break;
+			}
+		}
+		++result.till;
+	}
+	return result;
+}
+
 } // namespace
 
 class Controller::PhotoPlayback final {
@@ -629,11 +666,19 @@ void Controller::rebuildFromContext(
 			}
 		}
 	});
+	_sliderIndex = 0;
+	_sliderCount = 0;
 	if (list) {
 		_source = std::nullopt;
 		if (_list != list) {
 			_list = std::move(list);
 		}
+		if (const auto maybe = user->owner().stories().lookup(storyId)) {
+			const auto now = *maybe;
+			const auto range = ComputeSameDayRange(now, _list->ids, _index);
+			_sliderCount = range.till - range.from + 1;
+			_sliderIndex = _index - range.from;
+		}
 	} else {
 		if (source) {
 			const auto i = source->ids.lower_bound(StoryIdDates{ id });
@@ -659,7 +704,10 @@ void Controller::rebuildFromContext(
 		}
 	}
 	preloadNext();
-	_slider->show({ .index = _index, .total = shownCount() });
+	_slider->show({
+		.index = _sliderCount ? _sliderIndex : _index,
+		.total = _sliderCount ? _sliderCount : shownCount(),
+	});
 }
 
 void Controller::preloadNext() {
@@ -750,6 +798,8 @@ void Controller::show(
 	_header->show({
 		.user = user,
 		.date = story->date(),
+		.fullIndex = _sliderCount ? _index : 0,
+		.fullCount = _sliderCount ? shownCount() : 0,
 		.edited = story->edited(),
 	});
 	if (!changeShown(story)) {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 37d7b60fb..0df907acd 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -241,6 +241,8 @@ private:
 	FullStoryId _waitingForId;
 	int _waitingForDelta = 0;
 	int _index = 0;
+	int _sliderIndex = 0;
+	int _sliderCount = 0;
 	bool _started = false;
 	bool _viewed = false;
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index fd8f21b33..ab2a842e9 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/controls/userpic_button.h"
 #include "ui/layers/box_content.h"
 #include "ui/text/format_values.h"
+#include "ui/text/text_utilities.h"
 #include "ui/widgets/labels.h"
 #include "ui/painter.h"
 #include "ui/rp_widget.h"
@@ -75,6 +76,16 @@ struct Timestamp {
 	return { Ui::FormatDateTime(whenFull) };
 }
 
+[[nodiscard]] TextWithEntities ComposeName(HeaderData data) {
+	auto result = Ui::Text::Bold(data.user->shortName());
+	if (data.fullCount) {
+		result.append(QString::fromUtf8(" \xE2\x80\xA2 %1/%2"
+		).arg(data.fullIndex + 1
+		).arg(data.fullCount));
+	}
+	return result;
+}
+
 [[nodiscard]] Timestamp ComposeDetails(HeaderData data, TimeId now) {
 	auto result = ComposeTimestamp(data.date, now);
 	if (data.edited) {
@@ -98,9 +109,12 @@ void Header::show(HeaderData data) {
 	if (_data == data) {
 		return;
 	}
-	const auto userChanged = (!_data || _data->user != data.user);
+	const auto nameDataChanged = !_data
+		|| (_data->user != data.user)
+		|| (_data->fullCount != data.fullCount)
+		|| (data.fullCount && _data->fullIndex != data.fullIndex);
 	_data = data;
-	if (userChanged) {
+	if (nameDataChanged) {
 		_date = nullptr;
 		const auto parent = _controller->wrap();
 		auto widget = std::make_unique<Ui::AbstractButton>(parent);
@@ -119,7 +133,7 @@ void Header::show(HeaderData data) {
 			st::storiesHeaderMargin.top());
 		const auto name = Ui::CreateChild<Ui::FlatLabel>(
 			raw,
-			data.user->firstName,
+			rpl::single(ComposeName(data)),
 			st::storiesHeaderName);
 		name->setAttribute(Qt::WA_TransparentForMouseEvents);
 		name->setOpacity(kNameOpacity);
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index cab943464..1393eefd5 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -22,6 +22,8 @@ class Controller;
 struct HeaderData {
 	not_null<UserData*> user;
 	TimeId date = 0;
+	int fullIndex = 0;
+	int fullCount = 0;
 	bool edited = false;
 
 	friend inline auto operator<=>(HeaderData, HeaderData) = default;
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index 00db429d1..ac5d9e062 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -426,7 +426,7 @@ storiesHeaderPhoto: UserpicButton(defaultUserpicButton) {
 }
 storiesHeaderName: FlatLabel(defaultFlatLabel) {
 	textFg: mediaviewControlFg;
-	style: semiboldTextStyle;
+	style: defaultTextStyle;
 }
 storiesHeaderNamePosition: point(50px, 0px);
 storiesHeaderDate: FlatLabel(defaultFlatLabel) {

From 4e046ca31abd87d465873f46cece7851223690c3 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 22:36:11 +0400
Subject: [PATCH 202/259] Closed alpha version 4.8.4.5.

---
 Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +-
 Telegram/Resources/winrc/Telegram.rc         | 8 ++++----
 Telegram/Resources/winrc/Updater.rc          | 8 ++++----
 Telegram/SourceFiles/core/version.h          | 2 +-
 Telegram/build/version                       | 4 ++--
 5 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 1553b95ad..e2e0f8a2d 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.4.4" />
+    Version="4.8.4.5" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 1c0a03179..6d78ed1c8 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,4
- PRODUCTVERSION 4,8,4,4
+ FILEVERSION 4,8,4,5
+ PRODUCTVERSION 4,8,4,5
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.4.4"
+            VALUE "FileVersion", "4.8.4.5"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.4"
+            VALUE "ProductVersion", "4.8.4.5"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 87a38fb0b..3d37a211c 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,4
- PRODUCTVERSION 4,8,4,4
+ FILEVERSION 4,8,4,5
+ PRODUCTVERSION 4,8,4,5
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.4.4"
+            VALUE "FileVersion", "4.8.4.5"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.4"
+            VALUE "ProductVersion", "4.8.4.5"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 79690f889..8f9e79acb 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/const_string.h"
 
-#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004004ULL)
+#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004005ULL)
 
 #ifdef TDESKTOP_ALLOW_CLOSED_ALPHA
 #define TDESKTOP_ALPHA_VERSION TDESKTOP_REQUESTED_ALPHA_VERSION
diff --git a/Telegram/build/version b/Telegram/build/version
index 8b47580b7..659b269d5 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -3,5 +3,5 @@ AppVersionStrMajor 4.8
 AppVersionStrSmall 4.8.4
 AppVersionStr      4.8.4
 BetaChannel        0
-AlphaVersion       4008004004
-AppVersionOriginal 4.8.4.4
+AlphaVersion       4008004005
+AppVersionOriginal 4.8.4.5

From 04a969cc5fd54919b27c566439834099937498c5 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 18 Jul 2023 22:38:28 +0400
Subject: [PATCH 203/259] Fix calls peer-to-peer privacy button icon.

---
 .../SourceFiles/settings/settings_privacy_controllers.cpp    | 3 ++-
 Telegram/SourceFiles/settings/settings_privacy_security.cpp  | 5 +++--
 Telegram/SourceFiles/settings/settings_privacy_security.h    | 3 ++-
 3 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
index 5fbb5b4c3..349741dfb 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
@@ -764,7 +764,8 @@ object_ptr<Ui::RpWidget> CallsPrivacyController::setupBelowWidget(
 		tr::lng_settings_calls_peer_to_peer_button(),
 		{ &st::settingsIconArrows, kIconLightBlue },
 		UserPrivacy::Key::CallsPeer2Peer,
-		[] { return std::make_unique<CallsPeer2PeerPrivacyController>(); });
+		[] { return std::make_unique<CallsPeer2PeerPrivacyController>(); },
+		&st::settingsButton);
 	AddSkip(content);
 
 	return result;
diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
index 7554dba27..d9a1a97c3 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
@@ -814,14 +814,15 @@ void AddPrivacyButton(
 		rpl::producer<QString> label,
 		IconDescriptor &&descriptor,
 		Privacy::Key key,
-		Fn<std::unique_ptr<EditPrivacyController>()> controllerFactory) {
+		Fn<std::unique_ptr<EditPrivacyController>()> controllerFactory,
+		const style::SettingsButton *stOverride) {
 	const auto shower = Ui::CreateChild<rpl::lifetime>(container.get());
 	const auto session = &controller->session();
 	AddButtonWithLabel(
 		container,
 		std::move(label),
 		PrivacyString(session, key),
-		st::settingsButtonNoIcon,
+		stOverride ? *stOverride : st::settingsButtonNoIcon,
 		std::move(descriptor)
 	)->addClickHandler([=] {
 		*shower = session->api().userPrivacy().value(
diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.h b/Telegram/SourceFiles/settings/settings_privacy_security.h
index 67b2fcc64..350d6f1b5 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_security.h
+++ b/Telegram/SourceFiles/settings/settings_privacy_security.h
@@ -32,7 +32,8 @@ void AddPrivacyButton(
 	rpl::producer<QString> label,
 	IconDescriptor &&descriptor,
 	Api::UserPrivacy::Key key,
-	Fn<std::unique_ptr<EditPrivacyController>()> controllerFactory);
+	Fn<std::unique_ptr<EditPrivacyController>()> controllerFactory,
+	const style::SettingsButton *stOverride = nullptr);
 
 class PrivacySecurity : public Section<PrivacySecurity> {
 public:

From 585bbd45f407f73c5ccdfca42219eda0743cb8d6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 19 Jul 2023 11:02:35 +0400
Subject: [PATCH 204/259] Fix undesired lock/unlock button visibility.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index c25b31e5d..0a7c92a52 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -240,6 +240,7 @@ Widget::Widget(
 			_childListShown.value(),
 			makeChildListShown)));
 	_scrollToTop->raise();
+	_lockUnlock->toggle(false, anim::type::instant);
 
 	_inner->updated(
 	) | rpl::start_with_next([=] {

From ffd691e5565241b530c9a11f4e4649011184c49d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 19 Jul 2023 12:30:34 +0400
Subject: [PATCH 205/259] Update API scheme, pass privacy info to viewer.

---
 Telegram/SourceFiles/data/data_document.cpp   |  8 ++++
 Telegram/SourceFiles/data/data_document.h     |  1 +
 Telegram/SourceFiles/data/data_story.cpp      | 39 ++++++++++++-------
 Telegram/SourceFiles/data/data_story.h        | 17 ++++++--
 .../stories/media_stories_controller.cpp      |  5 +++
 .../media/stories/media_stories_header.h      |  7 ++++
 Telegram/SourceFiles/mtproto/scheme/api.tl    |  3 +-
 7 files changed, 61 insertions(+), 19 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 173cfbb91..66e34cff2 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -330,6 +330,7 @@ void DocumentData::setattributes(
 	_flags &= ~(Flag::ImageType
 		| Flag::HasAttachedStickers
 		| Flag::UseTextColor
+		| Flag::SilentVideo
 		| kStreamingSupportedMask);
 	_flags |= kStreamingSupportedUnknown;
 
@@ -402,6 +403,9 @@ void DocumentData::setattributes(
 			_duration = crl::time(
 				base::SafeRound(data.vduration().v * 1000));
 			setMaybeSupportsStreaming(data.is_supports_streaming());
+			if (data.is_nosound()) {
+				_flags |= Flag::SilentVideo;
+			}
 			dimensions = QSize(data.vw().v, data.vh().v);
 		}, [&](const MTPDdocumentAttributeAudio &data) {
 			if (type == FileDocument) {
@@ -1542,6 +1546,10 @@ bool DocumentData::isVideoFile() const {
 	return (type == VideoDocument);
 }
 
+bool DocumentData::isSilentVideo() const {
+	return _flags & Flag::SilentVideo;
+}
+
 crl::time DocumentData::duration() const {
 	return std::max(_duration, crl::time());
 }
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index 9f013e373..59891c005 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -166,6 +166,7 @@ public:
 	[[nodiscard]] bool isSongWithCover() const;
 	[[nodiscard]] bool isAudioFile() const;
 	[[nodiscard]] bool isVideoFile() const;
+	[[nodiscard]] bool isSilentVideo() const;
 	[[nodiscard]] bool isAnimation() const;
 	[[nodiscard]] bool isGifv() const;
 	[[nodiscard]] bool isTheme() const;
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index 210e4a8f4..8b21c7c34 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -255,12 +255,16 @@ bool Story::pinned() const {
 	return _pinned;
 }
 
-bool Story::isPublic() const {
-	return _isPublic;
-}
-
-bool Story::closeFriends() const {
-	return _closeFriends;
+StoryPrivacy Story::privacy() const {
+	return _privacyPublic
+		? StoryPrivacy::Public
+		: _privacyCloseFriends
+		? StoryPrivacy::CloseFriends
+		: _privacyContacts
+		? StoryPrivacy::Contacts
+		: _privacySelectedContacts
+		? StoryPrivacy::SelectedContacts
+		: StoryPrivacy::Other;
 }
 
 bool Story::forbidsForward() const {
@@ -276,7 +280,7 @@ bool Story::canDownload() const {
 }
 
 bool Story::canShare() const {
-	return isPublic() && !forbidsForward() && (pinned() || !expired());
+	return _privacyPublic && !forbidsForward() && (pinned() || !expired());
 }
 
 bool Story::canDelete() const {
@@ -288,7 +292,7 @@ bool Story::canReport() const {
 }
 
 bool Story::hasDirectLink() const {
-	if (!_isPublic || (!_pinned && expired())) {
+	if (!_privacyPublic || (!_pinned && expired())) {
 		return false;
 	}
 	const auto user = _peer->asUser();
@@ -410,8 +414,15 @@ void Story::applyFields(
 
 	const auto pinned = data.is_pinned();
 	const auto edited = data.is_edited();
-	const auto isPublic = data.is_public();
-	const auto closeFriends = data.is_close_friends();
+	const auto privacy = data.is_public()
+		? StoryPrivacy::Public
+		: data.is_close_friends()
+		? StoryPrivacy::CloseFriends
+		: data.is_contacts()
+		? StoryPrivacy::Contacts
+		: data.is_selected_contacts()
+		? StoryPrivacy::SelectedContacts
+		: StoryPrivacy::Other;
 	const auto noForwards = data.is_noforwards();
 	auto caption = TextWithEntities{
 		data.vcaption().value_or_empty(),
@@ -443,13 +454,13 @@ void Story::applyFields(
 	const auto viewsChanged = (_views != views)
 		|| (_recentViewers != viewers);
 
-	_isPublic = isPublic;
-	_closeFriends = closeFriends;
+	_privacyPublic = (privacy == StoryPrivacy::Public);
+	_privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends);
+	_privacyContacts = (privacy == StoryPrivacy::Contacts);
+	_privacySelectedContacts = (privacy == StoryPrivacy::SelectedContacts);
 	_noForwards = noForwards;
 	_edited = edited;
 	_pinned = pinned;
-	_isPublic = isPublic;
-	_closeFriends = closeFriends;
 	_noForwards = noForwards;
 	if (viewsChanged) {
 		_views = views;
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
index d95c4dd40..8ea17b7b7 100644
--- a/Telegram/SourceFiles/data/data_story.h
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -23,6 +23,14 @@ class Session;
 class Thread;
 class PhotoMedia;
 
+enum class StoryPrivacy : uchar {
+	Public,
+	CloseFriends,
+	Contacts,
+	SelectedContacts,
+	Other,
+};
+
 struct StoryIdDates {
 	StoryId id = 0;
 	TimeId date = 0;
@@ -88,8 +96,7 @@ public:
 
 	void setPinned(bool pinned);
 	[[nodiscard]] bool pinned() const;
-	[[nodiscard]] bool isPublic() const;
-	[[nodiscard]] bool closeFriends() const;
+	[[nodiscard]] StoryPrivacy privacy() const;
 	[[nodiscard]] bool forbidsForward() const;
 	[[nodiscard]] bool edited() const;
 
@@ -138,8 +145,10 @@ private:
 	const TimeId _expires = 0;
 	TimeId _lastUpdateTime = 0;
 	bool _pinned : 1 = false;
-	bool _isPublic : 1 = false;
-	bool _closeFriends : 1 = false;
+	bool _privacyPublic : 1 = false;
+	bool _privacyCloseFriends : 1 = false;
+	bool _privacyContacts : 1 = false;
+	bool _privacySelectedContacts : 1 = false;
 	bool _noForwards : 1 = false;
 	bool _edited : 1 = false;
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 2bdb19098..adad7ec8b 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/update_checker.h"
 #include "data/stickers/data_custom_emoji.h"
 #include "data/data_changes.h"
+#include "data/data_document.h"
 #include "data/data_file_origin.h"
 #include "data/data_message_reactions.h"
 #include "data/data_session.h"
@@ -795,12 +796,16 @@ void Controller::show(
 
 	_captionText = story->caption();
 	_captionFullView = nullptr;
+	const auto document = story->document();
 	_header->show({
 		.user = user,
 		.date = story->date(),
 		.fullIndex = _sliderCount ? _index : 0,
 		.fullCount = _sliderCount ? shownCount() : 0,
+		.privacy = story->privacy(),
 		.edited = story->edited(),
+		.video = (document != nullptr),
+		.silent = (document && document->isSilentVideo()),
 	});
 	if (!changeShown(story)) {
 		return;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index 1393eefd5..2b8f6e61f 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/timer.h"
 #include "ui/userpic_view.h"
 
+namespace Data {
+enum class StoryPrivacy : uchar;
+} // namespace Data
+
 namespace Ui {
 class RpWidget;
 class FlatLabel;
@@ -24,7 +28,10 @@ struct HeaderData {
 	TimeId date = 0;
 	int fullIndex = 0;
 	int fullCount = 0;
+	Data::StoryPrivacy privacy = {};
 	bool edited = false;
+	bool video = false;
+	bool silent = false;
 
 	friend inline auto operator<=>(HeaderData, HeaderData) = default;
 	friend inline bool operator==(HeaderData, HeaderData) = default;
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 18c9bcd5b..9c54c97bc 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -1530,7 +1530,7 @@ storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector<long>
 
 storyItemDeleted#51e6ee4f id:int = StoryItem;
 storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem;
-storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
+storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector<MessageEntity> media:MessageMedia privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews = StoryItem;
 
 userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector<StoryItem> = UserStories;
 
@@ -1677,6 +1677,7 @@ account.invalidateSignInCodes#ca8ae8ba codes:Vector<string> = Bool;
 users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
 users.getFullUser#b60f5918 id:InputUser = users.UserFull;
 users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector<SecureValueError> = Bool;
+users.getStoriesMaxIDs#ca1cb9ab id:Vector<InputUser> = Vector<int>;
 
 contacts.getContactIDs#7adc669d hash:long = Vector<int>;
 contacts.getStatuses#c4a353ee = Vector<ContactStatus>;

From 99926be57a859c2b149656ea943fa052f54dc944 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 19 Jul 2023 17:31:52 +0400
Subject: [PATCH 206/259] Add privacy badge to stories userpic.

---
 .../icons/mediaview/mini_close_friends.png    | Bin 0 -> 325 bytes
 .../icons/mediaview/mini_close_friends@2x.png | Bin 0 -> 557 bytes
 .../icons/mediaview/mini_close_friends@3x.png | Bin 0 -> 726 bytes
 .../icons/mediaview/mini_contacts.png         | Bin 0 -> 412 bytes
 .../icons/mediaview/mini_contacts@2x.png      | Bin 0 -> 720 bytes
 .../icons/mediaview/mini_contacts@3x.png      | Bin 0 -> 1106 bytes
 .../mediaview/mini_selected_contacts.png      | Bin 0 -> 395 bytes
 .../mediaview/mini_selected_contacts@2x.png   | Bin 0 -> 666 bytes
 .../mediaview/mini_selected_contacts@3x.png   | Bin 0 -> 996 bytes
 .../media/stories/media_stories_header.cpp    | 216 ++++++++++++++++--
 .../media/stories/media_stories_header.h      |  12 +
 .../SourceFiles/media/view/media_view.style   |   7 +
 12 files changed, 217 insertions(+), 18 deletions(-)
 create mode 100644 Telegram/Resources/icons/mediaview/mini_close_friends.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_close_friends@2x.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_close_friends@3x.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_contacts.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_contacts@2x.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_contacts@3x.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_selected_contacts.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png
 create mode 100644 Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png

diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends.png b/Telegram/Resources/icons/mediaview/mini_close_friends.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c3072447e6761a60964c3a7f5a9613034b93cf9
GIT binary patch
literal 325
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K$e)_b}*hG>YM
zov@qtu!2O3ZcsqLg&={Gx@k;DobvWIh&v>0;W@yT!%#Fa#^F=9i`S|rTqXaH%PQ=8
zXTD3Uw($Gf7nhs1ev4zdT&Aa;)bq7={noN`o|*^S6FC$Eyo%4A-n8cAazUqtYt6S!
zm$n>O*l@7=zMJ;JYnHLkGiTl6d+^#sL-ENM0~@vvxsIAw?=kGqm$`LJ;>}Ts<4O`e
tqVKF{AD@5j^BK$a9Bj%Drv32#&LH(>rkPa6ts0OQJYD@<);T3K0RRlkYpehO

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png b/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..82dd44af9567a6ae42ec3f9e0abe9d63dfaecb3f
GIT binary patch
literal 557
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={W7>k44ofy`glX(f`xTHpSruq6Z
zXaU(A42<oW3@jieKr98s3=GT*7#Wy>G$TlC0TWzSVF5FO4N|zKE$K5**>q1A#}E(R
zw^R17YD$newpC*Vx5kCGj;w-WU-A5dYq>Kr-$<R4h;#7{xUjcna<h<1Q!byC%Y{md
zrad)sU%&RW?|b<8@8{}y;r4b@mz@9gU0vbx{eWpt@8+@JE1dKF_gV3xlV{VI<8@eK
ze6`P&-mPJ13Ud<6i41!;@4ZK)kzL8om=_F7YI2P&=bw*XFD~04ax`tJ%HN{yUw{AQ
zm`V2^Uwn~c_acqgU#qO<a+Phr{kFo!El}k6$HyNJ$gv#!{8PlhaITM9qQs^4r^mnl
zmMuECQTfTNWh+-{iOkfzqnO|D(R8k#`oG{{A*U14YzufZ_TLXyRc~u~8h7S?-u6Rh
zS4uJS6e#8h-q;qK>h=0S)C{f_&nvI2mSGl9(Y+M6etK%;%_(7*OjyH`wc>)iUhT4z
z;K^7j!{6?l@U$v$rSyiVwOs4eRvdrDCeh|tJ6YxDuSkxJ*aI1>6~EQ)w|IB>VZ;9*
zJ%@%Z8I0KmtFq>wPu~%<KK!!DX`ZeO#slmz?>1@O%`4v+QDQaqc*nJ(oh-*T{dSly
d{Npue4Re#^JuB;scafl|@^tlcS?83{1OT%u-kSgb

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png b/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f024ce73a918ea36c5efa2249200872f12e4ea8
GIT binary patch
literal 726
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2Neyz~tcR;usRa
z`8Ira*C7XyP{H{(c6B5qCNEvv+SGev#r_1{7wi#Sw|>Z*nce=uX?Cq?K~pc+qNxiO
zJjmwfYnf4*VjOvbYqHWE%kT4w{qq*h+!6CCKo|sn{NM~(ef4bG=D78O7WIY`J)RU;
z*u=9l>BxS&l4I5>U3Bb{r*q!+YO~odc@sG=82T>_@_gTRghAHWM20WZnPINs6d$$s
z->q{w6oM|zv$=G}ZSlj81}_*UPMS7V<=N+-r=L3Yw<~xobnuJ%(tTj-vY?d_I%2(U
z`{UR-g*si-CL6vrpXn2}`fA?x+wZ?yOitK+_uTW(#~&wpnTs8H{PAeg!P$+nGMfa<
zzZu7t?~b*OJ$lD)`C;P&6<VGzKUO>`eDiqu<&&E{3?yWF-5zmIJ?&&B!WC)8(scj5
zJP%t@Y^4r|Q`j-yhYyZkdHuEGRal|N-?OvV#jU@eXTJLF*RKnlUFr->Hpl2a_Pp}_
zM=g)ii!IAymjq}S#K$G2FKEcvZr*P%*MIcwOC1dxQ?+w{<&(mG*Vw7)F{o-VZQUMX
zA+s-T{TH4i92yGyI61||)TW$G+Z(q&pm!n*tBYF`bELtP(@!mw)HbL-+HagvR2U^y
zt^D@)=?@p?oPYlQdgFl!O5Or9tG#_=)I647K9hFiQnqyl%b8sJjs>SnL<_RE&N_IZ
zXy=*`EfZGxD;|<vj?1-k6^|wbzHW@ZoVT61k-=Y3Q{=^ag%^90D)(H?@?CzpZvXnb
t${WQ_1@LfnJo1_v3XWl5953Pg!?5YL+hi?`KN6rc<>~6@vd$@?2>@_uCa3@a

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_contacts.png b/Telegram/Resources/icons/mediaview/mini_contacts.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc716cab41b4aa5c0aea7361c2e85903c9436a81
GIT binary patch
literal 412
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#j#^&kb7@{G%
zH^8>fH9_R*%y*(HSE^KmoRkH%H}LEhl-|I<k(qU>lXd5cDO}4Gp7qR*af|zXFL&SP
zxz*?X3b&NEmt-q=PI_!nD6@NA_Z5r2-}~;HJioqa&9ck|4-Q;6IU8K)8lZpL>C${<
zj`N@Y9BTI5Q5Gn2Uis7}75~`l0x_$(XUsUxad<*epon4T(T<u6k1OTG14LRk=$!WX
zwa@=Yw_M1hm}#k(O&UEfPdT~zLBEUpyM60ke-~u%xis~reu8?ATWDzL(w1eJFW5dE
zy%ZqPcJce)w@o{~8t{Y%+3c2?XLCMr^(w9!0hV3s)IBei%AWowc_7K`_O?P3ppBll
gx82^h@t5)g@ysn-&uy@~YYPe)Pgg&ebxsLQ0JKDy3jhEB

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_contacts@2x.png b/Telegram/Resources/icons/mediaview/mini_contacts@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7df593e8338ed0506ba718ff604c247c800e44b
GIT binary patch
literal 720
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={W7>k44ofy`glX(f`xTHpSruq6Z
zXaU(A42<oW3@jieKr98s3=GT*7#Wy>G$TlC0TWzSVF5FO4N|zKE$K4@1Cy1fi(`lf
z@7qx8{*8_zahvi)d6kYVeU^V==U>J@j6&Ppoc}PlMlBRybn(It6_xn*77nTDhfKXX
z@1}=`J)0}J=hokExpO|BDV}FsoObTxw{PG7{^IhOH!ooE#UC|x>#t{Tzs<|W{A2ai
zECY!Ut*J#jYb<2uY28uP5a~)W+Ij!I{kxjocllU$#?*yBI@lDq{(Xs6`4m=z3<diM
zk8~6h%w}_@$XO}8F0twp5I;0=XSMzO^Z)nROXTb-+pW7Ttkb2bSDvq3to!LM#o1@g
zq<SO#bi^LNi(q(kcEk2vF?zmg!Y7SqpMCvx)~QD8@YPpUdXw9o6t#HrtmdYzyqPmC
zm5qt9OLJ+>o^r0n+wZ@-HocJO@>F*)59HZ#r@7d%{qVsy?g^J)9+BG<qQ&WPpYPU;
zRHZqoTmpGa9Ii@&lf?PjBTE&|3-=sP&U^B-=;ZUydecw8EO}OBxg$pE(8=0@oRDT|
zO`(+TpMI*2sekM2Sr6ID^<REjVmJTrwy3o)OiOsY4|k+GGB$1BcsK9l-)pZ+x8K&C
z$icQdMC)sXO|M&XN>`RiO1InM?FK%{j&;X8mj;;^*~q0|t8DO5%Ql<Mu{vz^o>D1b
z=ql_tc%!i3YL@zB&O=<SO+hP{sMgm^^$OJyb3FWYkHNb24^I@@9#7hMK*va`*M3v{
z{u#A1eb~GM^0!9mua4gupnd1~+jHi#eYf9!`?boPWqX&P?zjAgI|`YPV-wvTH7@SF
ru;NF;0baJ|JB2RC{@wiV&T7Y)`1Hivw7-&%L8-~p)z4*}Q$iB}C#op9

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_contacts@3x.png b/Telegram/Resources/icons/mediaview/mini_contacts@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..50c9ff55fe6c057a87e5c825bbf2bbafdc315255
GIT binary patch
literal 1106
zcmV-Y1g-mtP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NFyh%hsR9Fe^SIaB>Q4}{rHbRZp
zc>LtOkYYU6N~mcTSRpBiVrNU#`~|`wl8q%bV<C1nvY8nr8;^xT`Nhgx^7#Ee%{SL|
zzxRB<_iJWnzKididmf*2?wNbfIp2hY_!G}SJOjTh1GZ@Po1dRwQBjc_d4Tgb@*E!@
zUtV5D18do~yu5s3Vq$M^?;r7m!{PDq@v^cq%g|O0kc-8|#kaRNB9d?H*VosDg$1Y6
zY1NCW{_ybd!^6Wjp9%|@3=Ivb`cTwMN=llapI7iss?5#J0YuTAi5h^mwzf!D%7V?!
z&Dg{gWp4O@eLkPb?oGw``*}nueROoRy1KfywsvxIB30m-gM)*nPDuq^5aUyPadFYw
z+REFz-EI^iJ_3QhySvNI&gM-l(73b$9KSq2KSveN!xa=17{`PIuD3GhpsX3!o95<b
z3dQ5`@SzJd>QhNc31v(2QECMsetB?kKw3rwhlhu}zCe$SjZx-yO3>ckPKm(Z-rhEl
znksa3bWrB<fY3GT>gp&P@X<yMq^1h>_4Rpqc~S&<K<MF8ZN|*V$Y8XTa5x-NK4VfY
z<N-;q55ofCa=G}Wo;FpmmLb!Cnwpx5ii-4cXdGOk#>PfUs_`io@(dYcL29?#mzS6E
z)@#~qwxy*dy!9d#jsy{ADkC3~<oEkiQ&V|2DJd!FArSFQDNS7q`A9%%3(#$CZD(g^
zD=RDe`}?RgRaI4eeSMjknSz>hsj}xUL!7vej}M`m#db*@>aQRKDi{pLHUb_k6bcFM
z*i!zNK^zDK8X6jyo}NGo(H}7Cqvzk;+~5?ar>7Sd7S`6*R#sLTAL{k>bv>X7r?|Kn
zU69`vqcK8EO-&gzNHpe|nHi`t6P^HUX=$m6d#QMKc9sVMjsA}hptQ6!slLEO!n|JZ
z`}?~9f)P~ER9$9eW!>K1GPvjG=a!ZhRpU=RjBGD2FAU`V{yrxs=Tm6?x4*xiVg2}P
zW@Kc9fkZXJYVl7_PHJ!%nNex1ZmOoH=JfPbL%_kZYA(@FOiaY3&=4OVAG^D|B}xRV
zx3~A{=}EKQ*w_FRp=gl}AeJYf@9gX#tmNcm3I?K{o}S&^T`d&6k(-!B!@S}=@n`(%
z>I#>?v$M0hy82J#0nydfH9b9jeSOVB!Q~$q7+@eVCUEai3#j_Ug3t)4;REC6A3f@^
zqh^kdj-t0v{SKW0W4uA-H1PGqsYOFXgTy3?w>E8IMnDh8Wwh4G4Wf&S;u(l%;5TI8
YKb9#Qwu<WdeE<Le07*qoM6N<$f*|Alj{pDw

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3ca5b06d89501605715441500c5e47cbbe12dd3
GIT binary patch
literal 395
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K$ezIwVihG>W;
zCrEs8@bl}NI(2G9x?aSF2mk;7@0YiqHgziNtHkv5^=sCgIdS5^x_A!{julc&UESS{
zU9)D-R=l%fg~kp!HH+7W+xgeW?QJT^zrT++<-^C1jywMR`BRXp!Nbhld~<X9@})~1
zU5*|(61FD7F!z=TA0OWit2B%5_V(kqZp~_Kefs<R`}qBJwV$8$c6M@_n}0t&U4No(
z`>O35H&%Xp#CnGN|Nnn~Z(qF{8XU|l$XHZdRP?~DS;D&PO~dLo<EEyjc{Y_y58vJ1
z&cCRU)#>b9YjFuLAD<3`A0Hnde|~;`zntx=HEaF=S@-V6b)0(Wz{J31-xJ^dweK}3
OAUs|DT-G@yGywpfbC=Bk

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..d12f95496d4a6772f01ef6e1156e63c469728599
GIT binary patch
literal 666
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={W7>k44ofy`glX(f`xTHpSruq6Z
zXaU(A42<oW3@jieKr98s3=GT*7#Wy>G$TlC0TWzSVF5FO4N|zKE$K4@0~3p<i(`lf
z@7s|5r#A*l)cw_2XrRF*nZvf)G0^MhKgRz}y_+I@jU;%wj1(5To^oQ_JgHSj?9_Tk
z&pMI&)qCInymz^9-sQ)yXYYGx$^3oiyVv{X{oZ%3cwRq$swUr`?|d04Rv{}x9u`=L
za5Zkeo0mG{|0JWCK8r8D_<mk%s+W`K^2?bfQmre^6V_k<E%^WQ{Ih8{rMBO;mFu6l
z`)=OWDBr~sjhl7<bk*9)FYnWREp#VZM@(mVfW`+cqu(AXhr}*NtiL{am4kwVqma&Y
z?}ZDZI=F7W{`#wKzwo}ZX^*+qhG`#r<ghb$_uXezKFcp}Ignrwvp#&<g_k86-_|pE
z@USs!DQ>lx=wTzr?-a2%EN*@H3T<-{H$Jvzz3JIjbI+w2XYRfKzW?~+-+%Yre)}lg
ztD^r!iB<Q}MPc3jTwl{RN3OoQPyCTYh3BlZYJO8K7_<!>H%63L$h1gyAN9Dd@IcN-
zZL-(WrbtWKgV*x5JOBDw6ZHM&gBrW%MV38Nebgf7967NpNV8F+^+TnD{v|KImDgWu
zoP7RS#&6rjj452780YD)nb<Vbhwa__GV9D$F?ySK=`Vl&vEs%YpPn$k<-z;!PcP~_
zuFSp9hG9CZ?8eNWKME`gSnlj|TYRudKmSI7#h<Tx3wm@qdoMCp9k*rQaWiMtRjspe
qdeg&Jo6bM4xrn8Uxr_NeUk#Jv(kr{tEptmi>B7_1&t;ucLK6V7G#14G

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..90860348b9d729a0a23d59f8e994773bad20966f
GIT binary patch
literal 996
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H<Xr$#jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(;Zuz(rC1}VI4p2GuF?&s;^7!twx
zHvF`AmZOYc_04R-8PdLslsEzp`p&*{^^iM%MsRo36{ST-{xu2wVvc%psB9ZgOy>os
zE|-HGYkXPTB3xP}SodySQ*Hll=ZVZ`XI{#1zl)u-dDhQozipq*{`g&b^{OfUfetV@
zjY;d}n_07FO`Iq=`)pWrbah3A1Xt_4`SX{~{%;^$nX>t2{GRu}e%*Rl@TWkc^IcVS
zwY<E1<;g#?`}XhW=i%8fCqieMgMvW+apj38{`}eFwm8u+^>L?5)44WhJw3gGrY*-0
z9z5u@@WA#ix5Zz-el-a;OEQ}IqweL$7cVp(Zdt$n{mYk~X>ErSn>_umnz$$g%yZ~i
zc{NLQ!qTNnj~#PkI#QC6v10%J`Nw{qxXx*6V)B9aMPYETFk_dCXyl_gHFGcAyeTQz
zKV#m!d7lKgM(JwsAJ5Fpd^lNK^7_@QPd``waw_mx8FK0M*OvQ}Cku<!S5?hY2sip#
zVY7x|_3G7I+~=QHKXKeQKR0*j&#4O8+1VvMXV3Z`eAT3EZD*(UWBvN|e#<vcx}h`s
z<HwJC_wG&F*pX~yWfc_>v0~l2w28NG-3s78ZF%?3ou1y_jVEei-1HxxDM+7q{Pu0>
z``p=g?%wS+Dl0E<pZn+ZvE!%wx%5I6w;1of+b2IIY;~)LMN^F4bRRW-K0Z4;yB8i0
z7AKUImX?;4Ir)`53enoScW-Tdz5nD?g&kZPyQfc|{`9s3PhNh0cvu+F1*iPl+uO^_
z$~dIVkM8ATYqqtuP2JSf=(Tj#?AgEw6e#g+xR6j;DJja$xNO<74_pcye>7*kd-P~g
zPl$u7*$uAy-BUikd#4wk1_T9tkJMI#8WkNXJ9>9x*!S<>ci!8+Z{N8`1r~kh6AU~~
zahi+g6qYNkuG=r4*yHy2^yzL6Uhm11Czm)eofYMc{BYJ)np<h}=FK;L{rvg!*s(T+
zg*;sQuCoX=*`&_qV*8>0plo+y$75l0|K*oW92FKu=(Nd;`%dk9QDQa0cxF!rXB<m~
zWVe8wuCI?zNKJ3RuQO@JcPjVBU6b9lYuBRtGh^7!c-_5sukX^GJ9qwQUbYu*S?H*c
zv8%!|@qR|!j9oE$zgDuygjQ!~zuv7HZNSVLA`$?FKlnv{@Fq^#x3&MN#8prZ^K|ud
JS?83{1OWH<x`hA$

literal 0
HcmV?d00001

diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index ab2a842e9..03f40701f 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -16,7 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/layers/box_content.h"
 #include "ui/text/format_values.h"
 #include "ui/text/text_utilities.h"
+#include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
+#include "ui/wrap/fade_wrap.h"
 #include "ui/painter.h"
 #include "ui/rp_widget.h"
 #include "styles/style_media_view.h"
@@ -32,6 +34,155 @@ struct Timestamp {
 	TimeId changes = 0;
 };
 
+struct PrivacyBadge {
+	const style::icon *icon = nullptr;
+	const style::color *bg1 = nullptr;
+	const style::color *bg2 = nullptr;
+};
+
+class UserpicBadge final : public Ui::RpWidget {
+public:
+	UserpicBadge(
+		not_null<QWidget*> userpic,
+		PrivacyBadge badge,
+		Fn<void()> clicked);
+
+private:
+	bool eventFilter(QObject *o, QEvent *e) override;
+	void paintEvent(QPaintEvent *e) override;
+
+	void updateGeometry();
+
+	const not_null<QWidget*> _userpic;
+	const PrivacyBadge _badgeData;
+	const std::unique_ptr<Ui::AbstractButton> _clickable;
+	QRect _badge;
+	QImage _layer;
+	bool _grabbing = false;
+
+};
+
+[[nodiscard]] PrivacyBadge LookupPrivacyBadge(Data::StoryPrivacy privacy) {
+	using namespace Data;
+	static const auto badges = base::flat_map<StoryPrivacy, PrivacyBadge>{
+		{ StoryPrivacy::CloseFriends, PrivacyBadge{
+			&st::storiesBadgeCloseFriends,
+			&st::historyPeer2UserpicBg,
+			&st::historyPeer2UserpicBg2,
+		} },
+		{ StoryPrivacy::Contacts, PrivacyBadge{
+			&st::storiesBadgeContacts,
+			&st::historyPeer5UserpicBg,
+			&st::historyPeer5UserpicBg2,
+		} },
+		{ StoryPrivacy::SelectedContacts, PrivacyBadge{
+			&st::storiesBadgeSelectedContacts,
+			&st::historyPeer8UserpicBg,
+			&st::historyPeer8UserpicBg2,
+		} },
+	};
+	if (const auto i = badges.find(privacy); i != end(badges)) {
+		return i->second;
+	}
+	return {};
+}
+
+UserpicBadge::UserpicBadge(
+	not_null<QWidget*> userpic,
+	PrivacyBadge badge,
+	Fn<void()> clicked)
+: RpWidget(userpic->parentWidget())
+, _userpic(userpic)
+, _badgeData(badge)
+, _clickable(std::make_unique<Ui::AbstractButton>(parentWidget())) {
+	_clickable->setClickedCallback(std::move(clicked));
+	userpic->installEventFilter(this);
+	updateGeometry();
+	setAttribute(Qt::WA_TransparentForMouseEvents);
+	Ui::PostponeCall(this, [=] {
+		_userpic->raise();
+	});
+	show();
+}
+
+bool UserpicBadge::eventFilter(QObject *o, QEvent *e) {
+	if (o != _userpic) {
+		return false;
+	}
+	const auto type = e->type();
+	switch (type) {
+	case QEvent::Move:
+	case QEvent::Resize:
+		updateGeometry();
+		return false;
+	case QEvent::Paint:
+		return !_grabbing;
+	}
+	return false;
+}
+
+void UserpicBadge::paintEvent(QPaintEvent *e) {
+	const auto ratio = style::DevicePixelRatio();
+	const auto layerSize = size() * ratio;
+	if (_layer.size() != layerSize) {
+		_layer = QImage(layerSize, QImage::Format_ARGB32_Premultiplied);
+		_layer.setDevicePixelRatio(ratio);
+	}
+	_layer.fill(Qt::transparent);
+	auto q = QPainter(&_layer);
+
+	_grabbing = true;
+	Ui::RenderWidget(q, _userpic);
+	_grabbing = false;
+
+	auto hq = PainterHighQualityEnabler(q);
+	auto pen = st::transparent->p;
+	pen.setWidthF(st::storiesBadgeOutline);
+	const auto half = st::storiesBadgeOutline / 2.;
+	auto outer = QRectF(_badge).marginsAdded({ half, half, half, half });
+	auto gradient = QLinearGradient(outer.topLeft(), outer.bottomLeft());
+	gradient.setStops({
+		{ 0., (*_badgeData.bg1)->c },
+		{ 1., (*_badgeData.bg2)->c },
+	});
+	q.setPen(pen);
+	q.setBrush(gradient);
+	q.setCompositionMode(QPainter::CompositionMode_Source);
+	q.drawEllipse(outer);
+	q.setCompositionMode(QPainter::CompositionMode_SourceOver);
+	_badgeData.icon->paintInCenter(q, _badge);
+	q.end();
+
+	QPainter(this).drawImage(0, 0, _layer);
+}
+
+void UserpicBadge::updateGeometry() {
+	const auto width = _userpic->width() + st::storiesBadgeShift.x();
+	const auto height = _userpic->height() + st::storiesBadgeShift.y();
+	setGeometry(QRect(_userpic->pos(), QSize{ width, height }));
+	const auto inner = QRect(QPoint(), _badgeData.icon->size());
+	const auto badge = inner.marginsAdded(st::storiesBadgePadding).size();
+	_badge = QRect(
+		QPoint(width - badge.width(), height - badge.height()),
+		badge);
+	_clickable->setGeometry(_badge.translated(pos()));
+	update();
+}
+
+[[nodiscard]] std::unique_ptr<Ui::RpWidget> MakePrivacyBadge(
+		not_null<QWidget*> userpic,
+		Data::StoryPrivacy privacy,
+		Fn<void()> clicked) {
+	const auto badge = LookupPrivacyBadge(privacy);
+	if (!badge.icon) {
+		return nullptr;
+	}
+	return std::make_unique<UserpicBadge>(
+		userpic,
+		badge,
+		std::move(clicked));
+}
+
 [[nodiscard]] Timestamp ComposeTimestamp(TimeId when, TimeId now) {
 	const auto minutes = (now - when) / 60;
 	if (!minutes) {
@@ -102,42 +253,41 @@ Header::Header(not_null<Controller*> controller)
 , _dateUpdateTimer([=] { updateDateText(); }) {
 }
 
-Header::~Header() {
-}
+Header::~Header() = default;
 
 void Header::show(HeaderData data) {
 	if (_data == data) {
 		return;
 	}
-	const auto nameDataChanged = !_data
-		|| (_data->user != data.user)
+	const auto userChanged = !_data
+		|| (_data->user != data.user);
+	const auto nameDataChanged = userChanged
+		|| !_name
 		|| (_data->fullCount != data.fullCount)
 		|| (data.fullCount && _data->fullIndex != data.fullIndex);
 	_data = data;
-	if (nameDataChanged) {
+	if (userChanged) {
 		_date = nullptr;
+		_name = nullptr;
+		_userpic = nullptr;
+		_info = nullptr;
+		_privacy = nullptr;
 		const auto parent = _controller->wrap();
-		auto widget = std::make_unique<Ui::AbstractButton>(parent);
+		auto widget = std::make_unique<Ui::RpWidget>(parent);
 		const auto raw = widget.get();
-		raw->setClickedCallback([=] {
+		_info = std::make_unique<Ui::AbstractButton>(raw);
+		_info->setClickedCallback([=] {
 			_controller->uiShow()->show(PrepareShortInfoBox(_data->user));
 		});
-		const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
+		_userpic = std::make_unique<Ui::UserpicButton>(
 			raw,
 			data.user,
 			st::storiesHeaderPhoto);
-		userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
-		userpic->show();
-		userpic->move(
+		_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
+		_userpic->show();
+		_userpic->move(
 			st::storiesHeaderMargin.left(),
 			st::storiesHeaderMargin.top());
-		const auto name = Ui::CreateChild<Ui::FlatLabel>(
-			raw,
-			rpl::single(ComposeName(data)),
-			st::storiesHeaderName);
-		name->setAttribute(Qt::WA_TransparentForMouseEvents);
-		name->setOpacity(kNameOpacity);
-		name->move(st::storiesHeaderNamePosition);
 		raw->show();
 		_widget = std::move(widget);
 
@@ -146,6 +296,26 @@ void Header::show(HeaderData data) {
 			raw->setGeometry(layout.header);
 		}, raw->lifetime());
 	}
+	if (nameDataChanged) {
+		_name = std::make_unique<Ui::FlatLabel>(
+			_widget.get(),
+			rpl::single(ComposeName(data)),
+			st::storiesHeaderName);
+		_name->setAttribute(Qt::WA_TransparentForMouseEvents);
+		_name->setOpacity(kNameOpacity);
+		_name->move(st::storiesHeaderNamePosition);
+		_name->show();
+
+		rpl::combine(
+			_name->widthValue(),
+			_widget->heightValue()
+		) | rpl::start_with_next([=](int width, int height) {
+			if (_date) {
+				_info->setGeometry(
+					{ 0, 0, std::max(width, _date->width()), height });
+			}
+		}, _name->lifetime());
+	}
 	auto timestamp = ComposeDetails(data, base::unixtime::now());
 	_date = std::make_unique<Ui::FlatLabel>(
 		_widget.get(),
@@ -156,6 +326,16 @@ void Header::show(HeaderData data) {
 	_date->show();
 	_date->move(st::storiesHeaderDatePosition);
 
+	_date->widthValue(
+	) | rpl::start_with_next([=](int width) {
+		_info->setGeometry(
+			{ 0, 0, std::max(width, _name->width()), _widget->height() });
+	}, _name->lifetime());
+
+	_privacy = MakePrivacyBadge(_userpic.get(), data.privacy, [=] {
+
+	});
+
 	if (timestamp.changes > 0) {
 		_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
 	}
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index 2b8f6e61f..ae584ed8c 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -17,6 +17,11 @@ enum class StoryPrivacy : uchar;
 namespace Ui {
 class RpWidget;
 class FlatLabel;
+class IconButton;
+class AbstractButton;
+class UserpicButton;
+template <typename Widget>
+class FadeWrap;
 } // namespace Ui
 
 namespace Media::Stories {
@@ -51,7 +56,14 @@ private:
 	const not_null<Controller*> _controller;
 
 	std::unique_ptr<Ui::RpWidget> _widget;
+	std::unique_ptr<Ui::AbstractButton> _info;
+	std::unique_ptr<Ui::UserpicButton> _userpic;
+	std::unique_ptr<Ui::FlatLabel> _name;
 	std::unique_ptr<Ui::FlatLabel> _date;
+	std::unique_ptr<Ui::IconButton> _playPause;
+	std::unique_ptr<Ui::IconButton> _volumeToggle;
+	std::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _volume;
+	std::unique_ptr<Ui::RpWidget> _privacy;
 	std::optional<HeaderData> _data;
 	base::Timer _dateUpdateTimer;
 
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index ac5d9e062..097b498cb 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -844,3 +844,10 @@ storiesReportBox: ReportBox(defaultReportBox) {
 storiesActionToast: Toast(defaultToast) {
 	maxWidth: 320px;
 }
+
+storiesBadgeCloseFriends: icon{{ "mediaview/mini_close_friends", historyPeerUserpicFg }};
+storiesBadgeContacts: icon{{ "mediaview/mini_contacts", historyPeerUserpicFg }};
+storiesBadgeSelectedContacts: icon{{ "mediaview/mini_selected_contacts", historyPeerUserpicFg }};
+storiesBadgePadding: margins(1px, 1px, 1px, 1px);
+storiesBadgeOutline: 2px;
+storiesBadgeShift: point(5px, 4px);

From c2805d92a58c8787d37186845f9a2f6a4e417dce Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 19 Jul 2023 18:34:04 +0400
Subject: [PATCH 207/259] Fix marking stories as read.

---
 Telegram/SourceFiles/data/data_stories.cpp | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 904813765..df0615038 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -286,7 +286,10 @@ void Stories::processExpired() {
 void Stories::parseAndApply(const MTPUserStories &stories) {
 	const auto &data = stories.data();
 	const auto peerId = peerFromUser(data.vuser_id());
-	const auto readTill = data.vmax_read_id().value_or_empty();
+	const auto already = _readTill.find(peerId);
+	const auto readTill = std::max(
+		data.vmax_read_id().value_or_empty(),
+		(already != end(_readTill) ? already->second : 0));
 	const auto user = _owner->peer(peerId)->asUser();
 	auto result = StoriesSource{
 		.user = user,

From 80bdf9b74cd4c0b50d5e6de90acea6072773231b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 19 Jul 2023 18:34:17 +0400
Subject: [PATCH 208/259] Add play/pause button to video stories.

---
 .../stories/media_stories_controller.cpp      | 14 +++++
 .../media/stories/media_stories_controller.h  |  8 +++
 .../media/stories/media_stories_header.cpp    | 52 +++++++++++++++++++
 .../media/stories/media_stories_header.h      |  6 +++
 .../SourceFiles/media/view/media_view.style   | 22 ++++++++
 5 files changed, 102 insertions(+)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index adad7ec8b..62019349d 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -902,10 +902,24 @@ void Controller::subscribeToSession() {
 	});
 }
 
+PauseState Controller::pauseState() const {
+	const auto inactive = !_windowActive
+		|| _replyActive
+		|| _layerShown
+		|| _menuShown;
+	const auto playing = !inactive && !_paused;
+	return playing
+		? PauseState::Playing
+		: inactive
+		? PauseState::Inactive
+		: PauseState::Paused;
+}
+
 void Controller::updatePlayingAllowed() {
 	if (!_shown) {
 		return;
 	}
+	_header->updatePauseState();
 	setPlayingAllowed(_started
 		&& _windowActive
 		&& !_paused
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 0df907acd..b29b6d520 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -72,6 +72,12 @@ enum class HeaderLayout {
 	Outside,
 };
 
+enum class PauseState {
+	Playing,
+	Paused,
+	Inactive,
+};
+
 struct SiblingLayout {
 	QRect geometry;
 	QRect userpic;
@@ -136,6 +142,8 @@ public:
 	void contentPressed(bool pressed);
 	void setMenuShown(bool shown);
 
+	[[nodiscard]] PauseState pauseState() const;
+
 	void repaintSibling(not_null<Sibling*> sibling);
 	[[nodiscard]] SiblingView sibling(SiblingType type) const;
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 03f40701f..cf752495a 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -272,6 +272,8 @@ void Header::show(HeaderData data) {
 		_userpic = nullptr;
 		_info = nullptr;
 		_privacy = nullptr;
+		_playPause = nullptr;
+		_volumeToggle = nullptr;
 		const auto parent = _controller->wrap();
 		auto widget = std::make_unique<Ui::RpWidget>(parent);
 		const auto raw = widget.get();
@@ -336,11 +338,61 @@ void Header::show(HeaderData data) {
 
 	});
 
+	if (data.video) {
+		_playPause = std::make_unique<Ui::IconButton>(
+			_widget.get(),
+			st::storiesPlayButton);
+		_playPause->show();
+		_playPause->setClickedCallback([=] {
+			_controller->togglePaused(_pauseState != PauseState::Paused);
+		});
+
+		_volumeToggle = std::make_unique<Ui::IconButton>(
+			_widget.get(),
+			st::storiesVolumeButton);
+		_volumeToggle->show();
+
+		_widget->widthValue() | rpl::start_with_next([=](int width) {
+			const auto playPause = st::storiesPlayButtonPosition;
+			_playPause->moveToRight(playPause.x(), playPause.y(), width);
+			const auto volume = st::storiesVolumeButtonPosition;
+			_volumeToggle->moveToRight(volume.x(), volume.y(), width);
+		}, _playPause->lifetime());
+
+		_pauseState = _controller->pauseState();
+		applyPauseState();
+	} else {
+		_playPause = nullptr;
+		_volumeToggle = nullptr;
+	}
+
 	if (timestamp.changes > 0) {
 		_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
 	}
 }
 
+void Header::updatePauseState() {
+	if (!_playPause) {
+		return;
+	} else if (const auto s = _controller->pauseState(); _pauseState != s) {
+		_pauseState = s;
+		applyPauseState();
+	}
+}
+
+void Header::applyPauseState() {
+	Expects(_playPause != nullptr);
+
+	const auto paused = (_pauseState == PauseState::Paused);
+	const auto inactive = (_pauseState == PauseState::Inactive);
+	_playPause->setAttribute(Qt::WA_TransparentForMouseEvents, inactive);
+	if (inactive) {
+		_playPause->clearState();
+	}
+	const auto icon = paused ? nullptr : &st::storiesPauseIcon;
+	_playPause->setIconOverride(icon, icon);
+}
+
 void Header::raise() {
 	if (_widget) {
 		_widget->raise();
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index ae584ed8c..8685e5bc8 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -27,6 +27,7 @@ class FadeWrap;
 namespace Media::Stories {
 
 class Controller;
+enum class PauseState;
 
 struct HeaderData {
 	not_null<UserData*> user;
@@ -47,14 +48,19 @@ public:
 	explicit Header(not_null<Controller*> controller);
 	~Header();
 
+	void updatePauseState();
+
 	void show(HeaderData data);
 	void raise();
 
 private:
 	void updateDateText();
+	void applyPauseState();
 
 	const not_null<Controller*> _controller;
 
+	PauseState _pauseState = {};
+
 	std::unique_ptr<Ui::RpWidget> _widget;
 	std::unique_ptr<Ui::AbstractButton> _info;
 	std::unique_ptr<Ui::UserpicButton> _userpic;
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index 097b498cb..a3ae2dac0 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -851,3 +851,25 @@ storiesBadgeSelectedContacts: icon{{ "mediaview/mini_selected_contacts", history
 storiesBadgePadding: margins(1px, 1px, 1px, 1px);
 storiesBadgeOutline: 2px;
 storiesBadgeShift: point(5px, 4px);
+
+storiesPlayIcon: icon{{ "player/player_play", mediaviewPlaybackIconFgOver }};
+storiesPauseIcon: icon{{ "player/player_pause", mediaviewPlaybackIconFgOver }};
+storiesPlayButton: IconButton(defaultIconButton) {
+	width: 40px;
+	height: 40px;
+	icon: storiesPlayIcon;
+	iconOver: storiesPlayIcon;
+	iconPosition: point(8px, 8px);
+	rippleAreaPosition: point(4px, 4px);
+	rippleAreaSize: 32px;
+	ripple: RippleAnimation(defaultRippleAnimation) {
+		color: mediaviewPlaybackIconRipple;
+	}
+}
+storiesPlayButtonPosition: point(54px, 0px);
+storiesVolumeButton: IconButton(storiesPlayButton) {
+	icon: mediaviewVolumeIcon2Over;
+	iconOver: mediaviewVolumeIcon2Over;
+	ripple: emptyRippleAnimation;
+}
+storiesVolumeButtonPosition: point(10px, 0px);

From 69b9c63a6960840cd51829b4a81a391b4f2700f4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 19 Jul 2023 23:59:28 +0400
Subject: [PATCH 209/259] Implement volume dropdown.

---
 .../stories/media_stories_controller.cpp      |  16 ++
 .../media/stories/media_stories_controller.h  |   4 +
 .../media/stories/media_stories_delegate.h    |   3 +
 .../media/stories/media_stories_header.cpp    | 256 ++++++++++++++++--
 .../media/stories/media_stories_header.h      |  11 +-
 .../SourceFiles/media/view/media_view.style   |  18 +-
 .../media/view/media_view_overlay_widget.cpp  |  13 +
 .../media/view/media_view_overlay_widget.h    |   3 +
 8 files changed, 304 insertions(+), 20 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 62019349d..99ab50de7 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -915,6 +915,22 @@ PauseState Controller::pauseState() const {
 		: PauseState::Paused;
 }
 
+float64 Controller::currentVolume() const {
+	return Core::App().settings().videoVolume();
+}
+
+void Controller::toggleVolume() {
+	_delegate->storiesVolumeToggle();
+}
+
+void Controller::changeVolume(float64 volume) {
+	_delegate->storiesVolumeChanged(volume);
+}
+
+void Controller::volumeChangeFinished() {
+	_delegate->storiesVolumeChangeFinished();
+}
+
 void Controller::updatePlayingAllowed() {
 	if (!_shown) {
 		return;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index b29b6d520..8b804c598 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -143,6 +143,10 @@ public:
 	void setMenuShown(bool shown);
 
 	[[nodiscard]] PauseState pauseState() const;
+	[[nodiscard]] float64 currentVolume() const;
+	void toggleVolume();
+	void changeVolume(float64 volume);
+	void volumeChangeFinished();
 
 	void repaintSibling(not_null<Sibling*> sibling);
 	[[nodiscard]] SiblingView sibling(SiblingType type) const;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h
index f42e3c463..ca663e69c 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h
@@ -61,6 +61,9 @@ public:
 	[[nodiscard]] virtual float64 storiesSiblingOver(SiblingType type) = 0;
 	virtual void storiesTogglePaused(bool paused) = 0;
 	virtual void storiesRepaint() = 0;
+	virtual void storiesVolumeToggle() = 0;
+	virtual void storiesVolumeChanged(float64 volume) = 0;
+	virtual void storiesVolumeChangeFinished() = 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 cf752495a..04a0e3f25 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -17,17 +17,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text/format_values.h"
 #include "ui/text/text_utilities.h"
 #include "ui/widgets/buttons.h"
+#include "ui/widgets/continuous_sliders.h"
 #include "ui/widgets/labels.h"
 #include "ui/wrap/fade_wrap.h"
 #include "ui/painter.h"
 #include "ui/rp_widget.h"
 #include "styles/style_media_view.h"
 
+#include <QtGui/QGuiApplication>
+
 namespace Media::Stories {
 namespace {
 
 constexpr auto kNameOpacity = 1.;
 constexpr auto kDateOpacity = 0.8;
+constexpr auto kControlOpacity = 0.6;
+constexpr auto kControlOpacityOver = 1.;
+constexpr auto kVolumeHideTimeoutShort = crl::time(20);
+constexpr auto kVolumeHideTimeoutLong = crl::time(200);
 
 struct Timestamp {
 	QString text;
@@ -267,6 +274,7 @@ void Header::show(HeaderData data) {
 		|| (data.fullCount && _data->fullIndex != data.fullIndex);
 	_data = data;
 	if (userChanged) {
+		_volume = nullptr;
 		_date = nullptr;
 		_name = nullptr;
 		_userpic = nullptr;
@@ -339,18 +347,8 @@ void Header::show(HeaderData data) {
 	});
 
 	if (data.video) {
-		_playPause = std::make_unique<Ui::IconButton>(
-			_widget.get(),
-			st::storiesPlayButton);
-		_playPause->show();
-		_playPause->setClickedCallback([=] {
-			_controller->togglePaused(_pauseState != PauseState::Paused);
-		});
-
-		_volumeToggle = std::make_unique<Ui::IconButton>(
-			_widget.get(),
-			st::storiesVolumeButton);
-		_volumeToggle->show();
+		createPlayPause();
+		createVolumeToggle();
 
 		_widget->widthValue() | rpl::start_with_next([=](int width) {
 			const auto playPause = st::storiesPlayButtonPosition;
@@ -362,6 +360,7 @@ void Header::show(HeaderData data) {
 		_pauseState = _controller->pauseState();
 		applyPauseState();
 	} else {
+		_volume = nullptr;
 		_playPause = nullptr;
 		_volumeToggle = nullptr;
 	}
@@ -371,6 +370,223 @@ void Header::show(HeaderData data) {
 	}
 }
 
+void Header::createPlayPause() {
+	struct PlayPauseState {
+		Ui::Animations::Simple overAnimation;
+		bool over = false;
+		bool down = false;
+	};
+	_playPause = std::make_unique<Ui::RpWidget>(_widget.get());
+	auto &lifetime = _playPause->lifetime();
+	const auto state = lifetime.make_state<PlayPauseState>();
+
+	_playPause->events(
+	) | rpl::start_with_next([=](not_null<QEvent*> e) {
+		const auto type = e->type();
+		if (type == QEvent::Enter || type == QEvent::Leave) {
+			const auto over = (e->type() == QEvent::Enter);
+			if (state->over != over) {
+				state->over = over;
+				state->overAnimation.start(
+					[=] { _playPause->update(); },
+					over ? 0. : 1.,
+					over ? 1. : 0.,
+					st::mediaviewFadeDuration);
+			}
+		} else if (type == QEvent::MouseButtonPress && state->over) {
+			state->down = true;
+		} else if (type == QEvent::MouseButtonRelease) {
+			const auto down = base::take(state->down);
+			if (down && state->over) {
+				_controller->togglePaused(_pauseState != PauseState::Paused);
+			}
+		}
+	}, lifetime);
+
+	_playPause->paintRequest() | rpl::start_with_next([=] {
+		auto p = QPainter(_playPause.get());
+		const auto paused = (_pauseState == PauseState::Paused);
+		const auto icon = paused
+			? &st::storiesPlayIcon
+			: &st::storiesPauseIcon;
+		const auto over = state->overAnimation.value(
+			state->over ? 1. : 0.);
+		p.setOpacity(over * kControlOpacityOver
+			+ (1. - over) * kControlOpacity);
+		icon->paint(
+			p,
+			st::storiesPlayButton.iconPosition,
+			_playPause->width());
+	}, lifetime);
+
+	_playPause->resize(
+		st::storiesPlayButton.width,
+		st::storiesPlayButton.height);
+	_playPause->show();
+	_playPause->setCursor(style::cur_pointer);
+}
+
+void Header::createVolumeToggle() {
+	struct VolumeState {
+		base::Timer hideTimer;
+		bool over = false;
+		bool dropdownOver = false;
+	};
+	_volumeToggle = std::make_unique<Ui::RpWidget>(_widget.get());
+	auto &lifetime = _volumeToggle->lifetime();
+	const auto state = lifetime.make_state<VolumeState>();
+	state->hideTimer.setCallback([=] {
+		_volume->toggle(false, anim::type::normal);
+	});
+
+	_volumeToggle->events(
+	) | rpl::start_with_next([=](not_null<QEvent*> e) {
+		const auto type = e->type();
+		if (type == QEvent::Enter || type == QEvent::Leave) {
+			const auto over = (e->type() == QEvent::Enter);
+			if (state->over != over) {
+				state->over = over;
+				if (over) {
+					state->hideTimer.cancel();
+					_volume->toggle(true, anim::type::normal);
+				} else if (!state->dropdownOver) {
+					state->hideTimer.callOnce(kVolumeHideTimeoutShort);
+				}
+			}
+		}
+	}, lifetime);
+
+	_volumeToggle->paintRequest() | rpl::start_with_next([=] {
+		auto p = QPainter(_volumeToggle.get());
+		p.setOpacity(kControlOpacity);
+		_volumeIcon.current()->paint(
+			p,
+			st::storiesVolumeButton.iconPosition,
+			_volumeToggle->width());
+	}, lifetime);
+	updateVolumeIcon();
+
+	_volume = std::make_unique<Ui::FadeWrap<Ui::RpWidget>>(
+		_widget->parentWidget(),
+		object_ptr<Ui::RpWidget>(_widget->parentWidget()));
+	_volume->events(
+	) | rpl::start_with_next([=](not_null<QEvent*> e) {
+		const auto type = e->type();
+		if (type == QEvent::Enter || type == QEvent::Leave) {
+			const auto over = (e->type() == QEvent::Enter);
+			if (state->dropdownOver != over) {
+				state->dropdownOver = over;
+				if (over) {
+					state->hideTimer.cancel();
+					_volume->toggle(true, anim::type::normal);
+				} else if (!state->over) {
+					state->hideTimer.callOnce(kVolumeHideTimeoutLong);
+				}
+			}
+		}
+	}, lifetime);
+	_controller->layoutValue(
+	) | rpl::map([](const Layout &layout) {
+		return (layout.headerLayout == HeaderLayout::Outside);
+	}) | rpl::distinct_until_changed(
+	) | rpl::start_with_next([=](bool horizontal) {
+		rebuildVolumeControls(_volume->entity(), horizontal);
+	}, lifetime);
+
+	rpl::combine(
+		_widget->positionValue(),
+		_volumeToggle->positionValue(),
+		rpl::mappers::_1 + rpl::mappers::_2
+	) | rpl::start_with_next([=](QPoint position) {
+		_volume->move(position);
+	}, _volume->lifetime());
+
+	_volumeToggle->resize(
+		st::storiesVolumeButton.width,
+		st::storiesVolumeButton.height);
+	_volumeToggle->show();
+	_volumeToggle->setCursor(style::cur_pointer);
+}
+
+void Header::rebuildVolumeControls(
+		not_null<Ui::RpWidget*> dropdown,
+		bool horizontal) {
+	auto removed = false;
+	do {
+		removed = false;
+		for (const auto &child : dropdown->children()) {
+			if (child->isWidgetType()) {
+				removed = true;
+				delete child;
+				break;
+			}
+		}
+	} while (removed);
+
+	const auto button = Ui::CreateChild<Ui::IconButton>(
+		dropdown.get(),
+		st::storiesVolumeButton);
+	_volumeIcon.value(
+	) | rpl::start_with_next([=](const style::icon *icon) {
+		button->setIconOverride(icon, icon);
+	}, button->lifetime());
+
+	const auto slider = Ui::CreateChild<Ui::MediaSlider>(
+		dropdown.get(),
+		st::storiesVolumeSlider);
+	slider->setMoveByWheel(true);
+	slider->setAlwaysDisplayMarker(true);
+	using Direction = Ui::MediaSlider::Direction;
+	slider->setDirection(horizontal
+		? Direction::Horizontal
+		: Direction::Vertical);
+
+	slider->setChangeProgressCallback([=](float64 value) {
+		_controller->changeVolume(value);
+		updateVolumeIcon();
+	});
+	slider->setChangeFinishedCallback([=](float64 value) {
+		_controller->volumeChangeFinished();
+	});
+	button->setClickedCallback([=] {
+		_controller->toggleVolume();
+		slider->setValue(_controller->currentVolume());
+		updateVolumeIcon();
+	});
+	slider->setValue(_controller->currentVolume());
+
+	const auto skip = button->width() / 2;
+	const auto size = button->width()
+		+ st::storiesVolumeSize
+		+ st::storiesVolumeBottom;
+	const auto seekSize = st::storiesVolumeSlider.seekSize;
+
+	button->move(0, 0);
+	if (horizontal) {
+		dropdown->resize(size, button->height());
+		slider->resize(st::storiesVolumeSize, seekSize.height());
+		slider->move(
+			button->width(),
+			(button->height() - slider->height()) / 2);
+	} else {
+		dropdown->resize(button->width(), size);
+		slider->resize(seekSize.width(), st::storiesVolumeSize);
+		slider->move(
+			(button->width() - slider->width()) / 2,
+			button->height());
+	}
+
+	dropdown->paintRequest(
+	) | rpl::start_with_next([=] {
+		auto p = QPainter(dropdown);
+		auto hq = PainterHighQualityEnabler(p);
+		const auto radius = button->width() / 2.;
+		p.setPen(Qt::NoPen);
+		p.setBrush(st::mediaviewSaveMsgBg);
+		p.drawRoundedRect(dropdown->rect(), radius, radius);
+	}, button->lifetime());
+}
+
 void Header::updatePauseState() {
 	if (!_playPause) {
 		return;
@@ -380,17 +596,25 @@ void Header::updatePauseState() {
 	}
 }
 
+void Header::updateVolumeIcon() {
+	const auto volume = _controller->currentVolume();
+	_volumeIcon = (volume <= 0.)
+		? &st::mediaviewVolumeIcon0Over
+		: (volume < 1 / 2.)
+		? &st::mediaviewVolumeIcon1Over
+		: &st::mediaviewVolumeIcon2Over;
+}
+
 void Header::applyPauseState() {
 	Expects(_playPause != nullptr);
 
-	const auto paused = (_pauseState == PauseState::Paused);
 	const auto inactive = (_pauseState == PauseState::Inactive);
 	_playPause->setAttribute(Qt::WA_TransparentForMouseEvents, inactive);
 	if (inactive) {
-		_playPause->clearState();
+		QEvent e(QEvent::Leave);
+		QGuiApplication::sendEvent(_playPause.get(), &e);
 	}
-	const auto icon = paused ? nullptr : &st::storiesPauseIcon;
-	_playPause->setIconOverride(icon, icon);
+	_playPause->update();
 }
 
 void Header::raise() {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index 8685e5bc8..b20f2b533 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -49,6 +49,7 @@ public:
 	~Header();
 
 	void updatePauseState();
+	void updateVolumeIcon();
 
 	void show(HeaderData data);
 	void raise();
@@ -56,6 +57,11 @@ public:
 private:
 	void updateDateText();
 	void applyPauseState();
+	void createPlayPause();
+	void createVolumeToggle();
+	void rebuildVolumeControls(
+		not_null<Ui::RpWidget*> dropdown, 
+		bool horizontal);
 
 	const not_null<Controller*> _controller;
 
@@ -66,9 +72,10 @@ private:
 	std::unique_ptr<Ui::UserpicButton> _userpic;
 	std::unique_ptr<Ui::FlatLabel> _name;
 	std::unique_ptr<Ui::FlatLabel> _date;
-	std::unique_ptr<Ui::IconButton> _playPause;
-	std::unique_ptr<Ui::IconButton> _volumeToggle;
+	std::unique_ptr<Ui::RpWidget> _playPause;
+	std::unique_ptr<Ui::RpWidget> _volumeToggle;
 	std::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _volume;
+	rpl::variable<const style::icon*> _volumeIcon;
 	std::unique_ptr<Ui::RpWidget> _privacy;
 	std::optional<HeaderData> _data;
 	base::Timer _dateUpdateTimer;
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index a3ae2dac0..580347539 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -868,8 +868,22 @@ storiesPlayButton: IconButton(defaultIconButton) {
 }
 storiesPlayButtonPosition: point(54px, 0px);
 storiesVolumeButton: IconButton(storiesPlayButton) {
-	icon: mediaviewVolumeIcon2Over;
-	iconOver: mediaviewVolumeIcon2Over;
+	icon: mediaviewVolumeIcon0Over;
+	iconOver: mediaviewVolumeIcon0Over;
 	ripple: emptyRippleAnimation;
 }
 storiesVolumeButtonPosition: point(10px, 0px);
+storiesVolumeSize: 75px;
+storiesVolumeBottom: 20px;
+storiesVolumeSlider: MediaSlider {
+	width: 3px;
+	activeFg: mediaviewPlaybackActiveOver;
+	inactiveFg: mediaviewPlaybackActive;
+	activeFgOver: mediaviewPlaybackActiveOver;
+	inactiveFgOver: mediaviewPlaybackActive;
+	activeFgDisabled: mediaviewPlaybackActiveOver;
+	inactiveFgDisabled: mediaviewPlaybackActive;
+	receivedTillFg: mediaviewPlaybackActive;
+	seekSize: size(12px, 12px);
+	duration: mediaviewOverDuration;
+}
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 379c341c0..dfa0f9085 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -4219,10 +4219,23 @@ float64 OverlayWidget::storiesSiblingOver(Stories::SiblingType type) {
 		? overLevel(Over::LeftStories)
 		: overLevel(Over::RightStories);
 }
+
 void OverlayWidget::storiesRepaint() {
 	update();
 }
 
+void OverlayWidget::storiesVolumeToggle() {
+	playbackControlsVolumeToggled();
+}
+
+void OverlayWidget::storiesVolumeChanged(float64 volume) {
+	playbackControlsVolumeChanged(volume);
+}
+
+void OverlayWidget::storiesVolumeChangeFinished() {
+	playbackControlsVolumeChangeFinished();
+}
+
 void OverlayWidget::playbackToggleFullScreen() {
 	Expects(_streamed != nullptr);
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index b5be1b16c..cbb9165d9 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -261,6 +261,9 @@ private:
 	void storiesTogglePaused(bool paused) override;
 	float64 storiesSiblingOver(Stories::SiblingType type) override;
 	void storiesRepaint() override;
+	void storiesVolumeToggle() override;
+	void storiesVolumeChanged(float64 volume) override;
+	void storiesVolumeChangeFinished() override;
 
 	void hideControls(bool force = false);
 	void subscribeToScreenGeometry();

From 92f2b6dfbfc0de1c29111bd452199e68869c307a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 10:00:29 +0400
Subject: [PATCH 210/259] Fix unintentional stories viewer move-by-mouse-drag.

---
 .../view/controls/history_view_compose_controls.cpp        | 7 +++++++
 .../history/view/controls/history_view_compose_controls.h  | 1 +
 .../view/controls/history_view_voice_record_bar.cpp        | 4 ++++
 .../history/view/controls/history_view_voice_record_bar.h  | 1 +
 .../SourceFiles/media/stories/media_stories_controller.cpp | 5 +++++
 .../SourceFiles/media/stories/media_stories_controller.h   | 2 ++
 .../SourceFiles/media/stories/media_stories_header.cpp     | 7 +++++++
 Telegram/SourceFiles/media/stories/media_stories_header.h  | 3 +++
 Telegram/SourceFiles/media/stories/media_stories_reply.cpp | 4 ++++
 Telegram/SourceFiles/media/stories/media_stories_reply.h   | 2 ++
 Telegram/SourceFiles/media/stories/media_stories_view.cpp  | 4 ++++
 Telegram/SourceFiles/media/stories/media_stories_view.h    | 2 ++
 .../SourceFiles/media/view/media_view_overlay_widget.cpp   | 2 ++
 Telegram/lib_ui                                            | 2 +-
 14 files changed, 45 insertions(+), 1 deletion(-)

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 e1b32aa7d..8d72bfbce 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -2962,6 +2962,13 @@ bool ComposeControls::isRecording() const {
 	return _voiceRecordBar->isRecording();
 }
 
+bool ComposeControls::isRecordingPressed() const {
+	return !_voiceRecordBar->isRecordingLocked()
+		&& (!_voiceRecordBar->isHidden()
+			|| (_send->type() == Ui::SendButton::Type::Record
+				&& _send->isDown()));
+}
+
 rpl::producer<bool> ComposeControls::recordingValue() const {
 	return _recording.value();
 }
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 518782954..ce734f280 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
@@ -216,6 +216,7 @@ public:
 	[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
 	[[nodiscard]] bool isLockPresent() const;
 	[[nodiscard]] bool isRecording() const;
+	[[nodiscard]] bool isRecordingPressed() const;
 	[[nodiscard]] rpl::producer<bool> recordingValue() const;
 	[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
 
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp
index c49495542..757b85444 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
@@ -1581,6 +1581,10 @@ bool VoiceRecordBar::isRecording() const {
 	return _recording.current();
 }
 
+bool VoiceRecordBar::isRecordingLocked() const {
+	return isRecording() && _lock->isLocked();
+}
+
 bool VoiceRecordBar::isActive() const {
 	return isRecording() || isListenState();
 }
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 97fed00d9..2d04e98a7 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
@@ -89,6 +89,7 @@ public:
 	void setStartRecordingFilter(Fn<bool()> &&callback);
 
 	[[nodiscard]] bool isRecording() const;
+	[[nodiscard]] bool isRecordingLocked() const;
 	[[nodiscard]] bool isLockPresent() const;
 	[[nodiscard]] bool isListenState() const;
 	[[nodiscard]] bool isActive() const;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 99ab50de7..f6d4ad519 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -1443,6 +1443,11 @@ void Controller::moveFromShown() {
 	}
 }
 
+bool Controller::ignoreWindowMove(QPoint position) const {
+	return _replyArea->ignoreWindowMove(position)
+		|| _header->ignoreWindowMove(position);
+}
+
 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 8b804c598..18951e7ad 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -160,6 +160,8 @@ public:
 	void reportRequested();
 	void togglePinnedRequested(bool pinned);
 
+	[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
+
 	[[nodiscard]] rpl::lifetime &lifetime();
 
 private:
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 04a0e3f25..0d05837a9 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -542,10 +542,12 @@ void Header::rebuildVolumeControls(
 		: Direction::Vertical);
 
 	slider->setChangeProgressCallback([=](float64 value) {
+		_ignoreWindowMove = true;
 		_controller->changeVolume(value);
 		updateVolumeIcon();
 	});
 	slider->setChangeFinishedCallback([=](float64 value) {
+		_ignoreWindowMove = false;
 		_controller->volumeChangeFinished();
 	});
 	button->setClickedCallback([=] {
@@ -623,6 +625,11 @@ void Header::raise() {
 	}
 }
 
+
+bool Header::ignoreWindowMove(QPoint position) const {
+	return _ignoreWindowMove;
+}
+
 void Header::updateDateText() {
 	if (!_date || !_data || !_data->date) {
 		return;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index b20f2b533..f83749482 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -54,6 +54,8 @@ public:
 	void show(HeaderData data);
 	void raise();
 
+	[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
+
 private:
 	void updateDateText();
 	void applyPauseState();
@@ -79,6 +81,7 @@ private:
 	std::unique_ptr<Ui::RpWidget> _privacy;
 	std::optional<HeaderData> _data;
 	base::Timer _dateUpdateTimer;
+	bool _ignoreWindowMove = false;
 
 };
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 9fa53b529..339a92181 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -677,6 +677,10 @@ rpl::producer<bool> ReplyArea::activeValue() const {
 	) | rpl::distinct_until_changed();
 }
 
+bool ReplyArea::ignoreWindowMove(QPoint position) const {
+	return _controls->isRecordingPressed();
+}
+
 void ReplyArea::showPremiumToast(not_null<DocumentData*> emoji) {
 	// #TODO stories
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h
index eeaa54726..25594c59b 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h
@@ -67,6 +67,8 @@ public:
 	[[nodiscard]] rpl::producer<bool> activeValue() const;
 	[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
 
+	[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
+
 private:
 	class Cant;
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
index 6a6a0dbd4..63fe641da 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
@@ -103,6 +103,10 @@ void View::togglePinnedRequested(bool pinned) {
 	_controller->togglePinnedRequested(pinned);
 }
 
+bool View::ignoreWindowMove(QPoint position) const {
+	return _controller->ignoreWindowMove(position);
+}
+
 SiblingView View::sibling(SiblingType type) const {
 	return _controller->sibling(type);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h
index 24b58feed..2e9ce1a74 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -82,6 +82,8 @@ public:
 	void reportRequested();
 	void togglePinnedRequested(bool pinned);
 
+	[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
+
 	[[nodiscard]] rpl::lifetime &lifetime();
 
 private:
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index dfa0f9085..1d36a6282 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -706,6 +706,8 @@ void OverlayWidget::setupWindow() {
 				&& (widgetPoint.y() > st::mediaviewHeaderTop)
 				&& QRect(_x, _y, _w, _h).contains(widgetPoint)) {
 			return Flag::None | Flag(0);
+		} else if (_stories && _stories->ignoreWindowMove(widgetPoint)) {
+			return Flag::None | Flag(0);
 		}
 		return Flag::Move | Flag(0);
 	});
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 048156ecd..ae4651092 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 048156ecda89b54ea48aafd5b0f97ccffcc922c9
+Subproject commit ae465109201b30fa61b643d72aaf374265c6a9ed

From 7d067d492470978177a482322fa429c6972c0de5 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 10:20:53 +0400
Subject: [PATCH 211/259] Fix build with Xcode.

---
 Telegram/SourceFiles/media/view/media_view_pip.cpp | 1 +
 Telegram/lib_ui                                    | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp
index 4a0183d96..c11db5196 100644
--- a/Telegram/SourceFiles/media/view/media_view_pip.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp
@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/power_save_blocker.h"
 #include "base/event_filter.h"
 #include "ui/platform/ui_platform_utility.h"
+#include "ui/platform/ui_platform_window_title.h"
 #include "ui/widgets/buttons.h"
 #include "ui/wrap/fade_wrap.h"
 #include "ui/widgets/shadow.h"
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index ae4651092..8f03125ec 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit ae465109201b30fa61b643d72aaf374265c6a9ed
+Subproject commit 8f03125ec1d6b0f4a3f0fe6b6849aa57a1006063

From 150cbe286626e44db86053ea78061461b914de3d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 10:21:04 +0400
Subject: [PATCH 212/259] Support silent video volume toggle state.

---
 .../media/stories/media_stories_header.cpp    | 21 +++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 0d05837a9..654d54102 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -31,8 +31,9 @@ namespace {
 
 constexpr auto kNameOpacity = 1.;
 constexpr auto kDateOpacity = 0.8;
-constexpr auto kControlOpacity = 0.6;
+constexpr auto kControlOpacity = 0.65;
 constexpr auto kControlOpacityOver = 1.;
+constexpr auto kControlOpacityDisabled = 0.45;
 constexpr auto kVolumeHideTimeoutShort = crl::time(20);
 constexpr auto kVolumeHideTimeoutLong = crl::time(200);
 
@@ -427,20 +428,27 @@ void Header::createPlayPause() {
 }
 
 void Header::createVolumeToggle() {
+	Expects(_data.has_value());
+
 	struct VolumeState {
 		base::Timer hideTimer;
 		bool over = false;
+		bool silent = false;
 		bool dropdownOver = false;
 	};
 	_volumeToggle = std::make_unique<Ui::RpWidget>(_widget.get());
 	auto &lifetime = _volumeToggle->lifetime();
 	const auto state = lifetime.make_state<VolumeState>();
+	state->silent = _data->silent;
 	state->hideTimer.setCallback([=] {
 		_volume->toggle(false, anim::type::normal);
 	});
 
 	_volumeToggle->events(
 	) | rpl::start_with_next([=](not_null<QEvent*> e) {
+		if (state->silent) {
+			return;
+		}
 		const auto type = e->type();
 		if (type == QEvent::Enter || type == QEvent::Leave) {
 			const auto over = (e->type() == QEvent::Enter);
@@ -458,7 +466,9 @@ void Header::createVolumeToggle() {
 
 	_volumeToggle->paintRequest() | rpl::start_with_next([=] {
 		auto p = QPainter(_volumeToggle.get());
-		p.setOpacity(kControlOpacity);
+		p.setOpacity(state->silent
+			? kControlOpacityDisabled
+			: kControlOpacity);
 		_volumeIcon.current()->paint(
 			p,
 			st::storiesVolumeButton.iconPosition,
@@ -469,6 +479,7 @@ void Header::createVolumeToggle() {
 	_volume = std::make_unique<Ui::FadeWrap<Ui::RpWidget>>(
 		_widget->parentWidget(),
 		object_ptr<Ui::RpWidget>(_widget->parentWidget()));
+	_volume->toggle(false, anim::type::instant);
 	_volume->events(
 	) | rpl::start_with_next([=](not_null<QEvent*> e) {
 		const auto type = e->type();
@@ -505,7 +516,9 @@ void Header::createVolumeToggle() {
 		st::storiesVolumeButton.width,
 		st::storiesVolumeButton.height);
 	_volumeToggle->show();
-	_volumeToggle->setCursor(style::cur_pointer);
+	if (!state->silent) {
+		_volumeToggle->setCursor(style::cur_pointer);
+	}
 }
 
 void Header::rebuildVolumeControls(
@@ -600,7 +613,7 @@ void Header::updatePauseState() {
 
 void Header::updateVolumeIcon() {
 	const auto volume = _controller->currentVolume();
-	_volumeIcon = (volume <= 0.)
+	_volumeIcon = (volume <= 0. || (_data && _data->silent))
 		? &st::mediaviewVolumeIcon0Over
 		: (volume < 1 / 2.)
 		? &st::mediaviewVolumeIcon1Over

From d7d493e0bf0013c42b5484830e52f41417a41902 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 13:55:18 +0400
Subject: [PATCH 213/259] Don't open stories on saved messages click.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 0a7c92a52..90722d7ea 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -528,7 +528,9 @@ void Widget::chosenRow(const ChosenRow &row) {
 		const auto peer = history->peer;
 		if (const auto user = history->peer->asUser()) {
 			if (row.message.fullId.msg == ShowAtUnreadMsgId) {
-				if (row.userpicClick && user->hasActiveStories()) {
+				if (row.userpicClick
+					&& user->hasActiveStories()
+					&& !user->isSelf()) {
 					controller()->openPeerStories(user->id);
 					return;
 				}

From f817df9d7fb107d9196d94f22c9ca6250b902c16 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Sat, 20 May 2023 18:20:10 +0400
Subject: [PATCH 214/259] Use new glibmm 2.78 API

---
 .../platform/linux/integration_linux.cpp      |  22 +--
 .../linux/linux_xdp_open_with_dialog.cpp      |  33 ++--
 .../platform/linux/main_window_linux.cpp      |  13 +-
 .../linux/notifications_manager_linux.cpp     | 147 +++++++++---------
 .../platform/linux/specific_linux.cpp         |  29 ++--
 Telegram/build/docker/centos_env/Dockerfile   |   4 +-
 cmake                                         |   2 +-
 snap/snapcraft.yaml                           |   2 +-
 8 files changed, 123 insertions(+), 129 deletions(-)

diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp
index 26ed0d9fe..05cf6be27 100644
--- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp
@@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "platform/platform_integration.h"
 #include "base/platform/base_platform_info.h"
-#include "base/platform/linux/base_linux_glibmm_helper.h"
 #include "base/platform/linux/base_linux_xdp_utilities.h"
 #include "core/sandbox.h"
 #include "core/application.h"
@@ -125,7 +124,7 @@ LinuxIntegration::LinuxIntegration()
 	if (group == "org.freedesktop.appearance"
 		&& key == "color-scheme") {
 		try {
-			const auto ivalue = base::Platform::GlibVariantCast<uint>(value);
+			const auto ivalue = value.get_dynamic<uint>();
 
 			crl::on_main([=] {
 				Core::App().settings().setSystemDarkMode(ivalue == 1);
@@ -266,8 +265,9 @@ void LinuxIntegration::LaunchNativeApplication() {
 
 	const auto notificationIdVariantType = [] {
 		try {
-			return base::Platform::MakeGlibVariant(
-				NotificationId().toTuple()).get_type();
+			return Glib::create_variant(
+				NotificationId().toTuple()
+			).get_type();
 		} catch (...) {
 			return Glib::VariantType();
 		}
@@ -282,9 +282,9 @@ void LinuxIntegration::LaunchNativeApplication() {
 					const auto &app = Core::App();
 					app.notifications().manager().notificationActivated(
 						NotificationId::FromTuple(
-							base::Platform::GlibVariantCast<
-								NotificationIdTuple
-							>(parameter)));
+							parameter.get_dynamic<NotificationIdTuple>()
+						)
+					);
 				} catch (...) {
 				}
 			});
@@ -299,10 +299,10 @@ void LinuxIntegration::LaunchNativeApplication() {
 					const auto &app = Core::App();
 					app.notifications().manager().notificationReplied(
 						NotificationId::FromTuple(
-							base::Platform::GlibVariantCast<
-								NotificationIdTuple
-							>(parameter)),
-						{});
+							parameter.get_dynamic<NotificationIdTuple>()
+						),
+						{}
+					);
 				} catch (...) {
 				}
 			});
diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp
index 70eebdf71..c37a123a3 100644
--- a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp
+++ b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp
@@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "platform/linux/linux_xdp_open_with_dialog.h"
 
 #include "base/platform/base_platform_info.h"
-#include "base/platform/linux/base_linux_glibmm_helper.h"
 #include "base/platform/linux/base_linux_xdp_utilities.h"
 #include "base/platform/linux/base_linux_wayland_integration.h"
 #include "core/application.h"
@@ -34,20 +33,17 @@ bool ShowXDPOpenWithDialog(const QString &filepath) {
 		const auto connection = Gio::DBus::Connection::get_sync(
 			Gio::DBus::BusType::SESSION);
 
-		auto reply = connection->call_sync(
+		const auto version = connection->call_sync(
 			std::string(base::Platform::XDP::kObjectPath),
 			std::string(kPropertiesInterface),
 			"Get",
-			base::Platform::MakeGlibVariant(std::tuple{
+			Glib::create_variant(std::tuple{
 				Glib::ustring(
 					std::string(kXDPOpenURIInterface)),
 				Glib::ustring("version"),
 			}),
-			std::string(base::Platform::XDP::kService));
-
-		const auto version = base::Platform::GlibVariantCast<uint>(
-			base::Platform::GlibVariantCast<Glib::VariantBase>(
-				reply.get_child(0)));
+			std::string(base::Platform::XDP::kService)
+		).get_child(0).get_dynamic<Glib::Variant<uint>>().get();
 
 		if (version < 3) {
 			return false;
@@ -112,7 +108,7 @@ bool ShowXDPOpenWithDialog(const QString &filepath) {
 				loop->quit();
 			},
 			std::string(base::Platform::XDP::kService),
-			"org.freedesktop.portal.Request",
+			std::string(base::Platform::XDP::kRequestInterface),
 			"Response",
 			requestPath);
 
@@ -128,26 +124,23 @@ bool ShowXDPOpenWithDialog(const QString &filepath) {
 			std::string(base::Platform::XDP::kObjectPath),
 			std::string(kXDPOpenURIInterface),
 			"OpenFile",
-			Glib::VariantContainerBase::create_tuple({
-				Glib::Variant<Glib::ustring>::create(parentWindowId),
-				Glib::Variant<int>::create_handle(0),
-				Glib::Variant<std::map<
-					Glib::ustring,
-					Glib::VariantBase
-				>>::create({
+			Glib::create_variant(std::tuple{
+				parentWindowId,
+				Glib::DBusHandle(),
+				std::map<Glib::ustring, Glib::VariantBase>{
 					{
 						"handle_token",
-						Glib::Variant<Glib::ustring>::create(handleToken)
+						Glib::create_variant(handleToken)
 					},
 					{
 						"activation_token",
-						Glib::Variant<Glib::ustring>::create(activationToken)
+						Glib::create_variant(activationToken)
 					},
 					{
 						"ask",
-						Glib::Variant<bool>::create(true)
+						Glib::create_variant(true)
 					},
-				}),
+				},
 			}),
 			Gio::UnixFDList::create(std::vector<int>{ fd }),
 			outFdList,
diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp
index c0b76494e..692c68353 100644
--- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp
@@ -26,7 +26,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "window/window_controller.h"
 #include "window/window_session_controller.h"
 #include "base/platform/base_platform_info.h"
-#include "base/platform/linux/base_linux_glibmm_helper.h"
 #include "base/event_filter.h"
 #include "ui/widgets/popup_menu.h"
 #include "ui/widgets/input_fields.h"
@@ -251,13 +250,11 @@ void MainWindow::updateUnityCounter() {
 		// According to the spec, it should be of 'x' D-Bus signature,
 		// which corresponds to signed 64-bit integer
 		// https://wiki.ubuntu.com/Unity/LauncherAPI#Low_level_DBus_API:_com.canonical.Unity.LauncherEntry
-		dbusUnityProperties["count"] = Glib::Variant<int64>::create(
-			counterSlice);
-		dbusUnityProperties["count-visible"] =
-			Glib::Variant<bool>::create(true);
+		dbusUnityProperties["count"] = Glib::create_variant(
+			int64(counterSlice));
+		dbusUnityProperties["count-visible"] = Glib::create_variant(true);
 	} else {
-		dbusUnityProperties["count-visible"] =
-			Glib::Variant<bool>::create(false);
+		dbusUnityProperties["count-visible"] = Glib::create_variant(false);
 	}
 
 	try {
@@ -270,7 +267,7 @@ void MainWindow::updateUnityCounter() {
 			"com.canonical.Unity.LauncherEntry",
 			"Update",
 			{},
-			base::Platform::MakeGlibVariant(std::tuple{
+			Glib::create_variant(std::tuple{
 				launcherUrl,
 				dbusUnityProperties,
 			}));
diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
index 210064ac0..7b4f172a7 100644
--- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
@@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/options.h"
 #include "base/platform/base_platform_info.h"
-#include "base/platform/linux/base_linux_glibmm_helper.h"
 #include "base/platform/linux/base_linux_dbus_utilities.h"
 #include "core/application.h"
 #include "core/sandbox.h"
@@ -43,8 +42,6 @@ constexpr auto kObjectPath = "/org/freedesktop/Notifications"_cs;
 constexpr auto kInterface = kService;
 constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs;
 
-using namespace base::Platform;
-
 struct ServerInformation {
 	QString name;
 	QString vendor;
@@ -116,10 +113,10 @@ void StartServiceAsync(Fn<void()> callback) {
 		const auto connection = Gio::DBus::Connection::get_sync(
 			Gio::DBus::BusType::SESSION);
 
-		DBus::StartServiceByNameAsync(
+		base::Platform::DBus::StartServiceByNameAsync(
 			connection,
 			std::string(kService),
-			[=](Fn<DBus::StartReply()> result) {
+			[=](Fn<base::Platform::DBus::StartReply()> result) {
 				Noexcept([&] {
 					try {
 						result(); // get the error if any
@@ -156,7 +153,7 @@ bool GetServiceRegistered() {
 
 		const auto hasOwner = [&] {
 			try {
-				return DBus::NameHasOwner(
+				return base::Platform::DBus::NameHasOwner(
 					connection,
 					std::string(kService));
 			} catch (...) {
@@ -167,7 +164,7 @@ bool GetServiceRegistered() {
 		static const auto activatable = [&] {
 			try {
 				return ranges::contains(
-					DBus::ListActivatableNames(connection),
+					base::Platform::DBus::ListActivatableNames(connection),
 					std::string(kService),
 					&Glib::ustring::raw);
 			} catch (...) {
@@ -195,19 +192,23 @@ void GetServerInformation(
 			{},
 			[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
 				Noexcept([&] {
-					auto reply = connection->call_finish(result);
+					const auto reply = connection->call_finish(result);
 
-					const auto name = GlibVariantCast<Glib::ustring>(
-						reply.get_child(0));
+					const auto name = reply.get_child(
+						0
+					).get_dynamic<Glib::ustring>();
 
-					const auto vendor = GlibVariantCast<Glib::ustring>(
-						reply.get_child(1));
+					const auto vendor = reply.get_child(
+						1
+					).get_dynamic<Glib::ustring>();
 
-					const auto version = GlibVariantCast<Glib::ustring>(
-						reply.get_child(2));
+					const auto version = reply.get_child(
+						2
+					).get_dynamic<Glib::ustring>();
 
-					const auto specVersion = GlibVariantCast<Glib::ustring>(
-						reply.get_child(3));
+					const auto specVersion = reply.get_child(
+						3
+					).get_dynamic<Glib::ustring>();
 
 					crl::on_main([=] {
 						callback(ServerInformation{
@@ -241,18 +242,17 @@ void GetCapabilities(Fn<void(const QStringList &)> callback) {
 			{},
 			[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
 				Noexcept([&] {
-					auto reply = connection->call_finish(result);
-
 					QStringList value;
 					ranges::transform(
-						GlibVariantCast<std::vector<Glib::ustring>>(
-							reply.get_child(0)),
+						connection->call_finish(
+							result
+						).get_child(
+							0
+						).get_dynamic<std::vector<Glib::ustring>>(),
 						ranges::back_inserter(value),
 						QString::fromStdString);
 
-					crl::on_main([=] {
-						callback(value);
-					});
+					crl::on_main([=] { callback(value); });
 				}, [&] {
 					crl::on_main([=] { callback({}); });
 				});
@@ -272,21 +272,20 @@ void GetInhibited(Fn<void(bool)> callback) {
 			std::string(kObjectPath),
 			std::string(kPropertiesInterface),
 			"Get",
-			MakeGlibVariant(std::tuple{
+			Glib::create_variant(std::tuple{
 				Glib::ustring(std::string(kInterface)),
 				Glib::ustring("Inhibited"),
 			}),
 			[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
 				Noexcept([&] {
-					auto reply = connection->call_finish(result);
+					const auto value = connection->call_finish(
+						result
+					).get_child(
+						0
+					).get_dynamic<Glib::Variant<bool>>(
+					).get();
 
-					const auto value = GlibVariantCast<bool>(
-						GlibVariantCast<Glib::VariantBase>(
-							reply.get_child(0)));
-
-					crl::on_main([=] {
-						callback(value);
-					});
+					crl::on_main([=] { callback(value); });
 				}, [&] {
 					crl::on_main([=] { callback(false); });
 				});
@@ -470,38 +469,38 @@ bool NotificationData::init(
 			const Glib::ustring &object_path,
 			const Glib::ustring &interface_name,
 			const Glib::ustring &signal_name,
-			Glib::VariantContainerBase parameters) {
+			const Glib::VariantContainerBase &parameters) {
 		Noexcept([&] {
 			if (signal_name == "ActionInvoked") {
-				const auto id = GlibVariantCast<uint>(
-					parameters.get_child(0));
+				const auto id = parameters.get_child(0).get_dynamic<uint>();
 
-				const auto actionName = GlibVariantCast<Glib::ustring>(
-					parameters.get_child(1));
+				const auto actionName = parameters.get_child(
+					1
+				).get_dynamic<Glib::ustring>();
 
 				crl::on_main(weak, [=] { actionInvoked(id, actionName); });
 			} else if (signal_name == "ActivationToken") {
-				const auto id = GlibVariantCast<uint>(
-					parameters.get_child(0));
+				const auto id = parameters.get_child(0).get_dynamic<uint>();
 
-				const auto token = GlibVariantCast<Glib::ustring>(
-					parameters.get_child(1));
+				const auto token = parameters.get_child(
+					1
+				).get_dynamic<Glib::ustring>();
 
 				crl::on_main(weak, [=] { activationToken(id, token); });
 			} else if (signal_name == "NotificationReplied") {
-				const auto id = GlibVariantCast<uint>(
-					parameters.get_child(0));
+				const auto id = parameters.get_child(0).get_dynamic<uint>();
 
-				const auto text = GlibVariantCast<Glib::ustring>(
-					parameters.get_child(1));
+				const auto text = parameters.get_child(
+					1
+				).get_dynamic<Glib::ustring>();
 
 				crl::on_main(weak, [=] { notificationReplied(id, text); });
 			} else if (signal_name == "NotificationClosed") {
-				const auto id = GlibVariantCast<uint>(
-					parameters.get_child(0));
+				const auto id = parameters.get_child(0).get_dynamic<uint>();
 
-				const auto reason = GlibVariantCast<uint>(
-					parameters.get_child(1));
+				const auto reason = parameters.get_child(
+					1
+				).get_dynamic<uint>();
 
 				crl::on_main(weak, [=] { notificationClosed(id, reason); });
 			}
@@ -568,30 +567,30 @@ bool NotificationData::init(
 	}
 
 	if (capabilities.contains("action-icons")) {
-		_hints["action-icons"] = Glib::Variant<bool>::create(true);
+		_hints["action-icons"] = Glib::create_variant(true);
 	}
 
 	// suppress system sound if telegram sound activated,
 	// otherwise use system sound
 	if (capabilities.contains("sound")) {
 		if (Core::App().settings().soundNotify()) {
-			_hints["suppress-sound"] = Glib::Variant<bool>::create(true);
+			_hints["suppress-sound"] = Glib::create_variant(true);
 		} else {
 			// sound name according to http://0pointer.de/public/sound-naming-spec.html
-			_hints["sound-name"] = Glib::Variant<Glib::ustring>::create(
-				"message-new-instant");
+			_hints["sound-name"] = Glib::create_variant(
+				Glib::ustring("message-new-instant"));
 		}
 	}
 
 	if (capabilities.contains("x-canonical-append")) {
-		_hints["x-canonical-append"] = Glib::Variant<Glib::ustring>::create(
-			"true");
+		_hints["x-canonical-append"] = Glib::create_variant(
+			Glib::ustring("true"));
 	}
 
-	_hints["category"] = Glib::Variant<Glib::ustring>::create("im.received");
+	_hints["category"] = Glib::create_variant(Glib::ustring("im.received"));
 
-	_hints["desktop-entry"] = Glib::Variant<Glib::ustring>::create(
-		QGuiApplication::desktopFileName().toStdString());
+	_hints["desktop-entry"] = Glib::create_variant(
+		Glib::ustring(QGuiApplication::desktopFileName().toStdString()));
 
 	_notificationClosedSignalId = _dbusConnection->signal_subscribe(
 		signalEmitted,
@@ -647,7 +646,7 @@ void NotificationData::show() {
 			std::string(kObjectPath),
 			std::string(kInterface),
 			"Notify",
-			MakeGlibVariant(std::tuple{
+			Glib::create_variant(std::tuple{
 				Glib::ustring(std::string(AppName)),
 				uint(0),
 				iconName,
@@ -659,9 +658,12 @@ void NotificationData::show() {
 			}),
 			[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
 				Noexcept([&] {
-					auto reply = connection->call_finish(result);
-					const auto notificationId = GlibVariantCast<uint>(
-						reply.get_child(0));
+					const auto notificationId = connection->call_finish(
+						result
+					).get_child(
+						0
+					).get_dynamic<uint>();
+
 					crl::on_main(weak, [=] {
 						_notificationId = notificationId;
 					});
@@ -691,7 +693,7 @@ void NotificationData::close() {
 		std::string(kObjectPath),
 		std::string(kInterface),
 		"CloseNotification",
-		MakeGlibVariant(std::tuple{
+		Glib::create_variant(std::tuple{
 			_notificationId,
 		}),
 		{},
@@ -728,7 +730,7 @@ void NotificationData::setImage(const QImage &image) {
 		? image.convertToFormat(QImage::Format_RGBA8888)
 		: image.convertToFormat(QImage::Format_RGB888);
 
-	_hints[_imageKey] = MakeGlibVariant(std::tuple{
+	_hints[_imageKey] = Glib::create_variant(std::tuple{
 		convertedImage.width(),
 		convertedImage.height(),
 		int(convertedImage.bytesPerLine()),
@@ -979,19 +981,22 @@ Manager::Private::Private(not_null<Manager*> manager)
 					const Glib::ustring &object_path,
 					const Glib::ustring &interface_name,
 					const Glib::ustring &signal_name,
-					Glib::VariantContainerBase parameters) {
+					const Glib::VariantContainerBase &parameters) {
 				Noexcept([&] {
-					const auto interface = GlibVariantCast<Glib::ustring>(
-						parameters.get_child(0));
+					const auto interface = parameters.get_child(
+						0
+					).get_dynamic<Glib::ustring>();
 
 					if (interface != kInterface.data()) {
 						return;
 					}
 
-					const auto inhibited = GlibVariantCast<bool>(
-						GlibVariantCast<
-							std::map<Glib::ustring, Glib::VariantBase>
-					>(parameters.get_child(1)).at("Inhibited"));
+					const auto inhibited = parameters.get_child(
+						1
+					).get_dynamic<std::map<Glib::ustring, Glib::VariantBase>>(
+					).at(
+						"Inhibited"
+					).get_dynamic<bool>();
 
 					crl::on_main(weak, [=] {
 						_inhibited = inhibited;
diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
index 9e9fe63ba..7e20b946a 100644
--- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
@@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/random.h"
 #include "base/platform/base_platform_info.h"
-#include "base/platform/linux/base_linux_glibmm_helper.h"
 #include "base/platform/linux/base_linux_dbus_utilities.h"
 #include "base/platform/linux/base_linux_xdp_utilities.h"
 #include "platform/linux/linux_desktop_environment.h"
@@ -90,13 +89,13 @@ bool PortalAutostart(bool start, bool silent) {
 		commandline.push_back("-autostart");
 
 		std::map<Glib::ustring, Glib::VariantBase> options;
-		options["handle_token"] = Glib::Variant<Glib::ustring>::create(
-			handleToken);
-		options["reason"] = Glib::Variant<Glib::ustring>::create(
-			tr::lng_settings_auto_start(tr::now).toStdString());
-		options["autostart"] = Glib::Variant<bool>::create(start);
-		options["commandline"] = base::Platform::MakeGlibVariant(commandline);
-		options["dbus-activatable"] = Glib::Variant<bool>::create(false);
+		options["handle_token"] = Glib::create_variant(handleToken);
+		options["reason"] = Glib::create_variant(
+			Glib::ustring(
+				tr::lng_settings_auto_start(tr::now).toStdString()));
+		options["autostart"] = Glib::create_variant(start);
+		options["commandline"] = Glib::create_variant(commandline);
+		options["dbus-activatable"] = Glib::create_variant(false);
 
 		auto uniqueName = connection->get_unique_name();
 		uniqueName.erase(0, 1);
@@ -117,10 +116,11 @@ bool PortalAutostart(bool start, bool silent) {
 				const Glib::ustring &object_path,
 				const Glib::ustring &interface_name,
 				const Glib::ustring &signal_name,
-				Glib::VariantContainerBase parameters) {
+				const Glib::VariantContainerBase &parameters) {
 				try {
-					const auto response = base::Platform::GlibVariantCast<
-						uint>(parameters.get_child(0));
+					const auto response = parameters.get_child(
+						0
+					).get_dynamic<uint>();
 
 					if (response) {
 						if (!silent) {
@@ -139,7 +139,7 @@ bool PortalAutostart(bool start, bool silent) {
 				loop->quit();
 			},
 			std::string(base::Platform::XDP::kService),
-			"org.freedesktop.portal.Request",
+			std::string(base::Platform::XDP::kRequestInterface),
 			"Response",
 			requestPath);
 
@@ -153,7 +153,7 @@ bool PortalAutostart(bool start, bool silent) {
 			std::string(base::Platform::XDP::kObjectPath),
 			"org.freedesktop.portal.Background",
 			"RequestBackground",
-			base::Platform::MakeGlibVariant(std::tuple{
+			Glib::create_variant(std::tuple{
 				parentWindowId,
 				options,
 			}),
@@ -429,8 +429,7 @@ std::optional<bool> IsDarkMode() {
 			"color-scheme");
 
 		if (result.has_value()) {
-			const auto value = base::Platform::GlibVariantCast<uint>(*result);
-			return value == 1;
+			return result->get_dynamic<uint>() == 1;
 		}
 	} catch (...) {
 	}
diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 88fe34eb2..121afb690 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -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 523b061671d4dd2f29979c982a2d03c2cc8eaf4f \
+	&& git fetch --depth=1 origin edafe1a2484cd6ac9be357aa1e4cda61bf6a8b9a \
 	&& git reset --hard FETCH_HEAD \
 	&& rm -rf .git
 
@@ -675,7 +675,7 @@ RUN git clone -b xkbcommon-1.3.1 --depth=1 {{ GIT }}/xkbcommon/libxkbcommon.git
 FROM patches AS glibmm
 COPY --link --from=libffi {{ LibrariesPath }}/libffi-cache /
 
-RUN git clone -b 2.76.0 --depth=1 {{ GIT }}/GNOME/glibmm.git \
+RUN git clone -b 2.77.0 --depth=1 {{ GIT }}/GNOME/glibmm.git \
 	&& cd glibmm \
 	&& git apply ../patches/glibmm.patch \
 	&& CFLAGS="$CFLAGS {{ CFLAGS_LTO }}" CXXFLAGS="$CXXFLAGS {{ CFLAGS_LTO }}" meson build \
diff --git a/cmake b/cmake
index 6d67365e5..0620bb7b8 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 6d67365e52480edbde2b2596844c26b56a8644dc
+Subproject commit 0620bb7b87a0ec9195151fd5eb0cf38656c1280b
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index f6ccbea64..40a3ca637 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -248,7 +248,7 @@ parts:
   glibmm:
     source: https://github.com/GNOME/glibmm.git
     source-depth: 1
-    source-tag: 2.76.0
+    source-tag: 2.77.0
     plugin: meson
     build-packages:
       - meson

From d60d80ba6342cce371e7c1770a2d16c037474c62 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Wed, 19 Jul 2023 14:20:14 +0400
Subject: [PATCH 215/259] Reuse lib_ui's title control side deduction code

---
 Telegram/SourceFiles/media/view/media_view_pip.cpp   | 10 +---------
 .../SourceFiles/platform/platform_overlay_widget.cpp | 12 +-----------
 2 files changed, 2 insertions(+), 20 deletions(-)

diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp
index c11db5196..dbee087b0 100644
--- a/Telegram/SourceFiles/media/view/media_view_pip.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp
@@ -49,14 +49,6 @@ namespace {
 constexpr auto kPipLoaderPriority = 2;
 constexpr auto kMsInSecond = 1000;
 
-[[nodiscard]] bool IsWindowControlsOnLeft() {
-	using Control = Ui::Platform::TitleControls::Control;
-	const auto controlsLayout = Ui::Platform::TitleControlsLayout();
-	return ranges::contains(controlsLayout.left, Control::Close)
-		|| (controlsLayout.left.size() > controlsLayout.right.size()
-			&& !ranges::contains(controlsLayout.right, Control::Close));
-}
-
 [[nodiscard]] QRect ScreenFromPosition(QPoint point) {
 	const auto screen = QGuiApplication::screenAt(point);
 	const auto use = screen ? screen : QGuiApplication::primaryScreen();
@@ -1266,7 +1258,7 @@ void Pip::setupButtons() {
 			rect.y(),
 			volumeToggleWidth,
 			volumeToggleHeight);
-		if (!IsWindowControlsOnLeft()) {
+		if (!Ui::Platform::TitleControlsOnLeft()) {
 			_close.area.moveLeft(rect.x()
 				+ rect.width()
 				- (_close.area.x() - rect.x())
diff --git a/Telegram/SourceFiles/platform/platform_overlay_widget.cpp b/Telegram/SourceFiles/platform/platform_overlay_widget.cpp
index 90bda5624..03e8d32aa 100644
--- a/Telegram/SourceFiles/platform/platform_overlay_widget.cpp
+++ b/Telegram/SourceFiles/platform/platform_overlay_widget.cpp
@@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "ui/effects/animations.h"
 #include "ui/platform/ui_platform_window_title.h"
-#include "ui/platform/ui_platform_utility.h"
 #include "ui/widgets/rp_window.h"
 #include "ui/abstract_button.h"
 #include "styles/style_media_view.h"
@@ -228,16 +227,7 @@ rpl::producer<bool> DefaultOverlayWidgetHelper::controlsSideRightValue() {
 
 	return TitleControlsLayoutValue(
 	) | rpl::map([=](const TitleControls::Layout &layout) {
-		// See TitleControls::updateControlsPosition.
-		if (ranges::contains(layout.left, TitleControl::Close)) {
-			return false;
-		} else if (ranges::contains(layout.right, TitleControl::Close)) {
-			return true;
-		} else if (layout.left.size() > layout.right.size()) {
-			return false;
-		} else {
-			return true;
-		}
+		return !TitleControlsOnLeft(layout);
 	}) | rpl::distinct_until_changed();
 }
 

From 93457c8ea32008d64f1e325186f5da3dea66c67f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 18:44:40 +0400
Subject: [PATCH 216/259] Fix possible crash in legacy group participants.

---
 .../boxes/peers/edit_participants_box.cpp            | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
index c35fc9fbb..72291faf4 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
@@ -1341,11 +1341,13 @@ void ParticipantsBoxController::rebuildChatParticipants(
 		}
 	}
 	for (const auto &user : participants) {
-		if (auto row = createRow(user)) {
-			const auto raw = row.get();
-			delegate()->peerListAppendRow(std::move(row));
-			if (_stories) {
-				_stories->process(raw);
+		if (!delegate()->peerListFindRow(user->id.value)) {
+			if (auto row = createRow(user)) {
+				const auto raw = row.get();
+				delegate()->peerListAppendRow(std::move(row));
+				if (_stories) {
+					_stories->process(raw);
+				}
 			}
 		}
 	}

From dd9ea293342a7667111a6f7d310aa80971602e1d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 19:07:51 +0400
Subject: [PATCH 217/259] Close short info box by outside click in stories.

---
 Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 1d36a6282..33dc18e22 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -432,6 +432,7 @@ OverlayWidget::OverlayWidget()
 , _stateAnimation([=](crl::time now) { return stateAnimationCallback(now); })
 , _dropdown(_body, st::mediaviewDropdownMenu) {
 	_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
+	_layerBg->setHideByBackgroundClick(true);
 
 	CrashReports::SetAnnotation("OpenGL Renderer", "[not-initialized]");
 

From 7e18ecfb7858d4353dca23860e54ba47af59dbac Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 19:10:10 +0400
Subject: [PATCH 218/259] Use "My Story" instead of name in viewer.

---
 Telegram/SourceFiles/media/stories/media_stories_header.cpp  | 4 +++-
 Telegram/SourceFiles/media/stories/media_stories_sibling.cpp | 5 ++++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 654d54102..5dafcb9e7 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -236,7 +236,9 @@ void UserpicBadge::updateGeometry() {
 }
 
 [[nodiscard]] TextWithEntities ComposeName(HeaderData data) {
-	auto result = Ui::Text::Bold(data.user->shortName());
+	auto result = Ui::Text::Bold(data.user->isSelf()
+		? tr::lng_stories_my_name(tr::now)
+		: data.user->shortName());
 	if (data.fullCount) {
 		result.append(QString::fromUtf8(" \xE2\x80\xA2 %1/%2"
 		).arg(data.fullIndex + 1
diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
index b8eda7d4a..0da004993 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_photo_media.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
+#include "lang/lang_keys.h"
 #include "main/main_session.h"
 #include "media/stories/media_stories_controller.h"
 #include "media/stories/media_stories_view.h"
@@ -339,7 +340,9 @@ QImage Sibling::nameImage(const SiblingLayout &layout) {
 			.linkFontOver = font,
 		});
 	};
-	const auto text = _peer->shortName();
+	const auto text = _peer->isSelf()
+		? tr::lng_stories_my_name(tr::now)
+		: _peer->shortName();
 	if (_nameText != text) {
 		_name.reset();
 		_nameText = text;

From da5bce00d42c219cdd548cf32ff37aab89c82d0c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 19:17:46 +0400
Subject: [PATCH 219/259] Gray out names of users with non-unread stories.

---
 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 9b3e90027..a1c6f452b 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -568,14 +568,17 @@ void List::validateSegments(
 }
 
 void List::validateName(not_null<Item*> item) {
-	const auto &color = st::dialogsNameFg;
+	const auto &element = item->element;
+	const auto &color = (element.unreadCount || element.skipSmall)
+		? st::dialogsNameFg
+		: st::windowSubTextFg;
 	if (!item->nameCache.isNull() && item->nameCacheColor == color->c) {
 		return;
 	}
 	const auto &full = _st.full;
 	const auto &font = full.nameStyle.font;
 	const auto available = AvailableNameWidth(_st);
-	const auto text = Ui::Text::String(full.nameStyle, item->element.name);
+	const auto text = Ui::Text::String(full.nameStyle, element.name);
 	const auto ratio = style::DevicePixelRatio();
 	item->nameCacheColor = color->c;
 	item->nameCache = QImage(

From 0b7af5bfe3b32380f3107e349f0f214f9f1ad3ec Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 19:31:43 +0400
Subject: [PATCH 220/259] Auto-focus story reply on input start.

---
 .../stories/media_stories_controller.cpp      |  4 ++++
 .../media/stories/media_stories_controller.h  |  1 +
 .../media/stories/media_stories_reply.cpp     |  4 ++++
 .../media/stories/media_stories_reply.h       |  1 +
 .../media/stories/media_stories_view.cpp      |  4 ++++
 .../media/stories/media_stories_view.h        |  1 +
 .../media/view/media_view_overlay_widget.cpp  | 20 ++++++++++++-------
 .../media/view/media_view_overlay_widget.h    |  1 +
 8 files changed, 29 insertions(+), 7 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index f6d4ad519..b42a0bd74 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -1448,6 +1448,10 @@ bool Controller::ignoreWindowMove(QPoint position) const {
 		|| _header->ignoreWindowMove(position);
 }
 
+void Controller::tryProcessKeyInput(not_null<QKeyEvent*> e) {
+	_replyArea->tryProcessKeyInput(e);
+}
+
 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 18951e7ad..19b2de4d9 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -161,6 +161,7 @@ public:
 	void togglePinnedRequested(bool pinned);
 
 	[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
+	void tryProcessKeyInput(not_null<QKeyEvent*> e);
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 339a92181..1497a60e4 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -681,6 +681,10 @@ bool ReplyArea::ignoreWindowMove(QPoint position) const {
 	return _controls->isRecordingPressed();
 }
 
+void ReplyArea::tryProcessKeyInput(not_null<QKeyEvent*> e) {
+	_controls->tryProcessKeyInput(e);
+}
+
 void ReplyArea::showPremiumToast(not_null<DocumentData*> emoji) {
 	// #TODO stories
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h
index 25594c59b..9b9662b4a 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h
@@ -68,6 +68,7 @@ public:
 	[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
 
 	[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
+	void tryProcessKeyInput(not_null<QKeyEvent*> e);
 
 private:
 	class Cant;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
index 63fe641da..240a39b4e 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
@@ -107,6 +107,10 @@ bool View::ignoreWindowMove(QPoint position) const {
 	return _controller->ignoreWindowMove(position);
 }
 
+void View::tryProcessKeyInput(not_null<QKeyEvent*> e) {
+	_controller->tryProcessKeyInput(e);
+}
+
 SiblingView View::sibling(SiblingType type) const {
 	return _controller->sibling(type);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h
index 2e9ce1a74..099689e31 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -83,6 +83,7 @@ public:
 	void togglePinnedRequested(bool pinned);
 
 	[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
+	void tryProcessKeyInput(not_null<QKeyEvent*> e);
 
 	[[nodiscard]] rpl::lifetime &lifetime();
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 33dc18e22..9a58198e3 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -4886,14 +4886,20 @@ bool OverlayWidget::isSaveMsgShown() const {
 }
 
 void OverlayWidget::handleKeyPress(not_null<QKeyEvent*> e) {
+	if (_processingKeyPress) {
+		return;
+	}
+	_processingKeyPress = true;
+	const auto guard = gsl::finally([&] { _processingKeyPress = false; });
 	const auto key = e->key();
 	const auto modifiers = e->modifiers();
 	const auto ctrl = modifiers.testFlag(Qt::ControlModifier);
-	if (_stories && key == Qt::Key_Space && _down != Over::Video) {
-		_stories->togglePaused(!_stories->paused());
-		return;
-	}
-	if (_streamed) {
+	if (_stories) {
+		if (key == Qt::Key_Space && _down != Over::Video) {
+			_stories->togglePaused(!_stories->paused());
+			return;
+		}
+	} else if (_streamed) {
 		// Ctrl + F for full screen toggle is in eventFilter().
 		const auto toggleFull = (modifiers.testFlag(Qt::AltModifier) || ctrl)
 			&& (key == Qt::Key_Enter || key == Qt::Key_Return);
@@ -4962,9 +4968,9 @@ void OverlayWidget::handleKeyPress(not_null<QKeyEvent*> e) {
 			zoomIn();
 		} else if (key == Qt::Key_Minus || key == Qt::Key_Underscore) {
 			zoomOut();
-		} else if (key == Qt::Key_I) {
-			update();
 		}
+	} else if (_stories) {
+		_stories->tryProcessKeyInput(e);
 	}
 }
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
index cbb9165d9..b0feb4f1d 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h
@@ -690,6 +690,7 @@ private:
 	base::Timer _dropdownShowTimer;
 
 	bool _receiveMouse = true;
+	bool _processingKeyPress = false;
 
 	bool _touchPress = false;
 	bool _touchMove = false;

From 5b10b7e15ff53a4c30f0c423413295d3cd2d4027 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 10:33:45 +0400
Subject: [PATCH 221/259] Use separate record cancel text in stories.

---
 Telegram/Resources/langs/lang.strings                         | 1 +
 .../history/view/controls/history_view_compose_controls.cpp   | 1 +
 .../history/view/controls/history_view_compose_controls.h     | 1 +
 .../history/view/controls/history_view_voice_record_bar.cpp   | 4 +++-
 .../history/view/controls/history_view_voice_record_bar.h     | 1 +
 Telegram/SourceFiles/media/stories/media_stories_reply.cpp    | 1 +
 6 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 592cc337e..ce176c6d1 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2062,6 +2062,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_send_as_premium_required" = "Subscribe to {link} to be able to comment on behalf of your channels in group chats.";
 "lng_send_as_premium_required_link" = "Telegram Premium";
 "lng_record_cancel" = "Release outside this field to cancel";
+"lng_record_cancel_stories" = "Release outside to cancel";
 "lng_record_lock_cancel_sure" = "Are you sure you want to stop recording and discard your voice message?";
 "lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded voice message?";
 "lng_record_lock_discard" = "Discard";
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 8d72bfbce..b18fba987 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -996,6 +996,7 @@ ComposeControls::ComposeControls(
 		.outerContainer = parent,
 		.show = _show,
 		.send = _send,
+		.customCancelText = descriptor.voiceCustomCancelText,
 		.stOverride = &_st.record,
 		.recorderHeight = st::historySendSize.height(),
 		.lockFromBottom = descriptor.voiceLockFromBottom,
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 ce734f280..984509323 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
@@ -103,6 +103,7 @@ struct ComposeControlsDescriptor {
 	Window::SessionController *regularWindow = nullptr;
 	rpl::producer<ChatHelpers::FileChosen> stickerOrEmojiChosen;
 	rpl::producer<QString> customPlaceholder;
+	QString voiceCustomCancelText;
 	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 757b85444..6f30a8e59 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
@@ -1024,7 +1024,9 @@ VoiceRecordBar::VoiceRecordBar(
 , _startTimer([=] { startRecording(); })
 , _message(
 	st::historyRecordTextStyle,
-	tr::lng_record_cancel(tr::now),
+	(!descriptor.customCancelText.isEmpty()
+		? descriptor.customCancelText
+		: tr::lng_record_cancel(tr::now)),
 	TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto })
 , _lockFromBottom(descriptor.lockFromBottom)
 , _cancelFont(st::historyRecordFont) {
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 2d04e98a7..09c491678 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
@@ -43,6 +43,7 @@ struct VoiceRecordBarDescriptor {
 	not_null<Ui::RpWidget*> outerContainer;
 	std::shared_ptr<ChatHelpers::Show> show;
 	std::shared_ptr<Ui::SendButton> send;
+	QString customCancelText;
 	const style::RecordBar *stOverride = nullptr;
 	int recorderHeight = 0;
 	bool lockFromBottom = false;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 1497a60e4..c9eeed94d 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -86,6 +86,7 @@ ReplyArea::ReplyArea(not_null<Controller*> controller)
 		.sendMenuType = SendMenu::Type::SilentOnly,
 		.stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(),
 		.customPlaceholder = tr::lng_story_reply_ph(),
+		.voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now),
 		.voiceLockFromBottom = true,
 		.features = {
 			.sendAs = false,

From d392c7e8f0e4904d32d5f4aba7e324e7c5541e7e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 10:33:58 +0400
Subject: [PATCH 222/259] Use only vertical volume dropdown layout.

---
 .../SourceFiles/media/stories/media_stories_header.cpp    | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 5dafcb9e7..d0b988cb5 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -498,13 +498,7 @@ void Header::createVolumeToggle() {
 			}
 		}
 	}, lifetime);
-	_controller->layoutValue(
-	) | rpl::map([](const Layout &layout) {
-		return (layout.headerLayout == HeaderLayout::Outside);
-	}) | rpl::distinct_until_changed(
-	) | rpl::start_with_next([=](bool horizontal) {
-		rebuildVolumeControls(_volume->entity(), horizontal);
-	}, lifetime);
+	rebuildVolumeControls(_volume->entity(), false);
 
 	rpl::combine(
 		_widget->positionValue(),

From 75f542747dda8453bd1f3359db96eecd68e7e36e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 20:12:41 +0400
Subject: [PATCH 223/259] Fix build with GCC.

---
 Telegram/SourceFiles/dialogs/dialogs_row.cpp                | 3 ---
 Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp    | 1 -
 Telegram/SourceFiles/media/stories/media_stories_header.cpp | 1 -
 Telegram/SourceFiles/ui/effects/outline_segments.cpp        | 1 -
 4 files changed, 6 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index f8d748d0d..c937f10f2 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -360,15 +360,12 @@ void Row::PaintCornerBadgeFrame(
 	if (storiesCount) {
 		q.restore();
 
-		const auto st = context.st;
 		const auto outline = QRectF(0, 0, photoSize, photoSize);
 		const auto storiesUnreadCount = data->storiesUnreadCount;
 		const auto storiesUnreadBrush = [&] {
 			if (context.active || !storiesUnreadCount) {
 				return st::dialogsUnreadBgMutedActive->b;
 			}
-			const auto left = st->padding.left();
-			const auto top = st->padding.top();
 			auto gradient = Ui::UnreadStoryOutlineGradient(outline);
 			return QBrush(gradient);
 		}();
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index a1c6f452b..b09083ed7 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -233,7 +233,6 @@ List::Layout List::computeLayout(float64 expanded) const {
 	const auto smallCount = std::min(
 		kSmallThumbsShown,
 		itemsCount - smallSkip);
-	const auto smallWidth = st.photo + (smallCount - 1) * st.shift;
 	const auto leftSmall = st.left - (smallSkip ? st.shift : 0);
 	const auto leftFull = full.left - _scrollLeft + skipSide;
 	const auto startIndexFull = std::max(-leftFull, 0) / singleFull;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index d0b988cb5..5b35ec1c7 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -566,7 +566,6 @@ void Header::rebuildVolumeControls(
 	});
 	slider->setValue(_controller->currentVolume());
 
-	const auto skip = button->width() / 2;
 	const auto size = button->width()
 		+ st::storiesVolumeSize
 		+ st::storiesVolumeBottom;
diff --git a/Telegram/SourceFiles/ui/effects/outline_segments.cpp b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
index 5b302056b..7d4dd47fa 100644
--- a/Telegram/SourceFiles/ui/effects/outline_segments.cpp
+++ b/Telegram/SourceFiles/ui/effects/outline_segments.cpp
@@ -30,7 +30,6 @@ void PaintOutlineSegments(
 		: (full / (count * 1.1));
 	const auto left = full - (separator * count);
 	const auto length = left / float64(count);
-	const auto step = length + separator;
 	const auto spin = separator * (1. - fromFullProgress);
 
 	auto start = 0. + (arc::kQuarterLength + (separator / 2)) + (3. * spin);

From 150957abcd29b700f77bcdeeffaa38f7e90d5eae Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 20:20:32 +0400
Subject: [PATCH 224/259] Fix story viewer after attach choose dialog.

---
 Telegram/SourceFiles/core/file_utilities.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/core/file_utilities.cpp b/Telegram/SourceFiles/core/file_utilities.cpp
index 6e3003893..7103e1bd6 100644
--- a/Telegram/SourceFiles/core/file_utilities.cpp
+++ b/Telegram/SourceFiles/core/file_utilities.cpp
@@ -371,6 +371,9 @@ bool GetDefault(
 		? parent->window()
 		: Core::App().getFileDialogParent();
 	Core::App().notifyFileDialogShown(true);
+	const auto guard = gsl::finally([] {
+		Core::App().notifyFileDialogShown(false);
+	});
 	if (type == Type::ReadFiles) {
 		files = QFileDialog::getOpenFileNames(resolvedParent, caption, startFile, filter);
 		QString path = files.isEmpty() ? QString() : QFileInfo(files.back()).absoluteDir().absolutePath();
@@ -386,7 +389,6 @@ bool GetDefault(
 	} else {
 		file = QFileDialog::getOpenFileName(resolvedParent, caption, startFile, filter);
 	}
-	Core::App().notifyFileDialogShown(false);
 
 	if (file.isEmpty()) {
 		files = QStringList();

From 68fa3e36d73ce0a39af4976bcecb7530a3ad1b17 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 20:49:46 +0400
Subject: [PATCH 225/259] Fix story mention text updating.

---
 Telegram/SourceFiles/data/data_media_types.cpp | 12 ++++++++++--
 Telegram/SourceFiles/data/data_media_types.h   |  4 ++--
 Telegram/SourceFiles/data/data_session.cpp     |  5 +++++
 Telegram/SourceFiles/history/history_item.cpp  |  2 +-
 4 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 1367970fc..19d89c500 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -412,7 +412,7 @@ FullStoryId Media::storyId() const {
 	return {};
 }
 
-bool Media::storyExpired() const {
+bool Media::storyExpired(bool revalidate) {
 	return false;
 }
 
@@ -2033,7 +2033,15 @@ FullStoryId MediaStory::storyId() const {
 	return _storyId;
 }
 
-bool MediaStory::storyExpired() const {
+bool MediaStory::storyExpired(bool revalidate) {
+	if (revalidate) {
+		const auto stories = &parent()->history()->owner().stories();
+		if (const auto maybeStory = stories->lookup(_storyId)) {
+			_expired = false;
+		} else if (maybeStory.error() == Data::NoStory::Deleted) {
+			_expired = true;
+		}
+	}
 	return _expired;
 }
 
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 81e034c02..7649a37ed 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -114,7 +114,7 @@ public:
 	virtual PollData *poll() const;
 	virtual const WallPaper *paper() const;
 	virtual FullStoryId storyId() const;
-	virtual bool storyExpired() const;
+	virtual bool storyExpired(bool revalidate = false);
 	virtual bool storyMention() const;
 
 	virtual bool uploading() const;
@@ -580,7 +580,7 @@ public:
 	std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
 
 	FullStoryId storyId() const override;
-	bool storyExpired() const override;
+	bool storyExpired(bool revalidate = false) override;
 	bool storyMention() const override;
 
 	TextWithEntities notificationText() const override;
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index ca39658a6..b3911b9d4 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -3917,6 +3917,11 @@ void Session::refreshStoryItemViews(FullStoryId id) {
 	const auto i = _storyItems.find(id);
 	if (i != _storyItems.end()) {
 		for (const auto item : i->second) {
+			if (const auto media = item->media()) {
+				if (media->storyMention()) {
+					item->updateStoryMentionText();
+				}
+			}
 			requestItemViewRefresh(item);
 		}
 	}
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index c6ddbb4c4..bc0fffeb7 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -4825,7 +4825,7 @@ PreparedServiceText HistoryItem::prepareStoryMentionText() {
 	auto result = PreparedServiceText();
 	const auto peer = history()->peer;
 	result.links.push_back(peer->createOpenLink());
-	const auto phrase = (this->media() && this->media()->storyExpired())
+	const auto phrase = (this->media() && this->media()->storyExpired(true))
 		? (out()
 			? tr::lng_action_story_mention_me_unavailable
 			: tr::lng_action_story_mention_unavailable)

From 9ccb11bd1a45adc376672a00b131d1847acd9354 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Thu, 20 Jul 2023 18:06:45 +0400
Subject: [PATCH 226/259] Avoid using const_string for constant consumed by
 std::string

---
 .../platform/linux/integration_linux.cpp      |  4 +-
 .../linux/linux_xdp_open_with_dialog.cpp      | 23 +++---
 .../linux/notifications_manager_linux.cpp     | 80 +++++++++----------
 .../platform/linux/specific_linux.cpp         | 12 ++-
 4 files changed, 58 insertions(+), 61 deletions(-)

diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp
index 05cf6be27..0303f5093 100644
--- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp
@@ -114,8 +114,8 @@ LinuxIntegration::LinuxIntegration()
 	XdpInhibit::InhibitProxy::new_for_bus_sync(
 		Gio::BusType::SESSION_,
 		Gio::DBusProxyFlags::DO_NOT_AUTO_START_AT_CONSTRUCTION_,
-		std::string(base::Platform::XDP::kService),
-		std::string(base::Platform::XDP::kObjectPath),
+		base::Platform::XDP::kService,
+		base::Platform::XDP::kObjectPath,
 		nullptr))
 , _darkModeWatcher([](
 	const Glib::ustring &group,
diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp
index c37a123a3..9efcb0b68 100644
--- a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp
+++ b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp
@@ -23,8 +23,8 @@ namespace File {
 namespace internal {
 namespace {
 
-constexpr auto kXDPOpenURIInterface = "org.freedesktop.portal.OpenURI"_cs;
-constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs;
+constexpr auto kXDPOpenURIInterface = "org.freedesktop.portal.OpenURI";
+constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties";
 
 } // namespace
 
@@ -34,15 +34,14 @@ bool ShowXDPOpenWithDialog(const QString &filepath) {
 			Gio::DBus::BusType::SESSION);
 
 		const auto version = connection->call_sync(
-			std::string(base::Platform::XDP::kObjectPath),
-			std::string(kPropertiesInterface),
+			base::Platform::XDP::kObjectPath,
+			kPropertiesInterface,
 			"Get",
 			Glib::create_variant(std::tuple{
-				Glib::ustring(
-					std::string(kXDPOpenURIInterface)),
+				Glib::ustring(kXDPOpenURIInterface),
 				Glib::ustring("version"),
 			}),
-			std::string(base::Platform::XDP::kService)
+			base::Platform::XDP::kService
 		).get_child(0).get_dynamic<Glib::Variant<uint>>().get();
 
 		if (version < 3) {
@@ -107,8 +106,8 @@ bool ShowXDPOpenWithDialog(const QString &filepath) {
 				const Glib::VariantContainerBase &parameters) {
 				loop->quit();
 			},
-			std::string(base::Platform::XDP::kService),
-			std::string(base::Platform::XDP::kRequestInterface),
+			base::Platform::XDP::kService,
+			base::Platform::XDP::kRequestInterface,
 			"Response",
 			requestPath);
 
@@ -121,8 +120,8 @@ bool ShowXDPOpenWithDialog(const QString &filepath) {
 		auto outFdList = Glib::RefPtr<Gio::UnixFDList>();
 
 		connection->call_sync(
-			std::string(base::Platform::XDP::kObjectPath),
-			std::string(kXDPOpenURIInterface),
+			base::Platform::XDP::kObjectPath,
+			kXDPOpenURIInterface,
 			"OpenFile",
 			Glib::create_variant(std::tuple{
 				parentWindowId,
@@ -144,7 +143,7 @@ bool ShowXDPOpenWithDialog(const QString &filepath) {
 			}),
 			Gio::UnixFDList::create(std::vector<int>{ fd }),
 			outFdList,
-			std::string(base::Platform::XDP::kService));
+			base::Platform::XDP::kService);
 
 		if (signalId != 0) {
 			QWidget window;
diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
index 7b4f172a7..849af9f37 100644
--- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
@@ -37,10 +37,10 @@ namespace Platform {
 namespace Notifications {
 namespace {
 
-constexpr auto kService = "org.freedesktop.Notifications"_cs;
-constexpr auto kObjectPath = "/org/freedesktop/Notifications"_cs;
+constexpr auto kService = "org.freedesktop.Notifications";
+constexpr auto kObjectPath = "/org/freedesktop/Notifications";
 constexpr auto kInterface = kService;
-constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs;
+constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties";
 
 struct ServerInformation {
 	QString name;
@@ -76,7 +76,7 @@ std::unique_ptr<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() {
 			try {
 				return ranges::contains(
 					base::Platform::DBus::ListActivatableNames(connection),
-					std::string(kService),
+					kService,
 					&Glib::ustring::raw);
 			} catch (...) {
 				// avoid service restart loop in sandboxed environments
@@ -86,7 +86,7 @@ std::unique_ptr<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() {
 
 		return std::make_unique<base::Platform::DBus::ServiceWatcher>(
 			connection,
-			std::string(kService),
+			kService,
 			[=](
 				const Glib::ustring &service,
 				const Glib::ustring &oldOwner,
@@ -115,7 +115,7 @@ void StartServiceAsync(Fn<void()> callback) {
 
 		base::Platform::DBus::StartServiceByNameAsync(
 			connection,
-			std::string(kService),
+			kService,
 			[=](Fn<base::Platform::DBus::StartReply()> result) {
 				Noexcept([&] {
 					try {
@@ -155,7 +155,7 @@ bool GetServiceRegistered() {
 			try {
 				return base::Platform::DBus::NameHasOwner(
 					connection,
-					std::string(kService));
+					kService);
 			} catch (...) {
 				return false;
 			}
@@ -165,7 +165,7 @@ bool GetServiceRegistered() {
 			try {
 				return ranges::contains(
 					base::Platform::DBus::ListActivatableNames(connection),
-					std::string(kService),
+					kService,
 					&Glib::ustring::raw);
 			} catch (...) {
 				return false;
@@ -186,8 +186,8 @@ void GetServerInformation(
 			Gio::DBus::BusType::SESSION);
 
 		connection->call(
-			std::string(kObjectPath),
-			std::string(kInterface),
+			kObjectPath,
+			kInterface,
 			"GetServerInformation",
 			{},
 			[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
@@ -224,7 +224,7 @@ void GetServerInformation(
 					crl::on_main([=] { callback(std::nullopt); });
 				});
 			},
-			std::string(kService));
+			kService);
 	}, [&] {
 		crl::on_main([=] { callback(std::nullopt); });
 	});
@@ -236,8 +236,8 @@ void GetCapabilities(Fn<void(const QStringList &)> callback) {
 			Gio::DBus::BusType::SESSION);
 
 		connection->call(
-			std::string(kObjectPath),
-			std::string(kInterface),
+			kObjectPath,
+			kInterface,
 			"GetCapabilities",
 			{},
 			[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
@@ -257,7 +257,7 @@ void GetCapabilities(Fn<void(const QStringList &)> callback) {
 					crl::on_main([=] { callback({}); });
 				});
 			},
-			std::string(kService));
+			kService);
 	}, [&] {
 		crl::on_main([=] { callback({}); });
 	});
@@ -269,11 +269,11 @@ void GetInhibited(Fn<void(bool)> callback) {
 			Gio::DBus::BusType::SESSION);
 
 		connection->call(
-			std::string(kObjectPath),
-			std::string(kPropertiesInterface),
+			kObjectPath,
+			kPropertiesInterface,
 			"Get",
 			Glib::create_variant(std::tuple{
-				Glib::ustring(std::string(kInterface)),
+				Glib::ustring(kInterface),
 				Glib::ustring("Inhibited"),
 			}),
 			[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
@@ -290,7 +290,7 @@ void GetInhibited(Fn<void(bool)> callback) {
 					crl::on_main([=] { callback(false); });
 				});
 			},
-			std::string(kService));
+			kService);
 	}, [&] {
 		crl::on_main([=] { callback(false); });
 	});
@@ -545,25 +545,25 @@ bool NotificationData::init(
 			_notificationRepliedSignalId =
 				_dbusConnection->signal_subscribe(
 					signalEmitted,
-					std::string(kService),
-					std::string(kInterface),
+					kService,
+					kInterface,
 					"NotificationReplied",
-					std::string(kObjectPath));
+					kObjectPath);
 		}
 
 		_actionInvokedSignalId = _dbusConnection->signal_subscribe(
 			signalEmitted,
-			std::string(kService),
-			std::string(kInterface),
+			kService,
+			kInterface,
 			"ActionInvoked",
-			std::string(kObjectPath));
+			kObjectPath);
 
 		_activationTokenSignalId = _dbusConnection->signal_subscribe(
 			signalEmitted,
-			std::string(kService),
-			std::string(kInterface),
+			kService,
+			kInterface,
 			"ActivationToken",
-			std::string(kObjectPath));
+			kObjectPath);
 	}
 
 	if (capabilities.contains("action-icons")) {
@@ -594,10 +594,10 @@ bool NotificationData::init(
 
 	_notificationClosedSignalId = _dbusConnection->signal_subscribe(
 		signalEmitted,
-		std::string(kService),
-		std::string(kInterface),
+		kService,
+		kInterface,
 		"NotificationClosed",
-		std::string(kObjectPath));
+		kObjectPath);
 	return true;
 }
 
@@ -643,8 +643,8 @@ void NotificationData::show() {
 		const auto connection = _dbusConnection;
 
 		connection->call(
-			std::string(kObjectPath),
-			std::string(kInterface),
+			kObjectPath,
+			kInterface,
 			"Notify",
 			Glib::create_variant(std::tuple{
 				Glib::ustring(std::string(AppName)),
@@ -673,7 +673,7 @@ void NotificationData::show() {
 					});
 				});
 			},
-			std::string(kService));
+			kService);
 	}));
 }
 
@@ -690,14 +690,14 @@ void NotificationData::close() {
 	}
 
 	_dbusConnection->call(
-		std::string(kObjectPath),
-		std::string(kInterface),
+		kObjectPath,
+		kInterface,
 		"CloseNotification",
 		Glib::create_variant(std::tuple{
 			_notificationId,
 		}),
 		{},
-		std::string(kService),
+		kService,
 		-1,
 		Gio::DBus::CallFlags::NO_AUTO_START);
 	_manager->clearNotification(_id);
@@ -987,7 +987,7 @@ Manager::Private::Private(not_null<Manager*> manager)
 						0
 					).get_dynamic<Glib::ustring>();
 
-					if (interface != kInterface.data()) {
+					if (interface != kInterface) {
 						return;
 					}
 
@@ -1003,10 +1003,10 @@ Manager::Private::Private(not_null<Manager*> manager)
 					});
 				});
 			},
-			std::string(kService),
-			std::string(kPropertiesInterface),
+			kService,
+			kPropertiesInterface,
 			"PropertiesChanged",
-			std::string(kObjectPath));
+			kObjectPath);
 	}
 }
 
diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
index 7e20b946a..9fdb7a914 100644
--- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
@@ -54,8 +54,6 @@ using Platform::internal::WaylandIntegration;
 namespace Platform {
 namespace {
 
-constexpr auto kDesktopFile = ":/misc/org.telegram.desktop.desktop"_cs;
-
 bool PortalAutostart(bool start, bool silent) {
 	if (cExeName().isEmpty()) {
 		return false;
@@ -138,8 +136,8 @@ bool PortalAutostart(bool start, bool silent) {
 
 				loop->quit();
 			},
-			std::string(base::Platform::XDP::kService),
-			std::string(base::Platform::XDP::kRequestInterface),
+			base::Platform::XDP::kService,
+			base::Platform::XDP::kRequestInterface,
 			"Response",
 			requestPath);
 
@@ -150,14 +148,14 @@ bool PortalAutostart(bool start, bool silent) {
 		});
 
 		connection->call_sync(
-			std::string(base::Platform::XDP::kObjectPath),
+			base::Platform::XDP::kObjectPath,
 			"org.freedesktop.portal.Background",
 			"RequestBackground",
 			Glib::create_variant(std::tuple{
 				parentWindowId,
 				options,
 			}),
-			std::string(base::Platform::XDP::kService));
+			base::Platform::XDP::kService);
 
 		if (signalId != 0) {
 			QWidget window;
@@ -190,7 +188,7 @@ bool GenerateDesktopFile(
 	DEBUG_LOG(("App Info: placing .desktop file to %1").arg(targetPath));
 	if (!QDir(targetPath).exists()) QDir().mkpath(targetPath);
 
-	const auto sourceFile = kDesktopFile.utf16();
+	const auto sourceFile = u":/misc/org.telegram.desktop.desktop"_q;
 	const auto targetFile = targetPath
 		+ QGuiApplication::desktopFileName()
 		+ u".desktop"_q;

From 6e3c3a8dd22d0ceb94830c0834c94f0049ce9875 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 21:42:22 +0400
Subject: [PATCH 227/259] Collapse stories on archive open/close.

---
 .../SourceFiles/dialogs/dialogs_widget.cpp    | 47 ++++++++++++++++---
 Telegram/SourceFiles/dialogs/dialogs_widget.h |  6 +++
 .../dialogs/ui/dialogs_stories_list.cpp       |  1 +
 Telegram/lib_base                             |  2 +-
 Telegram/lib_ui                               |  2 +-
 Telegram/lib_webview                          |  2 +-
 6 files changed, 50 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 90722d7ea..9ff80e9fd 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -1075,19 +1075,52 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) {
 		cancelSearch();
 		closeChildList(anim::type::instant);
 		controller()->closeForum();
-		const auto was = (_openedFolder != nullptr);
 		_openedFolder = folder;
 		_inner->changeOpenedFolder(folder);
-		storiesToggleExplicitExpand(false);
-		if (was != (_openedFolder != nullptr)) {
-			using List = Data::StorySourcesList;
-			_storiesContents.fire(Stories::ContentForSession(
-				&controller()->session(),
-				folder ? List::Hidden : List::NotHidden));
+		if (_stories) {
+			storiesExplicitCollapse();
 		}
 	}, (folder != nullptr), animated);
 }
 
+void Widget::storiesExplicitCollapse() {
+	if (_storiesExplicitExpand) {
+		storiesToggleExplicitExpand(false);
+	} else if (_stories) {
+		using Type = Ui::ElasticScroll::OverscrollType;
+		_scroll->setOverscrollDefaults(0, 0);
+		_scroll->setOverscrollTypes(Type::None, Type::Real);
+		_scroll->setOverscrollTypes(
+			_stories->isHidden() ? Type::Real : Type::Virtual,
+			Type::Real);
+	}
+	_storiesExplicitExpandAnimation.stop();
+	_storiesExplicitExpandValue = 0;
+
+	using List = Data::StorySourcesList;
+	collectStoriesUserpicsViews(_openedFolder
+		? List::NotHidden
+		: List::Hidden);
+	_storiesContents.fire(Stories::ContentForSession(
+		&session(),
+		_openedFolder ? List::Hidden : List::NotHidden));
+}
+
+void Widget::collectStoriesUserpicsViews(Data::StorySourcesList list) {
+	auto &map = (list == Data::StorySourcesList::Hidden)
+		? _storiesUserpicsViewsHidden
+		: _storiesUserpicsViewsShown;
+	map.clear();
+	auto &owner = session().data();
+	for (const auto &source : owner.stories().sources(list)) {
+		if (const auto peer = owner.peerLoaded(source.id)) {
+			if (auto view = peer->activeUserpicView(); view.cloud) {
+				map.emplace(source.id, std::move(view));
+			}
+		}
+	}
+}
+
 void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) {
 	if (_openedForum == forum) {
 		return;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 83a8d70e6..9473ac348 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/dialogs_key.h"
 #include "window/section_widget.h"
 #include "ui/effects/animations.h"
+#include "ui/userpic_view.h"
 #include "mtproto/sender.h"
 #include "api/api_single_message_search.h"
 
@@ -20,6 +21,7 @@ class Error;
 
 namespace Data {
 class Forum;
+enum class StorySourcesList : uchar;
 } // namespace Data
 
 namespace Main {
@@ -168,6 +170,8 @@ private:
 	void setupDownloadBar();
 	void setupShortcuts();
 	void setupStories();
+	void storiesExplicitCollapse();
+	void collectStoriesUserpicsViews(Data::StorySourcesList list);
 	void storiesToggleExplicitExpand(bool expand);
 	void trackScroll(not_null<Ui::RpWidget*> widget);
 	[[nodiscard]] bool searchForPeersRequired(const QString &query) const;
@@ -281,6 +285,8 @@ private:
 	QString _lastFilterText;
 
 	rpl::event_stream<rpl::producer<Stories::Content>> _storiesContents;
+	base::flat_map<PeerId, Ui::PeerUserpicView> _storiesUserpicsViewsHidden;
+	base::flat_map<PeerId, Ui::PeerUserpicView> _storiesUserpicsViewsShown;
 	Fn<void()> _updateScrollGeometryCached;
 	std::unique_ptr<Stories::List> _stories;
 	Ui::Animations::Simple _storiesExplicitExpandAnimation;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index b09083ed7..1703c29d4 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -622,6 +622,7 @@ void List::mousePressEvent(QMouseEvent *e) {
 		return;
 	} else if (_state == State::Small) {
 		requestExpanded(true);
+		return;
 	} else if (_state != State::Full) {
 		return;
 	}
diff --git a/Telegram/lib_base b/Telegram/lib_base
index 74be75339..2669a0457 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit 74be75339d474df1a2863028ec146744597bd0bb
+Subproject commit 2669a04579069942b6208a18abe93c26adfddf2a
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 8f03125ec..288bec715 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 8f03125ec1d6b0f4a3f0fe6b6849aa57a1006063
+Subproject commit 288bec7157bf654d121ebfd2828801b49beb34ec
diff --git a/Telegram/lib_webview b/Telegram/lib_webview
index ec24c7a96..ebb8b8b91 160000
--- a/Telegram/lib_webview
+++ b/Telegram/lib_webview
@@ -1 +1 @@
-Subproject commit ec24c7a96036268b2024ca9765a66c63e6b8396a
+Subproject commit ebb8b8b91fe357b2c397a3eb98655c585b8c856e

From 94ad9221edf1328aeff127d7f8494af65712a3c4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 22:08:24 +0400
Subject: [PATCH 228/259] Fix opening recent viewers short profiles.

---
 .../media/stories/media_stories_recent_views.cpp         | 9 +++++++--
 .../media/stories/media_stories_recent_views.h           | 1 +
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
index 5f24fe717..6f11e6169 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
@@ -342,11 +342,15 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
 	};
 	if (_menuPlaceholderCount > 0) {
 		const auto i = _menuEntries.end() - (_menuPlaceholderCount--);
+		auto data = prepare(i->view);
 		i->peer = peer;
 		i->date = date;
-		i->action->setData(prepare(i->view));
+		i->callback = data.callback;
+		i->action->setData(std::move(data));
 	} else {
 		auto view = Ui::PeerUserpicView();
+		auto data = prepare(view);
+		auto callback = data.callback;
 		auto action = base::make_unique_q<Ui::WhoReactedEntryAction>(
 			_menu->menu(),
 			nullptr,
@@ -358,6 +362,7 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
 			.action = raw,
 			.peer = peer,
 			.date = date,
+			.callback = std::move(callback),
 			.view = std::move(view),
 		});
 	}
@@ -440,7 +445,7 @@ void RecentViews::subscribeToMenuUserpicsLoading(
 					.text = peer->name(),
 					.date = entry.date,
 					.userpic = std::move(userpic),
-					.callback = [] {},
+					.callback = entry.callback,
 				});
 				entry.key = key;
 				if (!peer->hasUserpic() || !peer->useEmptyUserpic(view)) {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h
index 2ecddf4ac..14bbdba68 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h
@@ -55,6 +55,7 @@ private:
 		not_null<Ui::WhoReactedEntryAction*> action;
 		PeerData *peer = nullptr;
 		QString date;
+		Fn<void()> callback;
 		Ui::PeerUserpicView view;
 		InMemoryKey key;
 	};

From 1f47b8e130d49e044090f1aed0cddfb67ed50352 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 22:19:32 +0400
Subject: [PATCH 229/259] Fix userpics list glitch.

---
 Telegram/SourceFiles/ui/chat/group_call_userpics.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
index 3c2ad72b9..4044c7d26 100644
--- a/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
+++ b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
@@ -364,6 +364,9 @@ void GroupCallUserpics::finishAnimating() {
 }
 
 void GroupCallUserpics::toggle(Userpic &userpic, bool shown) {
+	if (userpic.hiding == !shown && !userpic.shownAnimation.animating()) {
+		return;
+	}
 	userpic.hiding = !shown;
 	userpic.shownAnimation.start(
 		[=] { recountAndRepaint(); },

From c35556b33a6ff0fe8c65300dac3c4e32bd38f455 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 22:31:47 +0400
Subject: [PATCH 230/259] Improve short info button geometry.

---
 .../media/stories/media_stories_header.cpp    | 22 ++++++++++---------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 5b35ec1c7..e710cd288 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -309,6 +309,16 @@ void Header::show(HeaderData data) {
 			raw->setGeometry(layout.header);
 		}, raw->lifetime());
 	}
+	const auto updateInfoGeometry = [=] {
+		if (_name && _date) {
+			const auto namex = st::storiesHeaderNamePosition.x();
+			const auto namer = namex + _name->width();
+			const auto datex = st::storiesHeaderDatePosition.x();
+			const auto dater = datex + _date->width();
+			const auto r = std::max(namer, dater);
+			_info->setGeometry({ 0, 0, r, _widget->height() });
+		}
+	};
 	if (nameDataChanged) {
 		_name = std::make_unique<Ui::FlatLabel>(
 			_widget.get(),
@@ -322,12 +332,7 @@ void Header::show(HeaderData data) {
 		rpl::combine(
 			_name->widthValue(),
 			_widget->heightValue()
-		) | rpl::start_with_next([=](int width, int height) {
-			if (_date) {
-				_info->setGeometry(
-					{ 0, 0, std::max(width, _date->width()), height });
-			}
-		}, _name->lifetime());
+		) | rpl::start_with_next(updateInfoGeometry, _name->lifetime());
 	}
 	auto timestamp = ComposeDetails(data, base::unixtime::now());
 	_date = std::make_unique<Ui::FlatLabel>(
@@ -340,10 +345,7 @@ void Header::show(HeaderData data) {
 	_date->move(st::storiesHeaderDatePosition);
 
 	_date->widthValue(
-	) | rpl::start_with_next([=](int width) {
-		_info->setGeometry(
-			{ 0, 0, std::max(width, _name->width()), _widget->height() });
-	}, _name->lifetime());
+	) | rpl::start_with_next(updateInfoGeometry, _date->lifetime());
 
 	_privacy = MakePrivacyBadge(_userpic.get(), data.privacy, [=] {
 

From 2cd08b8923f5e5b7f16fa4edff67636915751c79 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 22:50:18 +0400
Subject: [PATCH 231/259] Cut off name / date correctly.

---
 .../stories/media_stories_controller.cpp      |   8 +-
 .../media/stories/media_stories_controller.h  |   1 +
 .../media/stories/media_stories_header.cpp    | 165 ++++++++++++------
 .../media/stories/media_stories_header.h      |   4 +-
 .../SourceFiles/media/view/media_view.style   |   6 +-
 5 files changed, 121 insertions(+), 63 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index b42a0bd74..f32aec6c6 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -910,9 +910,11 @@ PauseState Controller::pauseState() const {
 	const auto playing = !inactive && !_paused;
 	return playing
 		? PauseState::Playing
-		: inactive
-		? PauseState::Inactive
-		: PauseState::Paused;
+		: !inactive
+		? PauseState::Paused
+		: _paused
+		? PauseState::InactivePaused
+		: PauseState::Inactive;
 }
 
 float64 Controller::currentVolume() const {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 19b2de4d9..d234d540c 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -76,6 +76,7 @@ enum class PauseState {
 	Playing,
 	Paused,
 	Inactive,
+	InactivePaused,
 };
 
 struct SiblingLayout {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index e710cd288..7a9e92bfa 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -235,16 +235,12 @@ void UserpicBadge::updateGeometry() {
 	return { Ui::FormatDateTime(whenFull) };
 }
 
-[[nodiscard]] TextWithEntities ComposeName(HeaderData data) {
-	auto result = Ui::Text::Bold(data.user->isSelf()
-		? tr::lng_stories_my_name(tr::now)
-		: data.user->shortName());
-	if (data.fullCount) {
-		result.append(QString::fromUtf8(" \xE2\x80\xA2 %1/%2"
-		).arg(data.fullIndex + 1
-		).arg(data.fullCount));
-	}
-	return result;
+[[nodiscard]] QString ComposeCounter(HeaderData data) {
+	const auto index = data.fullIndex + 1;
+	const auto count = data.fullCount;
+	return count
+		? QString::fromUtf8(" \xE2\x80\xA2 %1/%2").arg(index).arg(count)
+		: QString();
 }
 
 [[nodiscard]] Timestamp ComposeDetails(HeaderData data, TimeId now) {
@@ -269,46 +265,8 @@ void Header::show(HeaderData data) {
 	if (_data == data) {
 		return;
 	}
-	const auto userChanged = !_data
-		|| (_data->user != data.user);
-	const auto nameDataChanged = userChanged
-		|| !_name
-		|| (_data->fullCount != data.fullCount)
-		|| (data.fullCount && _data->fullIndex != data.fullIndex);
+	const auto userChanged = !_data || (_data->user != data.user);
 	_data = data;
-	if (userChanged) {
-		_volume = nullptr;
-		_date = nullptr;
-		_name = nullptr;
-		_userpic = nullptr;
-		_info = nullptr;
-		_privacy = nullptr;
-		_playPause = nullptr;
-		_volumeToggle = nullptr;
-		const auto parent = _controller->wrap();
-		auto widget = std::make_unique<Ui::RpWidget>(parent);
-		const auto raw = widget.get();
-		_info = std::make_unique<Ui::AbstractButton>(raw);
-		_info->setClickedCallback([=] {
-			_controller->uiShow()->show(PrepareShortInfoBox(_data->user));
-		});
-		_userpic = std::make_unique<Ui::UserpicButton>(
-			raw,
-			data.user,
-			st::storiesHeaderPhoto);
-		_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
-		_userpic->show();
-		_userpic->move(
-			st::storiesHeaderMargin.left(),
-			st::storiesHeaderMargin.top());
-		raw->show();
-		_widget = std::move(widget);
-
-		_controller->layoutValue(
-		) | rpl::start_with_next([=](const Layout &layout) {
-			raw->setGeometry(layout.header);
-		}, raw->lifetime());
-	}
 	const auto updateInfoGeometry = [=] {
 		if (_name && _date) {
 			const auto namex = st::storiesHeaderNamePosition.x();
@@ -319,20 +277,58 @@ void Header::show(HeaderData data) {
 			_info->setGeometry({ 0, 0, r, _widget->height() });
 		}
 	};
-	if (nameDataChanged) {
+	if (userChanged) {
+		_volume = nullptr;
+		_date = nullptr;
+		_name = nullptr;
+		_counter = nullptr;
+		_userpic = nullptr;
+		_info = nullptr;
+		_privacy = nullptr;
+		_playPause = nullptr;
+		_volumeToggle = nullptr;
+		const auto parent = _controller->wrap();
+		auto widget = std::make_unique<Ui::RpWidget>(parent);
+		const auto raw = widget.get();
+
+		_info = std::make_unique<Ui::AbstractButton>(raw);
+		_info->setClickedCallback([=] {
+			_controller->uiShow()->show(PrepareShortInfoBox(_data->user));
+		});
+
+		_userpic = std::make_unique<Ui::UserpicButton>(
+			raw,
+			data.user,
+			st::storiesHeaderPhoto);
+		_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
+		_userpic->show();
+		_userpic->move(
+			st::storiesHeaderMargin.left(),
+			st::storiesHeaderMargin.top());
+
 		_name = std::make_unique<Ui::FlatLabel>(
-			_widget.get(),
-			rpl::single(ComposeName(data)),
+			raw,
+			rpl::single(data.user->isSelf()
+				? tr::lng_stories_my_name(tr::now)
+				: data.user->shortName()),
 			st::storiesHeaderName);
 		_name->setAttribute(Qt::WA_TransparentForMouseEvents);
 		_name->setOpacity(kNameOpacity);
-		_name->move(st::storiesHeaderNamePosition);
 		_name->show();
+		_name->move(st::storiesHeaderNamePosition);
 
 		rpl::combine(
 			_name->widthValue(),
-			_widget->heightValue()
+			raw->heightValue()
 		) | rpl::start_with_next(updateInfoGeometry, _name->lifetime());
+
+		raw->show();
+		_widget = std::move(widget);
+
+		_controller->layoutValue(
+		) | rpl::start_with_next([=](const Layout &layout) {
+			raw->setGeometry(layout.header);
+		}, raw->lifetime());
 	}
 	auto timestamp = ComposeDetails(data, base::unixtime::now());
 	_date = std::make_unique<Ui::FlatLabel>(
@@ -347,8 +343,21 @@ void Header::show(HeaderData data) {
 	_date->widthValue(
 	) | rpl::start_with_next(updateInfoGeometry, _date->lifetime());
 
-	_privacy = MakePrivacyBadge(_userpic.get(), data.privacy, [=] {
+	auto counter = ComposeCounter(data);
+	if (!counter.isEmpty()) {
+		_counter = std::make_unique<Ui::FlatLabel>(
+			_widget.get(),
+			std::move(counter),
+			st::storiesHeaderDate);
+		_counter->resizeToNaturalWidth(_counter->naturalWidth());
+		_counter->setAttribute(Qt::WA_TransparentForMouseEvents);
+		_counter->setOpacity(kNameOpacity);
+		_counter->show();
+	} else {
+		_counter = nullptr;
+	}
 
+	_privacy = MakePrivacyBadge(_userpic.get(), data.privacy, [=] {
 	});
 
 	if (data.video) {
@@ -370,6 +379,41 @@ void Header::show(HeaderData data) {
 		_volumeToggle = nullptr;
 	}
 
+	rpl::combine(
+		_widget->widthValue(),
+		_counter ? _counter->widthValue() : rpl::single(0),
+		_dateUpdated.events_starting_with_copy(rpl::empty)
+	) | rpl::start_with_next([=](int outer, int counter, auto) {
+		const auto right = _playPause
+			? _playPause->x()
+			: (outer - st::storiesHeaderMargin.right());
+		const auto nameLeft = st::storiesHeaderNamePosition.x();
+		const auto nameNatural = _name->naturalWidth();
+		if (counter) {
+			counter += st::normalFont->spacew;
+		}
+		const auto nameAvailable = right - nameLeft - counter;
+		auto counterLeft = nameLeft;
+		if (nameAvailable <= 0) {
+			_name->hide();
+		} else {
+			_name->show();
+			_name->resizeToNaturalWidth(nameAvailable);
+			counterLeft += _name->width() + st::normalFont->spacew;
+		}
+		if (_counter) {
+			_counter->move(counterLeft, _name->y());
+		}
+		const auto dateLeft = st::storiesHeaderDatePosition.x();
+		const auto dateAvailable = right - dateLeft;
+		if (dateAvailable <= 0) {
+			_date->hide();
+		} else {
+			_date->show();
+			_date->resizeToNaturalWidth(dateAvailable);
+		}
+	}, _date->lifetime());
+
 	if (timestamp.changes > 0) {
 		_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
 	}
@@ -403,14 +447,17 @@ void Header::createPlayPause() {
 		} else if (type == QEvent::MouseButtonRelease) {
 			const auto down = base::take(state->down);
 			if (down && state->over) {
-				_controller->togglePaused(_pauseState != PauseState::Paused);
+				const auto paused = (_pauseState == PauseState::Paused)
+					|| (_pauseState == PauseState::InactivePaused);
+				_controller->togglePaused(!paused);
 			}
 		}
 	}, lifetime);
 
 	_playPause->paintRequest() | rpl::start_with_next([=] {
 		auto p = QPainter(_playPause.get());
-		const auto paused = (_pauseState == PauseState::Paused);
+		const auto paused = (_pauseState == PauseState::Paused)
+			|| (_pauseState == PauseState::InactivePaused);
 		const auto icon = paused
 			? &st::storiesPlayIcon
 			: &st::storiesPauseIcon;
@@ -620,7 +667,8 @@ void Header::updateVolumeIcon() {
 void Header::applyPauseState() {
 	Expects(_playPause != nullptr);
 
-	const auto inactive = (_pauseState == PauseState::Inactive);
+	const auto inactive = (_pauseState == PauseState::Inactive)
+		|| (_pauseState == PauseState::InactivePaused);
 	_playPause->setAttribute(Qt::WA_TransparentForMouseEvents, inactive);
 	if (inactive) {
 		QEvent e(QEvent::Leave);
@@ -646,6 +694,7 @@ void Header::updateDateText() {
 	}
 	auto timestamp = ComposeDetails(*_data, base::unixtime::now());
 	_date->setText(timestamp.text);
+	_dateUpdated.fire({});
 	if (timestamp.changes > 0) {
 		_dateUpdateTimer.callOnce(timestamp.changes * crl::time(1000));
 	}
diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h
index f83749482..12a66b1a1 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.h
@@ -62,7 +62,7 @@ private:
 	void createPlayPause();
 	void createVolumeToggle();
 	void rebuildVolumeControls(
-		not_null<Ui::RpWidget*> dropdown, 
+		not_null<Ui::RpWidget*> dropdown,
 		bool horizontal);
 
 	const not_null<Controller*> _controller;
@@ -73,7 +73,9 @@ private:
 	std::unique_ptr<Ui::AbstractButton> _info;
 	std::unique_ptr<Ui::UserpicButton> _userpic;
 	std::unique_ptr<Ui::FlatLabel> _name;
+	std::unique_ptr<Ui::FlatLabel> _counter;
 	std::unique_ptr<Ui::FlatLabel> _date;
+	rpl::event_stream<> _dateUpdated;
 	std::unique_ptr<Ui::RpWidget> _playPause;
 	std::unique_ptr<Ui::RpWidget> _volumeToggle;
 	std::unique_ptr<Ui::FadeWrap<Ui::RpWidget>> _volume;
diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style
index 580347539..d463e8adc 100644
--- a/Telegram/SourceFiles/media/view/media_view.style
+++ b/Telegram/SourceFiles/media/view/media_view.style
@@ -426,11 +426,15 @@ storiesHeaderPhoto: UserpicButton(defaultUserpicButton) {
 }
 storiesHeaderName: FlatLabel(defaultFlatLabel) {
 	textFg: mediaviewControlFg;
-	style: defaultTextStyle;
+	style: semiboldTextStyle;
+	minWidth: 10px;
+	maxHeight: 20px;
 }
 storiesHeaderNamePosition: point(50px, 0px);
 storiesHeaderDate: FlatLabel(defaultFlatLabel) {
 	textFg: mediaviewControlFg;
+	minWidth: 10px;
+	maxHeight: 20px;
 }
 storiesHeaderDatePosition: point(50px, 17px);
 storiesShadowTop: icon{{ "mediaview/shadow_bottom-flip_vertical", windowShadowFg }};

From 1bdab16d7b3bc95b795ddf04923af52d1b83a558 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 23:13:35 +0400
Subject: [PATCH 232/259] Pause story while previewing a voice note.

---
 .../history/view/controls/history_view_compose_controls.cpp     | 2 +-
 .../history/view/controls/history_view_compose_controls.h       | 2 +-
 Telegram/SourceFiles/media/stories/media_stories_reply.cpp      | 2 +-
 3 files changed, 3 insertions(+), 3 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 b18fba987..c35ff0816 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -2971,7 +2971,7 @@ bool ComposeControls::isRecordingPressed() const {
 }
 
 rpl::producer<bool> ComposeControls::recordingValue() const {
-	return _recording.value();
+	return _voiceRecordBar->shownValue();
 }
 
 rpl::producer<bool> ComposeControls::hasSendTextValue() const {
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 984509323..95a72c356 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
@@ -218,7 +218,7 @@ public:
 	[[nodiscard]] bool isLockPresent() const;
 	[[nodiscard]] bool isRecording() const;
 	[[nodiscard]] bool isRecordingPressed() const;
-	[[nodiscard]] rpl::producer<bool> recordingValue() const;
+	[[nodiscard]] rpl::producer<bool> recordingActiveValue() const;
 	[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
 
 	void applyCloudDraft();
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index c9eeed94d..9d7728b72 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -671,7 +671,7 @@ rpl::producer<bool> ReplyArea::activeValue() const {
 	using namespace rpl::mappers;
 	return rpl::combine(
 		_controls->focusedValue(),
-		_controls->recordingValue(),
+		_controls->recordingActiveValue(),
 		_controls->tabbedPanelShownValue(),
 		_choosingAttach.value(),
 		_1 || _2 || _3 || _4

From e034f5e3047e21ab1f34124ff16d4394d197f77e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 23:15:45 +0400
Subject: [PATCH 233/259] Version 4.8.5.

- Bug fixes and other minor improvements. And stories preview.
---
 Telegram/Resources/uwp/AppX/AppxManifest.xml |  2 +-
 Telegram/Resources/winrc/Telegram.rc         |  8 ++++----
 Telegram/Resources/winrc/Updater.rc          |  8 ++++----
 Telegram/SourceFiles/core/version.h          |  6 +++---
 Telegram/SourceFiles/data/data_story.cpp     |  2 +-
 Telegram/build/version                       | 10 +++++-----
 changelog.txt                                |  4 ++++
 7 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index e2e0f8a2d..4c223ef01 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.4.5" />
+    Version="4.8.5.0" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 6d78ed1c8..ce5cf123d 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,5
- PRODUCTVERSION 4,8,4,5
+ FILEVERSION 4,8,5,0
+ PRODUCTVERSION 4,8,5,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.4.5"
+            VALUE "FileVersion", "4.8.5.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.5"
+            VALUE "ProductVersion", "4.8.5.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 3d37a211c..5691c986a 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,4,5
- PRODUCTVERSION 4,8,4,5
+ FILEVERSION 4,8,5,0
+ PRODUCTVERSION 4,8,5,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.4.5"
+            VALUE "FileVersion", "4.8.5.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.4.5"
+            VALUE "ProductVersion", "4.8.5.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 8f9e79acb..101e781dc 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/const_string.h"
 
-#define TDESKTOP_REQUESTED_ALPHA_VERSION (4008004005ULL)
+#define TDESKTOP_REQUESTED_ALPHA_VERSION (0ULL)
 
 #ifdef TDESKTOP_ALLOW_CLOSED_ALPHA
 #define TDESKTOP_ALPHA_VERSION TDESKTOP_REQUESTED_ALPHA_VERSION
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
 constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
 constexpr auto AppName = "Telegram Desktop"_cs;
 constexpr auto AppFile = "Telegram"_cs;
-constexpr auto AppVersion = 4008004;
-constexpr auto AppVersionStr = "4.8.4";
+constexpr auto AppVersion = 4008005;
+constexpr auto AppVersionStr = "4.8.5";
 constexpr auto AppBetaVersion = false;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index 8b21c7c34..2a139de87 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -276,7 +276,7 @@ bool Story::edited() const {
 }
 
 bool Story::canDownload() const {
-	return !forbidsForward() || _peer->isSelf();
+	return /*!forbidsForward() || */_peer->isSelf();
 }
 
 bool Story::canShare() const {
diff --git a/Telegram/build/version b/Telegram/build/version
index 659b269d5..92713d02f 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         4008004
+AppVersion         4008005
 AppVersionStrMajor 4.8
-AppVersionStrSmall 4.8.4
-AppVersionStr      4.8.4
+AppVersionStrSmall 4.8.5
+AppVersionStr      4.8.5
 BetaChannel        0
-AlphaVersion       4008004005
-AppVersionOriginal 4.8.4.5
+AlphaVersion       0
+AppVersionOriginal 4.8.5
diff --git a/changelog.txt b/changelog.txt
index 5906c8a22..bd7743446 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,7 @@
+4.8.5 (20.07.23)
+
+- Bug fixes and other minor improvements. And stories preview.
+
 4.8.4 (13.06.23)
 
 - Fix opening links on Linux.

From e646205bca05a1ae9d31a040bc86e54b13c81b5b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 20 Jul 2023 23:30:12 +0400
Subject: [PATCH 234/259] Version 4.8.5: Fix build.

---
 .../history/view/controls/history_view_compose_controls.cpp     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 c35ff0816..f6ad9ffe3 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -2970,7 +2970,7 @@ bool ComposeControls::isRecordingPressed() const {
 				&& _send->isDown()));
 }
 
-rpl::producer<bool> ComposeControls::recordingValue() const {
+rpl::producer<bool> ComposeControls::recordingActiveValue() const {
 	return _voiceRecordBar->shownValue();
 }
 

From 9a6cb68d71b872f27ea0b2e898b89458d45e9190 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 00:53:53 +0400
Subject: [PATCH 235/259] Version 4.8.5: Fix build with GCC.

---
 Telegram/SourceFiles/media/stories/media_stories_header.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
index 7a9e92bfa..5dc7f6b15 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp
@@ -388,7 +388,6 @@ void Header::show(HeaderData data) {
 			? _playPause->x()
 			: (outer - st::storiesHeaderMargin.right());
 		const auto nameLeft = st::storiesHeaderNamePosition.x();
-		const auto nameNatural = _name->naturalWidth();
 		if (counter) {
 			counter += st::normalFont->spacew;
 		}

From 5fc10c934ab7086391bdb21836e15d24a9fc2b64 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 06:18:03 +0400
Subject: [PATCH 236/259] Version 4.8.6.

- Fix langpack keys by a full rebuild on macOS.
---
 Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +-
 Telegram/Resources/winrc/Telegram.rc         | 8 ++++----
 Telegram/Resources/winrc/Updater.rc          | 8 ++++----
 Telegram/SourceFiles/core/version.h          | 4 ++--
 Telegram/build/version                       | 8 ++++----
 changelog.txt                                | 4 ++++
 6 files changed, 19 insertions(+), 15 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 4c223ef01..c5647a2b5 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.5.0" />
+    Version="4.8.6.0" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index ce5cf123d..9fc997288 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,5,0
- PRODUCTVERSION 4,8,5,0
+ FILEVERSION 4,8,6,0
+ PRODUCTVERSION 4,8,6,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.5.0"
+            VALUE "FileVersion", "4.8.6.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.5.0"
+            VALUE "ProductVersion", "4.8.6.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 5691c986a..a581fc869 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,5,0
- PRODUCTVERSION 4,8,5,0
+ FILEVERSION 4,8,6,0
+ PRODUCTVERSION 4,8,6,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.5.0"
+            VALUE "FileVersion", "4.8.6.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.5.0"
+            VALUE "ProductVersion", "4.8.6.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 101e781dc..5b4f6e797 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
 constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
 constexpr auto AppName = "Telegram Desktop"_cs;
 constexpr auto AppFile = "Telegram"_cs;
-constexpr auto AppVersion = 4008005;
-constexpr auto AppVersionStr = "4.8.5";
+constexpr auto AppVersion = 4008006;
+constexpr auto AppVersionStr = "4.8.6";
 constexpr auto AppBetaVersion = false;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index 92713d02f..f85771589 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         4008005
+AppVersion         4008006
 AppVersionStrMajor 4.8
-AppVersionStrSmall 4.8.5
-AppVersionStr      4.8.5
+AppVersionStrSmall 4.8.6
+AppVersionStr      4.8.6
 BetaChannel        0
 AlphaVersion       0
-AppVersionOriginal 4.8.5
+AppVersionOriginal 4.8.6
diff --git a/changelog.txt b/changelog.txt
index bd7743446..fb06e9a51 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,7 @@
+4.8.6 (21.07.23)
+
+- Fix langpack keys by a full rebuild on macOS.
+
 4.8.5 (20.07.23)
 
 - Bug fixes and other minor improvements. And stories preview.

From 3cb49127f4c8ec4712f9a2a0058b0b9eb5b964aa Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Fri, 21 Jul 2023 04:00:59 +0400
Subject: [PATCH 237/259] Make a weak pointer for GApplication startup lambda

Or it never gets destroyed
---
 Telegram/SourceFiles/platform/linux/integration_linux.cpp | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp
index 0303f5093..a465ddefe 100644
--- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp
@@ -222,7 +222,12 @@ void LinuxIntegration::LaunchNativeApplication() {
 				G_APPLICATION_HANDLES_OPEN,
 				nullptr)));
 
-	app->signal_startup().connect([=] {
+	app->signal_startup().connect([weak = std::weak_ptr(app)] {
+		const auto app = weak.lock();
+		if (!app) {
+			return;
+		}
+
 		// GNotification
 		InvokeQueued(qApp, [] {
 			Core::App().notifications().createManager();

From 76f7a870edaaccd687f0817517ebf14e85ac853c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 11:53:56 +0400
Subject: [PATCH 238/259] Return loading of full archive.

---
 Telegram/SourceFiles/data/data_stories_ids.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp
index 6a96f4e5f..7506abea5 100644
--- a/Telegram/SourceFiles/data/data_stories_ids.cpp
+++ b/Telegram/SourceFiles/data/data_stories_ids.cpp
@@ -116,7 +116,7 @@ rpl::producer<StoriesIdsSlice> ArchiveStoriesIds(
 			const auto hasBefore = int(i - begin(archive.list));
 			const auto hasAfter = int(end(archive.list) - i);
 			if (hasAfter < limit) {
-				//stories->archiveLoadMore();
+				stories->archiveLoadMore();
 			}
 			const auto takeBefore = std::min(hasBefore, limit);
 			const auto takeAfter = std::min(hasAfter, limit);

From b7370127ff9a481de85238e8757eea992c09a6eb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 12:18:16 +0400
Subject: [PATCH 239/259] Correctly clear story instances on deletion.

---
 Telegram/SourceFiles/data/data_stories.cpp    | 10 ++++++++++
 .../info/media/info_media_list_widget.cpp     |  2 ++
 .../info/stories/info_stories_provider.cpp    | 19 +++++++++++++++----
 .../info/stories/info_stories_provider.h      |  6 +++++-
 4 files changed, 32 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index df0615038..4768714dd 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -723,6 +723,16 @@ void Stories::applyDeleted(FullStoryId id) {
 			}
 			_owner->refreshStoryItemViews(id);
 			Assert(!_pollingSettings.contains(story.get()));
+			if (const auto j = _items.find(id.peer); j != end(_items)) {
+				const auto k = j->second.find(id.story);
+				if (k != end(j->second)) {
+					Assert(!k->second.lock());
+					j->second.erase(k);
+					if (j->second.empty()) {
+						_items.erase(j);
+					}
+				}
+			}
 			if (i->second.empty()) {
 				_stories.erase(i);
 			}
diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
index 459f1408c..ae029cf9e 100644
--- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
+++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp
@@ -548,6 +548,7 @@ void ListWidget::refreshRows() {
 	resizeToWidth(width());
 	restoreScrollState();
 	mouseActionUpdate();
+	update();
 }
 
 bool ListWidget::preventAutoHide() const {
@@ -1942,6 +1943,7 @@ void ListWidget::applyDragSelection(SelectedMap &applyTo) const {
 
 void ListWidget::refreshHeight() {
 	resize(width(), recountHeight());
+	update();
 }
 
 int ListWidget::recountHeight() {
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
index fbe81ebc5..e582f9ce7 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "info/media/info_media_widget.h"
 #include "info/media/info_media_list_section.h"
 #include "info/info_controller.h"
+#include "data/data_changes.h"
 #include "data/data_document.h"
 #include "data/data_media_types.h"
 #include "data/data_session.h"
@@ -54,6 +55,14 @@ Provider::Provider(not_null<AbstractController*> controller)
 			layout.second.item->invalidateCache();
 		}
 	}, _lifetime);
+
+	_peer->session().changes().storyUpdates(
+		Data::StoryUpdate::Flag::Destroyed
+	) | rpl::filter([=](const Data::StoryUpdate &update) {
+		return update.story->peer()  == _peer;
+	}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
+		storyRemoved(update.story);
+	}, _lifetime);
 }
 
 Provider::~Provider() {
@@ -253,15 +262,17 @@ bool Provider::isAfter(
 	return (a->id < b->id);
 }
 
-void Provider::itemRemoved(not_null<const HistoryItem*> item) {
-	const auto id = StoryIdFromMsgId(item->id);
-	if (const auto i = _layouts.find(id); i != end(_layouts)) {
+void Provider::storyRemoved(not_null<Data::Story*> story) {
+	Expects(story->peer() == _peer);
+
+	if (const auto i = _layouts.find(story->id()); i != end(_layouts)) {
 		_peer->owner().stories().unregisterPolling(
-			{ _peer->id, id },
+			story,
 			Data::Stories::Polling::Chat);
 		_layoutRemoved.fire(i->second.item.get());
 		_layouts.erase(i);
 	}
+	_items.remove(story->id());
 }
 
 BaseLayout *Provider::getLayout(
diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.h b/Telegram/SourceFiles/info/stories/info_stories_provider.h
index 390234128..b43350d97 100644
--- a/Telegram/SourceFiles/info/stories/info_stories_provider.h
+++ b/Telegram/SourceFiles/info/stories/info_stories_provider.h
@@ -16,6 +16,10 @@ class HistoryItem;
 class PeerData;
 class History;
 
+namespace Data {
+class Story;
+} // namespace Data
+
 namespace Info {
 class AbstractController;
 } // namespace Info
@@ -97,7 +101,7 @@ private:
 		not_null<const Media::BaseLayout*> item,
 		not_null<const Media::BaseLayout*> previous) override;
 
-	void itemRemoved(not_null<const HistoryItem*> item);
+	void storyRemoved(not_null<Data::Story*> story);
 	void markLayoutsStale();
 	void clearStaleLayouts();
 	void clear();

From 315b95a2144a695dad719a14c52787e6200a887d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 12:58:36 +0400
Subject: [PATCH 240/259] Fix voice recording indicator.

---
 .../view/controls/history_view_voice_record_bar.cpp        | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

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 6f30a8e59..9376e3372 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
@@ -1352,8 +1352,11 @@ void VoiceRecordBar::initLevelGeometry() {
 		_send->geometryValue(),
 		geometryValue(),
 		static_cast<Ui::RpWidget*>(parentWidget())->geometryValue()
-	) | rpl::start_with_next([=](QRect send, QRect me, QRect parent) {
-		const auto mapped = Ui::MapFrom(_outerContainer, this, send);
+	) | rpl::start_with_next([=](QRect send, auto, auto) {
+		const auto mapped = Ui::MapFrom(
+			_outerContainer,
+			_send->parentWidget(),
+			send);
 		const auto center = (send.width() - _level->width()) / 2;
 		_level->moveToLeft(mapped.x() + center, mapped.y() + center);
 	}, lifetime());

From 2402285d0346518208c4751b851a2195572059d6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 13:11:53 +0400
Subject: [PATCH 241/259] Fix possible crash in views refresh.

---
 .../SourceFiles/media/stories/media_stories_recent_views.cpp  | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
index 6f11e6169..806f21ebd 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp
@@ -136,6 +136,8 @@ void RecentViews::show(RecentViewsData data) {
 	_data = data;
 	if (!_data.valid) {
 		_text = {};
+		_clickHandlerLifetime.destroy();
+		_userpicsLifetime.destroy();
 		_userpics = nullptr;
 		_widget = nullptr;
 		return;
@@ -305,6 +307,8 @@ void RecentViews::showMenu() {
 	_controller->setMenuShown(true);
 	_menu->setDestroyedCallback(crl::guard(_widget.get(), [=] {
 		_controller->setMenuShown(false);
+		_waitingForUserpicsLifetime.destroy();
+		_waitingForUserpics.clear();
 		_menuShortLifetime.destroy();
 		_menuEntries.clear();
 		_menuEntriesCount = 0;

From 9d8d0398861cf50ccacd308ad9682fd80eea662d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 13:15:38 +0400
Subject: [PATCH 242/259] Fix possible crash in pinned item translations.

---
 Telegram/SourceFiles/history/history_inner_widget.cpp | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index cd2fa4685..5074219da 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -1840,13 +1840,12 @@ void HistoryInner::performDrag() {
 }
 
 void HistoryInner::itemRemoved(not_null<const HistoryItem*> item) {
-	if (_history != item->history() && _migrated != item->history()) {
-		return;
-	}
-
 	if (_pinnedItem == item) {
 		_pinnedItem = nullptr;
 	}
+	if (_history != item->history() && _migrated != item->history()) {
+		return;
+	}
 	if (_reactionsItem.current() == item) {
 		_reactionsItem = nullptr;
 	}

From 863313531d7135af216939b8a48fcfbeacce1bfa Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 13:44:43 +0400
Subject: [PATCH 243/259] Fix crash in viewed shared story deletion.

---
 Telegram/SourceFiles/data/data_stories.cpp | 16 +++++++++++-----
 Telegram/SourceFiles/data/data_stories.h   |  1 +
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp
index 4768714dd..085958ea2 100644
--- a/Telegram/SourceFiles/data/data_stories.cpp
+++ b/Telegram/SourceFiles/data/data_stories.cpp
@@ -692,10 +692,10 @@ void Stories::applyDeleted(FullStoryId id) {
 	if (i != end(_stories)) {
 		const auto j = i->second.find(id.story);
 		if (j != end(i->second)) {
-			// Duplicated in Stories::apply(peer, const MTPUserStories*).
-			auto story = std::move(j->second);
+			const auto &story = _deletingStories[id] = std::move(j->second);
 			_expiring.remove(story->expires(), story->fullId());
 			i->second.erase(j);
+
 			session().changes().storyUpdated(
 				story.get(),
 				UpdateFlag::Destroyed);
@@ -736,6 +736,7 @@ void Stories::applyDeleted(FullStoryId id) {
 			if (i->second.empty()) {
 				_stories.erase(i);
 			}
+			_deletingStories.remove(id);
 		}
 	}
 }
@@ -1628,9 +1629,14 @@ bool Stories::registerPolling(FullStoryId id, Polling polling) {
 }
 
 void Stories::unregisterPolling(FullStoryId id, Polling polling) {
-	const auto maybeStory = lookup(id);
-	Assert(maybeStory.has_value());
-	unregisterPolling(*maybeStory, polling);
+	if (const auto maybeStory = lookup(id)) {
+		unregisterPolling(*maybeStory, polling);
+	} else if (const auto i = _deletingStories.find(id)
+		; i != end(_deletingStories)) {
+		unregisterPolling(i->second.get(), polling);
+	} else {
+		Unexpected("Couldn't find story for unregistering polling.");
+	}
 }
 
 int Stories::pollingInterval(const PollingSettings &settings) const {
diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h
index 27eb0f761..c4d55a129 100644
--- a/Telegram/SourceFiles/data/data_stories.h
+++ b/Telegram/SourceFiles/data/data_stories.h
@@ -299,6 +299,7 @@ private:
 	std::unordered_map<
 		PeerId,
 		base::flat_map<StoryId, std::unique_ptr<Story>>> _stories;
+	base::flat_map<FullStoryId, std::unique_ptr<Story>> _deletingStories;
 	std::unordered_map<
 		PeerId,
 		base::flat_map<StoryId, std::weak_ptr<HistoryItem>>> _items;

From f8e815545247f22c3b1ed7a8f3f8496af34d9e9a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 14:42:26 +0400
Subject: [PATCH 244/259] Fix possible crash with large UI scale values.

---
 Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp | 7 ++++++-
 Telegram/lib_ui                                          | 2 +-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp
index 04565a741..ffedf1f50 100644
--- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp
+++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp
@@ -344,7 +344,12 @@ void CustomEmojiLoader::check() {
 			sizeOverride);
 	};
 	auto put = [=, key = cacheKey(document)](QByteArray value) {
-		document->owner().cacheBigFile().put(key, std::move(value));
+		const auto size = value.size();
+		if (size <= Storage::Cache::Database::Settings().maxDataSize) {
+			document->owner().cacheBigFile().put(key, std::move(value));
+		} else {
+			LOG(("Data Error: Cached emoji size too big: %1.").arg(size));
+		}
 	};
 	const auto type = document->sticker()->type;
 	auto generator = [=, bytes = Lottie::ReadContent(data, filepath)]()
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 288bec715..39d440f77 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 288bec7157bf654d121ebfd2828801b49beb34ec
+Subproject commit 39d440f774b5b0986200299c4893e3f6a4678c85

From 75dc7e6e81cdb036d1b8c4868bfaf8ff4f78149f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 14:42:51 +0400
Subject: [PATCH 245/259] Fix a crash with possible empty contact name.

---
 Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
index 04a9d025f..7174aef59 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
@@ -896,6 +896,10 @@ void OverlayWidget::RendererGL::paintStoriesSiblingPart(
 		float64 opacity) {
 	Expects(index >= 0 && index < kStoriesSiblingPartsCount);
 
+	if (image.isNull() || rect.isEmpty()) {
+		return;
+	}
+
 	_f->glActiveTexture(GL_TEXTURE0);
 
 	auto &part = _storiesSiblingParts[index];

From 21fa3264e3b1281cb8dc46a40c496a77fc30d964 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 16:40:20 +0400
Subject: [PATCH 246/259] Pause video while caption is expanded.

---
 .../media_stories_caption_full_view.cpp       |  8 +++++--
 .../stories/media_stories_controller.cpp      | 22 +++++++++++++++----
 .../media/stories/media_stories_controller.h  |  2 ++
 .../media/stories/media_stories_view.cpp      |  4 ++++
 .../media/stories/media_stories_view.h        |  1 +
 .../media/view/media_view_overlay_widget.cpp  | 11 ++++++++--
 6 files changed, 40 insertions(+), 8 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp
index b301ced92..6506dd62c 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_caption_full_view.cpp
@@ -68,13 +68,17 @@ void CaptionFullView::resizeEvent(QResizeEvent *e) {
 
 void CaptionFullView::keyPressEvent(QKeyEvent *e) {
 	if (e->key() == Qt::Key_Escape) {
-		_close();
+		if (const auto onstack = _close) {
+			onstack();
+		}
 	}
 }
 
 void CaptionFullView::mousePressEvent(QMouseEvent *e) {
 	if (e->button() == Qt::LeftButton) {
-		_close();
+		if (const auto onstack = _close) {
+			onstack();
+		}
 	}
 }
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index f32aec6c6..bb9480c26 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -345,10 +345,11 @@ Controller::~Controller() {
 }
 
 void Controller::updateContentFaded() {
-	if (_contentFaded == _replyActive) {
+	const auto faded = _replyActive || _captionFullView || _captionExpanded;
+	if (_contentFaded == faded) {
 		return;
 	}
-	_contentFaded = _replyActive;
+	_contentFaded = faded;
 	_contentFadeAnimation.start(
 		[=] { _delegate->storiesRepaint(); },
 		_contentFaded ? 0. : 1.,
@@ -570,16 +571,24 @@ TextWithEntities Controller::captionText() const {
 	return _captionText;
 }
 
+void Controller::setCaptionExpanded(bool expanded) {
+	if (_captionExpanded == expanded) {
+		return;
+	}
+	_captionExpanded = expanded;
+	updateContentFaded();
+}
+
 void Controller::showFullCaption() {
 	if (_captionText.empty()) {
 		return;
 	}
-	togglePaused(true);
 	_captionFullView = std::make_unique<CaptionFullView>(
 		wrap(),
 		&_delegate->storiesShow()->session(),
 		_captionText,
-		[=] { togglePaused(false); });
+		[=] { _captionFullView = nullptr; updateContentFaded(); });
+	updateContentFaded();
 }
 
 std::shared_ptr<ChatHelpers::Show> Controller::uiShow() const {
@@ -796,6 +805,9 @@ void Controller::show(
 
 	_captionText = story->caption();
 	_captionFullView = nullptr;
+	_captionExpanded = false;
+	_contentFaded = false;
+	_contentFadeAnimation.stop();
 	const auto document = story->document();
 	_header->show({
 		.user = user,
@@ -942,6 +954,8 @@ void Controller::updatePlayingAllowed() {
 		&& _windowActive
 		&& !_paused
 		&& !_replyActive
+		&& !_captionFullView
+		&& !_captionExpanded
 		&& !_layerShown
 		&& !_menuShown);
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index d234d540c..a7dd60da2 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -122,6 +122,7 @@ public:
 	[[nodiscard]] bool closeByClickAt(QPoint position) const;
 	[[nodiscard]] Data::FileOrigin fileOrigin() const;
 	[[nodiscard]] TextWithEntities captionText() const;
+	void setCaptionExpanded(bool expanded);
 	void showFullCaption();
 
 	[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
@@ -241,6 +242,7 @@ private:
 	Ui::Animations::Simple _contentFadeAnimation;
 	bool _contentFaded = false;
 
+	bool _captionExpanded = false;
 	bool _windowActive = false;
 	bool _replyFocused = false;
 	bool _replyActive = false;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
index 240a39b4e..159999281 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp
@@ -123,6 +123,10 @@ TextWithEntities View::captionText() const {
 	return _controller->captionText();
 }
 
+void View::setCaptionExpanded(bool expanded) {
+	_controller->setCaptionExpanded(expanded);
+}
+
 void View::showFullCaption() {
 	_controller->showFullCaption();
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h
index 099689e31..57e38e70b 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_view.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_view.h
@@ -64,6 +64,7 @@ public:
 	[[nodiscard]] SiblingView sibling(SiblingType type) const;
 	[[nodiscard]] Data::FileOrigin fileOrigin() const;
 	[[nodiscard]] TextWithEntities captionText() const;
+	void setCaptionExpanded(bool expanded);
 	void showFullCaption();
 
 	void updatePlayback(const Player::TrackState &state);
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 9a58198e3..32fd05e73 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -486,6 +486,10 @@ OverlayWidget::OverlayWidget()
 			return base::EventFilterResult::Cancel;
 		} else if (type == QEvent::ThemeChange && Platform::IsLinux()) {
 			_window->setWindowIcon(Window::CreateIcon(_session));
+		} else if (type == QEvent::FocusOut) {
+			if (const auto popup = QApplication::activePopupWidget()) {
+				int a = popup->x();
+			}
 		}
 		return base::EventFilterResult::Continue;
 	});
@@ -1384,8 +1388,9 @@ void OverlayWidget::refreshCaptionGeometry() {
 	_captionFitsIfExpanded = _stories
 		&& (wantedHeight <= maxExpandedHeight);
 	_captionShownFull = (wantedHeight <= maxCollapsedHeight);
-	if (_captionShownFull) {
+	if (_captionShownFull && _captionExpanded && _stories) {
 		_captionExpanded = false;
+		_stories->setCaptionExpanded(false);
 	}
 	_captionRect = QRect(
 		(width() - captionWidth) / 2,
@@ -3120,7 +3125,7 @@ void OverlayWidget::setCursor(style::cursor cursor) {
 }
 
 void OverlayWidget::setFocus() {
-	_widget->setFocus();
+	_body->setFocus();
 }
 
 bool OverlayWidget::takeFocusFrom(not_null<QWidget*> window) const {
@@ -5570,10 +5575,12 @@ ClickHandlerPtr OverlayWidget::ensureCaptionExpandLink() {
 				return;
 			} else if (_captionExpanded) {
 				_captionExpanded = false;
+				_stories->setCaptionExpanded(false);
 				refreshCaptionGeometry();
 				update();
 			} else if (_captionFitsIfExpanded) {
 				_captionExpanded = true;
+				_stories->setCaptionExpanded(true);
 				refreshCaptionGeometry();
 				update();
 			} else {

From 0b5c0e3e983d4107f0bc12eabec064e52de284d6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 16:44:28 +0400
Subject: [PATCH 247/259] Force internal player for stories.

---
 Telegram/SourceFiles/data/data_document.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 66e34cff2..c9e66759b 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -1331,6 +1331,7 @@ bool DocumentData::canBeStreamed(HistoryItem *item) const {
 	return hasRemoteLocation()
 		&& supportsStreaming()
 		&& (!isVideoFile()
+			|| storyMedia()
 			|| !ExternalVideoPlayer.value()
 			|| (item && !item->allowsForward()));
 }

From 2cc0faa5b32e5a6e0342f8299f32db3f500b2067 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 17:11:49 +0400
Subject: [PATCH 248/259] Pause stories if reply context menu is shown.

---
 .../history/view/controls/history_view_compose_controls.cpp   | 4 ++++
 .../history/view/controls/history_view_compose_controls.h     | 1 +
 Telegram/SourceFiles/media/stories/media_stories_reply.cpp    | 3 ++-
 Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 4 ----
 Telegram/lib_ui                                               | 2 +-
 5 files changed, 8 insertions(+), 6 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 f6ad9ffe3..86d412b2a 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -2978,6 +2978,10 @@ rpl::producer<bool> ComposeControls::hasSendTextValue() const {
 	return _hasSendText.value();
 }
 
+rpl::producer<bool> ComposeControls::fieldMenuShownValue() const {
+	return _field->menuShownValue();
+}
+
 bool ComposeControls::preventsClose(Fn<void()> &&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 95a72c356..de7002012 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h
@@ -220,6 +220,7 @@ public:
 	[[nodiscard]] bool isRecordingPressed() const;
 	[[nodiscard]] rpl::producer<bool> recordingActiveValue() const;
 	[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
+	[[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const;
 
 	void applyCloudDraft();
 	void applyDraft(
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
index 9d7728b72..9df6d290f 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp
@@ -673,8 +673,9 @@ rpl::producer<bool> ReplyArea::activeValue() const {
 		_controls->focusedValue(),
 		_controls->recordingActiveValue(),
 		_controls->tabbedPanelShownValue(),
+		_controls->fieldMenuShownValue(),
 		_choosingAttach.value(),
-		_1 || _2 || _3 || _4
+		_1 || _2 || _3 || _4 || _5
 	) | rpl::distinct_until_changed();
 }
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index 32fd05e73..0a8be2dc6 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -486,10 +486,6 @@ OverlayWidget::OverlayWidget()
 			return base::EventFilterResult::Cancel;
 		} else if (type == QEvent::ThemeChange && Platform::IsLinux()) {
 			_window->setWindowIcon(Window::CreateIcon(_session));
-		} else if (type == QEvent::FocusOut) {
-			if (const auto popup = QApplication::activePopupWidget()) {
-				int a = popup->x();
-			}
 		}
 		return base::EventFilterResult::Continue;
 	});
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 39d440f77..079d966e4 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 39d440f774b5b0986200299c4893e3f6a4678c85
+Subproject commit 079d966e45922f90de297fc9f7435cbf8b127b09

From 30c73fbdf29ff8e6f36641f4b4e271c7974ed88e Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Fri, 21 Jul 2023 17:47:45 +0400
Subject: [PATCH 249/259] Fix D-Bus service with non-standard workdir or
 backslash in the path

Working dir not set leads to unrelevant instance being launched with unrelevant bus name and entire launchf fails.

D-Bus service files also don't need backslash escaping unlike the .desktop files.
---
 Telegram/SourceFiles/platform/linux/specific_linux.cpp | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
index 9fdb7a914..e53ac78b0 100644
--- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
@@ -346,12 +346,16 @@ bool GenerateServiceFile(bool silent = false) {
 		"Name",
 		QGuiApplication::desktopFileName().toStdString());
 
+	QStringList exec;
+	exec.append(executable);
+	if (Core::Launcher::Instance().customWorkingDir()) {
+		exec.append(u"-workdir"_q);
+		exec.append(cWorkingDir());
+	}
 	target->set_string(
 		group,
 		"Exec",
-		KShell::joinArgs({ executable }).replace(
-			'\\',
-			qstr("\\\\")).toStdString());
+		KShell::joinArgs(exec).toStdString());
 
 	try {
 		target->save_to_file(targetFile.toStdString());

From 06e49c6813a292d8d9d37585cfeb953115092604 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 18:06:00 +0400
Subject: [PATCH 250/259] Fix opening t.me/channel with min-loaded channels.

---
 Telegram/SourceFiles/data/data_session.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index b3911b9d4..73ebae0e1 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -1158,7 +1158,8 @@ UserData *Session::userByPhone(const QString &phone) const {
 PeerData *Session::peerByUsername(const QString &username) const {
 	const auto uname = username.trimmed();
 	for (const auto &[peerId, peer] : _peers) {
-		if (!peer->userName().compare(uname, Qt::CaseInsensitive)) {
+		if (peer->isLoaded()
+			&& !peer->userName().compare(uname, Qt::CaseInsensitive)) {
 			return peer.get();
 		}
 	}

From c0b7577db929339335fe1fb8199e45ffb64ac31d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 18:21:34 +0400
Subject: [PATCH 251/259] If opening an unread story show only unread.

---
 .../stories/media_stories_controller.cpp      | 39 +++++++++++++------
 .../media/stories/media_stories_controller.h  |  1 +
 2 files changed, 29 insertions(+), 11 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index bb9480c26..489ed9ad4 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -669,6 +669,9 @@ void Controller::rebuildFromContext(
 			storyId.peer,
 			&StoriesSourceInfo::id);
 		if (i != end(sources)) {
+			if (_cachedSourcesList.empty()) {
+				_showingUnreadSources = source && (source->readTill < id);
+			}
 			rebuildCachedSourcesList(sources, (i - begin(sources)));
 			showSiblings(&user->session());
 			if (int(sources.end() - i) < kPreloadUsersCount) {
@@ -1294,6 +1297,8 @@ void Controller::rebuildCachedSourcesList(
 		int index) {
 	Expects(index >= 0 && index < lists.size());
 
+	const auto currentPeerId = lists[index].id;
+
 	// Remove removed.
 	_cachedSourcesList.erase(ranges::remove_if(_cachedSourcesList, [&](
 			PeerId id) {
@@ -1301,7 +1306,7 @@ void Controller::rebuildCachedSourcesList(
 	}), end(_cachedSourcesList));
 
 	// Find current, full rebuild if can't find.
-	const auto i = ranges::find(_cachedSourcesList, lists[index].id);
+	const auto i = ranges::find(_cachedSourcesList, currentPeerId);
 	if (i == end(_cachedSourcesList)) {
 		_cachedSourcesList.clear();
 	} else {
@@ -1310,38 +1315,50 @@ void Controller::rebuildCachedSourcesList(
 
 	if (_cachedSourcesList.empty()) {
 		// Full rebuild.
+		const auto predicate = [&](const Data::StoriesSourceInfo &info) {
+			return !_showingUnreadSources
+				|| (info.unreadCount > 0)
+				|| (info.id == currentPeerId);
+		};
 		_cachedSourcesList = lists
+			| ranges::views::filter(predicate)
 			| ranges::views::transform(&Data::StoriesSourceInfo::id)
 			| ranges::to_vector;
-		_cachedSourceIndex = index;
+		_cachedSourceIndex = ranges::find(_cachedSourcesList, currentPeerId)
+			- begin(_cachedSourcesList);
 	} else if (ranges::equal(
 			lists,
 			_cachedSourcesList,
 			ranges::equal_to(),
 			&Data::StoriesSourceInfo::id)) {
 		// No rebuild needed.
-		_cachedSourceIndex = index;
 	} else {
 		// All that go before the current push to front.
 		for (auto before = index; before > 0;) {
-			const auto peerId = lists[--before].id;
-			if (!ranges::contains(_cachedSourcesList, peerId)) {
+			const auto &info = lists[--before];
+			if (_showingUnreadSources && !info.unreadCount) {
+				continue;
+			} else if (!ranges::contains(_cachedSourcesList, info.id)) {
 				_cachedSourcesList.insert(
 					begin(_cachedSourcesList),
-					peerId);
+					info.id);
 				++_cachedSourceIndex;
 			}
 		}
 		// All that go after the current push to back.
-		for (auto after = index + 1, count = int(lists.size()); after != count; ++after) {
-			const auto peerId = lists[after].id;
-			if (!ranges::contains(_cachedSourcesList, peerId)) {
-				_cachedSourcesList.push_back(peerId);
+		for (auto after = index + 1, count = int(lists.size())
+			; after != count
+			; ++after) {
+			const auto &info = lists[after];
+			if (_showingUnreadSources && !info.unreadCount) {
+				continue;
+			} else if (!ranges::contains(_cachedSourcesList, info.id)) {
+				_cachedSourcesList.push_back(info.id);
 			}
 		}
 	}
 
-	Ensures(_cachedSourcesList.size() == lists.size());
+	Ensures(_cachedSourcesList.size() <= lists.size());
 	Ensures(_cachedSourceIndex >= 0
 		&& _cachedSourceIndex < _cachedSourcesList.size());
 }
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index a7dd60da2..1d83d24e5 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -266,6 +266,7 @@ private:
 
 	std::vector<PeerId> _cachedSourcesList;
 	int _cachedSourceIndex = -1;
+	bool _showingUnreadSources = false;
 
 	ViewsSlice _viewsSlice;
 	rpl::event_stream<> _moreViewsLoaded;

From e7312697bf242f5674461514b4efc39e6708182d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 18:49:54 +0400
Subject: [PATCH 252/259] In the viewer remember which story was opened.

---
 .../stories/media_stories_controller.cpp      | 56 +++++++++++++------
 .../media/stories/media_stories_controller.h  | 12 +++-
 .../media/stories/media_stories_sibling.cpp   | 28 ++++++++--
 .../media/stories/media_stories_sibling.h     |  7 ++-
 4 files changed, 75 insertions(+), 28 deletions(-)

diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index 489ed9ad4..2c2efc7d6 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -673,6 +673,7 @@ void Controller::rebuildFromContext(
 				_showingUnreadSources = source && (source->readTill < id);
 			}
 			rebuildCachedSourcesList(sources, (i - begin(sources)));
+			_cachedSourcesList[_cachedSourceIndex].shownId = storyId.story;
 			showSiblings(&user->session());
 			if (int(sources.end() - i) < kPreloadUsersCount) {
 				stories.loadMore(list);
@@ -980,13 +981,13 @@ void Controller::showSiblings(not_null<Main::Session*> session) {
 		session,
 		(_cachedSourceIndex > 0
 			? _cachedSourcesList[_cachedSourceIndex - 1]
-			: PeerId()));
+			: CachedSource()));
 	showSibling(
 		_siblingRight,
 		session,
 		(_cachedSourceIndex + 1 < _cachedSourcesList.size()
 			? _cachedSourcesList[_cachedSourceIndex + 1]
-			: PeerId()));
+			: CachedSource()));
 }
 
 void Controller::hideSiblings() {
@@ -997,16 +998,16 @@ void Controller::hideSiblings() {
 void Controller::showSibling(
 		std::unique_ptr<Sibling> &sibling,
 		not_null<Main::Session*> session,
-		PeerId peerId) {
-	if (!peerId) {
+		CachedSource cached) {
+	if (!cached) {
 		sibling = nullptr;
 		return;
 	}
-	const auto source = session->data().stories().source(peerId);
+	const auto source = session->data().stories().source(cached.peerId);
 	if (!source) {
 		sibling = nullptr;
-	} else if (!sibling || !sibling->shows(*source)) {
-		sibling = std::make_unique<Sibling>(this, *source);
+	} else if (!sibling || !sibling->shows(*source, cached.shownId)) {
+		sibling = std::make_unique<Sibling>(this, *source, cached.shownId);
 	}
 }
 
@@ -1301,12 +1302,18 @@ void Controller::rebuildCachedSourcesList(
 
 	// Remove removed.
 	_cachedSourcesList.erase(ranges::remove_if(_cachedSourcesList, [&](
-			PeerId id) {
-		return !ranges::contains(lists, id, &Data::StoriesSourceInfo::id);
+			CachedSource source) {
+		return !ranges::contains(
+			lists,
+			source.peerId,
+			&Data::StoriesSourceInfo::id);
 	}), end(_cachedSourcesList));
 
 	// Find current, full rebuild if can't find.
-	const auto i = ranges::find(_cachedSourcesList, currentPeerId);
+	const auto i = ranges::find(
+		_cachedSourcesList,
+		currentPeerId,
+		&CachedSource::peerId);
 	if (i == end(_cachedSourcesList)) {
 		_cachedSourcesList.clear();
 	} else {
@@ -1320,17 +1327,24 @@ void Controller::rebuildCachedSourcesList(
 				|| (info.unreadCount > 0)
 				|| (info.id == currentPeerId);
 		};
+		const auto mapper = [](const Data::StoriesSourceInfo &info) {
+			return CachedSource{ info.id };
+		};
 		_cachedSourcesList = lists
 			| ranges::views::filter(predicate)
-			| ranges::views::transform(&Data::StoriesSourceInfo::id)
+			| ranges::views::transform(mapper)
 			| ranges::to_vector;
-		_cachedSourceIndex = ranges::find(_cachedSourcesList, currentPeerId)
-			- begin(_cachedSourcesList);
+		_cachedSourceIndex = ranges::find(
+			_cachedSourcesList,
+			currentPeerId,
+			&CachedSource::peerId
+		) - begin(_cachedSourcesList);
 	} else if (ranges::equal(
 			lists,
 			_cachedSourcesList,
 			ranges::equal_to(),
-			&Data::StoriesSourceInfo::id)) {
+			&Data::StoriesSourceInfo::id,
+			&CachedSource::peerId)) {
 		// No rebuild needed.
 	} else {
 		// All that go before the current push to front.
@@ -1338,10 +1352,13 @@ void Controller::rebuildCachedSourcesList(
 			const auto &info = lists[--before];
 			if (_showingUnreadSources && !info.unreadCount) {
 				continue;
-			} else if (!ranges::contains(_cachedSourcesList, info.id)) {
+			} else if (!ranges::contains(
+					_cachedSourcesList,
+					info.id,
+					&CachedSource::peerId)) {
 				_cachedSourcesList.insert(
 					begin(_cachedSourcesList),
-					info.id);
+					{ info.id });
 				++_cachedSourceIndex;
 			}
 		}
@@ -1352,8 +1369,11 @@ void Controller::rebuildCachedSourcesList(
 			const auto &info = lists[after];
 			if (_showingUnreadSources && !info.unreadCount) {
 				continue;
-			} else if (!ranges::contains(_cachedSourcesList, info.id)) {
-				_cachedSourcesList.push_back(info.id);
+			} else if (!ranges::contains(
+					_cachedSourcesList,
+					info.id,
+					&CachedSource::peerId)) {
+				_cachedSourcesList.push_back({ info.id });
 			}
 		}
 	}
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 1d83d24e5..760835a81 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -177,6 +177,14 @@ private:
 			const StoriesList &,
 			const StoriesList &) = default;
 	};
+	struct CachedSource {
+		PeerId peerId = 0;
+		StoryId shownId = 0;
+
+		explicit operator bool() const {
+			return peerId != 0;
+		}
+	};
 	class PhotoPlayback;
 	class Unsupported;
 
@@ -198,7 +206,7 @@ private:
 	void showSibling(
 		std::unique_ptr<Sibling> &sibling,
 		not_null<Main::Session*> session,
-		PeerId peerId);
+		CachedSource cached);
 
 	void subjumpTo(int index);
 	void checkWaitingFor();
@@ -264,7 +272,7 @@ private:
 	bool _started = false;
 	bool _viewed = false;
 
-	std::vector<PeerId> _cachedSourcesList;
+	std::vector<CachedSource> _cachedSourcesList;
 	int _cachedSourceIndex = -1;
 	bool _showingUnreadSources = false;
 
diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
index 0da004993..1024edadb 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
@@ -33,6 +33,17 @@ constexpr auto kSiblingFadeOver = 0.4;
 constexpr auto kSiblingNameOpacity = 0.8;
 constexpr auto kSiblingNameOpacityOver = 1.;
 
+[[nodiscard]] StoryId LookupShownId(
+		const Data::StoriesSource &source,
+		StoryId suggestedId) {
+	const auto i = suggestedId
+		? source.ids.lower_bound(Data::StoryIdDates{ suggestedId })
+		: end(source.ids);
+	return (i != end(source.ids) && i->id == suggestedId)
+		? suggestedId
+		: source.toOpen().id;
+}
+
 } // namespace
 
 class Sibling::Loader {
@@ -229,9 +240,10 @@ bool Sibling::LoaderVideo::updateAfterGoodCheck() {
 
 Sibling::Sibling(
 	not_null<Controller*> controller,
-	const Data::StoriesSource &source)
+	const Data::StoriesSource &source,
+	StoryId suggestedId)
 : _controller(controller)
-, _id{ source.user->id, source.ids.front().id }
+, _id{ source.user->id, LookupShownId(source, suggestedId) }
 , _peer(source.user) {
 	checkStory();
 	_goodShown.stop();
@@ -288,10 +300,14 @@ not_null<PeerData*> Sibling::peer() const {
 	return _peer;
 }
 
-bool Sibling::shows(const Data::StoriesSource &source) const {
-	Expects(!source.ids.empty());
-
-	return _id == FullStoryId{ source.user->id, source.ids.front().id };
+bool Sibling::shows(
+		const Data::StoriesSource &source,
+		StoryId suggestedId) const {
+	const auto fullId = FullStoryId{
+		source.user->id,
+		LookupShownId(source, suggestedId),
+	};
+	return (_id == fullId);
 }
 
 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 28cfc9cbc..24f334cae 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_sibling.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.h
@@ -26,12 +26,15 @@ class Sibling final : public base::has_weak_ptr {
 public:
 	Sibling(
 		not_null<Controller*> controller,
-		const Data::StoriesSource &source);
+		const Data::StoriesSource &source,
+		StoryId suggestedId);
 	~Sibling();
 
 	[[nodiscard]] FullStoryId shownId() const;
 	[[nodiscard]] not_null<PeerData*> peer() const;
-	[[nodiscard]] bool shows(const Data::StoriesSource &source) const;
+	[[nodiscard]] bool shows(
+		const Data::StoriesSource &source,
+		StoryId suggestedId) const;
 
 	[[nodiscard]] SiblingView view(
 		const SiblingLayout &layout,

From fd3169f82d31c68e26eaf23725cb01ff1c3b20a2 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 19:17:14 +0400
Subject: [PATCH 253/259] Fix admins list restore in group profile.

---
 .../SourceFiles/boxes/peers/edit_participants_box.cpp  | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
index 72291faf4..e1d09efec 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp
@@ -1165,15 +1165,19 @@ void ParticipantsBoxController::restoreState(
 		if (my->wasLoading) {
 			loadMoreRows();
 		}
+		const auto was = _fullCountValue.current();
 		PeerListController::restoreState(std::move(state));
-		const auto count = delegate()->peerListFullRowsCount();
-		if (count > 0 || _allLoaded) {
+		const auto now = delegate()->peerListFullRowsCount();
+		if (now > 0 || _allLoaded) {
 			refreshDescription();
 			if (_stories) {
-				for (auto i = 0; i != count; ++i) {
+				for (auto i = 0; i != now; ++i) {
 					_stories->process(delegate()->peerListRowAt(i));
 				}
 			}
+			if (now != was) {
+				refreshRows();
+			}
 		}
 		if (_onlineSorter) {
 			_onlineSorter->sort();

From 5bda700c2ca00b2332ed7e33ec58c74dfa398bf1 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 19:25:55 +0400
Subject: [PATCH 254/259] Fix t.me/s/domain links.

---
 Telegram/SourceFiles/core/local_url_handlers.cpp | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index df0c92771..d18b842c0 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -345,7 +345,13 @@ bool ResolveUsernameOrPhone(
 	const auto params = url_parse_params(
 		match->captured(1),
 		qthelp::UrlParamNameTransform::ToLower);
-	const auto domain = params.value(u"domain"_q);
+	const auto domainParam = params.value(u"domain"_q);
+	const auto appnameParam = params.value(u"appname"_q);
+
+	// Fix t.me/s/username links.
+	const auto webChannelPreviewLink = (domainParam == u"s"_q)
+		&& !appnameParam.isEmpty();
+	const auto domain = webChannelPreviewLink ? appnameParam : domainParam;
 	const auto phone = params.value(u"phone"_q);
 	const auto validDomain = [](const QString &domain) {
 		return qthelp::regex_match(
@@ -385,7 +391,7 @@ bool ResolveUsernameOrPhone(
 	}
 	const auto storyParam = params.value(u"story"_q);
 	const auto storyId = storyParam.toInt();
-	const auto appname = params.value(u"appname"_q);
+	const auto appname = webChannelPreviewLink ? QString() : appnameParam;
 	const auto appstart = params.value(u"startapp"_q);
 	const auto commentParam = params.value(u"comment"_q);
 	const auto commentId = commentParam.toInt();

From 87206a6c799ceca58c02b6808165ea6ce6a96dac Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 19:49:58 +0400
Subject: [PATCH 255/259] Use transparent outline for stories in chats list.

---
 .../dialogs/ui/dialogs_stories_list.cpp       | 70 +++++++++++++++----
 .../dialogs/ui/dialogs_stories_list.h         |  8 +++
 2 files changed, 65 insertions(+), 13 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index 1703c29d4..b7f5abd3a 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -41,6 +41,7 @@ struct List::Layout {
 	int itemsCount = 0;
 	QPointF geometryShift;
 	float64 expandedRatio = 0.;
+	float64 expandRatio = 0.;
 	float64 ratio = 0.;
 	float64 segmentsSpinProgress = 0.;
 	float64 thumbnailLeft = 0.;
@@ -210,6 +211,12 @@ List::Layout List::computeLayout(float64 expanded) const {
 	const auto collapsedRatio = expandedRatio * kFrictionRatio;
 	const auto ratio = expandedRatio * expanded
 		+ collapsedRatio * (1. - expanded);
+	const auto expandRatio = (ratio >= kCollapseAfterRatio)
+		? 1.
+		: (ratio <= kExpandAfterRatio * kFrictionRatio)
+		? 0.
+		: ((ratio - (kExpandAfterRatio * kFrictionRatio))
+			/ (kCollapseAfterRatio - (kExpandAfterRatio * kFrictionRatio)));
 
 	const auto lerp = [&](float64 a, float64 b) {
 		return a + (b - a) * ratio;
@@ -257,6 +264,7 @@ List::Layout List::computeLayout(float64 expanded) const {
 				? (lerp(_changingGeometryFrom.y(), _geometryFull.y()) - y())
 				: 0.)),
 		.expandedRatio = expandedRatio,
+		.expandRatio = expandRatio,
 		.ratio = ratio,
 		.segmentsSpinProgress = segmentsSpinProgress,
 		.thumbnailLeft = thumbnailLeft,
@@ -280,12 +288,7 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto &full = _st.full;
 	const auto layout = computeLayout();
 	const auto ratio = layout.ratio;
-	const auto expandRatio = (ratio >= kCollapseAfterRatio)
-		? 1.
-		: (ratio <= kExpandAfterRatio * kFrictionRatio)
-		? 0.
-		: ((ratio - kExpandAfterRatio * kFrictionRatio)
-			/ (kCollapseAfterRatio - kExpandAfterRatio * kFrictionRatio));
+	const auto expandRatio = layout.expandRatio;
 	const auto lerp = [&](float64 a, float64 b) {
 		return a + (b - a) * ratio;
 	};
@@ -293,11 +296,50 @@ void List::paintEvent(QPaintEvent *e) {
 		return a + (b - a) * expandRatio;
 	};
 	const auto line = elerp(st.lineTwice, full.lineTwice) / 2.;
+	const auto photo = lerp(st.photo, full.photo);
+	const auto layered = layout.single < (photo + 4 * line);
+	auto p = QPainter(this);
+	if (layered) {
+		ensureLayer();
+		auto q = QPainter(&_layer);
+		paint(q, layout, photo, line, true);
+		q.end();
+		p.drawImage(0, 0, _layer);
+	} else {
+		paint(p, layout, photo, line, false);
+	}
+}
+
+void List::ensureLayer() {
+	const auto ratio = style::DevicePixelRatio();
+	const auto layer = size() * ratio;
+	if (_layer.size() != layer) {
+		_layer = QImage(layer, QImage::Format_ARGB32_Premultiplied);
+		_layer.setDevicePixelRatio(ratio);
+	}
+	_layer.fill(Qt::transparent);
+}
+
+void List::paint(
+		QPainter &p,
+		const Layout &layout,
+		float64 photo,
+		float64 line,
+		bool layered) {
+	const auto &st = _st.small;
+	const auto &full = _st.full;
+	const auto ratio = layout.ratio;
+	const auto expandRatio = layout.expandRatio;
+	const auto lerp = [&](float64 a, float64 b) {
+		return a + (b - a) * ratio;
+	};
+	const auto elerp = [&](float64 a, float64 b) {
+		return a + (b - a) * expandRatio;
+	};
 	const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.;
 	const auto photoTopSmall = st.photoTop;
 	const auto photoTop = photoTopSmall
 		+ (full.photoTop - photoTopSmall) * layout.expandedRatio;
-	const auto photo = lerp(st.photo, full.photo);
 	const auto nameScale = _lastRatio;
 	const auto nameTop = full.nameTop
 		+ (photoTop + photo - full.photoTop - full.photo);
@@ -306,9 +348,6 @@ void List::paintEvent(QPaintEvent *e) {
 	const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;
 	const auto readUserpicOpacity = elerp(_st.readOpacity, 1.);
 	const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.);
-
-	auto p = QPainter(this);
-
 	if (_state == State::Changing) {
 		p.translate(layout.geometryShift);
 	}
@@ -438,6 +477,7 @@ void List::paintEvent(QPaintEvent *e) {
 			gradient.setFinalStop(userpic.bottomLeft());
 			if (!fullUnreadCount) {
 				p.setPen(QPen(gradient, line));
+				p.setBrush(Qt::NoBrush);
 				p.drawEllipse(outer);
 			} else {
 				validateSegments(itemFull, gradient, line, true);
@@ -475,9 +515,13 @@ void List::paintEvent(QPaintEvent *e) {
 			: expandRatio);
 		const auto add = line + (hasReadLine ? (lineRead / 2.) : 0.);
 		const auto rect = userpic.marginsAdded({ add, add, add, add });
-		p.setPen(Qt::NoPen);
-		p.setBrush(st::dialogsBg);
-		p.drawEllipse(rect);
+		if (layered) {
+			p.setCompositionMode(QPainter::CompositionMode_Source);
+			p.setPen(Qt::NoPen);
+			p.setBrush(st::transparent);
+			p.drawEllipse(rect);
+			p.setCompositionMode(QPainter::CompositionMode_SourceOver);
+		}
 		if (hasReadLine) {
 			if (small && !small->element.unreadCount) {
 				p.setOpacity(expandRatio);
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index ef3b2917e..a870f83d6 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -123,6 +123,13 @@ private:
 	void mouseReleaseEvent(QMouseEvent *e) override;
 	void contextMenuEvent(QContextMenuEvent *e) override;
 
+	void paint(
+		QPainter &p,
+		const Layout &layout,
+		float64 photo,
+		float64 line,
+		bool layered);
+	void ensureLayer();
 	void validateThumbnail(not_null<Item*> item);
 	void validateName(not_null<Item*> item);
 	void updateScrollMax();
@@ -156,6 +163,7 @@ private:
 	rpl::event_stream<> _loadMoreRequests;
 	rpl::event_stream<> _collapsedGeometryChanged;
 
+	QImage _layer;
 	QPoint _positionSmall;
 	style::align _alignSmall = {};
 	QRect _geometryFull;

From daef7faaa4566af44c7f013dd04493d7b429e61f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 20:23:52 +0400
Subject: [PATCH 256/259] Don't open stories from userpic in narrow chats mode.

---
 Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 0a01ddda7..d0d01778b 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -3336,7 +3336,8 @@ bool InnerWidget::chooseRow(
 			Qt::KeyboardModifiers modifiers) {
 		row.newWindow = (modifiers & Qt::ControlModifier);
 		row.userpicClick = (_lastRowLocalMouseX >= 0)
-			&& (_lastRowLocalMouseX < _st->nameLeft);
+			&& (_lastRowLocalMouseX < _st->nameLeft)
+			&& (width() > _narrowWidth);
 		return row;
 	};
 	auto chosen = modifyChosenRow(computeChosenRow(), modifiers);

From 676a3f8cfa8101a500bcf60e1ea9f110104a85c8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 20:31:10 +0400
Subject: [PATCH 257/259] Open hidden stories by archive userpic click.

---
 Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 9ff80e9fd..5b1518d65 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -526,7 +526,7 @@ void Widget::chosenRow(const ChosenRow &row) {
 		return;
 	} else if (history) {
 		const auto peer = history->peer;
-		if (const auto user = history->peer->asUser()) {
+		if (const auto user = peer->asUser()) {
 			if (row.message.fullId.msg == ShowAtUnreadMsgId) {
 				if (row.userpicClick
 					&& user->hasActiveStories()
@@ -549,6 +549,14 @@ void Widget::chosenRow(const ChosenRow &row) {
 			hideChildList();
 		}
 	} else if (const auto folder = row.key.folder()) {
+		if (row.userpicClick) {
+			const auto list = Data::StorySourcesList::Hidden;
+			const auto &sources = session().data().stories().sources(list);
+			if (!sources.empty()) {
+				controller()->openPeerStories(sources.front().id, list);
+				return;
+			}
+		}
 		controller()->openFolder(folder);
 		hideChildList();
 	}

From 0b32a0a1eadc01764f26d3bbf2f227c591f714e3 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 21:50:35 +0400
Subject: [PATCH 258/259] Lock scroll either horizontal or vertical.

---
 .../SourceFiles/dialogs/dialogs_widget.cpp    |  5 ++-
 .../dialogs/ui/dialogs_stories_list.cpp       | 34 ++++++++++++++-----
 .../dialogs/ui/dialogs_stories_list.h         |  6 ++++
 3 files changed, 35 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 5b1518d65..1b696ae8d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -793,7 +793,10 @@ void Widget::setupMainMenuToggle() {
 }
 
 void Widget::setupStories() {
-	trackScroll(_stories.get());
+	_stories->verticalScrollEvents(
+	) | rpl::start_with_next([=](not_null<QWheelEvent*> e) {
+		_scroll->viewportEvent(e);
+	}, _stories->lifetime());
 
 	_storiesContents.fire(Stories::ContentForSession(
 		&controller()->session(),
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
index b7f5abd3a..1a1a395d3 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
@@ -152,6 +152,10 @@ rpl::producer<> List::loadMoreRequests() const {
 	return _loadMoreRequests.events();
 }
 
+rpl::producer<not_null<QWheelEvent*>> List::verticalScrollEvents() const {
+	return _verticalScrollEvents.events();
+}
+
 void List::requestExpanded(bool expanded) {
 	if (_expanded != expanded) {
 		_expanded = expanded;
@@ -635,18 +639,30 @@ void List::validateName(not_null<Item*> item) {
 }
 
 void List::wheelEvent(QWheelEvent *e) {
-	const auto horizontal = (e->angleDelta().x() != 0);
-	if (!horizontal || _state == State::Small) {
+	const auto phase = e->phase();
+	const auto fullDelta = e->pixelDelta().isNull()
+		? e->angleDelta()
+		: e->pixelDelta();
+	if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) {
+		_scrollingLock = Qt::Orientation();
+		if (fullDelta.isNull()) {
+			return;
+		}
+	}
+	const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y());
+	if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) {
+		_scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal;
+	}
+	if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) {
+		_verticalScrollEvents.fire(e);
+		return;
+	} else if (_state == State::Small) {
 		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());
+	const auto delta = vertical
+		? fullDelta.y()
+		: ((style::RightToLeft() ? -1 : 1) * fullDelta.x());
 
 	const auto now = _scrollLeft;
 	const auto used = now - delta;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
index a870f83d6..3182d7229 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
@@ -91,6 +91,9 @@ public:
 	[[nodiscard]] rpl::producer<> entered() const;
 	[[nodiscard]] rpl::producer<> loadMoreRequests() const;
 
+	[[nodiscard]] auto verticalScrollEvents() const
+		-> rpl::producer<not_null<QWheelEvent*>>;
+
 private:
 	struct Layout;
 	enum class State {
@@ -177,6 +180,7 @@ private:
 	int _scrollLeft = 0;
 	int _scrollLeftMax = 0;
 	bool _dragging = false;
+	Qt::Orientation _scrollingLock = {};
 
 	Ui::Animations::Simple _expandedAnimation;
 	Ui::Animations::Simple _expandCatchUpAnimation;
@@ -188,6 +192,8 @@ private:
 	int _selected = -1;
 	int _pressed = -1;
 
+	rpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;
+
 	base::unique_qptr<Ui::PopupMenu> _menu;
 	base::has_weak_ptr _menuGuard;
 

From 0c61e0e184db790359704ddf3b91c915ead19fd0 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 21 Jul 2023 21:59:08 +0400
Subject: [PATCH 259/259] Version 4.8.7.

- Several crash fixes and small stories improvements.
---
 Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +-
 Telegram/Resources/winrc/Telegram.rc         | 8 ++++----
 Telegram/Resources/winrc/Updater.rc          | 8 ++++----
 Telegram/SourceFiles/core/version.h          | 4 ++--
 Telegram/build/version                       | 8 ++++----
 changelog.txt                                | 4 ++++
 6 files changed, 19 insertions(+), 15 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index c5647a2b5..898e980a2 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
   <Identity Name="TelegramMessengerLLP.TelegramDesktop"
     ProcessorArchitecture="ARCHITECTURE"
     Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
-    Version="4.8.6.0" />
+    Version="4.8.7.0" />
   <Properties>
     <DisplayName>Telegram Desktop</DisplayName>
     <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 9fc997288..8f8652e9e 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,6,0
- PRODUCTVERSION 4,8,6,0
+ FILEVERSION 4,8,7,0
+ PRODUCTVERSION 4,8,7,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "4.8.6.0"
+            VALUE "FileVersion", "4.8.7.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.6.0"
+            VALUE "ProductVersion", "4.8.7.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index a581fc869..c1a090788 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,8,6,0
- PRODUCTVERSION 4,8,6,0
+ FILEVERSION 4,8,7,0
+ PRODUCTVERSION 4,8,7,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop Updater"
-            VALUE "FileVersion", "4.8.6.0"
+            VALUE "FileVersion", "4.8.7.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2023"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "4.8.6.0"
+            VALUE "ProductVersion", "4.8.7.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 5b4f6e797..71ea8719a 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
 constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
 constexpr auto AppName = "Telegram Desktop"_cs;
 constexpr auto AppFile = "Telegram"_cs;
-constexpr auto AppVersion = 4008006;
-constexpr auto AppVersionStr = "4.8.6";
+constexpr auto AppVersion = 4008007;
+constexpr auto AppVersionStr = "4.8.7";
 constexpr auto AppBetaVersion = false;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index f85771589..1322ed7d7 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         4008006
+AppVersion         4008007
 AppVersionStrMajor 4.8
-AppVersionStrSmall 4.8.6
-AppVersionStr      4.8.6
+AppVersionStrSmall 4.8.7
+AppVersionStr      4.8.7
 BetaChannel        0
 AlphaVersion       0
-AppVersionOriginal 4.8.6
+AppVersionOriginal 4.8.7
diff --git a/changelog.txt b/changelog.txt
index fb06e9a51..314f815c6 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,7 @@
+4.8.7 (21.07.23)
+
+- Several crash fixes and small stories improvements.
+
 4.8.6 (21.07.23)
 
 - Fix langpack keys by a full rebuild on macOS.