From e6e1b9446d692d1e8b8869edb2c9bc6427bb6773 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= <dnovomesky@gmail.com>
Date: Wed, 3 Jul 2024 16:38:23 +0200
Subject: [PATCH 001/163] Upgrade libjxl on Linux to 0.10.3

---
 Telegram/build/docker/centos_env/Dockerfile | 2 +-
 snap/snapcraft.yaml                         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 2b0c82ed0..71c9533eb 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -250,7 +250,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.10.2 --depth=1 {{ GIT }}/libjxl/libjxl.git \
+RUN git clone -b v0.10.3 --depth=1 {{ GIT }}/libjxl/libjxl.git \
 	&& cd libjxl \
 	&& git apply ../patches/libjxl.patch \
 	&& git submodule update --init --recursive --depth=1 third_party/libjpeg-turbo \
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 454da33b1..033424e1d 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -237,7 +237,7 @@ parts:
   libjxl:
     source: https://github.com/libjxl/libjxl.git
     source-depth: 1
-    source-tag: v0.10.2
+    source-tag: v0.10.3
     plugin: cmake
     build-environment:
       - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s

From 9ca990473232ff0dced5fbdb9f848bae61a0b797 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= <dnovomesky@gmail.com>
Date: Thu, 4 Jul 2024 09:12:54 +0200
Subject: [PATCH 002/163] Upgrade libjxl to v0.10.3

---
 Telegram/build/prepare/prepare.py | 8 +++-----
 cmake                             | 2 +-
 2 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index 60d60ea47..8b1f71c68 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -938,7 +938,7 @@ mac:
 """)
 
 stage('libjxl', """
-    git clone -b v0.8.2 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git
+    git clone -b v0.10.3 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git
     cd libjxl
 """ + setVar("cmake_defines", """
     -DBUILD_SHARED_LIBS=OFF
@@ -954,12 +954,10 @@ stage('libjxl', """
     -DJPEGXL_ENABLE_SJPEG=OFF
     -DJPEGXL_ENABLE_OPENEXR=OFF
     -DJPEGXL_ENABLE_SKCMS=ON
-    -DJPEGXL_BUNDLE_SKCMS=ON
     -DJPEGXL_ENABLE_VIEWERS=OFF
     -DJPEGXL_ENABLE_TCMALLOC=OFF
     -DJPEGXL_ENABLE_PLUGINS=OFF
     -DJPEGXL_ENABLE_COVERAGE=OFF
-    -DJPEGXL_ENABLE_PROFILER=OFF
     -DJPEGXL_WARNINGS_AS_ERRORS=OFF
 """) + """
 win:
@@ -967,8 +965,8 @@ win:
         -A %WIN32X64% ^
         -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^
         -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$<CONFIG:Debug>:Debug>" ^
-        -DCMAKE_C_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^
-        -DCMAKE_CXX_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^
+        -DCMAKE_C_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE" ^
+        -DCMAKE_CXX_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE" ^
         -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^
         -DCMAKE_CXX_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^
         -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^
diff --git a/cmake b/cmake
index 5742caae6..4a4bc4cd3 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 5742caae65e4163e7faec238eb4e3e5c219ad09c
+Subproject commit 4a4bc4cd34b3ade038541a2b8b2c79f05393d67b

From 8d0d9bb0bd2fbeb8ef826e582bef5b0620950620 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Sat, 6 Jul 2024 00:43:18 +0400
Subject: [PATCH 003/163] Delay clearing transient parent until the pip window
 is really exposed

---
 Telegram/SourceFiles/media/view/media_view_pip.cpp | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp
index 0975b5c6f..da359769f 100644
--- a/Telegram/SourceFiles/media/view/media_view_pip.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp
@@ -351,10 +351,14 @@ void PipPanel::init() {
 	widget()->resize(0, 0);
 	widget()->hide();
 
-	rp()->shownValue(
-	) | rpl::filter([=](bool shown) {
-		return shown;
-	}) | rpl::start_with_next([=] {
+	rpl::merge(
+		rp()->shownValue() | rpl::to_empty,
+		rp()->paintRequest() | rpl::to_empty
+	) | rpl::map([=] {
+		return widget()->windowHandle()
+			&& widget()->windowHandle()->isExposed();
+	}) | rpl::distinct_until_changed(
+	) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] {
 		// Workaround Qt's forced transient parent.
 		Ui::Platform::ClearTransientParent(widget());
 	}, rp()->lifetime());

From aa140b2919c46c0e0f0117f59bbf946aa3124043 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Wed, 3 Jul 2024 22:55:10 +0400
Subject: [PATCH 004/163] Fix warnings on Windows

---
 .../history/view/history_view_pinned_bar.cpp  |  4 ++--
 .../media/history_view_similar_channels.cpp   |  4 ++--
 .../SourceFiles/platform/win/specific_win.cpp |  7 +++----
 Telegram/SourceFiles/platform/win/tray_win.h  |  2 +-
 .../details/storage_settings_scheme.cpp       |  2 +-
 Telegram/cmake/lib_tgvoip.cmake               | 21 +++++++------------
 6 files changed, 16 insertions(+), 24 deletions(-)

diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp
index d530e4779..eebbb1848 100644
--- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp
@@ -76,9 +76,9 @@ namespace {
 			return rpl::single(ContentWithoutPreview(item, repaint));
 		}
 		constexpr auto kFullLoaded = 2;
-		constexpr auto kSomeLoaded = 1;
-		constexpr auto kNotLoaded = 0;
 		const auto loadedLevel = [=] {
+			constexpr auto kSomeLoaded = 1;
+			constexpr auto kNotLoaded = 0;
 			const auto preview = media->replyPreview();
 			return media->replyPreviewLoaded()
 				? kFullLoaded
diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp
index 05a03df4d..aa14b12c1 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp
@@ -167,7 +167,7 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const {
 	_hasHeavyPart = 1;
 	validateLastPremiumLock();
 	const auto drawOne = [&](const Channel &channel) {
-		const auto geometry = channel.geometry.translated(-_scrollLeft, 0);
+		const auto geometry = channel.geometry.translated(-int(_scrollLeft), 0);
 		const auto right = geometry.x() + geometry.width();
 		if (right <= 0) {
 			return;
@@ -501,7 +501,7 @@ TextState SimilarChannels::textState(
 		return result;
 	}
 	for (const auto &channel : _channels) {
-		if (channel.geometry.translated(-_scrollLeft, 0).contains(point)) {
+		if (channel.geometry.translated(-int(_scrollLeft), 0).contains(point)) {
 			result.link = channel.link;
 			_lastPoint = point
 				+ QPoint(_scrollLeft, 0)
diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp
index 7a3b133c4..a35cfd3c4 100644
--- a/Telegram/SourceFiles/platform/win/specific_win.cpp
+++ b/Telegram/SourceFiles/platform/win/specific_win.cpp
@@ -98,7 +98,6 @@ BOOL CALLBACK FindToActivate(HWND hwnd, LPARAM lParam) {
 		return TRUE;
 	}
 	// Found a Top-Level window.
-	auto level = 0;
 	if (WindowIdFromHWND(hwnd) == request->windowId) {
 		request->result = hwnd;
 		request->resultLevel = 3;
@@ -310,8 +309,8 @@ void psDoFixPrevious() {
 		if (oldKeyRes2 == ERROR_SUCCESS) RegCloseKey(oldKey2);
 
 		if (existNew1 || existNew2) {
-			const auto deleteKeyRes1 = existOld1 ? RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str()) : ERROR_SUCCESS;
-			const auto deleteKeyRes2 = existOld2 ? RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str()) : ERROR_SUCCESS;
+			if (existOld1) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str());
+			if (existOld2) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str());
 		}
 
 		QString userDesktopLnk, commonDesktopLnk;
@@ -326,7 +325,7 @@ void psDoFixPrevious() {
 		}
 		QFile userDesktopFile(userDesktopLnk), commonDesktopFile(commonDesktopLnk);
 		if (QFile::exists(userDesktopLnk) && QFile::exists(commonDesktopLnk) && userDesktopLnk != commonDesktopLnk) {
-			bool removed = QFile::remove(commonDesktopLnk);
+			QFile::remove(commonDesktopLnk);
 		}
 	} catch (...) {
 	}
diff --git a/Telegram/SourceFiles/platform/win/tray_win.h b/Telegram/SourceFiles/platform/win/tray_win.h
index cb79c3beb..aed4cc6e3 100644
--- a/Telegram/SourceFiles/platform/win/tray_win.h
+++ b/Telegram/SourceFiles/platform/win/tray_win.h
@@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/unique_qptr.h"
 
 namespace Window {
-class CounterLayerArgs;
+struct CounterLayerArgs;
 } // namespace Window
 
 namespace Ui {
diff --git a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp
index 9baca321c..695f8f58a 100644
--- a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp
+++ b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp
@@ -553,7 +553,7 @@ bool ReadSetting(
 				const auto proxy = readProxy();
 				if (proxy) {
 					list.push_back(proxy);
-				} else if (index < -list.size()) {
+				} else if (index < -int64(list.size())) {
 					++index;
 				} else if (index > list.size()) {
 					--index;
diff --git a/Telegram/cmake/lib_tgvoip.cmake b/Telegram/cmake/lib_tgvoip.cmake
index 886cd9935..fbae70966 100644
--- a/Telegram/cmake/lib_tgvoip.cmake
+++ b/Telegram/cmake/lib_tgvoip.cmake
@@ -134,20 +134,13 @@ PRIVATE
 )
 
 if (WIN32)
-    if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
-        target_compile_options(lib_tgvoip_bundled
-        PRIVATE
-            /wd4005
-            /wd4244 # conversion from 'int' to 'float', possible loss of data (several in webrtc)
-            /wd5055 # operator '>' deprecated between enumerations and floating-point types
-        )
-    else()
-        target_compile_definitions(lib_tgvoip_bundled
-        PUBLIC
-            # Doesn't build with mingw for now
-            TGVOIP_NO_DSP
-        )
-    endif()
+    target_compile_options_if_exists(lib_tgvoip_bundled
+    PRIVATE
+        /wd4005 # 'identifier' : macro redefinition
+        /wd4068 # unknown pragma
+        /wd4996 # deprecated
+        /wd5055 # operator '>' deprecated between enumerations and floating-point types
+    )
 elseif (APPLE)
     target_compile_definitions(lib_tgvoip_bundled
     PUBLIC

From bf7042df4456302f2104fb41afcaa5c5dfaf3662 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Wed, 3 Jul 2024 22:55:30 +0400
Subject: [PATCH 005/163] Enable warnings as errors on Windows

---
 .github/workflows/win.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml
index ca0c7bf09..6de53f12d 100644
--- a/.github/workflows/win.yml
+++ b/.github/workflows/win.yml
@@ -169,6 +169,8 @@ jobs:
           %TDESKTOP_BUILD_GENERATOR% ^
           %TDESKTOP_BUILD_ARCH% ^
           %TDESKTOP_BUILD_API% ^
+          -D CMAKE_C_FLAGS="/WX" ^
+          -D CMAKE_CXX_FLAGS="/WX" ^
           -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^
           -D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^
           -D DESKTOP_APP_NO_PDB=ON ^

From 054a6db3aee58d2cd024e747bfba9c29f19b13b3 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 11:07:07 +0400
Subject: [PATCH 006/163] Fix warnings on Windows in submodules.

---
 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 f30400147..1a50fd230 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit f30400147d997fedc787e214467d305db6c159e7
+Subproject commit 1a50fd2300da3198e751a22bf728d33822180e15
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index ebd8609ee..067733899 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit ebd8609ee73d48186b905787dd5bb3bcbb4f9f3f
+Subproject commit 067733899d62caa5411f54904a431d3484fd02e3

From 66d6b461f39013ba710e978aba2296f3d4a0e56c Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Thu, 4 Jul 2024 09:34:27 +0300
Subject: [PATCH 007/163] Fixed support type of credits history entry for ads
 in earn section.

---
 Telegram/SourceFiles/ui/effects/credits.style        | 2 ++
 Telegram/SourceFiles/ui/effects/credits_graphics.cpp | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style
index e78642348..baab38fb7 100644
--- a/Telegram/SourceFiles/ui/effects/credits.style
+++ b/Telegram/SourceFiles/ui/effects/credits.style
@@ -47,3 +47,5 @@ creditsBoxButtonLabel: FlatLabel(defaultFlatLabel) {
 
 starIconSmall: icon{{ "payments/small_star", windowFg }};
 starIconSmallPadding: margins(0px, -2px, 0px, 0px);
+
+creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }};
diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
index 9f48191f0..3f4ce02f2 100644
--- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
+++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
@@ -205,6 +205,8 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback(
 			return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 };
 		case Data::CreditsHistoryEntry::PeerType::PremiumBot:
 			return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 };
+		case Data::CreditsHistoryEntry::PeerType::Ads:
+			return { st::historyPeer6UserpicBg, st::historyPeer6UserpicBg2 };
 		case Data::CreditsHistoryEntry::PeerType::Unsupported:
 			return {
 				st::historyPeerArchiveUserpicBg,
@@ -227,6 +229,8 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback(
 			? st::sessionIconAndroid
 			: (entry.peerType == PeerType::Fragment)
 			? st::introFragmentIcon
+			: (entry.peerType == PeerType::Ads)
+			? st::creditsHistoryEntryTypeAds
 			: st::dialogsInaccessibleUserpic).paintInCenter(p, rect);
 	};
 }
@@ -442,6 +446,8 @@ Fn<PaintRoundImageCallback(Fn<void()>)> PaintPreviewCallback(
 TextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) {
 	return ((entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment)
 		? tr::lng_bot_username_description1_link
+		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::Ads)
+		? tr::lng_credits_box_history_entry_ads
 		: tr::lng_credits_summary_history_entry_inner_in)(
 			tr::now,
 			TextWithEntities::Simple);

From b377c02ad38d5043cbfaab37b3db7a86cfe381c9 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Thu, 4 Jul 2024 10:07:18 +0300
Subject: [PATCH 008/163] Added support of min boost level for channel ads to
 feature list.

---
 .../premium/features/feature_off_sponsored.png   | Bin 0 -> 771 bytes
 .../features/feature_off_sponsored@2x.png        | Bin 0 -> 1540 bytes
 .../features/feature_off_sponsored@3x.png        | Bin 0 -> 2194 bytes
 .../boxes/peers/replace_boost_box.cpp            |   1 +
 Telegram/SourceFiles/ui/boxes/boost_box.cpp      |  11 +++++++++--
 Telegram/SourceFiles/ui/boxes/boost_box.h        |   1 +
 Telegram/SourceFiles/ui/effects/premium.style    |   1 +
 7 files changed, 12 insertions(+), 2 deletions(-)
 create mode 100644 Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png
 create mode 100644 Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png
 create mode 100644 Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png

diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored.png
new file mode 100644
index 0000000000000000000000000000000000000000..06a33dc3c089717f0c8ad67beca035d1daea3184
GIT binary patch
literal 771
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfhpJ1#WBP}
z@NS5$$Erk;bMvQMaB<}3bQMwMIJlc*nzQ`{F%6D~HJvgnf+n{_SR7?aiUf04EY#$2
z(a02<xF};+_l}T`XGvWF|L&_V4X^&a;^#f%6PC{_&wc!}_x_nP_1ZF*B)g9){W9lj
zKYa7vy|C3+`T6+|Kb(;E>g%uR)5W`wIx)}BT^m+iUw=PudwhKS`Sa)Po0YpyKb<#k
z-nS~-_QMAm`eSzQ-hDRh^P4w5I%4|z`io`$*fLA5bzK~Ipl-kX1{Wnm37-9SDq`IW
z1*V^NRZ3j9e!cMIFg`v$wq{2gx#K!!=H}A9Y(dWR=FjhSdu%e>>xk-&fB)>V_*$K&
zdZ|wI(9zRdBwmr0o^IF&v}pEhX{UdyHgulO+RDYkG~@X3<JQ*J($do5;o%t@F2A&~
zvs<KbC?!O#Q%p>3)zw!sN(u`bU8H)|=AYM|>eYYz^Or9!M}t?YcWT^w`}XavTT;KY
zM7Xr3zIySZLn+{yieQ+wQ0KCTK`XcH+SN7jX_2MI<qPGz&Og@{>n_?kM~Sob(CO2s
zw{JI}eb$Wk;Nd<0cJADnW7fTN(FA+>Cf}JpY9<v$MVH=xpX;Z7(sk|pANNJ?q$mii
zxc<7jx_ZC+qpwwkUOSQ<9u`P213mv(;>S#Fsl%T?e_k8L{aSvux3@P(n1)Esx8Ejr
z^UTkQHXomCH2bW@U&G#?<}-aJv1?8~If0|xVMW<)S>IG6NtwI+@;o{#Zogf(W{pCG
z)>Ng5e#@0R1@>&++A0c+C}!2VG}WnIQ%^tjP-(i9wN+~V`Q;TAr!Baz`2EbBeEhMj
toZPa@nL6BtCN`S_&s~pBO!@cv-2T3`qBhw)RyRQj(9_k=Wt~$(69A{aJm3HT

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..996851dbc9f57f50181b43546053c9f9a7389393
GIT binary patch
literal 1540
zcmV+f2K)JmP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NHZb?KzR9Fe^SXn5hZ5U>3VeFD6
zOUM?b$Tl%UYRs4j2ew2)NLdQWgsD+z!~q8s2TGVDLu!yEJK2+xiG!K3jAm?OU;h94
z-FfDn?|r?#@B8QAuity{d7gW@?)!b7dwVG;JUkC4@NfeEvlCF{zpt8_+S%C|pDs5=
zOG_&)EzQl%O<7sFySux!we{fOKw30kU*EmGy}7wLTU%RcbwuUD!ot3P|Na-}SzcbY
zw6qje;xO~`^GWyHw{Pz5?i`NHOI20X>gsCd$@uvA%*+gJ@xqFV0+%CTLtkHiVPS#P
ze*XOV{P}YMAt^{!R#uaflZ3OevGMHLGg5l}`ZW;)0|TXixe?LP(HR&RAhq-J^SHP;
zX*F-<LPJA;{P;l#y}i9gMn<<B1zla;i;D}ea&mGQ@%{zk`u+R&gm`^@otKw)zs`+>
zii%2QWhE(IU0oFw6+L?NNJ4jie;)!RB_$Gc+A0fDk!4X)QQO<w85tS4?DFz*Q&ZFI
z>}&u*UQbUiC@4rnLxZoIl$7M<<puiI)>dq6Y=3`0U*=y?3JMD7b@AKQ)`mDGI1394
zBnx9nEZ~Q+vGHAo#ZO3Bi+j%T@$q!)unJ4s+S;Up@E;u=m2d^yw{PEa)%gbGap=vP
zH{76m@!|zCj%?4*&zqQ-Ff+I~xQGYxB!Kv1T)~CR4%yh)(2*o3Co@D3509Ol9WuGT
zzHV=C&selbPf!0N4oRi}&_hB(P~)-a>+54yr>3TSe0<;vD5x>GEKg5Q#{Gi}0f(%u
ztziIZvbMIywY0jr3ZGD9K{PNhKs*Hp2Qz`e!9nCX+seHVa7bNU9dM{@d{B7#^yyPN
zIYdu;dwXJHB8egC!o$N)PEPn*GEoE^64yUGJbd}`B|0|5aI*~!4TXh;q|(vRAwxsB
z!;H)h0SEmgCMKq;s){g?=e4!9l<H$+W8mIlODeKF4gnYmg*?yA&BX#+q!?NaB|K52
zGA<+};1I<ox{{FmZ?t#s-l6rPMt~X-5z*7rgWma@gzOsuhcF2N2f0SkEsojT-2Cd*
zE0j%$`T6+`4-cD}nTe~2a|Ik48XAI*l9CcGLR^`+I2epO^cv32&KSfT9UY}nlvW1e
z37-%O$YE}=7S~68N1=ii9?Yyyo;<lrUc7Y(&yhgK+^DIk3DB=!zoLH~A0G=E`1I*h
zSy>qk?HI(my1Hoa#mhuTM}PS6fv*LJ(EHFX?;45tD*sSiTwG>nXX%Z_e{nEW+@baO
z@#BvlKa%|A<z-4r3Ljnkg!uBmC`dFEV(Rk<5d;VSSz1~;Jw5&W`LmOg6KO)Lsi}#*
zLveBOFT|Ub{0<RNeSJO9kB*KoYfC_5i%vqeU0q!n7ei)6MFsPzuC5L#!npJiA=`a1
zWh^jw5Oiv4Dwj@cZ*Onn(&Wk&$jZvXJWgD6Jyt{zYSYuxtl@A~q+KsAE&>-Qd<A@u
zl1UUHJ~uKl65}}ujEsz6$Rrtrnn)V*aU?03nVAI2y%!Qf>=Wy*wi*x+KtTgBOa$iU
z=91`g?D1xZTp-re)JUj1I5?0)7;`09Y>U^}+}tFwFJHc(crup!i<XuaAnxw&vQQu_
zBs^Kd$Ds&1hRl_f6_Nyq;QflI3$XnC{RvJYjqU90h>jsiK#fB(G&G!;m>^-CH<~er
zDc2JJNdX)Zr?j+`zK-$nMJETkH2=`Rkcm=GK4GRn{K@6WtrGeP3LwGZmwl{ZjbZ3k
z>Yl$CG8-Ek2}Jys?*Sq<MhiM8<`x#6ot?6dL72W15)!!8l9?B7u8b|XKxW|^2p=9O
q-xz?9%z|PM#D^1jID!A>3H%L+7`mZr(d-}q0000<MNUMnLSTY;iMhQ1

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png b/Telegram/Resources/icons/settings/premium/features/feature_off_sponsored@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..0863b33e8df564738eaaae505f5f6d44a73ab374
GIT binary patch
literal 2194
zcmYLLcOca7AHQ=l%9fdokQ_2Ak@>|TbjT(v>&wrcot+Wq98oSKdqi0$d)#rHLeeFq
zIwPm^wb#k~efs_J`~LBM-p?Md=k<O)Pa50|#(G}xJO~70H8Rw*0KDNJW1<7nuV@Jl
zz=1<7V7j3C4?<L+pyy$0<Y{UOf&zIa5IEHbME|D?V1j^wK#&qJ2m*NUpSlvd|F;^J
zK>p4D>6oQxumJ)w9vkWD-1k1WmLE3E<G_L2+re@-sVi!m5Z2iUY!~(Kv%v8<`ZM^d
zw!RK03(Lg|vW+}lJcIrH>;5qtsYjSwg(w6qwo8Ql#}BRR*bs_I5s-}f<*Q7{Z)^Wd
z7=~msUSZc0esU+vUf3|tK9MUsCpY(g4ZKv=t~vVj_;&Vh{}3gw4~`|;;I;|))?}Rx
zxy6Tj|9Eeiw!Jj6G?JGGwSt05<s4d%H|HUKQw`4OWN=^xT0b~=d-R2J+{vNGi;6Sn
zo<~+~{@)L#+bxT3hZtal)x%{Y*1xT{B%m%zThQBGnPQ9ny<KUC^^|@Zaj@w+M*I5K
zcQBpjE+^48$Dz%C>^VtSMB`D5-%Rr}8JlM7#J<>*N1dV4OsrOQ)-flCZyO<zz_3c=
zXKt_ZhE>zeiZxGY8{{i&k^8GN0?M8((@SFov~L|DN?*%iXg$JkVLFeDX5^;!G`KtF
zsMU2KMVEsK-X1tF4$JhGetoYie2s;oYVS=MEAYxVjMZBKR!tJe0jKK<r7jd?kI?IW
z-dP^MamFoW*1i@cgVE{sox08!8~d>%cuDDw@vz`cKa7YlTZ>=cE6eJYwbUV5{li`A
z<(jh+!nsto%faPaspGlQBw+rx&I#3~1r#*0?7D5E=zE`(?(xfNp>PSd68V9rm-(mR
zetq>$UDqFUM-FF+DXF^l(|<}#fH0mO|F+C}$o!a*{kl_Ui;EEWW=L=J?}xv3sif~+
z5q%LGFI_AyVN`schAVG=C{kB6_y@o3Khr!Yx)bTx=%SOtBA~)}YkDfZLosL&K<bQ$
z7O|hOtTtPEmjk;eN_1#`m{t{(l-5+F7WRp|XFi@aXF#ul3#k=zSP|9}v}lmVC7Fby
zTOFyZz7uo!IbfZy!qtaVwInB^;+<6*`o{ikhV1;i94V{X`-;QU&kNsdle{JhZ-=lf
zV9+{(U$9>HM#e0wOcd$!WNb30oaFnvA~y0>eB83A#DziRg_GBI*j@OYz^oLiUX{*c
zhLp&%{Xrz3zD;}pxi{*k`WeqaI<FkmI4AUywH^9)@K4pix%ez_ASF}i_NQ#s8?1MO
zKjU;)aw9f=^kn94ko%-$$n8Pp8Qb<BinWT17(@*(9GsqXB`|MHj3|WXGik=1cH<au
z`-@hN?SZ93Xn-<%E0YGXyQ?$G&uwy&84&_i5Dl5dQu^5d!V}KLArwia2SNMM<PK}~
zo1teG80m5yV8ACaMhZ}Dpf-oSF-I#bb^1u&LS8k5+pYbTNu=L^{EjFUxO=F1sd>3U
zP_zGJ?IeJ@#}(Y#Y}?w5I~`5Ud0umv2P;bk-~pC!?v518#MLem(Uy^1ETBrp6Mj{p
zZr2(^u^O`?H6VOA!)8#cM&VU1{S3N_ER{2{(^c-$=1I1?dd#yD$T-^+e9_#8f{p;P
zW>eCyaRG%#yWaoX;yz{U66fkI?zXEImENSPvCgo6lGv+nE=d`K_7@$zbs1Nt8szb)
zBdh-HM%VRk9sTd=t|<_s`Yv|#mX(bLgH%f@74&jR#B4~I?B^Qu@{CWcT`GwE)h5b`
zx-iDI(Gly3ip;+dcb>)$Q)X;Ypa%j=_c#VE$|_@7m0_JcAk~y-`ps;qXw_tP=oP%x
z&rWO;6fNL+buPS@UhzQ$-d!r95$iR+B5rH2hMOpd0R(N>nyj@dUwEMv7poI25HQ;+
z_ZEs*rQ;v+C=@%s6nFLK@QR4UtD^lTXG}fan3!C=M_<@K&nbWL8pOW#S#50K!QP7)
zf@?_}`dUTf0kVDZ(Nx;hxLff8ehi$p7<-^K4QF|*EY}Q>mfRQT?8~iX^3sDa2@uh(
z5$}3@u;t&H?17O)J6hi_P0)r79aSR(bN|Y~BIP>&o`kx3gOR*)WlBD`<Q)gHU>U6{
zbB<8E<_aeWib|?R|N7n=gV1=07ZOwU9OV$aPy&9_(7`K7#oRE~{LeBsD8(B2ONU4(
z5Z8|q0|3Wf+S;5SK=mqrT^@a5Ng|b$;+flkQ2#sHMFAyAT_#a>{R_iar_EW)RTfJ1
z0A;;a%%q3|)R>Ckx2y^DpKTq}X4tP$=0ecSk@N9fPSQfZ#MDPwpViiVgmBco&yx>;
ze|c?3!qM*!4?Z9uj?g2Oa5jDg(JBT&>0*x$noPhYAr62HVz?pV?NK{bk}r3N;$(4s
z4ms&Eg80i$XfeaEEGWF!eQs-x!eOORv-c5@)YmZGRwCZ`{8em*t$ZZJ<!Gz)Jpkyr
zaXuA5#Zg{(590+;odinBdK_hr61+j6xf3(P#IcK1DGTGQ4qojPmueQ_$9(#Y4ove!
zocU?oQim3aJ-FU1DsN*b)q=P<%eTx=$mP(7A`Q|X7HgvoyuI(uGP}8{LX#h#n}e$b
zC`wg5iT6x4tXw7_8fJnB)N$68$+~LTVrrV&(3a`OEaK7X5r%ero(JmK8ETZz{<$qm
z@qm)-%k&$uLm8V%=Vi2i>FB_Hx}D{}2fRXiy4hpNN2TCba^$<iogH9dR-FLyyK7zc
z^=5^Oy(^Oa1QBhG5pN^kooW4_2dAo?aI8*i0NG4%rp3pS?`g@zH}NR$`UgF=ru_}M
z@rJpH&v^@+jme5#^yva$Y#u}p1<}@>zFReO!!Z9o#vD;|aZ3q(;DZ4jEgQL|i9i1V
NBYiWydR>>q{{ktQFPs1X

literal 0
HcmV?d00001

diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
index 70648d627..a3bc8b69b 100644
--- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp
@@ -459,6 +459,7 @@ Ui::BoostFeatures LookupBoostFeatures(not_null<ChannelData*> channel) {
 		.customWallpaperLevel = group
 			? levelLimits.groupCustomWallpaperLevelMin()
 			: levelLimits.channelCustomWallpaperLevelMin(),
+		.sponsoredLevel = levelLimits.channelRestrictSponsoredLevelMin(),
 	};
 }
 
diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp
index 691d84f16..bb99e43a8 100644
--- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp
+++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp
@@ -175,7 +175,7 @@ void AddFeaturesList(
 			st::boostFeatureIconPosition);
 	};
 	const auto proj = &Ui::Text::RichLangValue;
-	const auto max = std::max({
+	const auto lowMax = std::max({
 		features.linkLogoLevel,
 		features.transcribeLevel,
 		features.emojiPackLevel,
@@ -189,9 +189,13 @@ void AddFeaturesList(
 			? 0
 			: features.linkStylesByLevel.back().first),
 	});
+	const auto highMax = std::max(lowMax, features.sponsoredLevel);
 	auto nameColors = 0;
 	auto linkStyles = 0;
-	for (auto i = std::max(startFromLevel, 1); i <= max; ++i) {
+	for (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) {
+		if ((i > lowMax) && (i < highMax)) {
+			continue;
+		}
 		const auto unlocks = (i == startFromLevel);
 		container->add(
 			MakeFeaturesBadge(
@@ -202,6 +206,9 @@ void AddFeaturesList(
 						lt_count,
 						rpl::single(float64(i)))),
 			st::boostLevelBadgePadding);
+		if (i >= features.sponsoredLevel) {
+			add(tr::lng_channel_earn_off(proj), st::boostFeatureOffSponsored);
+		}
 		if (i >= features.customWallpaperLevel) {
 			add(
 				(group
diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h
index ecac121b9..6c129512d 100644
--- a/Telegram/SourceFiles/ui/boxes/boost_box.h
+++ b/Telegram/SourceFiles/ui/boxes/boost_box.h
@@ -40,6 +40,7 @@ struct BoostFeatures {
 	int wallpaperLevel = 0;
 	int wallpapersCount = 0;
 	int customWallpaperLevel = 0;
+	int sponsoredLevel = 0;
 };
 
 struct BoostBoxData {
diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style
index afdac7146..33fae62eb 100644
--- a/Telegram/SourceFiles/ui/effects/premium.style
+++ b/Telegram/SourceFiles/ui/effects/premium.style
@@ -364,3 +364,4 @@ boostFeatureLink: icon{{ "settings/premium/features/feature_links", windowBgActi
 boostFeatureName: icon{{ "settings/premium/features/feature_color_names", windowBgActive }};
 boostFeatureStories: icon{{ "settings/premium/features/feature_stories", windowBgActive }};
 boostFeatureTranscribe: icon{{ "settings/premium/features/feature_voice", windowBgActive }};
+boostFeatureOffSponsored: icon{{ "settings/premium/features/feature_off_sponsored", windowBgActive }};

From b648548001866158420cda364f61c19dd86752b4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 4 Jul 2024 08:57:41 +0400
Subject: [PATCH 009/163] Don't insert "data:image.." after image paste cancel.

---
 Telegram/SourceFiles/history/history_widget.cpp               | 2 +-
 .../history/view/controls/history_view_compose_controls.cpp   | 2 +-
 Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp        | 4 ++++
 Telegram/SourceFiles/ui/chat/attach/attach_prepare.h          | 1 +
 4 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index fe0610daf..0a3c5a064 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -5668,7 +5668,7 @@ bool HistoryWidget::confirmSendingFiles(
 			cursor.setPosition(position, QTextCursor::KeepAnchor);
 		}
 		_field->setTextCursor(cursor);
-		if (!insertTextOnCancel.isEmpty()) {
+		if (Ui::InsertTextOnImageCancel(insertTextOnCancel)) {
 			_field->textCursor().insertText(insertTextOnCancel);
 		}
 	}));
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 779f0386b..09aa2178e 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -3415,7 +3415,7 @@ Fn<void()> ComposeControls::restoreTextCallback(
 			cursor.setPosition(position, QTextCursor::KeepAnchor);
 		}
 		_field->setTextCursor(cursor);
-		if (!insertTextOnCancel.isEmpty()) {
+		if (Ui::InsertTextOnImageCancel(insertTextOnCancel)) {
 			_field->textCursor().insertText(insertTextOnCancel);
 		}
 	});
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
index e3f90912f..505fa455b 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
@@ -82,6 +82,10 @@ bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) {
 	Unexpected("AlbumType in CanBeInAlbumType.");
 }
 
+bool InsertTextOnImageCancel(const QString &text) {
+	return !text.isEmpty() && !text.startsWith(u"data:image"_q);
+}
+
 PreparedList PreparedList::Reordered(
 		PreparedList &&list,
 		std::vector<int> order) {
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
index 49fd71fa1..c1fb3a379 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
@@ -88,6 +88,7 @@ struct PreparedFile {
 };
 
 [[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album);
+[[nodiscard]] bool InsertTextOnImageCancel(const QString &text);
 
 struct PreparedList {
 	enum class Error {

From 8c97e915ec8580491485605ea19ab24ec7a7bcfa Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 4 Jul 2024 09:06:17 +0400
Subject: [PATCH 010/163] Create .mm source blanks for macOS modules.

---
 Telegram/create.bat | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/Telegram/create.bat b/Telegram/create.bat
index 85ec9439b..2307b0d2a 100644
--- a/Telegram/create.bat
+++ b/Telegram/create.bat
@@ -67,26 +67,30 @@ exit /b %errorlevel%
   set "CommandPath=%1"
   set "CommandPathUnix=!CommandPath:\=/!"
   set "CommandPathWin=!CommandPath:/=\!"
-
+  if "!CommandPathUnix:~-4!" == "_mac" (
+    set "CommandExt=mm"
+  ) else (
+    set "CommandExt=cpp"
+  )
   if "!CommandPathUnix!" == "" (
     echo Provide source path.
     exit /b 1
-  ) else if exist "SourceFiles\!CommandPathWin!.cpp" (
+  ) else if exist "SourceFiles\!CommandPathWin!.!CommandExt!" (
     echo This source already exists.
     exit /b 1
   )
-  echo Generating source !CommandPathUnix!.cpp..
-  mkdir "SourceFiles\!CommandPathWin!.cpp"
-  rmdir "SourceFiles\!CommandPathWin!.cpp"
+  echo Generating source !CommandPathUnix!.!CommandExt!..
+  mkdir "SourceFiles\!CommandPathWin!.!CommandExt!"
+  rmdir "SourceFiles\!CommandPathWin!.!CommandExt!"
 
-  call :write_comment !CommandPathWin!.cpp
+  call :write_comment !CommandPathWin!.!CommandExt!
   set "quote="""
   set "quote=!quote:~0,1!"
   set "source1=#include !quote!!CommandPathUnix!.h!quote!"
   (
     echo !source1!
     echo.
-  )>> "SourceFiles\!CommandPathWin!.cpp"
+  )>> "SourceFiles\!CommandPathWin!.!CommandExt!"
   exit /b
 )
 

From 219671a3bcd73cd5859fbc174eaa09abe082a3fd Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 4 Jul 2024 12:01:59 +0400
Subject: [PATCH 011/163] Fix archive in Main Menu context menu.

---
 Telegram/SourceFiles/window/window_peer_menu.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp
index bce7a9ab1..f8259eec7 100644
--- a/Telegram/SourceFiles/window/window_peer_menu.cpp
+++ b/Telegram/SourceFiles/window/window_peer_menu.cpp
@@ -1472,7 +1472,8 @@ void Filler::fillArchiveActions() {
 
 	const auto controller = _controller;
 	const auto hidden = controller->session().settings().archiveCollapsed();
-	{
+	const auto inmenu = controller->session().settings().archiveInMainMenu();
+	if (!inmenu) {
 		const auto text = hidden
 			? tr::lng_context_archive_expand(tr::now)
 			: tr::lng_context_archive_collapse(tr::now);
@@ -1481,7 +1482,6 @@ void Filler::fillArchiveActions() {
 			controller->session().saveSettingsDelayed();
 		}, hidden ? &st::menuIconExpand : &st::menuIconCollapse);
 	}
-	const auto inmenu = controller->session().settings().archiveInMainMenu();
 	{
 		const auto text = inmenu
 			? tr::lng_context_archive_to_list(tr::now)

From 1028219276fc1d87fde9e9fd8dad9cb2af2180ed Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 10:52:44 +0400
Subject: [PATCH 012/163] Allow chats list preview for narrow photos.

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

diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index 15dfedecd..a7b2690ea 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -121,8 +121,8 @@ struct AlbumCounts {
 		ImageRoundRadius radius,
 		bool spoiler) {
 	const auto original = image->original();
-	if (original.width() * 10 < original.height()
-		|| original.height() * 10 < original.width()) {
+	if (original.width() * 20 < original.height()
+		|| original.height() * 20 < original.width()) {
 		return QImage();
 	}
 	const auto factor = style::DevicePixelRatio();

From b6664625ea6b92fc12180225799ebe6fefc478bc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 13:25:39 +0400
Subject: [PATCH 013/163] Fix assigning text after formatted text.

Fixes #28115.
---
 Telegram/lib_ui | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 067733899..366fc9843 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 067733899d62caa5411f54904a431d3484fd02e3
+Subproject commit 366fc9843562da1628385e796a4d6f114f8057a7

From f20475f07edaf2394a3cba5ec7397284be897acf Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 13:30:39 +0400
Subject: [PATCH 014/163] Show forbidden icon on disabled webview button.

---
 Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index 5e9dde92e..569c61bb9 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -178,6 +178,8 @@ void Panel::Button::updateFg(QColor fg) {
 void Panel::Button::updateArgs(MainButtonArgs &&args) {
 	_textFull = std::move(args.text);
 	setDisabled(!args.isActive);
+	setPointerCursor(false);
+	setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor);
 	setVisible(args.isVisible);
 	toggleProgress(args.isProgressVisible);
 	update();

From df277b366b13d41e180168529a5385dfde1a02a4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 13:30:58 +0400
Subject: [PATCH 015/163] Fix build on Windows.

---
 Telegram/SourceFiles/_other/updater_win.cpp | 4 ++--
 Telegram/ThirdParty/libtgvoip               | 2 +-
 Telegram/lib_webrtc                         | 2 +-
 Telegram/lib_webview                        | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/_other/updater_win.cpp b/Telegram/SourceFiles/_other/updater_win.cpp
index 0b9b24785..e779c8f20 100644
--- a/Telegram/SourceFiles/_other/updater_win.cpp
+++ b/Telegram/SourceFiles/_other/updater_win.cpp
@@ -571,8 +571,8 @@ void _generateDump(EXCEPTION_POINTERS* pExceptionPointers) {
 	}
 	if (!hDumpFile || hDumpFile == INVALID_HANDLE_VALUE) {
 		WCHAR wstrPath[maxFileLen];
-		DWORD wstrPathLen;
-		if (wstrPathLen = GetEnvironmentVariable(L"APPDATA", wstrPath, maxFileLen)) {
+		DWORD wstrPathLen = GetEnvironmentVariable(L"APPDATA", wstrPath, maxFileLen);
+		if (wstrPathLen) {
 			wsprintf(wstrPath + wstrPathLen, L"\\%s\\", _programName);
 			hDumpFile = _generateDumpFileAtPath(wstrPath);
 		}
diff --git a/Telegram/ThirdParty/libtgvoip b/Telegram/ThirdParty/libtgvoip
index 25facad34..2d2592860 160000
--- a/Telegram/ThirdParty/libtgvoip
+++ b/Telegram/ThirdParty/libtgvoip
@@ -1 +1 @@
-Subproject commit 25facad342c3280315f9ef553906f46c3eeba1e4
+Subproject commit 2d2592860478e60d972b96e67ee034b8a71bb57a
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index f701713cd..eb9496540 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit f701713cd798bd7d5f69d318fdefb125d101aa76
+Subproject commit eb9496540356945e2c9fb700bcfa51444fd36f41
diff --git a/Telegram/lib_webview b/Telegram/lib_webview
index 659b91812..363db4e49 160000
--- a/Telegram/lib_webview
+++ b/Telegram/lib_webview
@@ -1 +1 @@
-Subproject commit 659b9181240aae16c05ef8ab7e6c4dd527afcf8a
+Subproject commit 363db4e49a0b78e5dd08bd922e09cf8810318c09

From 149c69c9f5deb5577fb44394796ed15774024b73 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 22:13:13 +0400
Subject: [PATCH 016/163] Use a separate string for Your Stars in Settings.

---
 Telegram/Resources/langs/lang.strings           | 1 +
 Telegram/SourceFiles/settings/settings_main.cpp | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index db9afbe0e..ea754fcf9 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1115,6 +1115,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_settings_faq" = "Telegram FAQ";
 "lng_settings_faq_link" = "https://telegram.org/faq#general-questions";
 "lng_settings_features" = "Telegram Features";
+"lng_settings_credits" = "Your Stars";
 "lng_settings_logout" = "Log Out";
 "lng_sure_logout" = "Are you sure you want to log out?";
 
diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp
index d08ce9a11..363ad342f 100644
--- a/Telegram/SourceFiles/settings/settings_main.cpp
+++ b/Telegram/SourceFiles/settings/settings_main.cpp
@@ -503,7 +503,7 @@ void SetupPremium(
 		AddPremiumStar(
 			AddButtonWithLabel(
 				wrap->entity(),
-				tr::lng_credits_summary_title(),
+				tr::lng_settings_credits(),
 				controller->session().creditsValue(
 				) | rpl::map([=](uint64 c) {
 					return c ? Lang::FormatCountToShort(c).string : QString{};

From a01d48f0631a6cce547ce3137a15102edd8ddafb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 22:21:04 +0400
Subject: [PATCH 017/163] Update submodules.

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

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 366fc9843..96be2a6b7 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 366fc9843562da1628385e796a4d6f114f8057a7
+Subproject commit 96be2a6b72f0405a9c01407148303fba2dab101c

From 78093173a92e9b0b60086ed5d91bdc051be7fb67 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 6 Jul 2024 22:25:44 +0400
Subject: [PATCH 018/163] Version 5.2.3.

- Fix crash in bot star stats page.
- Bug fixes and other minor 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 ++++----
 Telegram/lib_storage                         | 2 +-
 changelog.txt                                | 5 +++++
 7 files changed, 21 insertions(+), 16 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 53203464b..46918013d 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="5.2.2.0" />
+    Version="5.2.3.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 2d9446598..c47677eb2 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,2,0
- PRODUCTVERSION 5,2,2,0
+ FILEVERSION 5,2,3,0
+ PRODUCTVERSION 5,2,3,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "5.2.2.0"
+            VALUE "FileVersion", "5.2.3.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.2.0"
+            VALUE "ProductVersion", "5.2.3.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index abd5f4d0e..5594959fe 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,2,0
- PRODUCTVERSION 5,2,2,0
+ FILEVERSION 5,2,3,0
+ PRODUCTVERSION 5,2,3,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", "5.2.2.0"
+            VALUE "FileVersion", "5.2.3.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.2.0"
+            VALUE "ProductVersion", "5.2.3.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 57252c888..e1ac92bf4 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 = 5002002;
-constexpr auto AppVersionStr = "5.2.2";
+constexpr auto AppVersion = 5002003;
+constexpr auto AppVersionStr = "5.2.3";
 constexpr auto AppBetaVersion = false;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index 553191613..4d2b86059 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         5002002
+AppVersion         5002003
 AppVersionStrMajor 5.2
-AppVersionStrSmall 5.2.2
-AppVersionStr      5.2.2
+AppVersionStrSmall 5.2.3
+AppVersionStr      5.2.3
 BetaChannel        0
 AlphaVersion       0
-AppVersionOriginal 5.2.2
+AppVersionOriginal 5.2.3
diff --git a/Telegram/lib_storage b/Telegram/lib_storage
index 0971b69ca..ccdc72548 160000
--- a/Telegram/lib_storage
+++ b/Telegram/lib_storage
@@ -1 +1 @@
-Subproject commit 0971b69ca90f1697ef81276d9820dcd6d26de4ac
+Subproject commit ccdc72548a5065b5991b4e06e610d76bc4f6023e
diff --git a/changelog.txt b/changelog.txt
index 6e398bcc6..c0298f855 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,8 @@
+5.2.3 (07.07.24)
+
+- Fix crash in bot star stats page.
+- Bug fixes and other minor improvements.
+
 5.2.2 (02.07.24)
 
 - Fix topics search in topic groups.

From 6effac7915e271e7762bd7997a1c858e67bb3e9e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 9 Jul 2024 15:47:14 +0200
Subject: [PATCH 019/163] Migrate games to AttachWebView.

---
 Telegram/SourceFiles/api/api_bot.cpp          |   6 +-
 Telegram/SourceFiles/boxes/share_box.cpp      | 200 ++----------------
 Telegram/SourceFiles/boxes/share_box.h        |  12 +-
 .../SourceFiles/core/click_handler_types.cpp  |  37 +++-
 .../SourceFiles/core/local_url_handlers.cpp   |  19 --
 .../view/history_view_context_menu.cpp        |  11 +-
 .../history/view/history_view_context_menu.h  |   4 +
 .../inline_bots/bot_attach_web_view.cpp       |  81 +++++++
 .../inline_bots/bot_attach_web_view.h         |  15 +-
 .../ui/chat/attach/attach_bot_webview.cpp     |  51 ++++-
 .../ui/chat/attach/attach_bot_webview.h       |  10 +
 11 files changed, 223 insertions(+), 223 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index cf4611563..929051261 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -127,11 +127,7 @@ void SendBotCallbackData(
 				UrlClickHandler::Open(link);
 				return;
 			}
-			const auto scoreLink = AppendShareGameScoreUrl(
-				session,
-				link,
-				item->fullId());
-			BotGameUrlClickHandler(bot, scoreLink).onClick({
+			BotGameUrlClickHandler(bot, link).onClick({
 				Qt::LeftButton,
 				QVariant::fromValue(ClickHandlerContext{
 					.itemId = item->fullId(),
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index 043a8ad18..e7e3054aa 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -1409,55 +1409,6 @@ std::vector<not_null<Data::Thread*>> ShareBox::Inner::selected() const {
 	return result;
 }
 
-QString AppendShareGameScoreUrl(
-		not_null<Main::Session*> session,
-		const QString &url,
-		const FullMsgId &fullId) {
-	auto shareHashData = QByteArray(0x20, Qt::Uninitialized);
-	auto shareHashDataInts = reinterpret_cast<uint64*>(shareHashData.data());
-	const auto peer = fullId.peer
-		? session->data().peerLoaded(fullId.peer)
-		: static_cast<PeerData*>(nullptr);
-	const auto channelAccessHash = uint64((peer && peer->isChannel())
-		? peer->asChannel()->access
-		: 0);
-	shareHashDataInts[0] = session->userId().bare;
-	shareHashDataInts[1] = fullId.peer.value;
-	shareHashDataInts[2] = uint64(fullId.msg.bare);
-	shareHashDataInts[3] = channelAccessHash;
-
-	// Count SHA1() of data.
-	auto key128Size = 0x10;
-	auto shareHashEncrypted = QByteArray(key128Size + shareHashData.size(), Qt::Uninitialized);
-	hashSha1(shareHashData.constData(), shareHashData.size(), shareHashEncrypted.data());
-
-	//// Mix in channel access hash to the first 64 bits of SHA1 of data.
-	//*reinterpret_cast<uint64*>(shareHashEncrypted.data()) ^= channelAccessHash;
-
-	// Encrypt data.
-	if (!session->local().encrypt(shareHashData.constData(), shareHashEncrypted.data() + key128Size, shareHashData.size(), shareHashEncrypted.constData())) {
-		return url;
-	}
-
-	auto shareHash = shareHashEncrypted.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
-	auto shareUrl = u"tg://share_game_score?hash="_q + QString::fromLatin1(shareHash);
-
-	auto shareComponent = u"tgShareScoreUrl="_q + qthelp::url_encode(shareUrl);
-
-	auto hashPosition = url.indexOf('#');
-	if (hashPosition < 0) {
-		return url + '#' + shareComponent;
-	}
-	auto hash = url.mid(hashPosition + 1);
-	if (hash.indexOf('=') >= 0 || hash.indexOf('?') >= 0) {
-		return url + '&' + shareComponent;
-	}
-	if (!hash.isEmpty()) {
-		return url + '?' + shareComponent;
-	}
-	return url + shareComponent;
-}
-
 ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs(
 		const std::vector<not_null<Data::Thread*>> &result,
 		const MessageIdsList &msgIds) {
@@ -1612,9 +1563,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
 }
 
 void FastShareMessage(
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<Main::SessionShow> show,
 		not_null<HistoryItem*> item) {
-	const auto show = controller->uiShow();
 	const auto history = item->history();
 	const auto owner = &history->owner();
 	const auto session = &history->session();
@@ -1643,7 +1593,7 @@ void FastShareMessage(
 		}
 		if (item->hasDirectLink()) {
 			using namespace HistoryView;
-			CopyPostLink(controller, item->fullId(), Context::History);
+			CopyPostLink(show, item->fullId(), Context::History);
 		} else if (const auto bot = item->getMessageBot()) {
 			if (const auto media = item->media()) {
 				if (const auto game = media->game()) {
@@ -1675,23 +1625,27 @@ void FastShareMessage(
 	auto copyLinkCallback = canCopyLink
 		? Fn<void()>(std::move(copyCallback))
 		: Fn<void()>();
-	controller->show(
-		Box<ShareBox>(ShareBox::Descriptor{
-			.session = session,
-			.copyCallback = std::move(copyLinkCallback),
-			.submitCallback = ShareBox::DefaultForwardCallback(
-				show,
-				history,
-				msgIds),
-			.filterCallback = std::move(filterCallback),
-			.forwardOptions = {
-				.sendersCount = ItemsForwardSendersCount(items),
-				.captionsCount = ItemsForwardCaptionsCount(items),
-				.show = !hasOnlyForcedForwardedInfo,
-			},
-			.premiumRequiredError = SharePremiumRequiredError(),
-		}),
-		Ui::LayerOption::CloseOther);
+	show->show(Box<ShareBox>(ShareBox::Descriptor{
+		.session = session,
+		.copyCallback = std::move(copyLinkCallback),
+		.submitCallback = ShareBox::DefaultForwardCallback(
+			show,
+			history,
+			msgIds),
+		.filterCallback = std::move(filterCallback),
+		.forwardOptions = {
+			.sendersCount = ItemsForwardSendersCount(items),
+			.captionsCount = ItemsForwardCaptionsCount(items),
+			.show = !hasOnlyForcedForwardedInfo,
+		},
+		.premiumRequiredError = SharePremiumRequiredError(),
+	}), Ui::LayerOption::CloseOther);
+}
+
+void FastShareMessage(
+		not_null<Window::SessionController*> controller,
+		not_null<HistoryItem*> item) {
+	FastShareMessage(controller->uiShow(), item);
 }
 
 void FastShareLink(
@@ -1793,111 +1747,3 @@ auto SharePremiumRequiredError()
 -> Fn<RecipientPremiumRequiredError(not_null<UserData*>)> {
 	return WritePremiumRequiredError;
 }
-
-void ShareGameScoreByHash(
-		not_null<Window::SessionController*> controller,
-		const QString &hash) {
-	auto &session = controller->session();
-	auto key128Size = 0x10;
-
-	auto hashEncrypted = QByteArray::fromBase64(hash.toLatin1(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
-	if (hashEncrypted.size() <= key128Size || (hashEncrypted.size() != key128Size + 0x20)) {
-		controller->show(
-			Ui::MakeInformBox(tr::lng_confirm_phone_link_invalid()),
-			Ui::LayerOption::CloseOther);
-		return;
-	}
-
-	// Decrypt data.
-	auto hashData = QByteArray(hashEncrypted.size() - key128Size, Qt::Uninitialized);
-	if (!session.local().decrypt(hashEncrypted.constData() + key128Size, hashData.data(), hashEncrypted.size() - key128Size, hashEncrypted.constData())) {
-		return;
-	}
-
-	// Count SHA1() of data.
-	char dataSha1[20] = { 0 };
-	hashSha1(hashData.constData(), hashData.size(), dataSha1);
-
-	//// Mix out channel access hash from the first 64 bits of SHA1 of data.
-	//auto channelAccessHash = *reinterpret_cast<uint64*>(hashEncrypted.data()) ^ *reinterpret_cast<uint64*>(dataSha1);
-
-	//// Check next 64 bits of SHA1() of data.
-	//auto skipSha1Part = sizeof(channelAccessHash);
-	//if (memcmp(dataSha1 + skipSha1Part, hashEncrypted.constData() + skipSha1Part, key128Size - skipSha1Part) != 0) {
-	//	Ui::show(Box<Ui::InformBox>(tr::lng_share_wrong_user(tr::now)));
-	//	return;
-	//}
-
-	// Check 128 bits of SHA1() of data.
-	if (memcmp(dataSha1, hashEncrypted.constData(), key128Size) != 0) {
-		controller->show(
-			Ui::MakeInformBox(tr::lng_share_wrong_user()),
-			Ui::LayerOption::CloseOther);
-		return;
-	}
-
-	auto hashDataInts = reinterpret_cast<uint64*>(hashData.data());
-	if (hashDataInts[0] != session.userId().bare) {
-		controller->show(
-			Ui::MakeInformBox(tr::lng_share_wrong_user()),
-			Ui::LayerOption::CloseOther);
-		return;
-	}
-
-	const auto peerId = PeerId(hashDataInts[1]);
-	const auto channelAccessHash = hashDataInts[3];
-	if (!peerIsChannel(peerId) && channelAccessHash) {
-		// If there is no channel id, there should be no channel access_hash.
-		controller->show(
-			Ui::MakeInformBox(tr::lng_share_wrong_user()),
-			Ui::LayerOption::CloseOther);
-		return;
-	}
-
-	const auto msgId = MsgId(int64(hashDataInts[2]));
-	if (const auto item = session.data().message(peerId, msgId)) {
-		FastShareMessage(controller, item);
-	} else {
-		const auto weak = base::make_weak(controller);
-		const auto resolveMessageAndShareScore = crl::guard(weak, [=](
-				PeerData *peer) {
-			auto done = crl::guard(weak, [=] {
-				const auto item = weak->session().data().message(
-					peerId,
-					msgId);
-				if (item) {
-					FastShareMessage(weak.get(), item);
-				} else {
-					weak->show(
-						Ui::MakeInformBox(tr::lng_edit_deleted()),
-						Ui::LayerOption::CloseOther);
-				}
-			});
-			auto &api = weak->session().api();
-			api.requestMessageData(peer, msgId, std::move(done));
-		});
-
-		const auto peer = peerIsChannel(peerId)
-			? controller->session().data().peerLoaded(peerId)
-			: nullptr;
-		if (peer || !peerIsChannel(peerId)) {
-			resolveMessageAndShareScore(peer);
-		} else {
-			const auto owner = &controller->session().data();
-			controller->session().api().request(MTPchannels_GetChannels(
-				MTP_vector<MTPInputChannel>(
-					1,
-					MTP_inputChannel(
-						MTP_long(peerToChannel(peerId).bare),
-						MTP_long(channelAccessHash)))
-			)).done([=](const MTPmessages_Chats &result) {
-				result.match([&](const auto &data) {
-					owner->processChats(data.vchats());
-				});
-				if (const auto peer = owner->peerLoaded(peerId)) {
-					resolveMessageAndShareScore(peer);
-				}
-			}).send();
-		}
-	}
-}
diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h
index 32e824b15..d0bf28ce9 100644
--- a/Telegram/SourceFiles/boxes/share_box.h
+++ b/Telegram/SourceFiles/boxes/share_box.h
@@ -59,13 +59,11 @@ class SlideWrap;
 class PopupMenu;
 } // namespace Ui
 
-QString AppendShareGameScoreUrl(
-	not_null<Main::Session*> session,
-	const QString &url,
-	const FullMsgId &fullId);
-void ShareGameScoreByHash(
-	not_null<Window::SessionController*> controller,
-	const QString &hash);
+class ShareBox;
+
+void FastShareMessage(
+	std::shared_ptr<Main::SessionShow> show,
+	not_null<HistoryItem*> item);
 void FastShareMessage(
 	not_null<Window::SessionController*> controller,
 	not_null<HistoryItem*> item);
diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp
index 9b03e0b74..eadb16181 100644
--- a/Telegram/SourceFiles/core/click_handler_types.cpp
+++ b/Telegram/SourceFiles/core/click_handler_types.cpp
@@ -20,6 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/history.h"
 #include "history/view/history_view_element.h"
 #include "history/history_item.h"
+#include "inline_bots/bot_attach_web_view.h"
+#include "data/data_game.h"
 #include "data/data_user.h"
 #include "data/data_session.h"
 #include "window/window_controller.h"
@@ -171,23 +173,40 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const {
 	if (Core::InternalPassportLink(url)) {
 		return;
 	}
-
-	const auto open = [=] {
+	const auto openLink = [=] {
 		UrlClickHandler::Open(url, context.other);
 	};
-	if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive)) {
-		open();
-	} else if (!_bot
-		|| _bot->isVerified()
+	const auto my = context.other.value<ClickHandlerContext>();
+	const auto weakController = my.sessionWindow;
+	const auto controller = weakController.get();
+	const auto item = controller
+		? controller->session().data().message(my.itemId)
+		: nullptr;
+	const auto media = item ? item->media() : nullptr;
+	const auto game = media ? media->game() : nullptr;
+	if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive) || !_bot || !game) {
+		openLink();
+	}
+	const auto bot = _bot;
+	const auto title = game->title;
+	const auto itemId = my.itemId;
+	const auto openGame = [=] {
+		bot->session().attachWebView().showGame({
+			.bot = bot,
+			.context = itemId,
+			.url = url,
+			.title = title,
+		});
+	};
+	if (_bot->isVerified()
 		|| _bot->session().local().isBotTrustedOpenGame(_bot->id)) {
-		open();
+		openGame();
 	} else {
-		const auto my = context.other.value<ClickHandlerContext>();
 		if (const auto controller = my.sessionWindow.get()) {
 			const auto callback = [=, bot = _bot](Fn<void()> close) {
 				close();
 				bot->session().local().markBotTrustedOpenGame(bot->id);
-				open();
+				openGame();
 			};
 			controller->show(Ui::MakeConfirmBox({
 				.text = tr::lng_allow_bot_pass(
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 03c3254ab..9f4860da3 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -327,21 +327,6 @@ bool ConfirmPhone(
 	return true;
 }
 
-bool ShareGameScore(
-		Window::SessionController *controller,
-		const Match &match,
-		const QVariant &context) {
-	if (!controller) {
-		return false;
-	}
-	const auto params = url_parse_params(
-		match->captured(1),
-		qthelp::UrlParamNameTransform::ToLower);
-	ShareGameScoreByHash(controller, params.value(u"hash"_q));
-	controller->window().activate();
-	return true;
-}
-
 bool ApplySocksProxy(
 		Window::SessionController *controller,
 		const Match &match,
@@ -1230,10 +1215,6 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
 			u"^confirmphone/?\\?(.+)(#|$)"_q,
 			ConfirmPhone
 		},
-		{
-			u"^share_game_score/?\\?(.+)(#|$)"_q,
-			ShareGameScore
-		},
 		{
 			u"^socks/?\\?(.+)(#|$)"_q,
 			ApplySocksProxy
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
index a53ab2028..49013f1c8 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp
@@ -1284,7 +1284,14 @@ void CopyPostLink(
 		not_null<Window::SessionController*> controller,
 		FullMsgId itemId,
 		Context context) {
-	const auto item = controller->session().data().message(itemId);
+	CopyPostLink(controller->uiShow(), itemId, context);
+}
+
+void CopyPostLink(
+		std::shared_ptr<Main::SessionShow> show,
+		FullMsgId itemId,
+		Context context) {
+	const auto item = show->session().data().message(itemId);
 	if (!item || !item->hasDirectLink()) {
 		return;
 	}
@@ -1311,7 +1318,7 @@ void CopyPostLink(
 		return channel->hasUsername();
 	}();
 
-	controller->showToast(isPublicLink
+	show->showToast(isPublicLink
 		? tr::lng_channel_public_link_copied(tr::now)
 		: tr::lng_context_about_private_link(tr::now));
 }
diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h
index 8f00f4da8..efb2a5fdd 100644
--- a/Telegram/SourceFiles/history/view/history_view_context_menu.h
+++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h
@@ -61,6 +61,10 @@ void CopyPostLink(
 	not_null<Window::SessionController*> controller,
 	FullMsgId itemId,
 	Context context);
+void CopyPostLink(
+	std::shared_ptr<Main::SessionShow> show,
+	FullMsgId itemId,
+	Context context);
 void CopyStoryLink(
 	std::shared_ptr<Main::SessionShow> show,
 	FullStoryId storyId);
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index eb938ed26..ff2637011 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "api/api_blocked_peers.h"
 #include "api/api_common.h"
 #include "base/qthelp_url.h"
+#include "boxes/share_box.h"
 #include "core/click_handler_types.h"
 #include "data/data_bot_app.h"
 #include "data/data_changes.h"
@@ -802,6 +803,16 @@ void AttachWebView::botInvokeCustomMethod(
 	}).send();
 }
 
+void AttachWebView::botShareGameScore() {
+	if (!_panel || !_gameContext) {
+		return;
+	} else if (const auto item = _session->data().message(_gameContext)) {
+		FastShareMessage(uiShow(), item);
+	} else {
+		_panel->showToast({ tr::lng_message_not_found(tr::now) });
+	}
+}
+
 void AttachWebView::botClose() {
 	crl::on_main(this, [=] { cancel(); });
 }
@@ -1547,6 +1558,7 @@ void AttachWebView::show(
 	_lastShownUrl = url;
 	_lastShownQueryId = queryId;
 	_lastShownButtonText = buttonText;
+	_gameContext = {};
 	base::take(_panel);
 	_catchingCancelInShowCall = true;
 	_panel = Ui::BotWebView::Show({
@@ -1562,6 +1574,24 @@ void AttachWebView::show(
 	started(queryId);
 }
 
+void AttachWebView::showGame(ShowGameParams &&params) {
+	ActiveWebViews().emplace(this);
+
+	base::take(_panel);
+	_gameContext = params.context;
+
+	_catchingCancelInShowCall = true;
+	_panel = Ui::BotWebView::Show({
+		.url = params.url,
+		.storageId = _session->local().resolveStorageIdBots(),
+		.title = rpl::single(params.title),
+		.bottom = rpl::single('@' + params.bot->username()),
+		.delegate = static_cast<Ui::BotWebView::Delegate*>(this),
+		.menuButtons = Ui::BotWebView::MenuButton::ShareGame,
+	});
+	_catchingCancelInShowCall = false;
+}
+
 void AttachWebView::started(uint64 queryId) {
 	Expects(_bot != nullptr);
 	Expects(_context != nullptr);
@@ -1601,6 +1631,57 @@ void AttachWebView::started(uint64 queryId) {
 	}, _panel->lifetime());
 }
 
+std::shared_ptr<Main::SessionShow> AttachWebView::uiShow() {
+	class Show final : public Main::SessionShow {
+	public:
+		explicit Show(not_null<AttachWebView*> that) : _that(that) {
+		}
+
+		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 {
+			using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
+			using ObjectBox = object_ptr<Ui::BoxContent>;
+			const auto panel = _that ? _that->_panel.get() : nullptr;
+			if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
+				Unexpected("Layers in AttachWebView are not implemented.");
+			} else if (auto box = std::get_if<ObjectBox>(&layer)) {
+				if (panel) {
+					panel->showBox(std::move(*box), options, animated);
+				}
+			} else if (panel) {
+				panel->hideLayer(animated);
+			}
+		}
+		[[nodiscard]] not_null<QWidget*> toastParent() const override {
+			const auto panel = _that ? _that->_panel.get() : nullptr;
+
+			Ensures(panel != nullptr);
+			return panel->toastParent();
+		}
+		[[nodiscard]] bool valid() const override {
+			return _that && (_that->_panel != nullptr);
+		}
+		operator bool() const override {
+			return valid();
+		}
+
+		[[nodiscard]] Main::Session &session() const override {
+			Expects(_that.get() != nullptr);
+			return *_that->_session;
+		}
+
+	private:
+		const base::weak_ptr<AttachWebView> _that;
+
+	};
+	return std::make_shared<Show>(this);
+}
+
 void AttachWebView::showToast(
 		const QString &text,
 		Window::SessionController *controller) {
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
index 0f8cb6e13..87648b07e 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
@@ -29,6 +29,7 @@ class Panel;
 
 namespace Main {
 class Session;
+class SessionShow;
 } // namespace Main
 
 namespace Window {
@@ -155,12 +156,21 @@ public:
 	[[nodiscard]] std::optional<Api::SendAction> lookupLastAction(
 		const QString &url) const;
 
+	struct ShowGameParams {
+		not_null<UserData*> bot;
+		FullMsgId context;
+		QString url;
+		QString title;
+	};
+	void showGame(ShowGameParams &&params);
+
+	[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();
+
 	static void ClearAll();
 
 private:
 	struct Context;
 
-
 	Webview::ThemeParams botThemeParams() override;
 	bool botHandleLocalUri(QString uri, bool keepOpen) override;
 	void botHandleInvoice(QString slug) override;
@@ -176,6 +186,7 @@ private:
 	void botSharePhone(Fn<void(bool shared)> callback) override;
 	void botInvokeCustomMethod(
 		Ui::BotWebView::CustomMethodRequest request) override;
+	void botShareGameScore() override;
 	void botClose() override;
 
 	[[nodiscard]] static Context LookupContext(
@@ -271,6 +282,8 @@ private:
 	rpl::event_stream<> _attachBotsUpdates;
 	base::flat_set<not_null<UserData*>> _disclaimerAccepted;
 
+	FullMsgId _gameContext;
+
 	std::unique_ptr<Ui::BotWebView::Panel> _panel;
 	bool _catchingCancelInShowCall = false;
 
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index 569c61bb9..d367e3382 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "webview/webview_interface.h"
 #include "base/debug_log.h"
 #include "base/invoke_queued.h"
+#include "base/qt_signal_producer.h"
 #include "styles/style_payments.h"
 #include "styles/style_layers.h"
 #include "styles/style_menu_icons.h"
@@ -34,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include <QtCore/QJsonArray>
 #include <QtGui/QGuiApplication>
 #include <QtGui/QClipboard>
+#include <QtGui/QWindow>
 
 namespace Ui::BotWebView {
 namespace {
@@ -373,6 +375,13 @@ Panel::~Panel() {
 
 void Panel::requestActivate() {
 	_widget->showAndActivate();
+	if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
+		InvokeQueued(widget, [=] {
+			if (widget->isVisible()) {
+				_webview->window.focus();
+			}
+		});
+	}
 }
 
 void Panel::toggleProgress(bool shown) {
@@ -527,9 +536,15 @@ bool Panel::showWebview(
 				_webview->window.navigate(url);
 			}
 		}, &st::menuIconRestore);
-		callback(tr::lng_bot_terms(tr::now), [=] {
-			File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now));
-		}, &st::menuIconGroupLog);
+		if (_menuButtons & MenuButton::ShareGame) {
+			callback(tr::lng_iv_share(tr::now), [=] {
+				_delegate->botShareGameScore();
+			}, &st::menuIconShare);
+		} else {
+			callback(tr::lng_bot_terms(tr::now), [=] {
+				File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now));
+			}, &st::menuIconGroupLog);
+		}
 		const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu);
 		if (main || (_menuButtons & MenuButton::RemoveFromMenu)) {
 			const auto handler = [=] {
@@ -691,6 +706,8 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
 			requestClipboardText(arguments);
 		} else if (command == "web_app_set_header_color") {
 			processHeaderColor(arguments);
+		} else if (command == "share_score") {
+			_delegate->botShareGameScore();
 		}
 	});
 
@@ -722,6 +739,17 @@ postEvent: function(eventType, eventData) {
 
 	setupProgressGeometry();
 
+	base::qt_signal_producer(
+		_widget->window()->windowHandle(),
+		&QWindow::activeChanged
+	) | rpl::filter([=] {
+		return _webview && _widget->window()->windowHandle()->isActive();
+	}) | rpl::start_with_next([=] {
+		if (_webview && !_webview->window.widget()->isHidden()) {
+			_webview->window.focus();
+		}
+	}, _webview->lifetime);
+
 	return true;
 }
 
@@ -1207,6 +1235,13 @@ void Panel::updateFooterHeight() {
 }
 
 void Panel::showBox(object_ptr<BoxContent> box) {
+	showBox(std::move(box), LayerOption::KeepOther, anim::type::normal);
+}
+
+void Panel::showBox(
+		object_ptr<BoxContent> box,
+		LayerOptions options,
+		anim::type animated) {
 	if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
 		const auto hideNow = !widget->isHidden();
 		if (hideNow || _webview->lastHidingBox) {
@@ -1220,10 +1255,12 @@ void Panel::showBox(object_ptr<BoxContent> box) {
 					&& widget->isHidden()
 					&& _webview->lastHidingBox == raw) {
 					widget->show();
+					_webviewBottom->show();
 				}
 			}, _webview->lifetime);
 			if (hideNow) {
 				widget->hide();
+				_webviewBottom->hide();
 			}
 		}
 	}
@@ -1237,6 +1274,14 @@ void Panel::showToast(TextWithEntities &&text) {
 	_widget->showToast(std::move(text));
 }
 
+not_null<QWidget*> Panel::toastParent() const {
+	return _widget->uiShow()->toastParent();
+}
+
+void Panel::hideLayer(anim::type animated) {
+	_widget->hideLayer(animated);
+}
+
 void Panel::showCriticalError(const TextWithEntities &text) {
 	_progress = nullptr;
 	_webviewProgress = false;
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
index 91e423279..d9071c846 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
@@ -20,6 +20,8 @@ namespace Ui {
 class BoxContent;
 class RpWidget;
 class SeparatePanel;
+enum class LayerOption;
+using LayerOptions = base::flags<LayerOption>;
 } // namespace Ui
 
 namespace Webview {
@@ -40,6 +42,7 @@ enum class MenuButton {
 	OpenBot            = 0x01,
 	RemoveFromMenu     = 0x02,
 	RemoveFromMainMenu = 0x04,
+	ShareGame          = 0x08,
 };
 inline constexpr bool is_flag_type(MenuButton) { return true; }
 using MenuButtons = base::flags<MenuButton>;
@@ -67,6 +70,7 @@ public:
 	virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
 	virtual void botSharePhone(Fn<void(bool shared)> callback) = 0;
 	virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;
+	virtual void botShareGameScore() = 0;
 	virtual void botClose() = 0;
 };
 
@@ -89,7 +93,13 @@ public:
 		rpl::producer<QString> bottomText);
 
 	void showBox(object_ptr<BoxContent> box);
+	void showBox(
+		object_ptr<BoxContent> box,
+		LayerOptions options,
+		anim::type animated);
+	void hideLayer(anim::type animated);
 	void showToast(TextWithEntities &&text);
+	not_null<QWidget*> toastParent() const;
 	void showCriticalError(const TextWithEntities &text);
 	void showWebviewError(
 		const QString &text,

From 94ad8f9bc38d2225601c9eb9529c75f789acc7e9 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 9 Jul 2024 16:59:56 +0200
Subject: [PATCH 020/163] Don't register collapsed row click as userpic.

Fixes #28149.
---
 Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index cb33e8a9a..6e60f072d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -3814,7 +3814,9 @@ ChosenRow InnerWidget::computeChosenRow() const {
 
 bool InnerWidget::isUserpicPress() const {
 	return  (_lastRowLocalMouseX >= 0)
-		&& (_lastRowLocalMouseX < _st->nameLeft);
+		&& (_lastRowLocalMouseX < _st->nameLeft)
+		&& (_collapsedSelected < 0
+			|| _collapsedSelected >= _collapsedRows.size());
 }
 
 bool InnerWidget::isUserpicPressOnWide() const {

From ebba58217c08decc97b6bc85cc7847bb16b122d6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 9 Jul 2024 17:00:24 +0200
Subject: [PATCH 021/163] Don't set focus to shown third section.

I hope it fixes #28142.
---
 Telegram/SourceFiles/window/section_widget.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp
index ae24ad4d3..4ec94b0ab 100644
--- a/Telegram/SourceFiles/window/section_widget.cpp
+++ b/Telegram/SourceFiles/window/section_widget.cpp
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "window/section_widget.h"
 
 #include "mainwidget.h"
+#include "mainwindow.h"
 #include "ui/ui_utility.h"
 #include "ui/chat/chat_theme.h"
 #include "ui/painter.h"
@@ -456,7 +457,7 @@ void SectionWidget::showFinished() {
 	showChildren();
 	showFinishedHook();
 
-	setInnerFocus();
+	controller()->widget()->setInnerFocus();
 }
 
 rpl::producer<int> SectionWidget::desiredHeight() const {

From 46b69a938b6d0ed3ef2660d632c8c4c6a9934271 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 10 Jul 2024 11:41:18 +0200
Subject: [PATCH 022/163] Revert "Register tg:// scheme on initial launch."

This reverts commit 8cbeadc68a88acb207305ad02e6c01df3a80b690.
---
 Telegram/SourceFiles/core/application.cpp | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index e3e0de6c6..79dfd0977 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -270,14 +270,9 @@ void Application::run() {
 
 	refreshGlobalProxy(); // Depends on app settings being read.
 
-	if (const auto old = Local::oldSettingsVersion()) {
-		if (old < AppVersion) {
-			autoRegisterUrlScheme();
-			Platform::NewVersionLaunched(old);
-		}
-	} else {
-		// Initial launch.
+	if (const auto old = Local::oldSettingsVersion(); old < AppVersion) {
 		autoRegisterUrlScheme();
+		Platform::NewVersionLaunched(old);
 	}
 
 	if (cAutoStart() && !Platform::AutostartSupported()) {

From f7d698b9ff05aaa829d119e4c38d485e01a53259 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 11 Jul 2024 14:23:34 +0200
Subject: [PATCH 023/163] Fix last seen within week/month.

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

diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp
index 60a1c5f43..e4694f88d 100644
--- a/Telegram/SourceFiles/data/data_peer_values.cpp
+++ b/Telegram/SourceFiles/data/data_peer_values.cpp
@@ -65,12 +65,14 @@ std::optional<QString> OnlineTextCommon(LastseenStatus status, TimeId now) {
 		return tr::lng_status_online(tr::now);
 	} else if (status.isLongAgo()) {
 		return tr::lng_status_offline(tr::now);
-	} else if (status.isRecently() || status.isHidden()) {
+	} else if (status.isRecently()) {
 		return tr::lng_status_recently(tr::now);
 	} else if (status.isWithinWeek()) {
 		return tr::lng_status_last_week(tr::now);
 	} else if (status.isWithinMonth()) {
 		return tr::lng_status_last_month(tr::now);
+	} else if (status.isHidden()) {
+		return tr::lng_status_recently(tr::now);
 	}
 	return std::nullopt;
 }

From 03d4dd00d44d51440466c3b554e28f6018aaa903 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 12 Jul 2024 10:30:42 +0200
Subject: [PATCH 024/163] Delete selection on Ctrl+Backspace.

Fixes #28143.
---
 Telegram/lib_ui | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 96be2a6b7..9b69f3855 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 96be2a6b72f0405a9c01407148303fba2dab101c
+Subproject commit 9b69f3855ac9feb760aef92ce98e874129303d4d

From 5b9278ecedbe990ba9e5e00dd8baf3248885991f Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Wed, 10 Jul 2024 17:51:45 +0400
Subject: [PATCH 025/163] Switch Docker to distro-provided cmake

---
 Telegram/build/docker/centos_env/Dockerfile | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 71c9533eb..e4f7d1fb1 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -2,8 +2,6 @@
 {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%}
 {%- set QT = "6.7.2" -%}
 {%- set QT_TAG = "v" ~ QT -%}
-{%- set CMAKE_VER = "3.27.6" -%}
-{%- set CMAKE_FILE = "cmake-" ~ CMAKE_VER ~ "-Linux-x86_64.sh" -%}
 {%- set CFLAGS_DEBUG = "-g -pipe -fPIC -fstack-protector-all -fstack-clash-protection -fcf-protection -D_GLIBCXX_ASSERTIONS" -%}
 {%- set CFLAGS_LTO = "-flto=auto -ffat-lto-objects" -%}
 {%- set LibrariesPath = "/usr/src/Libraries" -%}
@@ -18,7 +16,7 @@ ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/loc
 
 RUN dnf -y install epel-release \
 	&& dnf config-manager --set-enabled powertools \
-	&& dnf -y install autoconf automake libtool pkgconfig make patch git \
+	&& dnf -y install cmake autoconf automake libtool pkgconfig make patch git \
 		python3.11-pip python3.11-devel gperf flex bison clang lld nasm yasm \
 		file which perl-open perl-XML-Parser perl-IPC-Cmd xorg-x11-util-macros \
 		gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils \
@@ -34,12 +32,6 @@ WORKDIR {{ LibrariesPath }}
 
 RUN python3 -m pip install meson ninja
 
-RUN mkdir /opt/cmake \
-	&& curl -sSLo {{ CMAKE_FILE }} {{ GIT }}/Kitware/CMake/releases/download/v{{ CMAKE_VER }}/{{ CMAKE_FILE }} \
-	&& sh {{ CMAKE_FILE }} --prefix=/opt/cmake --skip-license \
-	&& ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake \
-	&& rm {{ CMAKE_FILE }}
-
 FROM builder-base AS builder
 ENV AR gcc-ar
 ENV RANLIB gcc-ranlib

From c6e1cf639e16249ab811b153b8e956740403851a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 12 Jul 2024 10:32:23 +0200
Subject: [PATCH 026/163] Update API scheme to layer 184.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 .../export/data/export_data_types.cpp         |  7 ++++
 .../export/data/export_data_types.h           | 10 ++++-
 .../export/output/export_output_html.cpp      |  6 +++
 .../export/output/export_output_json.cpp      |  7 ++++
 Telegram/SourceFiles/history/history_item.cpp | 37 +++++++++++++++++++
 .../history/history_item_components.h         |  9 +++++
 .../view/history_view_service_message.cpp     |  2 +
 Telegram/SourceFiles/mtproto/scheme/api.tl    |  5 ++-
 .../settings/settings_credits_graphics.cpp    | 28 ++++++++++++++
 .../settings/settings_credits_graphics.h      |  3 ++
 11 files changed, 112 insertions(+), 3 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index ea754fcf9..0c60906ec 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1889,6 +1889,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_action_boost_apply#one" = "{from} boosted the group";
 "lng_action_boost_apply#other" = "{from} boosted the group {count} times";
 "lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?";
+"lng_action_payment_refunded" = "{peer} refunded back {amount}";
 
 "lng_similar_channels_title" = "Similar channels";
 "lng_similar_channels_view_all" = "View all";
diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp
index 16dbfb994..9e65eac39 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.cpp
+++ b/Telegram/SourceFiles/export/data/export_data_types.cpp
@@ -1515,6 +1515,13 @@ ServiceAction ParseServiceAction(
 		result.content = content;
 	}, [&](const MTPDmessageActionRequestedPeerSentMe &data) {
 		// Should not be in user inbox.
+	}, [&](const MTPDmessageActionPaymentRefunded &data) {
+		auto content = ActionPaymentRefunded();
+		content.currency = ParseString(data.vcurrency());
+		content.amount = data.vtotal_amount().v;
+		content.peerId = ParsePeerId(data.vpeer());
+		content.transactionId = data.vcharge().data().vid().v;
+		result.content = content;
 	}, [](const MTPDmessageActionEmpty &data) {});
 	return result;
 }
diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h
index cc16c47ba..72824a2ee 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.h
+++ b/Telegram/SourceFiles/export/data/export_data_types.h
@@ -576,6 +576,13 @@ struct ActionBoostApply {
 	int boosts = 0;
 };
 
+struct ActionPaymentRefunded {
+	PeerId peerId = 0;
+	Utf8String currency;
+	uint64 amount = 0;
+	Utf8String transactionId;
+};
+
 struct ServiceAction {
 	std::variant<
 		v::null_t,
@@ -617,7 +624,8 @@ struct ServiceAction {
 		ActionGiftCode,
 		ActionGiveawayLaunch,
 		ActionGiveawayResults,
-		ActionBoostApply> content;
+		ActionBoostApply,
+		ActionPaymentRefunded> content;
 };
 
 ServiceAction ParseServiceAction(
diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp
index f49776ede..d988fc00c 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html.cpp
@@ -1315,6 +1315,12 @@ auto HtmlWriter::Wrap::pushMessage(
 			+ " boosted the group "
 			+ QByteArray::number(data.boosts)
 			+ (data.boosts > 1 ? " times" : " time");
+	}, [&](const ActionPaymentRefunded &data) {
+		const auto amount = FormatMoneyAmount(data.amount, data.currency);
+		auto result = peers.wrapPeerName(data.peerId)
+			+ " refunded back "
+			+ amount;
+		return result;
 	}, [](v::null_t) { return QByteArray(); });
 
 	if (!serviceText.isEmpty()) {
diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp
index adbad2b7f..5d4c7e42d 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_json.cpp
@@ -625,6 +625,13 @@ QByteArray SerializeMessage(
 		pushActor();
 		pushAction("boost_apply");
 		push("boosts", data.boosts);
+	}, [&](const ActionPaymentRefunded &data) {
+		pushAction("refunded_payment");
+		push("amount", data.amount);
+		push("currency", data.currency);
+		pushBare("peer_name", wrapPeerName(data.peerId));
+		push("peer_id", data.peerId);
+		push("charge_id", data.transactionId);
 	}, [](v::null_t) {});
 
 	if (v::is_null(message.action.content)) {
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index d9c306013..d2386e7e6 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text/format_values.h"
 #include "ui/text/text_isolated_emoji.h"
 #include "ui/text/text_utilities.h"
+#include "settings/settings_credits_graphics.h" // ShowRefundInfoBox.
 #include "storage/file_upload.h"
 #include "storage/storage_shared_media.h"
 #include "main/main_account.h"
@@ -4028,6 +4029,22 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
 		}
 	} else if (type == mtpc_messageActionGiveawayResults) {
 		UpdateComponents(HistoryServiceGiveawayResults::Bit());
+	} else if (type == mtpc_messageActionPaymentRefunded) {
+		const auto &data = action.c_messageActionPaymentRefunded();
+		UpdateComponents(HistoryServicePaymentRefund::Bit());
+		const auto refund = Get<HistoryServicePaymentRefund>();
+		refund->peer = _history->owner().peer(peerFromMTP(data.vpeer()));
+		refund->amount = data.vtotal_amount().v;
+		refund->currency = qs(data.vcurrency());
+		refund->transactionId = qs(data.vcharge().data().vid());
+		const auto id = fullId();
+		refund->link = std::make_shared<LambdaClickHandler>([=](
+				ClickContext context) {
+			const auto my = context.other.value<ClickHandlerContext>();
+			if (const auto window = my.sessionWindow.get()) {
+				Settings::ShowRefundInfoBox(window, id);
+			}
+		});
 	}
 	if (const auto replyTo = message.vreply_to()) {
 		replyTo->match([&](const MTPDmessageReplyHeader &data) {
@@ -4941,6 +4958,25 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 		return result;
 	};
 
+	auto preparePaymentRefunded = [&](const MTPDmessageActionPaymentRefunded &action) {
+		auto result = PreparedServiceText();
+		const auto refund = Get<HistoryServicePaymentRefund>();
+		Assert(refund != nullptr);
+		Assert(refund->peer != nullptr);
+
+		const auto amount = refund->amount;
+		const auto currency = refund->currency;
+		result.links.push_back(refund->peer->createOpenLink());
+		result.text = tr::lng_action_payment_refunded(
+			tr::now,
+			lt_peer,
+			Ui::Text::Link(refund->peer->name(), 1), // Link 1.
+			lt_amount,
+			{ Ui::FillAmountAndCurrency(amount, currency) },
+			Ui::Text::WithEntities);
+		return result;
+	};
+
 	setServiceText(action.match(
 		prepareChatAddUserText,
 		prepareChatJoinedByLink,
@@ -4983,6 +5019,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 		prepareGiveawayLaunch,
 		prepareGiveawayResults,
 		prepareBoostApply,
+		preparePaymentRefunded,
 		PrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>,
 		PrepareErrorText<MTPDmessageActionEmpty>));
 
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 8d262a52e..7e9fb6396 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -671,6 +671,15 @@ struct HistoryServiceCustomLink
 	ClickHandlerPtr link;
 };
 
+struct HistoryServicePaymentRefund
+: public RuntimeComponent<HistoryServicePaymentRefund, HistoryItem> {
+	ClickHandlerPtr link;
+	PeerData *peer = nullptr;
+	QString transactionId;
+	QString currency;
+	uint64 amount = 0;
+};
+
 enum class HistorySelfDestructType {
 	Photo,
 	Video,
diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp
index 107bb9416..09fa2b43f 100644
--- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp
@@ -679,6 +679,8 @@ TextState Service::textState(QPoint point, StateRequest request) const {
 				result.link = results->lnk;
 			} else if (const auto custom = item->Get<HistoryServiceCustomLink>()) {
 				result.link = custom->link;
+			} else if (const auto payment = item->Get<HistoryServicePaymentRefund>()) {
+				result.link = payment->link;
 			} else if (media && data()->showSimilarChannels()) {
 				result = media->textState(mediaPoint, request);
 			}
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 7b8700811..394a50efa 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -102,7 +102,7 @@ channel#aadfc8f flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5
 channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat;
 
 chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull;
-channelFull#bbab348d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet = ChatFull;
+channelFull#bbab348d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet = ChatFull;
 
 chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant;
 chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant;
@@ -179,6 +179,7 @@ messageActionGiveawayLaunch#332ba9ed = MessageAction;
 messageActionGiveawayResults#2a9fadc5 winners_count:int unclaimed_count:int = MessageAction;
 messageActionBoostApply#cc02aa6d boosts:int = MessageAction;
 messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector<RequestedPeer> = MessageAction;
+messageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction;
 
 dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog;
 dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
@@ -2493,4 +2494,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool;
 
 fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;
 
-// LAYER 183
+// LAYER 184
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index 648a64900..3b88e8d48 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/stickers/data_custom_emoji.h"
 #include "history/history.h"
 #include "history/history_item.h"
+#include "history/history_item_components.h" // HistoryServicePaymentRefund.
 #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData.
 #include "info/statistics/info_statistics_list_controllers.h"
 #include "lang/lang_keys.h"
@@ -633,6 +634,33 @@ void ReceiptCreditsBox(
 	}, button->lifetime());
 }
 
+void ShowRefundInfoBox(
+		not_null<Window::SessionController*> controller,
+		FullMsgId refundItemId) {
+	const auto owner = &controller->session().data();
+	const auto item = owner->message(refundItemId);
+	const auto refund = item
+		? item->Get<HistoryServicePaymentRefund>()
+		: nullptr;
+	if (!refund) {
+		return;
+	}
+	Assert(refund->peer != nullptr);
+	auto info = Data::CreditsHistoryEntry();
+	info.id = refund->transactionId;
+	info.date = base::unixtime::parse(item->date());
+	info.credits = refund->amount;
+	info.barePeerId = refund->peer->id.value;
+	info.peerType = Data::CreditsHistoryEntry::PeerType::Peer;
+	info.refunded = true;
+	info.in = true;
+	controller->show(Box(
+		::Settings::ReceiptCreditsBox,
+		controller,
+		nullptr, // premiumBot
+		info));
+}
+
 object_ptr<Ui::RpWidget> GenericEntryPhoto(
 		not_null<Ui::RpWidget*> parent,
 		Fn<Fn<void(Painter &, int, int, int, int)>(Fn<void()>)> callback,
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h
index efea501b9..406bfabca 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.h
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h
@@ -54,6 +54,9 @@ void ReceiptCreditsBox(
 	not_null<Window::SessionController*> controller,
 	PeerData *premiumBot,
 	const Data::CreditsHistoryEntry &e);
+void ShowRefundInfoBox(
+	not_null<Window::SessionController*> controller,
+	FullMsgId refundItemId);
 
 [[nodiscard]] object_ptr<Ui::RpWidget> GenericEntryPhoto(
 	not_null<Ui::RpWidget*> parent,

From 8ac1ad348462e7ff2475200cc9d7800312c9fcae Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Mon, 8 Jul 2024 09:20:12 +0300
Subject: [PATCH 027/163] Updated Qt to 6.2.9 on macOS.

---
 Telegram/build/prepare/prepare.py | 2 +-
 Telegram/build/qt_version.py      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index 8b1f71c68..6704bdbd2 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -445,7 +445,7 @@ if customRunCommand:
 stage('patches', """
     git clone https://github.com/desktop-app/patches.git
     cd patches
-    git checkout 20a7c5ffd8
+    git checkout e0cdca2e79
 """)
 
 stage('msys64', """
diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py
index 1703e15d0..2bc778741 100644
--- a/Telegram/build/qt_version.py
+++ b/Telegram/build/qt_version.py
@@ -2,7 +2,7 @@ import sys, os
 
 def resolve(arch):
     if sys.platform == 'darwin':
-        os.environ['QT'] = '6.2.8'
+        os.environ['QT'] = '6.2.9'
     elif sys.platform == 'win32':
         if arch == 'arm' or 'qt6' in sys.argv:
             print('Choosing Qt 6.')

From 3f0b962ae52b993e4c133937a552b7ad9b403dc1 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 10 Jul 2024 09:19:33 +0300
Subject: [PATCH 028/163] Fixed color reset for chat filters on saving.

---
 .../SourceFiles/boxes/choose_filter_box.cpp   |  2 ++
 .../boxes/filters/edit_filter_box.cpp         |  4 +++
 .../SourceFiles/data/data_chat_filters.cpp    | 25 +++++++++++++++----
 Telegram/SourceFiles/data/data_chat_filters.h |  4 +++
 4 files changed, 30 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp
index 5af8577bf..4e99a01ba 100644
--- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp
+++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp
@@ -39,6 +39,7 @@ Data::ChatFilter ChangedFilter(
 		filter.id(),
 		filter.title(),
 		filter.iconEmoji(),
+		filter.colorIndex(),
 		filter.flags(),
 		std::move(always),
 		filter.pinned(),
@@ -58,6 +59,7 @@ Data::ChatFilter ChangedFilter(
 		filter.id(),
 		filter.title(),
 		filter.iconEmoji(),
+		filter.colorIndex(),
 		filter.flags(),
 		std::move(always),
 		filter.pinned(),
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
index ce23d235f..df85e6254 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
@@ -83,6 +83,7 @@ not_null<FilterChatsPreview*> SetupChatsPreview(
 			rules.id(),
 			rules.title(),
 			rules.iconEmoji(),
+			rules.colorIndex(),
 			(rules.flags() & ~flag),
 			rules.always(),
 			rules.pinned(),
@@ -104,6 +105,7 @@ not_null<FilterChatsPreview*> SetupChatsPreview(
 			rules.id(),
 			rules.title(),
 			rules.iconEmoji(),
+			rules.colorIndex(),
 			rules.flags(),
 			std::move(always),
 			std::move(pinned),
@@ -170,6 +172,7 @@ void EditExceptions(
 				rules.id(),
 				rules.title(),
 				rules.iconEmoji(),
+				rules.colorIndex(),
 				((rules.flags() & ~options)
 					| rawController->chosenOptions()),
 				include ? std::move(changed) : std::move(removeFrom),
@@ -240,6 +243,7 @@ void CreateIconSelector(
 			rules.id(),
 			rules.title(),
 			Ui::LookupFilterIcon(icon).emoji,
+			rules.colorIndex(),
 			rules.flags(),
 			rules.always(),
 			rules.pinned(),
diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp
index efac543af..7a6f63a74 100644
--- a/Telegram/SourceFiles/data/data_chat_filters.cpp
+++ b/Telegram/SourceFiles/data/data_chat_filters.cpp
@@ -43,6 +43,7 @@ ChatFilter::ChatFilter(
 	FilterId id,
 	const QString &title,
 	const QString &iconEmoji,
+	std::optional<uint8> colorIndex,
 	Flags flags,
 	base::flat_set<not_null<History*>> always,
 	std::vector<not_null<History*>> pinned,
@@ -50,6 +51,7 @@ ChatFilter::ChatFilter(
 : _id(id)
 , _title(title)
 , _iconEmoji(iconEmoji)
+, _colorIndex(colorIndex)
 , _always(std::move(always))
 , _pinned(std::move(pinned))
 , _never(std::move(never))
@@ -95,6 +97,9 @@ ChatFilter ChatFilter::FromTL(
 			data.vid().v,
 			qs(data.vtitle()),
 			qs(data.vemoticon().value_or_empty()),
+			data.vcolor()
+				? std::make_optional(data.vcolor()->v)
+				: std::nullopt,
 			flags,
 			std::move(list),
 			std::move(pinned),
@@ -140,6 +145,9 @@ ChatFilter ChatFilter::FromTL(
 			data.vid().v,
 			qs(data.vtitle()),
 			qs(data.vemoticon().value_or_empty()),
+			data.vcolor()
+				? std::make_optional(data.vcolor()->v)
+				: std::nullopt,
 			(Flag::Chatlist
 				| (data.is_has_my_invites() ? Flag::HasMyLinks : Flag())),
 			std::move(list),
@@ -189,18 +197,20 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const {
 	}
 	if (_flags & Flag::Chatlist) {
 		using TLFlag = MTPDdialogFilterChatlist::Flag;
-		const auto flags = TLFlag::f_emoticon;
+		const auto flags = TLFlag::f_emoticon
+			| (_colorIndex ? TLFlag::f_color : TLFlag(0));
 		return MTP_dialogFilterChatlist(
 			MTP_flags(flags),
 			MTP_int(replaceId ? replaceId : _id),
 			MTP_string(_title),
 			MTP_string(_iconEmoji),
-			MTPint(), // color
+			MTP_int(_colorIndex.value_or(0)),
 			MTP_vector<MTPInputPeer>(pinned),
 			MTP_vector<MTPInputPeer>(include));
 	}
 	using TLFlag = MTPDdialogFilter::Flag;
 	const auto flags = TLFlag::f_emoticon
+		| (_colorIndex ? TLFlag::f_color : TLFlag(0))
 		| ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0))
 		| ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0))
 		| ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0))
@@ -221,7 +231,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const {
 		MTP_int(replaceId ? replaceId : _id),
 		MTP_string(_title),
 		MTP_string(_iconEmoji),
-		MTPint(), // color
+		MTP_int(_colorIndex.value_or(0)),
 		MTP_vector<MTPInputPeer>(pinned),
 		MTP_vector<MTPInputPeer>(include),
 		MTP_vector<MTPInputPeer>(never));
@@ -239,6 +249,10 @@ QString ChatFilter::iconEmoji() const {
 	return _iconEmoji;
 }
 
+std::optional<uint8> ChatFilter::colorIndex() const {
+	return _colorIndex;
+}
+
 ChatFilter::Flags ChatFilter::flags() const {
 	return _flags;
 }
@@ -555,7 +569,7 @@ void ChatFilters::applyInsert(ChatFilter filter, int position) {
 
 	_list.insert(
 		begin(_list) + position,
-		ChatFilter(filter.id(), {}, {}, {}, {}, {}, {}));
+		ChatFilter(filter.id(), {}, {}, {}, {}, {}, {}, {}));
 	applyChange(*(begin(_list) + position), std::move(filter));
 }
 
@@ -582,7 +596,7 @@ void ChatFilters::applyRemove(int position) {
 	Expects(position >= 0 && position < _list.size());
 
 	const auto i = begin(_list) + position;
-	applyChange(*i, ChatFilter(i->id(), {}, {}, {}, {}, {}, {}));
+	applyChange(*i, ChatFilter(i->id(), {}, {}, {}, {}, {}, {}, {}));
 	_list.erase(i);
 }
 
@@ -711,6 +725,7 @@ const ChatFilter &ChatFilters::applyUpdatedPinned(
 		id,
 		i->title(),
 		i->iconEmoji(),
+		i->colorIndex(),
 		i->flags(),
 		std::move(always),
 		std::move(pinned),
diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h
index 7b5a96476..e123ab2c1 100644
--- a/Telegram/SourceFiles/data/data_chat_filters.h
+++ b/Telegram/SourceFiles/data/data_chat_filters.h
@@ -52,6 +52,7 @@ public:
 		FilterId id,
 		const QString &title,
 		const QString &iconEmoji,
+		std::optional<uint8> colorIndex,
 		Flags flags,
 		base::flat_set<not_null<History*>> always,
 		std::vector<not_null<History*>> pinned,
@@ -71,6 +72,7 @@ public:
 	[[nodiscard]] FilterId id() const;
 	[[nodiscard]] QString title() const;
 	[[nodiscard]] QString iconEmoji() const;
+	[[nodiscard]] std::optional<uint8> colorIndex() const;
 	[[nodiscard]] Flags flags() const;
 	[[nodiscard]] bool chatlist() const;
 	[[nodiscard]] bool hasMyLinks() const;
@@ -84,6 +86,7 @@ private:
 	FilterId _id = 0;
 	QString _title;
 	QString _iconEmoji;
+	std::optional<uint8> _colorIndex;
 	base::flat_set<not_null<History*>> _always;
 	std::vector<not_null<History*>> _pinned;
 	base::flat_set<not_null<History*>> _never;
@@ -94,6 +97,7 @@ private:
 inline bool operator==(const ChatFilter &a, const ChatFilter &b) {
 	return (a.title() == b.title())
 		&& (a.iconEmoji() == b.iconEmoji())
+		&& (a.colorIndex() == b.colorIndex())
 		&& (a.flags() == b.flags())
 		&& (a.always() == b.always())
 		&& (a.never() == b.never());

From 6818b8d8dc6d8f21754bc823b620d5be9575b355 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Sun, 14 Jul 2024 22:22:05 +0300
Subject: [PATCH 029/163] Fixed row name in table for credits history entries
 in earn box.

---
 Telegram/Resources/langs/lang.strings           | 1 +
 Telegram/SourceFiles/boxes/gift_premium_box.cpp | 4 +++-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 0c60906ec..e7b50f139 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2364,6 +2364,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance.";
 "lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance.";
 "lng_credits_box_history_entry_peer" = "Recipient";
+"lng_credits_box_history_entry_peer_in" = "From";
 "lng_credits_box_history_entry_via" = "Via";
 "lng_credits_box_history_entry_play_market" = "Play Market";
 "lng_credits_box_history_entry_app_store" = "App Store";
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index 972a7a5f4..58dc2941f 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -1648,7 +1648,9 @@ void AddCreditsHistoryEntryTable(
 		st::giveawayGiftCodeTableMargin);
 	const auto peerId = PeerId(entry.barePeerId);
 	if (peerId) {
-		auto text = tr::lng_credits_box_history_entry_peer();
+		auto text = entry.in
+			? tr::lng_credits_box_history_entry_peer_in()
+			: tr::lng_credits_box_history_entry_peer();
 		AddTableRow(table, std::move(text), controller, peerId);
 	}
 	if (const auto msgId = MsgId(peerId ? entry.bareMsgId : 0)) {

From 1d5e4040f408d1d6541d10c0a4f57491a4b551d4 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Sun, 14 Jul 2024 22:59:27 +0300
Subject: [PATCH 030/163] Added api support of flag to view credits stats from
 full channel.

---
 Telegram/SourceFiles/data/data_channel.cpp               | 8 ++++++--
 Telegram/SourceFiles/data/data_channel.h                 | 1 +
 .../channel_statistics/earn/info_channel_earn_list.cpp   | 9 ++++++++-
 Telegram/SourceFiles/window/window_peer_menu.cpp         | 4 +++-
 4 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp
index 2f7694395..6162024fd 100644
--- a/Telegram/SourceFiles/data/data_channel.cpp
+++ b/Telegram/SourceFiles/data/data_channel.cpp
@@ -1089,7 +1089,8 @@ void ApplyChannelUpdate(
 		| Flag::CanGetStatistics
 		| Flag::ViewAsMessages
 		| Flag::CanViewRevenue
-		| Flag::PaidMediaAllowed;
+		| Flag::PaidMediaAllowed
+		| Flag::CanViewCreditsRevenue;
 	channel->setFlags((channel->flags() & ~mask)
 		| (update.is_can_set_username() ? Flag::CanSetUsername : Flag())
 		| (update.is_can_view_participants()
@@ -1107,7 +1108,10 @@ void ApplyChannelUpdate(
 			? Flag::ViewAsMessages
 			: Flag())
 		| (update.is_paid_media_allowed() ? Flag::PaidMediaAllowed : Flag())
-		| (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag()));
+		| (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag())
+		| (update.is_can_view_stars_revenue()
+			? Flag::CanViewCreditsRevenue
+			: Flag()));
 	channel->setUserpicPhoto(update.vchat_photo());
 	if (const auto migratedFrom = update.vmigrated_from_chat_id()) {
 		channel->addFlags(Flag::Megagroup);
diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h
index d6f880bda..81ad7778d 100644
--- a/Telegram/SourceFiles/data/data_channel.h
+++ b/Telegram/SourceFiles/data/data_channel.h
@@ -67,6 +67,7 @@ enum class ChannelDataFlag : uint64 {
 	SimilarExpanded = (1ULL << 31),
 	CanViewRevenue = (1ULL << 32),
 	PaidMediaAllowed = (1ULL << 33),
+	CanViewCreditsRevenue = (1ULL << 34),
 };
 inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
 using ChannelDataFlags = base::flags<ChannelDataFlag>;
diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp
index 84ad81390..c16acc204 100644
--- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp
+++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp
@@ -281,6 +281,9 @@ void InnerWidget::load() {
 		rpl::lifetime apiPremiumBotLifetime;
 	};
 	const auto state = lifetime().make_state<State>(_peer);
+	using ChannelFlag = ChannelDataFlag;
+	const auto canViewCredits = !_peer->isChannel()
+		|| (_peer->asChannel()->flags() & ChannelFlag::CanViewCreditsRevenue);
 
 	Info::Statistics::FillLoading(
 		this,
@@ -363,7 +366,11 @@ void InnerWidget::load() {
 					_state.premiumBotId = bot->id;
 					state->apiCredits.request(
 					) | rpl::start_with_error_done([=](const QString &error) {
-						fail(error);
+						if (canViewCredits) {
+							fail(error);
+						} else {
+							_state.creditsEarn = {};
+						}
 						finish();
 					}, [=] {
 						_state.creditsEarn = state->apiCredits.data();
diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp
index f8259eec7..74f6f7ed8 100644
--- a/Telegram/SourceFiles/window/window_peer_menu.cpp
+++ b/Telegram/SourceFiles/window/window_peer_menu.cpp
@@ -1090,6 +1090,8 @@ void Filler::addViewStatistics() {
 		using Flag = ChannelDataFlag;
 		const auto canGetStats = (channel->flags() & Flag::CanGetStatistics);
 		const auto canViewEarn = (channel->flags() & Flag::CanViewRevenue);
+		const auto canViewCreditsEarn
+			= (channel->flags() & Flag::CanViewCreditsRevenue);
 		if (canGetStats) {
 			_addAction(tr::lng_stats_title(tr::now), [=] {
 				if (const auto strong = weak.get()) {
@@ -1107,7 +1109,7 @@ void Filler::addViewStatistics() {
 				}
 			}, &st::menuIconBoosts);
 		}
-		if (canViewEarn) {
+		if (canViewEarn || canViewCreditsEarn) {
 			_addAction(tr::lng_channel_earn_title(tr::now), [=] {
 				if (const auto strong = weak.get()) {
 					controller->showSection(Info::ChannelEarn::Make(peer));

From 4b9eb37bd53503c856a8e4d3014636f15b84b9ef Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Mon, 15 Jul 2024 00:05:19 +0300
Subject: [PATCH 031/163] Added phrases for single winner of giveaway in
 channels.

---
 Telegram/Resources/langs/lang.strings                |  3 +++
 Telegram/SourceFiles/data/data_media_types.cpp       |  6 +++++-
 .../history/view/media/history_view_giveaway.cpp     | 12 +++++++++---
 3 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index e7b50f139..451f34da5 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2813,12 +2813,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_prizes_badge" = "x{amount}";
 
 "lng_prizes_results_title" = "Winners Selected!";
+"lng_prizes_results_title_one" = "Winner Selected!";
 "lng_prizes_results_about#one" = "**{count}** winner of the {link} was randomly selected by Telegram.";
 "lng_prizes_results_about#other" = "**{count}** winners of the {link} were randomly selected by Telegram.";
 "lng_prizes_results_link" = "Giveaway";
+"lng_prizes_results_winner" = "Winner";
 "lng_prizes_results_winners" = "Winners";
 "lng_prizes_results_more#one" = "and {count} more!";
 "lng_prizes_results_more#other" = "and {count} more!";
+"lng_prizes_results_one" = "The winner received their gift link in a private message.";
 "lng_prizes_results_all" = "All winners received gift links in private messages.";
 "lng_prizes_results_some" = "Some winners couldn't be selected.";
 
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index a7b2690ea..da16410b0 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -2631,7 +2631,11 @@ const GiveawayResults *MediaGiveawayResults::giveawayResults() const {
 }
 
 TextWithEntities MediaGiveawayResults::notificationText() const {
-	return Ui::Text::Colorized({ tr::lng_prizes_results_title(tr::now) });
+	return Ui::Text::Colorized({
+		((_data.winnersCount == 1)
+			? tr::lng_prizes_results_title_one
+			: tr::lng_prizes_results_title)(tr::now)
+	});
 }
 
 QString MediaGiveawayResults::pinnedTextSubstring() const {
diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp
index 310af8fa6..f2f98d05e 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp
@@ -200,9 +200,11 @@ auto GenerateGiveawayResults(
 				margins,
 				links));
 		};
+		const auto isSingleWinner = (data->winnersCount == 1);
 		pushText(
-			Ui::Text::Bold(
-				tr::lng_prizes_results_title(tr::now)),
+			(isSingleWinner
+				? tr::lng_prizes_results_title_one
+				: tr::lng_prizes_results_title)(tr::now, Ui::Text::Bold),
 			st::chatGiveawayPrizesTitleMargin);
 		const auto showGiveawayHandler = JumpToMessageClickHandler(
 			data->channel,
@@ -219,7 +221,9 @@ auto GenerateGiveawayResults(
 			st::chatGiveawayPrizesMargin,
 			{ { 1, showGiveawayHandler } });
 		pushText(
-			Ui::Text::Bold(tr::lng_prizes_results_winners(tr::now)),
+			(isSingleWinner
+				? tr::lng_prizes_results_winner
+				: tr::lng_prizes_results_winners)(tr::now, Ui::Text::Bold),
 			st::chatGiveawayPrizesTitleMargin);
 
 		push(std::make_unique<PeerBubbleListPart>(
@@ -235,6 +239,8 @@ auto GenerateGiveawayResults(
 		}
 		pushText({ data->unclaimedCount
 			? tr::lng_prizes_results_some(tr::now)
+			: isSingleWinner
+			? tr::lng_prizes_results_one(tr::now)
 			: tr::lng_prizes_results_all(tr::now)
 		}, st::chatGiveawayEndDateMargin);
 	};

From 4f7a124f3e85f3f61d862b94fb5a45236976f38f Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Mon, 15 Jul 2024 00:37:33 +0300
Subject: [PATCH 032/163] Added phrases to credits history entries when peer
 type is premium bot.

---
 Telegram/Resources/langs/lang.strings         |  2 ++
 .../SourceFiles/boxes/gift_premium_box.cpp    |  6 ++++
 .../info_statistics_list_controllers.cpp      |  2 --
 .../settings/settings_credits_graphics.cpp    | 29 ++++++++++++++++++-
 .../ui/effects/credits_graphics.cpp           | 21 +++++++++++---
 5 files changed, 53 insertions(+), 7 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 451f34da5..5c461c1ef 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2370,6 +2370,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_credits_box_history_entry_app_store" = "App Store";
 "lng_credits_box_history_entry_fragment" = "Fragment";
 "lng_credits_box_history_entry_ads" = "Ads Platform";
+"lng_credits_box_history_entry_premium_bot" = "Stars Top-Up";
+"lng_credits_box_history_entry_via_premium_bot" = "Premium Bot";
 "lng_credits_box_history_entry_id" = "Transaction ID";
 "lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard.";
 "lng_credits_box_history_entry_success_date" = "Transaction date";
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index 58dc2941f..fdf1eda03 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -1702,6 +1702,12 @@ void AddCreditsHistoryEntryTable(
 			table,
 			tr::lng_credits_box_history_entry_via(),
 			tr::lng_credits_box_history_entry_ads(Ui::Text::RichLangValue));
+	} else if (entry.peerType == Type::PremiumBot) {
+		AddTableRow(
+			table,
+			tr::lng_credits_box_history_entry_via(),
+			tr::lng_credits_box_history_entry_via_premium_bot(
+				Ui::Text::RichLangValue));
 	}
 	if (!entry.id.isEmpty()) {
 		constexpr auto kOneLineCount = 18;
diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
index 956c7d0c3..a176be565 100644
--- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
+++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
@@ -954,8 +954,6 @@ void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) {
 			if (const auto peerId = PeerId(item.barePeerId)) {
 				const auto peer = session().data().peer(peerId);
 				return std::make_unique<CreditsRow>(peer, descriptor);
-			} else if (item.peerType == Type::PremiumBot) {
-				return std::make_unique<CreditsRow>(_premiumBot, descriptor);
 			} else {
 				return std::make_unique<CreditsRow>(descriptor);
 			}
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index 3b88e8d48..282945d9e 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -448,7 +448,7 @@ void ReceiptCreditsBox(
 	const auto &stUser = st::boostReplaceUserpic;
 	const auto session = &controller->session();
 	const auto peer = (e.peerType == Type::PremiumBot)
-		? premiumBot
+		? nullptr
 		: e.barePeerId
 		? session->data().peer(PeerId(e.barePeerId)).get()
 		: nullptr;
@@ -622,6 +622,33 @@ void ReceiptCreditsBox(
 
 	Ui::AddSkip(content);
 
+	if (e.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) {
+		const auto widget = Ui::CreateChild<Ui::RpWidget>(content);
+		using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
+		const auto stars = widget->lifetime().make_state<ColoredMiniStars>(
+			widget,
+			false,
+			Ui::Premium::MiniStars::Type::BiStars);
+		stars->setColorOverride(Ui::Premium::CreditsIconGradientStops());
+		widget->resize(
+			st::boxWidth - stUser.photoSize,
+			stUser.photoSize * 2);
+		content->sizeValue(
+		) | rpl::start_with_next([=](const QSize &size) {
+			widget->moveToLeft(stUser.photoSize / 2, 0);
+			const auto starsRect = Rect(widget->size());
+			stars->setPosition(starsRect.topLeft());
+			stars->setSize(starsRect.size());
+			widget->lower();
+		}, widget->lifetime());
+		widget->paintRequest(
+		) | rpl::start_with_next([=](const QRect &r) {
+			auto p = QPainter(widget);
+			p.fillRect(r, Qt::transparent);
+			stars->paint(p);
+		}, widget->lifetime());
+	}
+
 	const auto button = box->addButton(tr::lng_box_ok(), [=] {
 		box->closeBox();
 	});
diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
index 3f4ce02f2..e99a4d4eb 100644
--- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
+++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
@@ -193,6 +193,21 @@ not_null<MaskedInputField*> AddInputFieldForCredits(
 
 PaintRoundImageCallback GenerateCreditsPaintUserpicCallback(
 		const Data::CreditsHistoryEntry &entry) {
+	using PeerType = Data::CreditsHistoryEntry::PeerType;
+	if (entry.peerType == PeerType::PremiumBot) {
+		const auto svg = std::make_shared<QSvgRenderer>(Ui::Premium::Svg());
+		return [=](Painter &p, int x, int y, int, int size) mutable {
+			const auto hq = PainterHighQualityEnabler(p);
+			p.setPen(Qt::NoPen);
+			{
+				auto gradient = QLinearGradient(x + size, y + size, x, y);
+				gradient.setStops(Ui::Premium::ButtonGradientStops());
+				p.setBrush(gradient);
+			}
+			p.drawEllipse(x, y, size, size);
+			svg->render(&p, QRectF(x, y, size, size) - Margins(size / 5.));
+		};
+	}
 	const auto bg = [&]() -> EmptyUserpic::BgColors {
 		switch (entry.peerType) {
 		case Data::CreditsHistoryEntry::PeerType::Peer:
@@ -218,10 +233,6 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback(
 	const auto userpic = std::make_shared<EmptyUserpic>(bg, QString());
 	return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
 		userpic->paintCircle(p, x, y, outerWidth, size);
-		using PeerType = Data::CreditsHistoryEntry::PeerType;
-		if (entry.peerType == PeerType::PremiumBot) {
-			return;
-		}
 		const auto rect = QRect(x, y, size, size);
 		((entry.peerType == PeerType::AppStore)
 			? st::sessionIconiPhone
@@ -446,6 +457,8 @@ Fn<PaintRoundImageCallback(Fn<void()>)> PaintPreviewCallback(
 TextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) {
 	return ((entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment)
 		? tr::lng_bot_username_description1_link
+		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot)
+		? tr::lng_credits_box_history_entry_premium_bot
 		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::Ads)
 		? tr::lng_credits_box_history_entry_ads
 		: tr::lng_credits_summary_history_entry_inner_in)(

From a52d4eb4e8c4a0e3ca71ea359d2c5abb733343a4 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 18:09:25 +0300
Subject: [PATCH 033/163] Fixed peer in list of credits history entries.

---
 .../info/bot/earn/info_bot_earn_list.cpp         | 16 ++++++++--------
 .../earn/info_channel_earn_list.cpp              |  2 +-
 .../info_statistics_list_controllers.cpp         |  8 +++-----
 .../info_statistics_list_controllers.h           |  2 +-
 .../SourceFiles/settings/settings_credits.cpp    |  8 ++++----
 5 files changed, 17 insertions(+), 19 deletions(-)

diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp
index 169fe8883..cbe32faf5 100644
--- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp
+++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp
@@ -261,7 +261,7 @@ void InnerWidget::fillHistory() {
 
 	const auto sectionIndex = history->lifetime().make_state<int>(0);
 
-	const auto fill = [=](
+	const auto fill = [=, peer = _peer](
 			not_null<PeerData*> premiumBot,
 			const Data::CreditsStatusSlice &fullSlice,
 			const Data::CreditsStatusSlice &inSlice,
@@ -368,7 +368,7 @@ void InnerWidget::fillHistory() {
 			fullSlice,
 			fullWrap->entity(),
 			entryClicked,
-			premiumBot,
+			peer,
 			star,
 			true,
 			true);
@@ -377,7 +377,7 @@ void InnerWidget::fillHistory() {
 			inSlice,
 			inWrap->entity(),
 			entryClicked,
-			premiumBot,
+			peer,
 			star,
 			true,
 			false);
@@ -386,7 +386,7 @@ void InnerWidget::fillHistory() {
 			outSlice,
 			outWrap->entity(),
 			std::move(entryClicked),
-			premiumBot,
+			peer,
 			star,
 			false,
 			true);
@@ -398,11 +398,11 @@ void InnerWidget::fillHistory() {
 	const auto apiLifetime = history->lifetime().make_state<rpl::lifetime>();
 	rpl::single(rpl::empty) | rpl::then(
 		_stateUpdated.events()
-	) | rpl::start_with_next([=] {
+	) | rpl::start_with_next([=, peer = _peer] {
 		using Api = Api::CreditsHistory;
-		const auto apiFull = apiLifetime->make_state<Api>(_peer, true, true);
-		const auto apiIn = apiLifetime->make_state<Api>(_peer, true, false);
-		const auto apiOut = apiLifetime->make_state<Api>(_peer, false, true);
+		const auto apiFull = apiLifetime->make_state<Api>(peer, true, true);
+		const auto apiIn = apiLifetime->make_state<Api>(peer, true, false);
+		const auto apiOut = apiLifetime->make_state<Api>(peer, false, true);
 		apiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) {
 			apiIn->request({}, [=](Data::CreditsStatusSlice inSlice) {
 				apiOut->request({}, [=](Data::CreditsStatusSlice outSlice) {
diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp
index c16acc204..c96dac4ea 100644
--- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp
+++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp
@@ -1419,7 +1419,7 @@ void InnerWidget::fill() {
 				data.creditsStatusSlice,
 				tabCreditsList->entity(),
 				entryClicked,
-				premiumBot,
+				_peer,
 				star,
 				true,
 				true);
diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
index a176be565..a9649fb4d 100644
--- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
+++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
@@ -131,7 +131,7 @@ struct BoostsDescriptor final {
 struct CreditsDescriptor final {
 	Data::CreditsStatusSlice firstSlice;
 	Fn<void(const Data::CreditsHistoryEntry &)> entryClickedCallback;
-	not_null<PeerData*> premiumBot;
+	not_null<PeerData*> peer;
 	not_null<QImage*> creditIcon;
 	bool in = false;
 	bool out = false;
@@ -889,7 +889,6 @@ private:
 	void applySlice(const Data::CreditsStatusSlice &slice);
 
 	const not_null<Main::Session*> _session;
-	const not_null<PeerData*> _premiumBot;
 	Fn<void(const Data::CreditsHistoryEntry &)> _entryClickedCallback;
 	not_null<QImage*> const _creditIcon;
 
@@ -903,11 +902,10 @@ private:
 };
 
 CreditsController::CreditsController(CreditsDescriptor d)
-: _session(&d.premiumBot->session())
-, _premiumBot(d.premiumBot)
+: _session(&d.peer->session())
 , _entryClickedCallback(std::move(d.entryClickedCallback))
 , _creditIcon(d.creditIcon)
-, _api(d.premiumBot->session().user(), d.in, d.out)
+, _api(d.peer, d.in, d.out)
 , _firstSlice(std::move(d.firstSlice)) {
 	PeerListController::setStyleOverrides(&st::boostsListBox);
 }
diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h
index 2d381cb7c..8423de55c 100644
--- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h
+++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h
@@ -55,7 +55,7 @@ void AddCreditsHistoryList(
 	const Data::CreditsStatusSlice &firstSlice,
 	not_null<Ui::VerticalLayout*> container,
 	Fn<void(const Data::CreditsHistoryEntry &)> entryClickedCallback,
-	not_null<PeerData*> premiumBot,
+	not_null<PeerData*> peer,
 	not_null<QImage*> creditIcon,
 	bool in,
 	bool out);
diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp
index 0890f2703..5088ce62a 100644
--- a/Telegram/SourceFiles/settings/settings_credits.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits.cpp
@@ -128,6 +128,7 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) {
 			container,
 			object_ptr<Ui::VerticalLayout>(container)));
 	const auto content = history->entity();
+	const auto self = _controller->session().user();
 
 	Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top());
 
@@ -231,7 +232,7 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) {
 			fullSlice,
 			fullWrap->entity(),
 			entryClicked,
-			premiumBot,
+			self,
 			&_star,
 			true,
 			true);
@@ -240,7 +241,7 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) {
 			inSlice,
 			inWrap->entity(),
 			entryClicked,
-			premiumBot,
+			self,
 			&_star,
 			true,
 			false);
@@ -249,7 +250,7 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) {
 			outSlice,
 			outWrap->entity(),
 			std::move(entryClicked),
-			premiumBot,
+			self,
 			&_star,
 			false,
 			true);
@@ -263,7 +264,6 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) {
 	const auto apiLifetime = content->lifetime().make_state<rpl::lifetime>();
 	{
 		using Api = Api::CreditsHistory;
-		const auto self = _controller->session().user();
 		const auto apiFull = apiLifetime->make_state<Api>(self, true, true);
 		const auto apiIn = apiLifetime->make_state<Api>(self, true, false);
 		const auto apiOut = apiLifetime->make_state<Api>(self, false, true);

From 2a63496054324e4a797d26cc029b701225fd5cee Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Thu, 18 Jul 2024 01:54:05 +0300
Subject: [PATCH 034/163] Extended conditions to ability to view channel
 message statistics.

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

diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 1a302bd10..7838dda79 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -2227,8 +2227,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 		if (!item->isService()
 			&& peerIsChannel(itemId.peer)
 			&& !_peer->isMegagroup()) {
+			constexpr auto kMinViewsCount = 10;
 			if (const auto channel = _peer->asChannel()) {
-				if (channel->flags() & ChannelDataFlag::CanGetStatistics) {
+				if ((channel->flags() & ChannelDataFlag::CanGetStatistics)
+					|| (channel->canPostMessages()
+						&& item->viewsCount() >= kMinViewsCount)) {
 					auto callback = crl::guard(controller, [=] {
 						controller->showSection(
 							Info::Statistics::Make(channel, itemId, {}));

From 2a5071b66c648f05674b595bfab9659cb4e39dcb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 4 Jul 2024 12:18:30 +0400
Subject: [PATCH 035/163] Initial location sending on Windows.

---
 Telegram/CMakeLists.txt                       |   1 +
 Telegram/Resources/langs/lang.strings         |   1 +
 Telegram/Resources/picker_html/picker.css     |  60 +++
 Telegram/Resources/picker_html/picker.js      |  80 ++++
 Telegram/Resources/qrc/telegram/picker.qrc    |   6 +
 Telegram/SourceFiles/api/api_sending.cpp      | 106 ++++++
 Telegram/SourceFiles/api/api_sending.h        |  19 +-
 .../SourceFiles/core/current_geo_location.cpp |  50 +++
 .../SourceFiles/core/current_geo_location.h   |  41 ++
 Telegram/SourceFiles/data/data_location.h     |  10 +
 .../data/raw/raw_countries_bounds.cpp         | 193 ++++++++++
 .../data/raw/raw_countries_bounds.h           |  23 ++
 .../inline_bots/bot_attach_web_view.cpp       |  30 ++
 .../linux/current_geo_location_linux.cpp      |  18 +
 .../linux/current_geo_location_linux.h        |  10 +
 .../platform/mac/current_geo_location_mac.h   |  10 +
 .../platform/mac/current_geo_location_mac.mm  |  18 +
 .../platform/platform_current_geo_location.h  |  18 +
 .../platform/win/current_geo_location_win.cpp |  59 +++
 .../platform/win/current_geo_location_win.h   |  10 +
 .../ui/controls/location_picker.cpp           | 350 ++++++++++++++++++
 .../SourceFiles/ui/controls/location_picker.h |  67 ++++
 Telegram/cmake/td_ui.cmake                    |  19 +
 23 files changed, 1195 insertions(+), 4 deletions(-)
 create mode 100644 Telegram/Resources/picker_html/picker.css
 create mode 100644 Telegram/Resources/picker_html/picker.js
 create mode 100644 Telegram/Resources/qrc/telegram/picker.qrc
 create mode 100644 Telegram/SourceFiles/core/current_geo_location.cpp
 create mode 100644 Telegram/SourceFiles/core/current_geo_location.h
 create mode 100644 Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp
 create mode 100644 Telegram/SourceFiles/data/raw/raw_countries_bounds.h
 create mode 100644 Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
 create mode 100644 Telegram/SourceFiles/platform/linux/current_geo_location_linux.h
 create mode 100644 Telegram/SourceFiles/platform/mac/current_geo_location_mac.h
 create mode 100644 Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
 create mode 100644 Telegram/SourceFiles/platform/platform_current_geo_location.h
 create mode 100644 Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
 create mode 100644 Telegram/SourceFiles/platform/win/current_geo_location_win.h
 create mode 100644 Telegram/SourceFiles/ui/controls/location_picker.cpp
 create mode 100644 Telegram/SourceFiles/ui/controls/location_picker.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 6528a858e..6f3bc7a00 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1614,6 +1614,7 @@ PRIVATE
     qrc/telegram/animations.qrc
     qrc/telegram/export.qrc
     qrc/telegram/iv.qrc
+    qrc/telegram/picker.qrc
     qrc/telegram/telegram.qrc
     qrc/telegram/sounds.qrc
     winrc/Telegram.rc
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 5c461c1ef..122260d22 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3194,6 +3194,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_unread_bar_some" = "Unread messages";
 
 "lng_maps_point" = "Location";
+"lng_maps_point_send" = "Send This Location";
 "lng_live_location" = "Live Location";
 "lng_live_location_now" = "updated just now";
 "lng_live_location_minutes#one" = "updated {count} minute ago";
diff --git a/Telegram/Resources/picker_html/picker.css b/Telegram/Resources/picker_html/picker.css
new file mode 100644
index 000000000..0c791008d
--- /dev/null
+++ b/Telegram/Resources/picker_html/picker.css
@@ -0,0 +1,60 @@
+:root {
+	--font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, Segoe UI Variable Text, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, tahoma, arial, sans-serif;
+	--font-serif: Iowan Old Style, Apple Garamond, Baskerville, Georgia, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
+	--font-mono: Menlo, Cascadia Code, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
+}
+
+html {
+	width: 100%;
+	height: 100%;
+	padding: 0;
+	margin: 0;
+}
+
+body {
+	font-family: var(--font-sans);
+	font-size: 17px;
+	line-height: 25px;
+	width: 100%;
+	height: 100%;
+	padding: 0;
+	margin: 0;
+	background-color: var(--td-window-bg);
+	color: var(--td-window-fg);
+}
+
+html.custom_scroll ::-webkit-scrollbar {
+	border-radius: 5px !important;
+	border: 3px solid transparent !important;
+	background-color: var(--td-scroll-bg) !important;
+	background-clip: content-box !important;
+	width: 10px !important;
+}
+html.custom_scroll ::-webkit-scrollbar:hover {
+	background-color: var(--td-scroll-bg-over) !important;
+}
+html.custom_scroll ::-webkit-scrollbar-thumb {
+	border-radius: 5px !important;
+	border: 3px solid transparent !important;
+	background-color: var(--td-scroll-bar-bg) !important;
+	background-clip: content-box !important;
+}
+html.custom_scroll ::-webkit-scrollbar-thumb:hover {
+	background-color: var(--td-scroll-bar-bg-over) !important;
+}
+
+#map {
+	position: relative;
+	width: 100%;
+	height: 100%;
+}
+#marker {
+	pointer-events: none;
+	display: flex;
+	z-index: 2;
+	position: absolute;
+	width: 100%;
+	height: 100%;
+	justify-content: center;
+	align-items: center;
+}
diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js
new file mode 100644
index 000000000..6a35060f0
--- /dev/null
+++ b/Telegram/Resources/picker_html/picker.js
@@ -0,0 +1,80 @@
+var LocationPicker = {
+	startZoom: 14,
+	flySpeed: 2.4,
+	notify: function(message) {
+		if (window.external && window.external.invoke) {
+			window.external.invoke(JSON.stringify(message));
+		}
+	},
+	frameKeyDown: function (e) {
+		const keyW = (e.key === 'w')
+			|| (e.code === 'KeyW')
+			|| (e.keyCode === 87);
+		const keyQ = (e.key === 'q')
+			|| (e.code === 'KeyQ')
+			|| (e.keyCode === 81);
+		const keyM = (e.key === 'm')
+			|| (e.code === 'KeyM')
+			|| (e.keyCode === 77);
+		if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) {
+			e.preventDefault();
+			LocationPicker.notify({
+				event: 'keydown',
+				modifier: e.ctrlKey ? 'ctrl' : 'cmd',
+				key: keyW ? 'w' : keyQ ? 'q' : 'm',
+			});
+		} else if (e.key === 'Escape' || e.keyCode === 27) {
+			e.preventDefault();
+			LocationPicker.notify({
+				event: 'keydown',
+				key: 'escape',
+			});
+		}
+	},
+	updateStyles: function (styles) {
+		if (LocationPicker.styles !== styles) {
+			LocationPicker.styles = styles;
+			document.getElementsByTagName('html')[0].style = styles;
+		}
+	},
+	init: function (token, center, bounds) {
+		mapboxgl.accessToken = token;
+
+		var options = { container: 'map' };
+		if (center) {
+			center = [center[1], center[0]];
+			options.center = center;
+			options.zoom = LocationPicker.startZoom;
+		} else if (bounds) {
+			options.bounds = bounds;
+			center = new mapboxgl.LngLatBounds(bounds).getCenter();
+		} else {
+			center = [0, 0];
+		}
+		LocationPicker.map = new mapboxgl.Map(options);
+
+		const marker = new mapboxgl.Marker()
+			.setLngLat(center)
+			.addTo(LocationPicker.map);
+		const drop = document.getElementById('marker_drop');
+		const element = marker.getElement();
+		drop.innerHTML = element.innerHTML;
+		const offset = marker.getOffset();
+		drop.style.transform = 'translate(' + offset.x + 'px, ' + offset.y + 'px)';
+		marker.remove();
+	},
+	narrowTo: function (point) {
+		LocationPicker.map.flyTo({
+			center: [point[1], point[0]],
+			zoom: LocationPicker.startZoom,
+			speed: LocationPicker.flySpeed,
+		});
+	},
+	send: function () {
+		LocationPicker.notify({
+			event: 'send',
+			latitude: LocationPicker.map.getCenter().lat,
+			longitude: LocationPicker.map.getCenter().lng
+		});
+	}
+};
diff --git a/Telegram/Resources/qrc/telegram/picker.qrc b/Telegram/Resources/qrc/telegram/picker.qrc
new file mode 100644
index 000000000..10a810aa9
--- /dev/null
+++ b/Telegram/Resources/qrc/telegram/picker.qrc
@@ -0,0 +1,6 @@
+<RCC>
+  <qresource prefix="/picker">
+    <file alias="picker.css">../../picker_html/picker.css</file>
+    <file alias="picker.js">../../picker_html/picker.js</file>
+  </qresource>
+</RCC>
diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index ade419248..bf4d472fe 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -62,6 +62,85 @@ void InnerFillMessagePostFlags(
 	}
 }
 
+void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
+	const auto history = action.history;
+	const auto peer = history->peer;
+	const auto session = &history->session();
+	const auto api = &session->api();
+
+	action.clearDraft = false;
+	action.generateLocal = false;
+	api->sendAction(action);
+
+	const auto randomId = base::RandomValue<uint64>();
+
+	auto flags = NewMessageFlags(peer);
+	auto sendFlags = MTPmessages_SendMedia::Flags(0);
+	if (action.replyTo) {
+		flags |= MessageFlag::HasReplyInfo;
+		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
+	}
+	const auto anonymousPost = peer->amAnonymous();
+	const auto silentPost = ShouldSendSilent(peer, action.options);
+	InnerFillMessagePostFlags(action.options, peer, flags);
+	if (silentPost) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
+	}
+	const auto sendAs = action.options.sendAs;
+	const auto messageFromId = sendAs
+		? sendAs->id
+		: anonymousPost
+		? 0
+		: session->userPeerId();
+	if (sendAs) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
+	}
+	const auto messagePostAuthor = peer->isBroadcast()
+		? session->user()->name()
+		: QString();
+
+	if (action.options.scheduled) {
+		flags |= MessageFlag::IsOrWasScheduled;
+		sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
+	}
+	if (action.options.shortcutId) {
+		flags |= MessageFlag::ShortcutMessage;
+		sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
+	}
+	if (action.options.effectId) {
+		sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
+	}
+	if (action.options.invertCaption) {
+		flags |= MessageFlag::InvertMedia;
+		sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
+	}
+
+	auto &histories = history->owner().histories();
+	histories.sendPreparedMessage(
+		history,
+		action.replyTo,
+		randomId,
+		Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
+			MTP_flags(sendFlags),
+			peer->input,
+			Data::Histories::ReplyToPlaceholder(),
+			std::move(inputMedia),
+			MTPstring(),
+			MTP_long(randomId),
+			MTPReplyMarkup(),
+			MTPvector<MTPMessageEntity>(),
+			MTP_int(action.options.scheduled),
+			(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
+			Data::ShortcutIdToMTP(session, action.options.shortcutId),
+			MTP_long(action.options.effectId)
+		), [=](const MTPUpdates &result, const MTP::Response &response) {
+	}, [=](const MTP::Error &error, const MTP::Response &response) {
+		api->sendMessageFail(error, peer, randomId);
+	});
+
+	api->finishForwarding(action);
+}
+
 template <typename MediaData>
 void SendExistingMedia(
 		MessageToSend &&message,
@@ -362,6 +441,33 @@ bool SendDice(MessageToSend &message) {
 	return true;
 }
 
+void SendLocation(SendAction action, float64 lat, float64 lon) {
+	SendSimpleMedia(
+		action,
+		MTP_inputMediaGeoPoint(
+			MTP_inputGeoPoint(
+				MTP_flags(0),
+				MTP_double(lat),
+				MTP_double(lon),
+				MTPint()))); // accuracy_radius
+}
+
+void SendVenue(SendAction action, Data::InputVenue venue) {
+	SendSimpleMedia(
+		action,
+		MTP_inputMediaVenue(
+			MTP_inputGeoPoint(
+				MTP_flags(0),
+				MTP_double(venue.lat),
+				MTP_double(venue.lon),
+				MTPint()), // accuracy_radius
+			MTP_string(venue.title),
+			MTP_string(venue.address),
+			MTP_string(venue.provider),
+			MTP_string(venue.id),
+			MTP_string(venue.venueType)));
+}
+
 void FillMessagePostFlags(
 		const SendAction &action,
 		not_null<PeerData*> peer,
diff --git a/Telegram/SourceFiles/api/api_sending.h b/Telegram/SourceFiles/api/api_sending.h
index 2fdbad843..c4bafc537 100644
--- a/Telegram/SourceFiles/api/api_sending.h
+++ b/Telegram/SourceFiles/api/api_sending.h
@@ -7,15 +7,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
-namespace Main {
-class Session;
-} // namespace Main
-
 class History;
 class PhotoData;
 class DocumentData;
 struct FilePrepareResult;
 
+namespace Data {
+struct InputVenue;
+} // namespace Data
+
+namespace Main {
+class Session;
+} // namespace Main
+
 namespace Api {
 
 struct MessageToSend;
@@ -33,6 +37,13 @@ void SendExistingPhoto(
 
 bool SendDice(MessageToSend &message);
 
+// We can't create Data::LocationPoint() and use it
+// for a local sending message, because we can't request
+// map thumbnail in messages history without access hash.
+void SendLocation(SendAction action, float64 lat, float64 lon);
+
+void SendVenue(SendAction action, Data::InputVenue venue);
+
 void FillMessagePostFlags(
 	const SendAction &action,
 	not_null<PeerData*> peer,
diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp
new file mode 100644
index 000000000..295bde824
--- /dev/null
+++ b/Telegram/SourceFiles/core/current_geo_location.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 "core/current_geo_location.h"
+
+#include "base/platform/base_platform_info.h"
+#include "data/raw/raw_countries_bounds.h"
+#include "platform/platform_current_geo_location.h"
+
+namespace Core {
+
+GeoLocation ResolveCurrentCountryLocation() {
+	const auto iso2 = Platform::SystemCountry().toUpper();
+	const auto &bounds = Raw::CountryBounds();
+	const auto i = bounds.find(iso2);
+	if (i == end(bounds)) {
+		return {
+			.accuracy = GeoLocationAccuracy::Failed,
+		};
+	}
+	return {
+		.point = {
+			(i->second.minLat + i->second.maxLat) / 2.,
+			(i->second.minLon + i->second.maxLon) / 2.,
+		},
+		.bounds = {
+			i->second.minLat,
+			i->second.minLon,
+			i->second.maxLat - i->second.minLat,
+			i->second.maxLon - i->second.minLon,
+		},
+		.accuracy = GeoLocationAccuracy::Country,
+	};
+}
+
+void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback) {
+	using namespace Platform;
+	return ResolveCurrentExactLocation([done = std::move(callback)](
+		GeoLocation result) {
+		done(result.accuracy != GeoLocationAccuracy::Failed
+			? result
+			: ResolveCurrentCountryLocation());
+	});
+}
+
+} // namespace Core
diff --git a/Telegram/SourceFiles/core/current_geo_location.h b/Telegram/SourceFiles/core/current_geo_location.h
new file mode 100644
index 000000000..2715699ee
--- /dev/null
+++ b/Telegram/SourceFiles/core/current_geo_location.h
@@ -0,0 +1,41 @@
+/*
+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 Core {
+
+enum class GeoLocationAccuracy : uchar {
+	Exact,
+	Country,
+	Failed,
+};
+
+struct GeoLocation {
+	QPointF point;
+	QRectF bounds;
+	GeoLocationAccuracy accuracy = GeoLocationAccuracy::Failed;
+
+	[[nodiscard]] bool exact() const {
+		return accuracy == GeoLocationAccuracy::Exact;
+	}
+	[[nodiscard]] bool country() const {
+		return accuracy == GeoLocationAccuracy::Country;
+	}
+	[[nodiscard]] bool failed() const {
+		return accuracy == GeoLocationAccuracy::Failed;
+	}
+
+	explicit operator bool() const {
+		return !failed();
+	}
+};
+
+[[nodiscard]] GeoLocation ResolveCurrentCountryLocation();
+void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback);
+
+} // namespace Core
diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h
index a5e0090db..6fb00d550 100644
--- a/Telegram/SourceFiles/data/data_location.h
+++ b/Telegram/SourceFiles/data/data_location.h
@@ -45,6 +45,16 @@ private:
 
 };
 
+struct InputVenue {
+	float64 lat = 0.;
+	float64 lon = 0.;
+	QString title;
+	QString address;
+	QString provider;
+	QString id;
+	QString venueType;
+};
+
 [[nodiscard]] GeoPointLocation ComputeLocation(const LocationPoint &point);
 
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp b/Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp
new file mode 100644
index 000000000..d4e383121
--- /dev/null
+++ b/Telegram/SourceFiles/data/raw/raw_countries_bounds.cpp
@@ -0,0 +1,193 @@
+/*
+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/raw/raw_countries_bounds.h"
+
+// Source: https://github.com/sandstrom/country-bounding-boxes
+
+namespace Raw {
+
+const base::flat_map<QString, GeoBounds> &CountryBounds() {
+	static const auto result = base::flat_map<QString, GeoBounds>{
+		{ u"AF"_q, GeoBounds{ 60.53, 29.32, 75.16, 38.49 } },
+		{ u"AO"_q, GeoBounds{ 11.64, -17.93, 24.08, -4.44 } },
+		{ u"AL"_q, GeoBounds{ 19.3, 39.62, 21.02, 42.69 } },
+		{ u"AE"_q, GeoBounds{ 51.58, 22.5, 56.4, 26.06 } },
+		{ u"AR"_q, GeoBounds{ -73.42, -55.25, -53.63, -21.83 } },
+		{ u"AM"_q, GeoBounds{ 43.58, 38.74, 46.51, 41.25 } },
+		{ u"AQ"_q, GeoBounds{ -180.0, -90.0, 180.0, -63.27 } },
+		{ u"TF"_q, GeoBounds{ 68.72, -49.78, 70.56, -48.63 } },
+		{ u"AU"_q, GeoBounds{ 113.34, -43.63, 153.57, -10.67 } },
+		{ u"AT"_q, GeoBounds{ 9.48, 46.43, 16.98, 49.04 } },
+		{ u"AZ"_q, GeoBounds{ 44.79, 38.27, 50.39, 41.86 } },
+		{ u"BI"_q, GeoBounds{ 29.02, -4.5, 30.75, -2.35 } },
+		{ u"BE"_q, GeoBounds{ 2.51, 49.53, 6.16, 51.48 } },
+		{ u"BJ"_q, GeoBounds{ 0.77, 6.14, 3.8, 12.24 } },
+		{ u"BF"_q, GeoBounds{ -5.47, 9.61, 2.18, 15.12 } },
+		{ u"BD"_q, GeoBounds{ 88.08, 20.67, 92.67, 26.45 } },
+		{ u"BG"_q, GeoBounds{ 22.38, 41.23, 28.56, 44.23 } },
+		{ u"BS"_q, GeoBounds{ -78.98, 23.71, -77.0, 27.04 } },
+		{ u"BA"_q, GeoBounds{ 15.75, 42.65, 19.6, 45.23 } },
+		{ u"BY"_q, GeoBounds{ 23.2, 51.32, 32.69, 56.17 } },
+		{ u"BZ"_q, GeoBounds{ -89.23, 15.89, -88.11, 18.5 } },
+		{ u"BO"_q, GeoBounds{ -69.59, -22.87, -57.5, -9.76 } },
+		{ u"BR"_q, GeoBounds{ -73.99, -33.77, -34.73, 5.24 } },
+		{ u"BN"_q, GeoBounds{ 114.2, 4.01, 115.45, 5.45 } },
+		{ u"BT"_q, GeoBounds{ 88.81, 26.72, 92.1, 28.3 } },
+		{ u"BW"_q, GeoBounds{ 19.9, -26.83, 29.43, -17.66 } },
+		{ u"CF"_q, GeoBounds{ 14.46, 2.27, 27.37, 11.14 } },
+		{ u"CA"_q, GeoBounds{ -141.0, 41.68, -52.65, 73.23 } },
+		{ u"CH"_q, GeoBounds{ 6.02, 45.78, 10.44, 47.83 } },
+		{ u"CL"_q, GeoBounds{ -75.64, -55.61, -66.96, -17.58 } },
+		{ u"CN"_q, GeoBounds{ 73.68, 18.2, 135.03, 53.46 } },
+		{ u"CI"_q, GeoBounds{ -8.6, 4.34, -2.56, 10.52 } },
+		{ u"CM"_q, GeoBounds{ 8.49, 1.73, 16.01, 12.86 } },
+		{ u"CD"_q, GeoBounds{ 12.18, -13.26, 31.17, 5.26 } },
+		{ u"CG"_q, GeoBounds{ 11.09, -5.04, 18.45, 3.73 } },
+		{ u"CO"_q, GeoBounds{ -78.99, -4.3, -66.88, 12.44 } },
+		{ u"CR"_q, GeoBounds{ -85.94, 8.23, -82.55, 11.22 } },
+		{ u"CU"_q, GeoBounds{ -84.97, 19.86, -74.18, 23.19 } },
+		{ u"CY"_q, GeoBounds{ 32.26, 34.57, 34.0, 35.17 } },
+		{ u"CZ"_q, GeoBounds{ 12.24, 48.56, 18.85, 51.12 } },
+		{ u"DE"_q, GeoBounds{ 5.99, 47.3, 15.02, 54.98 } },
+		{ u"DJ"_q, GeoBounds{ 41.66, 10.93, 43.32, 12.7 } },
+		{ u"DK"_q, GeoBounds{ 8.09, 54.8, 12.69, 57.73 } },
+		{ u"DO"_q, GeoBounds{ -71.95, 17.6, -68.32, 19.88 } },
+		{ u"DZ"_q, GeoBounds{ -8.68, 19.06, 12.0, 37.12 } },
+		{ u"EC"_q, GeoBounds{ -80.97, -4.96, -75.23, 1.38 } },
+		{ u"EG"_q, GeoBounds{ 24.7, 22.0, 36.87, 31.59 } },
+		{ u"ER"_q, GeoBounds{ 36.32, 12.46, 43.08, 18.0 } },
+		{ u"ES"_q, GeoBounds{ -9.39, 35.95, 3.04, 43.75 } },
+		{ u"EE"_q, GeoBounds{ 23.34, 57.47, 28.13, 59.61 } },
+		{ u"ET"_q, GeoBounds{ 32.95, 3.42, 47.79, 14.96 } },
+		{ u"FI"_q, GeoBounds{ 20.65, 59.85, 31.52, 70.16 } },
+		{ u"FJ"_q, GeoBounds{ -180.0, -18.29, 180.0, -16.02 } },
+		{ u"FK"_q, GeoBounds{ -61.2, -52.3, -57.75, -51.1 } },
+		{ u"FR"_q, GeoBounds{ -5.0, 42.5, 9.56, 51.15 } },
+		{ u"GA"_q, GeoBounds{ 8.8, -3.98, 14.43, 2.33 } },
+		{ u"GB"_q, GeoBounds{ -7.57, 49.96, 1.68, 58.64 } },
+		{ u"GE"_q, GeoBounds{ 39.96, 41.06, 46.64, 43.55 } },
+		{ u"GH"_q, GeoBounds{ -3.24, 4.71, 1.06, 11.1 } },
+		{ u"GN"_q, GeoBounds{ -15.13, 7.31, -7.83, 12.59 } },
+		{ u"GM"_q, GeoBounds{ -16.84, 13.13, -13.84, 13.88 } },
+		{ u"GW"_q, GeoBounds{ -16.68, 11.04, -13.7, 12.63 } },
+		{ u"GQ"_q, GeoBounds{ 9.31, 1.01, 11.29, 2.28 } },
+		{ u"GR"_q, GeoBounds{ 20.15, 34.92, 26.6, 41.83 } },
+		{ u"GL"_q, GeoBounds{ -73.3, 60.04, -12.21, 83.65 } },
+		{ u"GT"_q, GeoBounds{ -92.23, 13.74, -88.23, 17.82 } },
+		{ u"GY"_q, GeoBounds{ -61.41, 1.27, -56.54, 8.37 } },
+		{ u"HN"_q, GeoBounds{ -89.35, 12.98, -83.15, 16.01 } },
+		{ u"HR"_q, GeoBounds{ 13.66, 42.48, 19.39, 46.5 } },
+		{ u"HT"_q, GeoBounds{ -74.46, 18.03, -71.62, 19.92 } },
+		{ u"HU"_q, GeoBounds{ 16.2, 45.76, 22.71, 48.62 } },
+		{ u"ID"_q, GeoBounds{ 95.29, -10.36, 141.03, 5.48 } },
+		{ u"IN"_q, GeoBounds{ 68.18, 7.97, 97.4, 35.49 } },
+		{ u"IE"_q, GeoBounds{ -9.98, 51.67, -6.03, 55.13 } },
+		{ u"IR"_q, GeoBounds{ 44.11, 25.08, 63.32, 39.71 } },
+		{ u"IQ"_q, GeoBounds{ 38.79, 29.1, 48.57, 37.39 } },
+		{ u"IS"_q, GeoBounds{ -24.33, 63.5, -13.61, 66.53 } },
+		{ u"IL"_q, GeoBounds{ 34.27, 29.5, 35.84, 33.28 } },
+		{ u"IT"_q, GeoBounds{ 6.75, 36.62, 18.48, 47.12 } },
+		{ u"JM"_q, GeoBounds{ -78.34, 17.7, -76.2, 18.52 } },
+		{ u"JO"_q, GeoBounds{ 34.92, 29.2, 39.2, 33.38 } },
+		{ u"JP"_q, GeoBounds{ 129.41, 31.03, 145.54, 45.55 } },
+		{ u"KZ"_q, GeoBounds{ 46.47, 40.66, 87.36, 55.39 } },
+		{ u"KE"_q, GeoBounds{ 33.89, -4.68, 41.86, 5.51 } },
+		{ u"KG"_q, GeoBounds{ 69.46, 39.28, 80.26, 43.3 } },
+		{ u"KH"_q, GeoBounds{ 102.35, 10.49, 107.61, 14.57 } },
+		{ u"KR"_q, GeoBounds{ 126.12, 34.39, 129.47, 38.61 } },
+		{ u"KW"_q, GeoBounds{ 46.57, 28.53, 48.42, 30.06 } },
+		{ u"LA"_q, GeoBounds{ 100.12, 13.88, 107.56, 22.46 } },
+		{ u"LB"_q, GeoBounds{ 35.13, 33.09, 36.61, 34.64 } },
+		{ u"LR"_q, GeoBounds{ -11.44, 4.36, -7.54, 8.54 } },
+		{ u"LY"_q, GeoBounds{ 9.32, 19.58, 25.16, 33.14 } },
+		{ u"LK"_q, GeoBounds{ 79.7, 5.97, 81.79, 9.82 } },
+		{ u"LS"_q, GeoBounds{ 27.0, -30.65, 29.33, -28.65 } },
+		{ u"LT"_q, GeoBounds{ 21.06, 53.91, 26.59, 56.37 } },
+		{ u"LU"_q, GeoBounds{ 5.67, 49.44, 6.24, 50.13 } },
+		{ u"LV"_q, GeoBounds{ 21.06, 55.62, 28.18, 57.97 } },
+		{ u"MA"_q, GeoBounds{ -17.02, 21.42, -1.12, 35.76 } },
+		{ u"MD"_q, GeoBounds{ 26.62, 45.49, 30.02, 48.47 } },
+		{ u"MG"_q, GeoBounds{ 43.25, -25.6, 50.48, -12.04 } },
+		{ u"MX"_q, GeoBounds{ -117.13, 14.54, -86.81, 32.72 } },
+		{ u"MK"_q, GeoBounds{ 20.46, 40.84, 22.95, 42.32 } },
+		{ u"ML"_q, GeoBounds{ -12.17, 10.1, 4.27, 24.97 } },
+		{ u"MM"_q, GeoBounds{ 92.3, 9.93, 101.18, 28.34 } },
+		{ u"ME"_q, GeoBounds{ 18.45, 41.88, 20.34, 43.52 } },
+		{ u"MN"_q, GeoBounds{ 87.75, 41.6, 119.77, 52.05 } },
+		{ u"MZ"_q, GeoBounds{ 30.18, -26.74, 40.78, -10.32 } },
+		{ u"MR"_q, GeoBounds{ -17.06, 14.62, -4.92, 27.4 } },
+		{ u"MW"_q, GeoBounds{ 32.69, -16.8, 35.77, -9.23 } },
+		{ u"MY"_q, GeoBounds{ 100.09, 0.77, 119.18, 6.93 } },
+		{ u"NA"_q, GeoBounds{ 11.73, -29.05, 25.08, -16.94 } },
+		{ u"NC"_q, GeoBounds{ 164.03, -22.4, 167.12, -20.11 } },
+		{ u"NE"_q, GeoBounds{ 0.3, 11.66, 15.9, 23.47 } },
+		{ u"NG"_q, GeoBounds{ 2.69, 4.24, 14.58, 13.87 } },
+		{ u"NI"_q, GeoBounds{ -87.67, 10.73, -83.15, 15.02 } },
+		{ u"NL"_q, GeoBounds{ 3.31, 50.8, 7.09, 53.51 } },
+		{ u"NO"_q, GeoBounds{ 4.99, 58.08, 31.29, 70.92 } },
+		{ u"NP"_q, GeoBounds{ 80.09, 26.4, 88.17, 30.42 } },
+		{ u"NZ"_q, GeoBounds{ 166.51, -46.64, 178.52, -34.45 } },
+		{ u"OM"_q, GeoBounds{ 52.0, 16.65, 59.81, 26.4 } },
+		{ u"PK"_q, GeoBounds{ 60.87, 23.69, 77.84, 37.13 } },
+		{ u"PA"_q, GeoBounds{ -82.97, 7.22, -77.24, 9.61 } },
+		{ u"PE"_q, GeoBounds{ -81.41, -18.35, -68.67, -0.06 } },
+		{ u"PH"_q, GeoBounds{ 117.17, 5.58, 126.54, 18.51 } },
+		{ u"PG"_q, GeoBounds{ 141.0, -10.65, 156.02, -2.5 } },
+		{ u"PL"_q, GeoBounds{ 14.07, 49.03, 24.03, 54.85 } },
+		{ u"PR"_q, GeoBounds{ -67.24, 17.95, -65.59, 18.52 } },
+		{ u"KP"_q, GeoBounds{ 124.27, 37.67, 130.78, 42.99 } },
+		{ u"PT"_q, GeoBounds{ -9.53, 36.84, -6.39, 42.28 } },
+		{ u"PY"_q, GeoBounds{ -62.69, -27.55, -54.29, -19.34 } },
+		{ u"QA"_q, GeoBounds{ 50.74, 24.56, 51.61, 26.11 } },
+		{ u"RO"_q, GeoBounds{ 20.22, 43.69, 29.63, 48.22 } },
+		{ u"RU"_q, GeoBounds{ -180.0, 41.15, 180.0, 81.25 } },
+		{ u"RW"_q, GeoBounds{ 29.02, -2.92, 30.82, -1.13 } },
+		{ u"SA"_q, GeoBounds{ 34.63, 16.35, 55.67, 32.16 } },
+		{ u"SD"_q, GeoBounds{ 21.94, 8.62, 38.41, 22.0 } },
+		{ u"SS"_q, GeoBounds{ 23.89, 3.51, 35.3, 12.25 } },
+		{ u"SN"_q, GeoBounds{ -17.63, 12.33, -11.47, 16.6 } },
+		{ u"SB"_q, GeoBounds{ 156.49, -10.83, 162.4, -6.6 } },
+		{ u"SL"_q, GeoBounds{ -13.25, 6.79, -10.23, 10.05 } },
+		{ u"SV"_q, GeoBounds{ -90.1, 13.15, -87.72, 14.42 } },
+		{ u"SO"_q, GeoBounds{ 40.98, -1.68, 51.13, 12.02 } },
+		{ u"RS"_q, GeoBounds{ 18.83, 42.25, 22.99, 46.17 } },
+		{ u"SR"_q, GeoBounds{ -58.04, 1.82, -53.96, 6.03 } },
+		{ u"SK"_q, GeoBounds{ 16.88, 47.76, 22.56, 49.57 } },
+		{ u"SI"_q, GeoBounds{ 13.7, 45.45, 16.56, 46.85 } },
+		{ u"SE"_q, GeoBounds{ 11.03, 55.36, 23.9, 69.11 } },
+		{ u"SZ"_q, GeoBounds{ 30.68, -27.29, 32.07, -25.66 } },
+		{ u"SY"_q, GeoBounds{ 35.7, 32.31, 42.35, 37.23 } },
+		{ u"TD"_q, GeoBounds{ 13.54, 7.42, 23.89, 23.41 } },
+		{ u"TG"_q, GeoBounds{ -0.05, 5.93, 1.87, 11.02 } },
+		{ u"TH"_q, GeoBounds{ 97.38, 5.69, 105.59, 20.42 } },
+		{ u"TJ"_q, GeoBounds{ 67.44, 36.74, 74.98, 40.96 } },
+		{ u"TM"_q, GeoBounds{ 52.5, 35.27, 66.55, 42.75 } },
+		{ u"TL"_q, GeoBounds{ 124.97, -9.39, 127.34, -8.27 } },
+		{ u"TT"_q, GeoBounds{ -61.95, 10.0, -60.9, 10.89 } },
+		{ u"TN"_q, GeoBounds{ 7.52, 30.31, 11.49, 37.35 } },
+		{ u"TR"_q, GeoBounds{ 26.04, 35.82, 44.79, 42.14 } },
+		{ u"TW"_q, GeoBounds{ 120.11, 21.97, 121.95, 25.3 } },
+		{ u"TZ"_q, GeoBounds{ 29.34, -11.72, 40.32, -0.95 } },
+		{ u"UG"_q, GeoBounds{ 29.58, -1.44, 35.04, 4.25 } },
+		{ u"UA"_q, GeoBounds{ 22.09, 44.36, 40.08, 52.34 } },
+		{ u"UY"_q, GeoBounds{ -58.43, -34.95, -53.21, -30.11 } },
+		{ u"US"_q, GeoBounds{ -125.0, 25.0, -66.96, 49.5 } },
+		{ u"UZ"_q, GeoBounds{ 55.93, 37.14, 73.06, 45.59 } },
+		{ u"VE"_q, GeoBounds{ -73.3, 0.72, -59.76, 12.16 } },
+		{ u"VN"_q, GeoBounds{ 102.17, 8.6, 109.34, 23.35 } },
+		{ u"VU"_q, GeoBounds{ 166.63, -16.6, 167.84, -14.63 } },
+		{ u"PS"_q, GeoBounds{ 34.93, 31.35, 35.55, 32.53 } },
+		{ u"YE"_q, GeoBounds{ 42.6, 12.59, 53.11, 19.0 } },
+		{ u"ZA"_q, GeoBounds{ 16.34, -34.82, 32.83, -22.09 } },
+		{ u"ZM"_q, GeoBounds{ 21.89, -17.96, 33.49, -8.24 } },
+		{ u"ZW"_q, GeoBounds{ 25.26, -22.27, 32.85, -15.51 } }
+	};
+	return result;
+}
+
+} // namespace Raw
diff --git a/Telegram/SourceFiles/data/raw/raw_countries_bounds.h b/Telegram/SourceFiles/data/raw/raw_countries_bounds.h
new file mode 100644
index 000000000..4f0944c81
--- /dev/null
+++ b/Telegram/SourceFiles/data/raw/raw_countries_bounds.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
+
+#include <QtCore/QString>
+
+namespace Raw {
+
+struct GeoBounds {
+	double minLat = 0.;
+	double minLon = 0.;
+	double maxLat = 0.;
+	double maxLon = 0.;
+};
+
+[[nodiscard]] const base::flat_map<QString, GeoBounds> &CountryBounds();
+
+} // namespace Raw
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index ff2637011..c6b013956 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -9,9 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "api/api_blocked_peers.h"
 #include "api/api_common.h"
+#include "api/api_sending.h"
 #include "base/qthelp_url.h"
 #include "boxes/share_box.h"
 #include "core/click_handler_types.h"
+#include "core/shortcuts.h"
 #include "data/data_bot_app.h"
 #include "data/data_changes.h"
 #include "data/data_user.h"
@@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "iv/iv_instance.h"
 #include "ui/boxes/confirm_box.h"
 #include "ui/chat/attach/attach_bot_webview.h"
+#include "ui/controls/location_picker.h"
 #include "ui/widgets/checkbox.h"
 #include "ui/widgets/dropdown_menu.h"
 #include "ui/widgets/popup_menu.h"
@@ -155,6 +158,10 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
 	return result;
 }
 
+[[nodiscard]] QString ResolveMapsToken(not_null<Main::Session*> session) {
+	return u""_q;
+}
+
 void ShowChooseBox(
 		not_null<Window::SessionController*> controller,
 		PeerTypes types,
@@ -1793,6 +1800,21 @@ void AttachWebView::toggleInMenu(
 	}).send();
 }
 
+void ChooseAndSendLocation(
+		not_null<Window::SessionController*> controller,
+		Api::SendAction action) {
+	const auto callback = [=](Ui::LocationInfo info) {
+		Api::SendLocation(action, info.lat, info.lon);
+	};
+	Ui::LocationPicker::Show({
+		.parent = controller->widget(),
+		.callback = crl::guard(controller, callback),
+		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
+		.storageId = controller->session().local().resolveStorageIdBots(),
+		.closeRequests = controller->content()->death(),
+	});
+}
+
 std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
 		not_null<QWidget*> parent,
 		not_null<Window::SessionController*> controller,
@@ -1847,6 +1869,14 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
 				{ sendMenuType });
 		}, &st::menuIconCreatePoll);
 	}
+	const auto session = &controller->session();
+	const auto locationType = ChatRestriction::SendOther;
+	if (Data::CanSendAnyOf(peer, locationType)
+		&& Ui::LocationPicker::Available(ResolveMapsToken(session))) {
+		raw->addAction(tr::lng_maps_point(tr::now), [=] {
+			ChooseAndSendLocation(controller, actionFactory());
+		}, &st::menuIconAddress);
+	}
 	for (const auto &bot : bots->attachBots()) {
 		if (!bot.inAttachMenu
 			|| !PeerMatchesTypes(peer, bot.user, bot.types)) {
diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
new file mode 100644
index 000000000..5ac0c49a7
--- /dev/null
+++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.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 "platform/linux/current_geo_location_linux.h"
+
+#include "core/current_geo_location.h"
+
+namespace Platform {
+
+void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
+	callback({});
+}
+
+} // namespace Platform
diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.h b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.h
new file mode 100644
index 000000000..6bb819543
--- /dev/null
+++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.h
@@ -0,0 +1,10 @@
+/*
+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 "platform/platform_current_geo_location.h"
diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.h b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.h
new file mode 100644
index 000000000..6bb819543
--- /dev/null
+++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.h
@@ -0,0 +1,10 @@
+/*
+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 "platform/platform_current_geo_location.h"
diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
new file mode 100644
index 000000000..03bf727ed
--- /dev/null
+++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
@@ -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 "platform/mac/current_geo_location_mac.h"
+
+#include "core/current_geo_location.h"
+
+namespace Platform {
+
+void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
+	callback({});
+}
+
+} // namespace Platform
diff --git a/Telegram/SourceFiles/platform/platform_current_geo_location.h b/Telegram/SourceFiles/platform/platform_current_geo_location.h
new file mode 100644
index 000000000..245342fc3
--- /dev/null
+++ b/Telegram/SourceFiles/platform/platform_current_geo_location.h
@@ -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
+*/
+#pragma once
+
+namespace Core {
+struct GeoLocation;
+} // namespace Core
+
+namespace Platform {
+
+void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback);
+
+} // namespace Platform
diff --git a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
new file mode 100644
index 000000000..37682c108
--- /dev/null
+++ b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
@@ -0,0 +1,59 @@
+/*
+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 "platform/win/current_geo_location_win.h"
+
+#include "base/platform/win/base_windows_winrt.h"
+#include "core/current_geo_location.h"
+
+#include <winrt/Windows.Devices.Geolocation.h>
+#include <winrt/Windows.Foundation.h>
+
+namespace Platform {
+
+void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
+	using namespace winrt::Windows::Foundation;
+	using namespace winrt::Windows::Devices::Geolocation;
+
+	const auto success = base::WinRT::Try([&] {
+		Geolocator geolocator;
+		geolocator.DesiredAccuracy(PositionAccuracy::High);
+		if (geolocator.LocationStatus() == PositionStatus::NotAvailable) {
+			callback({});
+			return;
+		}
+		geolocator.GetGeopositionAsync().Completed([=](
+				IAsyncOperation<Geoposition> that,
+				AsyncStatus status) {
+			if (status != AsyncStatus::Completed) {
+				crl::on_main([=] {
+					callback({});
+				});
+				return;
+			}
+			const auto point = base::WinRT::Try([&] {
+				const auto coordinate = that.GetResults().Coordinate();
+				return coordinate.Point().Position();
+			});
+			crl::on_main([=] {
+				if (!point) {
+					callback({});
+				} else {
+					callback({
+						.point = { point->Latitude, point->Longitude },
+						.accuracy = Core::GeoLocationAccuracy::Exact,
+					});
+				}
+			});
+		});
+	});
+	if (!success) {
+		callback({});
+	}
+}
+
+} // namespace Platform
diff --git a/Telegram/SourceFiles/platform/win/current_geo_location_win.h b/Telegram/SourceFiles/platform/win/current_geo_location_win.h
new file mode 100644
index 000000000..6bb819543
--- /dev/null
+++ b/Telegram/SourceFiles/platform/win/current_geo_location_win.h
@@ -0,0 +1,10 @@
+/*
+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 "platform/platform_current_geo_location.h"
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
new file mode 100644
index 000000000..21c5b9861
--- /dev/null
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -0,0 +1,350 @@
+/*
+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/controls/location_picker.h"
+
+#include "base/platform/base_platform_info.h"
+#include "core/current_geo_location.h"
+#include "lang/lang_keys.h"
+#include "ui/widgets/rp_window.h"
+#include "ui/widgets/buttons.h"
+#include "webview/webview_data_stream_memory.h"
+#include "webview/webview_embed.h"
+#include "webview/webview_interface.h"
+#include "styles/style_dialogs.h"
+#include "styles/style_window.h"
+
+#include <QtCore/QJsonDocument>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonValue>
+#include <QtGui/QGuiApplication>
+#include <QtGui/QScreen>
+
+namespace Ui {
+namespace {
+
+Core::GeoLocation LastExactLocation;
+QString MapsProviderToken;
+
+[[nodiscard]] QByteArray DefaultCenter() {
+	if (!LastExactLocation) {
+		return "null";
+	}
+	return "["_q
+		+ QByteArray::number(LastExactLocation.point.x() - 1)
+		+ ","_q
+		+ QByteArray::number(LastExactLocation.point.y() - 1)
+		+ "]"_q;
+}
+
+[[nodiscard]] QByteArray DefaultBounds() {
+	const auto country = Core::ResolveCurrentCountryLocation();
+	if (!country) {
+		return "null";
+	}
+	return "[["_q
+		+ QByteArray::number(country.bounds.x())
+		+ ","_q
+		+ QByteArray::number(country.bounds.y())
+		+ "],["_q
+		+ QByteArray::number(country.bounds.x() + country.bounds.width())
+		+ ","_q
+		+ QByteArray::number(country.bounds.y() + country.bounds.height())
+		+ "]]"_q;
+}
+
+[[nodiscard]] QByteArray ComputeStyles() {
+	return "";
+}
+
+[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value) {
+	return value
+		.replace('&', "&amp;")
+		.replace('"', "&quot;")
+		.replace('\'', "&#039;")
+		.replace('<', "&lt;")
+		.replace('>', "&gt;");
+}
+
+[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) {
+	return value
+		.replace('\\', "\\\\")
+		.replace('"', "\\\"")
+		.replace('\'', "\\\'");
+}
+
+[[nodiscard]] QByteArray ReadResource(const QString &name) {
+	auto file = QFile(u":/picker/"_q + name);
+	return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
+}
+
+[[nodiscard]] QByteArray PickerContent() {
+	return R"(<!DOCTYPE html>
+<html style=")"
++ EscapeForAttribute(ComputeStyles())
++ R"(">
+	<head>
+		<meta charset="utf-8">
+		<meta name="robots" content="noindex, nofollow">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<script src="/location/picker.js"></script>
+		<link rel="stylesheet" href="/location/picker.css" />
+		<script src='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.js'></script>
+		<link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />
+	</head>
+	<body>
+		<div id="marker"><div id="marker_drop"></div></div>
+		<div id="map"></div>
+		<script>LocationPicker.notify({ event: 'ready' });</script>
+	</body>
+</html>
+)"_q;
+}
+
+} // namespace
+
+LocationPicker::LocationPicker(Descriptor &&descriptor)
+: _callback(std::move(descriptor.callback))
+, _quit(std::move(descriptor.quit))
+, _window(std::make_unique<RpWindow>())
+, _updateStyles([=] {
+	const auto str = EscapeForScriptString(ComputeStyles());
+	if (_webview) {
+		_webview->eval("IV.updateStyles('" + str + "');");
+	}
+}) {
+	std::move(
+		descriptor.closeRequests
+	) | rpl::start_with_next([=] {
+		_window = nullptr;
+		delete this;
+	}, _lifetime);
+
+	setup(descriptor);
+}
+
+bool LocationPicker::Available(const QString &token) {
+	static const auto Supported = Webview::NavigateToDataSupported();
+	MapsProviderToken = token;
+	return Supported && !MapsProviderToken.isEmpty();
+}
+
+void LocationPicker::setup(const Descriptor &descriptor) {
+	setupWindow(descriptor);
+	setupWebview(descriptor);
+}
+
+void LocationPicker::setupWindow(const Descriptor &descriptor) {
+	const auto window = _window.get();
+
+	const auto parent = descriptor.parent
+		? descriptor.parent->window()->geometry()
+		: QGuiApplication::primaryScreen()->availableGeometry();
+	window->setGeometry(QRect(
+		parent.x() + (parent.width() - st::windowMinHeight) / 2,
+		parent.y() + (parent.height() - st::windowMinWidth) / 2,
+		st::windowMinHeight,
+		st::windowMinWidth));
+	window->setMinimumSize({ st::windowMinHeight, st::windowMinWidth });
+
+	_container = Ui::CreateChild<Ui::RpWidget>(window->body().get());
+	const auto button = Ui::CreateChild<FlatButton>(
+		window->body(),
+		tr::lng_maps_point_send(tr::now),
+		st::dialogsUpdateButton);
+	button->show();
+	button->setClickedCallback([=] {
+		_webview->eval("LocationPicker.send();");
+	});
+	window->body()->sizeValue(
+	) | rpl::start_with_next([=](QSize size) {
+		_container->setGeometry(QRect(QPoint(), size).marginsRemoved(
+			{ 0, 0, 0, button->height() }));
+		button->resizeToWidth(size.width());
+		button->setGeometry(
+			0,
+			size.height() - button->height(),
+			button->width(),
+			button->height());
+	}, _container->lifetime());
+
+	_container->paintRequest() | rpl::start_with_next([=](QRect clip) {
+		QPainter(_container).fillRect(clip, st::windowBg);
+	}, _container->lifetime());
+
+	_container->show();
+	window->show();
+}
+
+void LocationPicker::setupWebview(const Descriptor &descriptor) {
+	Expects(!_webview);
+
+	const auto window = _window.get();
+	_webview = std::make_unique<Webview::Window>(
+		_container,
+		Webview::WindowConfig{
+			.opaqueBg = st::windowBg->c,
+			.storageId = descriptor.storageId,
+		});
+	const auto raw = _webview.get();
+
+	window->lifetime().add([=] {
+		_webview = nullptr;
+	});
+
+	window->events(
+	) | rpl::start_with_next([=](not_null<QEvent*> e) {
+		if (e->type() == QEvent::Close) {
+			close();
+		} else if (e->type() == QEvent::KeyPress) {
+			const auto event = static_cast<QKeyEvent*>(e.get());
+			if (event->key() == Qt::Key_Escape) {
+				close();
+			}
+		}
+	}, window->lifetime());
+	raw->widget()->show();
+
+	_container->sizeValue(
+	) | rpl::start_with_next([=](QSize size) {
+		raw->widget()->setGeometry(QRect(QPoint(), size));
+	}, _container->lifetime());
+
+	raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
+		return true;
+	});
+	raw->setNavigationDoneHandler([=](bool success) {
+	});
+	raw->setMessageHandler([=](const QJsonDocument &message) {
+		crl::on_main(_window.get(), [=] {
+			const auto object = message.object();
+			const auto event = object.value("event").toString();
+			if (event == u"ready"_q) {
+				initMap();
+				resolveCurrentLocation();
+			} else if (event == u"keydown"_q) {
+				const auto key = object.value("key").toString();
+				const auto modifier = object.value("modifier").toString();
+				processKey(key, modifier);
+			} else if (event == u"send"_q) {
+				const auto lat = object.value("latitude").toDouble();
+				const auto lon = object.value("longitude").toDouble();
+				_callback({ lat, lon });
+				close();
+			}
+		});
+	});
+	raw->setDataRequestHandler([=](Webview::DataRequest request) {
+		const auto pos = request.id.find('#');
+		if (pos != request.id.npos) {
+			request.id = request.id.substr(0, pos);
+		}
+		if (!request.id.starts_with("location/")) {
+			return Webview::DataResult::Failed;
+		}
+		const auto finishWith = [&](QByteArray data, std::string mime) {
+			request.done({
+				.stream = std::make_unique<Webview::DataStreamFromMemory>(
+					std::move(data),
+					std::move(mime)),
+				});
+			return Webview::DataResult::Done;
+		};
+		if (!_subscribedToColors) {
+			_subscribedToColors = true;
+
+			rpl::merge(
+				Lang::Updated(),
+				style::PaletteChanged()
+			) | rpl::start_with_next([=] {
+				_updateStyles.call();
+			}, _webview->lifetime());
+		}
+		const auto id = std::string_view(request.id).substr(9);
+		if (id == "picker.html") {
+			return finishWith(PickerContent(), "text/html; charset=utf-8");
+		}
+		const auto css = id.ends_with(".css");
+		const auto js = !css && id.ends_with(".js");
+		if (!css && !js) {
+			return Webview::DataResult::Failed;
+		}
+		const auto qstring = QString::fromUtf8(id.data(), id.size());
+		const auto pattern = u"^[a-zA-Z\\.\\-_0-9]+$"_q;
+		if (QRegularExpression(pattern).match(qstring).hasMatch()) {
+			const auto bytes = ReadResource(qstring);
+			if (!bytes.isEmpty()) {
+				const auto mime = css ? "text/css" : "text/javascript";
+				return finishWith(bytes, mime);
+			}
+		}
+		return Webview::DataResult::Failed;
+	});
+
+	raw->init(R"()");
+	raw->navigateToData("location/picker.html");
+}
+
+void LocationPicker::initMap() {
+	const auto token = MapsProviderToken.toUtf8();
+	const auto center = DefaultCenter();
+	const auto bounds = DefaultBounds();
+	const auto arguments = "'" + token + "', " + center + ", " + bounds;
+	_webview->eval("LocationPicker.init(" + arguments + ");");
+}
+
+void LocationPicker::resolveCurrentLocation() {
+	using namespace Core;
+	const auto window = _window.get();
+	ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
+		if (location) {
+			LastExactLocation = location;
+		}
+		if (_webview && location.accuracy == GeoLocationAccuracy::Exact) {
+			const auto point = QByteArray::number(location.point.x())
+				+ ","_q
+				+ QByteArray::number(location.point.y());
+			_webview->eval("LocationPicker.narrowTo([" + point + "]);");
+		}
+	}));
+}
+
+void LocationPicker::processKey(
+		const QString &key,
+		const QString &modifier) {
+	const auto ctrl = ::Platform::IsMac() ? u"cmd"_q : u"ctrl"_q;
+	if (key == u"escape"_q || (key == u"w"_q && modifier == ctrl)) {
+		close();
+	} else if (key == u"m"_q && modifier == ctrl) {
+		minimize();
+	} else if (key == u"q"_q && modifier == ctrl) {
+		quit();
+	}
+}
+
+void LocationPicker::close() {
+	_window->close();
+}
+
+void LocationPicker::minimize() {
+	if (_window) {
+		_window->setWindowState(_window->windowState()
+			| Qt::WindowMinimized);
+	}
+}
+
+void LocationPicker::quit() {
+	if (const auto onstack = _quit) {
+		onstack();
+	}
+}
+
+not_null<LocationPicker*> LocationPicker::Show(Descriptor &&descriptor) {
+	return new LocationPicker(std::move(descriptor));
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
new file mode 100644
index 000000000..220c0c426
--- /dev/null
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -0,0 +1,67 @@
+/*
+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/invoke_queued.h"
+#include "base/weak_ptr.h"
+#include "webview/webview_common.h"
+
+namespace Webview {
+class Window;
+} // namespace Webview
+
+namespace Ui {
+
+class RpWindow;
+class RpWidget;
+
+struct LocationInfo {
+	float64 lat = 0.;
+	float64 lon = 0.;
+};
+
+class LocationPicker final : public base::has_weak_ptr {
+public:
+	struct Descriptor {
+		RpWidget *parent = nullptr;
+		Fn<void(LocationInfo)> callback;
+		Fn<void()> quit;
+		Webview::StorageId storageId;
+		rpl::producer<> closeRequests;
+	};
+
+	[[nodiscard]] static bool Available(const QString &token);
+	static not_null<LocationPicker*> Show(Descriptor &&descriptor);
+
+	void close();
+	void minimize();
+	void quit();
+
+private:
+	explicit LocationPicker(Descriptor &&descriptor);
+
+	void setup(const Descriptor &descriptor);
+	void setupWindow(const Descriptor &descriptor);
+	void setupWebview(const Descriptor &descriptor);
+	void processKey(const QString &key, const QString &modifier);
+	void resolveCurrentLocation();
+	void initMap();
+
+	rpl::lifetime _lifetime;
+
+	Fn<void(LocationInfo)> _callback;
+	Fn<void()> _quit;
+	std::unique_ptr<RpWindow> _window;
+	RpWidget *_container = nullptr;
+	std::unique_ptr<Webview::Window> _webview;
+	SingleQueuedInvokation _updateStyles;
+	bool _subscribedToColors = false;
+
+};
+
+} // namespace Ui
diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake
index 3555c9218..b11016acb 100644
--- a/Telegram/cmake/td_ui.cmake
+++ b/Telegram/cmake/td_ui.cmake
@@ -70,6 +70,8 @@ PRIVATE
     chat_helpers/stickers_emoji_image_loader.cpp
     chat_helpers/stickers_emoji_image_loader.h
 
+    core/current_geo_location.cpp
+    core/current_geo_location.h
     core/file_location.cpp
     core/file_location.h
     core/mime_type.cpp
@@ -78,6 +80,8 @@ PRIVATE
     countries/countries_instance.cpp
     countries/countries_instance.h
 
+    data/raw/raw_countries_bounds.cpp
+    data/raw/raw_countries_bounds.h
     data/data_birthday.cpp
     data/data_birthday.h
     data/data_channel_earn.h
@@ -194,9 +198,16 @@ PRIVATE
     payments/ui/payments_panel_data.h
     payments/ui/payments_panel_delegate.h
 
+    platform/linux/current_geo_location_linux.cpp
+    platform/linux/current_geo_location_linux.h
     platform/mac/file_bookmark_mac.h
     platform/mac/file_bookmark_mac.mm
+    platform/mac/current_geo_location_mac.h
+    platform/mac/current_geo_location_mac.mm
+    platform/win/current_geo_location_win.cpp
+    platform/win/current_geo_location_win.h
     platform/platform_file_bookmark.h
+    platform/platform_current_geo_location.h
 
     settings/settings_common.cpp
     settings/settings_common.h
@@ -344,6 +355,8 @@ PRIVATE
     ui/controls/invite_link_buttons.h
     ui/controls/invite_link_label.cpp
     ui/controls/invite_link_label.h
+    ui/controls/location_picker.cpp
+    ui/controls/location_picker.h
     ui/controls/peer_list_dummy.cpp
     ui/controls/peer_list_dummy.h
     ui/controls/send_as_button.cpp
@@ -439,6 +452,12 @@ PRIVATE
     ui/ui_pch.h
 )
 
+nice_target_sources(td_ui ${res_loc}
+PRIVATE
+    picker_html/picker.css
+    picker_html/picker.js
+)
+
 if (DESKTOP_APP_SPECIAL_TARGET)
     remove_target_sources(td_ui ${src_loc}
         ui/controls/window_outdated_bar_dummy.cpp

From 025ab40687f9a0f40f6058358afd318f9522ce7c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 5 Jul 2024 21:25:25 +0400
Subject: [PATCH 036/163] Implement location sending on macOS.

---
 Telegram/Resources/picker_html/picker.js      |  14 ++-
 .../SourceFiles/core/current_geo_location.cpp |   2 +-
 .../platform/mac/current_geo_location_mac.mm  | 103 +++++++++++++++++-
 .../ui/controls/location_picker.cpp           |  34 ++++--
 Telegram/Telegram.plist                       |  14 ++-
 Telegram/Telegram/Telegram Lite.entitlements  |   2 +
 Telegram/Telegram/Telegram.entitlements       |   2 +
 cmake                                         |   2 +-
 8 files changed, 155 insertions(+), 18 deletions(-)

diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js
index 6a35060f0..54eb60aba 100644
--- a/Telegram/Resources/picker_html/picker.js
+++ b/Telegram/Resources/picker_html/picker.js
@@ -37,17 +37,21 @@ var LocationPicker = {
 			document.getElementsByTagName('html')[0].style = styles;
 		}
 	},
-	init: function (token, center, bounds) {
-		mapboxgl.accessToken = token;
+	init: function (params) {
+		mapboxgl.accessToken = params.token;
+		if (params.protocol) {
+			mapboxgl.config.API_URL = params.protocol + '://domain/api.mapbox.com';
+		}
 
 		var options = { container: 'map' };
+		var center = params.center;
 		if (center) {
 			center = [center[1], center[0]];
 			options.center = center;
 			options.zoom = LocationPicker.startZoom;
-		} else if (bounds) {
-			options.bounds = bounds;
-			center = new mapboxgl.LngLatBounds(bounds).getCenter();
+		} else if (params.bounds) {
+			options.bounds = params.bounds;
+			center = new mapboxgl.LngLatBounds(params.bounds).getCenter();
 		} else {
 			center = [0, 0];
 		}
diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp
index 295bde824..eb2125bf5 100644
--- a/Telegram/SourceFiles/core/current_geo_location.cpp
+++ b/Telegram/SourceFiles/core/current_geo_location.cpp
@@ -40,7 +40,7 @@ GeoLocation ResolveCurrentCountryLocation() {
 void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback) {
 	using namespace Platform;
 	return ResolveCurrentExactLocation([done = std::move(callback)](
-		GeoLocation result) {
+			GeoLocation result) {
 		done(result.accuracy != GeoLocationAccuracy::Failed
 			? result
 			: ResolveCurrentCountryLocation());
diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
index 03bf727ed..2812a0c33 100644
--- a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
+++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
@@ -9,10 +9,111 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "core/current_geo_location.h"
 
+#include <CoreLocation/CoreLocation.h>
+
+@interface LocationDelegate : NSObject<CLLocationManagerDelegate>
+
+- (id) initWithCallback:(Fn<void(Core::GeoLocation)>)callback;
+- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations;
+- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error;
+- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status;
+- (void) dealloc;
+
+@end
+
+@implementation LocationDelegate {
+CLLocationManager *_manager;
+Fn<void(Core::GeoLocation)> _callback;
+}
+
+- (void) fail {
+	[_manager stopUpdatingLocation];
+
+	const auto onstack = _callback;
+	[self release];
+
+	onstack({});
+}
+
+- (void) processWithStatus:(CLAuthorizationStatus)status {
+	switch (status) {
+	case kCLAuthorizationStatusNotDetermined:
+		if (@available(macOS 10.15, *)) {
+			[_manager requestWhenInUseAuthorization];
+		} else {
+			[_manager startUpdatingLocation];
+		}
+		break;
+	case kCLAuthorizationStatusAuthorizedAlways:
+		[_manager startUpdatingLocation];
+		return;
+	case kCLAuthorizationStatusRestricted:
+	case kCLAuthorizationStatusDenied:
+	default:
+		[self fail];
+		return;
+	}
+}
+
+- (id) initWithCallback:(Fn<void(Core::GeoLocation)>)callback {
+	if (self = [super init]) {
+		_callback = std::move(callback);
+		_manager = [[CLLocationManager alloc] init];
+		_manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
+		_manager.delegate = self;
+		if ([CLLocationManager locationServicesEnabled]) {
+			if (@available(macOS 11, *)) {
+				[self processWithStatus:[_manager authorizationStatus]];
+			} else {
+				[self processWithStatus:[CLLocationManager authorizationStatus]];
+			}
+		} else {
+			[self fail];
+		}
+	}
+	return self;
+}
+
+- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation*>*)locations {
+	[_manager stopUpdatingLocation];
+
+	auto result = Core::GeoLocation();
+	if ([locations count] > 0) {
+		const auto coordinate = [locations lastObject].coordinate;
+		result.accuracy = Core::GeoLocationAccuracy::Exact;
+		result.point = QPointF(coordinate.latitude, coordinate.longitude);
+	}
+
+	const auto onstack = _callback;
+	[self release];
+
+	onstack(result);
+}
+
+- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
+	if (error.code != kCLErrorLocationUnknown) {
+		[self fail];
+	}
+}
+
+- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status {
+	[self processWithStatus:status];
+}
+
+- (void) dealloc {
+	if (_manager) {
+		_manager.delegate = nil;
+		[_manager release];
+	}
+	[super dealloc];
+}
+
+@end
+
 namespace Platform {
 
 void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
-	callback({});
+	[[LocationDelegate alloc] initWithCallback:std::move(callback)];
 }
 
 } // namespace Platform
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 21c5b9861..804593b07 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/style_dialogs.h"
 #include "styles/style_window.h"
 
+#include <QtCore/QFile>
 #include <QtCore/QJsonDocument>
 #include <QtCore/QJsonObject>
 #include <QtCore/QJsonValue>
@@ -27,6 +28,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Ui {
 namespace {
 
+#ifdef Q_OS_MAC
+const auto kProtocolOverride = "mapboxapihelper";
+#else // Q_OS_MAC
+const auto kProtocolOverride = "";
+#endif // Q_OS_MAC
+
 Core::GeoLocation LastExactLocation;
 QString MapsProviderToken;
 
@@ -35,9 +42,9 @@ QString MapsProviderToken;
 		return "null";
 	}
 	return "["_q
-		+ QByteArray::number(LastExactLocation.point.x() - 1)
+		+ QByteArray::number(LastExactLocation.point.x())
 		+ ","_q
-		+ QByteArray::number(LastExactLocation.point.y() - 1)
+		+ QByteArray::number(LastExactLocation.point.y())
 		+ "]"_q;
 }
 
@@ -189,6 +196,7 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 		Webview::WindowConfig{
 			.opaqueBg = st::windowBg->c,
 			.storageId = descriptor.storageId,
+			.dataProtocolOverride = kProtocolOverride,
 		});
 	const auto raw = _webview.get();
 
@@ -293,18 +301,25 @@ void LocationPicker::initMap() {
 	const auto token = MapsProviderToken.toUtf8();
 	const auto center = DefaultCenter();
 	const auto bounds = DefaultBounds();
-	const auto arguments = "'" + token + "', " + center + ", " + bounds;
-	_webview->eval("LocationPicker.init(" + arguments + ");");
+	const auto protocol = *kProtocolOverride
+		? "'"_q + kProtocolOverride + "'"
+		: "null";
+	const auto params = "token: '" + token + "'"
+		+ ", center: " + center
+		+ ", bounds: " + bounds
+		+ ", protocol: " + protocol;
+	_webview->eval("LocationPicker.init({ " + params + " });");
 }
 
 void LocationPicker::resolveCurrentLocation() {
 	using namespace Core;
 	const auto window = _window.get();
 	ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
-		if (location) {
-			LastExactLocation = location;
+		if (location.accuracy != GeoLocationAccuracy::Exact) {
+			return;
 		}
-		if (_webview && location.accuracy == GeoLocationAccuracy::Exact) {
+		LastExactLocation = location;
+		if (_webview) {
 			const auto point = QByteArray::number(location.point.x())
 				+ ","_q
 				+ QByteArray::number(location.point.y());
@@ -327,7 +342,10 @@ void LocationPicker::processKey(
 }
 
 void LocationPicker::close() {
-	_window->close();
+	crl::on_main(this, [=] {
+		_window = nullptr;
+		delete this;
+	});
 }
 
 void LocationPicker::minimize() {
diff --git a/Telegram/Telegram.plist b/Telegram/Telegram.plist
index 53b8daf8b..b68e2109c 100644
--- a/Telegram/Telegram.plist
+++ b/Telegram/Telegram.plist
@@ -12,12 +12,20 @@
 	<string>Icon.icns</string>
 	<key>CFBundleIdentifier</key>
 	<string>@bundle_identifier_plist@</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>@output_name@</string>
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
 	<string>@desktop_app_version_string@</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
+	<key>CFBundleSupportedPlatforms</key>
+	<array>
+		<string>MacOSX</string>
+	</array>
 	<key>CFBundleURLTypes</key>
 	<array>
 		<dict>
@@ -39,16 +47,18 @@
 	<false/>
 	<key>LSApplicationCategoryType</key>
 	<string>public.app-category.social-networking</string>
-	<key>LSMinimumSystemVersion</key>
-	<string>@CMAKE_OSX_DEPLOYMENT_TARGET@</string>
 	<key>LSFileQuarantineEnabled</key>
 	<true/>
+	<key>LSMinimumSystemVersion</key>
+	<string>@CMAKE_OSX_DEPLOYMENT_TARGET@</string>
 	<key>NOTE</key>
 	<string></string>
 	<key>NSMicrophoneUsageDescription</key>
 	<string>We need access to your microphone so that you can record voice messages and make calls.</string>
 	<key>NSCameraUsageDescription</key>
 	<string>We need access to your camera so that you can record video messages and make video calls.</string>
+	<key>NSLocationUsageDescription</key>
+	<string>We need access to your location so that you can send your current locations.</string>
 	<key>NSPrincipalClass</key>
 	<string>NSApplication</string>
 	<key>NSSupportsAutomaticGraphicsSwitching</key>
diff --git a/Telegram/Telegram/Telegram Lite.entitlements b/Telegram/Telegram/Telegram Lite.entitlements
index 46355b637..050eea08f 100644
--- a/Telegram/Telegram/Telegram Lite.entitlements	
+++ b/Telegram/Telegram/Telegram Lite.entitlements	
@@ -18,5 +18,7 @@
 	<true/>
 	<key>com.apple.security.device.camera</key>
 	<true/>
+	<key>com.apple.security.personal-information.location</key>
+	<true/>
 </dict>
 </plist>
diff --git a/Telegram/Telegram/Telegram.entitlements b/Telegram/Telegram/Telegram.entitlements
index 97c1f6d58..af2883220 100644
--- a/Telegram/Telegram/Telegram.entitlements
+++ b/Telegram/Telegram/Telegram.entitlements
@@ -6,5 +6,7 @@
 	<true/>
 	<key>com.apple.security.device.camera</key>
 	<true/>
+	<key>com.apple.security.personal-information.location</key>
+	<true/>
 </dict>
 </plist>
diff --git a/cmake b/cmake
index 4a4bc4cd3..78b441c9c 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 4a4bc4cd34b3ade038541a2b8b2c79f05393d67b
+Subproject commit 78b441c9c6ad8a14a8f97a28825babcadc6bf781

From 8e6d7bb1903a84c12610e348b27663dc58bb3f7e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sun, 7 Jul 2024 15:10:35 +0400
Subject: [PATCH 037/163] Improve location picker design.

---
 .../chat_helpers/chat_helpers.style           |  4 ++
 .../ui/controls/location_picker.cpp           | 70 +++++++++++++------
 .../SourceFiles/ui/controls/location_picker.h |  5 +-
 3 files changed, 54 insertions(+), 25 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 0b1290dc8..8c7bcbb93 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1423,3 +1423,7 @@ paidTagLabel: FlatLabel(defaultFlatLabel) {
 	style: semiboldTextStyle;
 }
 paidTagPadding: margins(16px, 6px, 16px, 6px);
+
+pickLocationWindow: size(364px, 680px);
+pickLocationMapHeight: 220px;
+pickLocationCollapsedHeight: 108px;
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 804593b07..5b77f017a 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -10,11 +10,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/platform/base_platform_info.h"
 #include "core/current_geo_location.h"
 #include "lang/lang_keys.h"
-#include "ui/widgets/rp_window.h"
+#include "ui/widgets/scroll_area.h"
+#include "ui/widgets/separate_panel.h"
 #include "ui/widgets/buttons.h"
+#include "ui/wrap/vertical_layout.h"
 #include "webview/webview_data_stream_memory.h"
 #include "webview/webview_embed.h"
 #include "webview/webview_interface.h"
+#include "styles/style_chat_helpers.h"
 #include "styles/style_dialogs.h"
 #include "styles/style_window.h"
 
@@ -117,7 +120,10 @@ QString MapsProviderToken;
 LocationPicker::LocationPicker(Descriptor &&descriptor)
 : _callback(std::move(descriptor.callback))
 , _quit(std::move(descriptor.quit))
-, _window(std::make_unique<RpWindow>())
+, _window(std::make_unique<SeparatePanel>())
+, _body((_window->setInnerSize(st::pickLocationWindow)
+	, _window->showInner(base::make_unique_q<RpWidget>(_window.get()))
+	, _window->inner()))
 , _updateStyles([=] {
 	const auto str = EscapeForScriptString(ComputeStyles());
 	if (_webview) {
@@ -148,35 +154,50 @@ void LocationPicker::setup(const Descriptor &descriptor) {
 void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	const auto window = _window.get();
 
+	window->setWindowFlag(Qt::WindowStaysOnTopHint, false);
+	window->closeRequests() | rpl::start_with_next([=] {
+		close();
+	}, _lifetime);
+
 	const auto parent = descriptor.parent
 		? descriptor.parent->window()->geometry()
 		: QGuiApplication::primaryScreen()->availableGeometry();
-	window->setGeometry(QRect(
-		parent.x() + (parent.width() - st::windowMinHeight) / 2,
-		parent.y() + (parent.height() - st::windowMinWidth) / 2,
-		st::windowMinHeight,
-		st::windowMinWidth));
-	window->setMinimumSize({ st::windowMinHeight, st::windowMinWidth });
+	window->setTitle(tr::lng_maps_point());
+	window->move(
+		parent.x() + (parent.width() - window->width()) / 2,
+		parent.y() + (parent.height() - window->height()) / 2);
 
-	_container = Ui::CreateChild<Ui::RpWidget>(window->body().get());
-	const auto button = Ui::CreateChild<FlatButton>(
-		window->body(),
+	_container = CreateChild<RpWidget>(_body.get());
+	const auto scroll = CreateChild<ScrollArea>(_body.get());
+	const auto controls = scroll->setOwnedWidget(
+		object_ptr<VerticalLayout>(scroll));
+	const auto toppad = controls->add(object_ptr<RpWidget>(controls));
+
+	const auto button = controls->add(object_ptr<FlatButton>(
+		controls,
 		tr::lng_maps_point_send(tr::now),
-		st::dialogsUpdateButton);
-	button->show();
+		st::dialogsUpdateButton));
 	button->setClickedCallback([=] {
 		_webview->eval("LocationPicker.send();");
 	});
-	window->body()->sizeValue(
-	) | rpl::start_with_next([=](QSize size) {
-		_container->setGeometry(QRect(QPoint(), size).marginsRemoved(
-			{ 0, 0, 0, button->height() }));
-		button->resizeToWidth(size.width());
-		button->setGeometry(
-			0,
-			size.height() - button->height(),
-			button->width(),
-			button->height());
+	controls->add(object_ptr<RpWidget>(controls))->resize(
+		st::pickLocationWindow);
+
+	rpl::combine(
+		_body->sizeValue(),
+		scroll->scrollTopValue()
+	) | rpl::start_with_next([=](QSize size, int scrollTop) {
+		const auto width = size.width();
+		const auto height = size.height();
+		const auto sub = std::min(
+			(st::pickLocationMapHeight - st::pickLocationCollapsedHeight),
+			scrollTop);
+		const auto mapHeight = st::pickLocationMapHeight - sub;
+		const auto scrollHeight = height - mapHeight;
+		button->resizeToWidth(width);
+		_container->setGeometry(0, 0, width, mapHeight);
+		scroll->setGeometry(0, mapHeight, width, scrollHeight);
+		toppad->resize(width, sub);
 	}, _container->lifetime());
 
 	_container->paintRequest() | rpl::start_with_next([=](QRect clip) {
@@ -184,6 +205,9 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	}, _container->lifetime());
 
 	_container->show();
+	scroll->show();
+	controls->show();
+	button->show();
 	window->show();
 }
 
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index 220c0c426..f18957b9d 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -17,7 +17,7 @@ class Window;
 
 namespace Ui {
 
-class RpWindow;
+class SeparatePanel;
 class RpWidget;
 
 struct LocationInfo {
@@ -56,7 +56,8 @@ private:
 
 	Fn<void(LocationInfo)> _callback;
 	Fn<void()> _quit;
-	std::unique_ptr<RpWindow> _window;
+	std::unique_ptr<SeparatePanel> _window;
+	not_null<RpWidget*> _body;
 	RpWidget *_container = nullptr;
 	std::unique_ptr<Webview::Window> _webview;
 	SingleQueuedInvokation _updateStyles;

From 310837c9e18261ceca238c2fbd682fa15f8e8e9e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sun, 7 Jul 2024 21:01:26 +0400
Subject: [PATCH 038/163] Add venues list and chosen place name.

---
 Telegram/CMakeLists.txt                       |  10 +-
 .../Resources/icons/chat/filled_location.png  | Bin 0 -> 536 bytes
 .../icons/chat/filled_location@2x.png         | Bin 0 -> 987 bytes
 .../icons/chat/filled_location@3x.png         | Bin 0 -> 1525 bytes
 Telegram/Resources/langs/lang.strings         |   3 +
 Telegram/Resources/picker_html/picker.js      |  16 +-
 .../chat_helpers/chat_helpers.style           |  51 +-
 .../chat_helpers/gifs_list_widget.cpp         |   8 +-
 .../SourceFiles/core/current_geo_location.cpp | 126 ++++
 .../SourceFiles/core/current_geo_location.h   |  20 +
 Telegram/SourceFiles/data/data_location.h     |   4 +
 Telegram/SourceFiles/data/data_session.cpp    |  16 +
 Telegram/SourceFiles/data/data_session.h      |   3 +
 .../inline_bots/bot_attach_web_view.cpp       |   9 +-
 Telegram/SourceFiles/iv/iv_controller.cpp     |  73 +--
 Telegram/SourceFiles/main/main_session.cpp    |   8 +-
 .../SourceFiles/mtproto/mtproto_config.cpp    |  46 +-
 Telegram/SourceFiles/mtproto/mtproto_config.h |  11 +-
 .../linux/current_geo_location_linux.cpp      |   5 +
 .../platform/mac/current_geo_location_mac.mm  |   6 +
 .../platform/platform_current_geo_location.h  |   4 +
 .../platform/win/current_geo_location_win.cpp |   9 +
 .../ui/controls/location_picker.cpp           | 578 ++++++++++++++++--
 .../SourceFiles/ui/controls/location_picker.h |  79 ++-
 Telegram/SourceFiles/ui/webview_helpers.cpp   |  82 +++
 Telegram/SourceFiles/ui/webview_helpers.h     |  27 +
 Telegram/cmake/td_ui.cmake                    |  13 +-
 27 files changed, 1066 insertions(+), 141 deletions(-)
 create mode 100644 Telegram/Resources/icons/chat/filled_location.png
 create mode 100644 Telegram/Resources/icons/chat/filled_location@2x.png
 create mode 100644 Telegram/Resources/icons/chat/filled_location@3x.png
 create mode 100644 Telegram/SourceFiles/ui/webview_helpers.cpp
 create mode 100644 Telegram/SourceFiles/ui/webview_helpers.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 6f3bc7a00..592f1329f 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1472,6 +1472,8 @@ PRIVATE
     ui/chat/choose_send_as.h
     ui/chat/choose_theme_controller.cpp
     ui/chat/choose_theme_controller.h
+    ui/controls/location_picker.cpp
+    ui/controls/location_picker.h
     ui/controls/silent_toggle.cpp
     ui/controls/silent_toggle.h
     ui/controls/userpic_button.cpp
@@ -1493,6 +1495,10 @@ PRIVATE
     ui/image/image_location.h
     ui/image/image_location_factory.cpp
     ui/image/image_location_factory.h
+    ui/text/format_song_document_name.cpp
+    ui/text/format_song_document_name.h
+    ui/widgets/label_with_custom_emoji.cpp
+    ui/widgets/label_with_custom_emoji.h
     ui/countryinput.cpp
     ui/countryinput.h
     ui/dynamic_thumbnails.cpp
@@ -1506,10 +1512,6 @@ PRIVATE
     ui/resize_area.h
     ui/search_field_controller.cpp
     ui/search_field_controller.h
-    ui/text/format_song_document_name.cpp
-    ui/text/format_song_document_name.h
-    ui/widgets/label_with_custom_emoji.cpp
-    ui/widgets/label_with_custom_emoji.h
     ui/unread_badge.cpp
     ui/unread_badge.h
     window/main_window.cpp
diff --git a/Telegram/Resources/icons/chat/filled_location.png b/Telegram/Resources/icons/chat/filled_location.png
new file mode 100644
index 0000000000000000000000000000000000000000..12cd2dcc81112e4b61bf1abab39b18c1a0eff905
GIT binary patch
literal 536
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgftjW{GF~maf
zYRK8HrT~$18|7OMDHMkPW(jA_OBeqll2^@D%O-v)_{FZNZZ5e!Vyd=o2B)XY>~FCO
z|9tB}&a-vrzP~o*H<kMTOrq`Ku6I9c{_T?IX;7&D$TZh4eP>Ks<hNKW1;v8_@z?XV
zUw>V?vQcr7M$_75r$5`Mg)k;O7Ja#E-m*;BX$upZXO`6lBt%Zr=$gXW^x$)yL4cgu
z49&x5jz6AgSHAfsN7Dpr1BM{cG^w;a6RF-6&wkrYvTSCYu<G!`2~Nva+{_8`y==ml
zvo$It_1WJ#fBpKsamODQzB|C+5jrDt#!jYd*G<m0uhH<5IllTT*O?r%$*DgT6D{WY
zi5PvZv0Hxr$$10gM6M5+degb>=da#iYp0Q0ajyI5qq#~q=FOZwS;h0m(jT#&lMZ|{
zIyYhR$tPvILrWcJ`7QTpV9vR{POC_!U0BEK#u)>i)3HC8HFa_gBahw9E8l+mqFzqI
z`s>=(uV>cn?{7V`o3EWYp?29RLkXU3w{_PoTa`8SdH+xOR~|x^&yH||Vwb_w)z4*}
HQ$iB}F6`5b

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/chat/filled_location@2x.png b/Telegram/Resources/icons/chat/filled_location@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..cdef3f274a76935568c1501b895922906429f340
GIT binary patch
literal 987
zcmV<110?*3P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp)
z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@
z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NFMM*?KR9Fe^SUpH`Q5e1jhBygD
z`4bH-Leykawg}%4)D+<yVS_=F5R_tb;N&Dufrg7?;!q&%qF4|_qu3w)3|2~(-}mqZ
zVtL>9oO`sbJDvMJfA2lsdp+lB+VAl@1HH|_fSTNFHhaC^rKP2rnHjs?ZnN3ob8~ZZ
zeSLj)c6NApn9t`MjfR@JG1d6^csw3|d3pKE9w2}QvN3l<Y|KhD8m-ssY$snpkRgH4
zP>>XPVq&6DD11qiK7kN&sTfI;+wH!)yOVA}hX^@LB;y1$r_))jR&_!QmS6%aL6{J>
zSS-b2(O@)5hZPKkp&Vg%cbBwR0$|AH@z~hdSglr5$|XfGge{*-qOn+vH0}X_Eg7k4
z<ePeMaPao_c4TCP5d(U7c*tZjc(5^^ot<4-Ss5N4<|X&{_n4$stHsm4qx19g!db{>
zv!8z&Z81(!6bfP9mdFnF_V#$+)6>(Tp`mU&6an#Cn0E^Wm0T{zo6gP6Q5JxBEzBt`
zSud4JjOp|9Gj1|U3<v~94s%jV0a#Q}5b75f7tCD=2n0sXo9heT-rkr?e0q8s7$Bce
zKp-%3nCqu%8aeHckB^iFb*t4nJv~hk0D(ZEsPpFf!g9IHTq2;8lM~%ZLJ<(Jg}Htz
zQ%$GSyeZH`BJu6d5~nB%g)nEL^`Sr@AoK;{^73+fd;8D-0|T7Os9>(2%2cPOrev(u
zI520TN$C9iTrH1R!Iq3<o-%0m_xBm4GNf+na5&!I-}%cmioy`K$`_+0lS!kIJU^96
zsdodV%jNp`_~0|-QCPvSr-mTLHOWWv%!7l2Ub}&Qa&q$Z^_5SOL9vU4RUaCH#m>%-
zOeD_<g+l%51}-L(smFH`<hsjC^%gxIkMb)3LX}0du-R}p%>O798i_=N<|+g%OplI^
zs8p(ce0<C-c2)Z5)QpafUR_;L+1+(~P*dNhb_O$Jd{vR$Ut^t*ts`UAH)vsDq1kM9
z&qrqkH}c}*V&5W#Mr&(po$=k{_4Re3NuLS9V6eN7&&t-;R-eNp?KU<xum$UE3#Xf#
zo09S$p{=g2wwD7K`2GGLi7{llyu4hgRPYJW=kpm7{*sku;2!|1B}CFd<az)A002ov
JPDHLkV1jj1xJdv2

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/chat/filled_location@3x.png b/Telegram/Resources/icons/chat/filled_location@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..11caf17cade61efbac78edf521e8528e02b041d8
GIT binary patch
literal 1525
zcmV<R1q%9!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=Ur9tkRA>e5TgxkUT@-iib-&B~
zQX&&2QZA(=W(+YUrKFI7F=b%lKOo77Tv8;G7@*v9A5cW@ce#c4`@Ns9=RNPa?6dZM
z&VIjV@EB;HwSMcjers>XInUbradC0}IA_2)1I`(6&cN>(aQ!{BbmZgXQ&?D7P*4yP
z6B88`6&V?c-^g%!dU|$thTkVAC)3l@Q&UsV&(D@-mO~aA8d_UhJ2p1<^78VN@B<jY
z2tlA6MRv0W1O&9Sv^+dK5X#YkAQ0gov0I3B1m51>&CSi%*Vj6FIZYq|6$q_UC(4wV
zn7FdC!l~Z`4nok1DzYLD{~oTcu1sVKxj_qZD>U+RG&VMVe0&JWCIx{UDfo5S7g1SR
zNm6Gw2#M@d$`hTRpa1suX18V%0TLk{k2+CoQc{wn`^u<Lq(f3sy|g%YclY)6bt47x
z>`02#G>t;2s;Vk^>e<XlEhL!`gy-h*@i9BSJTFpnJ-FQn75xWkXlTG2Egf$y5~=I!
z>#gPd#pdVdcXxLuZ$T)`5C9p(+B}H3;^N}Kz(68c1v@%AnwXe4JUoOwAt3=&@$vC0
z4CN965IZ<HNC8tZ-QC@kNmXI?_V!9jO0+p(gi8gcTtbGHP2A$<=5~2`NlC24jE;`_
z`}-SZg$n>Bo-&3Eh%w^iw&&&LQL-vAi;Ih1US8}>005?pqKqMiP5bR7RbNz8MB%bw
zzP`Tjj`#ZdYP0AL0Kn4;U|p|~ktl{#SZZpjQ5s$Q$jHd%=BBPoW!%`<09a*YUlPTT
z3QJB-W+zj5hlYk!7RKf9@G!%xbrHpo3PX=cn}A{2+1X)uRj%#rZIy+6NfbjWjQ&{s
z)`ZRsTb$C%1x6rI45=_*UtgsZ({V&Zgek@b-~uBG9ZOp*V?ZkG{{Egpp><_vXKRf{
z7A`PIcvsjc?=L%U1IiP|hFM)*WzqfV1K1Ww84&MD(u$p*pZ_Vw<SIQqy`rMR1oW>P
zFn}pY8-`bmEl_9B+uKW-OC{#^_BJIYMTbja0st^&6lDxC3J${|X~pK}=M4!A96>=r
zv$L}~IXMP?7q|cb%m`p)7Z77a<&u_`Mwv@3=JWHjv$HcZGgC>8OBmtO;wcu0QL-70
zN!v)SuC8bmg@l9{v6$Ksj!$KP;geWcSePl^2yk<AgK*Su1mL#g#fLJTvN81cFVa1R
zY3S?g<DLu&0oKbvhsY6n9`r^Pb;YptX#H$)E-3^+24@ffj(^ayZy`2+Zf-6?rVs?5
z3)#8ld68O3GA*dAtW2JCHZxMwGzy{cK*MK#wgMSmB(<_RrCa;|vS{Vy<+@_5G-Bqq
zwzgy}ixDT%Nnft-8A2b|NQ4Y~X=$mIE5qhOA4*0IT2A^q6k9OW8yg$@`1q*RVXfuq
z=_xKQj;hHj9L_3OTZWdcsj0~-wH!Ry6y|MLXr(hOYinyB9@Jef4&4+e+7;-(GsFqI
za0((LBZHzr76vEsgw-<w>+0%cDW>!G_xCd-3b+Oa2I$!(B5}xYe}7+~0Ns#-g9CvM
zkF4_0pRwc+();^+R#uiQm14Zr)z!Kyz-6qdsS(RAH>$O@m8%RDxUH>Cu0DI%ySlnm
za*3BcJw5j1l^%;n>iGCLu^1a{a&pqs(^I-QI}>1IZh3jxCfam>Hw?!*yv?W|dwIm2
zr`_G%=;&w$mBYEh!^2JAB+;MY-OhA~I)j6Q7Zw(b{uGy%mK^un5vKEpb44>VGuqnb
z=H`TtoS7#5O91SJ<LH~RHhfoyxby#)Bnqeb@#9B(dpo{_ba8Rf(a}NgzYAw{20CZJ
bac1BjE>b#z=)o^$00000NkvXXu0mjfNxH1Z

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 122260d22..b15436b2b 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3195,6 +3195,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 "lng_maps_point" = "Location";
 "lng_maps_point_send" = "Send This Location";
+"lng_maps_or_choose" = "Or choose a venue";
+"lng_maps_places_in_area" = "Places in this area";
+"lng_maps_no_places" = "No places found";
 "lng_live_location" = "Live Location";
 "lng_live_location_now" = "updated just now";
 "lng_live_location_minutes#one" = "updated {count} minute ago";
diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js
index 54eb60aba..cb936cf0b 100644
--- a/Telegram/Resources/picker_html/picker.js
+++ b/Telegram/Resources/picker_html/picker.js
@@ -31,10 +31,22 @@ var LocationPicker = {
 			});
 		}
 	},
+	isNight: function() {
+		var html = document.getElementsByTagName('html')[0];
+		return html.style.getPropertyValue('--td-night') == '1';
+	},
+	lightPreset: function() {
+		return LocationPicker.isNight() ? 'night' : 'day';
+	},
 	updateStyles: function (styles) {
 		if (LocationPicker.styles !== styles) {
 			LocationPicker.styles = styles;
 			document.getElementsByTagName('html')[0].style = styles;
+
+			LocationPicker.map.setConfigProperty(
+				'basemap',
+				'lightPreset',
+				LocationPicker.lightPreset());
 		}
 	},
 	init: function (params) {
@@ -43,7 +55,9 @@ var LocationPicker = {
 			mapboxgl.config.API_URL = params.protocol + '://domain/api.mapbox.com';
 		}
 
-		var options = { container: 'map' };
+		var options = { container: 'map', config: {
+			basemap: { lightPreset: LocationPicker.lightPreset() }
+		} };
 		var center = params.center;
 		if (center) {
 			center = [center[1], center[0]];
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 8c7bcbb93..452a9f89b 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1426,4 +1426,53 @@ paidTagPadding: margins(16px, 6px, 16px, 6px);
 
 pickLocationWindow: size(364px, 680px);
 pickLocationMapHeight: 220px;
-pickLocationCollapsedHeight: 108px;
+pickLocationCollapsedHeight: 92px;
+pickLocationRowHeight: 52px;
+pickLocationVenue: PeerListItem(defaultPeerListItem) {
+	height: pickLocationRowHeight;
+	photoSize: 42px;
+	photoPosition: point(18px, 5px);
+	namePosition: point(70px, 9px);
+	statusPosition: point(70px, 29px);
+	button: OutlineButton(defaultPeerListButton) {
+		textBg: contactsBg;
+		textBgOver: contactsBgOver;
+		ripple: defaultRippleAnimation;
+	}
+	statusFg: contactsStatusFg;
+	statusFgOver: contactsStatusFgOver;
+	statusFgActive: contactsStatusFgOnline;
+}
+pickLocationButton: FlatButton {
+	height: pickLocationRowHeight;
+	bgColor: contactsBg;
+	overBgColor: contactsBgOver;
+	ripple: defaultRippleAnimation;
+}
+pickLocationButtonText: FlatLabel(defaultFlatLabel) {
+	minWidth: 128px;
+	style: semiboldTextStyle;
+	textFg: windowBoldFg;
+}
+pickLocationButtonStatus: FlatLabel(defaultFlatLabel) {
+	minWidth: 128px;
+	textFg: windowSubTextFg;
+}
+pickLocationButtonSkip: 6px;
+pickLocationSendIcon: icon{{ "chat/filled_location", windowFgActive }};
+pickLocationVenueItem: PeerListItem(defaultPeerListItem) {
+	button: OutlineButton(defaultPeerListButton) {
+		font: normalFont;
+		padding: margins(11px, 5px, 11px, 5px);
+	}
+	height: 52px;
+	photoPosition: point(18px, 5px);
+	namePosition: point(70px, 7px);
+	statusPosition: point(70px, 27px);
+	photoSize: 42px;
+}
+pickLocationVenueList: PeerList(defaultPeerList) {
+	item: pickLocationVenueItem;
+	padding: margins(0px, 0px, 0px, 0px);
+}
+pickLocationIconSkip: 6px;
diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp
index 2021e2943..f2ad99f46 100644
--- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_document_media.h"
 #include "data/stickers/data_stickers.h"
 #include "menu/menu_send.h" // SendMenu::FillSendMenu
+#include "mtproto/mtproto_config.h"
 #include "core/click_handler_types.h"
 #include "ui/controls/tabbed_search.h"
 #include "ui/widgets/buttons.h"
@@ -48,7 +49,6 @@ namespace ChatHelpers {
 namespace {
 
 constexpr auto kSearchRequestDelay = 400;
-constexpr auto kSearchBotUsername = "gif"_cs;
 constexpr auto kMinRepaintDelay = crl::time(33);
 constexpr auto kMinAfterScrollDelay = crl::time(33);
 
@@ -864,13 +864,11 @@ void GifsListWidget::searchForGifs(const QString &query) {
 	}
 
 	if (!_searchBot && !_searchBotRequestId) {
-		auto username = kSearchBotUsername.utf16();
+		const auto username = session().serverConfig().gifSearchUsername;
 		_searchBotRequestId = _api.request(MTPcontacts_ResolveUsername(
 			MTP_string(username)
 		)).done([=](const MTPcontacts_ResolvedPeer &result) {
-			Expects(result.type() == mtpc_contacts_resolvedPeer);
-
-			auto &data = result.c_contacts_resolvedPeer();
+			auto &data = result.data();
 			session().data().processUsers(data.vusers());
 			session().data().processChats(data.vchats());
 			const auto peer = session().data().peerLoaded(
diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp
index eb2125bf5..b649ea2ea 100644
--- a/Telegram/SourceFiles/core/current_geo_location.cpp
+++ b/Telegram/SourceFiles/core/current_geo_location.cpp
@@ -8,10 +8,122 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/current_geo_location.h"
 
 #include "base/platform/base_platform_info.h"
+#include "base/invoke_queued.h"
+#include "base/timer.h"
 #include "data/raw/raw_countries_bounds.h"
 #include "platform/platform_current_geo_location.h"
+#include "ui/ui_utility.h"
+
+#include <QtNetwork/QNetworkAccessManager>
+#include <QtNetwork/QNetworkReply>
+#include <QtCore/QCoreApplication>
+#include <QtCore/QPointer>
+#include <QtCore/QJsonDocument>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonArray>
 
 namespace Core {
+namespace {
+
+constexpr auto kDestroyManagerTimeout = 20 * crl::time(1000);
+
+void ResolveLocationAddressGeneric(
+		const GeoLocation &location,
+		const QString &token,
+		Fn<void(GeoAddress)> callback) {
+	const auto partialUrl = u"https://api.mapbox.com/search/geocode/v6"
+		"/reverse?longitude=%1&latitude=%2&access_token=%3"_q
+		.arg(location.point.y())
+		.arg(location.point.x());
+	static auto Cache = base::flat_map<QString, GeoAddress>();
+	const auto i = Cache.find(partialUrl);
+	if (i != end(Cache)) {
+		callback(i->second);
+		return;
+	}
+	const auto finishWith = [=](GeoAddress result) {
+		Cache[partialUrl] = result;
+		callback(result);
+	};
+
+	struct State final : QObject {
+		explicit State(QObject *parent)
+		: QObject(parent)
+		, manager(this)
+		, destroyer([=] { if (sent.empty()) delete this; }) {
+		}
+
+		QNetworkAccessManager manager;
+		std::vector<QPointer<QNetworkReply>> sent;
+		base::Timer destroyer;
+	};
+
+	static auto state = QPointer<State>();
+	if (!state) {
+		state = Ui::CreateChild<State>(qApp);
+	}
+	const auto destroyReplyDelayed = [](QNetworkReply *reply) {
+		InvokeQueued(reply, [=] {
+			for (auto i = begin(state->sent); i != end(state->sent);) {
+				if (!*i || *i == reply) {
+					i = state->sent.erase(i);
+				} else {
+					++i;
+				}
+			}
+			delete reply;
+			if (state->sent.empty()) {
+				state->destroyer.callOnce(kDestroyManagerTimeout);
+			}
+		});
+	};
+
+	auto request = QNetworkRequest(partialUrl.arg(token));
+	request.setRawHeader("Referer", "http://desktop-app-resource/");
+
+	const auto reply = state->manager.get(request);
+	QObject::connect(reply, &QNetworkReply::finished, [=] {
+		destroyReplyDelayed(reply);
+
+		const auto json = QJsonDocument::fromJson(reply->readAll());
+		if (!json.isObject()) {
+			finishWith({});
+			return;
+		}
+		const auto features = json["features"].toArray();
+		if (features.isEmpty()) {
+			finishWith({});
+			return;
+		}
+		const auto feature = features.at(0).toObject();
+		const auto properties = feature["properties"].toObject();
+		const auto context = properties["context"].toObject();
+		auto names = QStringList();
+		auto add = [&](std::vector<QString> keys) {
+			for (const auto &key : keys) {
+				const auto value = context[key];
+				if (value.isObject()) {
+					const auto name = value.toObject()["name"].toString();
+					if (!name.isEmpty()) {
+						names.push_back(name);
+						break;
+					}
+				}
+			}
+		};
+		add({ u"address"_q, u"street"_q, u"neighborhood"_q });
+		add({ u"place"_q, u"region"_q });
+		add({ u"country"_q });
+		finishWith({ .name = names.join(", ") });
+	});
+	QObject::connect(reply, &QNetworkReply::errorOccurred, [=] {
+		destroyReplyDelayed(reply);
+
+		finishWith({});
+	});
+}
+
+} // namespace
 
 GeoLocation ResolveCurrentCountryLocation() {
 	const auto iso2 = Platform::SystemCountry().toUpper();
@@ -47,4 +159,18 @@ void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback) {
 	});
 }
 
+void ResolveLocationAddress(
+		const GeoLocation &location,
+		const QString &token,
+		Fn<void(GeoAddress)> callback) {
+	auto done = [=, done = std::move(callback)](GeoAddress result) mutable {
+		if (!result && !token.isEmpty()) {
+			ResolveLocationAddressGeneric(location, token, std::move(done));
+		} else {
+			done(result);
+		}
+	};
+	Platform::ResolveLocationAddress(location, std::move(done));
+}
+
 } // namespace Core
diff --git a/Telegram/SourceFiles/core/current_geo_location.h b/Telegram/SourceFiles/core/current_geo_location.h
index 2715699ee..e3b92222e 100644
--- a/Telegram/SourceFiles/core/current_geo_location.h
+++ b/Telegram/SourceFiles/core/current_geo_location.h
@@ -33,9 +33,29 @@ struct GeoLocation {
 	explicit operator bool() const {
 		return !failed();
 	}
+
+	friend inline bool operator==(
+		const GeoLocation&,
+		const GeoLocation&) = default;
+};
+
+struct GeoAddress {
+	QString name;
+
+	[[nodiscard]] bool empty() const {
+		return name.isEmpty();
+	}
+	explicit operator bool() const {
+		return !empty();
+	}
 };
 
 [[nodiscard]] GeoLocation ResolveCurrentCountryLocation();
 void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback);
 
+void ResolveLocationAddress(
+	const GeoLocation &location,
+	const QString &token,
+	Fn<void(GeoAddress)> callback);
+
 } // namespace Core
diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h
index 6fb00d550..10f05adfb 100644
--- a/Telegram/SourceFiles/data/data_location.h
+++ b/Telegram/SourceFiles/data/data_location.h
@@ -53,6 +53,10 @@ struct InputVenue {
 	QString provider;
 	QString id;
 	QString venueType;
+
+	friend inline bool operator==(
+		const InputVenue &,
+		const InputVenue &) = default;
 };
 
 [[nodiscard]] GeoPointLocation ComputeLocation(const LocationPoint &point);
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index 869476243..e43e6423a 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -3328,6 +3328,22 @@ void Session::documentApplyFields(
 	}
 }
 
+not_null<DocumentData*> Session::venueIconDocument(const QString &icon) {
+	const auto i = _venueIcons.find(icon);
+	if (i != end(_venueIcons)) {
+		return i->second;
+	}
+	const auto result = documentFromWeb(MTP_webDocumentNoProxy(
+		MTP_string(u"https://ss3.4sqi.net/img/categories_v2/"_q
+			+ icon
+			+ u"_64.png"_q),
+		MTP_int(0),
+		MTP_string("image/png"),
+		MTP_vector<MTPDocumentAttribute>()), {}, {});
+	_venueIcons.emplace(icon, result);
+	return result;
+}
+
 not_null<WebPageData*> Session::webpage(WebPageId id) {
 	auto i = _webpages.find(id);
 	if (i == _webpages.cend()) {
diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h
index 12c4cba3a..06b4b267d 100644
--- a/Telegram/SourceFiles/data/data_session.h
+++ b/Telegram/SourceFiles/data/data_session.h
@@ -559,6 +559,8 @@ public:
 		const MTPWebDocument &data,
 		const ImageLocation &thumbnailLocation,
 		const ImageLocation &videoThumbnailLocation);
+	[[nodiscard]] not_null<DocumentData*> venueIconDocument(
+		const QString &icon);
 
 	[[nodiscard]] not_null<WebPageData*> webpage(WebPageId id);
 	not_null<WebPageData*> processWebpage(const MTPWebPage &data);
@@ -1002,6 +1004,7 @@ private:
 		FullStoryId,
 		base::flat_set<not_null<HistoryItem*>>> _storyItems;
 	base::flat_map<uint64, not_null<HistoryItem*>> _highlightings;
+	base::flat_map<QString, not_null<DocumentData*>> _venueIcons;
 
 	base::flat_set<not_null<WebPageData*>> _webpagesUpdated;
 	base::flat_set<not_null<GameData*>> _gamesUpdated;
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index c6b013956..b9233313c 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -162,6 +162,10 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
 	return u""_q;
 }
 
+[[nodiscard]] QString ResolveGeocodingToken(not_null<Main::Session*> session) {
+	return u""_q;
+}
+
 void ShowChooseBox(
 		not_null<Window::SessionController*> controller,
 		PeerTypes types,
@@ -1808,6 +1812,7 @@ void ChooseAndSendLocation(
 	};
 	Ui::LocationPicker::Show({
 		.parent = controller->widget(),
+		.session = &controller->session(),
 		.callback = crl::guard(controller, callback),
 		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
 		.storageId = controller->session().local().resolveStorageIdBots(),
@@ -1872,7 +1877,9 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
 	const auto session = &controller->session();
 	const auto locationType = ChatRestriction::SendOther;
 	if (Data::CanSendAnyOf(peer, locationType)
-		&& Ui::LocationPicker::Available(ResolveMapsToken(session))) {
+		&& Ui::LocationPicker::Available(
+			ResolveMapsToken(session),
+			ResolveGeocodingToken(session))) {
 		raw->addAction(tr::lng_maps_point(tr::now), [=] {
 			ChooseAndSendLocation(controller, actionFactory());
 		}, &st::menuIconAddress);
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 3b30dc6db..4161fe876 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/wrap/fade_wrap.h"
 #include "ui/basic_click_handlers.h"
 #include "ui/painter.h"
+#include "ui/webview_helpers.h"
 #include "webview/webview_data_stream_memory.h"
 #include "webview/webview_embed.h"
 #include "webview/webview_interface.h"
@@ -39,8 +40,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include <QtGui/QWindow>
 #include <charconv>
 
-#include "base/call_delayed.h"
-
 namespace Iv {
 namespace {
 
@@ -79,67 +78,7 @@ namespace {
 	static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
 		{ "iv-join-channel", tr::lng_iv_join_channel },
 	};
-	static const auto serialize = [](const style::color *color) {
-		const auto qt = (*color)->c;
-		if (qt.alpha() == 255) {
-			return '#'
-				+ QByteArray::number(qt.red(), 16).right(2)
-				+ QByteArray::number(qt.green(), 16).right(2)
-				+ QByteArray::number(qt.blue(), 16).right(2);
-		}
-		return "rgba("
-			+ QByteArray::number(qt.red()) + ","
-			+ QByteArray::number(qt.green()) + ","
-			+ QByteArray::number(qt.blue()) + ","
-			+ QByteArray::number(qt.alpha() / 255.) + ")";
-	};
-	static const auto escape = [](tr::phrase<> phrase) {
-		const auto text = phrase(tr::now);
-
-		auto result = QByteArray();
-		for (auto i = 0; i != text.size(); ++i) {
-			uint ucs4 = text[i].unicode();
-			if (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) {
-				ushort low = text[i + 1].unicode();
-				if (QChar::isLowSurrogate(low)) {
-					ucs4 = QChar::surrogateToUcs4(ucs4, low);
-					++i;
-				}
-			}
-			if (ucs4 == '\'' || ucs4 == '\"' || ucs4 == '\\') {
-				result.append('\\').append(char(ucs4));
-			} else if (ucs4 < 32 || ucs4 > 127) {
-				result.append('\\' + QByteArray::number(ucs4, 16) + ' ');
-			} else {
-				result.append(char(ucs4));
-			}
-		}
-		return result;
-	};
-	auto result = QByteArray();
-	for (const auto &[name, phrase] : phrases) {
-		result += "--td-lng-" + name + ":'" + escape(phrase) + "'; ";
-	}
-	for (const auto &[name, color] : map) {
-		result += "--td-" + name + ':' + serialize(color) + ';';
-	}
-	return result;
-}
-
-[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value) {
-	return value
-		.replace('&', "&amp;")
-		.replace('"', "&quot;")
-		.replace('\'', "&#039;")
-		.replace('<', "&lt;")
-		.replace('>', "&gt;");
-}
-
-[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) {
-	return value
-		.replace('\\', "\\\\")
-		.replace('"', "\\\"")
-		.replace('\'', "\\\'");
+	return Ui::ComputeStyles(map, phrases);
 }
 
 [[nodiscard]] QByteArray WrapPage(const Prepared &page) {
@@ -159,7 +98,7 @@ namespace {
 <html)"_q
 	+ classAttribute
 	+ R"( style=")"
-	+ EscapeForAttribute(ComputeStyles())
+	+ Ui::EscapeForAttribute(ComputeStyles())
 	+ R"(">
 	<head>
 		<meta charset="utf-8">
@@ -194,7 +133,7 @@ Controller::Controller(
 	Fn<ShareBoxResult(ShareBoxDescriptor)> showShareBox)
 : _delegate(delegate)
 , _updateStyles([=] {
-	const auto str = EscapeForScriptString(ComputeStyles());
+	const auto str = Ui::EscapeForScriptString(ComputeStyles());
 	if (_webview) {
 		_webview->eval("IV.updateStyles('" + str + "');");
 	}
@@ -612,7 +551,7 @@ QByteArray Controller::navigateScript(int index, const QString &hash) {
 	return "IV.navigateTo("
 		+ QByteArray::number(index)
 		+ ", '"
-		+ EscapeForScriptString(qthelp::url_decode(hash).toUtf8())
+		+ Ui::EscapeForScriptString(qthelp::url_decode(hash).toUtf8())
 		+ "');";
 }
 
@@ -679,7 +618,7 @@ bool Controller::active() const {
 void Controller::showJoinedTooltip() {
 	if (_webview && _ready) {
 		_webview->eval("IV.showTooltip('"
-			+ EscapeForScriptString(
+			+ Ui::EscapeForScriptString(
 				tr::lng_action_you_joined(tr::now).toUtf8())
 			+ "');");
 	}
diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp
index ea6111574..c14d49051 100644
--- a/Telegram/SourceFiles/main/main_session.cpp
+++ b/Telegram/SourceFiles/main/main_session.cpp
@@ -73,10 +73,14 @@ constexpr auto kTmpPasswordReserveTime = TimeId(10);
 		if (domain.startsWith(prefix, Qt::CaseInsensitive)) {
 			return domain.endsWith('/')
 				? domain
-				: MTP::ConfigFields().internalLinksDomain;
+				: MTP::ConfigFields(
+					session->mtp().environment()
+				).internalLinksDomain;
 		}
 	}
-	return MTP::ConfigFields().internalLinksDomain;
+	return MTP::ConfigFields(
+		session->mtp().environment()
+	).internalLinksDomain;
 }
 
 } // namespace
diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.cpp b/Telegram/SourceFiles/mtproto/mtproto_config.cpp
index 454339f49..1da5325ef 100644
--- a/Telegram/SourceFiles/mtproto/mtproto_config.cpp
+++ b/Telegram/SourceFiles/mtproto/mtproto_config.cpp
@@ -16,18 +16,30 @@ namespace {
 
 constexpr auto kVersion = 1;
 
-} // namespace
-
-QString ConfigDefaultReactionEmoji() {
+[[nodiscard]] QString ConfigDefaultReactionEmoji() {
 	static const auto result = QString::fromUtf8("\xf0\x9f\x91\x8d");
 	return result;
 }
 
-Config::Config(Environment environment) : _dcOptions(environment) {
-	_fields.webFileDcId = _dcOptions.isTestMode() ? 2 : 4;
-	_fields.txtDomainString = _dcOptions.isTestMode()
-		? u"tapv3.stel.com"_q
-		: u"apv3.stel.com"_q;
+} // namespace
+
+ConfigFields::ConfigFields(Environment environment)
+: webFileDcId(environment == Environment::Test ? 2 : 4)
+, txtDomainString(environment == Environment::Test
+	? u"tapv3.stel.com"_q
+	: u"apv3.stel.com"_q)
+, reactionDefaultEmoji(ConfigDefaultReactionEmoji())
+, gifSearchUsername(environment == Environment::Test
+	? u"izgifbot"_q
+	: u"gif"_q)
+, venueSearchUsername(environment == Environment::Test
+	? u"foursquarebot"_q
+	: u"foursquare"_q) {
+}
+
+Config::Config(Environment environment)
+: _dcOptions(environment)
+, _fields(environment) {
 }
 
 Config::Config(const Config &other)
@@ -46,7 +58,9 @@ QByteArray Config::serialize() const {
 		+ 3 * sizeof(qint32)
 		+ Serialize::stringSize(_fields.reactionDefaultEmoji)
 		+ sizeof(quint64)
-		+ sizeof(qint32);
+		+ sizeof(qint32)
+		+ Serialize::stringSize(_fields.gifSearchUsername)
+		+ Serialize::stringSize(_fields.venueSearchUsername);
 
 	auto result = QByteArray();
 	result.reserve(size);
@@ -91,7 +105,9 @@ QByteArray Config::serialize() const {
 			<< qint32(_fields.captionLengthMax)
 			<< _fields.reactionDefaultEmoji
 			<< quint64(_fields.reactionDefaultCustom)
-			<< qint32(_fields.ratingDecay);
+			<< qint32(_fields.ratingDecay)
+			<< _fields.gifSearchUsername
+			<< _fields.venueSearchUsername;
 	}
 	return result;
 }
@@ -190,6 +206,10 @@ std::unique_ptr<Config> Config::FromSerialized(const QByteArray &serialized) {
 	if (!stream.atEnd()) {
 		read(raw->_fields.ratingDecay);
 	}
+	if (!stream.atEnd()) {
+		read(raw->_fields.gifSearchUsername);
+		read(raw->_fields.venueSearchUsername);
+	}
 
 	if (stream.status() != QDataStream::Ok
 		|| !raw->_dcOptions.constructFromSerialized(dcOptionsSerialized)) {
@@ -256,8 +276,12 @@ void Config::apply(const MTPDconfig &data) {
 	_fields.autologinToken = qs(data.vautologin_token().value_or_empty());
 	_fields.ratingDecay = data.vrating_e_decay().v;
 	if (_fields.ratingDecay <= 0) {
-		_fields.ratingDecay = ConfigFields().ratingDecay;
+		_fields.ratingDecay = ConfigFields(
+			_dcOptions.environment()
+		).ratingDecay;
 	}
+	_fields.gifSearchUsername = qs(data.vgif_search_username().value_or_empty());
+	_fields.venueSearchUsername = qs(data.vvenue_search_username().value_or_empty());
 
 	if (data.vdc_options().v.empty()) {
 		LOG(("MTP Error: config with empty dc_options received!"));
diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.h b/Telegram/SourceFiles/mtproto/mtproto_config.h
index 8a4db80df..289230b64 100644
--- a/Telegram/SourceFiles/mtproto/mtproto_config.h
+++ b/Telegram/SourceFiles/mtproto/mtproto_config.h
@@ -11,9 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace MTP {
 
-[[nodiscard]] QString ConfigDefaultReactionEmoji();
-
 struct ConfigFields {
+	explicit ConfigFields(Environment environment);
+
 	int chatSizeMax = 200;
 	int megagroupSizeMax = 10000;
 	int forwardedCountMax = 100;
@@ -40,9 +40,12 @@ struct ConfigFields {
 	bool blockedMode = false;
 	int captionLengthMax = 1024;
 	int ratingDecay = 2419200;
-	QString reactionDefaultEmoji = ConfigDefaultReactionEmoji();
-	uint64 reactionDefaultCustom;
+	QString reactionDefaultEmoji;
+	uint64 reactionDefaultCustom = 0;
 	QString autologinToken;
+
+	QString gifSearchUsername;
+	QString venueSearchUsername;
 };
 
 class Config final {
diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
index 5ac0c49a7..6f42c0afd 100644
--- a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
@@ -14,5 +14,10 @@ namespace Platform {
 void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
 	callback({});
 }
+void ResolveLocationAddress(
+		const Core::GeoLocation &location,
+		Fn<void(Core::GeoAddress)> callback) {
+	callback({});
+}
 
 } // namespace Platform
diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
index 2812a0c33..452bc6e3a 100644
--- a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
+++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
@@ -116,4 +116,10 @@ void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
 	[[LocationDelegate alloc] initWithCallback:std::move(callback)];
 }
 
+void ResolveLocationAddress(
+		const Core::GeoLocation &location,
+		Fn<void(Core::GeoAddress)> callback) {
+	callback({});
+}
+
 } // namespace Platform
diff --git a/Telegram/SourceFiles/platform/platform_current_geo_location.h b/Telegram/SourceFiles/platform/platform_current_geo_location.h
index 245342fc3..9feb4b376 100644
--- a/Telegram/SourceFiles/platform/platform_current_geo_location.h
+++ b/Telegram/SourceFiles/platform/platform_current_geo_location.h
@@ -9,10 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace Core {
 struct GeoLocation;
+struct GeoAddress;
 } // namespace Core
 
 namespace Platform {
 
 void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback);
+void ResolveLocationAddress(
+	const Core::GeoLocation &location,
+	Fn<void(Core::GeoAddress)> callback);
 
 } // namespace Platform
diff --git a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
index 37682c108..a83dedb88 100644
--- a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
+++ b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
@@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include <winrt/Windows.Devices.Geolocation.h>
 #include <winrt/Windows.Foundation.h>
 
+#include <winrt/Windows.Services.Maps.h>
+#include <winrt/Windows.Foundation.Collections.h>
+
 namespace Platform {
 
 void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
@@ -56,4 +59,10 @@ void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
 	}
 }
 
+void ResolveLocationAddress(
+		const Core::GeoLocation &location,
+		Fn<void(Core::GeoAddress)> callback) {
+	callback({});
+}
+
 } // namespace Platform
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 5b77f017a..a4658e7be 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -7,16 +7,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "ui/controls/location_picker.h"
 
+#include "apiwrap.h"
 #include "base/platform/base_platform_info.h"
+#include "boxes/peer_list_box.h"
 #include "core/current_geo_location.h"
+#include "data/data_document.h"
+#include "data/data_document_media.h"
+#include "data/data_file_origin.h"
+#include "data/data_location.h"
+#include "data/data_session.h"
+#include "data/data_user.h"
 #include "lang/lang_keys.h"
+#include "main/session/session_show.h"
+#include "main/main_session.h"
+#include "mtproto/mtproto_config.h"
 #include "ui/widgets/scroll_area.h"
 #include "ui/widgets/separate_panel.h"
 #include "ui/widgets/buttons.h"
 #include "ui/wrap/vertical_layout.h"
+#include "ui/painter.h"
+#include "ui/vertical_list.h"
+#include "ui/webview_helpers.h"
 #include "webview/webview_data_stream_memory.h"
 #include "webview/webview_embed.h"
 #include "webview/webview_interface.h"
+#include "window/themes/window_theme.h"
 #include "styles/style_chat_helpers.h"
 #include "styles/style_dialogs.h"
 #include "styles/style_window.h"
@@ -39,6 +54,246 @@ const auto kProtocolOverride = "";
 
 Core::GeoLocation LastExactLocation;
 QString MapsProviderToken;
+QString GeocodingProviderToken;
+
+using VenueData = Data::InputVenue;
+
+class VenueRowDelegate {
+public:
+	virtual void rowPaintIcon(
+		QPainter &p,
+		int x,
+		int y,
+		int size,
+		const QString &type) = 0;
+};
+
+class VenueRow final : public PeerListRow {
+public:
+	VenueRow(not_null<VenueRowDelegate*> delegate, const VenueData &data);
+
+	void update(const VenueData &data);
+
+	[[nodiscard]] VenueData data() const;
+
+	QString generateName() override;
+	QString generateShortName() override;
+	PaintRoundImageCallback generatePaintUserpicCallback(
+		bool forceRound) override;
+
+private:
+	const not_null<VenueRowDelegate*> _delegate;
+	VenueData _data;
+
+};
+
+VenueRow::VenueRow(
+	not_null<VenueRowDelegate*> delegate,
+	const VenueData &data)
+: PeerListRow(UniqueRowIdFromString(data.id))
+, _delegate(delegate)
+, _data(data) {
+	setCustomStatus(data.address);
+}
+
+void VenueRow::update(const VenueData &data) {
+	_data = data;
+	setCustomStatus(data.address);
+}
+
+VenueData VenueRow::data() const {
+	return _data;
+}
+
+QString VenueRow::generateName() {
+	return _data.title;
+}
+
+QString VenueRow::generateShortName() {
+	return generateName();
+}
+
+PaintRoundImageCallback VenueRow::generatePaintUserpicCallback(
+		bool forceRound) {
+	return [=](
+			QPainter &p,
+			int x,
+			int y,
+			int outerWidth,
+			int size) {
+		_delegate->rowPaintIcon(p, x, y, size, _data.venueType);
+	};
+}
+
+class LinksController final
+	: public PeerListController
+	, public VenueRowDelegate
+	, public base::has_weak_ptr {
+public:
+	LinksController(
+		not_null<Main::Session*> session,
+		rpl::producer<std::vector<VenueData>> content);
+
+	void prepare() override;
+	void rowClicked(not_null<PeerListRow*> row) override;
+	void rowRightActionClicked(not_null<PeerListRow*> row) override;
+	Main::Session &session() const override;
+
+	void rowPaintIcon(
+		QPainter &p,
+		int x,
+		int y,
+		int size,
+		const QString &type) override;
+
+private:
+	struct VenueIcon {
+		not_null<DocumentData*> document;
+		std::shared_ptr<Data::DocumentMedia> media;
+		uint32 paletteVersion : 31 = 0;
+		uint32 iconLoaded : 1 = 0;
+		QImage image;
+		QImage icon;
+	};
+
+	void appendRow(const VenueData &data);
+
+	void rebuild(const std::vector<VenueData> &rows);
+
+	const not_null<Main::Session*> _session;
+	rpl::variable<std::vector<VenueData>> _rows;
+
+	base::flat_map<QString, VenueIcon> _icons;
+
+	rpl::lifetime _lifetime;
+
+};
+
+[[nodiscard]] QString NormalizeVenuesQuery(QString query) {
+	return query.trimmed().toLower();
+}
+
+LinksController::LinksController(
+	not_null<Main::Session*> session,
+	rpl::producer<std::vector<VenueData>> content)
+: _session(session)
+, _rows(std::move(content)) {
+}
+
+void LinksController::prepare() {
+	_rows.value(
+	) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
+		rebuild(rows);
+	}, _lifetime);
+}
+
+void LinksController::rebuild(const std::vector<VenueData> &rows) {
+	auto i = 0;
+	auto count = delegate()->peerListFullRowsCount();
+	while (i < rows.size()) {
+		if (i < count) {
+			const auto row = delegate()->peerListRowAt(i);
+			static_cast<VenueRow*>(row.get())->update(rows[i]);
+		} else {
+			appendRow(rows[i]);
+		}
+		++i;
+	}
+	while (i < count) {
+		delegate()->peerListRemoveRow(delegate()->peerListRowAt(i));
+		--count;
+	}
+	delegate()->peerListRefreshRows();
+}
+
+void LinksController::rowClicked(not_null<PeerListRow*> row) {
+	const auto venue = static_cast<VenueRow*>(row.get())->data();
+	venue;
+}
+
+void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
+	delegate()->peerListShowRowMenu(row, true);
+}
+
+Main::Session &LinksController::session() const {
+	return *_session;
+}
+
+void LinksController::appendRow(const VenueData &data) {
+	delegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data));
+}
+
+void LinksController::rowPaintIcon(
+		QPainter &p,
+		int x,
+		int y,
+		int size,
+		const QString &icon) {
+	auto i = _icons.find(icon);
+	if (i == end(_icons)) {
+		i = _icons.emplace(icon, VenueIcon{
+			.document = _session->data().venueIconDocument(icon),
+		}).first;
+		i->second.media = i->second.document->createMediaView();
+		i->second.document->forceToCache(true);
+		i->second.document->save({}, QString(), LoadFromCloudOrLocal, true);
+	}
+	auto &data = i->second;
+	const auto version = uint32(style::PaletteVersion());
+	const auto loaded = (!data.media || data.media->loaded()) ? 1 : 0;
+	const auto prepare = data.image.isNull()
+		|| (data.iconLoaded != loaded)
+		|| (data.paletteVersion != version);
+	if (prepare) {
+		const auto skip = st::pickLocationIconSkip;
+		const auto inner = size - skip * 2;
+		const auto ratio = style::DevicePixelRatio();
+
+		if (loaded && data.media) {
+			const auto bytes = base::take(data.media)->bytes();
+			data.icon = Images::Read({ .content = bytes }).image;
+			if (!data.icon.isNull()) {
+				data.icon = data.icon.scaled(
+					QSize(inner, inner) * ratio,
+					Qt::IgnoreAspectRatio,
+					Qt::SmoothTransformation);
+			}
+		}
+
+		const auto full = QSize(size, size) * ratio;
+		auto image = (data.image.size() == full)
+			? base::take(data.image)
+			: QImage(full, QImage::Format_ARGB32_Premultiplied);
+		image.fill(Qt::transparent);
+		image.setDevicePixelRatio(ratio);
+
+		const auto bg = EmptyUserpic::UserpicColor(
+			EmptyUserpic::ColorIndex(UniqueRowIdFromString(icon)));
+		auto p = QPainter(&image);
+		auto hq = PainterHighQualityEnabler(p);
+		{
+			auto gradient = QLinearGradient(0, 0, 0, size);
+			gradient.setStops({
+				{ 0., bg.color1->c },
+				{ 1., bg.color2->c }
+			});
+			p.setBrush(gradient);
+		}
+		p.setPen(Qt::NoPen);
+		p.drawEllipse(QRect(0, 0, size, size));
+		if (!data.icon.isNull()) {
+			p.drawImage(
+				QRect(skip, skip, inner, inner),
+				style::colorizeImage(data.icon, st::historyPeerUserpicFg));
+		}
+		p.end();
+
+		data.paletteVersion = version;
+		data.iconLoaded = loaded;
+		data.image = std::move(image);
+	}
+	p.drawImage(x, y, data.image);
+}
 
 [[nodiscard]] QByteArray DefaultCenter() {
 	if (!LastExactLocation) {
@@ -68,23 +323,16 @@ QString MapsProviderToken;
 }
 
 [[nodiscard]] QByteArray ComputeStyles() {
-	return "";
-}
-
-[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value) {
-	return value
-		.replace('&', "&amp;")
-		.replace('"', "&quot;")
-		.replace('\'', "&#039;")
-		.replace('<', "&lt;")
-		.replace('>', "&gt;");
-}
-
-[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) {
-	return value
-		.replace('\\', "\\\\")
-		.replace('"', "\\\"")
-		.replace('\'', "\\\'");
+	static const auto map = base::flat_map<QByteArray, const style::color*>{
+		{ "window-bg", &st::windowBg },
+		{ "window-bg-over", &st::windowBgOver },
+		{ "window-bg-ripple", &st::windowBgRipple },
+		{ "window-active-text-fg", &st::windowActiveTextFg },
+	};
+	static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
+		{ "maps-places-in-area", tr::lng_maps_places_in_area },
+	};
+	return Ui::ComputeStyles(map, phrases, Window::Theme::IsNightMode());
 }
 
 [[nodiscard]] QByteArray ReadResource(const QString &name) {
@@ -115,6 +363,129 @@ QString MapsProviderToken;
 )"_q;
 }
 
+[[nodiscard]] object_ptr<AbstractButton> MakeSendLocationButton(
+		QWidget *parent,
+		rpl::producer<QString> address) {
+	auto result = object_ptr<FlatButton>(
+		parent,
+		QString(),
+		st::pickLocationButton);
+	const auto raw = result.data();
+
+	const auto st = &st::pickLocationVenue;
+	const auto icon = CreateChild<RpWidget>(raw);
+	icon->setGeometry(
+		st->photoPosition.x(),
+		st->photoPosition.y(),
+		st->photoSize,
+		st->photoSize);
+	icon->paintRequest() | rpl::start_with_next([=] {
+		auto p = QPainter(icon);
+		auto hq = PainterHighQualityEnabler(p);
+		p.setPen(Qt::NoPen);
+		p.setBrush(st::windowBgActive);
+		p.drawEllipse(icon->rect());
+		st::pickLocationSendIcon.paintInCenter(p, icon->rect());
+	}, icon->lifetime());
+	icon->show();
+
+	const auto hadAddress = std::make_shared<bool>(false);
+	auto statusText = std::move(
+		address
+	) | rpl::map([=](const QString &text) {
+		if (!text.isEmpty()) {
+			*hadAddress = true;
+			return text;
+		}
+		return *hadAddress ? tr::lng_contacts_loading(tr::now) : QString();
+	});
+	const auto name = CreateChild<FlatLabel>(
+		raw,
+		tr::lng_maps_point_send(tr::now),
+		st::pickLocationButtonText);
+	name->show();
+	const auto status = CreateChild<FlatLabel>(
+		raw,
+		rpl::duplicate(statusText),
+		st::pickLocationButtonStatus);
+	status->showOn(std::move(
+		statusText
+	) | rpl::map([](const QString &text) {
+		return !text.isEmpty();
+	}) | rpl::distinct_until_changed());
+	rpl::combine(
+		result->widthValue(),
+		status->shownValue()
+	) | rpl::start_with_next([=](int width, bool statusShown) {
+		const auto available = width
+			- st->namePosition.x()
+			- st->button.padding.right();
+		const auto namePosition = st->namePosition;
+		const auto statusPosition = st->statusPosition;
+		name->resizeToWidth(available);
+		const auto nameTop = statusShown
+			? namePosition.y()
+			: (st->height - name->height()) / 2;
+		name->moveToLeft(namePosition.x(), nameTop, width);
+		status->resizeToWidth(available);
+		status->moveToLeft(statusPosition.x(), statusPosition.y(), width);
+	}, name->lifetime());
+
+	return result;
+}
+
+void SetupVenues(
+		not_null<VerticalLayout*> container,
+		std::shared_ptr<Main::SessionShow> show,
+		rpl::producer<std::vector<VenueData>> value) {
+	auto &lifetime = container->lifetime();
+	const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
+		show);
+	const auto controller = lifetime.make_state<LinksController>(
+		&show->session(),
+		std::move(value));
+	controller->setStyleOverrides(&st::pickLocationVenueList);
+	const auto content = container->add(object_ptr<PeerListContent>(
+		container,
+		controller));
+	delegate->setContent(content);
+	controller->setDelegate(delegate);
+
+	show->session().downloaderTaskFinished() | rpl::start_with_next([=] {
+		content->update();
+	}, content->lifetime());
+}
+
+[[nodiscard]] PickerVenueList ParseVenues(
+		not_null<Main::Session*> session,
+		const MTPmessages_BotResults &venues) {
+	const auto &data = venues.data();
+	session->data().processUsers(data.vusers());
+
+	auto &list = data.vresults().v;
+	auto result = PickerVenueList();
+	result.list.reserve(list.size());
+	for (const auto &found : list) {
+		found.match([&](const auto &data) {
+			data.vsend_message().match([&](
+					const MTPDbotInlineMessageMediaVenue &data) {
+				data.vgeo().match([&](const MTPDgeoPoint &geo) {
+					result.list.push_back({
+						.lat = geo.vlat().v,
+						.lon = geo.vlong().v,
+						.title = qs(data.vtitle()),
+						.address = qs(data.vaddress()),
+						.provider = qs(data.vprovider()),
+						.id = qs(data.vvenue_id()),
+						.venueType = qs(data.vvenue_type()),
+					});
+				}, [](const auto &) {});
+			}, [](const auto &) {});
+		});
+	}
+	return result;
+}
+
 } // namespace
 
 LocationPicker::LocationPicker(Descriptor &&descriptor)
@@ -127,9 +498,12 @@ LocationPicker::LocationPicker(Descriptor &&descriptor)
 , _updateStyles([=] {
 	const auto str = EscapeForScriptString(ComputeStyles());
 	if (_webview) {
-		_webview->eval("IV.updateStyles('" + str + "');");
+		_webview->eval("LocationPicker.updateStyles('" + str + "');");
 	}
-}) {
+})
+, _venueState(PickerVenueLoading())
+, _session(descriptor.session)
+, _api(&_session->mtp()) {
 	std::move(
 		descriptor.closeRequests
 	) | rpl::start_with_next([=] {
@@ -140,15 +514,26 @@ LocationPicker::LocationPicker(Descriptor &&descriptor)
 	setup(descriptor);
 }
 
-bool LocationPicker::Available(const QString &token) {
+std::shared_ptr<Main::SessionShow> LocationPicker::uiShow() {
+	return Main::MakeSessionShow(nullptr, _session);
+}
+
+bool LocationPicker::Available(
+		const QString &mapsToken,
+		const QString &geocodingToken) {
 	static const auto Supported = Webview::NavigateToDataSupported();
-	MapsProviderToken = token;
+	MapsProviderToken = mapsToken;
+	GeocodingProviderToken = geocodingToken;
 	return Supported && !MapsProviderToken.isEmpty();
 }
 
 void LocationPicker::setup(const Descriptor &descriptor) {
 	setupWindow(descriptor);
 	setupWebview(descriptor);
+	if (LastExactLocation) {
+		venuesRequest(LastExactLocation);
+		resolveAddress(LastExactLocation);
+	}
 }
 
 void LocationPicker::setupWindow(const Descriptor &descriptor) {
@@ -168,24 +553,32 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 		parent.y() + (parent.height() - window->height()) / 2);
 
 	_container = CreateChild<RpWidget>(_body.get());
-	const auto scroll = CreateChild<ScrollArea>(_body.get());
-	const auto controls = scroll->setOwnedWidget(
-		object_ptr<VerticalLayout>(scroll));
+	_scroll = CreateChild<ScrollArea>(_body.get());
+	const auto controls = _scroll->setOwnedWidget(
+		object_ptr<VerticalLayout>(_scroll));
 	const auto toppad = controls->add(object_ptr<RpWidget>(controls));
 
-	const auto button = controls->add(object_ptr<FlatButton>(
-		controls,
-		tr::lng_maps_point_send(tr::now),
-		st::dialogsUpdateButton));
+	const auto button = controls->add(
+		MakeSendLocationButton(controls, _geocoderAddress.value()),
+		{ 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip });
 	button->setClickedCallback([=] {
 		_webview->eval("LocationPicker.send();");
 	});
-	controls->add(object_ptr<RpWidget>(controls))->resize(
-		st::pickLocationWindow);
+
+	AddDivider(controls);
+	AddSkip(controls);
+	AddSubsectionTitle(controls, tr::lng_maps_or_choose());
+
+	SetupVenues(controls, uiShow(), _venueState.value(
+	) | rpl::filter([=](const PickerVenueState &state) {
+		return v::is<PickerVenueList>(state);
+	}) | rpl::map([=](PickerVenueState &&state) {
+		return std::move(v::get<PickerVenueList>(state).list);
+	}));
 
 	rpl::combine(
 		_body->sizeValue(),
-		scroll->scrollTopValue()
+		_scroll->scrollTopValue()
 	) | rpl::start_with_next([=](QSize size, int scrollTop) {
 		const auto width = size.width();
 		const auto height = size.height();
@@ -194,9 +587,9 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 			scrollTop);
 		const auto mapHeight = st::pickLocationMapHeight - sub;
 		const auto scrollHeight = height - mapHeight;
-		button->resizeToWidth(width);
 		_container->setGeometry(0, 0, width, mapHeight);
-		scroll->setGeometry(0, mapHeight, width, scrollHeight);
+		_scroll->setGeometry(0, mapHeight, width, scrollHeight);
+		controls->resizeToWidth(width);
 		toppad->resize(width, sub);
 	}, _container->lifetime());
 
@@ -205,7 +598,7 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	}, _container->lifetime());
 
 	_container->show();
-	scroll->show();
+	_scroll->hide();
 	controls->show();
 	button->show();
 	window->show();
@@ -256,7 +649,7 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 			const auto object = message.object();
 			const auto event = object.value("event").toString();
 			if (event == u"ready"_q) {
-				initMap();
+				mapReady();
 				resolveCurrentLocation();
 			} else if (event == u"keydown"_q) {
 				const auto key = object.value("key").toString();
@@ -321,7 +714,31 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 	raw->navigateToData("location/picker.html");
 }
 
-void LocationPicker::initMap() {
+void LocationPicker::resolveAddress(Core::GeoLocation location) {
+	if (_geocoderResolvingFor == location) {
+		return;
+	}
+	_geocoderResolvingFor = location;
+	const auto done = [=](Core::GeoAddress address) {
+		if (_geocoderResolvingFor != location) {
+			return;
+		} else if (address) {
+			_geocoderAddress = address.name;
+		} else {
+			_geocoderAddress = u"(%1, %2)"_q
+				.arg(location.point.x(), 0, 'f')
+				.arg(location.point.y(), 0, 'f');
+		}
+	};
+	Core::ResolveLocationAddress(
+		location,
+		GeocodingProviderToken,
+		crl::guard(this, done));
+}
+
+void LocationPicker::mapReady() {
+	Expects(_scroll != nullptr);
+
 	const auto token = MapsProviderToken.toUtf8();
 	const auto center = DefaultCenter();
 	const auto bounds = DefaultBounds();
@@ -333,16 +750,103 @@ void LocationPicker::initMap() {
 		+ ", bounds: " + bounds
 		+ ", protocol: " + protocol;
 	_webview->eval("LocationPicker.init({ " + params + " });");
+
+	_scroll->show();
+}
+
+void LocationPicker::venuesRequest(
+		Core::GeoLocation location,
+		QString query) {
+	query = NormalizeVenuesQuery(query);
+	auto &cache = _venuesCache[query];
+	const auto i = ranges::find(
+		cache,
+		location,
+		&VenuesCacheEntry::location);
+	if (i != end(cache)) {
+		_venueState = i->result;
+		return;
+	} else if (_venuesRequestLocation == location
+		&& _venuesRequestQuery == query) {
+		return;
+	} else if (const auto oldRequestId = base::take(_venuesRequestId)) {
+		_api.request(oldRequestId).cancel();
+	}
+	_venueState = PickerVenueLoading();
+	_venuesRequestLocation = location;
+	_venuesRequestQuery = query;
+	if (_venuesBot) {
+		venuesSendRequest();
+	} else if (_venuesBotRequestId) {
+		return;
+	}
+	const auto username = _session->serverConfig().venueSearchUsername;
+	_venuesBotRequestId = _api.request(MTPcontacts_ResolveUsername(
+		MTP_string(username)
+	)).done([=](const MTPcontacts_ResolvedPeer &result) {
+		auto &data = result.data();
+		_session->data().processUsers(data.vusers());
+		_session->data().processChats(data.vchats());
+		const auto peer = _session->data().peerLoaded(
+			peerFromMTP(data.vpeer()));
+		const auto user = peer ? peer->asUser() : nullptr;
+		if (user && user->isBotInlineGeo()) {
+			_venuesBot = user;
+			venuesSendRequest();
+		} else {
+			LOG(("API Error: Bad peer returned by: %1").arg(username));
+		}
+	}).fail([=] {
+		LOG(("API Error: Error returned on lookup: %1").arg(username));
+	}).send();
+}
+
+void LocationPicker::venuesSendRequest() {
+	Expects(_venuesBot != nullptr);
+
+	if (_venuesRequestId || !_venuesRequestLocation) {
+		return;
+	}
+	_venuesRequestId = _api.request(MTPmessages_GetInlineBotResults(
+		MTP_flags(MTPmessages_GetInlineBotResults::Flag::f_geo_point),
+		_venuesBot->inputUser,
+		MTP_inputPeerEmpty(),
+		MTP_inputGeoPoint(
+			MTP_flags(0),
+			MTP_double(_venuesRequestLocation.point.x()),
+			MTP_double(_venuesRequestLocation.point.y()),
+			MTP_int(0)), // accuracy_radius
+		MTP_string(_venuesRequestQuery),
+		MTP_string() // offset
+	)).done([=](const MTPmessages_BotResults &result) {
+		auto parsed = ParseVenues(_session, result);
+		_venuesCache[_venuesRequestQuery].push_back({
+			.location = _venuesRequestLocation,
+			.result = parsed,
+		});
+		if (parsed.list.empty()) {
+			_venueState = PickerVenueNothingFound{ _venuesRequestQuery };
+		} else {
+			_venueState = std::move(parsed);
+		}
+	}).fail([=] {
+		_venueState = PickerVenueNothingFound{ _venuesRequestQuery };
+	}).send();
 }
 
 void LocationPicker::resolveCurrentLocation() {
 	using namespace Core;
 	const auto window = _window.get();
 	ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
-		if (location.accuracy != GeoLocationAccuracy::Exact) {
+		const auto changed = (LastExactLocation != location);
+		if (location.accuracy != GeoLocationAccuracy::Exact || !changed) {
 			return;
 		}
 		LastExactLocation = location;
+		if (location) {
+			venuesRequest(location);
+			resolveAddress(location);
+		}
 		if (_webview) {
 			const auto point = QByteArray::number(location.point.x())
 				+ ","_q
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index f18957b9d..9a4ca56d5 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -9,8 +9,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/invoke_queued.h"
 #include "base/weak_ptr.h"
+#include "core/current_geo_location.h"
+#include "mtproto/sender.h"
 #include "webview/webview_common.h"
 
+namespace Data {
+struct InputVenue;
+} // namespace Data
+
+namespace Main {
+class Session;
+class SessionShow;
+} // namespace Main
+
 namespace Webview {
 class Window;
 } // namespace Webview
@@ -19,23 +30,61 @@ namespace Ui {
 
 class SeparatePanel;
 class RpWidget;
+class ScrollArea;
 
 struct LocationInfo {
 	float64 lat = 0.;
 	float64 lon = 0.;
 };
 
+struct PickerVenueLoading {
+	friend inline bool operator==(
+		PickerVenueLoading,
+		PickerVenueLoading) = default;
+};
+
+struct PickerVenueNothingFound {
+	QString query;
+
+	friend inline bool operator==(
+		const PickerVenueNothingFound&,
+		const PickerVenueNothingFound&) = default;
+};
+
+struct PickerVenueWaitingForLocation {
+	friend inline bool operator==(
+		PickerVenueWaitingForLocation,
+		PickerVenueWaitingForLocation) = default;
+};
+
+struct PickerVenueList {
+	std::vector<Data::InputVenue> list;
+
+	friend inline bool operator==(
+		const PickerVenueList&,
+		const PickerVenueList&) = default;
+};
+
+using PickerVenueState = std::variant<
+	PickerVenueLoading,
+	PickerVenueNothingFound,
+	PickerVenueWaitingForLocation,
+	PickerVenueList>;
+
 class LocationPicker final : public base::has_weak_ptr {
 public:
 	struct Descriptor {
 		RpWidget *parent = nullptr;
+		not_null<Main::Session*> session;
 		Fn<void(LocationInfo)> callback;
 		Fn<void()> quit;
 		Webview::StorageId storageId;
 		rpl::producer<> closeRequests;
 	};
 
-	[[nodiscard]] static bool Available(const QString &token);
+	[[nodiscard]] static bool Available(
+		const QString &mapsToken,
+		const QString &geocodingToken);
 	static not_null<LocationPicker*> Show(Descriptor &&descriptor);
 
 	void close();
@@ -43,14 +92,25 @@ public:
 	void quit();
 
 private:
+	struct VenuesCacheEntry {
+		Core::GeoLocation location;
+		PickerVenueList result;
+	};
+
 	explicit LocationPicker(Descriptor &&descriptor);
 
+	[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();
+
 	void setup(const Descriptor &descriptor);
 	void setupWindow(const Descriptor &descriptor);
 	void setupWebview(const Descriptor &descriptor);
 	void processKey(const QString &key, const QString &modifier);
 	void resolveCurrentLocation();
-	void initMap();
+	void resolveAddress(Core::GeoLocation location);
+	void mapReady();
+
+	void venuesRequest(Core::GeoLocation location, QString query = {});
+	void venuesSendRequest();
 
 	rpl::lifetime _lifetime;
 
@@ -59,10 +119,25 @@ private:
 	std::unique_ptr<SeparatePanel> _window;
 	not_null<RpWidget*> _body;
 	RpWidget *_container = nullptr;
+	ScrollArea *_scroll = nullptr;
 	std::unique_ptr<Webview::Window> _webview;
 	SingleQueuedInvokation _updateStyles;
 	bool _subscribedToColors = false;
 
+	Core::GeoLocation _geocoderResolvingFor;
+	rpl::variable<QString> _geocoderAddress;
+
+	rpl::variable<PickerVenueState> _venueState;
+
+	const not_null<Main::Session*> _session;
+	MTP::Sender _api;
+	UserData *_venuesBot = nullptr;
+	mtpRequestId _venuesBotRequestId = 0;
+	mtpRequestId _venuesRequestId = 0;
+	Core::GeoLocation _venuesRequestLocation;
+	QString _venuesRequestQuery;
+	base::flat_map<QString, std::vector<VenuesCacheEntry>> _venuesCache;
+
 };
 
 } // namespace Ui
diff --git a/Telegram/SourceFiles/ui/webview_helpers.cpp b/Telegram/SourceFiles/ui/webview_helpers.cpp
new file mode 100644
index 000000000..24b7155ce
--- /dev/null
+++ b/Telegram/SourceFiles/ui/webview_helpers.cpp
@@ -0,0 +1,82 @@
+/*
+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/webview_helpers.h"
+
+#include "lang/lang_keys.h"
+
+namespace Ui {
+
+[[nodiscard]] QByteArray ComputeStyles(
+		const base::flat_map<QByteArray, const style::color*> &colors,
+		const base::flat_map<QByteArray, tr::phrase<>> &phrases,
+		bool nightTheme) {
+	static const auto serialize = [](const style::color *color) {
+		const auto qt = (*color)->c;
+		if (qt.alpha() == 255) {
+			return '#'
+				+ QByteArray::number(qt.red(), 16).right(2)
+				+ QByteArray::number(qt.green(), 16).right(2)
+				+ QByteArray::number(qt.blue(), 16).right(2);
+		}
+		return "rgba("
+			+ QByteArray::number(qt.red()) + ","
+			+ QByteArray::number(qt.green()) + ","
+			+ QByteArray::number(qt.blue()) + ","
+			+ QByteArray::number(qt.alpha() / 255.) + ")";
+	};
+	static const auto escape = [](tr::phrase<> phrase) {
+		const auto text = phrase(tr::now);
+
+		auto result = QByteArray();
+		for (auto i = 0; i != text.size(); ++i) {
+			uint ucs4 = text[i].unicode();
+			if (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) {
+				ushort low = text[i + 1].unicode();
+				if (QChar::isLowSurrogate(low)) {
+					ucs4 = QChar::surrogateToUcs4(ucs4, low);
+					++i;
+				}
+			}
+			if (ucs4 == '\'' || ucs4 == '\"' || ucs4 == '\\') {
+				result.append('\\').append(char(ucs4));
+			} else if (ucs4 < 32 || ucs4 > 127) {
+				result.append('\\' + QByteArray::number(ucs4, 16) + ' ');
+			} else {
+				result.append(char(ucs4));
+			}
+		}
+		return result;
+	};
+	auto result = QByteArray();
+	for (const auto &[name, phrase] : phrases) {
+		result += "--td-lng-"_q + name + ":'"_q + escape(phrase) + "'; "_q;
+	}
+	for (const auto &[name, color] : colors) {
+		result += "--td-"_q + name + ':' + serialize(color) + ';';
+	}
+	result += "--td-night:"_q + (nightTheme ? "1" : "0") + ';';
+	return result;
+}
+
+QByteArray EscapeForAttribute(QByteArray value) {
+	return value
+		.replace('&', "&amp;")
+		.replace('"', "&quot;")
+		.replace('\'', "&#039;")
+		.replace('<', "&lt;")
+		.replace('>', "&gt;");
+}
+
+QByteArray EscapeForScriptString(QByteArray value) {
+	return value
+		.replace('\\', "\\\\")
+		.replace('"', "\\\"")
+		.replace('\'', "\\\'");
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/webview_helpers.h b/Telegram/SourceFiles/ui/webview_helpers.h
new file mode 100644
index 000000000..3d8a51066
--- /dev/null
+++ b/Telegram/SourceFiles/ui/webview_helpers.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
+
+#include "base/flat_map.h"
+
+namespace tr {
+template <typename ...Tags>
+struct phrase;
+} // namespace tr
+
+namespace Ui {
+
+[[nodiscard]] QByteArray ComputeStyles(
+	const base::flat_map<QByteArray, const style::color*> &colors,
+	const base::flat_map<QByteArray, tr::phrase<>> &phrases,
+	bool nightTheme = false);
+
+[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value);
+[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value);
+
+} // namespace Ui
diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake
index b11016acb..9bc3f6c76 100644
--- a/Telegram/cmake/td_ui.cmake
+++ b/Telegram/cmake/td_ui.cmake
@@ -355,8 +355,6 @@ PRIVATE
     ui/controls/invite_link_buttons.h
     ui/controls/invite_link_label.cpp
     ui/controls/invite_link_label.h
-    ui/controls/location_picker.cpp
-    ui/controls/location_picker.h
     ui/controls/peer_list_dummy.cpp
     ui/controls/peer_list_dummy.h
     ui/controls/send_as_button.cpp
@@ -403,6 +401,11 @@ PRIVATE
     ui/text/text_options.cpp
     ui/text/text_options.h
 
+    ui/widgets/fields/special_fields.cpp
+    ui/widgets/fields/special_fields.h
+    ui/widgets/fields/time_part_input_with_placeholder.cpp
+    ui/widgets/fields/time_part_input_with_placeholder.h
+
     ui/widgets/color_editor.cpp
     ui/widgets/color_editor.h
     ui/widgets/continuous_sliders.cpp
@@ -441,10 +444,8 @@ PRIVATE
     ui/unread_badge_paint.h
     ui/userpic_view.cpp
     ui/userpic_view.h
-    ui/widgets/fields/special_fields.cpp
-    ui/widgets/fields/special_fields.h
-    ui/widgets/fields/time_part_input_with_placeholder.cpp
-    ui/widgets/fields/time_part_input_with_placeholder.h
+    ui/webview_helpers.cpp
+    ui/webview_helpers.h
 
     window/window_slide_animation.cpp
     window/window_slide_animation.h

From de52ac6b28b8aaed265b0dbdf406d338e8f7e1c2 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 11 Jul 2024 16:31:33 +0200
Subject: [PATCH 039/163] Resolve different addresses.

---
 Telegram/Resources/picker_html/picker.css     | 12 +++-
 Telegram/Resources/picker_html/picker.js      | 45 ++++++++++---
 .../SourceFiles/core/current_geo_location.cpp | 50 ++++++++++++--
 .../SourceFiles/core/current_geo_location.h   |  1 +
 .../linux/current_geo_location_linux.cpp      |  1 +
 .../platform/mac/current_geo_location_mac.mm  |  1 +
 .../platform/platform_current_geo_location.h  |  1 +
 .../platform/win/current_geo_location_win.cpp |  1 +
 .../ui/controls/location_picker.cpp           | 66 ++++++++++++++++---
 .../SourceFiles/ui/controls/location_picker.h |  4 ++
 10 files changed, 159 insertions(+), 23 deletions(-)

diff --git a/Telegram/Resources/picker_html/picker.css b/Telegram/Resources/picker_html/picker.css
index 0c791008d..4d8f378ae 100644
--- a/Telegram/Resources/picker_html/picker.css
+++ b/Telegram/Resources/picker_html/picker.css
@@ -50,7 +50,7 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
 }
 #marker {
 	pointer-events: none;
-	display: flex;
+	display: none;
 	z-index: 2;
 	position: absolute;
 	width: 100%;
@@ -58,3 +58,13 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
 	justify-content: center;
 	align-items: center;
 }
+#marker_drop {
+	margin-bottom: 0px;
+	transition: margin 160ms ease-in-out;
+}
+#marker_drop.moving {
+	margin-bottom: 24px;
+}
+#marker_shadow {
+	position: absolute;
+}
diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js
index cb936cf0b..27ffd3b6c 100644
--- a/Telegram/Resources/picker_html/picker.js
+++ b/Telegram/Resources/picker_html/picker.js
@@ -70,16 +70,41 @@ var LocationPicker = {
 			center = [0, 0];
 		}
 		LocationPicker.map = new mapboxgl.Map(options);
-
-		const marker = new mapboxgl.Marker()
-			.setLngLat(center)
-			.addTo(LocationPicker.map);
-		const drop = document.getElementById('marker_drop');
-		const element = marker.getElement();
-		drop.innerHTML = element.innerHTML;
-		const offset = marker.getOffset();
-		drop.style.transform = 'translate(' + offset.x + 'px, ' + offset.y + 'px)';
-		marker.remove();
+		LocationPicker.createMarker(center);
+		LocationPicker.trackMovement();
+	},
+	marker: function() {
+		return document.getElementById('marker_drop');
+	},
+	createMarker: function(center) {
+		document.getElementById('marker').style.display = 'flex';
+	},
+	clearMovingTimer: function() {
+		if (LocationPicker.clearMovingTimeoutId) {
+			clearTimeout(LocationPicker.clearMovingTimeoutId);
+			LocationPicker.clearMovingTimeoutId = 0;
+		}
+	},
+	startMovingTimer: function(done) {
+		LocationPicker.clearMovingTimer();
+		LocationPicker.clearMovingTimeoutId = setTimeout(done, 500);
+	},
+	trackMovement: function() {
+		LocationPicker.map.on('movestart', function() {
+			LocationPicker.marker().classList.add('moving');
+			LocationPicker.clearMovingTimer();
+			LocationPicker.notify({ event: 'movestart' });
+		});
+		LocationPicker.map.on('moveend', function() {
+			LocationPicker.startMovingTimer(function() {
+				LocationPicker.marker().classList.remove('moving');
+				LocationPicker.notify({
+					event: 'moveend',
+					latitude: LocationPicker.map.getCenter().lat,
+					longitude: LocationPicker.map.getCenter().lng
+				});
+			});
+		});
 	},
 	narrowTo: function (point) {
 		LocationPicker.map.flyTo({
diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp
index b649ea2ea..818ea361a 100644
--- a/Telegram/SourceFiles/core/current_geo_location.cpp
+++ b/Telegram/SourceFiles/core/current_geo_location.cpp
@@ -27,14 +27,51 @@ namespace {
 
 constexpr auto kDestroyManagerTimeout = 20 * crl::time(1000);
 
+[[nodiscard]] QString ChooseLanguage(const QString &language) {
+	// https://docs.mapbox.com/api/search/geocoding#language-coverage
+	auto result = language.toLower().replace('-', '_');
+	static const auto kGood = std::array{
+		// Global coverage.
+		u"de"_q, u"en"_q, u"es"_q, u"fr"_q, u"it"_q, u"nl"_q, u"pl"_q,
+
+		// Local coverage.
+		u"az"_q, u"bn"_q, u"ca"_q, u"cs"_q, u"da"_q, u"el"_q, u"fa"_q,
+		u"fi"_q, u"ga"_q, u"hu"_q, u"id"_q, u"is"_q, u"ja"_q, u"ka"_q,
+		u"km"_q, u"ko"_q, u"lt"_q, u"lv"_q, u"mn"_q, u"pt"_q, u"ro"_q,
+		u"sk"_q, u"sq"_q, u"sv"_q, u"th"_q, u"tl"_q, u"uk"_q, u"vi"_q,
+		u"zh"_q, u"zh_Hans"_q, u"zh_TW"_q,
+
+		// Limited coverage.
+		u"ar"_q, u"bs"_q, u"gu"_q, u"he"_q, u"hi"_q, u"kk"_q, u"lo"_q,
+		u"my"_q, u"nb"_q, u"ru"_q, u"sr"_q, u"te"_q, u"tk"_q, u"tr"_q,
+		u"zh_Hant"_q,
+	};
+	for (const auto &known : kGood) {
+		if (known.toLower() == result) {
+			return known;
+		}
+	}
+	if (const auto delimeter = result.indexOf('_'); delimeter > 0) {
+		result = result.mid(0, delimeter);
+		for (const auto &known : kGood) {
+			if (known == result) {
+				return known;
+			}
+		}
+	}
+	return u"en"_q;
+}
+
 void ResolveLocationAddressGeneric(
 		const GeoLocation &location,
+		const QString &language,
 		const QString &token,
 		Fn<void(GeoAddress)> callback) {
 	const auto partialUrl = u"https://api.mapbox.com/search/geocode/v6"
-		"/reverse?longitude=%1&latitude=%2&access_token=%3"_q
+		"/reverse?longitude=%1&latitude=%2&language=%3&access_token=%4"_q
 		.arg(location.point.y())
-		.arg(location.point.x());
+		.arg(location.point.x())
+		.arg(ChooseLanguage(language));
 	static auto Cache = base::flat_map<QString, GeoAddress>();
 	const auto i = Cache.find(partialUrl);
 	if (i != end(Cache)) {
@@ -161,16 +198,21 @@ void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback) {
 
 void ResolveLocationAddress(
 		const GeoLocation &location,
+		const QString &language,
 		const QString &token,
 		Fn<void(GeoAddress)> callback) {
 	auto done = [=, done = std::move(callback)](GeoAddress result) mutable {
 		if (!result && !token.isEmpty()) {
-			ResolveLocationAddressGeneric(location, token, std::move(done));
+			ResolveLocationAddressGeneric(
+				location,
+				language,
+				token,
+				std::move(done));
 		} else {
 			done(result);
 		}
 	};
-	Platform::ResolveLocationAddress(location, std::move(done));
+	Platform::ResolveLocationAddress(location, language, std::move(done));
 }
 
 } // namespace Core
diff --git a/Telegram/SourceFiles/core/current_geo_location.h b/Telegram/SourceFiles/core/current_geo_location.h
index e3b92222e..dab4ffd00 100644
--- a/Telegram/SourceFiles/core/current_geo_location.h
+++ b/Telegram/SourceFiles/core/current_geo_location.h
@@ -55,6 +55,7 @@ void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback);
 
 void ResolveLocationAddress(
 	const GeoLocation &location,
+	const QString &language,
 	const QString &token,
 	Fn<void(GeoAddress)> callback);
 
diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
index 6f42c0afd..f87c343d3 100644
--- a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp
@@ -16,6 +16,7 @@ void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
 }
 void ResolveLocationAddress(
 		const Core::GeoLocation &location,
+		const QString &language,
 		Fn<void(Core::GeoAddress)> callback) {
 	callback({});
 }
diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
index 452bc6e3a..1a5382f19 100644
--- a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
+++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
@@ -118,6 +118,7 @@ void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
 
 void ResolveLocationAddress(
 		const Core::GeoLocation &location,
+		const QString &language,
 		Fn<void(Core::GeoAddress)> callback) {
 	callback({});
 }
diff --git a/Telegram/SourceFiles/platform/platform_current_geo_location.h b/Telegram/SourceFiles/platform/platform_current_geo_location.h
index 9feb4b376..269ee81f3 100644
--- a/Telegram/SourceFiles/platform/platform_current_geo_location.h
+++ b/Telegram/SourceFiles/platform/platform_current_geo_location.h
@@ -17,6 +17,7 @@ namespace Platform {
 void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback);
 void ResolveLocationAddress(
 	const Core::GeoLocation &location,
+	const QString &language,
 	Fn<void(Core::GeoAddress)> callback);
 
 } // namespace Platform
diff --git a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
index a83dedb88..9c7967588 100644
--- a/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
+++ b/Telegram/SourceFiles/platform/win/current_geo_location_win.cpp
@@ -61,6 +61,7 @@ void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
 
 void ResolveLocationAddress(
 		const Core::GeoLocation &location,
+		const QString &language,
 		Fn<void(Core::GeoAddress)> callback) {
 	callback({});
 }
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index a4658e7be..2eef9d698 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_location.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
+#include "lang/lang_instance.h"
 #include "lang/lang_keys.h"
 #include "main/session/session_show.h"
 #include "main/main_session.h"
@@ -46,6 +47,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Ui {
 namespace {
 
+constexpr auto kResolveAddressDelay = 3 * crl::time(1000);
+
 #ifdef Q_OS_MAC
 const auto kProtocolOverride = "mapboxapihelper";
 #else // Q_OS_MAC
@@ -355,7 +358,30 @@ void LinksController::rowPaintIcon(
 		<link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />
 	</head>
 	<body>
-		<div id="marker"><div id="marker_drop"></div></div>
+		<div id="marker">
+			<div id="marker_shadow" style="transform: translate(0px, -14px);">
+<svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
+	<defs>
+		<radialGradient id="shadowGradient">
+			<stop offset="10%" stop-opacity="0.4"></stop>
+			<stop offset="100%" stop-opacity="0.05"></stop>
+		</radialGradient>
+	</defs>
+	<ellipse
+		cx="13.5"
+		cy="34.8"
+		rx="10.5"
+		ry="5.25"
+		fill=")" + "url(#shadowGradient)" + R"("></ellipse>
+</svg>
+			</div>
+			<div id="marker_drop" style="transform: translate(0px, -14px);">
+<svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
+	<path fill="#3FB1CE" d="M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z"></path><path opacity="0.25" d="M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z"></path>
+	<circle fill="white" cx="13.5" cy="13.5" r="5.5"></circle>
+</svg>
+			</div>
+		</div>
 		<div id="map"></div>
 		<script>LocationPicker.notify({ event: 'ready' });</script>
 	</body>
@@ -408,26 +434,26 @@ void LinksController::rowPaintIcon(
 		raw,
 		rpl::duplicate(statusText),
 		st::pickLocationButtonStatus);
-	status->showOn(std::move(
+	status->showOn(rpl::duplicate(
 		statusText
 	) | rpl::map([](const QString &text) {
 		return !text.isEmpty();
 	}) | rpl::distinct_until_changed());
 	rpl::combine(
 		result->widthValue(),
-		status->shownValue()
-	) | rpl::start_with_next([=](int width, bool statusShown) {
+		std::move(statusText)
+	) | rpl::start_with_next([=](int width, const QString &statusText) {
 		const auto available = width
 			- st->namePosition.x()
 			- st->button.padding.right();
 		const auto namePosition = st->namePosition;
 		const auto statusPosition = st->statusPosition;
 		name->resizeToWidth(available);
-		const auto nameTop = statusShown
-			? namePosition.y()
-			: (st->height - name->height()) / 2;
+		const auto nameTop = statusText.isEmpty()
+			? ((st->height - name->height()) / 2)
+			: namePosition.y();
 		name->moveToLeft(namePosition.x(), nameTop, width);
-		status->resizeToWidth(available);
+		status->resizeToNaturalWidth(available);
 		status->moveToLeft(statusPosition.x(), statusPosition.y(), width);
 	}, name->lifetime());
 
@@ -501,6 +527,7 @@ LocationPicker::LocationPicker(Descriptor &&descriptor)
 		_webview->eval("LocationPicker.updateStyles('" + str + "');");
 	}
 })
+, _geocoderResolveTimer([=] { resolveAddressByTimer(); })
 , _venueState(PickerVenueLoading())
 , _session(descriptor.session)
 , _api(&_session->mtp()) {
@@ -660,6 +687,17 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 				const auto lon = object.value("longitude").toDouble();
 				_callback({ lat, lon });
 				close();
+			} else if (event == u"movestart"_q) {
+				_geocoderAddress = QString();
+				_geocoderResolveTimer.cancel();
+			} else if (event == u"moveend"_q) {
+				const auto lat = object.value("latitude").toDouble();
+				const auto lon = object.value("longitude").toDouble();
+				_geocoderResolvePostponed = Core::GeoLocation{
+					.point = { lat, lon },
+					.accuracy = Core::GeoLocationAccuracy::Exact,
+				};
+				_geocoderResolveTimer.callOnce(kResolveAddressDelay);
 			}
 		});
 	});
@@ -714,6 +752,12 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 	raw->navigateToData("location/picker.html");
 }
 
+void LocationPicker::resolveAddressByTimer() {
+	if (const auto location = base::take(_geocoderResolvePostponed)) {
+		resolveAddress(location);
+	}
+}
+
 void LocationPicker::resolveAddress(Core::GeoLocation location) {
 	if (_geocoderResolvingFor == location) {
 		return;
@@ -730,8 +774,14 @@ void LocationPicker::resolveAddress(Core::GeoLocation location) {
 				.arg(location.point.y(), 0, 'f');
 		}
 	};
+	const auto baseLangId = Lang::GetInstance().baseId();
+	const auto langId = baseLangId.isEmpty()
+		? Lang::GetInstance().id()
+		: baseLangId;
+	const auto nonEmptyId = langId.isEmpty() ? u"en"_q : langId;
 	Core::ResolveLocationAddress(
 		location,
+		langId,
 		GeocodingProviderToken,
 		crl::guard(this, done));
 }
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index 9a4ca56d5..e9c0d9801 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "base/invoke_queued.h"
+#include "base/timer.h"
 #include "base/weak_ptr.h"
 #include "core/current_geo_location.h"
 #include "mtproto/sender.h"
@@ -106,6 +107,7 @@ private:
 	void setupWebview(const Descriptor &descriptor);
 	void processKey(const QString &key, const QString &modifier);
 	void resolveCurrentLocation();
+	void resolveAddressByTimer();
 	void resolveAddress(Core::GeoLocation location);
 	void mapReady();
 
@@ -124,6 +126,8 @@ private:
 	SingleQueuedInvokation _updateStyles;
 	bool _subscribedToColors = false;
 
+	base::Timer _geocoderResolveTimer;
+	Core::GeoLocation _geocoderResolvePostponed;
 	Core::GeoLocation _geocoderResolvingFor;
 	rpl::variable<QString> _geocoderAddress;
 

From a130bb1be6d3c3fbae888fd64f062b794afcd56e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 11 Jul 2024 18:15:17 +0200
Subject: [PATCH 040/163] Search for venues by location.

---
 Telegram/Resources/picker_html/picker.css     | 58 ++++++++++++-
 Telegram/Resources/picker_html/picker.js      | 82 ++++++++++++++++++-
 .../SourceFiles/core/current_geo_location.cpp | 27 +++++-
 .../SourceFiles/core/current_geo_location.h   |  6 +-
 .../ui/controls/location_picker.cpp           | 75 +++++++++++------
 .../SourceFiles/ui/controls/location_picker.h |  1 +
 6 files changed, 213 insertions(+), 36 deletions(-)

diff --git a/Telegram/Resources/picker_html/picker.css b/Telegram/Resources/picker_html/picker.css
index 4d8f378ae..ac3d5912b 100644
--- a/Telegram/Resources/picker_html/picker.css
+++ b/Telegram/Resources/picker_html/picker.css
@@ -1,7 +1,5 @@
 :root {
 	--font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, Segoe UI Variable Text, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, tahoma, arial, sans-serif;
-	--font-serif: Iowan Old Style, Apple Garamond, Baskerville, Georgia, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
-	--font-mono: Menlo, Cascadia Code, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
 }
 
 html {
@@ -13,8 +11,6 @@ html {
 
 body {
 	font-family: var(--font-sans);
-	font-size: 17px;
-	line-height: 25px;
 	width: 100%;
 	height: 100%;
 	padding: 0;
@@ -68,3 +64,57 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
 #marker_shadow {
 	position: absolute;
 }
+#search_venues {
+	position: absolute;
+	left: 50%;
+	transform: translateX(-50%);
+	z-index: 2;
+	top: -30px;
+	transition: top 200ms ease-in-out;
+}
+#search_venues.shown {
+	top: 6px;
+}
+#search_venues_inner {
+	position: relative;
+	overflow: hidden;
+	font-size: 13px;
+	font-weight: 500;
+	background: var(--td-window-bg);
+	color: var(--td-window-active-text-fg);
+	cursor: pointer;
+	border-radius: 14px;
+	padding: 5px 12px 6px;
+	box-shadow: 0 0 3px 0px var(--td-history-to-down-shadow);
+}
+#search_venues_inner:hover {
+	background: var(--td-window-bg-over);
+}
+#search_venues_content {
+	position: relative;
+	z-index: 2;
+}
+#search_venues_content:before {
+	content: var(--td-lng-maps-places-in-area);
+}
+#search_venues_inner .ripple .inner {
+	position: absolute;
+	border-radius: 50%;
+	transform: scale(0);
+	opacity: 1;
+	animation: ripple 650ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
+	background-color: var(--td-window-bg-ripple);
+}
+#search_venues_inner .ripple.hiding {
+	animation: fadeOut 200ms linear forwards;
+}
+@keyframes ripple {
+	to {
+		transform: scale(2);
+	}
+}
+@keyframes fadeOut {
+	to {
+		opacity: 0;
+	}
+}
diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js
index 27ffd3b6c..e44fd51a9 100644
--- a/Telegram/Resources/picker_html/picker.js
+++ b/Telegram/Resources/picker_html/picker.js
@@ -72,6 +72,7 @@ var LocationPicker = {
 		LocationPicker.map = new mapboxgl.Map(options);
 		LocationPicker.createMarker(center);
 		LocationPicker.trackMovement();
+		LocationPicker.initSearchVenueRipple();
 	},
 	marker: function() {
 		return document.getElementById('marker_drop');
@@ -93,13 +94,14 @@ var LocationPicker = {
 		LocationPicker.map.on('movestart', function() {
 			LocationPicker.marker().classList.add('moving');
 			LocationPicker.clearMovingTimer();
-			LocationPicker.notify({ event: 'movestart' });
+			LocationPicker.toggleSearchVenues(false);
+			LocationPicker.notify({ event: 'move_start' });
 		});
 		LocationPicker.map.on('moveend', function() {
 			LocationPicker.startMovingTimer(function() {
 				LocationPicker.marker().classList.remove('moving');
 				LocationPicker.notify({
-					event: 'moveend',
+					event: 'move_end',
 					latitude: LocationPicker.map.getCenter().lat,
 					longitude: LocationPicker.map.getCenter().lng
 				});
@@ -119,5 +121,79 @@ var LocationPicker = {
 			latitude: LocationPicker.map.getCenter().lat,
 			longitude: LocationPicker.map.getCenter().lng
 		});
-	}
+	},
+	addRipple: function (button, x, y) {
+		const ripple = document.createElement('span');
+		ripple.classList.add('ripple');
+
+		const inner = document.createElement('span');
+		inner.classList.add('inner');
+
+		var rect = button.getBoundingClientRect();
+		x -= rect.x;
+		y -= rect.y;
+
+		const mx = button.clientWidth - x;
+		const my = button.clientHeight - y;
+		const sq1 = x * x + y * y;
+		const sq2 = mx * mx + y * y;
+		const sq3 = x * x + my * my;
+		const sq4 = mx * mx + my * my;
+		const radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4));
+
+		inner.style.width = inner.style.height = `${2 * radius}px`;
+		inner.style.left = `${x - radius}px`;
+		inner.style.top = `${y - radius}px`;
+		inner.classList.add('inner');
+
+		ripple.addEventListener('animationend', function (e) {
+			if (e.animationName === 'fadeOut') {
+				ripple.remove();
+			}
+		});
+
+		ripple.appendChild(inner);
+		button.appendChild(ripple);
+	},
+	stopRipples: function (button) {
+		const id = button.id ? button.id : button;
+		button = document.getElementById(id);
+		const ripples = button.getElementsByClassName('ripple');
+		for (var i = 0; i < ripples.length; ++i) {
+			const ripple = ripples[i];
+			if (!ripple.classList.contains('hiding')) {
+				ripple.classList.add('hiding');
+			}
+		}
+	},
+	initSearchVenueRipple: function() {
+		var button = document.getElementById('search_venues_inner');
+		button.addEventListener('mousedown', function (e) {
+			LocationPicker.addRipple(e.currentTarget, e.clientX, e.clientY);
+			LocationPicker.searchVenuesPressed = true;
+		});
+		button.addEventListener('mouseup', function (e) {
+			const id = e.currentTarget.id;
+			setTimeout(function () {
+				LocationPicker.stopRipples(id);
+			}, 0);
+			if (LocationPicker.searchVenuesPressed) {
+				LocationPicker.searchVenuesPressed = false;
+				LocationPicker.toggleSearchVenues(false);
+				LocationPicker.notify({
+					event: 'search_venues',
+					latitude: LocationPicker.map.getCenter().lat,
+					longitude: LocationPicker.map.getCenter().lng
+				});
+			}
+		});
+		button.addEventListener('mouseleave', function (e) {
+			LocationPicker.stopRipples(e.currentTarget);
+			LocationPicker.searchVenuesPressed = false;
+		});
+	},
+	toggleSearchVenues: function(shown) {
+		var button = document.getElementById('search_venues');
+		button.classList.toggle('shown', shown);
+	},
 };
diff --git a/Telegram/SourceFiles/core/current_geo_location.cpp b/Telegram/SourceFiles/core/current_geo_location.cpp
index 818ea361a..2ec4a9cfe 100644
--- a/Telegram/SourceFiles/core/current_geo_location.cpp
+++ b/Telegram/SourceFiles/core/current_geo_location.cpp
@@ -148,7 +148,7 @@ void ResolveLocationAddressGeneric(
 				}
 			}
 		};
-		add({ u"address"_q, u"street"_q, u"neighborhood"_q });
+		add({ /*u"address"_q, u"street"_q, */u"neighborhood"_q });
 		add({ u"place"_q, u"region"_q });
 		add({ u"country"_q });
 		finishWith({ .name = names.join(", ") });
@@ -215,4 +215,29 @@ void ResolveLocationAddress(
 	Platform::ResolveLocationAddress(location, language, std::move(done));
 }
 
+bool AreTheSame(const GeoLocation &a, const GeoLocation &b) {
+	if (a.accuracy != GeoLocationAccuracy::Exact
+		|| b.accuracy != GeoLocationAccuracy::Exact) {
+		return false;
+	}
+	const auto normalize = [](float64 value) {
+		value = std::fmod(value + 180., 360.);
+		return (value + (value < 0. ? 360. : 0.)) - 180.;
+	};
+	constexpr auto kEpsilon = 0.0001;
+	const auto lon1 = normalize(a.point.y());
+	const auto lon2 = normalize(b.point.y());
+	const auto diffLat = std::abs(a.point.x() - b.point.x());
+	if (std::abs(a.point.x()) >= (90. - kEpsilon)
+		|| std::abs(b.point.x()) >= (90. - kEpsilon)) {
+		return diffLat <= kEpsilon;
+	}
+	auto diffLon = std::abs(lon1 - lon2);
+	if (diffLon > 180.) {
+		diffLon = 360. - diffLon;
+	}
+
+	return diffLat <= kEpsilon && diffLon <= kEpsilon;
+}
+
 } // namespace Core
diff --git a/Telegram/SourceFiles/core/current_geo_location.h b/Telegram/SourceFiles/core/current_geo_location.h
index dab4ffd00..3b495f115 100644
--- a/Telegram/SourceFiles/core/current_geo_location.h
+++ b/Telegram/SourceFiles/core/current_geo_location.h
@@ -33,12 +33,10 @@ struct GeoLocation {
 	explicit operator bool() const {
 		return !failed();
 	}
-
-	friend inline bool operator==(
-		const GeoLocation&,
-		const GeoLocation&) = default;
 };
 
+[[nodiscard]] bool AreTheSame(const GeoLocation &a, const GeoLocation &b);
+
 struct GeoAddress {
 	QString name;
 
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 2eef9d698..b5920e131 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -102,6 +102,7 @@ VenueRow::VenueRow(
 void VenueRow::update(const VenueData &data) {
 	_data = data;
 	setCustomStatus(data.address);
+	refreshName(st::pickLocationVenueItem);
 }
 
 VenueData VenueRow::data() const {
@@ -128,12 +129,12 @@ PaintRoundImageCallback VenueRow::generatePaintUserpicCallback(
 	};
 }
 
-class LinksController final
+class VenuesController final
 	: public PeerListController
 	, public VenueRowDelegate
 	, public base::has_weak_ptr {
 public:
-	LinksController(
+	VenuesController(
 		not_null<Main::Session*> session,
 		rpl::producer<std::vector<VenueData>> content);
 
@@ -176,21 +177,21 @@ private:
 	return query.trimmed().toLower();
 }
 
-LinksController::LinksController(
+VenuesController::VenuesController(
 	not_null<Main::Session*> session,
 	rpl::producer<std::vector<VenueData>> content)
 : _session(session)
 , _rows(std::move(content)) {
 }
 
-void LinksController::prepare() {
+void VenuesController::prepare() {
 	_rows.value(
 	) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
 		rebuild(rows);
 	}, _lifetime);
 }
 
-void LinksController::rebuild(const std::vector<VenueData> &rows) {
+void VenuesController::rebuild(const std::vector<VenueData> &rows) {
 	auto i = 0;
 	auto count = delegate()->peerListFullRowsCount();
 	while (i < rows.size()) {
@@ -209,24 +210,24 @@ void LinksController::rebuild(const std::vector<VenueData> &rows) {
 	delegate()->peerListRefreshRows();
 }
 
-void LinksController::rowClicked(not_null<PeerListRow*> row) {
+void VenuesController::rowClicked(not_null<PeerListRow*> row) {
 	const auto venue = static_cast<VenueRow*>(row.get())->data();
 	venue;
 }
 
-void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
+void VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {
 	delegate()->peerListShowRowMenu(row, true);
 }
 
-Main::Session &LinksController::session() const {
+Main::Session &VenuesController::session() const {
 	return *_session;
 }
 
-void LinksController::appendRow(const VenueData &data) {
+void VenuesController::appendRow(const VenueData &data) {
 	delegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data));
 }
 
-void LinksController::rowPaintIcon(
+void VenuesController::rowPaintIcon(
 		QPainter &p,
 		int x,
 		int y,
@@ -331,6 +332,7 @@ void LinksController::rowPaintIcon(
 		{ "window-bg-over", &st::windowBgOver },
 		{ "window-bg-ripple", &st::windowBgRipple },
 		{ "window-active-text-fg", &st::windowActiveTextFg },
+		{ "history-to-down-shadow", &st::historyToDownShadow },
 	};
 	static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
 		{ "maps-places-in-area", tr::lng_maps_places_in_area },
@@ -358,6 +360,9 @@ void LinksController::rowPaintIcon(
 		<link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />
 	</head>
 	<body>
+		<div id="search_venues">
+			<div id="search_venues_inner"><span id="search_venues_content"></span></div>
+		</div>
 		<div id="marker">
 			<div id="marker_shadow" style="transform: translate(0px, -14px);">
 <svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
@@ -467,7 +472,7 @@ void SetupVenues(
 	auto &lifetime = container->lifetime();
 	const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
 		show);
-	const auto controller = lifetime.make_state<LinksController>(
+	const auto controller = lifetime.make_state<VenuesController>(
 		&show->session(),
 		std::move(value));
 	controller->setStyleOverrides(&st::pickLocationVenueList);
@@ -687,17 +692,40 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 				const auto lon = object.value("longitude").toDouble();
 				_callback({ lat, lon });
 				close();
-			} else if (event == u"movestart"_q) {
-				_geocoderAddress = QString();
+			} else if (event == u"move_start"_q) {
+				if (const auto now = _geocoderAddress.current()
+					; !now.isEmpty()) {
+					_geocoderSavedAddress = now;
+					_geocoderAddress = QString();
+				}
+				base::take(_geocoderResolvePostponed);
 				_geocoderResolveTimer.cancel();
-			} else if (event == u"moveend"_q) {
+			} else if (event == u"move_end"_q) {
 				const auto lat = object.value("latitude").toDouble();
 				const auto lon = object.value("longitude").toDouble();
-				_geocoderResolvePostponed = Core::GeoLocation{
+				const auto location = Core::GeoLocation{
 					.point = { lat, lon },
 					.accuracy = Core::GeoLocationAccuracy::Exact,
 				};
-				_geocoderResolveTimer.callOnce(kResolveAddressDelay);
+				if (AreTheSame(_geocoderResolvingFor, location)
+					&& !_geocoderSavedAddress.isEmpty()) {
+					_geocoderAddress = base::take(_geocoderSavedAddress);
+					_geocoderResolveTimer.cancel();
+				} else {
+					_geocoderResolvePostponed = location;
+					_geocoderResolveTimer.callOnce(kResolveAddressDelay);
+				}
+				if (!AreTheSame(_venuesRequestLocation, location)) {
+					_webview->eval(
+						"LocationPicker.toggleSearchVenues(true);");
+				}
+			} else if (event == u"search_venues"_q) {
+				const auto lat = object.value("latitude").toDouble();
+				const auto lon = object.value("longitude").toDouble();
+				venuesRequest({
+					.point = { lat, lon },
+					.accuracy = Core::GeoLocationAccuracy::Exact,
+				});
 			}
 		});
 	});
@@ -759,12 +787,12 @@ void LocationPicker::resolveAddressByTimer() {
 }
 
 void LocationPicker::resolveAddress(Core::GeoLocation location) {
-	if (_geocoderResolvingFor == location) {
+	if (AreTheSame(_geocoderResolvingFor, location)) {
 		return;
 	}
 	_geocoderResolvingFor = location;
 	const auto done = [=](Core::GeoAddress address) {
-		if (_geocoderResolvingFor != location) {
+		if (!AreTheSame(_geocoderResolvingFor, location)) {
 			return;
 		} else if (address) {
 			_geocoderAddress = address.name;
@@ -809,14 +837,13 @@ void LocationPicker::venuesRequest(
 		QString query) {
 	query = NormalizeVenuesQuery(query);
 	auto &cache = _venuesCache[query];
-	const auto i = ranges::find(
-		cache,
-		location,
-		&VenuesCacheEntry::location);
+	const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) {
+		return AreTheSame(v.location, location);
+	});
 	if (i != end(cache)) {
 		_venueState = i->result;
 		return;
-	} else if (_venuesRequestLocation == location
+	} else if (AreTheSame(_venuesRequestLocation, location)
 		&& _venuesRequestQuery == query) {
 		return;
 	} else if (const auto oldRequestId = base::take(_venuesRequestId)) {
@@ -888,7 +915,7 @@ void LocationPicker::resolveCurrentLocation() {
 	using namespace Core;
 	const auto window = _window.get();
 	ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
-		const auto changed = (LastExactLocation != location);
+		const auto changed = !AreTheSame(LastExactLocation, location);
 		if (location.accuracy != GeoLocationAccuracy::Exact || !changed) {
 			return;
 		}
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index e9c0d9801..0adfc166f 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -129,6 +129,7 @@ private:
 	base::Timer _geocoderResolveTimer;
 	Core::GeoLocation _geocoderResolvePostponed;
 	Core::GeoLocation _geocoderResolvingFor;
+	QString _geocoderSavedAddress;
 	rpl::variable<QString> _geocoderAddress;
 
 	rpl::variable<PickerVenueState> _venueState;

From 8ce10d55038e2d025e5008c4d84e2cb3210b1acc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 12 Jul 2024 12:23:47 +0200
Subject: [PATCH 041/163] Send chosen venues.

---
 Telegram/SourceFiles/data/data_location.h     |  4 ++
 .../inline_bots/bot_attach_web_view.cpp       | 33 ++++++++++------
 Telegram/SourceFiles/main/main_app_config.cpp | 26 +++++--------
 Telegram/SourceFiles/main/main_app_config.h   | 11 +++---
 .../ui/controls/location_picker.cpp           | 39 ++++++++++---------
 .../SourceFiles/ui/controls/location_picker.h | 24 ++++++------
 6 files changed, 73 insertions(+), 64 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h
index 10f05adfb..5157f79a1 100644
--- a/Telegram/SourceFiles/data/data_location.h
+++ b/Telegram/SourceFiles/data/data_location.h
@@ -54,6 +54,10 @@ struct InputVenue {
 	QString id;
 	QString venueType;
 
+	[[nodiscard]] bool justLocation() const {
+		return id.isEmpty() && title.isEmpty() && address.isEmpty();
+	}
+
 	friend inline bool operator==(
 		const InputVenue &,
 		const InputVenue &) = default;
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index b9233313c..d52e13b82 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -158,12 +158,16 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
 	return result;
 }
 
-[[nodiscard]] QString ResolveMapsToken(not_null<Main::Session*> session) {
-	return u""_q;
-}
-
-[[nodiscard]] QString ResolveGeocodingToken(not_null<Main::Session*> session) {
-	return u""_q;
+[[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig(
+		not_null<Main::Session*> session) {
+	const auto &appConfig = session->appConfig();
+	auto map = appConfig.get<base::flat_map<QString, QString>>(
+		u"tdesktop_config_map"_q,
+		base::flat_map<QString, QString>());
+	return {
+		.mapsToken = map[u"maps"_q],
+		.geoToken = map[u"geo"_q],
+	};
 }
 
 void ShowChooseBox(
@@ -1806,12 +1810,18 @@ void AttachWebView::toggleInMenu(
 
 void ChooseAndSendLocation(
 		not_null<Window::SessionController*> controller,
+		const Ui::LocationPickerConfig &config,
 		Api::SendAction action) {
-	const auto callback = [=](Ui::LocationInfo info) {
-		Api::SendLocation(action, info.lat, info.lon);
+	const auto callback = [=](Data::InputVenue venue) {
+		if (venue.justLocation()) {
+			Api::SendLocation(action, venue.lat, venue.lon);
+		} else {
+			Api::SendVenue(action, venue);
+		}
 	};
 	Ui::LocationPicker::Show({
 		.parent = controller->widget(),
+		.config = config,
 		.session = &controller->session(),
 		.callback = crl::guard(controller, callback),
 		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
@@ -1876,12 +1886,11 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
 	}
 	const auto session = &controller->session();
 	const auto locationType = ChatRestriction::SendOther;
+	const auto config = ResolveMapsConfig(session);
 	if (Data::CanSendAnyOf(peer, locationType)
-		&& Ui::LocationPicker::Available(
-			ResolveMapsToken(session),
-			ResolveGeocodingToken(session))) {
+		&& Ui::LocationPicker::Available(config)) {
 		raw->addAction(tr::lng_maps_point(tr::now), [=] {
-			ChooseAndSendLocation(controller, actionFactory());
+			ChooseAndSendLocation(controller, config, actionFactory());
 		}, &st::menuIconAddress);
 	}
 	for (const auto &bot : bots->attachBots()) {
diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp
index 2507dc8f1..6a4c63c35 100644
--- a/Telegram/SourceFiles/main/main_app_config.cpp
+++ b/Telegram/SourceFiles/main/main_app_config.cpp
@@ -144,28 +144,22 @@ std::vector<QString> AppConfig::getStringArray(
 	});
 }
 
-std::vector<std::map<QString, QString>> AppConfig::getStringMapArray(
+base::flat_map<QString, QString> AppConfig::getStringMap(
 		const QString &key,
-		std::vector<std::map<QString, QString>> &&fallback) const {
+		base::flat_map<QString, QString> &&fallback) const {
 	return getValue(key, [&](const MTPJSONValue &value) {
-		return value.match([&](const MTPDjsonArray &data) {
-			auto result = std::vector<std::map<QString, QString>>();
+		return value.match([&](const MTPDjsonObject &data) {
+			auto result = base::flat_map<QString, QString>();
 			result.reserve(data.vvalue().v.size());
 			for (const auto &entry : data.vvalue().v) {
-				if (entry.type() != mtpc_jsonObject) {
+				const auto &data = entry.data();
+				const auto &value = data.vvalue();
+				if (value.type() != mtpc_jsonString) {
 					return std::move(fallback);
 				}
-				auto element = std::map<QString, QString>();
-				for (const auto &field : entry.c_jsonObject().vvalue().v) {
-					const auto &data = field.c_jsonObjectValue();
-					if (data.vvalue().type() != mtpc_jsonString) {
-						return std::move(fallback);
-					}
-					element.emplace(
-						qs(data.vkey()),
-						qs(data.vvalue().c_jsonString().vvalue()));
-				}
-				result.push_back(std::move(element));
+				result.emplace(
+					qs(data.vkey()),
+					qs(value.c_jsonString().vvalue()));
 			}
 			return result;
 		}, [&](const auto &data) {
diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h
index 67b985348..f09d5f120 100644
--- a/Telegram/SourceFiles/main/main_app_config.h
+++ b/Telegram/SourceFiles/main/main_app_config.h
@@ -35,12 +35,11 @@ public:
 			return getString(key, fallback);
 		} else if constexpr (std::is_same_v<Type, std::vector<QString>>) {
 			return getStringArray(key, std::move(fallback));
+		} else if constexpr (
+				std::is_same_v<Type, base::flat_map<QString, QString>>) {
+			return getStringMap(key, std::move(fallback));
 		} else if constexpr (std::is_same_v<Type, std::vector<int>>) {
 			return getIntArray(key, std::move(fallback));
-		} else if constexpr (std::is_same_v<
-				Type,
-				std::vector<std::map<QString, QString>>>) {
-			return getStringMapArray(key, std::move(fallback));
 		} else if constexpr (std::is_same_v<Type, bool>) {
 			return getBool(key, fallback);
 		}
@@ -78,9 +77,9 @@ private:
 	[[nodiscard]] std::vector<QString> getStringArray(
 		const QString &key,
 		std::vector<QString> &&fallback) const;
-	[[nodiscard]] std::vector<std::map<QString, QString>> getStringMapArray(
+	[[nodiscard]] base::flat_map<QString, QString> getStringMap(
 		const QString &key,
-		std::vector<std::map<QString, QString>> &&fallback) const;
+		base::flat_map<QString, QString> &&fallback) const;
 	[[nodiscard]] std::vector<int> getIntArray(
 		const QString &key,
 		std::vector<int> &&fallback) const;
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index b5920e131..9d926f980 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -56,8 +56,6 @@ const auto kProtocolOverride = "";
 #endif // Q_OS_MAC
 
 Core::GeoLocation LastExactLocation;
-QString MapsProviderToken;
-QString GeocodingProviderToken;
 
 using VenueData = Data::InputVenue;
 
@@ -136,7 +134,8 @@ class VenuesController final
 public:
 	VenuesController(
 		not_null<Main::Session*> session,
-		rpl::producer<std::vector<VenueData>> content);
+		rpl::producer<std::vector<VenueData>> content,
+		Fn<void(VenueData)> callback);
 
 	void prepare() override;
 	void rowClicked(not_null<PeerListRow*> row) override;
@@ -165,6 +164,7 @@ private:
 	void rebuild(const std::vector<VenueData> &rows);
 
 	const not_null<Main::Session*> _session;
+	const Fn<void(VenueData)> _callback;
 	rpl::variable<std::vector<VenueData>> _rows;
 
 	base::flat_map<QString, VenueIcon> _icons;
@@ -179,8 +179,10 @@ private:
 
 VenuesController::VenuesController(
 	not_null<Main::Session*> session,
-	rpl::producer<std::vector<VenueData>> content)
+	rpl::producer<std::vector<VenueData>> content,
+	Fn<void(VenueData)> callback)
 : _session(session)
+, _callback(std::move(callback))
 , _rows(std::move(content)) {
 }
 
@@ -211,8 +213,7 @@ void VenuesController::rebuild(const std::vector<VenueData> &rows) {
 }
 
 void VenuesController::rowClicked(not_null<PeerListRow*> row) {
-	const auto venue = static_cast<VenueRow*>(row.get())->data();
-	venue;
+	_callback(static_cast<VenueRow*>(row.get())->data());
 }
 
 void VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {
@@ -468,13 +469,15 @@ void VenuesController::rowPaintIcon(
 void SetupVenues(
 		not_null<VerticalLayout*> container,
 		std::shared_ptr<Main::SessionShow> show,
-		rpl::producer<std::vector<VenueData>> value) {
+		rpl::producer<std::vector<VenueData>> value,
+		Fn<void(VenueData)> callback) {
 	auto &lifetime = container->lifetime();
 	const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
 		show);
 	const auto controller = lifetime.make_state<VenuesController>(
 		&show->session(),
-		std::move(value));
+		std::move(value),
+		std::move(callback));
 	controller->setStyleOverrides(&st::pickLocationVenueList);
 	const auto content = container->add(object_ptr<PeerListContent>(
 		container,
@@ -520,7 +523,8 @@ void SetupVenues(
 } // namespace
 
 LocationPicker::LocationPicker(Descriptor &&descriptor)
-: _callback(std::move(descriptor.callback))
+: _config(std::move(descriptor.config))
+, _callback(std::move(descriptor.callback))
 , _quit(std::move(descriptor.quit))
 , _window(std::make_unique<SeparatePanel>())
 , _body((_window->setInnerSize(st::pickLocationWindow)
@@ -550,13 +554,9 @@ std::shared_ptr<Main::SessionShow> LocationPicker::uiShow() {
 	return Main::MakeSessionShow(nullptr, _session);
 }
 
-bool LocationPicker::Available(
-		const QString &mapsToken,
-		const QString &geocodingToken) {
+bool LocationPicker::Available(const LocationPickerConfig &config) {
 	static const auto Supported = Webview::NavigateToDataSupported();
-	MapsProviderToken = mapsToken;
-	GeocodingProviderToken = geocodingToken;
-	return Supported && !MapsProviderToken.isEmpty();
+	return Supported && !config.mapsToken.isEmpty();
 }
 
 void LocationPicker::setup(const Descriptor &descriptor) {
@@ -606,7 +606,10 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 		return v::is<PickerVenueList>(state);
 	}) | rpl::map([=](PickerVenueState &&state) {
 		return std::move(v::get<PickerVenueList>(state).list);
-	}));
+	}), [=](VenueData info) {
+		_callback(std::move(info));
+		close();
+	});
 
 	rpl::combine(
 		_body->sizeValue(),
@@ -810,14 +813,14 @@ void LocationPicker::resolveAddress(Core::GeoLocation location) {
 	Core::ResolveLocationAddress(
 		location,
 		langId,
-		GeocodingProviderToken,
+		_config.geoToken,
 		crl::guard(this, done));
 }
 
 void LocationPicker::mapReady() {
 	Expects(_scroll != nullptr);
 
-	const auto token = MapsProviderToken.toUtf8();
+	const auto token = _config.mapsToken.toUtf8();
 	const auto center = DefaultCenter();
 	const auto bounds = DefaultBounds();
 	const auto protocol = *kProtocolOverride
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index 0adfc166f..b7996ee50 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -33,11 +33,6 @@ class SeparatePanel;
 class RpWidget;
 class ScrollArea;
 
-struct LocationInfo {
-	float64 lat = 0.;
-	float64 lon = 0.;
-};
-
 struct PickerVenueLoading {
 	friend inline bool operator==(
 		PickerVenueLoading,
@@ -72,20 +67,24 @@ using PickerVenueState = std::variant<
 	PickerVenueWaitingForLocation,
 	PickerVenueList>;
 
+struct LocationPickerConfig {
+	QString mapsToken;
+	QString geoToken;
+};
+
 class LocationPicker final : public base::has_weak_ptr {
 public:
 	struct Descriptor {
 		RpWidget *parent = nullptr;
+		LocationPickerConfig config;
 		not_null<Main::Session*> session;
-		Fn<void(LocationInfo)> callback;
+		Fn<void(Data::InputVenue)> callback;
 		Fn<void()> quit;
 		Webview::StorageId storageId;
 		rpl::producer<> closeRequests;
 	};
 
-	[[nodiscard]] static bool Available(
-		const QString &mapsToken,
-		const QString &geocodingToken);
+	[[nodiscard]] static bool Available(const LocationPickerConfig &config);
 	static not_null<LocationPicker*> Show(Descriptor &&descriptor);
 
 	void close();
@@ -114,9 +113,8 @@ private:
 	void venuesRequest(Core::GeoLocation location, QString query = {});
 	void venuesSendRequest();
 
-	rpl::lifetime _lifetime;
-
-	Fn<void(LocationInfo)> _callback;
+	LocationPickerConfig _config;
+	Fn<void(Data::InputVenue)> _callback;
 	Fn<void()> _quit;
 	std::unique_ptr<SeparatePanel> _window;
 	not_null<RpWidget*> _body;
@@ -143,6 +141,8 @@ private:
 	QString _venuesRequestQuery;
 	base::flat_map<QString, std::vector<VenuesCacheEntry>> _venuesCache;
 
+	rpl::lifetime _lifetime;
+
 };
 
 } // namespace Ui

From 917d1841c1ce1a6ca8afb78c14e891f4034d16a7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 12 Jul 2024 15:07:08 +0200
Subject: [PATCH 042/163] Try better webview focusing.

---
 Telegram/SourceFiles/iv/iv_controller.cpp     | 15 ++++++-------
 .../ui/chat/attach/attach_bot_webview.cpp     | 21 +++++++++++--------
 2 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 4161fe876..e72298f6c 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/platform/base_platform_info.h"
 #include "base/invoke_queued.h"
-#include "base/qt_signal_producer.h"
 #include "base/qthelp_url.h"
 #include "iv/iv_data.h"
 #include "lang/lang_keys.h"
@@ -281,12 +280,14 @@ void Controller::createWindow() {
 	_window = std::make_unique<Ui::RpWindow>();
 	const auto window = _window.get();
 
-	base::qt_signal_producer(
-		window->window()->windowHandle(),
-		&QWindow::activeChanged
-	) | rpl::filter([=] {
-		return _webview && window->window()->windowHandle()->isActive();
-	}) | rpl::start_with_next([=] {
+	window->windowActiveValue(
+	) | rpl::map([=] {
+		const auto handle = window->window()->windowHandle();
+		return (_shareFocus || _webview) && handle && handle->isActive();
+	}) | rpl::distinct_until_changed(
+	) | rpl::filter(
+		rpl::mappers::_1
+	) | rpl::start_with_next([=] {
 		setInnerFocus();
 	}, window->lifetime());
 
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index d367e3382..cbc27c0b8 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -739,15 +739,18 @@ postEvent: function(eventType, eventData) {
 
 	setupProgressGeometry();
 
-	base::qt_signal_producer(
-		_widget->window()->windowHandle(),
-		&QWindow::activeChanged
-	) | rpl::filter([=] {
-		return _webview && _widget->window()->windowHandle()->isActive();
-	}) | rpl::start_with_next([=] {
-		if (_webview && !_webview->window.widget()->isHidden()) {
-			_webview->window.focus();
-		}
+	_widget->windowActiveValue(
+	) | rpl::map([=] {
+		const auto handle = _widget->window()->windowHandle();
+		return _webview
+			&& !_webview->window.widget()->isHidden()
+			&& handle
+			&& handle->isActive();
+	}) | rpl::distinct_until_changed(
+	) | rpl::filter(
+		rpl::mappers::_1
+	) | rpl::start_with_next([=] {
+		_webview->window.focus();
 	}, _webview->lifetime);
 
 	return true;

From b4dfc25df5609adf0b140eaccbda347c2d8e2569 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 12 Jul 2024 15:07:21 +0200
Subject: [PATCH 043/163] Implement venues search.

---
 .../ui/controls/location_picker.cpp           | 140 ++++++++++++++----
 .../SourceFiles/ui/controls/location_picker.h |  14 ++
 Telegram/lib_ui                               |   2 +-
 3 files changed, 127 insertions(+), 29 deletions(-)

diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 9d926f980..d363bbe6d 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/scroll_area.h"
 #include "ui/widgets/separate_panel.h"
 #include "ui/widgets/buttons.h"
+#include "ui/wrap/slide_wrap.h"
 #include "ui/wrap/vertical_layout.h"
 #include "ui/painter.h"
 #include "ui/vertical_list.h"
@@ -48,6 +49,7 @@ namespace Ui {
 namespace {
 
 constexpr auto kResolveAddressDelay = 3 * crl::time(1000);
+constexpr auto kSearchDebounceDelay = crl::time(900);
 
 #ifdef Q_OS_MAC
 const auto kProtocolOverride = "mapboxapihelper";
@@ -539,6 +541,12 @@ LocationPicker::LocationPicker(Descriptor &&descriptor)
 , _geocoderResolveTimer([=] { resolveAddressByTimer(); })
 , _venueState(PickerVenueLoading())
 , _session(descriptor.session)
+, _venuesSearchDebounceTimer([=] {
+	Expects(_venuesSearchLocation.has_value());
+	Expects(_venuesSearchQuery.has_value());
+
+	venuesRequest(*_venuesSearchLocation, *_venuesSearchQuery);
+})
 , _api(&_session->mtp()) {
 	std::move(
 		descriptor.closeRequests
@@ -565,6 +573,7 @@ void LocationPicker::setup(const Descriptor &descriptor) {
 	if (LastExactLocation) {
 		venuesRequest(LastExactLocation);
 		resolveAddress(LastExactLocation);
+		venuesSearchEnableAt(LastExactLocation);
 	}
 }
 
@@ -588,18 +597,27 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	_scroll = CreateChild<ScrollArea>(_body.get());
 	const auto controls = _scroll->setOwnedWidget(
 		object_ptr<VerticalLayout>(_scroll));
-	const auto toppad = controls->add(object_ptr<RpWidget>(controls));
 
-	const auto button = controls->add(
-		MakeSendLocationButton(controls, _geocoderAddress.value()),
+	_mapControlsWrap = controls->add(
+		object_ptr<SlideWrap<VerticalLayout>>(
+			controls,
+			object_ptr<VerticalLayout>(controls))
+	)->setDuration(0);
+	_mapControlsWrap->show(anim::type::instant);
+	const auto mapControls = _mapControlsWrap->entity();
+
+	const auto toppad = mapControls->add(object_ptr<RpWidget>(controls));
+
+	const auto button = mapControls->add(
+		MakeSendLocationButton(mapControls, _geocoderAddress.value()),
 		{ 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip });
 	button->setClickedCallback([=] {
 		_webview->eval("LocationPicker.send();");
 	});
 
-	AddDivider(controls);
-	AddSkip(controls);
-	AddSubsectionTitle(controls, tr::lng_maps_or_choose());
+	AddDivider(mapControls);
+	AddSkip(mapControls);
+	AddSubsectionTitle(mapControls, tr::lng_maps_or_choose());
 
 	SetupVenues(controls, uiShow(), _venueState.value(
 	) | rpl::filter([=](const PickerVenueState &state) {
@@ -613,17 +631,19 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 
 	rpl::combine(
 		_body->sizeValue(),
-		_scroll->scrollTopValue()
-	) | rpl::start_with_next([=](QSize size, int scrollTop) {
+		_scroll->scrollTopValue(),
+		_venuesSearchShown.value()
+	) | rpl::start_with_next([=](QSize size, int scrollTop, bool search) {
 		const auto width = size.width();
 		const auto height = size.height();
 		const auto sub = std::min(
 			(st::pickLocationMapHeight - st::pickLocationCollapsedHeight),
 			scrollTop);
 		const auto mapHeight = st::pickLocationMapHeight - sub;
-		const auto scrollHeight = height - mapHeight;
 		_container->setGeometry(0, 0, width, mapHeight);
-		_scroll->setGeometry(0, mapHeight, width, scrollHeight);
+		const auto scrollWidgetTop = search ? 0 : mapHeight;
+		const auto scrollHeight = height - scrollWidgetTop;
+		_scroll->setGeometry(0, scrollWidgetTop, width, scrollHeight);
 		controls->resizeToWidth(width);
 		toppad->resize(width, sub);
 	}, _container->lifetime());
@@ -662,7 +682,7 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 			close();
 		} else if (e->type() == QEvent::KeyPress) {
 			const auto event = static_cast<QKeyEvent*>(e.get());
-			if (event->key() == Qt::Key_Escape) {
+			if (event->key() == Qt::Key_Escape && !_venuesSearchQuery) {
 				close();
 			}
 		}
@@ -722,6 +742,7 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 					_webview->eval(
 						"LocationPicker.toggleSearchVenues(true);");
 				}
+				venuesSearchEnableAt(location);
 			} else if (event == u"search_venues"_q) {
 				const auto lat = object.value("latitude").toDouble();
 				const auto lon = object.value("longitude").toDouble();
@@ -835,26 +856,38 @@ void LocationPicker::mapReady() {
 	_scroll->show();
 }
 
-void LocationPicker::venuesRequest(
+bool LocationPicker::venuesFromCache(
 		Core::GeoLocation location,
 		QString query) {
-	query = NormalizeVenuesQuery(query);
-	auto &cache = _venuesCache[query];
+	const auto normalized = NormalizeVenuesQuery(query);
+	auto &cache = _venuesCache[normalized];
 	const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) {
 		return AreTheSame(v.location, location);
 	});
-	if (i != end(cache)) {
-		_venueState = i->result;
-		return;
-	} else if (AreTheSame(_venuesRequestLocation, location)
-		&& _venuesRequestQuery == query) {
+	if (i == end(cache)) {
+		return false;
+	}
+	_venuesRequestLocation = location;
+	_venuesRequestQuery = normalized;
+	_venuesInitialQuery = query;
+	venuesApplyResults(i->result);
+	return true;
+}
+
+void LocationPicker::venuesRequest(
+		Core::GeoLocation location,
+		QString query) {
+	const auto normalized = NormalizeVenuesQuery(query);
+	if (AreTheSame(_venuesRequestLocation, location)
+		&& _venuesRequestQuery == normalized) {
 		return;
 	} else if (const auto oldRequestId = base::take(_venuesRequestId)) {
 		_api.request(oldRequestId).cancel();
 	}
 	_venueState = PickerVenueLoading();
 	_venuesRequestLocation = location;
-	_venuesRequestQuery = query;
+	_venuesRequestQuery = normalized;
+	_venuesInitialQuery = query;
 	if (_venuesBot) {
 		venuesSendRequest();
 	} else if (_venuesBotRequestId) {
@@ -904,16 +937,61 @@ void LocationPicker::venuesSendRequest() {
 			.location = _venuesRequestLocation,
 			.result = parsed,
 		});
-		if (parsed.list.empty()) {
-			_venueState = PickerVenueNothingFound{ _venuesRequestQuery };
-		} else {
-			_venueState = std::move(parsed);
-		}
+		venuesApplyResults(std::move(parsed));
 	}).fail([=] {
-		_venueState = PickerVenueNothingFound{ _venuesRequestQuery };
+		venuesApplyResults({});
 	}).send();
 }
 
+void LocationPicker::venuesApplyResults(PickerVenueList venues) {
+	_venuesRequestId = 0;
+	if (venues.list.empty()) {
+		_venueState = PickerVenueNothingFound{ _venuesInitialQuery };
+	} else {
+		_venueState = std::move(venues);
+	}
+}
+
+void LocationPicker::venuesSearchEnableAt(Core::GeoLocation location) {
+	if (!_venuesSearchLocation) {
+		_window->setSearchAllowed(
+			tr::lng_dlg_filter(),
+			[=](std::optional<QString> query) {
+				venuesSearchChanged(query);
+			});
+	}
+	_venuesSearchLocation = location;
+}
+
+void LocationPicker::venuesSearchChanged(
+		const std::optional<QString> &query) {
+	_venuesSearchQuery = query;
+
+	const auto shown = query && !query->trimmed().isEmpty();
+	_venuesSearchShown = shown;
+	if (_container->isHidden() != shown) {
+		_container->setVisible(!shown);
+		_mapControlsWrap->toggle(!shown, anim::type::instant);
+		if (shown) {
+			_venuesNoSearchLocation = _venuesRequestLocation;
+			_venueState = PickerVenueLoading();
+		} else if (_venuesNoSearchLocation) {
+			if (!venuesFromCache(_venuesNoSearchLocation)) {
+				venuesRequest(_venuesNoSearchLocation);
+			}
+		}
+	}
+
+	if (shown
+		&& !venuesFromCache(
+			*_venuesSearchLocation,
+			*_venuesSearchQuery)) {
+		_venuesSearchDebounceTimer.callOnce(kSearchDebounceDelay);
+	} else {
+		_venuesSearchDebounceTimer.cancel();
+	}
+}
+
 void LocationPicker::resolveCurrentLocation() {
 	using namespace Core;
 	const auto window = _window.get();
@@ -924,7 +1002,9 @@ void LocationPicker::resolveCurrentLocation() {
 		}
 		LastExactLocation = location;
 		if (location) {
-			venuesRequest(location);
+			if (_venuesSearchQuery.value_or(QString()).isEmpty()) {
+				venuesRequest(location);
+			}
 			resolveAddress(location);
 		}
 		if (_webview) {
@@ -940,7 +1020,11 @@ void LocationPicker::processKey(
 		const QString &key,
 		const QString &modifier) {
 	const auto ctrl = ::Platform::IsMac() ? u"cmd"_q : u"ctrl"_q;
-	if (key == u"escape"_q || (key == u"w"_q && modifier == ctrl)) {
+	if (key == u"escape"_q) {
+		if (!_window->closeSearch()) {
+			close();
+		}
+	} else if (key == u"w"_q && modifier == ctrl) {
 		close();
 	} else if (key == u"m"_q && modifier == ctrl) {
 		minimize();
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index b7996ee50..6014bd06b 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -32,6 +32,9 @@ namespace Ui {
 class SeparatePanel;
 class RpWidget;
 class ScrollArea;
+class VerticalLayout;
+template <typename Widget>
+class SlideWrap;
 
 struct PickerVenueLoading {
 	friend inline bool operator==(
@@ -110,8 +113,12 @@ private:
 	void resolveAddress(Core::GeoLocation location);
 	void mapReady();
 
+	bool venuesFromCache(Core::GeoLocation location, QString query = {});
 	void venuesRequest(Core::GeoLocation location, QString query = {});
 	void venuesSendRequest();
+	void venuesApplyResults(PickerVenueList venues);
+	void venuesSearchEnableAt(Core::GeoLocation location);
+	void venuesSearchChanged(const std::optional<QString> &query);
 
 	LocationPickerConfig _config;
 	Fn<void(Data::InputVenue)> _callback;
@@ -119,6 +126,7 @@ private:
 	std::unique_ptr<SeparatePanel> _window;
 	not_null<RpWidget*> _body;
 	RpWidget *_container = nullptr;
+	SlideWrap<VerticalLayout> *_mapControlsWrap = nullptr;
 	ScrollArea *_scroll = nullptr;
 	std::unique_ptr<Webview::Window> _webview;
 	SingleQueuedInvokation _updateStyles;
@@ -133,13 +141,19 @@ private:
 	rpl::variable<PickerVenueState> _venueState;
 
 	const not_null<Main::Session*> _session;
+	std::optional<Core::GeoLocation> _venuesSearchLocation;
+	std::optional<QString> _venuesSearchQuery;
+	base::Timer _venuesSearchDebounceTimer;
 	MTP::Sender _api;
 	UserData *_venuesBot = nullptr;
 	mtpRequestId _venuesBotRequestId = 0;
 	mtpRequestId _venuesRequestId = 0;
 	Core::GeoLocation _venuesRequestLocation;
 	QString _venuesRequestQuery;
+	QString _venuesInitialQuery;
 	base::flat_map<QString, std::vector<VenuesCacheEntry>> _venuesCache;
+	Core::GeoLocation _venuesNoSearchLocation;
+	rpl::variable<bool> _venuesSearchShown = false;
 
 	rpl::lifetime _lifetime;
 
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 9b69f3855..a95caea1a 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 9b69f3855ac9feb760aef92ce98e874129303d4d
+Subproject commit a95caea1adac69ca6d95b55fe920eac33cdf9580

From 8aac07b3c0f21d317af684f6852924bdf7fe8811 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 12 Jul 2024 16:34:14 +0200
Subject: [PATCH 044/163] Show empty venue search states.

---
 Telegram/Resources/langs/lang.strings         |   1 +
 .../chat_helpers/chat_helpers.style           |   5 +
 .../ui/controls/location_picker.cpp           | 104 ++++++++++++++++--
 Telegram/lib_ui                               |   2 +-
 4 files changed, 100 insertions(+), 12 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index b15436b2b..08368d748 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3198,6 +3198,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_maps_or_choose" = "Or choose a venue";
 "lng_maps_places_in_area" = "Places in this area";
 "lng_maps_no_places" = "No places found";
+"lng_maps_choose_to_search" = "Choose location to see places nearby.";
 "lng_live_location" = "Live Location";
 "lng_live_location_now" = "updated just now";
 "lng_live_location_minutes#one" = "updated {count} minute ago";
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 452a9f89b..d31d2919a 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1476,3 +1476,8 @@ pickLocationVenueList: PeerList(defaultPeerList) {
 	padding: margins(0px, 0px, 0px, 0px);
 }
 pickLocationIconSkip: 6px;
+pickLocationLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
+	size: size(56px, 56px);
+	color: windowSubTextFg;
+	thickness: 4px;
+}
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index d363bbe6d..ed9d84f7a 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -17,11 +17,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_location.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
+#include "dialogs/ui/chat_search_empty.h" // Dialogs::SearchEmpty.
 #include "lang/lang_instance.h"
 #include "lang/lang_keys.h"
 #include "main/session/session_show.h"
 #include "main/main_session.h"
 #include "mtproto/mtproto_config.h"
+#include "ui/effects/radial_animation.h"
+#include "ui/text/text_utilities.h"
 #include "ui/widgets/scroll_area.h"
 #include "ui/widgets/separate_panel.h"
 #include "ui/widgets/buttons.h"
@@ -468,17 +471,95 @@ void VenuesController::rowPaintIcon(
 	return result;
 }
 
+void SetupLoadingView(not_null<RpWidget*> container) {
+	class Loading final : public RpWidget {
+	public:
+		explicit Loading(QWidget *parent)
+		: RpWidget(parent)
+		, animation(
+				[=] { if (!anim::Disabled()) update(); },
+				st::pickLocationLoading) {
+			animation.start(st::pickLocationLoading.sineDuration);
+		}
+
+	private:
+		void paintEvent(QPaintEvent *e) override {
+			auto p = QPainter(this);
+			const auto size = st::pickLocationLoading.size;
+			const auto inner = QRect(QPoint(), size);
+			const auto positioned = style::centerrect(rect(), inner);
+			animation.draw(p, positioned.topLeft(), size, width());
+		}
+
+		InfiniteRadialAnimation animation;
+
+	};
+
+	const auto view = CreateChild<Loading>(container);
+	view->resize(container->width(), st::recentPeersEmptyHeightMin);
+	view->show();
+
+	ResizeFitChild(container, view);
+}
+
+void SetupEmptyView(
+		not_null<RpWidget*> container,
+		std::optional<QString> query) {
+	using Icon = Dialogs::SearchEmptyIcon;
+	const auto view = CreateChild<Dialogs::SearchEmpty>(
+		container,
+		(query ? Icon::NoResults : Icon::Search),
+		(query
+			? tr::lng_maps_no_places
+			: tr::lng_maps_choose_to_search)(Ui::Text::WithEntities));
+	view->setMinimalHeight(st::recentPeersEmptyHeightMin);
+	view->show();
+
+	ResizeFitChild(container, view);
+
+	InvokeQueued(view, [=] { view->animate(); });
+}
+
 void SetupVenues(
 		not_null<VerticalLayout*> container,
 		std::shared_ptr<Main::SessionShow> show,
-		rpl::producer<std::vector<VenueData>> value,
+		rpl::producer<PickerVenueState> value,
 		Fn<void(VenueData)> callback) {
+	const auto otherWrap = container->add(object_ptr<SlideWrap<RpWidget>>(
+		container,
+		object_ptr<RpWidget>(container)));
+	const auto other = otherWrap->entity();
+	rpl::duplicate(
+		value
+	) | rpl::start_with_next([=](const PickerVenueState &state) {
+		while (!other->children().isEmpty()) {
+			delete other->children()[0];
+		}
+		if (v::is<PickerVenueList>(state)) {
+			otherWrap->hide(anim::type::instant);
+			return;
+		} else if (v::is<PickerVenueLoading>(state)) {
+			SetupLoadingView(other);
+		} else {
+			const auto n = std::get_if<PickerVenueNothingFound>(&state);
+			SetupEmptyView(other, n ? n->query : std::optional<QString>());
+		}
+		otherWrap->show(anim::type::instant);
+	}, otherWrap->lifetime());
+
 	auto &lifetime = container->lifetime();
+	auto venuesList = rpl::duplicate(
+		value
+	) | rpl::map([=](PickerVenueState &&state) {
+		return v::is<PickerVenueList>(state)
+			? std::move(v::get<PickerVenueList>(state).list)
+			: std::vector<VenueData>();
+	});
 	const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
 		show);
 	const auto controller = lifetime.make_state<VenuesController>(
 		&show->session(),
-		std::move(value),
+		std::move(venuesList),
 		std::move(callback));
 	controller->setStyleOverrides(&st::pickLocationVenueList);
 	const auto content = container->add(object_ptr<PeerListContent>(
@@ -601,8 +682,7 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	_mapControlsWrap = controls->add(
 		object_ptr<SlideWrap<VerticalLayout>>(
 			controls,
-			object_ptr<VerticalLayout>(controls))
-	)->setDuration(0);
+			object_ptr<VerticalLayout>(controls)));
 	_mapControlsWrap->show(anim::type::instant);
 	const auto mapControls = _mapControlsWrap->entity();
 
@@ -619,12 +699,8 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	AddSkip(mapControls);
 	AddSubsectionTitle(mapControls, tr::lng_maps_or_choose());
 
-	SetupVenues(controls, uiShow(), _venueState.value(
-	) | rpl::filter([=](const PickerVenueState &state) {
-		return v::is<PickerVenueList>(state);
-	}) | rpl::map([=](PickerVenueState &&state) {
-		return std::move(v::get<PickerVenueList>(state).list);
-	}), [=](VenueData info) {
+	auto state = _venueState.value();
+	SetupVenues(controls, uiShow(), std::move(state), [=](VenueData info) {
 		_callback(std::move(info));
 		close();
 	});
@@ -706,6 +782,9 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 			if (event == u"ready"_q) {
 				mapReady();
 				resolveCurrentLocation();
+				if (_webview) {
+					_webview->focus();
+				}
 			} else if (event == u"keydown"_q) {
 				const auto key = object.value("key").toString();
 				const auto modifier = object.value("modifier").toString();
@@ -974,7 +1053,6 @@ void LocationPicker::venuesSearchChanged(
 		_mapControlsWrap->toggle(!shown, anim::type::instant);
 		if (shown) {
 			_venuesNoSearchLocation = _venuesRequestLocation;
-			_venueState = PickerVenueLoading();
 		} else if (_venuesNoSearchLocation) {
 			if (!venuesFromCache(_venuesNoSearchLocation)) {
 				venuesRequest(_venuesNoSearchLocation);
@@ -986,6 +1064,7 @@ void LocationPicker::venuesSearchChanged(
 		&& !venuesFromCache(
 			*_venuesSearchLocation,
 			*_venuesSearchQuery)) {
+		_venueState = PickerVenueLoading();
 		_venuesSearchDebounceTimer.callOnce(kSearchDebounceDelay);
 	} else {
 		_venuesSearchDebounceTimer.cancel();
@@ -998,6 +1077,9 @@ void LocationPicker::resolveCurrentLocation() {
 	ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
 		const auto changed = !AreTheSame(LastExactLocation, location);
 		if (location.accuracy != GeoLocationAccuracy::Exact || !changed) {
+			if (!_venuesSearchLocation) {
+				_venueState = PickerVenueWaitingForLocation();
+			}
 			return;
 		}
 		LastExactLocation = location;
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index a95caea1a..f046725ec 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit a95caea1adac69ca6d95b55fe920eac33cdf9580
+Subproject commit f046725ecde41bf5779d69393cc592786ee0440c

From b83b403b7581d7c691f367cf7cc9e2180616be5c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 12 Jul 2024 17:00:16 +0200
Subject: [PATCH 045/163] Pass correct peer to venue search.

---
 .../chat_helpers/chat_helpers.style           | 35 ++++++++-----------
 .../inline_bots/bot_attach_web_view.cpp       |  1 +
 .../ui/controls/location_picker.cpp           |  7 ++--
 .../SourceFiles/ui/controls/location_picker.h |  2 ++
 4 files changed, 21 insertions(+), 24 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index d31d2919a..a40d1e0ad 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1428,21 +1428,6 @@ pickLocationWindow: size(364px, 680px);
 pickLocationMapHeight: 220px;
 pickLocationCollapsedHeight: 92px;
 pickLocationRowHeight: 52px;
-pickLocationVenue: PeerListItem(defaultPeerListItem) {
-	height: pickLocationRowHeight;
-	photoSize: 42px;
-	photoPosition: point(18px, 5px);
-	namePosition: point(70px, 9px);
-	statusPosition: point(70px, 29px);
-	button: OutlineButton(defaultPeerListButton) {
-		textBg: contactsBg;
-		textBgOver: contactsBgOver;
-		ripple: defaultRippleAnimation;
-	}
-	statusFg: contactsStatusFg;
-	statusFgOver: contactsStatusFgOver;
-	statusFgActive: contactsStatusFgOnline;
-}
 pickLocationButton: FlatButton {
 	height: pickLocationRowHeight;
 	bgColor: contactsBg;
@@ -1451,25 +1436,33 @@ pickLocationButton: FlatButton {
 }
 pickLocationButtonText: FlatLabel(defaultFlatLabel) {
 	minWidth: 128px;
+	maxHeight: 20px;
 	style: semiboldTextStyle;
 	textFg: windowBoldFg;
 }
 pickLocationButtonStatus: FlatLabel(defaultFlatLabel) {
 	minWidth: 128px;
+	maxHeight: 20px;
 	textFg: windowSubTextFg;
 }
 pickLocationButtonSkip: 6px;
 pickLocationSendIcon: icon{{ "chat/filled_location", windowFgActive }};
 pickLocationVenueItem: PeerListItem(defaultPeerListItem) {
-	button: OutlineButton(defaultPeerListButton) {
-		font: normalFont;
-		padding: margins(11px, 5px, 11px, 5px);
-	}
-	height: 52px;
+	height: pickLocationRowHeight;
+	photoSize: 42px;
 	photoPosition: point(18px, 5px);
 	namePosition: point(70px, 7px);
 	statusPosition: point(70px, 27px);
-	photoSize: 42px;
+	button: OutlineButton(defaultPeerListButton) {
+		textBg: contactsBg;
+		textBgOver: contactsBgOver;
+		font: normalFont;
+		padding: margins(11px, 5px, 11px, 5px);
+		ripple: defaultRippleAnimation;
+	}
+	statusFg: contactsStatusFg;
+	statusFgOver: contactsStatusFgOver;
+	statusFgActive: contactsStatusFgOnline;
 }
 pickLocationVenueList: PeerList(defaultPeerList) {
 	item: pickLocationVenueItem;
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index d52e13b82..4dbd94e1d 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -1822,6 +1822,7 @@ void ChooseAndSendLocation(
 	Ui::LocationPicker::Show({
 		.parent = controller->widget(),
 		.config = config,
+		.recipient = action.history->peer,
 		.session = &controller->session(),
 		.callback = crl::guard(controller, callback),
 		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index ed9d84f7a..5438d00d4 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -409,7 +409,7 @@ void VenuesController::rowPaintIcon(
 		st::pickLocationButton);
 	const auto raw = result.data();
 
-	const auto st = &st::pickLocationVenue;
+	const auto st = &st::pickLocationVenueItem;
 	const auto icon = CreateChild<RpWidget>(raw);
 	icon->setGeometry(
 		st->photoPosition.x(),
@@ -628,7 +628,8 @@ LocationPicker::LocationPicker(Descriptor &&descriptor)
 
 	venuesRequest(*_venuesSearchLocation, *_venuesSearchQuery);
 })
-, _api(&_session->mtp()) {
+, _api(&_session->mtp())
+, _venueRecipient(descriptor.recipient) {
 	std::move(
 		descriptor.closeRequests
 	) | rpl::start_with_next([=] {
@@ -1002,7 +1003,7 @@ void LocationPicker::venuesSendRequest() {
 	_venuesRequestId = _api.request(MTPmessages_GetInlineBotResults(
 		MTP_flags(MTPmessages_GetInlineBotResults::Flag::f_geo_point),
 		_venuesBot->inputUser,
-		MTP_inputPeerEmpty(),
+		(_venueRecipient ? _venueRecipient->input : MTP_inputPeerEmpty()),
 		MTP_inputGeoPoint(
 			MTP_flags(0),
 			MTP_double(_venuesRequestLocation.point.x()),
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index 6014bd06b..756e6b162 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -80,6 +80,7 @@ public:
 	struct Descriptor {
 		RpWidget *parent = nullptr;
 		LocationPickerConfig config;
+		PeerData *recipient = nullptr;
 		not_null<Main::Session*> session;
 		Fn<void(Data::InputVenue)> callback;
 		Fn<void()> quit;
@@ -145,6 +146,7 @@ private:
 	std::optional<QString> _venuesSearchQuery;
 	base::Timer _venuesSearchDebounceTimer;
 	MTP::Sender _api;
+	PeerData *_venueRecipient = nullptr;
 	UserData *_venuesBot = nullptr;
 	mtpRequestId _venuesBotRequestId = 0;
 	mtpRequestId _venuesRequestId = 0;

From 8a92c89f39bc58e0192f5cb808d52724d7e95f10 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 15 Jul 2024 09:32:52 +0200
Subject: [PATCH 046/163] Add fsqr promo footer.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 .../chat_helpers/chat_helpers.style           |  1 +
 .../ui/controls/location_picker.cpp           | 27 +++++++++++++++++++
 3 files changed, 29 insertions(+)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 08368d748..fc725ff1d 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3199,6 +3199,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_maps_places_in_area" = "Places in this area";
 "lng_maps_no_places" = "No places found";
 "lng_maps_choose_to_search" = "Choose location to see places nearby.";
+"lng_maps_venues_source" = "Powered by Foursquare";
 "lng_live_location" = "Live Location";
 "lng_live_location_now" = "updated just now";
 "lng_live_location_minutes#one" = "updated {count} minute ago";
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index a40d1e0ad..9b92f4815 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1474,3 +1474,4 @@ pickLocationLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
 	color: windowSubTextFg;
 	thickness: 4px;
 }
+pickLocationPromoHeight: 32px;
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 5438d00d4..b461a902b 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text/text_utilities.h"
 #include "ui/widgets/scroll_area.h"
 #include "ui/widgets/separate_panel.h"
+#include "ui/widgets/shadow.h"
 #include "ui/widgets/buttons.h"
 #include "ui/wrap/slide_wrap.h"
 #include "ui/wrap/vertical_layout.h"
@@ -182,6 +183,27 @@ private:
 	return query.trimmed().toLower();
 }
 
+[[nodiscard]] object_ptr<RpWidget> MakeFoursquarePromo() {
+	auto result = object_ptr<RpWidget>((QWidget*)nullptr);
+	const auto raw = result.data();
+	raw->resize(0, st::pickLocationPromoHeight);
+	const auto shadow = CreateChild<PlainShadow>(raw);
+	raw->widthValue() | rpl::start_with_next([=](int width) {
+		shadow->setGeometry(0, 0, width, st::lineWidth);
+	}, raw->lifetime());
+	raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
+		auto p = QPainter(raw);
+		p.fillRect(clip, st::windowBg);
+		p.setPen(st::windowSubTextFg);
+		p.setFont(st::normalFont);
+		p.drawText(
+			raw->rect(),
+			tr::lng_maps_venues_source(tr::now),
+			style::al_center);
+	}, raw->lifetime());
+	return result;
+}
+
 VenuesController::VenuesController(
 	not_null<Main::Session*> session,
 	rpl::producer<std::vector<VenueData>> content,
@@ -214,6 +236,11 @@ void VenuesController::rebuild(const std::vector<VenueData> &rows) {
 		delegate()->peerListRemoveRow(delegate()->peerListRowAt(i));
 		--count;
 	}
+	if (i > 0) {
+		delegate()->peerListSetBelowWidget(MakeFoursquarePromo());
+	} else {
+		delegate()->peerListSetBelowWidget({ nullptr });
+	}
 	delegate()->peerListRefreshRows();
 }
 

From 03454ca3b4abf7e5e28e7758f2313811eacaa673 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 15 Jul 2024 11:35:01 +0200
Subject: [PATCH 047/163] Fix sticker quote reply layout.

---
 .../history/view/media/history_view_media_unwrapped.cpp        | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp
index 237c30b87..8f8b15ecf 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp
@@ -125,9 +125,6 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) {
 		if (via) {
 			via->resize(availw);
 		}
-		if (reply) {
-			[[maybe_unused]] int height = reply->resizeToWidth(availw);
-		}
 	}
 	return { newWidth, newHeight };
 }

From 2c3ef13b017e0f7111f0401daf29014973701cf2 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 16 Jul 2024 16:03:15 +0200
Subject: [PATCH 048/163] Update tg_owt to M123.

---
 .../SourceFiles/platform/win/specific_win.cpp   | 17 +++++++++++++++++
 .../SourceFiles/platform/win/windows_dlls.cpp   |  3 +++
 .../SourceFiles/platform/win/windows_dlls.h     |  8 ++++++++
 Telegram/ThirdParty/tgcalls                     |  2 +-
 Telegram/build/docker/centos_env/Dockerfile     |  2 +-
 Telegram/build/prepare/prepare.py               | 12 ++++++++----
 Telegram/cmake/lib_tgcalls.cmake                | 11 +++++++----
 Telegram/lib_webrtc                             |  2 +-
 cmake                                           |  2 +-
 snap/snapcraft.yaml                             |  2 +-
 10 files changed, 48 insertions(+), 13 deletions(-)

diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp
index a35cfd3c4..9b8b959e0 100644
--- a/Telegram/SourceFiles/platform/win/specific_win.cpp
+++ b/Telegram/SourceFiles/platform/win/specific_win.cpp
@@ -58,6 +58,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include <guiddef.h>
 #include <locale.h>
 
+#include <ShellScalingApi.h>
+
 #ifndef DCX_USESTYLE
 #define DCX_USESTYLE 0x00010000
 #endif
@@ -695,3 +697,18 @@ bool psLaunchMaps(const Data::LocationPoint &point) {
 	return QDesktopServices::openUrl(
 		url.arg(point.latAsString()).arg(point.lonAsString()));
 }
+
+// Stub while we still support Windows 7.
+extern "C" {
+
+STDAPI GetDpiForMonitor(
+		_In_ HMONITOR hmonitor,
+		_In_ MONITOR_DPI_TYPE dpiType,
+		_Out_ UINT *dpiX,
+		_Out_ UINT *dpiY) {
+	return Dlls::GetDpiForMonitor
+		? Dlls::GetDpiForMonitor(hmonitor, dpiType, dpiX, dpiY)
+		: E_FAIL;
+}
+
+} // extern "C"
diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.cpp b/Telegram/SourceFiles/platform/win/windows_dlls.cpp
index 0e03904f7..ded5dd0d1 100644
--- a/Telegram/SourceFiles/platform/win/windows_dlls.cpp
+++ b/Telegram/SourceFiles/platform/win/windows_dlls.cpp
@@ -63,6 +63,9 @@ SafeIniter::SafeIniter() {
 
 	const auto LibUser32 = LoadLibrary(L"user32.dll");
 	LOAD_SYMBOL(LibUser32, SetWindowCompositionAttribute);
+
+	const auto LibShCore = LoadLibrary(L"Shcore.dll");
+	LOAD_SYMBOL(LibShCore, GetDpiForMonitor);
 }
 
 SafeIniter kSafeIniter;
diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.h b/Telegram/SourceFiles/platform/win/windows_dlls.h
index 26f478376..9ebed92fc 100644
--- a/Telegram/SourceFiles/platform/win/windows_dlls.h
+++ b/Telegram/SourceFiles/platform/win/windows_dlls.h
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include <windows.h>
 #include <shellapi.h>
+#include <ShellScalingApi.h>
 #include <dwmapi.h>
 #include <RestartManager.h>
 #include <psapi.h>
@@ -124,5 +125,12 @@ inline BOOL(__stdcall *SetWindowCompositionAttribute)(
 	HWND hWnd,
 	WINDOWCOMPOSITIONATTRIBDATA*);
 
+// SHCORE.DLL
+inline HRESULT(__stdcall *GetDpiForMonitor)(
+	_In_ HMONITOR hmonitor,
+	_In_ MONITOR_DPI_TYPE dpiType,
+	_Out_ UINT *dpiX,
+	_Out_ UINT *dpiY);
+
 } // namespace Dlls
 } // namespace Platform
diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index b9fa8b84d..0c23e3803 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit b9fa8b84d8abe741183f157218ac038c596a54a5
+Subproject commit 0c23e3803bc29e115f886308275bf0e3a313bc58
diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index e4f7d1fb1..00a3dc3b7 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -753,7 +753,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache /
 RUN git init tg_owt \
 	&& cd tg_owt \
 	&& git remote add origin {{ GIT }}/desktop-app/tg_owt.git \
-	&& git fetch --depth=1 origin c9cc4390ab951f2cbc103ff783a11f398b27660b \
+	&& git fetch --depth=1 origin 996dbe2c83b5a71d9045ce47960b8432e223dbb3 \
 	&& git reset --hard FETCH_HEAD \
 	&& git submodule update --init --recursive --depth=1 \
 	&& rm -rf .git \
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index 6704bdbd2..2f5986864 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -1,4 +1,4 @@
-import os, sys, pprint, re, json, pathlib, hashlib, subprocess, glob
+import os, sys, pprint, re, json, pathlib, hashlib, subprocess, glob, tempfile
 
 executePath = os.getcwd()
 sys.dont_write_bytecode = True
@@ -435,8 +435,12 @@ if customRunCommand:
             modifiedEnv['PROMPT'] = '(prepare) $P$G'
             subprocess.run("cmd.exe", shell=True, env=modifiedEnv)
         else:
-            modifiedEnv['PS1'] = '(prepare) \\w \\$ '
-            subprocess.run("bash --noprofile --norc", env=modifiedEnv)
+            prompt = '(prepare) %~ %# '
+            with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp_zshrc:
+                tmp_zshrc.write(f'export PS1="{prompt}"\n')
+                tmp_zshrc_path = tmp_zshrc.name
+            subprocess.run(['zsh', '--rcs', tmp_zshrc_path], env=modifiedEnv)
+            os.remove(tmp_zshrc_path)
     elif not run(command):
         print('FAILED :(')
         finish(1)
@@ -1616,7 +1620,7 @@ win:
 stage('tg_owt', """
     git clone https://github.com/desktop-app/tg_owt.git
     cd tg_owt
-    git checkout afd9d5d317
+    git checkout 996dbe2c83
     git submodule init
     git submodule update
 win:
diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake
index 881ad9952..3f6938e72 100644
--- a/Telegram/cmake/lib_tgcalls.cmake
+++ b/Telegram/cmake/lib_tgcalls.cmake
@@ -88,10 +88,6 @@ PRIVATE
     v2/SignalingEncryption.h
     v2/SignalingSctpConnection.cpp
     v2/SignalingSctpConnection.h
-    v2_4_0_0/InstanceV2_4_0_0Impl.cpp
-    v2_4_0_0/InstanceV2_4_0_0Impl.h
-    v2_4_0_0/Signaling_4_0_0.cpp
-    v2_4_0_0/Signaling_4_0_0.h
 
     # Desktop capturer
     desktop_capturer/DesktopCaptureSource.h
@@ -152,10 +148,17 @@ PRIVATE
     platform/darwin/GLVideoView.mm
     platform/darwin/GLVideoViewMac.h
     platform/darwin/GLVideoViewMac.mm
+    platform/darwin/h265_nalu_rewriter.cc
+    platform/darwin/h265_nalu_rewriter.h
     platform/darwin/objc_video_encoder_factory.h
     platform/darwin/objc_video_encoder_factory.mm
     platform/darwin/objc_video_decoder_factory.h
     platform/darwin/objc_video_decoder_factory.mm
+    platform/darwin/RTCCodecSpecificInfoH265+Private.h
+    platform/darwin/RTCCodecSpecificInfoH265.h
+    platform/darwin/RTCCodecSpecificInfoH265.mm
+    platform/darwin/RTCH265ProfileLevelId.h
+    platform/darwin/RTCH265ProfileLevelId.mm
     platform/darwin/TGCMIOCapturer.h
     platform/darwin/TGCMIOCapturer.m
     platform/darwin/TGCMIODevice.h
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index eb9496540..81d01697c 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit eb9496540356945e2c9fb700bcfa51444fd36f41
+Subproject commit 81d01697c2ee2b46c5e5c41ce1176c230264e4cb
diff --git a/cmake b/cmake
index 78b441c9c..d50ff1a71 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 78b441c9c6ad8a14a8f97a28825babcadc6bf781
+Subproject commit d50ff1a71e080e63f71c451f500a2f03f9d48539
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 033424e1d..b3c8d1905 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -433,7 +433,7 @@ parts:
   webrtc:
     source: https://github.com/desktop-app/tg_owt.git
     source-depth: 1
-    source-commit: c9cc4390ab951f2cbc103ff783a11f398b27660b
+    source-commit: 996dbe2c83b5a71d9045ce47960b8432e223dbb3
     plugin: cmake
     build-environment:
       - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s

From 8c55364afad0ba598f53b4fd592d0b86db82addf Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 10:37:14 +0200
Subject: [PATCH 049/163] Allow editing business location in Settings.

---
 Telegram/Resources/langs/lang.strings         |   2 +
 Telegram/SourceFiles/calls/calls_call.cpp     |   2 -
 .../data/business/data_business_info.cpp      |  36 +++
 .../data/business/data_business_info.h        |   1 +
 Telegram/SourceFiles/data/data_location.cpp   |   5 +
 Telegram/SourceFiles/data/data_location.h     |   7 +-
 .../inline_bots/bot_attach_web_view.cpp       |   1 +
 .../settings/business/settings_chat_intro.cpp |   1 -
 .../settings/business/settings_location.cpp   | 208 ++++++++++++++++--
 .../ui/controls/location_picker.cpp           |  33 ++-
 .../SourceFiles/ui/controls/location_picker.h |   3 +
 11 files changed, 266 insertions(+), 33 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index fc725ff1d..c692195bc 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2387,6 +2387,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_location_title" = "Location";
 "lng_location_about" = "Display the location of your business on your account.";
 "lng_location_address" = "Enter Address";
+"lng_location_set_map" = "Set Location on Map";
 "lng_location_fallback" = "You can set your location on the map from your mobile device.";
 
 "lng_hours_title" = "Business Hours";
@@ -3195,6 +3196,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 "lng_maps_point" = "Location";
 "lng_maps_point_send" = "Send This Location";
+"lng_maps_point_set" = "Set This Location";
 "lng_maps_or_choose" = "Or choose a venue";
 "lng_maps_places_in_area" = "Places in this area";
 "lng_maps_no_places" = "No places found";
diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp
index 9f1272859..0ca37c629 100644
--- a/Telegram/SourceFiles/calls/calls_call.cpp
+++ b/Telegram/SourceFiles/calls/calls_call.cpp
@@ -39,7 +39,6 @@ namespace tgcalls {
 class InstanceImpl;
 class InstanceV2Impl;
 class InstanceV2ReferenceImpl;
-class InstanceV2_4_0_0Impl;
 class InstanceImplLegacy;
 void SetLegacyGlobalServerConfig(const std::string &serverConfig);
 } // namespace tgcalls
@@ -56,7 +55,6 @@ const auto kDefaultVersion = "2.4.4"_q;
 const auto Register = tgcalls::Register<tgcalls::InstanceImpl>();
 const auto RegisterV2 = tgcalls::Register<tgcalls::InstanceV2Impl>();
 const auto RegV2Ref = tgcalls::Register<tgcalls::InstanceV2ReferenceImpl>();
-const auto RegisterV240 = tgcalls::Register<tgcalls::InstanceV2_4_0_0Impl>();
 const auto RegisterLegacy = tgcalls::Register<tgcalls::InstanceImplLegacy>();
 
 [[nodiscard]] base::flat_set<int64> CollectEndpointIds(
diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp
index e158c7a3a..d9bb6ec7a 100644
--- a/Telegram/SourceFiles/data/business/data_business_info.cpp
+++ b/Telegram/SourceFiles/data/business/data_business_info.cpp
@@ -130,6 +130,42 @@ void BusinessInfo::saveChatIntro(ChatIntro data, Fn<void(QString)> fail) {
 	session->user()->setBusinessDetails(std::move(details));
 }
 
+void BusinessInfo::saveLocation(
+		BusinessLocation data,
+		Fn<void(QString)> fail) {
+	const auto session = &_owner->session();
+	auto details = session->user()->businessDetails();
+	const auto &was = details.location;
+	if (was == data) {
+		return;
+	} else {
+		const auto session = &_owner->session();
+		using Flag = MTPaccount_UpdateBusinessLocation::Flag;
+		session->api().request(MTPaccount_UpdateBusinessLocation(
+			MTP_flags((data.point ? Flag::f_geo_point : Flag())
+				| (data.address.isEmpty() ? Flag() : Flag::f_address)),
+			(data.point
+				? MTP_inputGeoPoint(
+					MTP_flags(0),
+					MTP_double(data.point->lat()),
+					MTP_double(data.point->lon()),
+					MTPint()) // accuracy_radius
+				: MTP_inputGeoPointEmpty()),
+			MTP_string(data.address)
+		)).fail([=](const MTP::Error &error) {
+			auto details = session->user()->businessDetails();
+			details.location = was;
+			session->user()->setBusinessDetails(std::move(details));
+			if (fail) {
+				fail(error.type());
+			}
+		}).send();
+	}
+
+	details.location = std::move(data);
+	session->user()->setBusinessDetails(std::move(details));
+}
+
 void BusinessInfo::applyAwaySettings(AwaySettings data) {
 	if (_awaySettings == data) {
 		return;
diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h
index 7b99e4c8f..01993f223 100644
--- a/Telegram/SourceFiles/data/business/data_business_info.h
+++ b/Telegram/SourceFiles/data/business/data_business_info.h
@@ -22,6 +22,7 @@ public:
 
 	void saveWorkingHours(WorkingHours data, Fn<void(QString)> fail);
 	void saveChatIntro(ChatIntro data, Fn<void(QString)> fail);
+	void saveLocation(BusinessLocation data, Fn<void(QString)> fail);
 
 	void saveAwaySettings(AwaySettings data, Fn<void(QString)> fail);
 	void applyAwaySettings(AwaySettings data);
diff --git a/Telegram/SourceFiles/data/data_location.cpp b/Telegram/SourceFiles/data/data_location.cpp
index 80727b3ba..457bb6411 100644
--- a/Telegram/SourceFiles/data/data_location.cpp
+++ b/Telegram/SourceFiles/data/data_location.cpp
@@ -26,6 +26,11 @@ LocationPoint::LocationPoint(const MTPDgeoPoint &point)
 , _access(point.vaccess_hash().v) {
 }
 
+LocationPoint::LocationPoint(float64 lat, float64 lon, IgnoreAccessHash)
+: _lat(lat)
+, _lon(lon) {
+}
+
 QString LocationPoint::latAsString() const {
 	return AsString(_lat);
 }
diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h
index 5157f79a1..114d21c08 100644
--- a/Telegram/SourceFiles/data/data_location.h
+++ b/Telegram/SourceFiles/data/data_location.h
@@ -16,6 +16,11 @@ public:
 	LocationPoint() = default;
 	explicit LocationPoint(const MTPDgeoPoint &point);
 
+	enum IgnoreAccessHash {
+		NoAccessHash,
+	};
+	LocationPoint(float64 lat, float64 lon, IgnoreAccessHash);
+
 	[[nodiscard]] QString latAsString() const;
 	[[nodiscard]] QString lonAsString() const;
 	[[nodiscard]] MTPGeoPoint toMTP() const;
@@ -55,7 +60,7 @@ struct InputVenue {
 	QString venueType;
 
 	[[nodiscard]] bool justLocation() const {
-		return id.isEmpty() && title.isEmpty() && address.isEmpty();
+		return id.isEmpty();
 	}
 
 	friend inline bool operator==(
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index 4dbd94e1d..c42ecb3b9 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -1822,6 +1822,7 @@ void ChooseAndSendLocation(
 	Ui::LocationPicker::Show({
 		.parent = controller->widget(),
 		.config = config,
+		.chooseLabel = tr::lng_maps_point_send(),
 		.recipient = action.history->peer,
 		.session = &controller->session(),
 		.callback = crl::guard(controller, callback),
diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp
index e9bc2f73e..688a6940d 100644
--- a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp
+++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp
@@ -632,7 +632,6 @@ void ChatIntro::setupContent(
 }
 
 void ChatIntro::save() {
-	const auto show = controller()->uiShow();
 	const auto fail = [=](QString error) {
 	};
 	controller()->session().data().businessInfo().saveChatIntro(
diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp
index 4a2f14e73..e5bb0f8e3 100644
--- a/Telegram/SourceFiles/settings/business/settings_location.cpp
+++ b/Telegram/SourceFiles/settings/business/settings_location.cpp
@@ -8,16 +8,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "settings/business/settings_location.h"
 
 #include "core/application.h"
+#include "core/shortcuts.h"
+#include "data/business/data_business_info.h"
+#include "data/data_file_origin.h"
 #include "data/data_session.h"
+#include "data/data_user.h"
 #include "lang/lang_keys.h"
+#include "main/main_app_config.h"
 #include "main/main_session.h"
+#include "mainwidget.h"
+#include "mainwindow.h"
 #include "settings/business/settings_recipients_helper.h"
+#include "settings/settings_common.h"
+#include "storage/storage_account.h"
+#include "ui/controls/location_picker.h"
 #include "ui/text/text_utilities.h"
 #include "ui/widgets/fields/input_field.h"
+#include "ui/widgets/buttons.h"
+#include "ui/wrap/slide_wrap.h"
 #include "ui/wrap/vertical_layout.h"
 #include "ui/vertical_list.h"
 #include "window/window_session_controller.h"
+#include "styles/style_chat.h"
 #include "styles/style_layers.h"
+#include "styles/style_menu_icons.h"
 #include "styles/style_settings.h"
 
 namespace Settings {
@@ -40,16 +54,37 @@ private:
 	void setupContent(not_null<Window::SessionController*> controller);
 	void save();
 
-	[[nodiscard]] bool mapSupported() const;
+	void setupPicker(not_null<Ui::VerticalLayout*> content);
+	void setupUnsupported(not_null<Ui::VerticalLayout*> content);
 
+	[[nodiscard]] bool mapSupported() const;
+	void chooseOnMap();
+
+	const Ui::LocationPickerConfig _config;
+	rpl::variable<Data::BusinessLocation> _data;
+	rpl::variable<Data::CloudImage*> _map = nullptr;
+	std::shared_ptr<QImage> _view;
 	Ui::RoundRect _bottomSkipRounding;
 
 };
 
+[[nodiscard]] Ui::LocationPickerConfig ResolveBusinessMapsConfig(
+		not_null<Main::Session*> session) {
+	const auto &appConfig = session->appConfig();
+	auto map = appConfig.get<base::flat_map<QString, QString>>(
+		u"tdesktop_config_map"_q,
+		base::flat_map<QString, QString>());
+	return {
+		.mapsToken = map[u"bmaps"_q],
+		.geoToken = map[u"bgeo"_q],
+	};
+}
+
 Location::Location(
 	QWidget *parent,
 	not_null<Window::SessionController*> controller)
 : BusinessSection(parent, controller)
+, _config(ResolveBusinessMapsConfig(&controller->session()))
 , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
 	setupContent(controller);
 }
@@ -65,12 +100,23 @@ rpl::producer<QString> Location::title() {
 }
 
 void Location::setupContent(
-	not_null<Window::SessionController*> controller) {
+		not_null<Window::SessionController*> controller) {
 	using namespace rpl::mappers;
 
 	const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
 
-#if 0 // #TODO location choosing
+	if (mapSupported()) {
+		setupPicker(content);
+	} else {
+		setupUnsupported(content);
+	}
+
+	Ui::ResizeFitChild(this, content);
+}
+
+void Location::setupPicker(not_null<Ui::VerticalLayout*> content) {
+	_data = controller()->session().user()->businessDetails().location;
+
 	AddDividerTextWithLottie(content, {
 		.lottie = u"location"_q,
 		.lottieSize = st::settingsCloudPasswordIconSize,
@@ -86,36 +132,158 @@ void Location::setupContent(
 			st::settingsLocationAddress,
 			Ui::InputField::Mode::MultiLine,
 			tr::lng_location_address(),
-			QString()),
+			_data.current().address),
 		st::settingsChatbotsUsernameMargins);
 
+	_data.value(
+	) | rpl::start_with_next([=](const Data::BusinessLocation &location) {
+		address->setText(location.address);
+	}, address->lifetime());
+
+	address->changes() | rpl::start_with_next([=] {
+		auto copy = _data.current();
+		copy.address = address->getLastText();
+		_data = std::move(copy);
+		}, address->lifetime());
+
+	AddDivider(content);
+	AddSkip(content);
+
+	const auto maptoggle = AddButtonWithIcon(
+		content,
+		tr::lng_location_set_map(),
+		st::settingsButton,
+		{ &st::menuIconAddress }
+	)->toggleOn(_data.value(
+	) | rpl::map([](const Data::BusinessLocation &location) {
+		return location.point.has_value();
+	}));
+
+	maptoggle->toggledValue() | rpl::start_with_next([=](bool toggled) {
+		if (!toggled) {
+			auto copy = _data.current();
+			if (copy.point.has_value()) {
+				copy.point = std::nullopt;
+				_data = std::move(copy);
+			}
+		} else if (!_data.current().point.has_value()) {
+			_data.force_assign(_data.current());
+			chooseOnMap();
+		}
+	}, maptoggle->lifetime());
+
+	const auto mapSkip = st::defaultVerticalListSkip;
+	const auto mapWrap = content->add(
+		object_ptr<Ui::SlideWrap<Ui::AbstractButton>>(
+			content,
+			object_ptr<Ui::AbstractButton>(content),
+			st::boxRowPadding + QMargins(0, mapSkip, 0, mapSkip)));
+	mapWrap->toggle(_data.current().point.has_value(), anim::type::instant);
+
+	const auto map = mapWrap->entity();
+	map->resize(map->width(), st::locationSize.height());
+
+	_data.value(
+	) | rpl::start_with_next([=](const Data::BusinessLocation &location) {
+		const auto image = location.point.has_value()
+			? controller()->session().data().location(*location.point).get()
+			: nullptr;
+		if (image) {
+			image->load(&controller()->session(), {});
+			_view = image->createView();
+		}
+		mapWrap->toggle(image != nullptr, anim::type::normal);
+		_map = image;
+	}, mapWrap->lifetime());
+
+	map->paintRequest() | rpl::start_with_next([=] {
+		auto p = QPainter(map);
+
+		const auto left = (map->width() - st::locationSize.width()) / 2;
+		const auto rect = QRect(QPoint(left, 0), st::locationSize);
+		const auto &image = _view ? *_view : QImage();
+		if (!image.isNull()) {
+			p.drawImage(rect, image);
+		}
+
+		const auto paintMarker = [&](const style::icon &icon) {
+			icon.paint(
+				p,
+				rect.x() + ((rect.width() - icon.width()) / 2),
+				rect.y() + (rect.height() / 2) - icon.height(),
+				width());
+			};
+		paintMarker(st::historyMapPoint);
+		paintMarker(st::historyMapPointInner);
+	}, map->lifetime());
+
+	controller()->session().downloaderTaskFinished(
+	) | rpl::start_with_next([=] {
+		map->update();
+	}, map->lifetime());
+
+	map->setClickedCallback([=] {
+		chooseOnMap();
+	});
+
 	showFinishes() | rpl::start_with_next([=] {
 		address->setFocus();
 	}, address->lifetime());
-#endif
+}
 
-	if (!mapSupported()) {
-		AddDividerTextWithLottie(content, {
-			.lottie = u"phone"_q,
-			.lottieSize = st::settingsCloudPasswordIconSize,
-			.lottieMargins = st::peerAppearanceIconPadding,
-			.showFinished = showFinishes(),
-			.about = tr::lng_location_fallback(Ui::Text::WithEntities),
-			.aboutMargins = st::peerAppearanceCoverLabelMargin,
-			.parts = RectPart::Top,
-		});
-	} else {
+void Location::chooseOnMap() {
+	const auto callback = [=](Data::InputVenue venue) {
+		auto copy = _data.current();
+		copy.point = Data::LocationPoint(
+			venue.lat,
+			venue.lon,
+			Data::LocationPoint::NoAccessHash);
+		copy.address = venue.address;
+		_data = std::move(copy);
+	};
+	const auto session = &controller()->session();
+	const auto current = _data.current().point;
+	const auto initial = current
+		? Core::GeoLocation{
+			.point = { current->lat(), current->lon() },
+			.accuracy = Core::GeoLocationAccuracy::Exact,
+		}
+		: Core::GeoLocation();
+	Ui::LocationPicker::Show({
+		.parent = controller()->widget(),
+		.config = _config,
+		.chooseLabel = tr::lng_maps_point_set(),
+		.session = session,
+		.initial = initial,
+		.callback = crl::guard(this, callback),
+		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
+		.storageId = session->local().resolveStorageIdBots(),
+		.closeRequests = controller()->content()->death(),
+	});
+}
 
-	}
-
-	Ui::ResizeFitChild(this, content);
+void Location::setupUnsupported(not_null<Ui::VerticalLayout*> content) {
+	AddDividerTextWithLottie(content, {
+		.lottie = u"phone"_q,
+		.lottieSize = st::settingsCloudPasswordIconSize,
+		.lottieMargins = st::peerAppearanceIconPadding,
+		.showFinished = showFinishes(),
+		.about = tr::lng_location_fallback(Ui::Text::WithEntities),
+		.aboutMargins = st::peerAppearanceCoverLabelMargin,
+		.parts = RectPart::Top,
+	});
 }
 
 void Location::save() {
+	const auto fail = [=](QString error) {
+	};
+	auto value = _data.current();
+	value.address = value.address.trimmed();
+	controller()->session().data().businessInfo().saveLocation(value, fail);
 }
 
 bool Location::mapSupported() const {
-	return false;
+	return Ui::LocationPicker::Available(_config);
 }
 
 } // namespace
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index b461a902b..3f1c74fe0 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -427,8 +427,9 @@ void VenuesController::rowPaintIcon(
 )"_q;
 }
 
-[[nodiscard]] object_ptr<AbstractButton> MakeSendLocationButton(
+[[nodiscard]] object_ptr<AbstractButton> MakeChooseLocationButton(
 		QWidget *parent,
+		rpl::producer<QString> label,
 		rpl::producer<QString> address) {
 	auto result = object_ptr<FlatButton>(
 		parent,
@@ -465,7 +466,7 @@ void VenuesController::rowPaintIcon(
 	});
 	const auto name = CreateChild<FlatLabel>(
 		raw,
-		tr::lng_maps_point_send(tr::now),
+		std::move(label),
 		st::pickLocationButtonText);
 	name->show();
 	const auto status = CreateChild<FlatLabel>(
@@ -679,10 +680,15 @@ bool LocationPicker::Available(const LocationPickerConfig &config) {
 void LocationPicker::setup(const Descriptor &descriptor) {
 	setupWindow(descriptor);
 	setupWebview(descriptor);
-	if (LastExactLocation) {
-		venuesRequest(LastExactLocation);
-		resolveAddress(LastExactLocation);
-		venuesSearchEnableAt(LastExactLocation);
+
+	_initialProvided = descriptor.initial.exact();
+	const auto initial = _initialProvided
+		? descriptor.initial
+		: LastExactLocation;
+	if (initial) {
+		venuesRequest(initial);
+		resolveAddress(initial);
+		venuesSearchEnableAt(initial);
 	}
 }
 
@@ -717,7 +723,10 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	const auto toppad = mapControls->add(object_ptr<RpWidget>(controls));
 
 	const auto button = mapControls->add(
-		MakeSendLocationButton(mapControls, _geocoderAddress.value()),
+		MakeChooseLocationButton(
+			mapControls,
+			std::move(descriptor.chooseLabel),
+			_geocoderAddress.value()),
 		{ 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip });
 	button->setClickedCallback([=] {
 		_webview->eval("LocationPicker.send();");
@@ -809,7 +818,9 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 			const auto event = object.value("event").toString();
 			if (event == u"ready"_q) {
 				mapReady();
-				resolveCurrentLocation();
+				if (!_initialProvided) {
+					resolveCurrentLocation();
+				}
 				if (_webview) {
 					_webview->focus();
 				}
@@ -820,7 +831,11 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 			} else if (event == u"send"_q) {
 				const auto lat = object.value("latitude").toDouble();
 				const auto lon = object.value("longitude").toDouble();
-				_callback({ lat, lon });
+				_callback({
+					.lat = lat,
+					.lon = lon,
+					.address = _geocoderAddress.current(),
+				});
 				close();
 			} else if (event == u"move_start"_q) {
 				if (const auto now = _geocoderAddress.current()
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index 756e6b162..0a1485157 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -80,8 +80,10 @@ public:
 	struct Descriptor {
 		RpWidget *parent = nullptr;
 		LocationPickerConfig config;
+		rpl::producer<QString> chooseLabel;
 		PeerData *recipient = nullptr;
 		not_null<Main::Session*> session;
+		Core::GeoLocation initial;
 		Fn<void(Data::InputVenue)> callback;
 		Fn<void()> quit;
 		Webview::StorageId storageId;
@@ -132,6 +134,7 @@ private:
 	std::unique_ptr<Webview::Window> _webview;
 	SingleQueuedInvokation _updateStyles;
 	bool _subscribedToColors = false;
+	bool _initialProvided = false;
 
 	base::Timer _geocoderResolveTimer;
 	Core::GeoLocation _geocoderResolvePostponed;

From c22698084f169d662871b86315ade067a0376725 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 16 Jul 2024 16:14:15 +0200
Subject: [PATCH 050/163] Location search cancel by X button.

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

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index f046725ec..58329bfe1 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit f046725ecde41bf5779d69393cc592786ee0440c
+Subproject commit 58329bfe19c031ec6065a3a098743bb3d7732a64

From 6f86acf7128fcad9c9da1be1d9043997c5c4a8ee Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 16 Jul 2024 18:11:59 +0200
Subject: [PATCH 051/163] Use system reverse geocoding on macOS.

---
 .../platform/mac/current_geo_location_mac.mm  | 30 ++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
index 1a5382f19..0ea554463 100644
--- a/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
+++ b/Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "platform/mac/current_geo_location_mac.h"
 
+#include "base/platform/mac/base_utilities_mac.h"
 #include "core/current_geo_location.h"
 
 #include <CoreLocation/CoreLocation.h>
@@ -120,7 +121,34 @@ void ResolveLocationAddress(
 		const Core::GeoLocation &location,
 		const QString &language,
 		Fn<void(Core::GeoAddress)> callback) {
-	callback({});
+	CLGeocoder *geocoder = [[CLGeocoder alloc] init];
+	CLLocation *request = [[CLLocation alloc]
+		initWithLatitude:location.point.x()
+		longitude:location.point.y()];
+	[geocoder reverseGeocodeLocation:request completionHandler:^(
+			NSArray<CLPlacemark*> * __nullable placemarks,
+			NSError * __nullable error) {
+		if (placemarks && [placemarks count] > 0) {
+			CLPlacemark *placemark = [placemarks firstObject];
+			auto list = QStringList();
+			const auto push = [&](NSString *text) {
+				if (text) {
+					const auto qt = NS2QString(text);
+					if (!qt.isEmpty()) {
+						list.push_back(qt);
+					}
+				}
+			};
+			push([placemark thoroughfare]);
+			push([placemark locality]);
+			push([placemark country]);
+			callback({ .name = list.join(u", "_q) });
+		} else {
+			callback({});
+		}
+		[geocoder release];
+	}];
+	[request release];
 }
 
 } // namespace Platform

From e83704982f9c0c81793de2d6eb6035731104a374 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 16 Jul 2024 18:13:17 +0200
Subject: [PATCH 052/163] Set initial location correctly.

---
 .../ui/controls/location_picker.cpp           | 21 ++++++++++++-------
 .../SourceFiles/ui/controls/location_picker.h |  2 +-
 2 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 3f1c74fe0..8cce44571 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -332,14 +332,15 @@ void VenuesController::rowPaintIcon(
 	p.drawImage(x, y, data.image);
 }
 
-[[nodiscard]] QByteArray DefaultCenter() {
-	if (!LastExactLocation) {
+[[nodiscard]] QByteArray DefaultCenter(Core::GeoLocation initial) {
+	const auto &use = initial.exact() ? initial : LastExactLocation;
+	if (!use) {
 		return "null";
 	}
 	return "["_q
-		+ QByteArray::number(LastExactLocation.point.x())
+		+ QByteArray::number(use.point.x())
 		+ ","_q
-		+ QByteArray::number(LastExactLocation.point.y())
+		+ QByteArray::number(use.point.y())
 		+ "]"_q;
 }
 
@@ -496,6 +497,10 @@ void VenuesController::rowPaintIcon(
 		status->moveToLeft(statusPosition.x(), statusPosition.y(), width);
 	}, name->lifetime());
 
+	icon->setAttribute(Qt::WA_TransparentForMouseEvents);
+	name->setAttribute(Qt::WA_TransparentForMouseEvents);
+	status->setAttribute(Qt::WA_TransparentForMouseEvents);
+
 	return result;
 }
 
@@ -681,9 +686,9 @@ void LocationPicker::setup(const Descriptor &descriptor) {
 	setupWindow(descriptor);
 	setupWebview(descriptor);
 
-	_initialProvided = descriptor.initial.exact();
-	const auto initial = _initialProvided
-		? descriptor.initial
+	_initialProvided = descriptor.initial;
+	const auto initial = _initialProvided.exact()
+		? _initialProvided
 		: LastExactLocation;
 	if (initial) {
 		venuesRequest(initial);
@@ -964,7 +969,7 @@ void LocationPicker::mapReady() {
 	Expects(_scroll != nullptr);
 
 	const auto token = _config.mapsToken.toUtf8();
-	const auto center = DefaultCenter();
+	const auto center = DefaultCenter(_initialProvided);
 	const auto bounds = DefaultBounds();
 	const auto protocol = *kProtocolOverride
 		? "'"_q + kProtocolOverride + "'"
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index 0a1485157..97ce724f3 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -133,8 +133,8 @@ private:
 	ScrollArea *_scroll = nullptr;
 	std::unique_ptr<Webview::Window> _webview;
 	SingleQueuedInvokation _updateStyles;
+	Core::GeoLocation _initialProvided;
 	bool _subscribedToColors = false;
-	bool _initialProvided = false;
 
 	base::Timer _geocoderResolveTimer;
 	Core::GeoLocation _geocoderResolvePostponed;

From 2412183b835b98a8d5991a2bcf9016c3da79fe68 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 10:24:13 +0200
Subject: [PATCH 053/163] Build most dependencies for Windows on ARM.

---
 Telegram/CMakeLists.txt           |   9 +-
 Telegram/build/prepare/prepare.py | 152 +++++++++++++++++++++++++-----
 Telegram/lib_crl                  |   2 +-
 cmake                             |   2 +-
 4 files changed, 138 insertions(+), 27 deletions(-)

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 592f1329f..48cf60eff 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1847,9 +1847,14 @@ if (WIN32)
         /DELAYLOAD:propsys.dll
     )
     if (QT_VERSION GREATER 6)
+        if (NOT build_winarm)
+            target_link_options(Telegram PRIVATE
+                /DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll
+            )
+        endif()
+
         target_link_options(Telegram
         PRIVATE
-            /DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll
             /DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll
             /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll
             /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll
@@ -1866,7 +1871,7 @@ if (WIN32)
             /DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
             /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll
             /DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll
-            /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll
+            # /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll # We shadowed GetDpiForMonitor
             /DELAYLOAD:authz.dll # Authz.lib
             /DELAYLOAD:comdlg32.dll
             /DELAYLOAD:dwrite.dll # DWrite.lib
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index 2f5986864..6300390d3 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -26,7 +26,7 @@ if win and not 'Platform' in os.environ:
 
 win32 = win and (os.environ['Platform'] == 'x86')
 win64 = win and (os.environ['Platform'] == 'x64')
-winarm = win and (os.environ['Platform'] == 'arm')
+winarm = win and (os.environ['Platform'] == 'arm64')
 
 arch = ''
 if win32:
@@ -114,6 +114,12 @@ elif (win64):
         'X8664': 'x64',
         'WIN32X64': 'x64',
     })
+elif (winarm):
+    environment.update({
+        'SPECIAL_TARGET': 'winarm',
+        'X8664': 'ARM64',
+        'WIN32X64': 'ARM64',
+    })
 elif (mac):
     environment.update({
         'SPECIAL_TARGET': 'mac',
@@ -239,6 +245,8 @@ def filterByPlatform(commands):
                 inscope = True
             if win64 and 'win64' in scopes:
                 inscope = True
+            if winarm and 'winarm' in scopes:
+                inscope = True
             if mac and 'mac' in scopes:
                 inscope = True
             # if linux and 'linux' in scopes:
@@ -449,7 +457,7 @@ if customRunCommand:
 stage('patches', """
     git clone https://github.com/desktop-app/patches.git
     cd patches
-    git checkout e0cdca2e79
+    git checkout 514332f808
 """)
 
 stage('msys64', """
@@ -502,9 +510,9 @@ mac:
 if not mac or 'build-stackwalk' in options:
     stage('gyp', """
 win:
-    git clone https://chromium.googlesource.com/external/gyp
+    git clone https://github.com/desktop-app/gyp.git
     cd gyp
-    git checkout 9d09418933
+    git checkout 618958fdbe
 mac:
     python3 -m pip install \\
         --ignore-installed \\
@@ -610,6 +618,8 @@ win32:
     perl Configure no-shared no-tests debug-VC-WIN32 /FS
 win64:
     perl Configure no-shared no-tests debug-VC-WIN64A /FS
+winarm:
+    perl Configure no-shared no-tests debug-VC-WIN64-ARM /FS
 win:
     jom -j%NUMBER_OF_PROCESSORS%
     mkdir out.dbg
@@ -624,6 +634,8 @@ win32_release:
     perl Configure no-shared no-tests VC-WIN32 /FS
 win64_release:
     perl Configure no-shared no-tests VC-WIN64A /FS
+winarm_release:
+    perl Configure no-shared no-tests VC-WIN64-ARM /FS
 win_release:
     jom -j%NUMBER_OF_PROCESSORS%
     mkdir out
@@ -720,18 +732,32 @@ mac:
     make install
 """)
 
+stage('gas-preprocessor', """
+winarm:
+    git clone https://github.com/FFmpeg/gas-preprocessor
+    cd gas-preprocessor
+    echo @echo off > cpp.bat
+    echo cl %%%%%%** >> cpp.bat
+""")
+
 # Somehow in x86 Debug build dav1d crashes on AV1 10bpc videos.
 stage('dav1d', """
     git clone -b 1.4.1 https://code.videolan.org/videolan/dav1d.git
     cd dav1d
+win32:
+    SET "TARGET=x86"
+    SET "DAV1D_ASM_DISABLE=-Denable_asm=false"
+win64:
+    SET "TARGET=x86_64"
+    SET "DAV1D_ASM_DISABLE="
+winarm:
+    SET "TARGET=aarch64"
+    SET "DAV1D_ASM_DISABLE="
+    SET "PATH_BACKUP_=%PATH%"
+    SET "PATH=%LIBS_DIR%\\gas-preprocessor;%PATH%"
+    echo armasm64 fails with 'syntax error in expression: tbnz x14, #4, 8f' as if this instruction is unknown/unsupported.
+    git revert --no-edit d503bb0ccaf104b2f13da0f092e09cc9411b3297
 win:
-    if "%X8664%" equ "x64" (
-        SET "TARGET=x86_64"
-        SET "DAV1D_ASM_DISABLE="
-    ) else (
-        SET "TARGET=x86"
-        SET "DAV1D_ASM_DISABLE=-Denable_asm=false"
-    )
     set FILE=cross-file.txt
     echo [binaries] > %FILE%
     echo c = 'cl' >> %FILE%
@@ -756,6 +782,8 @@ release:
 win:
     copy %LIBS_DIR%\\local\\lib\\libdav1d.a %LIBS_DIR%\\local\\lib\\dav1d.lib
     deactivate
+winarm:
+    SET "PATH=%PATH_BACKUP_%"
 mac:
     buildOneArch() {
         arch=$1
@@ -781,6 +809,67 @@ mac:
     lipo -create build.arm64/libdav1d.a build/libdav1d.a -output ${USED_PREFIX}/lib/libdav1d.a
 """)
 
+stage('openh264', """
+    git clone -b v2.4.1 https://github.com/cisco/openh264.git
+    cd openh264
+win32:
+    SET "TARGET=x86"
+win64:
+    SET "TARGET=x86_64"
+winarm:
+    SET "TARGET=aarch64"
+    SET "PATH_BACKUP_=%PATH%"
+    SET "PATH=%LIBS_DIR%\\gas-preprocessor;%PATH%"
+win:
+    set FILE=cross-file.txt
+    echo [binaries] > %FILE%
+    echo c = 'cl' >> %FILE%
+    echo cpp = 'cl' >> %FILE%
+    echo ar = 'lib' >> %FILE%
+    echo windres = 'rc' >> %FILE%
+    echo [host_machine] >> %FILE%
+    echo system = 'windows' >> %FILE%
+    echo cpu_family = '%TARGET%' >> %FILE%
+    echo cpu = '%TARGET%' >> %FILE%
+    echo endian = 'little' >> %FILE%
+
+depends:python/Scripts/activate.bat
+    %THIRDPARTY_DIR%\\python\\Scripts\\activate.bat
+    meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=debug -Db_vscrt=mtd builddir-debug
+    meson compile -C builddir-debug
+    meson install -C builddir-debug
+release:
+    meson setup --cross-file %FILE% --prefix %LIBS_DIR%/local --default-library=static --buildtype=release -Db_vscrt=mt builddir-release
+    meson compile -C builddir-release
+    meson install -C builddir-release
+win:
+    copy %LIBS_DIR%\\local\\lib\\libopenh264.a %LIBS_DIR%\\local\\lib\\openh264.lib
+    deactivate
+winarm:
+    SET "PATH=%PATH_BACKUP_%"
+mac:
+    buildOneArch() {
+        arch=$1
+        folder=`pwd`/$2
+
+        meson setup \\
+            --cross-file ../patches/macos_meson_${arch}.txt \\
+            --prefix ${USED_PREFIX} \\
+            --default-library=static \\
+            --buildtype=minsize \\
+            ${folder}
+        meson compile -C ${folder}
+        meson install -C ${folder}
+
+        mv ${USED_PREFIX}/lib/libopenh264.a ${folder}/libopenh264.a
+    }
+
+    buildOneArch arm64 build.arm64
+    buildOneArch x86_64 build
+
+    lipo -create build.arm64/libopenh264.a build/libopenh264.a -output ${USED_PREFIX}/lib/libopenh264.a
+""")
+
 stage('libavif', """
     git clone -b v1.0.4 https://github.com/AOMediaCodec/libavif.git
     cd libavif
@@ -850,7 +939,7 @@ mac:
 """)
 
 stage('libwebp', """
-    git clone -b v1.3.2 https://github.com/webmproject/libwebp.git
+    git clone -b v1.4.0 https://github.com/webmproject/libwebp.git
     cd libwebp
 win:
     nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all
@@ -1005,12 +1094,13 @@ win:
     SET CHERE_INVOKING=enabled_from_arguments
     SET MSYS2_PATH_TYPE=inherit
 
-    if "%X8664%" equ "x64" (
-        SET "TOOLCHAIN=x86_64-win64-vs17"
-    ) else (
-        SET "TOOLCHAIN=x86-win32-vs17"
-    )
-
+win32:
+    SET "TOOLCHAIN=x86-win32-vs17"
+win64:
+    SET "TOOLCHAIN=x86_64-win64-vs17"
+winarm:
+    SET "TOOLCHAIN=arm64-win64-vs17"
+win:
 depends:patches/build_libvpx_win.sh
     bash --login ../patches/build_libvpx_win.sh
 
@@ -1097,12 +1187,19 @@ stage('ffmpeg', """
     git clone -b n6.1.1 https://github.com/FFmpeg/FFmpeg.git ffmpeg
     cd ffmpeg
 win:
+depends:patches/ffmpeg.patch
+    git apply ../patches/ffmpeg.patch
+
     SET PATH_BACKUP_=%PATH%
     SET PATH=%ROOT_DIR%\\ThirdParty\\msys64\\usr\\bin;%PATH%
 
     SET CHERE_INVOKING=enabled_from_arguments
     SET MSYS2_PATH_TYPE=inherit
 
+    SET "ARCH_PARAM="
+winarm:
+    SET "ARCH_PARAM=--arch=aarch64"
+win:
 depends:patches/build_ffmpeg_win.sh
     bash --login ../patches/build_ffmpeg_win.sh
 
@@ -1319,11 +1416,12 @@ depends:patches/breakpad.diff
     git clone -b release-1.11.0 https://github.com/google/googletest src/testing
 win:
     SET "PYTHONUTF8=1"
-    if "%X8664%" equ "x64" (
-        SET "FolderPostfix=_x64"
-    ) else (
-        SET "FolderPostfix="
-    )
+    SET "FolderPostfix="
+win64:
+    SET "FolderPostfix=_x64"
+winarm:
+    SET "FolderPostfix=_ARM64"
+win:
 depends:python/Scripts/activate.bat
     %THIRDPARTY_DIR%\\python\\Scripts\\activate.bat
     cd src\\client\\windows
@@ -1628,6 +1726,7 @@ win:
     SET OPUS_PATH=$USED_PREFIX/include/opus
     SET OPENSSL_PATH=$LIBS_DIR/openssl3/include
     SET LIBVPX_PATH=$USED_PREFIX/include
+    SET OPENH264_PATH=$USED_PREFIX/include
     SET FFMPEG_PATH=$LIBS_DIR/ffmpeg
     mkdir out
     cd out
@@ -1641,6 +1740,7 @@ win:
         -DTG_OWT_OPENSSL_INCLUDE_PATH=$OPENSSL_PATH \
         -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \
         -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \
+        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \
         -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..
     ninja
 release:
@@ -1655,12 +1755,14 @@ release:
         -DTG_OWT_OPENSSL_INCLUDE_PATH=$OPENSSL_PATH \
         -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \
         -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \
+        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \
         -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..
     ninja
 mac:
     MOZJPEG_PATH=$USED_PREFIX/include
     OPUS_PATH=$USED_PREFIX/include/opus
     LIBVPX_PATH=$USED_PREFIX/include
+    OPENH264_PATH=$USED_PREFIX/include
     FFMPEG_PATH=$USED_PREFIX/include
     mkdir out
     cd out
@@ -1675,6 +1777,7 @@ mac:
         -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \
         -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \
         -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \
+        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \
         -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..
     ninja
     cd ..
@@ -1689,6 +1792,7 @@ mac:
         -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \
         -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \
         -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \
+        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \
         -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..
     ninja
     cd ..
@@ -1705,6 +1809,7 @@ release:
         -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \
         -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \
         -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \
+        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \
         -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..
     ninja
     cd ..
@@ -1718,6 +1823,7 @@ release:
         -DTG_OWT_OPENSSL_INCLUDE_PATH=$LIBS_DIR/openssl3/include \
         -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_PATH \
         -DTG_OWT_LIBVPX_INCLUDE_PATH=$LIBVPX_PATH \
+        -DTG_OWT_OPENH264_INCLUDE_PATH=$OPENH264_PATH \
         -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_PATH ../..
     ninja
     cd ..
diff --git a/Telegram/lib_crl b/Telegram/lib_crl
index 078006d29..c1d6b0273 160000
--- a/Telegram/lib_crl
+++ b/Telegram/lib_crl
@@ -1 +1 @@
-Subproject commit 078006d29af0002e6cd8c61a405cdeaf65b37142
+Subproject commit c1d6b0273653095b10b4d0f4f7c30b614b690fd5
diff --git a/cmake b/cmake
index d50ff1a71..37d42a722 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit d50ff1a71e080e63f71c451f500a2f03f9d48539
+Subproject commit 37d42a72255aabde49deafb02904f58f06f4a97d

From ee9f99a754c3daea037bd37df6e3d2a0956cf6a9 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 12:25:27 +0200
Subject: [PATCH 054/163] Add some Windows on ARM special cases.

---
 Telegram/SourceFiles/boxes/about_box.cpp    | 4 ++++
 Telegram/SourceFiles/core/crash_reports.cpp | 6 +++++-
 Telegram/lib_base                           | 2 +-
 3 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/about_box.cpp b/Telegram/SourceFiles/boxes/about_box.cpp
index 6d001d407..75674299f 100644
--- a/Telegram/SourceFiles/boxes/about_box.cpp
+++ b/Telegram/SourceFiles/boxes/about_box.cpp
@@ -100,6 +100,8 @@ void AboutBox::showVersionHistory() {
 			url += u"win/%1.zip"_q;
 		} else if (Platform::IsWindows64Bit()) {
 			url += u"win64/%1.zip"_q;
+		} else if (Platform::IsWindowsARM64()) {
+			url += u"winarm/%1.zip"_q;
 		} else if (Platform::IsMac()) {
 			url += u"mac/%1.zip"_q;
 		} else if (Platform::IsLinux()) {
@@ -155,6 +157,8 @@ QString currentVersionText() {
 	}
 	if (Platform::IsWindows64Bit()) {
 		result += " x64";
+	} else if (Platform::IsWindowsARM64()) {
+		result += " arm64";
 	}
 	return result;
 }
diff --git a/Telegram/SourceFiles/core/crash_reports.cpp b/Telegram/SourceFiles/core/crash_reports.cpp
index 6f4e2d466..1e38d721a 100644
--- a/Telegram/SourceFiles/core/crash_reports.cpp
+++ b/Telegram/SourceFiles/core/crash_reports.cpp
@@ -296,13 +296,17 @@ bool DumpCallback(const google_breakpad::MinidumpDescriptor &md, void *context,
 
 QString PlatformString() {
 	if (Platform::IsWindowsStoreBuild()) {
-		return Platform::IsWindows64Bit()
+		return Platform::IsWindowsARM64()
+			? u"WinStoreARM64"_q
+			: Platform::IsWindows64Bit()
 			? u"WinStore64Bit"_q
 			: u"WinStore32Bit"_q;
 	} else if (Platform::IsWindows32Bit()) {
 		return u"Windows32Bit"_q;
 	} else if (Platform::IsWindows64Bit()) {
 		return u"Windows64Bit"_q;
+	} else if (Platform::IsWindowsARM64()) {
+		return u"WindowsARM64"_q;
 	} else if (Platform::IsMacStoreBuild()) {
 		return u"MacAppStore"_q;
 	} else if (Platform::IsMac()) {
diff --git a/Telegram/lib_base b/Telegram/lib_base
index 1a50fd230..54639131b 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit 1a50fd2300da3198e751a22bf728d33822180e15
+Subproject commit 54639131bf1e5dce87c10dba45062cfec804e343

From 08ec9e6bfd15bfae304776a0611b800c8d661349 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 13:52:57 +0200
Subject: [PATCH 055/163] Fix build on macOS with new dependencies.

---
 Telegram/build/prepare/prepare.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index 6300390d3..e1e31ddfb 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -852,11 +852,11 @@ mac:
         arch=$1
         folder=`pwd`/$2
 
-        meson setup \\
-            --cross-file ../patches/macos_meson_${arch}.txt \\
-            --prefix ${USED_PREFIX} \\
-            --default-library=static \\
-            --buildtype=minsize \\
+        meson setup \
+            --cross-file ../patches/macos_meson_${arch}.txt \
+            --prefix ${USED_PREFIX} \
+            --default-library=static \
+            --buildtype=minsize \
             ${folder}
         meson compile -C ${folder}
         meson install -C ${folder}
@@ -864,10 +864,10 @@ mac:
         mv ${USED_PREFIX}/lib/libopenh264.a ${folder}/libopenh264.a
     }
 
-    buildOneArch arm64 build.arm64
-    buildOneArch x86_64 build
+    buildOneArch aarch64 build.aarch64
+    buildOneArch x86_64 build.x86_64
 
-    lipo -create build.arm64/libopenh264.a build/libopenh264.a -output ${USED_PREFIX}/lib/libopenh264.a
+    lipo -create build.aarch64/libopenh264.a build.x86_64/libopenh264.a -output ${USED_PREFIX}/lib/libopenh264.a
 """)
 
 stage('libavif', """

From 28a6aa45b9a97c4db635d1e01d5fdd71b9973b3b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 14:03:36 +0200
Subject: [PATCH 056/163] Update msys2 and patches.

---
 Telegram/build/docker/centos_env/Dockerfile | 2 +-
 Telegram/build/prepare/prepare.py           | 6 +++---
 snap/snapcraft.yaml                         | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 00a3dc3b7..b5f7c86cd 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -43,7 +43,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 20a7c5ffd8265fc6e45203ea2536f7b1965be19a \
+	&& git fetch --depth=1 origin 6898f0d215f249917c076f00d3fc954a43f35e6a \
 	&& git reset --hard FETCH_HEAD \
 	&& rm -rf .git
 
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index e1e31ddfb..0658703b7 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -457,7 +457,7 @@ if customRunCommand:
 stage('patches', """
     git clone https://github.com/desktop-app/patches.git
     cd patches
-    git checkout 514332f808
+    git checkout 6898f0d215
 """)
 
 stage('msys64', """
@@ -468,12 +468,12 @@ win:
     SET CHERE_INVOKING=enabled_from_arguments
     SET MSYS2_PATH_TYPE=inherit
 
-    powershell -Command "iwr -OutFile ./msys64.exe https://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20221028.sfx.exe"
+    powershell -Command "iwr -OutFile ./msys64.exe https://github.com/msys2/msys2-installer/releases/download/2024-05-07/msys2-base-x86_64-20240507.sfx.exe"
     msys64.exe
     del msys64.exe
 
     bash -c "pacman-key --init; pacman-key --populate; pacman -Syu --noconfirm"
-    pacman -Syu --noconfirm mingw-w64-x86_64-perl mingw-w64-x86_64-nasm mingw-w64-x86_64-yasm mingw-w64-x86_64-ninja
+    pacman -Syu --noconfirm mingw-w64-x86_64-perl mingw-w64-x86_64-nasm mingw-w64-x86_64-yasm mingw-w64-x86_64-ninja msys/make diffutils pkg-config
 
     SET PATH=%PATH_BACKUP_%
 """, 'ThirdParty')
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index b3c8d1905..0a3d3f855 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -165,7 +165,7 @@ parts:
   patches:
     source: https://github.com/desktop-app/patches.git
     source-depth: 1
-    source-commit: 20a7c5ffd8265fc6e45203ea2536f7b1965be19a
+    source-commit: 6898f0d215f249917c076f00d3fc954a43f35e6a
     plugin: dump
     override-pull: |
       craftctl default

From 675ee9088ff2a7f0bc3c6b0aa472204b358cce23 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 15:24:29 +0200
Subject: [PATCH 057/163] Fix build on Windows.

---
 Telegram/build/prepare/prepare.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index 0658703b7..ea305575e 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -621,7 +621,7 @@ win64:
 winarm:
     perl Configure no-shared no-tests debug-VC-WIN64-ARM /FS
 win:
-    jom -j%NUMBER_OF_PROCESSORS%
+    jom -j%NUMBER_OF_PROCESSORS% build_libs
     mkdir out.dbg
     move libcrypto.lib out.dbg
     move libssl.lib out.dbg
@@ -637,7 +637,7 @@ win64_release:
 winarm_release:
     perl Configure no-shared no-tests VC-WIN64-ARM /FS
 win_release:
-    jom -j%NUMBER_OF_PROCESSORS%
+    jom -j%NUMBER_OF_PROCESSORS% build_libs
     mkdir out
     move libcrypto.lib out
     move libssl.lib out
@@ -733,7 +733,7 @@ mac:
 """)
 
 stage('gas-preprocessor', """
-winarm:
+win:
     git clone https://github.com/FFmpeg/gas-preprocessor
     cd gas-preprocessor
     echo @echo off > cpp.bat

From 61ca619db40be13c7725e00069f78ff17e4c8bad Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 16:20:18 +0200
Subject: [PATCH 058/163] Fix build on macOS.

---
 .../info/statistics/info_statistics_list_controllers.cpp         | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
index a9649fb4d..9c2a692e7 100644
--- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
+++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
@@ -948,7 +948,6 @@ void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) {
 					delegate()->peerListUpdateRow(row);
 				},
 			};
-			using Type = Data::CreditsHistoryEntry::PeerType;
 			if (const auto peerId = PeerId(item.barePeerId)) {
 				const auto peer = session().data().peer(peerId);
 				return std::make_unique<CreditsRow>(peer, descriptor);

From 2cd6bfef067dcf351db1dbc472ed780ea99aaa1c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 16:20:45 +0200
Subject: [PATCH 059/163] Fix H265 in webrtc.

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

diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake
index 3f6938e72..018a7cfb0 100644
--- a/Telegram/cmake/lib_tgcalls.cmake
+++ b/Telegram/cmake/lib_tgcalls.cmake
@@ -221,6 +221,7 @@ PUBLIC
     TGCALLS_USE_STD_OPTIONAL
 PRIVATE
     WEBRTC_APP_TDESKTOP
+    RTC_ENABLE_H265
     RTC_ENABLE_VP9
 )
 
diff --git a/cmake b/cmake
index 37d42a722..3fe312335 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 37d42a72255aabde49deafb02904f58f06f4a97d
+Subproject commit 3fe312335453073028a7bc5e7b3d65f42011e34a

From 11c45b03420f6d7c803c36d8b1d98359a75bd7ae Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 17:11:42 +0200
Subject: [PATCH 060/163] Update submodules.

---
 Telegram/ThirdParty/tgcalls | 2 +-
 Telegram/lib_webrtc         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index 0c23e3803..2dc886d05 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit 0c23e3803bc29e115f886308275bf0e3a313bc58
+Subproject commit 2dc886d05e4b7848dd7964b8e89bafdce905ba4f
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index 81d01697c..839d9f0aa 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit 81d01697c2ee2b46c5e5c41ce1176c230264e4cb
+Subproject commit 839d9f0aade5e20a03d36caceec22ed6c8c7a63f

From f123a9e16cff1bb17390b1d3dcb3f484f9fb2242 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 23:32:12 +0800
Subject: [PATCH 061/163] Add openh264 to Dockerfile.

---
 Telegram/build/docker/centos_env/Dockerfile | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index b5f7c86cd..d60e6d403 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -147,6 +147,17 @@ RUN git clone -b 1.4.1 --depth=1 {{ GIT }}/videolan/dav1d.git \
 	&& cd .. \
 	&& rm -rf dav1d
 
+FROM builder AS openh264
+RUN git clone -b v2.4.1 --depth=1 {{ GIT }}/cisco/openh264.git \
+	&& cd openh264 \
+	&& meson build \
+		--buildtype=plain \
+		--default-library=both \
+	&& meson compile -C build \
+	&& DESTDIR="{{ LibrariesPath }}/openh264-cache" meson install -C build \
+	&& cd .. \
+	&& rm -rf openh264
+
 FROM builder AS libde265
 RUN git clone -b v1.0.15 --depth=1 {{ GIT }}/strukturag/libde265.git \
 	&& cd libde265 \
@@ -742,6 +753,7 @@ RUN git clone -b v2023.06.01 --depth=1 https://chromium.googlesource.com/breakpa
 
 FROM builder AS webrtc
 COPY --link --from=opus {{ LibrariesPath }}/opus-cache /
+COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache /
 COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache /
 COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache /
 COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache /
@@ -767,6 +779,7 @@ RUN git init tg_owt \
 		-DTG_OWT_OPENSSL_INCLUDE_PATH=/usr/local/include \
 		-DTG_OWT_OPUS_INCLUDE_PATH=/usr/local/include/opus \
 		-DTG_OWT_LIBVPX_INCLUDE_PATH=/usr/local/include \
+		-DTG_OWT_OPENH264_INCLUDE_PATH=/usr/local/include \
 		-DTG_OWT_FFMPEG_INCLUDE_PATH=/usr/local/include
 
 WORKDIR tg_owt
@@ -791,6 +804,7 @@ COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache /
 COPY --link --from=highway {{ LibrariesPath }}/highway-cache /
 COPY --link --from=opus {{ LibrariesPath }}/opus-cache /
 COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache /
+COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache /
 COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache /
 COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache /
 COPY --link --from=libavif {{ LibrariesPath }}/libavif-cache /

From 2f22a8f46b6aabd45308ca54acd36cfb92215e90 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 Jul 2024 10:33:58 +0200
Subject: [PATCH 062/163] Add Windows on ARM to build scripts.

---
 Telegram/SourceFiles/_other/packer.cpp       |  4 +-
 Telegram/SourceFiles/core/update_checker.cpp |  1 +
 Telegram/build/build.bat                     | 67 ++++++++++++++------
 Telegram/build/deploy.sh                     | 35 +++++++++-
 Telegram/build/release.py                    | 14 ++++
 Telegram/build/setup.iss                     |  9 ++-
 Telegram/lib_base                            |  2 +-
 7 files changed, 108 insertions(+), 24 deletions(-)

diff --git a/Telegram/SourceFiles/_other/packer.cpp b/Telegram/SourceFiles/_other/packer.cpp
index 156ba726a..e563e1675 100644
--- a/Telegram/SourceFiles/_other/packer.cpp
+++ b/Telegram/SourceFiles/_other/packer.cpp
@@ -155,6 +155,7 @@ int main(int argc, char *argv[])
 	QString remove;
 	int version = 0;
 	[[maybe_unused]] bool targetwin64 = false;
+	[[maybe_unused]] bool targetwinarm = false;
 	[[maybe_unused]] bool targetarmac = false;
 	QFileInfoList files;
 	for (int i = 0; i < argc; ++i) {
@@ -165,6 +166,7 @@ int main(int argc, char *argv[])
 			if (remove.isEmpty()) remove = info.canonicalPath() + "/";
 		} else if (string("-target") == argv[i] && i + 1 < argc) {
 			targetwin64 = (string("win64") == argv[i + 1]);
+			targetwinarm = (string("winarm") == argv[i + 1]);
 		} else if (string("-arch") == argv[i] && i + 1 < argc) {
 			targetarmac = (string("arm64") == argv[i + 1]);
 			if (!targetarmac && string("x86_64") != argv[i + 1]) {
@@ -493,7 +495,7 @@ int main(int argc, char *argv[])
 	cout << "Signature verified!\n";
 	RSA_free(pbKey);
 #ifdef Q_OS_WIN
-	QString outName((targetwin64 ? QString("tx64upd%1") : QString("tupdate%1")).arg(AlphaVersion ? AlphaVersion : version));
+	QString outName((targetwinarm ? QString("tarm64upd%1") : targetwin64 ? QString("tx64upd%1") : QString("tupdate%1")).arg(AlphaVersion ? AlphaVersion : version));
 #elif defined Q_OS_MAC
 	QString outName((targetarmac ? QString("tarmacupd%1") : QString("tmacupd%1")).arg(AlphaVersion ? AlphaVersion : version));
 #else
diff --git a/Telegram/SourceFiles/core/update_checker.cpp b/Telegram/SourceFiles/core/update_checker.cpp
index fe95f60db..777dde80c 100644
--- a/Telegram/SourceFiles/core/update_checker.cpp
+++ b/Telegram/SourceFiles/core/update_checker.cpp
@@ -245,6 +245,7 @@ QString FindUpdateFile() {
 			"^("
 			"tupdate|"
 			"tx64upd|"
+			"tarm64upd|"
 			"tmacupd|"
 			"tarmacupd|"
 			"tlinuxupd|"
diff --git a/Telegram/build/build.bat b/Telegram/build/build.bat
index 3f3b65caa..6dc539c61 100644
--- a/Telegram/build/build.bat
+++ b/Telegram/build/build.bat
@@ -14,42 +14,54 @@ if not exist "%FullScriptPath%..\..\..\DesktopPrivate" (
 
 FOR /F "tokens=1* delims= " %%i in (%FullScriptPath%target) do set "BuildTarget=%%i"
 
-if "%BuildTarget%" equ "uwp" (
-  set "BuildUWP=1"
-) else if "%BuildTarget%" equ "uwp64" (
-  set "BuildUWP=1"
-) else (
-  set "BuildUWP=0"
-)
-
+set "Build64=0"
+set "BuildARM=0"
+set "BuildUWP=0"
 if "%BuildTarget%" equ "win64" (
   set "Build64=1"
+) else if "%BuildTarget%" equ "winarm" (
+  set "BuildARM=1"
+) else if "%BuildTarget%" equ "uwp" (
+  set "BuildUWP=1"
 ) else if "%BuildTarget%" equ "uwp64" (
   set "Build64=1"
-) else (
-  set "Build64=0"
+  set "BuildUWP=1"
+) else if "%BuildTarget%" equ "uwparm" (
+  set "BuildARM=1"
+  set "BuildUWP=1"
 )
 
 if %Build64% neq 0 (
   if "%Platform%" neq "x64" (
-    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2019'.
+    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'.
     exit /b
   ) else if "%VSCMD_ARG_HOST_ARCH%" neq "x64" (
-    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2019'.
+    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'.
     exit /b
   ) else if "%VSCMD_ARG_TGT_ARCH%" neq "x64" (
-    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2019'.
+    echo Bad environment. Make sure to run from 'x64 Native Tools Command Prompt for VS 2022'.
+    exit /b
+  )
+) else if %BuildARM% neq 0 (
+  if "%Platform%" neq "arm64" (
+    echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'.
+    exit /b
+  ) else if "%VSCMD_ARG_HOST_ARCH%" neq "arm64" (
+    echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'.
+    exit /b
+  ) else if "%VSCMD_ARG_TGT_ARCH%" neq "arm64" (
+    echo Bad environment. Make sure to run from 'ARM64 Native Tools Command Prompt for VS 2022'.
     exit /b
   )
 ) else (
   if "%Platform%" neq "x86" (
-    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2019'.
+    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'.
     exit /b
   ) else if "%VSCMD_ARG_HOST_ARCH%" neq "x86" (
-    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2019'.
+    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'.
     exit /b
   ) else if "%VSCMD_ARG_TGT_ARCH%" neq "x86" (
-    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2019'.
+    echo Bad environment. Make sure to run from 'x86 Native Tools Command Prompt for VS 2022'.
     exit /b
   )
 )
@@ -76,12 +88,16 @@ echo.
 if %BuildUWP% neq 0 (
   if %Build64% neq 0 (
     echo Building version %AppVersionStrFull% for UWP 64 bit..
+  ) else if %BuildARM% neq 0 (
+    echo Building version %AppVersionStrFull% for UWP ARM..
   ) else (
     echo Building version %AppVersionStrFull% for UWP..
   )
 ) else (
   if %Build64% neq 0 (
     echo Building version %AppVersionStrFull% for Windows 64 bit..
+  ) else if %BuildARM% neq 0 (
+    echo Building version %AppVersionStrFull% for Windows on ARM..
   ) else (
     echo Building version %AppVersionStrFull% for Windows..
   )
@@ -96,6 +112,11 @@ if %Build64% neq 0 (
   set "SetupFile=tsetup-x64.%AppVersionStrFull%.exe"
   set "PortableFile=tportable-x64.%AppVersionStrFull%.zip"
   set "DumpSymsPath=%SolutionPath%\..\..\Libraries\win64\breakpad\src\tools\windows\dump_syms\Release\dump_syms.exe"
+) else if %BuildARM% neq 0 (
+  set "UpdateFile=tarm64upd%AppVersion%"
+  set "SetupFile=tsetup-arm64.%AppVersionStrFull%.exe"
+  set "PortableFile=tportable-arm64.%AppVersionStrFull%.zip"
+  set "DumpSymsPath=%SolutionPath%\..\..\Libraries\breakpad\src\tools\windows\dump_syms\Release\dump_syms.exe"
 ) else (
   set "UpdateFile=tupdate%AppVersion%"
   set "SetupFile=tsetup.%AppVersionStrFull%.exe"
@@ -210,7 +231,11 @@ if %BuildUWP% equ 0 (
     if not exist "%SetupFile%" goto error
   )
 
-  call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -path "modules\%Platform%\d3d\d3dcompiler_47.dll" -target %BuildTarget% %AlphaBetaParam%
+  if %BuildARM% neq 0 (
+    call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -target %BuildTarget% %AlphaBetaParam%
+  ) else (
+    call Packer.exe -version %VersionForPacker% -path %BinaryName%.exe -path Updater.exe -path "modules\%Platform%\d3d\d3dcompiler_47.dll" -target %BuildTarget% %AlphaBetaParam%
+  )
   if %errorlevel% neq 0 goto error
 
   if %AlphaVersion% neq 0 (
@@ -309,10 +334,12 @@ if %BuildUWP% neq 0 (
   if %errorlevel% neq 0 goto error
 )
 
-if %Build64% equ 0 (
-  set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tsetup"
-) else (
+if %Build64% neq 0 (
   set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tx64"
+) else if %BuildARM% neq 0 (
+  set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tarm64"
+) else (
+  set "FinalDeployPath=%FinalReleasePath%\%AppVersionStrMajor%\%AppVersionStrFull%\tsetup"
 )
 
 if %BuildUWP% equ 0 (
diff --git a/Telegram/build/deploy.sh b/Telegram/build/deploy.sh
index 23e8143e3..21183fa38 100755
--- a/Telegram/build/deploy.sh
+++ b/Telegram/build/deploy.sh
@@ -49,6 +49,7 @@ HomePath="$FullScriptPath/.."
 DeployMac="0"
 DeployWin="0"
 DeployWin64="0"
+DeployWinArm="0"
 DeployLinux="0"
 if [ "$DeployTarget" == "mac" ]; then
   DeployMac="1"
@@ -59,6 +60,9 @@ elif [ "$DeployTarget" == "win" ]; then
 elif [ "$DeployTarget" == "win64" ]; then
   DeployWin64="1"
   echo "Deploying version $AppVersionStrFull for Windows 64 bit.."
+elif [ "$DeployTarget" == "winarm" ]; then
+  DeployWinArm="1"
+  echo "Deploying version $AppVersionStrFull for Windows on ARM.."
 elif [ "$DeployTarget" == "linux" ]; then
   DeployLinux="1"
   echo "Deploying version $AppVersionStrFull for Linux 64 bit.."
@@ -66,8 +70,9 @@ else
   DeployMac="1"
   DeployWin="1"
   DeployWin64="1"
+  DeployWinArm="1"
   DeployLinux="1"
-  echo "Deploying four versions of $AppVersionStrFull: for Windows 32 bit, Windows 64 bit, macOS and Linux 64 bit.."
+  echo "Deploying five versions of $AppVersionStrFull: for Windows 32 bit, Windows 64 bit, Windows on ARM, macOS and Linux 64 bit.."
 fi
 if [ "$BuildTarget" == "mac" ]; then
   BackupPath="$HOME/Projects/backup/tdesktop"
@@ -94,6 +99,11 @@ Win64UpdateFile="tx64upd$AppVersion"
 Win64SetupFile="tsetup-x64.$AppVersionStrFull.exe"
 Win64PortableFile="tportable-x64.$AppVersionStrFull.zip"
 Win64RemoteFolder="tx64"
+WinArmDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tarm64"
+WinArmUpdateFile="tarm64upd$AppVersion"
+WinArmSetupFile="tsetup-arm64.$AppVersionStrFull.exe"
+WinArmPortablefile="tportable-arm64.$AppVersionStrFull.zip"
+WinArmRemoteFolder="tarm64"
 LinuxDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tlinux"
 LinuxUpdateFile="tlinuxupd$AppVersion"
 LinuxSetupFile="tsetup.$AppVersionStrFull.tar.xz"
@@ -105,6 +115,8 @@ if [ "$AlphaVersion" != "0" ]; then
     AlphaFilePath="$WinDeployPath/$AlphaKeyFile"
   elif [ "$DeployTarget" == "win64" ]; then
     AlphaFilePath="$Win64DeployPath/$AlphaKeyFile"
+  elif [ "$DeployTarget" == "winarm" ]; then
+    AlphaFilePath="$WinArmDeployPath/$AlphaKeyFile"
   elif [ "$DeployTarget" == "linux" ]; then
     AlphaFilePath="$LinuxDeployPath/$AlphaKeyFile"
   else
@@ -125,6 +137,8 @@ if [ "$AlphaVersion" != "0" ]; then
   WinPortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip"
   Win64UpdateFile="${Win64UpdateFile}_${AlphaSignature}"
   Win64PortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip"
+  WinArmUpdateFile="${WinArmUpdateFile}_${AlphaSignature}"
+  WinArmPortablefile="talpha${AlphaVersion}_${AlphaSignature}.zip"
   LinuxUpdateFile="${LinuxUpdateFile}_${AlphaSignature}"
   LinuxSetupFile="talpha${AlphaVersion}_${AlphaSignature}.tar.xz"
 fi
@@ -166,6 +180,19 @@ if [ "$DeployWin64" == "1" ]; then
     Error "$Win64PortableFile not found!"
   fi
 fi
+if [ "$DeployWinArm" == "1" ]; then
+  if [ ! -f "$WinArmDeployPath/$WinArmUpdateFile" ]; then
+    Error "$WinArmUpdateFile not found!"
+  fi
+  if [ "$AlphaVersion" == "0" ]; then
+    if [ ! -f "$WinArmDeployPath/$WinArmSetupFile" ]; then
+      Error "$WinArmSetupFile not found!"
+    fi
+  fi
+  if [ ! -f "$WinArmDeployPath/$WinArmPortableFile" ]; then
+    Error "$WinArmPortableFile not found!"
+  fi
+fi
 if [ "$DeployLinux" == "1" ]; then
   if [ ! -f "$LinuxDeployPath/$LinuxUpdateFile" ]; then
     Error "$LinuxDeployPath/$LinuxUpdateFile not found!"
@@ -193,6 +220,12 @@ if [ "$DeployWin64" == "1" ]; then
     Files+=("tx64/$Win64SetupFile")
   fi
 fi
+if [ "$DeployWinArm" == "1" ]; then
+  Files+=("tarm64/$WinArmUpdateFile" "tarm64/$WinArmPortableFile")
+  if [ "$AlphaVersion" == "0" ]; then
+    Files+=("tarm64/$WinArmSetupFile")
+  fi
+fi
 if [ "$DeployLinux" == "1" ]; then
   Files+=("tlinux/$LinuxUpdateFile" "tlinux/$LinuxSetupFile")
 fi
diff --git a/Telegram/build/release.py b/Telegram/build/release.py
index 4bcf93fe9..4e6f38352 100644
--- a/Telegram/build/release.py
+++ b/Telegram/build/release.py
@@ -220,6 +220,20 @@ files.append({
   'mime': 'application/zip',
   'label': 'Windows 64 bit: Portable',
 })
+files.append({
+  'local': 'tsetup-arm64.' + version_full + '.exe',
+  'remote': 'tsetup-arm64.' + version_full + '.exe',
+  'backup_folder': 'tarm64',
+  'mime': 'application/octet-stream',
+  'label': 'Windows on ARM: Installer',
+})
+files.append({
+  'local': 'tportable-arm64.' + version_full + '.zip',
+  'remote': 'tportable-arm64.' + version_full + '.zip',
+  'backup_folder': 'tarm64',
+  'mime': 'application/zip',
+  'label': 'Windows on ARM: Portable',
+})
 files.append({
   'local': 'tsetup.' + version_full + '.dmg',
   'remote': 'tsetup.' + version_full + '.dmg',
diff --git a/Telegram/build/setup.iss b/Telegram/build/setup.iss
index 5c5ee7635..b0dd43f65 100644
--- a/Telegram/build/setup.iss
+++ b/Telegram/build/setup.iss
@@ -36,7 +36,12 @@ DisableProgramGroupPage=no
 WizardStyle=modern
 SignTool=sha256
 
-#if MyBuildTarget == "win64"
+#if MyBuildTarget == "winarm"
+  ArchitecturesAllowed="arm64"
+  OutputBaseFilename=tsetup-arm64.{#MyAppVersionFull}
+  #define ArchModulesFolder "arm64"
+  AppVerName={#MyAppName} {#MyAppVersion} arm64
+#elif MyBuildTarget == "win64"
   ArchitecturesAllowed="x64 arm64"
   ArchitecturesInstallIn64BitMode="x64 arm64"
   OutputBaseFilename=tsetup-x64.{#MyAppVersionFull}
@@ -68,7 +73,9 @@ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescrip
 [Files]
 Source: "{#ReleasePath}\Telegram.exe"; DestDir: "{app}"; Flags: ignoreversion
 Source: "{#ReleasePath}\Updater.exe"; DestDir: "{app}"; Flags: ignoreversion
+#if MyBuildTarget != "winarm"
 Source: "{#ReleasePath}\{#ModulesFolder}\d3d\d3dcompiler_47.dll"; DestDir: "{app}\{#ModulesFolder}\d3d"; Flags: ignoreversion
+#endif
 ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
 
 [Icons]
diff --git a/Telegram/lib_base b/Telegram/lib_base
index 54639131b..4ac8cf9d6 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit 54639131bf1e5dce87c10dba45062cfec804e343
+Subproject commit 4ac8cf9d65e47efa9d2022939c6d0c38f32d9c7a

From 8a6a749296e58b684b14b2a7117cb70bbb10a5de Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 Jul 2024 10:47:50 +0200
Subject: [PATCH 063/163] Show error on attempt to scan QR in bot app.

Fixes #26886.
---
 Telegram/Resources/langs/lang.strings             |  1 +
 .../ui/chat/attach/attach_bot_webview.cpp         | 15 +++++++++++++++
 .../ui/chat/attach/attach_bot_webview.h           |  1 +
 3 files changed, 17 insertions(+)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index c692195bc..d3427dce9 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3160,6 +3160,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_bot_close_warning_sure" = "Close anyway";
 "lng_bot_add_to_side_menu" = "{bot} asks your permission to be added as an option to your main menu so you can access it any time.";
 "lng_bot_add_to_side_menu_done" = "Bot added to the main menu.";
+"lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps.";
 
 "lng_typing" = "typing";
 "lng_user_typing" = "{user} is typing";
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index cbc27c0b8..cb922a4ff 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -694,6 +694,8 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
 			openInvoice(arguments);
 		} else if (command == "web_app_open_popup") {
 			openPopup(arguments);
+		} else if (command == "web_app_open_scan_qr_popup") {
+			openScanQrPopup(arguments);
 		} else if (command == "web_app_request_write_access") {
 			requestWriteAccess();
 		} else if (command == "web_app_request_phone") {
@@ -918,6 +920,19 @@ void Panel::openPopup(const QJsonObject &args) {
 	}
 }
 
+void Panel::openScanQrPopup(const QJsonObject &args) {
+	const auto widget = _webview->window.widget();
+	[[maybe_unused]] const auto ok = Webview::ShowBlockingPopup({
+		.parent = widget ? widget->window() : nullptr,
+		.text = tr::lng_bot_no_scan_qr(tr::now),
+		.buttons = { {
+			.id = "ok",
+			.text = tr::lng_box_ok(tr::now),
+			.type = Webview::PopupArgs::Button::Type::Ok,
+		}},
+	});
+}
+
 void Panel::requestWriteAccess() {
 	if (_inBlockingRequest) {
 		replyRequestWriteAccess(false);
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
index d9071c846..e25483b89 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
@@ -132,6 +132,7 @@ private:
 	void openExternalLink(const QJsonObject &args);
 	void openInvoice(const QJsonObject &args);
 	void openPopup(const QJsonObject &args);
+	void openScanQrPopup(const QJsonObject &args);
 	void requestWriteAccess();
 	void replyRequestWriteAccess(bool allowed);
 	void requestPhone();

From 730c968b1efb245871eba5b2e771ebb969ec395d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 Jul 2024 13:21:14 +0200
Subject: [PATCH 064/163] Use always 1:1 ratio in limits boxes.

Otherwise "Free" label doesn't fit in 2:20 shareable folders.
---
 Telegram/SourceFiles/boxes/premium_limits_box.cpp | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp
index 451957c21..8febb5f42 100644
--- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp
+++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp
@@ -418,7 +418,9 @@ void SimpleLimitBox(
 		BoxShowFinishes(box),
 		0,
 		descriptor.current,
-		descriptor.premiumLimit,
+		(descriptor.complexRatio
+			? descriptor.premiumLimit
+			: 2 * descriptor.current),
 		premiumPossible,
 		descriptor.phrase,
 		descriptor.icon);
@@ -769,7 +771,7 @@ void FilterLinksLimitBox(
 			premiumLimit,
 			&st::premiumIconChats,
 			std::nullopt,
-			true });
+			/*true */}); // Don't use real ratio, "Free" doesn't fit.
 }
 
 
@@ -856,7 +858,7 @@ void ShareableFiltersLimitBox(
 			premiumLimit,
 			&st::premiumIconFolders,
 			std::nullopt,
-			true });
+			/*true*/ }); // Don't use real ratio, "Free" doesn't fit.
 }
 
 void FilterPinsLimitBox(

From 484c647b5b22ad344fe59d6309905dc324a35914 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 Jul 2024 13:55:13 +0200
Subject: [PATCH 065/163] Fix bot about text layout on wide windows.

---
 Telegram/SourceFiles/history/view/history_view_message.cpp | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index 0d93ee0b6..4c6d4970d 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -929,9 +929,6 @@ QSize Message::performCountOptimalSize() {
 					- st::msgPadding.left()
 					- st::msgPadding.right();
 				if (withVisibleText) {
-					if (botTop) {
-						minHeight += botTop->height;
-					}
 					if (maxWidth < textualWidth) {
 						minHeight -= text().minHeight();
 						minHeight += text().countHeight(innerWidth);

From f8b756d4470749a9a07d6112a118ee5e48e4e380 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 Jul 2024 16:41:18 +0200
Subject: [PATCH 066/163] Improve bot checkout error messages.

---
 Telegram/Resources/langs/lang.strings         |   4 +
 .../SourceFiles/boxes/send_credits_box.cpp    |  30 +++-
 .../view/media/history_view_media_common.cpp  |  21 +--
 .../inline_bots/bot_attach_web_view.cpp       |  34 +++-
 .../inline_bots/bot_attach_web_view.h         |   7 +
 .../payments/payments_checkout_process.cpp    |  10 ++
 .../payments/payments_checkout_process.h      |  11 +-
 .../payments/payments_non_panel_process.cpp   | 159 ++++++++++--------
 .../payments/payments_non_panel_process.h     |  17 ++
 .../SourceFiles/settings/settings_credits.cpp |   2 +-
 .../settings/settings_credits_graphics.cpp    |  32 ++--
 .../settings/settings_credits_graphics.h      |   8 +-
 .../ui/chat/attach/attach_bot_webview.cpp     |   6 +-
 13 files changed, 221 insertions(+), 120 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index d3427dce9..6c9828189 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3161,6 +3161,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_bot_add_to_side_menu" = "{bot} asks your permission to be added as an option to your main menu so you can access it any time.";
 "lng_bot_add_to_side_menu_done" = "Bot added to the main menu.";
 "lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps.";
+"lng_bot_click_to_start" = "Click here to use this bot.";
 
 "lng_typing" = "typing";
 "lng_user_typing" = "{user} is typing";
@@ -3757,6 +3758,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_payments_card_declined" = "Your card was declined.";
 "lng_payments_payment_failed" = "Payment failed. Your card has not been billed.";
 "lng_payments_precheckout_failed" = "The bot couldn't process your payment. Your card has not been billed.";
+"lng_payments_precheckout_timeout" = "The bot didn't respond in time. Your card has not been billed.";
+"lng_payments_precheckout_stars_failed" = "The bot couldn't process your payment.";
+"lng_payments_precheckout_stars_timeout" = "The bot didn't respond in time.";
 "lng_payments_already_paid" = "You have already paid for this item.";
 
 "lng_payments_terms_title" = "Terms of Service";
diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp
index 39d7983d7..a8771cc17 100644
--- a/Telegram/SourceFiles/boxes/send_credits_box.cpp
+++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "payments/payments_checkout_process.h"
 #include "payments/payments_form.h"
 #include "settings/settings_credits_graphics.h"
+#include "ui/boxes/confirm_box.h"
 #include "ui/controls/userpic_button.h"
 #include "ui/effects/premium_graphics.h"
 #include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg.
@@ -257,6 +258,8 @@ void SendCreditsBox(
 		if (state->confirmButtonBusy.current()) {
 			return;
 		}
+		const auto show = box->uiShow();
+		const auto weak = MakeWeak(box.get());
 		state->confirmButtonBusy = true;
 		session->api().request(
 			MTPpayments_SendStarsForm(
@@ -264,12 +267,31 @@ void SendCreditsBox(
 				MTP_long(form->formId),
 				form->inputInvoice)
 		).done([=](auto result) {
-			state->confirmButtonBusy = false;
-			box->closeBox();
+			if (weak) {
+				state->confirmButtonBusy = false;
+				box->closeBox();
+			}
 			sent();
 		}).fail([=](const MTP::Error &error) {
-			state->confirmButtonBusy = false;
-			box->uiShow()->showToast(error.type());
+			if (weak) {
+				state->confirmButtonBusy = false;
+			}
+			const auto id = error.type();
+			if (id == u"BOT_PRECHECKOUT_FAILED"_q) {
+				auto error = ::Ui::MakeInformBox(
+					tr::lng_payments_precheckout_stars_failed(tr::now));
+				error->boxClosing() | rpl::start_with_next([=] {
+					if (const auto paybox = weak.data()) {
+						paybox->closeBox();
+					}
+				}, error->lifetime());
+				show->showBox(std::move(error));
+			} else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) {
+				show->showToast(
+					tr::lng_payments_precheckout_stars_timeout(tr::now));
+			} else {
+				show->showToast(id);
+			}
 		}).send();
 	});
 	{
diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp
index dd32fa00f..71b01fbf8 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp
@@ -241,19 +241,20 @@ ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
 				}
 			}
 		});
+		const auto reactivate = controller
+			? crl::guard(
+				controller,
+				[=](auto) { controller->widget()->activate(); })
+			: Fn<void(Payments::CheckoutResult)>();
+		const auto credits = Payments::IsCreditsInvoice(item);
+		const auto nonPanelPaymentFormProcess = (controller && credits)
+			? Payments::ProcessNonPanelPaymentFormFactory(controller, done)
+			: nullptr;
 		Payments::CheckoutProcess::Start(
 			item,
 			Payments::Mode::Payment,
-			(controller
-				? crl::guard(
-					controller,
-					[=](auto) { controller->widget()->activate(); })
-				: Fn<void(Payments::CheckoutResult)>()),
-			((controller && Payments::IsCreditsInvoice(item))
-				? Payments::ProcessNonPanelPaymentFormFactory(
-					controller,
-					done)
-				: nullptr));
+			reactivate,
+			nonPanelPaymentFormProcess);
 	});
 }
 
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index c42ecb3b9..c8e88aed7 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -621,16 +621,38 @@ void AttachWebView::botHandleInvoice(QString slug) {
 			}());
 		}
 	};
-	_panel->hideForPayment();
 	Payments::CheckoutProcess::Start(
 		&_bot->session(),
 		slug,
 		reactivate,
-		_context
-			? Payments::ProcessNonPanelPaymentFormFactory(
-				_context->controller.get(),
-				reactivate)
-			: nullptr);
+		nonPanelPaymentFormFactory(reactivate));
+}
+
+auto AttachWebView::nonPanelPaymentFormFactory(
+	Fn<void(Payments::CheckoutResult)> reactivate)
+-> Fn<void(Payments::NonPanelPaymentForm)> {
+	using namespace Payments;
+	const auto panel = base::make_weak(_panel.get());
+	const auto weak = _context ? _context->controller : nullptr;
+	return [=](Payments::NonPanelPaymentForm form) {
+		using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
+		using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
+		v::match(form, [&](const CreditsFormDataPtr &form) {
+			if (const auto strong = panel.get()) {
+				ProcessCreditsPayment(
+					uiShow(),
+					strong->toastParent().get(),
+					form,
+					reactivate);
+			}
+		}, [&](const CreditsReceiptPtr &receipt) {
+			if (const auto controller = weak.get()) {
+				ProcessCreditsReceipt(controller, receipt, reactivate);
+			}
+		}, [&](RealFormPresentedNotification) {
+			_panel->hideForPayment();
+		});
+	};
 }
 
 void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) {
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
index 87648b07e..f9f59f1ae 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
@@ -40,6 +40,11 @@ namespace Data {
 class DocumentMedia;
 } // namespace Data
 
+namespace Payments {
+struct NonPanelPaymentForm;
+enum class CheckoutResult;
+} // namespace Payments
+
 namespace InlineBots {
 
 enum class PeerType : uint8 {
@@ -246,6 +251,8 @@ private:
 	void showToast(
 		const QString &text,
 		Window::SessionController *controller = nullptr);
+	Fn<void(Payments::NonPanelPaymentForm)> nonPanelPaymentFormFactory(
+		Fn<void(Payments::CheckoutResult)> reactivate);
 
 	const not_null<Main::Session*> _session;
 
diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp
index cbb435389..9a5daf9a4 100644
--- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp
+++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp
@@ -536,6 +536,8 @@ void CheckoutProcess::handleError(const Error &error) {
 			showToast({ tr::lng_payments_payment_failed(tr::now) });
 		} else if (id == u"BOT_PRECHECKOUT_FAILED"_q) {
 			showToast({ tr::lng_payments_precheckout_failed(tr::now) });
+		} else if (id == u"BOT_PRECHECKOUT_TIMEOUT"_q) {
+			showToast({ tr::lng_payments_precheckout_timeout(tr::now) });
 		} else if (id == u"REQUESTED_INFO_INVALID"_q
 			|| id == u"SHIPPING_OPTION_INVALID"_q
 			|| id == u"PAYMENT_CREDENTIALS_INVALID"_q
@@ -764,6 +766,14 @@ void CheckoutProcess::showForm() {
 		_form->information(),
 		_form->paymentMethod().ui,
 		_form->shippingOptions());
+	if (_nonPanelPaymentFormProcess && !_realFormNotified) {
+		_realFormNotified = true;
+		const auto weak = base::make_weak(_panel.get());
+		_nonPanelPaymentFormProcess(RealFormPresentedNotification());
+		if (weak) {
+			requestActivate();
+		}
+	}
 }
 
 void CheckoutProcess::showEditInformation(Ui::InformationField field) {
diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h
index 3c7f112ca..e783bba58 100644
--- a/Telegram/SourceFiles/payments/payments_checkout_process.h
+++ b/Telegram/SourceFiles/payments/payments_checkout_process.h
@@ -55,9 +55,13 @@ enum class CheckoutResult {
 	Failed,
 };
 
-struct NonPanelPaymentForm : std::variant<
-	std::shared_ptr<CreditsFormData>,
-	std::shared_ptr<CreditsReceiptData>> {
+struct RealFormPresentedNotification {
+};
+struct NonPanelPaymentForm
+	: std::variant<
+		std::shared_ptr<CreditsFormData>,
+		std::shared_ptr<CreditsReceiptData>,
+		RealFormPresentedNotification> {
 	using variant::variant;
 };
 
@@ -183,6 +187,7 @@ private:
 	Fn<void(NonPanelPaymentForm)> _nonPanelPaymentFormProcess;
 	SubmitState _submitState = SubmitState::None;
 	bool _initialSilentValidation = false;
+	bool _realFormNotified = false;
 	bool _sendFormPending = false;
 	bool _sendFormFailed = false;
 
diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp
index 69cf7abde..b62987a82 100644
--- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp
+++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/boxes/boost_box.h" // Ui::StartFireworks.
 #include "ui/layers/generic_box.h"
 #include "ui/text/format_values.h"
+#include "window/window_controller.h"
 #include "window/window_session_controller.h"
 
 namespace Payments {
@@ -37,84 +38,98 @@ bool IsCreditsInvoice(not_null<HistoryItem*> item) {
 	return invoice && (invoice->currency == Ui::kCreditsCurrency);
 }
 
+void ProcessCreditsPayment(
+		std::shared_ptr<Main::SessionShow> show,
+		QPointer<QWidget> fireworks,
+		std::shared_ptr<CreditsFormData> form,
+		Fn<void(CheckoutResult)> maybeReturnToBot) {
+	const auto lifetime = std::make_shared<rpl::lifetime>();
+	const auto api = lifetime->make_state<Api::CreditsStatus>(
+		show->session().user());
+	const auto sendBox = [=] {
+		const auto unsuccessful = std::make_shared<bool>(true);
+		const auto box = show->show(Box(
+			Ui::SendCreditsBox,
+			form,
+			[=] {
+				*unsuccessful = false;
+				if (const auto widget = fireworks.data()) {
+					Ui::StartFireworks(widget);
+				}
+				if (maybeReturnToBot) {
+					maybeReturnToBot(CheckoutResult::Paid);
+				}
+			}));
+		box->boxClosing() | rpl::start_with_next([=] {
+			crl::on_main([=] {
+				if ((*unsuccessful) && maybeReturnToBot) {
+					maybeReturnToBot(CheckoutResult::Cancelled);
+				}
+			});
+		}, box->lifetime());
+	};
+	api->request({}, [=](Data::CreditsStatusSlice slice) {
+		show->session().setCredits(slice.balance);
+		const auto creditsNeeded = int64(form->invoice.credits)
+			- int64(slice.balance);
+		if (creditsNeeded <= 0) {
+			sendBox();
+		} else if (show->session().premiumPossible()) {
+			show->show(Box(
+				Settings::SmallBalanceBox,
+				show,
+				creditsNeeded,
+				form->botId,
+				sendBox));
+		} else {
+			show->showToast(
+				tr::lng_credits_purchase_blocked(tr::now));
+			if (maybeReturnToBot) {
+				maybeReturnToBot(CheckoutResult::Failed);
+			}
+		}
+		lifetime->destroy();
+	});
+}
+
+void ProcessCreditsReceipt(
+		not_null<Window::SessionController*> controller,
+		std::shared_ptr<CreditsReceiptData> receipt,
+		Fn<void(CheckoutResult)> maybeReturnToBot) {
+	const auto entry = Data::CreditsHistoryEntry{
+		.id = receipt->id,
+		.title = receipt->title,
+		.description = receipt->description,
+		.date = base::unixtime::parse(receipt->date),
+		.photoId = receipt->photo ? receipt->photo->id : 0,
+		.credits = receipt->credits,
+		.bareMsgId = uint64(),
+		.barePeerId = receipt->peerId.value,
+		.peerType = Data::CreditsHistoryEntry::PeerType::Peer,
+	};
+	controller->uiShow()->show(Box(
+		Settings::ReceiptCreditsBox,
+		controller,
+		nullptr,
+		entry));
+	controller->window().activate();
+}
+
 Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
 		not_null<Window::SessionController*> controller,
 		Fn<void(CheckoutResult)> maybeReturnToBot) {
 	return [=](NonPanelPaymentForm form) {
 		using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
 		using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
-		if (const auto creditsData = std::get_if<CreditsFormDataPtr>(&form)) {
-			const auto form = *creditsData;
-			const auto lifetime = std::make_shared<rpl::lifetime>();
-			const auto api = lifetime->make_state<Api::CreditsStatus>(
-				controller->session().user());
-			const auto sendBox = [=, weak = base::make_weak(controller)] {
-				if (const auto strong = weak.get()) {
-					const auto unsuccessful = std::make_shared<bool>(true);
-					const auto box = controller->uiShow()->show(Box(
-						Ui::SendCreditsBox,
-						form,
-						crl::guard(strong, [=] {
-							*unsuccessful = false;
-							Ui::StartFireworks(strong->content());
-							if (maybeReturnToBot) {
-								maybeReturnToBot(CheckoutResult::Paid);
-							}
-						})));
-					box->boxClosing() | rpl::start_with_next([=] {
-						crl::on_main([=] {
-							if ((*unsuccessful) && maybeReturnToBot) {
-								maybeReturnToBot(CheckoutResult::Cancelled);
-							}
-						});
-					}, box->lifetime());
-				}
-			};
-			const auto weak = base::make_weak(controller);
-			api->request({}, [=](Data::CreditsStatusSlice slice) {
-				if (const auto strong = weak.get()) {
-					strong->session().setCredits(slice.balance);
-					const auto creditsNeeded = int64(form->invoice.credits)
-						- int64(slice.balance);
-					if (creditsNeeded <= 0) {
-						sendBox();
-					} else if (strong->session().premiumPossible()) {
-						strong->uiShow()->show(Box(
-							Settings::SmallBalanceBox,
-							strong,
-							creditsNeeded,
-							form->botId,
-							sendBox));
-					} else {
-						strong->uiShow()->showToast(
-							tr::lng_credits_purchase_blocked(tr::now));
-						if (maybeReturnToBot) {
-							maybeReturnToBot(CheckoutResult::Failed);
-						}
-					}
-				}
-				lifetime->destroy();
-			});
-		}
-		if (const auto r = std::get_if<CreditsReceiptPtr>(&form)) {
-			const auto receipt = *r;
-			const auto entry = Data::CreditsHistoryEntry{
-				.id = receipt->id,
-				.title = receipt->title,
-				.description = receipt->description,
-				.date = base::unixtime::parse(receipt->date),
-				.photoId = receipt->photo ? receipt->photo->id : 0,
-				.credits = receipt->credits,
-				.bareMsgId = uint64(),
-				.barePeerId = receipt->peerId.value,
-				.peerType = Data::CreditsHistoryEntry::PeerType::Peer,
-			};
-			controller->uiShow()->show(Box(
-				Settings::ReceiptCreditsBox,
-				controller,
-				nullptr,
-				entry));
-		}
+		v::match(form, [&](const CreditsFormDataPtr &form) {
+			ProcessCreditsPayment(
+				controller->uiShow(),
+				controller->content().get(),
+				form,
+				maybeReturnToBot);
+		}, [&](const CreditsReceiptPtr &receipt) {
+			ProcessCreditsReceipt(controller, receipt, maybeReturnToBot);
+		}, [](RealFormPresentedNotification) {});
 	};
 }
 
diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.h b/Telegram/SourceFiles/payments/payments_non_panel_process.h
index e8ab9375c..53a31f81c 100644
--- a/Telegram/SourceFiles/payments/payments_non_panel_process.h
+++ b/Telegram/SourceFiles/payments/payments_non_panel_process.h
@@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 class HistoryItem;
 
+namespace Main {
+class SessionShow;
+} // namespace Main
+
 namespace Window {
 class SessionController;
 } // namespace Window
@@ -16,10 +20,23 @@ class SessionController;
 namespace Payments {
 
 enum class CheckoutResult;
+struct CreditsFormData;
+struct CreditsReceiptData;
 struct NonPanelPaymentForm;
 
 [[nodiscard]] bool IsCreditsInvoice(not_null<HistoryItem*> item);
 
+void ProcessCreditsPayment(
+	std::shared_ptr<Main::SessionShow> show,
+	QPointer<QWidget> fireworks,
+	std::shared_ptr<CreditsFormData> form,
+	Fn<void(CheckoutResult)> maybeReturnToBot = nullptr);
+
+void ProcessCreditsReceipt(
+	not_null<Window::SessionController*> controller,
+	std::shared_ptr<CreditsReceiptData> receipt,
+	Fn<void(CheckoutResult)> maybeReturnToBot = nullptr);
+
 Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
 	not_null<Window::SessionController*> controller,
 	Fn<void(Payments::CheckoutResult)> maybeReturnToBot = nullptr);
diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp
index 5088ce62a..e80f82a62 100644
--- a/Telegram/SourceFiles/settings/settings_credits.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits.cpp
@@ -289,7 +289,7 @@ void Credits::setupContent() {
 			Ui::StartFireworks(_parent);
 		}
 	};
-	FillCreditOptions(_controller, content, 0, paid);
+	FillCreditOptions(_controller->uiShow(), content, 0, paid);
 	setupHistory(content);
 
 	Ui::ResizeFitChild(this, content);
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index 282945d9e..1eaef4568 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -225,7 +225,7 @@ void AddViewMediaHandler(
 } // namespace
 
 void FillCreditOptions(
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<Main::SessionShow> show,
 		not_null<Ui::VerticalLayout*> container,
 		int minimumCredits,
 		Fn<void()> paid) {
@@ -302,7 +302,7 @@ void FillCreditOptions(
 			}, button->lifetime());
 			button->setClickedCallback([=] {
 				const auto invoice = Payments::InvoiceCredits{
-					.session = &controller->session(),
+					.session = &show->session(),
 					.randomId = UniqueIdFromOption(option),
 					.credits = option.credits,
 					.product = option.product,
@@ -348,18 +348,18 @@ void FillCreditOptions(
 
 	using ApiOptions = Api::CreditsTopupOptions;
 	const auto apiCredits = content->lifetime().make_state<ApiOptions>(
-		controller->session().user());
+		show->session().user());
 
-	if (controller->session().premiumPossible()) {
+	if (show->session().premiumPossible()) {
 		apiCredits->request(
 		) | rpl::start_with_error_done([=](const QString &error) {
-			controller->showToast(error);
+			show->showToast(error);
 		}, [=] {
 			fill(apiCredits->options());
 		}, content->lifetime());
 	}
 
-	controller->session().premiumPossibleValue(
+	show->session().premiumPossibleValue(
 	) | rpl::start_with_next([=](bool premiumPossible) {
 		if (!premiumPossible) {
 			fill({});
@@ -739,7 +739,7 @@ object_ptr<Ui::RpWidget> PaidMediaThumbnail(
 
 void SmallBalanceBox(
 		not_null<Ui::GenericBox*> box,
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<Main::SessionShow> show,
 		int creditsNeeded,
 		UserId botId,
 		Fn<void()> paid) {
@@ -750,21 +750,13 @@ void SmallBalanceBox(
 		paid();
 	};
 
-	const auto bot = controller->session().data().user(botId).get();
+	const auto bot = show->session().data().user(botId).get();
 
 	const auto content = [&]() -> Ui::Premium::TopBarAbstract* {
-		const auto weak = base::make_weak(controller);
-		const auto clickContextOther = [=] {
-			return QVariant::fromValue(ClickHandlerContext{
-				.sessionWindow = weak,
-				.botStartAutoSubmit = true,
-			});
-		};
 		return box->setPinnedToTopContent(object_ptr<Ui::Premium::TopBar>(
 			box,
 			st::creditsLowBalancePremiumCover,
 			Ui::Premium::TopBarDescriptor{
-				.clickContextOther = clickContextOther,
 				.title = tr::lng_credits_small_balance_title(
 					lt_count,
 					rpl::single(creditsNeeded) | tr::to_count()),
@@ -777,7 +769,7 @@ void SmallBalanceBox(
 			}));
 	}();
 
-	FillCreditOptions(controller, box->verticalLayout(), creditsNeeded, done);
+	FillCreditOptions(show, box->verticalLayout(), creditsNeeded, done);
 
 	content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight);
 	content->setMinimumHeight(st::infoLayerTopBarHeight);
@@ -796,12 +788,12 @@ void SmallBalanceBox(
 	{
 		const auto balance = AddBalanceWidget(
 			content,
-			controller->session().creditsValue(),
+			show->session().creditsValue(),
 			true);
 		const auto api = balance->lifetime().make_state<Api::CreditsStatus>(
-			controller->session().user());
+			show->session().user());
 		api->request({}, [=](Data::CreditsStatusSlice slice) {
-			controller->session().setCredits(slice.balance);
+			show->session().setCredits(slice.balance);
 		});
 		rpl::combine(
 			balance->sizeValue(),
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h
index 406bfabca..47d5323c7 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.h
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h
@@ -16,6 +16,10 @@ namespace Data {
 struct CreditsHistoryEntry;
 } // namespace Data
 
+namespace Main {
+class SessionShow;
+} // namespace Main
+
 namespace Window {
 class SessionController;
 } // namespace Window
@@ -29,7 +33,7 @@ class VerticalLayout;
 namespace Settings {
 
 void FillCreditOptions(
-	not_null<Window::SessionController*> controller,
+	std::shared_ptr<Main::SessionShow> show,
 	not_null<Ui::VerticalLayout*> container,
 	int minCredits,
 	Fn<void()> paid);
@@ -77,7 +81,7 @@ void ShowRefundInfoBox(
 
 void SmallBalanceBox(
 	not_null<Ui::GenericBox*> box,
-	not_null<Window::SessionController*> controller,
+	std::shared_ptr<Main::SessionShow> show,
 	int creditsNeeded,
 	UserId botId,
 	Fn<void()> paid);
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index cb922a4ff..ae27e4938 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -1344,8 +1344,10 @@ void Panel::invoiceClosed(const QString &slug, const QString &status) {
 		{ u"slug"_q, slug },
 		{ u"status"_q, status },
 	});
-	_widget->showAndActivate();
-	_hiddenForPayment = false;
+	if (_hiddenForPayment) {
+		_hiddenForPayment = false;
+		_widget->showAndActivate();
+	}
 }
 
 void Panel::hideForPayment() {

From 4d647e64b79ffab9d2d7708c84b86cfe6036375f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 Jul 2024 17:14:47 +0200
Subject: [PATCH 067/163] Fix chat links with '-' in slug.

Fixes https://bugs.telegram.org/c/41902.
---
 Telegram/SourceFiles/core/local_url_handlers.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 9f4860da3..675bb5edc 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -1268,7 +1268,7 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
 			ResolveBoost,
 		},
 		{
-			u"^message/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q,
+			u"^message/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
 			ResolveChatLink
 		},
 		{

From a847969e9c88bcd37e9ea31ed881980531003544 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 18 Jul 2024 17:59:12 +0200
Subject: [PATCH 068/163] Attempt to fix bad input field layout on Qt 6.

---
 Telegram/SourceFiles/chat_helpers/message_field.cpp | 2 +-
 Telegram/lib_ui                                     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp
index 5d8be6144..8ee5170fb 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.cpp
+++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp
@@ -525,7 +525,7 @@ void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
 		st::historySendSize.height() - 2 * st::historySendPadding);
 	field->setMaxHeight(st::historyComposeFieldMaxHeight);
 
-	field->document()->setDocumentMargin(4.);
+	field->setDocumentMargin(4.);
 	field->setAdditionalMargin(style::ConvertScale(4) - 4);
 }
 
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 58329bfe1..007e3a519 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 58329bfe19c031ec6065a3a098743bb3d7732a64
+Subproject commit 007e3a519ffbad0bd889de6cb4b19f34959afbce

From a2fa1a52e233b15c9e6095a0e266243ed029fa27 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 19 Jul 2024 11:53:49 +0200
Subject: [PATCH 069/163] Pass VideoCaptureOptions on Linux.

---
 Telegram/ThirdParty/tgcalls | 2 +-
 Telegram/lib_webrtc         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index 2dc886d05..ce592ecab 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit 2dc886d05e4b7848dd7964b8e89bafdce905ba4f
+Subproject commit ce592ecab26770fecca683a05f4611d1c00113ef
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index 839d9f0aa..5d44a8acc 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit 839d9f0aade5e20a03d36caceec22ed6c8c7a63f
+Subproject commit 5d44a8acc341d5f45c4150f07929484d15b9c5ba

From 9461095c88c4f3e3188427ba4e6054eafdb29041 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 19 Jul 2024 13:00:51 +0300
Subject: [PATCH 070/163] Fix build with GCC.

---
 Telegram/SourceFiles/api/api_sending.cpp      |  6 -----
 .../inline_bots/bot_attach_web_view.cpp       |  2 +-
 Telegram/build/docker/centos_env/Dockerfile   | 22 +++++++++++++------
 Telegram/cmake/lib_tgcalls.cmake              |  1 +
 cmake                                         |  2 +-
 5 files changed, 18 insertions(+), 15 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index bf4d472fe..c05a42e95 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -80,18 +80,12 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
 		flags |= MessageFlag::HasReplyInfo;
 		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
 	}
-	const auto anonymousPost = peer->amAnonymous();
 	const auto silentPost = ShouldSendSilent(peer, action.options);
 	InnerFillMessagePostFlags(action.options, peer, flags);
 	if (silentPost) {
 		sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
 	}
 	const auto sendAs = action.options.sendAs;
-	const auto messageFromId = sendAs
-		? sendAs->id
-		: anonymousPost
-		? 0
-		: session->userPeerId();
 	if (sendAs) {
 		sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
 	}
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index c8e88aed7..b701fe21a 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -1684,7 +1684,7 @@ std::shared_ptr<Main::SessionShow> AttachWebView::uiShow() {
 			using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
 			using ObjectBox = object_ptr<Ui::BoxContent>;
 			const auto panel = _that ? _that->_panel.get() : nullptr;
-			if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
+			if (v::is<UniqueLayer>(layer)) {
 				Unexpected("Layers in AttachWebView are not implemented.");
 			} else if (auto box = std::get_if<ObjectBox>(&layer)) {
 				if (panel) {
diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index d60e6d403..0091881ed 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -1,5 +1,6 @@
 {%- set GIT = "https://github.com" -%}
 {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%}
+{%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git config -f .gitmodules submodule.m4.shallow true && git submodule init && git submodule update" -%}
 {%- set QT = "6.7.2" -%}
 {%- set QT_TAG = "v" ~ QT -%}
 {%- set CFLAGS_DEBUG = "-g -pipe -fPIC -fstack-protector-all -fstack-clash-protection -fcf-protection -D_GLIBCXX_ASSERTIONS" -%}
@@ -313,8 +314,9 @@ RUN git clone -b libxcb-1.16 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb.git \
 	&& rm -rf libxcb
 
 FROM builder AS xcb-wm
-RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-wm.git \
+RUN git clone -b xcb-util-wm-0.4.2 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-wm.git \
 	&& cd libxcb-wm \
+	&& {{ GIT_UPDATE_M4 }} \
 	&& ./autogen.sh --enable-static \
 	&& make -j$(nproc) \
 	&& make DESTDIR="{{ LibrariesPath }}/xcb-wm-cache" install \
@@ -322,8 +324,9 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{
 	&& rm -rf libxcb-wm
 
 FROM builder AS xcb-util
-RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-util.git \
+RUN git clone -b xcb-util-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-util.git \
 	&& cd libxcb-util \
+	&& {{ GIT_UPDATE_M4 }} \
 	&& ./autogen.sh --enable-static \
 	&& make -j$(nproc) \
 	&& make DESTDIR="{{ LibrariesPath }}/xcb-util-cache" install \
@@ -333,8 +336,9 @@ RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GI
 FROM builder AS xcb-image
 COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache /
 
-RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-image.git \
+RUN git clone -b xcb-util-image-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-image.git \
 	&& cd libxcb-image \
+	&& {{ GIT_UPDATE_M4 }} \
 	&& ./autogen.sh --enable-static \
 	&& make -j$(nproc) \
 	&& make DESTDIR="{{ LibrariesPath }}/xcb-image-cache" install \
@@ -342,8 +346,9 @@ RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules
 	&& rm -rf libxcb-image
 
 FROM builder AS xcb-keysyms
-RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \
+RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \
 	&& cd libxcb-keysyms \
+	&& {{ GIT_UPDATE_M4 }} \
 	&& ./autogen.sh --enable-static \
 	&& make -j$(nproc) \
 	&& make DESTDIR="{{ LibrariesPath }}/xcb-keysyms-cache" install \
@@ -351,8 +356,9 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodul
 	&& rm -rf libxcb-keysyms
 
 FROM builder AS xcb-render-util
-RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \
+RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \
 	&& cd libxcb-render-util \
+	&& {{ GIT_UPDATE_M4 }} \
 	&& ./autogen.sh --enable-static \
 	&& make -j$(nproc) \
 	&& make DESTDIR="{{ LibrariesPath }}/xcb-render-util-cache" install \
@@ -364,8 +370,9 @@ COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache /
 COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache /
 COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache /
 
-RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \
+RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \
 	&& cd libxcb-cursor \
+	&& {{ GIT_UPDATE_M4 }} \
 	&& ./autogen.sh --enable-static \
 	&& make -j$(nproc) \
 	&& make DESTDIR="{{ LibrariesPath }}/xcb-cursor-cache" install \
@@ -613,12 +620,13 @@ RUN git clone -b n6.1.1 --depth=1 {{ GIT }}/FFmpeg/FFmpeg.git \
 	&& rm -rf ffmpeg
 
 FROM builder AS pipewire
-RUN git clone -b 0.3.25 --depth=1 {{ GIT }}/PipeWire/pipewire.git \
+RUN git clone -b 0.3.62 --depth=1 {{ GIT }}/PipeWire/pipewire.git \
 	&& cd pipewire \
 	&& meson build \
 		--buildtype=plain \
 		-Dtests=disabled \
 		-Dexamples=disabled \
+		-Dsession-managers=media-session \
 		-Dspa-plugins=disabled \
 	&& meson compile -C build \
 	&& DESTDIR="{{ LibrariesPath }}/pipewire-cache" meson install -C build \
diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake
index 018a7cfb0..44539b54b 100644
--- a/Telegram/cmake/lib_tgcalls.cmake
+++ b/Telegram/cmake/lib_tgcalls.cmake
@@ -259,6 +259,7 @@ PRIVATE
     -Wno-ambiguous-reversed-operator
     -Wno-deprecated-declarations
     -Wno-unqualified-std-cast-call
+    -Wno-unused-function
 )
 
 remove_target_sources(lib_tgcalls ${tgcalls_loc}
diff --git a/cmake b/cmake
index 3fe312335..b3871c4ef 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 3fe312335453073028a7bc5e7b3d65f42011e34a
+Subproject commit b3871c4efde37827263053d95e6f16fe34d4952e

From fd982b90dbb43b1dd623a67e2e9d9adccf48cc81 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 19 Jul 2024 14:06:30 +0200
Subject: [PATCH 071/163] Implement separate instances for web apps.

---
 Telegram/SourceFiles/api/api_bot.cpp          |   21 +-
 Telegram/SourceFiles/core/application.cpp     |    6 +-
 .../SourceFiles/core/click_handler_types.cpp  |   10 +-
 .../SourceFiles/core/click_handler_types.h    |    6 +-
 .../SourceFiles/core/local_url_handlers.cpp   |   11 +-
 .../SourceFiles/history/history_widget.cpp    |    9 +-
 .../inline_bots/bot_attach_web_view.cpp       | 1713 ++++++++---------
 .../inline_bots/bot_attach_web_view.h         |  388 ++--
 .../inline_bots/inline_results_inner.cpp      |   12 +-
 Telegram/SourceFiles/mainwindow.cpp           |    2 +-
 .../ui/chat/attach/attach_bot_webview.cpp     |   21 +
 .../window/window_main_menu_helpers.cpp       |    9 +-
 .../window/window_session_controller.cpp      |   69 +-
 .../window_session_controller_link_info.h     |    9 +-
 14 files changed, 1202 insertions(+), 1084 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index 929051261..7514121eb 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -488,20 +488,23 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
 
 	case ButtonType::WebView: {
 		if (const auto bot = item->getMessageBot()) {
-			bot->session().attachWebView().request(
-				controller,
-				Api::SendAction(bot->owner().history(bot)),
-				bot,
-				{ .text = button->text, .url = button->data });
+			bot->session().attachWebView().open({
+				.bot = bot,
+				.context = { .controller = controller },
+				.button = { .text = button->text, .url = button->data },
+				.source = InlineBots::WebViewSourceButton{ .simple = false },
+			});
 		}
 	} break;
 
 	case ButtonType::SimpleWebView: {
 		if (const auto bot = item->getMessageBot()) {
-			bot->session().attachWebView().requestSimple(
-				controller,
-				bot,
-				{ .text = button->text, .url = button->data });
+			bot->session().attachWebView().open({
+				.bot = bot,
+				.context = { .controller = controller },
+				.button = {.text = button->text, .url = button->data },
+				.source = InlineBots::WebViewSourceButton{ .simple = true },
+			});
 		}
 	} break;
 	}
diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index 79dfd0977..19f488b0b 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -231,7 +231,11 @@ Application::~Application() {
 	// For example Domain::removeRedundantAccounts() is called from
 	// Domain::finish() and there is a violation on Ensures(started()).
 	Payments::CheckoutProcess::ClearAll();
-	InlineBots::AttachWebView::ClearAll();
+	for (const auto &[index, account] : _domain->accounts()) {
+		if (account->sessionExists()) {
+			account->session().attachWebView().closeAll();
+		}
+	}
 	_iv->closeAll();
 
 	_domain->finish();
diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp
index eadb16181..915b12f4f 100644
--- a/Telegram/SourceFiles/core/click_handler_types.cpp
+++ b/Telegram/SourceFiles/core/click_handler_types.cpp
@@ -191,11 +191,13 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const {
 	const auto title = game->title;
 	const auto itemId = my.itemId;
 	const auto openGame = [=] {
-		bot->session().attachWebView().showGame({
+		bot->session().attachWebView().open({
 			.bot = bot,
-			.context = itemId,
-			.url = url,
-			.title = title,
+			.button = {.url = url.toUtf8() },
+			.source = InlineBots::WebViewSourceGame{
+				.messageId = itemId,
+				.title = title,
+			},
 		});
 	};
 	if (_bot->isVerified()
diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h
index a63594195..b3aa0bae0 100644
--- a/Telegram/SourceFiles/core/click_handler_types.h
+++ b/Telegram/SourceFiles/core/click_handler_types.h
@@ -21,6 +21,10 @@ namespace Ui {
 class Show;
 } // namespace Ui
 
+namespace InlineBots {
+struct WebViewContext;
+} // namespace InlineBots
+
 namespace Main {
 class Session;
 } // namespace Main
@@ -38,10 +42,10 @@ class SessionController;
 class PeerData;
 struct ClickHandlerContext {
 	FullMsgId itemId;
-	QString attachBotWebviewUrl;
 	// Is filled from sections.
 	Fn<HistoryView::ElementDelegate*()> elementDelegate;
 	base::weak_ptr<Window::SessionController> sessionWindow;
+	std::shared_ptr<InlineBots::WebViewContext> botWebviewContext;
 	std::shared_ptr<Ui::Show> show;
 	bool mayShowConfirmation = false;
 	bool skipBotAutoLogin = false;
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 675bb5edc..223cec043 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -573,8 +573,11 @@ bool ResolveUsernameOrPhone(
 			: (appname.isEmpty() && params.contains(u"startapp"_q))
 			? params.value(u"startapp"_q)
 			: std::optional<QString>()),
-		.attachBotMenuOpen = (appname.isEmpty()
+		.attachBotMainOpen = (appname.isEmpty()
 			&& params.contains(u"startapp"_q)),
+		.attachBotMainCompact = (appname.isEmpty()
+			&& params.contains(u"startapp"_q)
+			&& (params.value(u"mode"_q) == u"compact"_q)),
 		.attachBotChooseTypes = InlineBots::ParseChooseTypes(
 			params.value(u"choose"_q)),
 		.voicechatHash = (params.contains(u"livestream"_q)
@@ -585,7 +588,7 @@ bool ResolveUsernameOrPhone(
 			? std::make_optional(params.value(u"voicechat"_q))
 			: std::nullopt),
 		.clickFromMessageId = myContext.itemId,
-		.clickFromAttachBotWebviewUrl = myContext.attachBotWebviewUrl,
+		.clickFromBotWebviewContext = myContext.botWebviewContext,
 	});
 	return true;
 }
@@ -626,7 +629,7 @@ bool ResolvePrivatePost(
 			}
 			: Window::RepliesByLinkInfo{ v::null },
 		.clickFromMessageId = my.itemId,
-		.clickFromAttachBotWebviewUrl = my.attachBotWebviewUrl,
+		.clickFromBotWebviewContext = my.botWebviewContext,
 	});
 	controller->window().activate();
 	return true;
@@ -1178,7 +1181,7 @@ bool ResolveChatLink(
 	controller->showPeerByLink(Window::PeerByLinkInfo{
 		.chatLinkSlug = match->captured(1),
 		.clickFromMessageId = myContext.itemId,
-		.clickFromAttachBotWebviewUrl = myContext.attachBotWebviewUrl,
+		.clickFromBotWebviewContext = myContext.botWebviewContext,
 	});
 	return true;
 }
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index 0a3c5a064..789390bbf 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -4940,7 +4940,14 @@ bool HistoryWidget::updateCmdStartShown() {
 			const auto user = _peer ? _peer->asUser() : nullptr;
 			const auto bot = (user && user->isBot()) ? user : nullptr;
 			if (bot && !bot->botInfo->botMenuButtonUrl.isEmpty()) {
-				session().attachWebView().requestMenu(controller(), bot);
+				session().attachWebView().open({
+					.bot = bot,
+					.context = { .controller = controller() },
+					.button = {
+						.url = bot->botInfo->botMenuButtonUrl.toUtf8(),
+					},
+					.source = InlineBots::WebViewSourceBotMenu(),
+				});
 			} else if (!_fieldAutocomplete->isHidden()) {
 				_fieldAutocomplete->hideAnimated();
 			} else {
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index b701fe21a..8e993f4f7 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -170,8 +170,24 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
 	};
 }
 
+[[nodiscard]] Window::SessionController *WindowForThread(
+		base::weak_ptr<Window::SessionController> weak,
+		not_null<Data::Thread*> thread) {
+	if (const auto separate = Core::App().separateWindowFor(thread)) {
+		return separate->sessionController();
+	}
+	const auto strong = weak.get();
+	if (strong && strong->windowId().hasChatsList()) {
+		strong->showThread(thread);
+		return strong;
+	}
+	const auto window = Core::App().ensureSeparateWindowFor(thread);
+	return window ? window->sessionController() : nullptr;
+}
+
 void ShowChooseBox(
-		not_null<Window::SessionController*> controller,
+		std::shared_ptr<Ui::Show> show,
+		not_null<Main::Session*> session,
 		PeerTypes types,
 		Fn<void(not_null<Data::Thread*>)> callback,
 		rpl::producer<QString> titleOverride = nullptr) {
@@ -206,26 +222,36 @@ void ShowChooseBox(
 			box->closeBox();
 		});
 	};
-	*weak = controller->show(Box<PeerListBox>(
+	*weak = show->show(Box<PeerListBox>(
 		std::make_unique<ChooseRecipientBoxController>(
-			&controller->session(),
+			session,
 			std::move(done),
 			std::move(filter)),
 		std::move(initBox)));
 }
 
-[[nodiscard]] base::flat_set<not_null<AttachWebView*>> &ActiveWebViews() {
-	static auto result = base::flat_set<not_null<AttachWebView*>>();
-	return result;
+void ShowChooseBox(
+		not_null<Window::SessionController*> controller,
+		PeerTypes types,
+		Fn<void(not_null<Data::Thread*>)> callback,
+		rpl::producer<QString> titleOverride = nullptr) {
+	ShowChooseBox(
+		controller->uiShow(),
+		&controller->session(),
+		types,
+		std::move(callback),
+		std::move(titleOverride));
 }
 
-void FillDisclaimerBox(not_null<Ui::GenericBox*> box, Fn<void()> done) {
+void FillDisclaimerBox(
+		not_null<Ui::GenericBox*> box,
+		Fn<void(bool accepted)> done) {
 	const auto updateCheck = std::make_shared<Fn<void()>>();
 	const auto validateCheck = std::make_shared<Fn<bool()>>();
 
 	const auto callback = [=](Fn<void()> close) {
 		if (validateCheck && (*validateCheck)()) {
-			done();
+			done(true);
 			close();
 		}
 	};
@@ -236,6 +262,7 @@ void FillDisclaimerBox(not_null<Ui::GenericBox*> box, Fn<void()> done) {
 			tr::now,
 			Ui::Text::RichLangValue),
 		.confirmed = callback,
+		.cancelled = [=](Fn<void()> close) { done(false); close(); },
 		.confirmText = tr::lng_box_ok(),
 		.labelPadding = QMargins(padding.left(), 0, padding.right(), 0),
 		.title = tr::lng_mini_apps_disclaimer_title(),
@@ -291,10 +318,43 @@ void FillDisclaimerBox(not_null<Ui::GenericBox*> box, Fn<void()> done) {
 	};
 }
 
+WebViewContext ResolveContext(
+		not_null<UserData*> bot,
+		WebViewContext context) {
+	if (!context.dialogsEntryState.key) {
+		if (const auto strong = context.controller.get()) {
+			context.dialogsEntryState = strong->currentDialogsEntryState();
+		}
+	}
+	if (!context.action) {
+		const auto &state = context.dialogsEntryState;
+		if (const auto thread = state.key.thread()) {
+			context.action = Api::SendAction(thread);
+			context.action->replyTo = state.currentReplyTo;
+		} else {
+			context.action = Api::SendAction(bot->owner().history(bot));
+		}
+	}
+	if (!context.dialogsEntryState.key) {
+		using namespace Dialogs;
+		using Section = EntryState::Section;
+		const auto history = context.action->history;
+		const auto topicId = context.action->replyTo.topicRootId;
+		const auto topic = history->peer->forumTopicFor(topicId);
+		context.dialogsEntryState = EntryState{
+			.key = (topic ? Key{ topic } : Key{ history }),
+			.section = (topic ? Section::Replies : Section::History),
+			.currentReplyTo = context.action->replyTo,
+		};
+	}
+	return context;
+}
+
 class BotAction final : public Ui::Menu::ItemBase {
 public:
 	BotAction(
 		not_null<Ui::RpWidget*> parent,
+		std::shared_ptr<Ui::Show> show,
 		const style::Menu &st,
 		const AttachWebViewBot &bot,
 		Fn<void()> callback);
@@ -317,6 +377,7 @@ private:
 	void prepare();
 	void paint(Painter &p);
 
+	const std::shared_ptr<Ui::Show> _show;
 	const not_null<QAction*> _dummyAction;
 	const style::Menu &_st;
 	const AttachWebViewBot _bot;
@@ -334,10 +395,12 @@ private:
 
 BotAction::BotAction(
 	not_null<Ui::RpWidget*> parent,
+	std::shared_ptr<Ui::Show> show,
 	const style::Menu &st,
 	const AttachWebViewBot &bot,
 	Fn<void()> callback)
 : ItemBase(parent, st)
+, _show(std::move(show))
 , _dummyAction(new QAction(parent))
 , _st(st)
 , _bot(bot)
@@ -409,7 +472,8 @@ void BotAction::contextMenuEvent(QContextMenuEvent *e) {
 		this,
 		st::popupMenuWithIcons);
 	_menu->addAction(tr::lng_bot_remove_from_menu(tr::now), [=] {
-		_bot.user->session().attachWebView().removeFromMenu(_bot.user);
+		const auto bot = _bot.user;
+		bot->session().attachWebView().removeFromMenu(_show, bot);
 	}, &st::menuIconDelete);
 
 	QObject::connect(_menu, &QObject::destroyed, [=] {
@@ -449,6 +513,8 @@ void BotAction::handleKeyPress(not_null<QKeyEvent*> e) {
 
 } // namespace
 
+base::weak_ptr<WebViewInstance> WebViewInstance::PendingActivation;
+
 MenuBotIcon::MenuBotIcon(
 	QWidget *parent,
 	std::shared_ptr<Data::DocumentMedia> media)
@@ -528,82 +594,514 @@ PeerTypes ParseChooseTypes(QStringView choose) {
 	return result;
 }
 
-struct AttachWebView::Context {
-	base::weak_ptr<Window::SessionController> controller;
-	Dialogs::EntryState dialogsEntryState;
-	Api::SendAction action;
-	bool fromSwitch = false;
-	bool fromMainMenu = false;
-	bool fromBotApp = false;
-};
-
-AttachWebView::AttachWebView(not_null<Main::Session*> session)
-: _session(session)
-, _refreshTimer([=] { requestBots(); }) {
-	_refreshTimer.callEach(kRefreshBotsTimeout);
-}
-
-AttachWebView::~AttachWebView() {
-	ActiveWebViews().remove(this);
-}
-
-void AttachWebView::request(
-		not_null<Window::SessionController*> controller,
-		const Api::SendAction &action,
-		const QString &botUsername,
-		const QString &startCommand) {
-	if (botUsername.isEmpty()) {
-		return;
-	}
-	const auto username = _bot ? _bot->username() : _botUsername;
-	const auto context = LookupContext(controller, action);
-	if (IsSame(_context, context)
-		&& username.toLower() == botUsername.toLower()
-		&& _startCommand == startCommand) {
-		if (_panel) {
-			_panel->requestActivate();
-		}
-		return;
-	}
-	cancel();
-
-	_context = std::make_unique<Context>(context);
-	_botUsername = botUsername;
-	_startCommand = startCommand;
+WebViewInstance::WebViewInstance(WebViewDescriptor &&descriptor)
+: _parentShow(descriptor.parentShow
+	? std::move(descriptor.parentShow)
+	: descriptor.context.controller
+	? descriptor.context.controller.get()->uiShow()
+	: nullptr)
+, _session(&descriptor.bot->session())
+, _bot(descriptor.bot)
+, _context(ResolveContext(_bot, std::move(descriptor.context)))
+, _button(std::move(descriptor.button))
+, _source(std::move(descriptor.source)) {
 	resolve();
 }
 
-Webview::ThemeParams AttachWebView::botThemeParams() {
+WebViewInstance::~WebViewInstance() {
+	_session->api().request(base::take(_requestId)).cancel();
+	_session->api().request(base::take(_prolongId)).cancel();
+	base::take(_panel);
+}
+
+Main::Session &WebViewInstance::session() const {
+	return *_session;
+}
+
+not_null<UserData*> WebViewInstance::bot() const {
+	return _bot;
+}
+
+WebViewSource WebViewInstance::source() const {
+	return _source;
+}
+
+void WebViewInstance::activate() {
+	if (_panel) {
+		_panel->requestActivate();
+	} else {
+		PendingActivation = this;
+	}
+}
+
+void WebViewInstance::resolve() {
+	v::match(_source, [&](WebViewSourceButton data) {
+		confirmOpen([=] {
+			if (data.simple) {
+				requestSimple();
+			} else {
+				requestButton();
+			}
+		});
+	}, [&](WebViewSourceSwitch) {
+		confirmOpen([=] {
+			requestSimple();
+		});
+	}, [&](WebViewSourceLinkApp data) {
+		resolveApp(data.appname, data.token, !_context.maySkipConfirmation);
+	}, [&](WebViewSourceLinkBotProfile) {
+		requestWithMenuAdd();
+	}, [&](WebViewSourceLinkAttachMenu data) {
+		requestWithMenuAdd();
+	}, [&](WebViewSourceMainMenu) {
+		requestWithMainMenuDisclaimer();
+	}, [&](WebViewSourceAttachMenu) {
+		requestWithMenuAdd();
+	}, [&](WebViewSourceBotMenu) {
+		if (!openAppFromBotMenuLink()) {
+			confirmOpen([=] {
+				requestButton();
+			});
+		}
+	}, [&](WebViewSourceGame game) {
+		showGame();
+	}, [&](WebViewSourceBotProfile) {
+		requestWithMenuAdd();
+	});
+}
+
+bool WebViewInstance::openAppFromBotMenuLink() {
+	const auto url = QString::fromUtf8(_button.url);
+	const auto local = Core::TryConvertUrlToLocal(url);
+	const auto prefix = u"tg://resolve?"_q;
+	if (!local.startsWith(prefix)) {
+		return false;
+	}
+	const auto params = qthelp::url_parse_params(
+		local.mid(prefix.size()),
+		qthelp::UrlParamNameTransform::ToLower);
+	const auto domainParam = params.value(u"domain"_q);
+	const auto appnameParam = params.value(u"appname"_q);
+	const auto webChannelPreviewLink = (domainParam == u"s"_q)
+		&& !appnameParam.isEmpty();
+	const auto appname = webChannelPreviewLink ? QString() : appnameParam;
+	if (appname.isEmpty()) {
+		return false;
+	}
+	resolveApp(appname, params.value(u"startapp"_q), true);
+	return true;
+}
+
+void WebViewInstance::resolveApp(
+		const QString &appname,
+		const QString &startparam,
+		bool forceConfirmation) {
+	const auto already = _session->data().findBotApp(_bot->id, appname);
+	_requestId = _session->api().request(MTPmessages_GetBotApp(
+		MTP_inputBotAppShortName(
+			_bot->inputUser,
+			MTP_string(appname)),
+		MTP_long(already ? already->hash : 0)
+	)).done([=](const MTPmessages_BotApp &result) {
+		_requestId = 0;
+		const auto &data = result.data();
+		const auto received = _session->data().processBotApp(
+			_bot->id,
+			data.vapp());
+		_app = received ? received : already;
+		_appStartParam = startparam;
+		if (!_app) {
+			_parentShow->showToast(tr::lng_username_app_not_found(tr::now));
+			close();
+			return;
+		}
+		const auto confirm = data.is_inactive() || forceConfirmation;
+		const auto writeAccess = result.data().is_request_write_access();
+
+		// Check if this app can be added to main menu.
+		// On fail it'll still be opened.
+		using Result = AttachWebView::AddToMenuResult;
+		const auto done = crl::guard(this, [=](Result value, auto) {
+			if (value == Result::Cancelled) {
+				close();
+			} else if (value != Result::Unsupported) {
+				requestApp(true);
+			} else if (confirm) {
+				confirmAppOpen(writeAccess, [=](bool allowWrite) {
+					requestApp(allowWrite);
+				});
+			} else {
+				requestApp(false);
+			}
+		});
+		_session->attachWebView().requestAddToMenu(_bot, done);
+	}).fail([=] {
+		_parentShow->showToast(tr::lng_username_app_not_found(tr::now));
+		close();
+	}).send();
+}
+
+void WebViewInstance::confirmOpen(Fn<void()> done) {
+	if (_bot->isVerified()
+		|| _session->local().isBotTrustedOpenWebView(_bot->id)) {
+		done();
+		return;
+	}
+	const auto callback = [=](Fn<void()> close) {
+		_session->local().markBotTrustedOpenWebView(_bot->id);
+		close();
+		done();
+	};
+	_parentShow->show(Ui::MakeConfirmBox({
+		.text = tr::lng_allow_bot_webview(
+			tr::now,
+			lt_bot_name,
+			Ui::Text::Bold(_bot->name()),
+			Ui::Text::RichLangValue),
+		.confirmed = crl::guard(this, callback),
+		.cancelled = crl::guard(this, [=] { botClose(); }),
+		.confirmText = tr::lng_box_ok(),
+	}));
+}
+
+void WebViewInstance::confirmAppOpen(
+		bool writeAccess,
+		Fn<void(bool allowWrite)> done) {
+	_parentShow->show(Box([=](not_null<Ui::GenericBox*> box) {
+		const auto allowed = std::make_shared<Ui::Checkbox*>();
+		const auto callback = [=](Fn<void()> close) {
+			done((*allowed) && (*allowed)->checked());
+			close();
+		};
+		Ui::ConfirmBox(box, {
+			tr::lng_allow_bot_webview(
+				tr::now,
+				lt_bot_name,
+				Ui::Text::Bold(_bot->name()),
+				Ui::Text::RichLangValue),
+			crl::guard(this, callback),
+			crl::guard(this, [=] { botClose(); }),
+		});
+		if (writeAccess) {
+			(*allowed) = box->addRow(
+				object_ptr<Ui::Checkbox>(
+					box,
+					tr::lng_url_auth_allow_messages(
+						tr::now,
+						lt_bot,
+						Ui::Text::Bold(_bot->name()),
+						Ui::Text::WithEntities),
+					true,
+					st::urlAuthCheckbox),
+				style::margins(
+					st::boxRowPadding.left(),
+					st::boxPhotoCaptionSkip,
+					st::boxRowPadding.right(),
+					st::boxPhotoCaptionSkip));
+			(*allowed)->setAllowTextLines();
+		}
+	}));
+}
+
+void WebViewInstance::requestButton() {
+	Expects(_context.action.has_value());
+
+	const auto &action = *_context.action;
+	using Flag = MTPmessages_RequestWebView::Flag;
+	_requestId = _session->api().request(MTPmessages_RequestWebView(
+		MTP_flags(Flag::f_theme_params
+			| (_button.url.isEmpty() ? Flag(0) : Flag::f_url)
+			| (_button.startCommand.isEmpty()
+				? Flag(0)
+				: Flag::f_start_param)
+			| (v::is<WebViewSourceBotMenu>(_source)
+				? Flag::f_from_bot_menu
+				: 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_bytes(_button.url),
+		MTP_string(_button.startCommand),
+		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
+		MTP_string("tdesktop"),
+		action.mtpReplyTo(),
+		(action.options.sendAs
+			? action.options.sendAs->input
+			: MTP_inputPeerEmpty())
+	)).done([=](const MTPWebViewResult &result) {
+		const auto &data = result.data();
+		show(qs(data.vurl()), data.vquery_id().value_or_empty());
+	}).fail([=](const MTP::Error &error) {
+		_parentShow->showToast(error.type());
+		if (error.type() == u"BOT_INVALID"_q) {
+			_session->attachWebView().requestBots();
+		}
+		close();
+	}).send();
+}
+
+void WebViewInstance::requestSimple() {
+	using Flag = MTPmessages_RequestSimpleWebView::Flag;
+	_requestId = _session->api().request(MTPmessages_RequestSimpleWebView(
+		MTP_flags(Flag::f_theme_params
+			| (v::is<WebViewSourceSwitch>(_source)
+				? (Flag::f_url | Flag::f_from_switch_webview)
+				: v::is<WebViewSourceMainMenu>(_source)
+				? (Flag::f_from_side_menu
+					| (_button.startCommand.isEmpty() // from LinkMainMenu
+						? Flag()
+						: Flag::f_start_param))
+				: Flag::f_url)),
+		_bot->inputUser,
+		MTP_bytes(_button.url),
+		MTP_string(_button.startCommand),
+		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
+		MTP_string("tdesktop")
+	)).done([=](const MTPWebViewResult &result) {
+		show(qs(result.data().vurl()));
+	}).fail([=](const MTP::Error &error) {
+		_parentShow->showToast(error.type());
+		close();
+	}).send();
+}
+
+void WebViewInstance::requestApp(bool allowWrite) {
+	Expects(_app != nullptr);
+	Expects(_context.action.has_value());
+
+	using Flag = MTPmessages_RequestAppWebView::Flag;
+	const auto app = _app;
+	const auto flags = Flag::f_theme_params
+		| (_appStartParam.isEmpty() ? Flag(0) : Flag::f_start_param)
+		| (allowWrite ? Flag::f_write_allowed : Flag(0));
+	_requestId = _session->api().request(MTPmessages_RequestAppWebView(
+		MTP_flags(flags),
+		_context.action->history->peer->input,
+		MTP_inputBotAppID(MTP_long(app->id), MTP_long(app->accessHash)),
+		MTP_string(_appStartParam),
+		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
+		MTP_string("tdesktop")
+	)).done([=](const MTPWebViewResult &result) {
+		_requestId = 0;
+		show(qs(result.data().vurl()));
+	}).fail([=](const MTP::Error &error) {
+		_requestId = 0;
+		if (error.type() == u"BOT_INVALID"_q) {
+			_session->attachWebView().requestBots();
+		}
+		close();
+	}).send();
+}
+
+void WebViewInstance::requestWithMainMenuDisclaimer() {
+	using Result = AttachWebView::AddToMenuResult;
+	const auto done = crl::guard(this, [=](Result value, auto) {
+		if (value == Result::Cancelled) {
+			close();
+		} else if (value == Result::Unsupported) {
+			_parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));
+			close();
+		} else {
+			requestSimple();
+		}
+	});
+	_session->attachWebView().acceptMainMenuDisclaimer(
+		_parentShow,
+		_bot,
+		done);
+}
+
+void WebViewInstance::requestWithMenuAdd() {
+	using Result = AttachWebView::AddToMenuResult;
+	const auto done = crl::guard(this, [=](Result value, PeerTypes types) {
+		if (value == Result::Cancelled) {
+			close();
+		} else if (value == Result::Unsupported) {
+			_parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));
+			close();
+		} else if (v::is<WebViewSourceLinkAttachMenu>(_source)) {
+			maybeChooseAndRequestButton(types);
+		} else if (v::is<WebViewSourceAttachMenu>(_source)) {
+			requestButton();
+		} else {
+			requestSimple();
+		}
+	});
+	_session->attachWebView().requestAddToMenu(_bot, done);
+}
+
+void WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) {
+	Expects(v::is<WebViewSourceLinkAttachMenu>(_source));
+
+	const auto link = v::get<WebViewSourceLinkAttachMenu>(_source);
+	const auto chooseFrom = (link.choose & supported);
+	if (!chooseFrom) {
+		requestButton();
+		return;
+	}
+	const auto bot = _bot;
+	const auto button = _button;
+	const auto weak = _context.controller;
+	const auto done = [=](not_null<Data::Thread*> thread) {
+		if (const auto controller = WindowForThread(weak, thread)) {
+			thread->session().attachWebView().open({
+				.bot = bot,
+				.context = {
+					.controller = controller,
+					.action = Api::SendAction(thread),
+				},
+				.button = button,
+				.source = InlineBots::WebViewSourceLinkAttachMenu{
+					.thread = thread,
+					.token = button.startCommand,
+				},
+			});
+		}
+	};
+	ShowChooseBox(_parentShow, _session, chooseFrom, done);
+	close();
+}
+
+void WebViewInstance::show(const QString &url, uint64 queryId) {
+	auto title = Info::Profile::NameValue(_bot);
+
+	const auto &bots = _session->attachWebView().attachBots();
+
+	using Button = Ui::BotWebView::MenuButton;
+	const auto attached = ranges::find(
+		bots,
+		not_null{ _bot },
+		&AttachWebViewBot::user);
+	const auto hasOpenBot = v::is<WebViewSourceMainMenu>(_source)
+		|| (_context.action->history->peer != _bot);
+	const auto hasRemoveFromMenu = (attached != end(bots))
+		&& (!attached->inactive || attached->inMainMenu)
+		&& (v::is<WebViewSourceMainMenu>(_source)
+			|| v::is<WebViewSourceAttachMenu>(_source)
+			|| v::is<WebViewSourceLinkAttachMenu>(_source));
+	const auto buttons = (hasOpenBot ? Button::OpenBot : Button::None)
+		| (!hasRemoveFromMenu
+			? Button::None
+			: attached->inMainMenu
+			? Button::RemoveFromMainMenu
+			: Button::RemoveFromMenu);
+	const auto allowClipboardRead = v::is<WebViewSourceAttachMenu>(_source)
+		|| v::is<WebViewSourceAttachMenu>(_source)
+		|| (attached != end(bots)
+			&& (attached->inAttachMenu || attached->inMainMenu));
+	_panelUrl = url;
+	_panel = Ui::BotWebView::Show({
+		.url = url,
+		.storageId = _session->local().resolveStorageIdBots(),
+		.title = std::move(title),
+		.bottom = rpl::single('@' + _bot->username()),
+		.delegate = static_cast<Ui::BotWebView::Delegate*>(this),
+		.menuButtons = buttons,
+		.allowClipboardRead = allowClipboardRead,
+	});
+	started(queryId);
+
+	if (const auto strong = PendingActivation.get()) {
+		if (strong == this) {
+			PendingActivation = nullptr;
+			_panel->requestActivate();
+		}
+	}
+}
+
+void WebViewInstance::showGame() {
+	Expects(v::is<WebViewSourceGame>(_source));
+
+	const auto game = v::get<WebViewSourceGame>(_source);
+	_panelUrl = QString::fromUtf8(_button.url);
+	_panel = Ui::BotWebView::Show({
+		.url = _panelUrl,
+		.storageId = _session->local().resolveStorageIdBots(),
+		.title = rpl::single(game.title),
+		.bottom = rpl::single('@' + _bot->username()),
+		.delegate = static_cast<Ui::BotWebView::Delegate*>(this),
+		.menuButtons = Ui::BotWebView::MenuButton::ShareGame,
+	});
+}
+
+void WebViewInstance::close() {
+	_session->attachWebView().close(this);
+}
+
+void WebViewInstance::started(uint64 queryId) {
+	Expects(_context.action.has_value());
+
+	if (!queryId) {
+		return;
+	}
+
+	_session->data().webViewResultSent(
+	) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) {
+		return (sent.queryId == queryId);
+	}) | rpl::start_with_next([=] {
+		close();
+	}, _panel->lifetime());
+
+	const auto action = *_context.action;
+	base::timer_each(
+		kProlongTimeout
+	) | rpl::start_with_next([=] {
+		using Flag = MTPmessages_ProlongWebView::Flag;
+		_session->api().request(base::take(_prolongId)).cancel();
+		_prolongId = _session->api().request(MTPmessages_ProlongWebView(
+			MTP_flags(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),
+			action.mtpReplyTo(),
+			(action.options.sendAs
+				? action.options.sendAs->input
+				: MTP_inputPeerEmpty())
+		)).done([=] {
+			_prolongId = 0;
+		}).send();
+	}, _panel->lifetime());
+}
+
+Webview::ThemeParams WebViewInstance::botThemeParams() {
 	return Window::Theme::WebViewParams();
 }
 
-bool AttachWebView::botHandleLocalUri(QString uri, bool keepOpen) {
+bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {
 	const auto local = Core::TryConvertUrlToLocal(uri);
 	if (uri == local || Core::InternalPassportLink(local)) {
 		return local.startsWith(u"tg://"_q);
 	} else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) {
 		return false;
 	}
+	const auto bot = _bot;
+	const auto context = std::make_shared<WebViewContext>(_context);
 	if (!keepOpen) {
 		botClose();
 	}
-	crl::on_main([=, shownUrl = _lastShownUrl, bot = _bot] {
+	crl::on_main([=] {
 		if (bot->session().windows().empty()) {
 			Core::App().domain().activate(&bot->session().account());
 		}
 		const auto window = !bot->session().windows().empty()
 			? bot->session().windows().front().get()
 			: nullptr;
+		context->controller = window;
 		const auto variant = QVariant::fromValue(ClickHandlerContext{
-			.attachBotWebviewUrl = shownUrl,
 			.sessionWindow = window,
+			.botWebviewContext = context,
 		});
 		UrlClickHandler::Open(local, variant);
 	});
 	return true;
 }
 
-void AttachWebView::botHandleInvoice(QString slug) {
+void WebViewInstance::botHandleInvoice(QString slug) {
 	Expects(_panel != nullptr);
 
 	using Result = Payments::CheckoutResult;
@@ -628,12 +1126,12 @@ void AttachWebView::botHandleInvoice(QString slug) {
 		nonPanelPaymentFormFactory(reactivate));
 }
 
-auto AttachWebView::nonPanelPaymentFormFactory(
+auto WebViewInstance::nonPanelPaymentFormFactory(
 	Fn<void(Payments::CheckoutResult)> reactivate)
 -> Fn<void(Payments::NonPanelPaymentForm)> {
 	using namespace Payments;
 	const auto panel = base::make_weak(_panel.get());
-	const auto weak = _context ? _context->controller : nullptr;
+	const auto weak = _context.controller;
 	return [=](Payments::NonPanelPaymentForm form) {
 		using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
 		using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
@@ -655,8 +1153,8 @@ auto AttachWebView::nonPanelPaymentFormFactory(
 	};
 }
 
-void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) {
-	Expects(_bot != nullptr);
+void WebViewInstance::botHandleMenuButton(
+		Ui::BotWebView::MenuButton button) {
 	Expects(_panel != nullptr);
 
 	using Button = Ui::BotWebView::MenuButton;
@@ -675,17 +1173,23 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) {
 		break;
 	case Button::RemoveFromMenu:
 	case Button::RemoveFromMainMenu:
+		const auto &bots = _session->attachWebView().attachBots();
 		const auto attached = ranges::find(
-			_attachBots,
-			not_null{ _bot },
+			bots,
+			_bot,
 			&AttachWebViewBot::user);
-		const auto name = (attached != end(_attachBots))
+		const auto name = (attached != end(bots))
 			? attached->name
 			: _bot->name();
 		const auto done = crl::guard(this, [=] {
-			removeFromMenu(bot);
+			const auto session = _session;
+			const auto was = _parentShow;
 			botClose();
-			if (const auto active = Core::App().activeWindow()) {
+
+			const auto active = Core::App().activeWindow();
+			const auto show = active ? active->uiShow() : was;
+			session->attachWebView().removeFromMenu(show, bot);
+			if (active) {
 				active->activate();
 			}
 		});
@@ -704,7 +1208,7 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) {
 	}
 }
 
-bool AttachWebView::botValidateExternalLink(QString uri) {
+bool WebViewInstance::botValidateExternalLink(QString uri) {
 	const auto lower = uri.toLower();
 	const auto allowed = _session->appConfig().get<std::vector<QString>>(
 		"web_app_allowed_protocols",
@@ -717,8 +1221,8 @@ bool AttachWebView::botValidateExternalLink(QString uri) {
 	return false;
 }
 
-void AttachWebView::botOpenIvLink(QString uri) {
-	const auto window = _context ? _context->controller.get() : nullptr;
+void WebViewInstance::botOpenIvLink(QString uri) {
+	const auto window = _context.controller.get();
 	if (window) {
 		Core::App().iv().openWithIvPreferred(window, uri);
 	} else {
@@ -726,33 +1230,32 @@ void AttachWebView::botOpenIvLink(QString uri) {
 	}
 }
 
-void AttachWebView::botSendData(QByteArray data) {
-	if (!_context
-		|| _context->fromSwitch
-		|| _context->fromBotApp
-		|| _context->fromMainMenu
-		|| _context->action.history->peer != _bot
-		|| _lastShownQueryId) {
+void WebViewInstance::botSendData(QByteArray data) {
+	Expects(_context.action.has_value());
+
+	const auto button = std::get_if<WebViewSourceButton>(&_source);
+	if (!button
+		|| !button->simple
+		|| _context.action->history->peer != _bot
+		|| _dataSent) {
 		return;
 	}
-	const auto randomId = base::RandomValue<uint64>();
+	_dataSent = true;
 	_session->api().request(MTPmessages_SendWebViewData(
 		_bot->inputUser,
-		MTP_long(randomId),
-		MTP_string(_lastShownButtonText),
+		MTP_long(base::RandomValue<uint64>()),
+		MTP_string(_button.text),
 		MTP_bytes(data)
-	)).done([=](const MTPUpdates &result) {
-		_session->api().applyUpdates(result);
+	)).done([session = _session](const MTPUpdates &result) {
+		session->api().applyUpdates(result);
 	}).send();
-	crl::on_main(this, [=] { cancel(); });
+	botClose();
 }
 
-void AttachWebView::botSwitchInlineQuery(
+void WebViewInstance::botSwitchInlineQuery(
 		std::vector<QString> chatTypes,
 		QString query) {
-	const auto controller = _context
-		? _context->controller.get()
-		: nullptr;
+	const auto controller = _context.controller.get();
 	const auto types = PeerTypesFromNames(chatTypes);
 	if (!_bot
 		|| !_bot->isBot()
@@ -760,9 +1263,9 @@ void AttachWebView::botSwitchInlineQuery(
 		|| !controller) {
 		return;
 	} else if (!types) {
-		if (_context->dialogsEntryState.key.owningHistory()) {
+		if (_context.dialogsEntryState.key.owningHistory()) {
 			controller->switchInlineQuery(
-				_context->dialogsEntryState,
+				_context.dialogsEntryState,
 				_bot,
 				query);
 		}
@@ -777,10 +1280,10 @@ void AttachWebView::botSwitchInlineQuery(
 			done,
 			tr::lng_inline_switch_choose());
 	}
-	crl::on_main(this, [=] { cancel(); });
+	botClose();
 }
 
-void AttachWebView::botCheckWriteAccess(Fn<void(bool allowed)> callback) {
+void WebViewInstance::botCheckWriteAccess(Fn<void(bool allowed)> callback) {
 	_session->api().request(MTPbots_CanSendMessage(
 		_bot->inputUser
 	)).done([=](const MTPBool &result) {
@@ -790,46 +1293,43 @@ void AttachWebView::botCheckWriteAccess(Fn<void(bool allowed)> callback) {
 	}).send();
 }
 
-void AttachWebView::botAllowWriteAccess(Fn<void(bool allowed)> callback) {
+void WebViewInstance::botAllowWriteAccess(Fn<void(bool allowed)> callback) {
 	_session->api().request(MTPbots_AllowSendMessage(
 		_bot->inputUser
-	)).done([=](const MTPUpdates &result) {
-		_session->api().applyUpdates(result);
+	)).done([session = _session, callback](const MTPUpdates &result) {
+		session->api().applyUpdates(result);
 		callback(true);
 	}).fail([=] {
 		callback(false);
 	}).send();
 }
 
-void AttachWebView::botSharePhone(Fn<void(bool shared)> callback) {
+void WebViewInstance::botSharePhone(Fn<void(bool shared)> callback) {
 	const auto bot = _bot;
 	const auto history = _bot->owner().history(_bot);
 	if (_bot->isBlocked()) {
-		const auto done = [=](bool success) {
-			if (success && _bot == bot) {
-				Assert(!_bot->isBlocked());
+		const auto done = crl::guard(this, [=](bool success) {
+			if (success) {
 				botSharePhone(callback);
 			} else {
 				callback(false);
 			}
-		};
-		_bot->session().api().blockedPeers().unblock(
-			_bot,
-			crl::guard(this, done));
+		});
+		_session->api().blockedPeers().unblock(_bot, done);
 		return;
 	}
 	auto action = Api::SendAction(history);
 	action.clearDraft = false;
-	history->session().api().shareContact(
-		_bot->session().user(),
+	_session->api().shareContact(
+		_session->user(),
 		action,
 		std::move(callback));
 }
 
-void AttachWebView::botInvokeCustomMethod(
+void WebViewInstance::botInvokeCustomMethod(
 		Ui::BotWebView::CustomMethodRequest request) {
 	const auto callback = request.callback;
-	_bot->session().api().request(MTPbots_InvokeWebViewCustomMethod(
+	_session->api().request(MTPbots_InvokeWebViewCustomMethod(
 		_bot->inputUser,
 		MTP_string(request.method),
 		MTP_dataJSON(MTP_bytes(request.params))
@@ -840,148 +1340,141 @@ void AttachWebView::botInvokeCustomMethod(
 	}).send();
 }
 
-void AttachWebView::botShareGameScore() {
-	if (!_panel || !_gameContext) {
+void WebViewInstance::botShareGameScore() {
+	const auto itemId = v::is<WebViewSourceGame>(_source)
+		? v::get<WebViewSourceGame>(_source).messageId
+		: FullMsgId();
+	if (!_panel || !itemId) {
 		return;
-	} else if (const auto item = _session->data().message(_gameContext)) {
+	} else if (const auto item = _session->data().message(itemId)) {
 		FastShareMessage(uiShow(), item);
 	} else {
 		_panel->showToast({ tr::lng_message_not_found(tr::now) });
 	}
 }
 
-void AttachWebView::botClose() {
-	crl::on_main(this, [=] { cancel(); });
+void WebViewInstance::botClose() {
+	crl::on_main(this, [=] { close(); });
 }
 
-AttachWebView::Context AttachWebView::LookupContext(
-		not_null<Window::SessionController*> controller,
-		const Api::SendAction &action) {
-	return {
-		.controller = controller,
-		.dialogsEntryState = controller->currentDialogsEntryState(),
-		.action = action,
+std::shared_ptr<Main::SessionShow> WebViewInstance::uiShow() {
+	class Show final : public Main::SessionShow {
+	public:
+		explicit Show(not_null<WebViewInstance*> that) : _that(that) {
+		}
+
+		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 {
+			using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
+			using ObjectBox = object_ptr<Ui::BoxContent>;
+			const auto panel = _that ? _that->_panel.get() : nullptr;
+			if (v::is<UniqueLayer>(layer)) {
+				Unexpected("Layers in WebView are not implemented.");
+			} else if (auto box = std::get_if<ObjectBox>(&layer)) {
+				if (panel) {
+					panel->showBox(std::move(*box), options, animated);
+				}
+			} else if (panel) {
+				panel->hideLayer(animated);
+			}
+		}
+		[[nodiscard]] not_null<QWidget*> toastParent() const override {
+			const auto panel = _that ? _that->_panel.get() : nullptr;
+
+			Ensures(panel != nullptr);
+			return panel->toastParent();
+		}
+		[[nodiscard]] bool valid() const override {
+			return _that && (_that->_panel != nullptr);
+		}
+		operator bool() const override {
+			return valid();
+		}
+
+		[[nodiscard]] Main::Session &session() const override {
+			Expects(_that.get() != nullptr);
+
+			return *_that->_session;
+		}
+
+	private:
+		const base::weak_ptr<WebViewInstance> _that;
+
 	};
+	return std::make_shared<Show>(this);
 }
 
-bool AttachWebView::IsSame(
-		const std::unique_ptr<Context> &a,
-		const Context &b) {
-	// Check fields that are sent to API in bot attach webview requests.
-	return a
-		&& (a->controller == b.controller)
-		&& (a->dialogsEntryState == b.dialogsEntryState)
-		&& (a->fromSwitch == b.fromSwitch)
-		&& (a->fromMainMenu == b.fromMainMenu)
-		&& (a->action.history == b.action.history)
-		&& (a->action.replyTo == b.action.replyTo)
-		&& (a->action.options.sendAs == b.action.options.sendAs)
-		&& (a->action.options.silent == b.action.options.silent);
+AttachWebView::AttachWebView(not_null<Main::Session*> session)
+: _session(session)
+, _refreshTimer([=] { requestBots(); }) {
+	_refreshTimer.callEach(kRefreshBotsTimeout);
 }
 
-void AttachWebView::request(
+AttachWebView::~AttachWebView() = default;
+
+void AttachWebView::openByUsername(
 		not_null<Window::SessionController*> controller,
 		const Api::SendAction &action,
-		not_null<UserData*> bot,
-		const WebViewButton &button) {
-	requestWithOptionalConfirm(
-		bot,
-		button,
-		LookupContext(controller, action),
-		button.fromAttachMenu ? nullptr : controller.get());
-}
-
-void AttachWebView::requestWithOptionalConfirm(
-		not_null<UserData*> bot,
-		const WebViewButton &button,
-		const Context &context,
-		Window::SessionController *controllerForConfirm) {
-	if (IsSame(_context, context) && _bot == bot) {
-		if (_panel) {
-			_panel->requestActivate();
-		} else if (_requestId) {
-			return;
-		}
+		const QString &botUsername,
+		const QString &startCommand) {
+	if (botUsername.isEmpty()
+		|| (_botUsername == botUsername && _startCommand == startCommand)) {
+		return;
 	}
 	cancel();
 
-	_bot = bot;
-	_context = std::make_unique<Context>(context);
-	if (controllerForConfirm) {
-		confirmOpen(controllerForConfirm, [=] {
-			request(button);
+	_botUsername = botUsername;
+	_startCommand = startCommand;
+	const auto weak = base::make_weak(controller);
+	const auto show = controller->uiShow();
+	resolveUsername(show, crl::guard(weak, [=](not_null<PeerData*> peer) {
+		_botUsername = QString();
+		const auto token = base::take(_startCommand);
+
+		const auto bot = peer->asUser();
+		if (!bot || !bot->isBot()) {
+			if (const auto strong = weak.get()) {
+				strong->showToast(tr::lng_bot_menu_not_supported(tr::now));
+			}
+			return;
+		}
+
+		open({
+			.bot = bot,
+			.context = {
+				.controller = controller,
+				.action = action,
+			},
+			.button = { .startCommand = token },
+			.source = InlineBots::WebViewSourceLinkAttachMenu{},
 		});
-	} else {
-		request(button);
+	}));
+}
+
+void AttachWebView::close(not_null<WebViewInstance*> instance) {
+	const auto i = ranges::find(
+		_instances,
+		instance.get(),
+		&std::unique_ptr<WebViewInstance>::get);
+	if (i != end(_instances)) {
+		const auto taken = base::take(*i);
+		_instances.erase(i);
 	}
 }
 
-void AttachWebView::request(const WebViewButton &button) {
-	Expects(_context != nullptr && _bot != nullptr);
-
-	if (button.fromAttachMenu) {
-		const auto bot = ranges::find(
-			_attachBots,
-			not_null{ _bot },
-			&AttachWebViewBot::user);
-		if (bot == end(_attachBots) || bot->inactive) {
-			requestAddToMenu(_bot, AddToMenuOpenAttach{
-				.startCommand = button.startCommand,
-			});
-			return;
-		}
-	}
-
-	_startCommand = button.startCommand;
-	const auto &action = _context->action;
-
-	using Flag = MTPmessages_RequestWebView::Flag;
-	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 : 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(
-		MTP_flags(flags),
-		action.history->peer->input,
-		_bot->inputUser,
-		MTP_bytes(button.url),
-		MTP_string(_startCommand),
-		MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
-		MTP_string("tdesktop"),
-		action.mtpReplyTo(),
-		(action.options.sendAs
-			? action.options.sendAs->input
-			: MTP_inputPeerEmpty())
-	)).done([=](const MTPWebViewResult &result) {
-		_requestId = 0;
-		const auto &data = result.data();
-		show(
-			data.vquery_id().value_or_empty(),
-			qs(data.vurl()),
-			button.text,
-			button.fromAttachMenu || button.url.isEmpty());
-	}).fail([=](const MTP::Error &error) {
-		_requestId = 0;
-		if (error.type() == u"BOT_INVALID"_q) {
-			requestBots();
-		}
-	}).send();
+void AttachWebView::closeAll() {
+	cancel();
+	base::take(_instances);
 }
 
 void AttachWebView::cancel() {
-	Expects(!_catchingCancelInShowCall);
-
-	ActiveWebViews().remove(this);
 	_session->api().request(base::take(_requestId)).cancel();
-	_session->api().request(base::take(_prolongId)).cancel();
-	base::take(_panel);
-	_lastShownContext = base::take(_context);
-	_bot = nullptr;
-	_app = nullptr;
 	_botUsername = QString();
-	_botAppName = QString();
 	_startCommand = QString();
 }
 
@@ -1033,190 +1526,83 @@ bool AttachWebView::showMainMenuNewBadge(
 
 void AttachWebView::requestAddToMenu(
 		not_null<UserData*> bot,
-		AddToMenuOpen open) {
-	requestAddToMenu(bot, open, nullptr, std::nullopt);
-}
-
-void AttachWebView::requestAddToMenu(
-		not_null<UserData*> bot,
-		AddToMenuOpen open,
-		Window::SessionController *controller,
-		std::optional<Api::SendAction> action) {
-	Expects(controller != nullptr || _context != nullptr);
-
-	const auto wasController = (controller != nullptr);
-	_addToMenuChooseController = base::make_weak(controller);
-	_addToMenuOpen = open;
-	if (!controller) {
-		_addToMenuContext = base::take(_context);
-	} else if (action) {
-		_addToMenuContext = std::make_unique<Context>(
-			LookupContext(controller, *action));
+		Fn<void(AddToMenuResult, PeerTypes supported)> done) {
+	auto &process = _addToMenu[bot];
+	if (done) {
+		process.done.push_back(std::move(done));
 	}
-
-	const auto unsupported = [=] {
-		auto context = base::take(_addToMenuContext);
-		const auto open = base::take(_addToMenuOpen);
-		if (const auto openApp = std::get_if<AddToMenuOpenApp>(&open)) {
-			_app = openApp->app;
-			_startCommand = openApp->startCommand;
-			_context = std::move(context);
-			if (_appConfirmationRequired) {
-				confirmAppOpen(_appRequestWriteAccess);
-			} else {
-				requestAppView(false);
-			}
-		} else {
-			showToast(
-				tr::lng_bot_menu_not_supported(tr::now),
-				_addToMenuChooseController.get());
-		}
-	};
-	if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {
-		unsupported();
+	if (process.requestId) {
 		return;
 	}
 
-	if (_addToMenuId) {
-		if (_addToMenuBot == bot) {
+	const auto finish = [=](AddToMenuResult result, PeerTypes supported) {
+		if (auto process = _addToMenu.take(bot)) {
+			for (const auto &done : process->done) {
+				done(result, supported);
+			}
+		}
+	};
+	if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {
+		finish(AddToMenuResult::Unsupported, {});
+		return;
+	}
+
+	process.requestId = _session->api().request(
+		MTPmessages_GetAttachMenuBot(bot->inputUser)
+	).done([=](const MTPAttachMenuBotsBot &result) {
+		_addToMenu[bot].requestId = 0;
+		const auto &data = result.data();
+		_session->data().processUsers(data.vusers());
+		const auto parsed = ParseAttachBot(_session, data.vbot());
+		if (!parsed || bot != parsed->user) {
+			finish(AddToMenuResult::Unsupported, {});
 			return;
 		}
-		_session->api().request(base::take(_addToMenuId)).cancel();
-	}
-	_addToMenuBot = bot;
-	_addToMenuId = _session->api().request(MTPmessages_GetAttachMenuBot(
-		bot->inputUser
-	)).done([=](const MTPAttachMenuBotsBot &result) {
-		_addToMenuId = 0;
-		const auto bot = base::take(_addToMenuBot);
-		const auto context = std::shared_ptr(base::take(_addToMenuContext));
-		const auto open = base::take(_addToMenuOpen);
-		const auto chooseController = base::take(_addToMenuChooseController);
-		const auto launch = [=](PeerTypes types) {
-			const auto openAttach = v::is<AddToMenuOpenAttach>(open)
-				? v::get<AddToMenuOpenAttach>(open)
-				: AddToMenuOpenAttach();
-			const auto chooseTypes = openAttach.chooseTypes;
-			const auto strong = chooseController.get();
-			if (v::is<AddToMenuOpenApp>(open)) {
-				if (!context) {
-					return false;
-				}
-				const auto &openApp = v::get<AddToMenuOpenApp>(open);
-				_app = openApp.app;
-				_startCommand = openApp.startCommand;
-				_context = std::make_unique<Context>(*context);
-				requestAppView(true);
-				return true;
-			} else if (!strong) {
-				if (wasController || !v::is<AddToMenuOpenAttach>(open)) {
-					// Just ignore the click if controller was destroyed.
-					return true;
-				}
-			} else if (v::is<AddToMenuOpenMenu>(open)) {
-				const auto &openMenu = v::get<AddToMenuOpenMenu>(open);
-				_bot = bot;
-				requestSimple(strong, bot, {
-					.startCommand = openMenu.startCommand,
-					.fromMainMenu = true,
-				});
-				return true;
-			} else if (const auto useTypes = chooseTypes & types) {
-				const auto done = [=](not_null<Data::Thread*> thread) {
-					strong->showThread(thread);
-					requestWithOptionalConfirm(
-						bot,
-						{ .startCommand = openAttach.startCommand },
-						LookupContext(strong, Api::SendAction(thread)));
-				};
-				ShowChooseBox(strong, useTypes, done);
-				return true;
-			}
-			if (!context) {
-				return false;
-			}
-			requestWithOptionalConfirm(
-				bot,
-				{ .startCommand = openAttach.startCommand },
-				*context);
-			return true;
-		};
-		result.match([&](const MTPDattachMenuBotsBot &data) {
-			_session->data().processUsers(data.vusers());
-			if (const auto parsed = ParseAttachBot(_session, data.vbot())) {
-				if (bot == parsed->user) {
-					const auto i = ranges::find(
-						_attachBots,
-						not_null(bot),
-						&AttachWebViewBot::user);
-					if (i != end(_attachBots)) {
-						// Save flags in our list, like 'inactive'.
-						*i = *parsed;
-					}
-					const auto types = parsed->types;
-					if (parsed->inactive) {
-						confirmAddToMenu(*parsed, [=] {
-							launch(types);
-						});
-					} else {
-						requestBots();
-						if (!launch(types)) {
-							showToast(
-								tr::lng_bot_menu_already_added(tr::now));
-						}
-					}
-				}
-			}
-		});
+		const auto i = ranges::find(
+			_attachBots,
+			not_null(bot),
+			&AttachWebViewBot::user);
+		if (i != end(_attachBots)) {
+			// Save flags in our list, like 'inactive'.
+			*i = *parsed;
+		}
+		const auto types = parsed->types;
+		if (parsed->inactive) {
+			confirmAddToMenu(*parsed, [=](bool added) {
+				const auto result = added
+					? AddToMenuResult::Added
+					: AddToMenuResult::Cancelled;
+				finish(result, types);
+			});
+		} else {
+			requestBots();
+			finish(AddToMenuResult::AlreadyInMenu, types);
+		}
 	}).fail([=] {
-		_addToMenuId = 0;
-		_addToMenuBot = nullptr;
-		unsupported();
+		finish(AddToMenuResult::Unsupported, {});
 	}).send();
 }
 
-void AttachWebView::removeFromMenu(not_null<UserData*> bot) {
-	toggleInMenu(bot, ToggledState::Removed, [=] {
-		showToast(tr::lng_bot_remove_from_menu_done(tr::now));
-	});
-}
-
-std::optional<Api::SendAction> AttachWebView::lookupLastAction(
-		const QString &url) const {
-	if (_lastShownUrl == url && _lastShownContext) {
-		return _lastShownContext->action;
-	}
-	return std::nullopt;
-}
-
-void AttachWebView::resolve() {
-	Expects(!_panel);
-
-	resolveUsername(_botUsername, [=](not_null<PeerData*> bot) {
-		if (!_context) {
-			return;
+void AttachWebView::removeFromMenu(
+		std::shared_ptr<Ui::Show> show,
+		not_null<UserData*> bot) {
+	toggleInMenu(bot, ToggledState::Removed, [=](bool success) {
+		if (success) {
+			show->showToast(tr::lng_bot_remove_from_menu_done(tr::now));
 		}
-		_bot = bot->asUser();
-		if (!_bot) {
-			showToast(tr::lng_bot_menu_not_supported(tr::now));
-			return;
-		}
-		requestAddToMenu(_bot, AddToMenuOpenAttach{
-			.startCommand = _startCommand,
-		});
 	});
 }
 
 void AttachWebView::resolveUsername(
-		const QString &username,
+		std::shared_ptr<Ui::Show> show,
 		Fn<void(not_null<PeerData*>)> done) {
-	if (const auto peer = _session->data().peerByUsername(username)) {
+	if (const auto peer = _session->data().peerByUsername(_botUsername)) {
 		done(peer);
 		return;
 	}
 	_session->api().request(base::take(_requestId)).cancel();
 	_requestId = _session->api().request(MTPcontacts_ResolveUsername(
-		MTP_string(username)
+		MTP_string(_botUsername)
 	)).done([=](const MTPcontacts_ResolvedPeer &result) {
 		_requestId = 0;
 		result.match([&](const MTPDcontacts_resolvedPeer &data) {
@@ -1229,519 +1615,64 @@ void AttachWebView::resolveUsername(
 	}).fail([=](const MTP::Error &error) {
 		_requestId = 0;
 		if (error.code() == 400) {
-			showToast(
-				tr::lng_username_not_found(tr::now, lt_user, username));
+			show->showToast(
+				tr::lng_username_not_found(tr::now, lt_user, _botUsername));
 		}
 	}).send();
 }
 
-void AttachWebView::requestSimple(
-		not_null<Window::SessionController*> controller,
-		not_null<UserData*> bot,
-		const WebViewButton &button) {
-	cancel();
-	_bot = bot;
-	_context = std::make_unique<Context>(LookupContext(
-		controller,
-		Api::SendAction(bot->owner().history(bot))));
-	_context->fromSwitch = button.fromSwitch;
-	_context->fromMainMenu = button.fromMainMenu;
-	if (button.fromMainMenu) {
-		acceptMainMenuDisclaimer(controller, button);
-	} else {
-		confirmOpen(controller, [=] {
-			requestSimple(button);
-		});
-	}
-}
-
-void AttachWebView::requestSimple(const WebViewButton &button) {
-	using Flag = MTPmessages_RequestSimpleWebView::Flag;
-	_requestId = _session->api().request(MTPmessages_RequestSimpleWebView(
-		MTP_flags(Flag::f_theme_params
-			| (button.fromMainMenu
-				? (Flag::f_from_side_menu
-					| (button.startCommand.isEmpty()
-						? Flag()
-						: Flag::f_start_param))
-				: Flag::f_url)
-			| (button.fromSwitch ? Flag::f_from_switch_webview : Flag())),
-		_bot->inputUser,
-		MTP_bytes(button.url),
-		MTP_string(button.startCommand),
-		MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
-		MTP_string("tdesktop")
-	)).done([=](const MTPWebViewResult &result) {
-		_requestId = 0;
-		const auto &data = result.data();
-		const auto queryId = uint64();
-		show(
-			queryId,
-			qs(data.vurl()),
-			button.text,
-			false,
-			nullptr,
-			button.fromMainMenu);
-	}).fail([=](const MTP::Error &error) {
-		_requestId = 0;
-	}).send();
-}
-
-bool AttachWebView::openAppFromMenuLink(
-		not_null<Window::SessionController*> controller,
-		not_null<UserData*> bot) {
-	Expects(bot->botInfo != nullptr);
-
-	const auto &url = bot->botInfo->botMenuButtonUrl;
-	const auto local = Core::TryConvertUrlToLocal(url);
-	const auto prefix = u"tg://resolve?"_q;
-	if (!local.startsWith(prefix)) {
-		return false;
-	}
-	const auto params = qthelp::url_parse_params(
-		local.mid(prefix.size()),
-		qthelp::UrlParamNameTransform::ToLower);
-	const auto domainParam = params.value(u"domain"_q);
-	const auto appnameParam = params.value(u"appname"_q);
-	const auto webChannelPreviewLink = (domainParam == u"s"_q)
-		&& !appnameParam.isEmpty();
-	const auto appname = webChannelPreviewLink ? QString() : appnameParam;
-	if (appname.isEmpty()) {
-		return false;
-	}
-	requestApp(
-		controller,
-		Api::SendAction(bot->owner().history(bot)),
-		bot,
-		appname,
-		params.value(u"startapp"_q),
-		true);
-	return true;
-}
-
-void AttachWebView::requestMenu(
-	not_null<Window::SessionController*> controller,
-		not_null<UserData*> bot) {
-	if (openAppFromMenuLink(controller, bot)) {
-		return;
-	}
-
-	cancel();
-	_bot = bot;
-	_context = std::make_unique<Context>(LookupContext(
-		controller,
-		Api::SendAction(bot->owner().history(bot))));
-	const auto url = bot->botInfo->botMenuButtonUrl;
-	const auto text = bot->botInfo->botMenuButtonText;
-	confirmOpen(controller, [=] {
-		const auto &action = _context->action;
-		using Flag = MTPmessages_RequestWebView::Flag;
-		_requestId = _session->api().request(MTPmessages_RequestWebView(
-			MTP_flags(Flag::f_theme_params
-				| Flag::f_url
-				| Flag::f_from_bot_menu
-				| (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_string(url),
-			MTPstring(), // start_param
-			MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
-			MTP_string("tdesktop"),
-			action.mtpReplyTo(),
-			(action.options.sendAs
-				? action.options.sendAs->input
-				: MTP_inputPeerEmpty())
-		)).done([=](const MTPWebViewResult &result) {
-			_requestId = 0;
-			const auto &data = result.data();
-			show(data.vquery_id().value_or_empty(), qs(data.vurl()), text);
-		}).fail([=](const MTP::Error &error) {
-			_requestId = 0;
-			if (error.type() == u"BOT_INVALID"_q) {
-				requestBots();
-			}
-		}).send();
-	});
-}
-
-void AttachWebView::requestApp(
-		not_null<Window::SessionController*> controller,
-		const Api::SendAction &action,
-		not_null<UserData*> bot,
-		const QString &appName,
-		const QString &startParam,
-		bool forceConfirmation) {
-	const auto context = LookupContext(controller, action);
-	if (_requestId
-		&& _bot == bot
-		&& _startCommand == startParam
-		&& _botAppName == appName
-		&& IsSame(_context, context)) {
-		return;
-	}
-	cancel();
-	_bot = bot;
-	_startCommand = startParam;
-	_botAppName = appName;
-	_context = std::make_unique<Context>(context);
-	_context->fromBotApp = true;
-	const auto already = _session->data().findBotApp(_bot->id, appName);
-	_requestId = _session->api().request(MTPmessages_GetBotApp(
-		MTP_inputBotAppShortName(
-			bot->inputUser,
-			MTP_string(appName)),
-		MTP_long(already ? already->hash : 0)
-	)).done([=](const MTPmessages_BotApp &result) {
-		_requestId = 0;
-		if (!_bot || !_context) {
+void AttachWebView::open(WebViewDescriptor &&descriptor) {
+	for (const auto &instance : _instances) {
+		if (instance->bot() == descriptor.bot
+			&& instance->source() == descriptor.source) {
+			instance->activate();
 			return;
 		}
-		const auto &data = result.data();
-		const auto firstTime = data.is_inactive();
-		const auto received = _session->data().processBotApp(
-			_bot->id,
-			data.vapp());
-		_app = received ? received : already;
-		if (!_app) {
-			cancel();
-			showToast(tr::lng_username_app_not_found(tr::now));
-			return;
-		}
-		// Check if this app can be added to main menu.
-		// On fail it'll still be opened.
-		_appConfirmationRequired = firstTime || forceConfirmation;
-		_appRequestWriteAccess = result.data().is_request_write_access();
-		requestAddToMenu(_bot, AddToMenuOpenApp{
-			.app = _app,
-			.startCommand = _startCommand,
-		});
-	}).fail([=] {
-		showToast(tr::lng_username_app_not_found(tr::now));
-		cancel();
-	}).send();
-}
-
-void AttachWebView::confirmAppOpen(bool requestWriteAccess) {
-	const auto controller = _context ? _context->controller.get() : nullptr;
-	if (!controller || !_bot) {
-		return;
 	}
-	controller->show(Box([=](not_null<Ui::GenericBox*> box) {
-		const auto allowed = std::make_shared<Ui::Checkbox*>();
-		const auto done = [=](Fn<void()> close) {
-			requestAppView((*allowed) && (*allowed)->checked());
-			close();
-		};
-		Ui::ConfirmBox(box, {
-			tr::lng_allow_bot_webview(
-				tr::now,
-				lt_bot_name,
-				Ui::Text::Bold(_bot->name()),
-				Ui::Text::RichLangValue),
-			done,
-		});
-		if (requestWriteAccess) {
-			(*allowed) = box->addRow(
-				object_ptr<Ui::Checkbox>(
-					box,
-					tr::lng_url_auth_allow_messages(
-						tr::now,
-						lt_bot,
-						Ui::Text::Bold(_bot->name()),
-						Ui::Text::WithEntities),
-					true,
-					st::urlAuthCheckbox),
-				style::margins(
-					st::boxRowPadding.left(),
-					st::boxPhotoCaptionSkip,
-					st::boxRowPadding.right(),
-					st::boxPhotoCaptionSkip));
-			(*allowed)->setAllowTextLines();
-		}
-	}));
-}
-
-void AttachWebView::requestAppView(bool allowWrite) {
-	if (!_context || !_app) {
-		return;
-	}
-	using Flag = MTPmessages_RequestAppWebView::Flag;
-	const auto app = _app;
-	const auto flags = Flag::f_theme_params
-		| (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param)
-		| (allowWrite ? Flag::f_write_allowed : Flag(0));
-	_requestId = _session->api().request(MTPmessages_RequestAppWebView(
-		MTP_flags(flags),
-		_context->action.history->peer->input,
-		MTP_inputBotAppID(MTP_long(app->id), MTP_long(app->accessHash)),
-		MTP_string(_startCommand),
-		MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
-		MTP_string("tdesktop")
-	)).done([=](const MTPWebViewResult &result) {
-		_requestId = 0;
-		const auto &data = result.data();
-		const auto queryId = uint64();
-		show(queryId, qs(data.vurl()), QString(), false, app);
-	}).fail([=](const MTP::Error &error) {
-		_requestId = 0;
-		if (error.type() == u"BOT_INVALID"_q) {
-			requestBots();
-		}
-	}).send();
-}
-
-void AttachWebView::confirmOpen(
-		not_null<Window::SessionController*> controller,
-		Fn<void()> done) {
-	if (!_bot) {
-		return;
-	} else if (_bot->isVerified()
-		|| _bot->session().local().isBotTrustedOpenWebView(_bot->id)) {
-		done();
-		return;
-	}
-	const auto callback = [=] {
-		_bot->session().local().markBotTrustedOpenWebView(_bot->id);
-		controller->hideLayer();
-		done();
-	};
-	controller->show(Ui::MakeConfirmBox({
-		.text = tr::lng_allow_bot_webview(
-			tr::now,
-			lt_bot_name,
-			Ui::Text::Bold(_bot->name()),
-			Ui::Text::RichLangValue),
-		.confirmed = callback,
-		.confirmText = tr::lng_box_ok(),
-	}));
+	_instances.push_back(
+		std::make_unique<WebViewInstance>(std::move(descriptor)));
+	_instances.back()->activate();
 }
 
 void AttachWebView::acceptMainMenuDisclaimer(
-		not_null<Window::SessionController*> controller,
-		const WebViewButton &button) {
-	Expects(button.fromMainMenu);
-
-	const auto local = _bot ? &_bot->session().local() : nullptr;
-	if (!local) {
-		return;
-	}
-	const auto i = ranges::find(
-		_attachBots,
-		not_null(_bot),
-		&AttachWebViewBot::user);
+		std::shared_ptr<Ui::Show> show,
+		not_null<UserData*> bot,
+		Fn<void(AddToMenuResult, PeerTypes supported)> done) {
+	const auto i = ranges::find(_attachBots, bot, &AttachWebViewBot::user);
 	if (i == end(_attachBots)) {
 		_attachBotsUpdates.fire({});
 		return;
 	} else if (i->inactive) {
-		requestAddToMenu(_bot, AddToMenuOpenMenu{
-			.startCommand = button.startCommand,
-		}, controller, {});
+		requestAddToMenu(bot, std::move(done));
 		return;
 	} else if (!i->disclaimerRequired || disclaimerAccepted(*i)) {
-		requestSimple(button);
+		done(AddToMenuResult::AlreadyInMenu, i->types);
 		return;
 	}
-
-	const auto weak = base::make_weak(this);
-	controller->show(Box(FillDisclaimerBox, crl::guard(this, [=] {
-		_disclaimerAccepted.emplace(_bot);
-		_attachBotsUpdates.fire({});
-		requestSimple(button);
+	const auto types = i->types;
+	show->show(Box(FillDisclaimerBox, crl::guard(this, [=](bool accepted) {
+		if (accepted) {
+			_disclaimerAccepted.emplace(bot);
+			_attachBotsUpdates.fire({});
+			done(AddToMenuResult::AlreadyInMenu, types);
+		} else {
+			done(AddToMenuResult::Cancelled, {});
+		}
 	})));
 }
 
-void AttachWebView::ClearAll() {
-	while (!ActiveWebViews().empty()) {
-		ActiveWebViews().front()->cancel();
-	}
-}
-
-void AttachWebView::show(
-		uint64 queryId,
-		const QString &url,
-		const QString &buttonText,
-		bool allowClipboardRead,
-		const BotAppData *app,
-		bool fromMainMenu) {
-	Expects(_bot != nullptr && _context != nullptr);
-
-	auto title = Info::Profile::NameValue(_bot);
-	ActiveWebViews().emplace(this);
-
-	using Button = Ui::BotWebView::MenuButton;
-	const auto attached = ranges::find(
-		_attachBots,
-		not_null{ _bot },
-		&AttachWebViewBot::user);
-	const auto hasOpenBot = !_context
-		|| (_bot != _context->action.history->peer)
-		|| fromMainMenu;
-	const auto hasRemoveFromMenu = !app
-		&& (attached != end(_attachBots))
-		&& (!attached->inactive || attached->inMainMenu);
-	const auto buttons = (hasOpenBot ? Button::OpenBot : Button::None)
-		| (!hasRemoveFromMenu
-			? Button::None
-			: attached->inMainMenu
-			? Button::RemoveFromMainMenu
-			: Button::RemoveFromMenu);
-	if (attached != end(_attachBots)
-		&& (attached->inAttachMenu || attached->inMainMenu)) {
-		allowClipboardRead = true;
-	}
-
-	_lastShownUrl = url;
-	_lastShownQueryId = queryId;
-	_lastShownButtonText = buttonText;
-	_gameContext = {};
-	base::take(_panel);
-	_catchingCancelInShowCall = true;
-	_panel = Ui::BotWebView::Show({
-		.url = url,
-		.storageId = _session->local().resolveStorageIdBots(),
-		.title = std::move(title),
-		.bottom = rpl::single('@' + _bot->username()),
-		.delegate = static_cast<Ui::BotWebView::Delegate*>(this),
-		.menuButtons = buttons,
-		.allowClipboardRead = allowClipboardRead,
-	});
-	_catchingCancelInShowCall = false;
-	started(queryId);
-}
-
-void AttachWebView::showGame(ShowGameParams &&params) {
-	ActiveWebViews().emplace(this);
-
-	base::take(_panel);
-	_gameContext = params.context;
-
-	_catchingCancelInShowCall = true;
-	_panel = Ui::BotWebView::Show({
-		.url = params.url,
-		.storageId = _session->local().resolveStorageIdBots(),
-		.title = rpl::single(params.title),
-		.bottom = rpl::single('@' + params.bot->username()),
-		.delegate = static_cast<Ui::BotWebView::Delegate*>(this),
-		.menuButtons = Ui::BotWebView::MenuButton::ShareGame,
-	});
-	_catchingCancelInShowCall = false;
-}
-
-void AttachWebView::started(uint64 queryId) {
-	Expects(_bot != nullptr);
-	Expects(_context != nullptr);
-
-	if (_context->fromSwitch || !queryId) {
-		return;
-	}
-
-	_session->data().webViewResultSent(
-	) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) {
-		return (sent.queryId == queryId);
-	}) | rpl::start_with_next([=] {
-		cancel();
-	}, _panel->lifetime());
-
-	const auto action = _context->action;
-	base::timer_each(
-		kProlongTimeout
-	) | rpl::start_with_next([=] {
-		using Flag = MTPmessages_ProlongWebView::Flag;
-		_session->api().request(base::take(_prolongId)).cancel();
-		_prolongId = _session->api().request(MTPmessages_ProlongWebView(
-			MTP_flags(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),
-			action.mtpReplyTo(),
-			(action.options.sendAs
-				? action.options.sendAs->input
-				: MTP_inputPeerEmpty())
-		)).done([=] {
-			_prolongId = 0;
-		}).send();
-	}, _panel->lifetime());
-}
-
-std::shared_ptr<Main::SessionShow> AttachWebView::uiShow() {
-	class Show final : public Main::SessionShow {
-	public:
-		explicit Show(not_null<AttachWebView*> that) : _that(that) {
-		}
-
-		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 {
-			using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
-			using ObjectBox = object_ptr<Ui::BoxContent>;
-			const auto panel = _that ? _that->_panel.get() : nullptr;
-			if (v::is<UniqueLayer>(layer)) {
-				Unexpected("Layers in AttachWebView are not implemented.");
-			} else if (auto box = std::get_if<ObjectBox>(&layer)) {
-				if (panel) {
-					panel->showBox(std::move(*box), options, animated);
-				}
-			} else if (panel) {
-				panel->hideLayer(animated);
-			}
-		}
-		[[nodiscard]] not_null<QWidget*> toastParent() const override {
-			const auto panel = _that ? _that->_panel.get() : nullptr;
-
-			Ensures(panel != nullptr);
-			return panel->toastParent();
-		}
-		[[nodiscard]] bool valid() const override {
-			return _that && (_that->_panel != nullptr);
-		}
-		operator bool() const override {
-			return valid();
-		}
-
-		[[nodiscard]] Main::Session &session() const override {
-			Expects(_that.get() != nullptr);
-			return *_that->_session;
-		}
-
-	private:
-		const base::weak_ptr<AttachWebView> _that;
-
-	};
-	return std::make_shared<Show>(this);
-}
-
-void AttachWebView::showToast(
-		const QString &text,
-		Window::SessionController *controller) {
-	const auto strong = controller
-		? controller
-		: _context
-		? _context->controller.get()
-		: _addToMenuContext
-		? _addToMenuContext->controller.get()
-		: nullptr;
-	if (strong) {
-		strong->showToast(text);
-	}
-}
-
 void AttachWebView::confirmAddToMenu(
 		AttachWebViewBot bot,
-		Fn<void()> callback) {
+		Fn<void(bool added)> callback) {
 	const auto active = Core::App().activeWindow();
 	if (!active) {
+		if (callback) {
+			callback(false);
+		}
 		return;
 	}
-	_confirmAddBox = active->show(Box([=](not_null<Ui::GenericBox*> box) {
+	const auto weak = base::make_weak(active);
+	active->show(Box([=](not_null<Ui::GenericBox*> box) {
 		const auto allowed = std::make_shared<Ui::Checkbox*>();
 		const auto disclaimer = !disclaimerAccepted(bot);
 		const auto done = [=](Fn<void()> close) {
@@ -1749,21 +1680,27 @@ void AttachWebView::confirmAddToMenu(
 				|| ((*allowed) && (*allowed)->checked()))
 				? ToggledState::AllowedToWrite
 				: ToggledState::Added;
-			toggleInMenu(bot.user, state, [=] {
+			toggleInMenu(bot.user, state, [=](bool success) {
 				if (callback) {
-					callback();
+					callback(success);
+				}
+				if (const auto strong = weak.get()) {
+					strong->showToast((bot.inMainMenu
+						? tr::lng_bot_add_to_side_menu_done
+						: tr::lng_bot_add_to_menu_done)(tr::now));
 				}
-				showToast((bot.inMainMenu
-					? tr::lng_bot_add_to_side_menu_done
-					: tr::lng_bot_add_to_menu_done)(tr::now));
 			});
 			close();
 		};
 		if (disclaimer) {
-			FillDisclaimerBox(box, [=] {
-				_disclaimerAccepted.emplace(bot.user);
-				_attachBotsUpdates.fire({});
-				done([] {});
+			FillDisclaimerBox(box, [=](bool accepted) {
+				if (accepted) {
+					_disclaimerAccepted.emplace(bot.user);
+					_attachBotsUpdates.fire({});
+					done([] {});
+				} else if (callback) {
+					callback(false);
+				}
 			});
 			box->addRow(object_ptr<Ui::FixedHeightWidget>(
 				box,
@@ -1785,6 +1722,9 @@ void AttachWebView::confirmAddToMenu(
 						Ui::Text::Bold(bot.name),
 						Ui::Text::WithEntities),
 				done,
+				(callback
+					? [=](Fn<void()> close) { callback(false); close(); }
+					: Fn<void(Fn<void()>)>()),
 			});
 			if (bot.requestWriteAccess) {
 				(*allowed) = box->addRow(
@@ -1813,7 +1753,7 @@ void AttachWebView::confirmAddToMenu(
 void AttachWebView::toggleInMenu(
 		not_null<UserData*> bot,
 		ToggledState state,
-		Fn<void()> callback) {
+		Fn<void(bool success)> callback) {
 	using Flag = MTPmessages_ToggleBotInAttachMenu::Flag;
 	_session->api().request(MTPmessages_ToggleBotInAttachMenu(
 		MTP_flags((state == ToggledState::AllowedToWrite)
@@ -1824,9 +1764,12 @@ void AttachWebView::toggleInMenu(
 	)).done([=] {
 		_requestId = 0;
 		_session->api().request(base::take(_botsRequestId)).cancel();
-		requestBots(std::move(callback));
+		requestBots(callback ? [=] { callback(true); } : Fn<void()>());
 	}).fail([=] {
 		cancel();
+		if (callback) {
+			callback(false);
+		}
 	}).send();
 }
 
@@ -1923,14 +1866,18 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
 			continue;
 		}
 		const auto callback = [=] {
-			bots->request(
-				controller,
-				actionFactory(),
-				bot.user,
-				{ .fromAttachMenu = true });
+			bots->open({
+				.bot = bot.user,
+				.context = {
+					.controller = controller,
+					.action = actionFactory(),
+				},
+				.source = InlineBots::WebViewSourceAttachMenu(),
+			});
 		};
 		auto action = base::make_unique_q<BotAction>(
 			raw,
+			controller->uiShow(),
 			raw->menu()->st(),
 			bot,
 			callback);
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
index f9f59f1ae..6e11c87cd 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
@@ -10,15 +10,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/flags.h"
 #include "base/timer.h"
 #include "base/weak_ptr.h"
+#include "dialogs/dialogs_key.h"
+#include "api/api_common.h"
 #include "mtproto/sender.h"
 #include "ui/chat/attach/attach_bot_webview.h"
 #include "ui/rp_widget.h"
 
-namespace Api {
-struct SendAction;
-} // namespace Api
+namespace Data {
+class Thread;
+} // namespace Data
 
 namespace Ui {
+class Show;
 class GenericBox;
 class DropdownMenu;
 } // namespace Ui
@@ -47,6 +50,8 @@ enum class CheckoutResult;
 
 namespace InlineBots {
 
+class WebViewInstance;
+
 enum class PeerType : uint8 {
 	SameBot   = 0x01,
 	Bot       = 0x02,
@@ -86,95 +91,176 @@ struct AddToMenuOpenApp {
 	not_null<BotAppData*> app;
 	QString startCommand;
 };
-using AddToMenuOpen = std::variant<
+struct AddToMenuOpen : std::variant<
 	AddToMenuOpenAttach,
 	AddToMenuOpenMenu,
-	AddToMenuOpenApp>;
+	AddToMenuOpenApp> {
+	using variant::variant;
+};
 
-class AttachWebView final
+struct WebViewSourceButton {
+	bool simple = false;
+
+	friend inline bool operator==(
+		WebViewSourceButton,
+		WebViewSourceButton) = default;
+};
+
+struct WebViewSourceSwitch {
+	friend inline bool operator==(
+		const WebViewSourceSwitch &,
+		const WebViewSourceSwitch &) = default;
+};
+
+struct WebViewSourceLinkApp { // t.me/botusername/appname
+	base::weak_ptr<WebViewInstance> from;
+	QString appname;
+	QString token;
+
+	friend inline bool operator==(
+		const WebViewSourceLinkApp &,
+		const WebViewSourceLinkApp &) = default;
+};
+
+struct WebViewSourceLinkAttachMenu { // ?startattach
+	base::weak_ptr<WebViewInstance> from;
+	base::weak_ptr<Data::Thread> thread;
+	PeerTypes choose;
+	QString token;
+
+	friend inline bool operator==(
+		const WebViewSourceLinkAttachMenu &,
+		const WebViewSourceLinkAttachMenu &) = default;
+};
+
+struct WebViewSourceLinkBotProfile { // t.me/botusername?startapp
+	base::weak_ptr<WebViewInstance> from;
+	QString token;
+	bool compact = false;
+
+	friend inline bool operator==(
+		const WebViewSourceLinkBotProfile &,
+		const WebViewSourceLinkBotProfile &) = default;
+};
+
+struct WebViewSourceMainMenu {
+	friend inline bool operator==(
+		WebViewSourceMainMenu,
+		WebViewSourceMainMenu) = default;
+};
+
+struct WebViewSourceAttachMenu {
+	base::weak_ptr<Data::Thread> thread;
+
+	friend inline bool operator==(
+		const WebViewSourceAttachMenu &,
+		const WebViewSourceAttachMenu &) = default;
+};
+
+struct WebViewSourceBotMenu {
+	friend inline bool operator==(
+		WebViewSourceBotMenu,
+		WebViewSourceBotMenu) = default;
+};
+
+struct WebViewSourceGame {
+	FullMsgId messageId;
+	QString title;
+
+	friend inline bool operator==(
+		WebViewSourceGame,
+		WebViewSourceGame) = default;
+};
+
+struct WebViewSourceBotProfile {
+	friend inline bool operator==(
+		WebViewSourceBotProfile,
+		WebViewSourceBotProfile) = default;
+};
+
+struct WebViewSource : std::variant<
+	WebViewSourceButton,
+	WebViewSourceSwitch,
+	WebViewSourceLinkApp,
+	WebViewSourceLinkAttachMenu,
+	WebViewSourceLinkBotProfile,
+	WebViewSourceMainMenu,
+	WebViewSourceAttachMenu,
+	WebViewSourceBotMenu,
+	WebViewSourceGame,
+	WebViewSourceBotProfile> {
+	using variant::variant;
+};
+
+struct WebViewButton {
+	QString text;
+	QString startCommand;
+	QByteArray url;
+	bool fromAttachMenu = false;
+	bool fromMainMenu = false;
+	bool fromSwitch = false;
+};
+
+struct WebViewContext {
+	base::weak_ptr<Window::SessionController> controller;
+	Dialogs::EntryState dialogsEntryState;
+	std::optional<Api::SendAction> action;
+	bool maySkipConfirmation = false;
+};
+
+struct WebViewDescriptor {
+	not_null<UserData*> bot;
+	std::shared_ptr<Ui::Show> parentShow;
+	WebViewContext context;
+	WebViewButton button;
+	WebViewSource source;
+};
+
+class WebViewInstance final
 	: public base::has_weak_ptr
 	, public Ui::BotWebView::Delegate {
 public:
-	explicit AttachWebView(not_null<Main::Session*> session);
-	~AttachWebView();
+	explicit WebViewInstance(WebViewDescriptor &&descriptor);
+	~WebViewInstance();
 
-	struct WebViewButton {
-		QString text;
-		QString startCommand;
-		QByteArray url;
-		bool fromAttachMenu = false;
-		bool fromMainMenu = false;
-		bool fromSwitch = false;
-	};
-	void request(
-		not_null<Window::SessionController*> controller,
-		const Api::SendAction &action,
-		const QString &botUsername,
-		const QString &startCommand);
-	void request(
-		not_null<Window::SessionController*> controller,
-		const Api::SendAction &action,
-		not_null<UserData*> bot,
-		const WebViewButton &button);
-	void requestSimple(
-		not_null<Window::SessionController*> controller,
-		not_null<UserData*> bot,
-		const WebViewButton &button);
-	void requestMenu(
-		not_null<Window::SessionController*> controller,
-		not_null<UserData*> bot);
-	void requestApp(
-		not_null<Window::SessionController*> controller,
-		const Api::SendAction &action,
-		not_null<UserData*> bot,
-		const QString &appName,
-		const QString &startParam,
-		bool forceConfirmation);
+	[[nodiscard]] Main::Session &session() const;
+	[[nodiscard]] not_null<UserData*> bot() const;
+	[[nodiscard]] WebViewSource source() const;
 
-	void cancel();
-
-	void requestBots(Fn<void()> callback = nullptr);
-	[[nodiscard]] const std::vector<AttachWebViewBot> &attachBots() const {
-		return _attachBots;
-	}
-	[[nodiscard]] rpl::producer<> attachBotsUpdates() const {
-		return _attachBotsUpdates.events();
-	}
-	void notifyBotIconLoaded() {
-		_attachBotsUpdates.fire({});
-	}
-	[[nodiscard]] bool disclaimerAccepted(
-		const AttachWebViewBot &bot) const;
-	[[nodiscard]] bool showMainMenuNewBadge(
-		const AttachWebViewBot &bot) const;
-
-	void requestAddToMenu(
-		not_null<UserData*> bot,
-		AddToMenuOpen open);
-	void requestAddToMenu(
-		not_null<UserData*> bot,
-		AddToMenuOpen open,
-		Window::SessionController *controller,
-		std::optional<Api::SendAction> action);
-	void removeFromMenu(not_null<UserData*> bot);
-
-	[[nodiscard]] std::optional<Api::SendAction> lookupLastAction(
-		const QString &url) const;
-
-	struct ShowGameParams {
-		not_null<UserData*> bot;
-		FullMsgId context;
-		QString url;
-		QString title;
-	};
-	void showGame(ShowGameParams &&params);
+	void activate();
+	void close();
 
 	[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();
 
-	static void ClearAll();
-
 private:
-	struct Context;
+	void resolve();
+
+	bool openAppFromBotMenuLink();
+
+	void requestButton();
+	void requestSimple();
+	void requestApp(bool allowWrite);
+	void requestWithMainMenuDisclaimer();
+	void requestWithMenuAdd();
+	void maybeChooseAndRequestButton(PeerTypes supported);
+
+	void resolveApp(
+		const QString &appname,
+		const QString &startparam,
+		bool forceConfirmation);
+	void confirmOpen(Fn<void()> done);
+	void confirmAppOpen(bool writeAccess, Fn<void(bool allowWrite)> done);
+
+	void show(const QString &url, uint64 queryId = 0);
+	void showGame();
+	void started(uint64 queryId);
+
+	[[nodiscard]] Window::SessionController *windowForThread(
+		not_null<Data::Thread*> thread);
+
+	auto nonPanelPaymentFormFactory(
+		Fn<void(Payments::CheckoutResult)> reactivate)
+	-> Fn<void(Payments::NonPanelPaymentForm)>;
 
 	Webview::ThemeParams botThemeParams() override;
 	bool botHandleLocalUri(QString uri, bool keepOpen) override;
@@ -194,35 +280,81 @@ private:
 	void botShareGameScore() override;
 	void botClose() override;
 
-	[[nodiscard]] static Context LookupContext(
-		not_null<Window::SessionController*> controller,
-		const Api::SendAction &action);
-	[[nodiscard]] static bool IsSame(
-		const std::unique_ptr<Context> &a,
-		const Context &b);
+	const std::shared_ptr<Ui::Show> _parentShow;
+	const not_null<Main::Session*> _session;
+	const not_null<UserData*> _bot;
+	const WebViewContext _context;
+	const WebViewButton _button;
+	const WebViewSource _source;
 
-	bool openAppFromMenuLink(
+	BotAppData *_app = nullptr;
+	QString _appStartParam;
+	bool _dataSent = false;
+
+	mtpRequestId _requestId = 0;
+	mtpRequestId _prolongId = 0;
+
+	QString _panelUrl;
+	std::unique_ptr<Ui::BotWebView::Panel> _panel;
+
+	static base::weak_ptr<WebViewInstance> PendingActivation;
+
+};
+
+class AttachWebView final : public base::has_weak_ptr {
+public:
+	explicit AttachWebView(not_null<Main::Session*> session);
+	~AttachWebView();
+
+	void open(WebViewDescriptor &&descriptor);
+	void openByUsername(
 		not_null<Window::SessionController*> controller,
+		const Api::SendAction &action,
+		const QString &botUsername,
+		const QString &startCommand);
+
+	void cancel();
+
+	void requestBots(Fn<void()> callback = nullptr);
+	[[nodiscard]] const std::vector<AttachWebViewBot> &attachBots() const {
+		return _attachBots;
+	}
+	[[nodiscard]] rpl::producer<> attachBotsUpdates() const {
+		return _attachBotsUpdates.events();
+	}
+	void notifyBotIconLoaded() {
+		_attachBotsUpdates.fire({});
+	}
+	[[nodiscard]] bool disclaimerAccepted(
+		const AttachWebViewBot &bot) const;
+	[[nodiscard]] bool showMainMenuNewBadge(
+		const AttachWebViewBot &bot) const;
+
+	void removeFromMenu(
+		std::shared_ptr<Ui::Show> show,
 		not_null<UserData*> bot);
-	void requestWithOptionalConfirm(
+
+	enum class AddToMenuResult {
+		AlreadyInMenu,
+		Added,
+		Unsupported,
+		Cancelled,
+	};
+	void requestAddToMenu(
 		not_null<UserData*> bot,
-		const WebViewButton &button,
-		const Context &context,
-		Window::SessionController *controllerForConfirm = nullptr);
-
-	void resolve();
-	void request(const WebViewButton &button);
-	void requestSimple(const WebViewButton &button);
-	void resolveUsername(
-		const QString &username,
-		Fn<void(not_null<PeerData*>)> done);
-
-	void confirmOpen(
-		not_null<Window::SessionController*> controller,
-		Fn<void()> done);
+		Fn<void(AddToMenuResult, PeerTypes supported)> done);
 	void acceptMainMenuDisclaimer(
-		not_null<Window::SessionController*> controller,
-		const WebViewButton &button);
+		std::shared_ptr<Ui::Show> show,
+		not_null<UserData*> bot,
+		Fn<void(AddToMenuResult, PeerTypes supported)> done);
+
+	void close(not_null<WebViewInstance*> instance);
+	void closeAll();
+
+private:
+	void resolveUsername(
+		std::shared_ptr<Ui::Show> show,
+		Fn<void(not_null<PeerData*>)> done);
 
 	enum class ToggledState {
 		Removed,
@@ -232,67 +364,35 @@ private:
 	void toggleInMenu(
 		not_null<UserData*> bot,
 		ToggledState state,
-		Fn<void()> callback = nullptr);
-
-	void show(
-		uint64 queryId,
-		const QString &url,
-		const QString &buttonText = QString(),
-		bool allowClipboardRead = false,
-		const BotAppData *app = nullptr,
-		bool fromMainMenu = false);
+		Fn<void(bool success)> callback = nullptr);
 	void confirmAddToMenu(
 		AttachWebViewBot bot,
-		Fn<void()> callback = nullptr);
-	void confirmAppOpen(bool requestWriteAccess);
-	void requestAppView(bool allowWrite);
-	void started(uint64 queryId);
-
-	void showToast(
-		const QString &text,
-		Window::SessionController *controller = nullptr);
-	Fn<void(Payments::NonPanelPaymentForm)> nonPanelPaymentFormFactory(
-		Fn<void(Payments::CheckoutResult)> reactivate);
+		Fn<void(bool added)> callback = nullptr);
 
 	const not_null<Main::Session*> _session;
 
 	base::Timer _refreshTimer;
 
-	std::unique_ptr<Context> _context;
-	std::unique_ptr<Context> _lastShownContext;
-	QString _lastShownUrl;
-	uint64 _lastShownQueryId = 0;
-	QString _lastShownButtonText;
-	UserData *_bot = nullptr;
 	QString _botUsername;
-	QString _botAppName;
 	QString _startCommand;
-	BotAppData *_app = nullptr;
-	QPointer<Ui::GenericBox> _confirmAddBox;
-	bool _appConfirmationRequired = false;
-	bool _appRequestWriteAccess = false;
 
 	mtpRequestId _requestId = 0;
-	mtpRequestId _prolongId = 0;
 
 	uint64 _botsHash = 0;
 	mtpRequestId _botsRequestId = 0;
 	std::vector<Fn<void()>> _botsRequestCallbacks;
 
-	std::unique_ptr<Context> _addToMenuContext;
-	UserData *_addToMenuBot = nullptr;
-	mtpRequestId _addToMenuId = 0;
-	AddToMenuOpen _addToMenuOpen;
-	base::weak_ptr<Window::SessionController> _addToMenuChooseController;
+	struct AddToMenuProcess {
+		mtpRequestId requestId = 0;
+		std::vector<Fn<void(AddToMenuResult, PeerTypes supported)>> done;
+	};
+	base::flat_map<not_null<UserData*>, AddToMenuProcess> _addToMenu;
 
 	std::vector<AttachWebViewBot> _attachBots;
 	rpl::event_stream<> _attachBotsUpdates;
 	base::flat_set<not_null<UserData*>> _disclaimerAccepted;
 
-	FullMsgId _gameContext;
-
-	std::unique_ptr<Ui::BotWebView::Panel> _panel;
-	bool _catchingCancelInShowCall = false;
+	std::vector<std::unique_ptr<WebViewInstance>> _instances;
 
 };
 
diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp
index 10c6236bf..2418c0f8a 100644
--- a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp
@@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/path_shift_gradient.h"
 #include "ui/painter.h"
 #include "history/view/history_view_cursor_state.h"
+#include "history/history.h"
 #include "styles/style_chat_helpers.h"
 #include "styles/style_menu_icons.h"
 
@@ -677,10 +678,13 @@ void Inner::switchPm() {
 	if (!_inlineBot || !_inlineBot->isBot()) {
 		return;
 	} else if (!_switchPmUrl.isEmpty()) {
-		_inlineBot->session().attachWebView().requestSimple(
-			_controller,
-			_inlineBot,
-			{ .url = _switchPmUrl, .fromSwitch = true });
+		const auto bot = _inlineBot;
+		_inlineBot->session().attachWebView().open({
+			.bot = bot,
+			.context = { .controller = _controller },
+			.button = { .url = _switchPmUrl },
+			.source = InlineBots::WebViewSourceSwitch(),
+		});
 	} else {
 		_inlineBot->botInfo->startToken = _switchPmStartToken;
 		_inlineBot->botInfo->inlineReturnTo
diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp
index df1f2ef25..0cfdeece2 100644
--- a/Telegram/SourceFiles/mainwindow.cpp
+++ b/Telegram/SourceFiles/mainwindow.cpp
@@ -193,7 +193,7 @@ void MainWindow::setupPasscodeLock() {
 		setInnerFocus();
 	}
 	if (const auto sessionController = controller().sessionController()) {
-		sessionController->session().attachWebView().cancel();
+		sessionController->session().attachWebView().closeAll();
 	}
 }
 
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index ae27e4938..6a50d2590 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -1282,6 +1282,27 @@ void Panel::showBox(
 			}
 		}
 	}
+	const auto raw = box.data();
+
+	InvokeQueued(raw, [=] {
+		if (raw->window()->isActiveWindow()) {
+			// In case focus is somewhat in a native child window,
+			// like a webview, Qt glitches here with input fields showing
+			// focused state, but not receiving any keyboard input:
+			//
+			// window()->windowHandle()->isActive() == false.
+			//
+			// Steps were: SeparatePanel with a WebView2 child,
+			// some interaction with mouse inside the WebView2,
+			// so that WebView2 gets focus and active window state,
+			// then we call setSearchAllowed() and after animation
+			// is finished try typing -> nothing happens.
+			//
+			// With this workaround it works fine.
+			_widget->activateWindow();
+		}
+	});
+
 	_widget->showBox(
 		std::move(box),
 		LayerOption::KeepOther,
diff --git a/Telegram/SourceFiles/window/window_main_menu_helpers.cpp b/Telegram/SourceFiles/window/window_main_menu_helpers.cpp
index b70fb833a..629b6cf16 100644
--- a/Telegram/SourceFiles/window/window_main_menu_helpers.cpp
+++ b/Telegram/SourceFiles/window/window_main_menu_helpers.cpp
@@ -367,12 +367,15 @@ void SetupMenuBots(
 					(height - icon->height()) / 2);
 			}, button->lifetime());
 			const auto weak = Ui::MakeWeak(container);
+			const auto show = controller->uiShow();
 			button->setAcceptBoth(true);
 			button->clicks(
 			) | rpl::start_with_next([=](Qt::MouseButton which) {
 				if (which == Qt::LeftButton) {
-					bots->requestSimple(controller, user, {
-						.fromMainMenu = true,
+					bots->open({
+						.bot = user,
+						.context = { .controller = controller },
+						.source = InlineBots::WebViewSourceMainMenu(),
 					});
 					if (weak) {
 						controller->window().hideSettingsAndLayer();
@@ -384,7 +387,7 @@ void SetupMenuBots(
 						st::popupMenuWithIcons);
 					(*menu)->addAction(
 						tr::lng_bot_remove_from_menu(tr::now),
-						[=] { bots->removeFromMenu(user); },
+						[=] { bots->removeFromMenu(show, user); },
 						&st::menuIconDelete);
 					(*menu)->popup(QCursor::pos());
 				}
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index 8380e458a..ebd403da4 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -614,17 +614,23 @@ void SessionNavigation::showPeerByLinkResolved(
 		const auto contextPeer = item
 			? item->history()->peer
 			: bot;
-		const auto action = bot->session().attachWebView().lookupLastAction(
-			info.clickFromAttachBotWebviewUrl
-		).value_or(Api::SendAction(bot->owner().history(contextPeer)));
+		const auto action = info.clickFromBotWebviewContext
+			? info.clickFromBotWebviewContext->action
+			: Api::SendAction(bot->owner().history(contextPeer));
 		crl::on_main(this, [=] {
-			bot->session().attachWebView().requestApp(
-				parentController(),
-				action,
-				bot,
-				info.botAppName,
-				info.startToken,
-				info.botAppForceConfirmation);
+			bot->session().attachWebView().open({
+				.bot = bot,
+				.context = {
+					.controller = parentController(),
+					.action = action,
+					.maySkipConfirmation = !info.botAppForceConfirmation,
+				},
+				.button = { .startCommand = info.startToken },
+				.source = InlineBots::WebViewSourceLinkApp{
+					.appname = info.botAppName,
+					.token = info.startToken,
+				},
+			});
 		});
 	} else if (bot && resolveType == ResolveType::ShareGame) {
 		Window::ShowShareGameBox(parentController(), bot, info.startToken);
@@ -672,20 +678,25 @@ void SessionNavigation::showPeerByLinkResolved(
 			crl::on_main(this, [=] {
 				const auto history = peer->owner().history(peer);
 				showPeerHistory(history, params, msgId);
-				peer->session().attachWebView().request(
+
+				peer->session().attachWebView().openByUsername(
 					parentController(),
 					Api::SendAction(history),
 					attachBotUsername,
 					info.attachBotToggleCommand.value_or(QString()));
 			});
-		} else if (bot && info.attachBotMenuOpen) {
+		} else if (bot && info.attachBotMainOpen) {
 			const auto startCommand = info.attachBotToggleCommand.value_or(
 				QString());
-			bot->session().attachWebView().requestAddToMenu(
-				bot,
-				InlineBots::AddToMenuOpenMenu{ startCommand },
-				parentController(),
-				std::optional<Api::SendAction>());
+			bot->session().attachWebView().open({
+				.bot = bot,
+				.context = { .controller = parentController() },
+				.button = { .startCommand = startCommand },
+				.source = InlineBots::WebViewSourceLinkBotProfile{
+					.token = startCommand,
+					.compact = info.attachBotMainCompact,
+				},
+			});
 		} else if (bot && info.attachBotToggleCommand) {
 			const auto itemId = info.clickFromMessageId;
 			const auto item = _session->data().message(itemId);
@@ -695,17 +706,21 @@ void SessionNavigation::showPeerByLinkResolved(
 			const auto contextUser = contextPeer
 				? contextPeer->asUser()
 				: nullptr;
-			bot->session().attachWebView().requestAddToMenu(
-				bot,
-				InlineBots::AddToMenuOpenAttach{
-					.startCommand = *info.attachBotToggleCommand,
-					.chooseTypes = info.attachBotChooseTypes,
+			bot->session().attachWebView().open({
+				.bot = bot,
+				.context = {
+					.controller = parentController(),
+					.action = (contextUser
+						? Api::SendAction(
+							contextUser->owner().history(contextUser))
+						: std::optional<Api::SendAction>()),
 				},
-				parentController(),
-				(contextUser
-					? Api::SendAction(
-						contextUser->owner().history(contextUser))
-					: std::optional<Api::SendAction>()));
+				.button = { .startCommand = *info.attachBotToggleCommand },
+				.source = InlineBots::WebViewSourceLinkAttachMenu{
+					.choose = info.attachBotChooseTypes,
+					.token = *info.attachBotToggleCommand,
+				},
+			});
 		} else {
 			const auto draft = info.text;
 			crl::on_main(this, [=] {
diff --git a/Telegram/SourceFiles/window/window_session_controller_link_info.h b/Telegram/SourceFiles/window/window_session_controller_link_info.h
index 2c7457122..74a783696 100644
--- a/Telegram/SourceFiles/window/window_session_controller_link_info.h
+++ b/Telegram/SourceFiles/window/window_session_controller_link_info.h
@@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+namespace InlineBots {
+struct WebViewContext;
+} // namespace InlineBots
+
 namespace Window {
 
 enum class ResolveType {
@@ -45,11 +49,12 @@ struct PeerByLinkInfo {
 	bool botAppForceConfirmation = false;
 	QString attachBotUsername;
 	std::optional<QString> attachBotToggleCommand;
-	bool attachBotMenuOpen = false;
+	bool attachBotMainOpen = false;
+	bool attachBotMainCompact = false;
 	InlineBots::PeerTypes attachBotChooseTypes;
 	std::optional<QString> voicechatHash;
 	FullMsgId clickFromMessageId;
-	QString clickFromAttachBotWebviewUrl;
+	std::shared_ptr<InlineBots::WebViewContext> clickFromBotWebviewContext;
 };
 
 } // namespace Window

From c70866a9956ae5ab2612e10637e7427c0ae29245 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 19 Jul 2024 17:42:08 +0200
Subject: [PATCH 072/163] Fix child native window focus.

---
 Telegram/SourceFiles/iv/iv_controller.cpp         | 15 ++++++++-------
 .../ui/chat/attach/attach_bot_webview.cpp         | 13 ++++++-------
 2 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index e72298f6c..b3fb82a0d 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/platform/base_platform_info.h"
 #include "base/invoke_queued.h"
+#include "base/qt_signal_producer.h"
 #include "base/qthelp_url.h"
 #include "iv/iv_data.h"
 #include "lang/lang_keys.h"
@@ -35,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include <QtCore/QJsonObject>
 #include <QtCore/QJsonValue>
 #include <QtCore/QFile>
+#include <QtGui/QGuiApplication>
 #include <QtGui/QPainter>
 #include <QtGui/QWindow>
 #include <charconv>
@@ -280,14 +282,13 @@ void Controller::createWindow() {
 	_window = std::make_unique<Ui::RpWindow>();
 	const auto window = _window.get();
 
-	window->windowActiveValue(
-	) | rpl::map([=] {
+	base::qt_signal_producer(
+		qApp,
+		&QGuiApplication::focusWindowChanged
+	) | rpl::filter([=](QWindow *focused) {
 		const auto handle = window->window()->windowHandle();
-		return (_shareFocus || _webview) && handle && handle->isActive();
-	}) | rpl::distinct_until_changed(
-	) | rpl::filter(
-		rpl::mappers::_1
-	) | rpl::start_with_next([=] {
+		return _webview && handle && (focused == handle);
+	}) | rpl::start_with_next([=] {
 		setInnerFocus();
 	}, window->lifetime());
 
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index 6a50d2590..a8dee5f76 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -741,17 +741,16 @@ postEvent: function(eventType, eventData) {
 
 	setupProgressGeometry();
 
-	_widget->windowActiveValue(
-	) | rpl::map([=] {
+	base::qt_signal_producer(
+		qApp,
+		&QGuiApplication::focusWindowChanged
+	) | rpl::filter([=](QWindow *focused) {
 		const auto handle = _widget->window()->windowHandle();
 		return _webview
 			&& !_webview->window.widget()->isHidden()
 			&& handle
-			&& handle->isActive();
-	}) | rpl::distinct_until_changed(
-	) | rpl::filter(
-		rpl::mappers::_1
-	) | rpl::start_with_next([=] {
+			&& (focused == handle);
+	}) | rpl::start_with_next([=] {
 		_webview->window.focus();
 	}, _webview->lifetime);
 

From 1ebe3255e0a584a014b8ef890e38cf7eedf66363 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 16:07:43 +0200
Subject: [PATCH 073/163] Fix share focus in IV.

---
 Telegram/SourceFiles/iv/iv_instance.cpp | 29 ++++++++++++++++++++++---
 1 file changed, 26 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp
index d070c93d2..a49ba8887 100644
--- a/Telegram/SourceFiles/iv/iv_instance.cpp
+++ b/Telegram/SourceFiles/iv/iv_instance.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "apiwrap.h"
 #include "base/platform/base_platform_info.h"
+#include "base/qt_signal_producer.h"
 #include "boxes/share_box.h"
 #include "core/application.h"
 #include "core/file_utilities.h"
@@ -49,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "window/window_session_controller_link_info.h"
 
 #include <QtGui/QGuiApplication>
+#include <QtGui/QWindow>
 
 namespace Iv {
 namespace {
@@ -298,12 +300,33 @@ ShareBoxResult Shown::shareBox(ShareBoxDescriptor &&descriptor) {
 		state->destroyRequests.fire({});
 	}, wrap->lifetime());
 
+	const auto waiting = layer->lifetime().make_state<rpl::lifetime>();
 	const auto focus = crl::guard(layer, [=] {
-		if (!layer->window()->isActiveWindow()) {
-			layer->window()->activateWindow();
+		const auto set = [=] {
 			layer->window()->setFocus();
+			layer->setInnerFocus();
+		};
+
+		const auto handle = layer->window()->windowHandle();
+		if (!handle) {
+			waiting->destroy();
+			return;
+		} else if (QGuiApplication::focusWindow() == handle) {
+			waiting->destroy();
+			set();
+		} else {
+			*waiting = base::qt_signal_producer(
+				qApp,
+				&QGuiApplication::focusWindowChanged
+			) | rpl::filter([=](QWindow *focused) {
+				const auto handle = layer->window()->windowHandle();
+				return handle && (focused == handle);
+			}) | rpl::start_with_next([=] {
+				waiting->destroy();
+				set();
+			});
+			layer->window()->activateWindow();
 		}
-		layer->setInnerFocus();
 	});
 	auto result = ShareBoxResult{
 		.focus = focus,

From 677fbdd84edb93a6bbb7f4241725abbf7e179d17 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 17:46:39 +0300
Subject: [PATCH 074/163] Fix webrtc camera usage on Linux.

---
 Telegram/ThirdParty/tgcalls | 2 +-
 Telegram/lib_webrtc         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index ce592ecab..358a7b0ec 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit ce592ecab26770fecca683a05f4611d1c00113ef
+Subproject commit 358a7b0ec0e22782b4833b9c0add465133d11632
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index 5d44a8acc..1f0b2531d 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit 5d44a8acc341d5f45c4150f07929484d15b9c5ba
+Subproject commit 1f0b2531d54a881e9a9f0818ce08f2e403917784

From 889ec0c731aea7e234fdf6e2648ef5edddad3c4d Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 18:25:33 +0300
Subject: [PATCH 075/163] Replace \r\n with \n on paste.

Fixes #28181.
---
 Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp | 1 -
 Telegram/lib_ui                                          | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index 8e993f4f7..27e37053a 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -1305,7 +1305,6 @@ void WebViewInstance::botAllowWriteAccess(Fn<void(bool allowed)> callback) {
 }
 
 void WebViewInstance::botSharePhone(Fn<void(bool shared)> callback) {
-	const auto bot = _bot;
 	const auto history = _bot->owner().history(_bot);
 	if (_bot->isBlocked()) {
 		const auto done = crl::guard(this, [=](bool success) {
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 007e3a519..7dd187eee 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 007e3a519ffbad0bd889de6cb4b19f34959afbce
+Subproject commit 7dd187eeeb46b68dadc5752c3f3fc34e5e1d7e94

From c81f406759cc3693f2104bb80e7c0e8f48a956ba Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 18:04:36 +0200
Subject: [PATCH 076/163] Use xz repository from GitHub.

Fixes #28189.
---
 Telegram/build/docker/centos_env/Dockerfile | 2 +-
 Telegram/build/prepare/prepare.py           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 0091881ed..463472041 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -61,7 +61,7 @@ RUN git init zlib \
 	&& rm -rf zlib
 
 FROM builder AS xz
-RUN git clone -b v5.4.4 --depth=1 https://git.tukaani.org/xz.git \
+RUN git clone -b v5.4.4 --depth=1 https://github.com/tukaani-project/xz.git \
 	&& cd xz \
 	&& cmake -B build . -DCMAKE_BUILD_TYPE=None \
 	&& cmake --build build -j$(nproc) \
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index ea305575e..f5a64b413 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -540,7 +540,7 @@ release:
 
 stage('xz', """
 !win:
-    git clone -b v5.4.5 https://git.tukaani.org/xz.git
+    git clone -b v5.4.5 https://github.com/tukaani-project/xz.git
     cd xz
     sed -i '' '\\@check_symbol_exists(futimens "sys/types.h;sys/stat.h" HAVE_FUTIMENS)@d' CMakeLists.txt
     CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake -B build . \\

From 1ef6f462f623b77181c794c4f3489b626553d24f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 18:05:25 +0200
Subject: [PATCH 077/163] Preserve link preview settings on reschedule.

---
 Telegram/SourceFiles/api/api_editing.cpp                      | 4 +---
 .../history/view/controls/history_view_webpage_processor.cpp  | 1 +
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp
index f326275fd..d5f76bf86 100644
--- a/Telegram/SourceFiles/api/api_editing.cpp
+++ b/Telegram/SourceFiles/api/api_editing.cpp
@@ -153,9 +153,7 @@ mtpRequestId EditMessage(
 	const auto &text = item->originalText();
 	const auto webpage = (!item->media() || !item->media()->webpage())
 		? Data::WebPageDraft{ .removed = true }
-		: Data::WebPageDraft{
-			.id = item->media()->webpage()->id,
-		};
+		: Data::WebPageDraft::FromItem(item);
 	return EditMessage(
 		item,
 		text,
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp
index acf09d845..214de059c 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp
@@ -257,6 +257,7 @@ void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
 	const auto was = _link;
 	if (draft.removed) {
 		_draft = draft;
+		_parsedLinks = _parser.list().current();
 		if (_parsedLinks.empty()) {
 			_draft.removed = false;
 		}

From ac78ae823c0e7865d69b6cbe133648978f0232fd Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Mon, 22 Jul 2024 02:12:39 +0400
Subject: [PATCH 078/163] Reduce portal autostart dialog modality to parent
 window

---
 .../platform/linux/specific_linux.cpp         | 28 +++++++++++++++----
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
index 6d8a8ec54..32437fb77 100644
--- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
@@ -18,8 +18,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "storage/localstorage.h"
 #include "core/launcher.h"
 #include "core/sandbox.h"
+#include "core/application.h"
 #include "core/core_settings.h"
 #include "core/update_checker.h"
+#include "window/window_controller.h"
 #include "webview/platform/linux/webview_linux_webkitgtk.h"
 
 #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
@@ -92,10 +94,23 @@ void PortalAutostart(bool enabled, Fn<void(bool)> done) {
 			uniqueName.erase(0, 1);
 			uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
 
-			const auto window = std::make_shared<QWidget>();
-			window->setAttribute(Qt::WA_DontShowOnScreen);
-			window->setWindowModality(Qt::ApplicationModal);
-			window->show();
+			const auto parent = []() -> QPointer<QWidget> {
+				const auto active = Core::App().activeWindow();
+				if (!active) {
+					return nullptr;
+				}
+
+				return active->widget().get();
+			}();
+
+			const auto window = std::make_shared<base::unique_qptr<QWidget>>(
+				std::in_place,
+				parent);
+
+			auto &raw = **window;
+			raw.setAttribute(Qt::WA_DontShowOnScreen);
+			raw.setWindowModality(Qt::WindowModal);
+			raw.show();
 
 			XdpRequest::RequestProxy::new_(
 				proxy->get_connection(),
@@ -146,7 +161,6 @@ void PortalAutostart(bool enabled, Fn<void(bool)> done) {
 						});
 					});
 
-
 					std::vector<std::string> commandline;
 					commandline.push_back(executable.toStdString());
 					if (Core::Launcher::Instance().customWorkingDir()) {
@@ -156,7 +170,9 @@ void PortalAutostart(bool enabled, Fn<void(bool)> done) {
 					commandline.push_back("-autostart");
 
 					interface.call_request_background(
-						base::Platform::XDP::ParentWindowID(),
+						base::Platform::XDP::ParentWindowID(parent
+							? parent->windowHandle()
+							: nullptr),
 						GLib::Variant::new_array({
 							GLib::Variant::new_dict_entry(
 								GLib::Variant::new_string("handle_token"),

From 2c7922ce7b40fcc4a1bc0b1a7e40452b841fbf7a Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Fri, 19 Jul 2024 18:07:45 +0400
Subject: [PATCH 079/163] Get release CFLAGS from Dockerfile on Linux

---
 Telegram/build/docker/centos_env/Dockerfile | 10 ++++------
 Telegram/build/docker/centos_env/build.sh   |  2 +-
 2 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 463472041..f19f8f1e4 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -3,13 +3,12 @@
 {%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git config -f .gitmodules submodule.m4.shallow true && git submodule init && git submodule update" -%}
 {%- set QT = "6.7.2" -%}
 {%- set QT_TAG = "v" ~ QT -%}
-{%- set CFLAGS_DEBUG = "-g -pipe -fPIC -fstack-protector-all -fstack-clash-protection -fcf-protection -D_GLIBCXX_ASSERTIONS" -%}
-{%- set CFLAGS_LTO = "-flto=auto -ffat-lto-objects" -%}
+{%- set CFLAGS_DEBUG = "$CFLAGS -O0 -fno-lto -U_FORTIFY_SOURCE" -%}
 {%- set LibrariesPath = "/usr/src/Libraries" -%}
 
 # syntax=docker/dockerfile:1
 
-FROM rockylinux:8 AS builder-base
+FROM rockylinux:8 AS builder
 ENV LANG C.UTF-8
 ENV LIBRARY_PATH /usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib
 ENV LD_LIBRARY_PATH $LIBRARY_PATH
@@ -33,11 +32,10 @@ WORKDIR {{ LibrariesPath }}
 
 RUN python3 -m pip install meson ninja
 
-FROM builder-base AS builder
 ENV AR gcc-ar
 ENV RANLIB gcc-ranlib
 ENV NM gcc-nm
-ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}{{ CFLAGS_LTO }}{% endif %} -pipe -fPIC -fno-omit-frame-pointer -fstack-protector-all -fstack-clash-protection -fcf-protection -DNDEBUG -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
+ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fno-omit-frame-pointer -fstack-protector-all -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
 ENV CXXFLAGS $CFLAGS
 
 FROM builder AS patches
@@ -803,7 +801,7 @@ RUN cmake --build out --config Debug --parallel \
 	&& find out -mindepth 1 -maxdepth 1 ! -name Debug -exec rm -rf {} \;
 {%- endif %}
 
-FROM builder-base
+FROM builder
 COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache /
 COPY --link --from=xz {{ LibrariesPath }}/xz-cache /
 COPY --link --from=protobuf {{ LibrariesPath }}/protobuf-cache /
diff --git a/Telegram/build/docker/centos_env/build.sh b/Telegram/build/docker/centos_env/build.sh
index 0bf1aae04..e7a34e6ae 100755
--- a/Telegram/build/docker/centos_env/build.sh
+++ b/Telegram/build/docker/centos_env/build.sh
@@ -3,4 +3,4 @@ set -e
 
 cd Telegram
 ./configure.sh "$@"
-cmake --build ../out --config "${CONFIG:-RelWithDebInfo}" --parallel
+cmake --build ../out --config "${CONFIG:-Release}" --parallel

From 5e1fb6ebbfa59cf06cf724e4ed52087383fdbc01 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Sat, 20 Jul 2024 12:45:49 +0400
Subject: [PATCH 080/163] Add -fasynchronous-unwind-tables and
 -mno-omit-leaf-frame-pointer for better debugging on Linux

---
 Telegram/build/docker/centos_env/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index f19f8f1e4..143bf072c 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -35,7 +35,7 @@ RUN python3 -m pip install meson ninja
 ENV AR gcc-ar
 ENV RANLIB gcc-ranlib
 ENV NM gcc-nm
-ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fno-omit-frame-pointer -fstack-protector-all -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
+ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-all -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
 ENV CXXFLAGS $CFLAGS
 
 FROM builder AS patches

From f4afa762d8ccc8de2aed0ba59fde3589f57c8fa6 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Sat, 20 Jul 2024 12:53:22 +0400
Subject: [PATCH 081/163] Replace -fstack-protector-all with
 -fstack-protector-strong to avoid slowdown of functions not using stack

---
 Telegram/build/docker/centos_env/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 143bf072c..ca962d935 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -35,7 +35,7 @@ RUN python3 -m pip install meson ninja
 ENV AR gcc-ar
 ENV RANLIB gcc-ranlib
 ENV NM gcc-nm
-ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-all -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
+ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-strong -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
 ENV CXXFLAGS $CFLAGS
 
 FROM builder AS patches

From c18e8fd777453c38530e694c3eb724fea69b9489 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Sat, 20 Jul 2024 12:57:19 +0400
Subject: [PATCH 082/163] Enable exceptions for C on Linux

---
 Telegram/build/docker/centos_env/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index ca962d935..0f101584b 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -35,7 +35,7 @@ RUN python3 -m pip install meson ninja
 ENV AR gcc-ar
 ENV RANLIB gcc-ranlib
 ENV NM gcc-nm
-ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-strong -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
+ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-strong -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS
 ENV CXXFLAGS $CFLAGS
 
 FROM builder AS patches

From 9b2847a11dbaf50e77168724888c06da77d1de9a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 18:14:46 +0200
Subject: [PATCH 083/163] Update submodules.

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

diff --git a/Telegram/lib_base b/Telegram/lib_base
index 4ac8cf9d6..21a8611ab 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit 4ac8cf9d65e47efa9d2022939c6d0c38f32d9c7a
+Subproject commit 21a8611ab764acc7cb622859ea4c97bd259570af
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 7dd187eee..45f3a3306 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 7dd187eeeb46b68dadc5752c3f3fc34e5e1d7e94
+Subproject commit 45f3a330679b775f0f56bbea7b40f83ebd8f5639
diff --git a/cmake b/cmake
index b3871c4ef..462b85cc6 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit b3871c4efde37827263053d95e6f16fe34d4952e
+Subproject commit 462b85cc62fd2422084bf6a66408ac7bec3d795d

From 24fabf259079a7e3b3a1fe83532d5ab3a67dfff7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 18:25:09 +0200
Subject: [PATCH 084/163] Fix build on Windows.

---
 Telegram/ThirdParty/tgcalls | 2 +-
 Telegram/lib_webrtc         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index 358a7b0ec..ce5bc41b2 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit 358a7b0ec0e22782b4833b9c0add465133d11632
+Subproject commit ce5bc41b27144dad10756c745f6098ba14beb36b
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index 1f0b2531d..70b83ba71 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit 1f0b2531d54a881e9a9f0818ce08f2e403917784
+Subproject commit 70b83ba71537016a04ff15d44dc078a22c18eced

From fb6445249580ef45a0a528148ae277c94f97f8ce Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 25 Jul 2024 09:32:20 +0300
Subject: [PATCH 085/163] Fix build on Linux.

---
 Telegram/ThirdParty/tgcalls                 | 2 +-
 Telegram/build/deploy.sh                    | 4 ++--
 Telegram/build/docker/centos_env/Dockerfile | 2 +-
 cmake                                       | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index ce5bc41b2..0d88a47c7 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit ce5bc41b27144dad10756c745f6098ba14beb36b
+Subproject commit 0d88a47c7f6dcebf4e48c16d253bb3db5a15e2e6
diff --git a/Telegram/build/deploy.sh b/Telegram/build/deploy.sh
index 21183fa38..e3217ca40 100755
--- a/Telegram/build/deploy.sh
+++ b/Telegram/build/deploy.sh
@@ -70,9 +70,9 @@ else
   DeployMac="1"
   DeployWin="1"
   DeployWin64="1"
-  DeployWinArm="1"
+  DeployWinArm="0"
   DeployLinux="1"
-  echo "Deploying five versions of $AppVersionStrFull: for Windows 32 bit, Windows 64 bit, Windows on ARM, macOS and Linux 64 bit.."
+  echo "Deploying four versions of $AppVersionStrFull: for Windows 32 bit, Windows 64 bit, macOS and Linux 64 bit.."
 fi
 if [ "$BuildTarget" == "mac" ]; then
   BackupPath="$HOME/Projects/backup/tdesktop"
diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 0f101584b..303aab478 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -59,7 +59,7 @@ RUN git init zlib \
 	&& rm -rf zlib
 
 FROM builder AS xz
-RUN git clone -b v5.4.4 --depth=1 https://github.com/tukaani-project/xz.git \
+RUN git clone -b v5.4.4 --depth=1 {{ GIT }}/tukaani-project/xz.git \
 	&& cd xz \
 	&& cmake -B build . -DCMAKE_BUILD_TYPE=None \
 	&& cmake --build build -j$(nproc) \
diff --git a/cmake b/cmake
index 462b85cc6..17c758e2b 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 462b85cc62fd2422084bf6a66408ac7bec3d795d
+Subproject commit 17c758e2b9f09a58f14582bde3561873ce026039

From 77d6e19214781859234e33865a209a8fe36502dc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 18:54:23 +0200
Subject: [PATCH 086/163] Beta version 5.2.4.

- Allow opening several web apps.
- Send location marks and venues.
---
 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/build/version                       | 10 +++++-----
 changelog.txt                                |  5 +++++
 6 files changed, 22 insertions(+), 17 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 46918013d..27e1bbfe4 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="5.2.3.0" />
+    Version="5.2.4.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 c47677eb2..a256817c4 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,3,0
- PRODUCTVERSION 5,2,3,0
+ FILEVERSION 5,2,4,0
+ PRODUCTVERSION 5,2,4,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "5.2.3.0"
+            VALUE "FileVersion", "5.2.4.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.3.0"
+            VALUE "ProductVersion", "5.2.4.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 5594959fe..8f3f05d48 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,3,0
- PRODUCTVERSION 5,2,3,0
+ FILEVERSION 5,2,4,0
+ PRODUCTVERSION 5,2,4,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", "5.2.3.0"
+            VALUE "FileVersion", "5.2.4.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.3.0"
+            VALUE "ProductVersion", "5.2.4.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index e1ac92bf4..45302d07a 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 = 5002003;
-constexpr auto AppVersionStr = "5.2.3";
-constexpr auto AppBetaVersion = false;
+constexpr auto AppVersion = 5002004;
+constexpr auto AppVersionStr = "5.2.4";
+constexpr auto AppBetaVersion = true;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index 4d2b86059..446ec84d7 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         5002003
+AppVersion         5002004
 AppVersionStrMajor 5.2
-AppVersionStrSmall 5.2.3
-AppVersionStr      5.2.3
-BetaChannel        0
+AppVersionStrSmall 5.2.4
+AppVersionStr      5.2.4
+BetaChannel        1
 AlphaVersion       0
-AppVersionOriginal 5.2.3
+AppVersionOriginal 5.2.4.beta
diff --git a/changelog.txt b/changelog.txt
index c0298f855..8d5ac75b8 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,8 @@
+5.2.4 beta (24.07.24)
+
+- Allow opening several web apps.
+- Send location marks and venues.
+
 5.2.3 (07.07.24)
 
 - Fix crash in bot star stats page.

From 517b456670d86a7844bddae5cc2557f269163016 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Thu, 25 Jul 2024 21:25:55 +0400
Subject: [PATCH 087/163] Add hit test for window context menu

---
 Telegram/SourceFiles/calls/calls_panel.cpp    |  2 +-
 .../calls/group/calls_group_panel.cpp         |  2 +-
 .../media/view/media_view_overlay_widget.cpp  | 25 +++++++++++--------
 Telegram/lib_ui                               |  2 +-
 4 files changed, 18 insertions(+), 13 deletions(-)

diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp
index 4b92ee075..ea204a770 100644
--- a/Telegram/SourceFiles/calls/calls_panel.cpp
+++ b/Telegram/SourceFiles/calls/calls_panel.cpp
@@ -215,7 +215,7 @@ void Panel::initWindow() {
 		}
 		const auto shown = _layerBg->topShownLayer();
 		return (!shown || !shown->geometry().contains(widgetPoint))
-			? (Flag::Move | Flag::FullScreen)
+			? (Flag::Move | Flag::Menu | Flag::FullScreen)
 			: Flag::None;
 	});
 
diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
index 84c336131..927dba8f8 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp
@@ -410,7 +410,7 @@ void Panel::initWindow() {
 		}
 		const auto shown = _layerBg->topShownLayer();
 		return (!shown || !shown->geometry().contains(widgetPoint))
-			? (Flag::Move | Flag::Maximize)
+			? (Flag::Move | Flag::Menu | Flag::Maximize)
 			: Flag::None;
 	});
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index c6a7e9c6e..985bc03f9 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -729,25 +729,27 @@ void OverlayWidget::orderWidgets() {
 void OverlayWidget::setupWindow() {
 	_window->setBodyTitleArea([=](QPoint widgetPoint) {
 		using Flag = Ui::WindowTitleHitTestFlag;
-		if (!_windowed
-			|| !_widget->rect().contains(widgetPoint)
+		Ui::WindowTitleHitTestFlags result;
+		if (!_widget->rect().contains(widgetPoint)
 			|| _helper->skipTitleHitTest(widgetPoint)) {
-			return Flag::None | Flag(0);
+			return result;
 		}
-		const auto inControls = (_over != Over::None) && (_over != Over::Video);
+		if (widgetPoint.y() <= st::mediaviewTitleButton.height) {
+			result |= Flag::Menu;
+		}
+		const auto inControls = ((_over != Over::None) && (_over != Over::Video));
 		if (inControls
 			|| (_streamed
 				&& _streamed->controls
 				&& _streamed->controls->dragging())) {
-			return Flag::None | Flag(0);
 		} else if ((_w > _widget->width() || _h > _maxUsedHeight)
 				&& (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);
+		} else if (_windowed) {
+			result |= Flag::Move;
 		}
-		return Flag::Move | Flag(0);
+		return result;
 	});
 
 	_window->setAttribute(Qt::WA_NoSystemBackground, true);
@@ -5926,8 +5928,11 @@ void OverlayWidget::handleMouseRelease(
 }
 
 bool OverlayWidget::handleContextMenu(std::optional<QPoint> position) {
-	if (position && !QRect(_x, _y, _w, _h).contains(*position)) {
-		return false;
+	if (position) {
+		if (!QRect(_x, _y, _w, _h).contains(*position)
+				|| position->y() <= st::mediaviewTitleButton.height) {
+			return false;
+		}
 	}
 	_menu = base::make_unique_q<Ui::PopupMenu>(
 		_window,
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 45f3a3306..03f250aab 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 45f3a330679b775f0f56bbea7b40f83ebd8f5639
+Subproject commit 03f250aab2e791bfb6a047bffa05273748518816

From 3888e8084acc87e54bdd1af353d539d3fc3f7bf7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 Jul 2024 11:38:09 +0200
Subject: [PATCH 088/163] Update tg_owt, lib_webrtc and patches.

---
 Telegram/build/docker/centos_env/Dockerfile | 4 ++--
 Telegram/build/prepare/prepare.py           | 4 ++--
 Telegram/lib_webrtc                         | 2 +-
 snap/snapcraft.yaml                         | 4 ++--
 4 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 303aab478..99816a198 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -42,7 +42,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 6898f0d215f249917c076f00d3fc954a43f35e6a \
+	&& git fetch --depth=1 origin 85a1c4ec327ed390a27e85f2162c31525220a50d \
 	&& git reset --hard FETCH_HEAD \
 	&& rm -rf .git
 
@@ -771,7 +771,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache /
 RUN git init tg_owt \
 	&& cd tg_owt \
 	&& git remote add origin {{ GIT }}/desktop-app/tg_owt.git \
-	&& git fetch --depth=1 origin 996dbe2c83b5a71d9045ce47960b8432e223dbb3 \
+	&& git fetch --depth=1 origin c0ba7b391c0fd1d408fc0d58f4fbecb68a6fcd55 \
 	&& git reset --hard FETCH_HEAD \
 	&& git submodule update --init --recursive --depth=1 \
 	&& rm -rf .git \
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index f5a64b413..b7c447019 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -457,7 +457,7 @@ if customRunCommand:
 stage('patches', """
     git clone https://github.com/desktop-app/patches.git
     cd patches
-    git checkout 6898f0d215
+    git checkout 85a1c4ec32
 """)
 
 stage('msys64', """
@@ -1718,7 +1718,7 @@ win:
 stage('tg_owt', """
     git clone https://github.com/desktop-app/tg_owt.git
     cd tg_owt
-    git checkout 996dbe2c83
+    git checkout c0ba7b391c
     git submodule init
     git submodule update
 win:
diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index 70b83ba71..d9a08df0c 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit 70b83ba71537016a04ff15d44dc078a22c18eced
+Subproject commit d9a08df0c0b64e4323e23c9ccefe754fd86d0f44
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 0a3d3f855..12fa23acb 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -165,7 +165,7 @@ parts:
   patches:
     source: https://github.com/desktop-app/patches.git
     source-depth: 1
-    source-commit: 6898f0d215f249917c076f00d3fc954a43f35e6a
+    source-commit: 85a1c4ec327ed390a27e85f2162c31525220a50d
     plugin: dump
     override-pull: |
       craftctl default
@@ -433,7 +433,7 @@ parts:
   webrtc:
     source: https://github.com/desktop-app/tg_owt.git
     source-depth: 1
-    source-commit: 996dbe2c83b5a71d9045ce47960b8432e223dbb3
+    source-commit: c0ba7b391c0fd1d408fc0d58f4fbecb68a6fcd55
     plugin: cmake
     build-environment:
       - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s

From 86cdda22771748e8cc4f635fc00a6f6f40003b27 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 Jul 2024 13:36:05 +0200
Subject: [PATCH 089/163] Fix text with blockquotes geometry counting.

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

diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 03f250aab..99d914213 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 03f250aab2e791bfb6a047bffa05273748518816
+Subproject commit 99d9142130025e817fe7f1ac278078c34a509a1c

From db0856f71c8673acbab9d2739083e1c38b04b1f2 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 Jul 2024 13:45:02 +0200
Subject: [PATCH 090/163] Support t.me/username?profile links.

---
 Telegram/SourceFiles/core/local_url_handlers.cpp              | 4 +++-
 Telegram/SourceFiles/window/window_session_controller.cpp     | 2 ++
 .../SourceFiles/window/window_session_controller_link_info.h  | 1 +
 3 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 223cec043..6777464c1 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -503,7 +503,9 @@ bool ResolveUsernameOrPhone(
 		return false;
 	}
 	using ResolveType = Window::ResolveType;
-	auto resolveType = ResolveType::Default;
+	auto resolveType = params.contains(u"profile"_q)
+		? ResolveType::Profile
+		: ResolveType::Default;
 	auto startToken = params.value(u"start"_q);
 	if (!startToken.isEmpty()) {
 		resolveType = ResolveType::BotStart;
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index ebd403da4..ef1bddf24 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -577,6 +577,8 @@ void SessionNavigation::showPeerByLinkResolved(
 			info.messageId,
 			commentId->id,
 			params);
+	} else if (resolveType == ResolveType::Profile) {
+		showPeerInfo(peer, params);
 	} else if (peer->isForum() && resolveType != ResolveType::Boost) {
 		const auto itemId = info.messageId;
 		if (!itemId) {
diff --git a/Telegram/SourceFiles/window/window_session_controller_link_info.h b/Telegram/SourceFiles/window/window_session_controller_link_info.h
index 74a783696..d64d82c53 100644
--- a/Telegram/SourceFiles/window/window_session_controller_link_info.h
+++ b/Telegram/SourceFiles/window/window_session_controller_link_info.h
@@ -22,6 +22,7 @@ enum class ResolveType {
 	ShareGame,
 	Mention,
 	Boost,
+	Profile,
 };
 
 struct CommentId {

From 3f0f3a3c11966f47ec058e2facee5e32010bc026 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 Jul 2024 17:23:41 +0200
Subject: [PATCH 091/163] Focus existing location picker.

---
 Telegram/CMakeLists.txt                       |   2 +
 Telegram/Resources/langs/lang.strings         |   1 +
 Telegram/SourceFiles/api/api_common.h         |   8 +
 .../chat_helpers/chat_helpers.style           |   6 +
 .../data/components/location_pickers.cpp      |  44 ++++
 .../data/components/location_pickers.h        |  39 ++++
 .../inline_bots/bot_attach_web_view.cpp       |  15 +-
 Telegram/SourceFiles/main/main_session.cpp    |   2 +
 Telegram/SourceFiles/main/main_session.h      |   5 +
 .../settings/business/settings_location.cpp   |   9 +-
 .../ui/controls/location_picker.cpp           | 191 +++++++++++++++---
 .../SourceFiles/ui/controls/location_picker.h |  10 +-
 12 files changed, 297 insertions(+), 35 deletions(-)
 create mode 100644 Telegram/SourceFiles/data/components/location_pickers.cpp
 create mode 100644 Telegram/SourceFiles/data/components/location_pickers.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 48cf60eff..b9ee4785c 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -475,6 +475,8 @@ PRIVATE
     data/business/data_shortcut_messages.h
     data/components/factchecks.cpp
     data/components/factchecks.h
+    data/components/location_pickers.cpp
+    data/components/location_pickers.h
     data/components/recent_peers.cpp
     data/components/recent_peers.h
     data/components/scheduled_messages.cpp
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 6c9828189..b902aa4f6 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3197,6 +3197,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_unread_bar_some" = "Unread messages";
 
 "lng_maps_point" = "Location";
+"lng_maps_select_on_map" = "Select on the Map";
 "lng_maps_point_send" = "Send This Location";
 "lng_maps_point_set" = "Set This Location";
 "lng_maps_or_choose" = "Or choose a venue";
diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h
index cd8aa54e2..81f098d67 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -30,6 +30,10 @@ struct SendOptions {
 	bool invertCaption = false;
 	bool hideViaBot = false;
 	crl::time ttlSeconds = 0;
+
+	friend inline bool operator==(
+		const SendOptions &,
+		const SendOptions &) = default;
 };
 [[nodiscard]] SendOptions DefaultSendWhenOnlineOptions();
 
@@ -52,6 +56,10 @@ struct SendAction {
 	MsgId replaceMediaOf = 0;
 
 	[[nodiscard]] MTPInputReplyTo mtpReplyTo() const;
+
+	friend inline bool operator==(
+		const SendAction &,
+		const SendAction &) = default;
 };
 
 struct MessageToSend {
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 9b92f4815..47c1e8b47 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1475,3 +1475,9 @@ pickLocationLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
 	thickness: 4px;
 }
 pickLocationPromoHeight: 32px;
+pickLocationChooseOnMap: RoundButton(defaultActiveButton) {
+	height: 44px;
+	textTop: 11px;
+	width: -96px;
+	font: font(15px semibold);
+}
diff --git a/Telegram/SourceFiles/data/components/location_pickers.cpp b/Telegram/SourceFiles/data/components/location_pickers.cpp
new file mode 100644
index 000000000..4402e4ccf
--- /dev/null
+++ b/Telegram/SourceFiles/data/components/location_pickers.cpp
@@ -0,0 +1,44 @@
+/*
+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/components/location_pickers.h"
+
+#include "api/api_common.h"
+#include "ui/controls/location_picker.h"
+
+namespace Data {
+
+struct LocationPickers::Entry {
+	Api::SendAction action;
+	base::weak_ptr<Ui::LocationPicker> picker;
+};
+
+LocationPickers::LocationPickers() = default;
+
+LocationPickers::~LocationPickers() = default;
+
+Ui::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) {
+	for (auto i = begin(_pickers); i != end(_pickers);) {
+		if (const auto strong = i->picker.get()) {
+			if (i->action == action) {
+				return i->picker.get();
+			}
+			++i;
+		} else {
+			i = _pickers.erase(i);
+		}
+	}
+	return nullptr;
+}
+
+void LocationPickers::emplace(
+		const Api::SendAction &action,
+		not_null<Ui::LocationPicker*> picker) {
+	_pickers.push_back({ action, picker });
+}
+
+} // namespace Data
\ No newline at end of file
diff --git a/Telegram/SourceFiles/data/components/location_pickers.h b/Telegram/SourceFiles/data/components/location_pickers.h
new file mode 100644
index 000000000..ad1046095
--- /dev/null
+++ b/Telegram/SourceFiles/data/components/location_pickers.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
+
+#include "base/weak_ptr.h"
+
+namespace Api {
+struct SendAction;
+} // namespace Api
+
+namespace Ui {
+class LocationPicker;
+} // namespace Ui
+
+namespace Data {
+
+class LocationPickers final {
+public:
+	LocationPickers();
+	~LocationPickers();
+
+	Ui::LocationPicker *lookup(const Api::SendAction &action);
+	void emplace(
+		const Api::SendAction &action,
+		not_null<Ui::LocationPicker*> picker);
+
+private:
+	struct Entry;
+
+	std::vector<Entry> _pickers;
+
+};
+
+} // namespace Data
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index 27e37053a..e7d842c64 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "boxes/share_box.h"
 #include "core/click_handler_types.h"
 #include "core/shortcuts.h"
+#include "data/components/location_pickers.h"
 #include "data/data_bot_app.h"
 #include "data/data_changes.h"
 #include "data/data_user.h"
@@ -1776,6 +1777,11 @@ void ChooseAndSendLocation(
 		not_null<Window::SessionController*> controller,
 		const Ui::LocationPickerConfig &config,
 		Api::SendAction action) {
+	const auto session = &controller->session();
+	if (const auto picker = session->locationPickers().lookup(action)) {
+		picker->activate();
+		return;
+	}
 	const auto callback = [=](Data::InputVenue venue) {
 		if (venue.justLocation()) {
 			Api::SendLocation(action, venue.lat, venue.lon);
@@ -1783,17 +1789,18 @@ void ChooseAndSendLocation(
 			Api::SendVenue(action, venue);
 		}
 	};
-	Ui::LocationPicker::Show({
+	const auto picker = Ui::LocationPicker::Show({
 		.parent = controller->widget(),
 		.config = config,
 		.chooseLabel = tr::lng_maps_point_send(),
 		.recipient = action.history->peer,
-		.session = &controller->session(),
-		.callback = crl::guard(controller, callback),
+		.session = session,
+		.callback = crl::guard(session, callback),
 		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
-		.storageId = controller->session().local().resolveStorageIdBots(),
+		.storageId = session->local().resolveStorageIdBots(),
 		.closeRequests = controller->content()->death(),
 	});
+	session->locationPickers().emplace(action, picker);
 }
 
 std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp
index c14d49051..cc09dacf5 100644
--- a/Telegram/SourceFiles/main/main_session.cpp
+++ b/Telegram/SourceFiles/main/main_session.cpp
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "storage/storage_account.h"
 #include "storage/storage_facade.h"
 #include "data/components/factchecks.h"
+#include "data/components/location_pickers.h"
 #include "data/components/recent_peers.h"
 #include "data/components/scheduled_messages.h"
 #include "data/components/sponsored_messages.h"
@@ -111,6 +112,7 @@ Session::Session(
 , _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this))
 , _topPeers(std::make_unique<Data::TopPeers>(this))
 , _factchecks(std::make_unique<Data::Factchecks>(this))
+, _locationPickers(std::make_unique<Data::LocationPickers>())
 , _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
 , _supportHelper(Support::Helper::Create(this))
 , _saveSettingsTimer([=] { saveSettings(); }) {
diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h
index 9581e7cd4..a69373a36 100644
--- a/Telegram/SourceFiles/main/main_session.h
+++ b/Telegram/SourceFiles/main/main_session.h
@@ -36,6 +36,7 @@ class ScheduledMessages;
 class SponsoredMessages;
 class TopPeers;
 class Factchecks;
+class LocationPickers;
 } // namespace Data
 
 namespace HistoryView::Reactions {
@@ -131,6 +132,9 @@ public:
 	[[nodiscard]] Data::Factchecks &factchecks() const {
 		return *_factchecks;
 	}
+	[[nodiscard]] Data::LocationPickers &locationPickers() const {
+		return *_locationPickers;
+	}
 	[[nodiscard]] Api::Updates &updates() const {
 		return *_updates;
 	}
@@ -259,6 +263,7 @@ private:
 	const std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages;
 	const std::unique_ptr<Data::TopPeers> _topPeers;
 	const std::unique_ptr<Data::Factchecks> _factchecks;
+	const std::unique_ptr<Data::LocationPickers> _locationPickers;
 
 	using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;
 	const std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;
diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp
index e5bb0f8e3..6b659d044 100644
--- a/Telegram/SourceFiles/settings/business/settings_location.cpp
+++ b/Telegram/SourceFiles/settings/business/settings_location.cpp
@@ -63,6 +63,7 @@ private:
 	const Ui::LocationPickerConfig _config;
 	rpl::variable<Data::BusinessLocation> _data;
 	rpl::variable<Data::CloudImage*> _map = nullptr;
+	base::weak_ptr<Ui::LocationPicker> _picker;
 	std::shared_ptr<QImage> _view;
 	Ui::RoundRect _bottomSkipRounding;
 
@@ -232,6 +233,10 @@ void Location::setupPicker(not_null<Ui::VerticalLayout*> content) {
 }
 
 void Location::chooseOnMap() {
+	if (const auto strong = _picker.get()) {
+		strong->activate();
+		return;
+	}
 	const auto callback = [=](Data::InputVenue venue) {
 		auto copy = _data.current();
 		copy.point = Data::LocationPoint(
@@ -249,7 +254,7 @@ void Location::chooseOnMap() {
 			.accuracy = Core::GeoLocationAccuracy::Exact,
 		}
 		: Core::GeoLocation();
-	Ui::LocationPicker::Show({
+	_picker = Ui::LocationPicker::Show({
 		.parent = controller()->widget(),
 		.config = _config,
 		.chooseLabel = tr::lng_maps_point_set(),
@@ -258,7 +263,7 @@ void Location::chooseOnMap() {
 		.callback = crl::guard(this, callback),
 		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
 		.storageId = session->local().resolveStorageIdBots(),
-		.closeRequests = controller()->content()->death(),
+		.closeRequests = death(),
 	});
 }
 
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 8cce44571..b6e3dc441 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "dialogs/ui/chat_search_empty.h" // Dialogs::SearchEmpty.
 #include "lang/lang_instance.h"
 #include "lang/lang_keys.h"
+#include "lottie/lottie_icon.h"
 #include "main/session/session_show.h"
 #include "main/main_session.h"
 #include "mtproto/mtproto_config.h"
@@ -41,6 +42,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/style_chat_helpers.h"
 #include "styles/style_dialogs.h"
 #include "styles/style_window.h"
+#include "styles/style_settings.h" // settingsCloudPasswordIconSize
+#include "styles/style_layers.h" // boxDividerHeight
 
 #include <QtCore/QFile>
 #include <QtCore/QJsonDocument>
@@ -185,11 +188,12 @@ private:
 
 [[nodiscard]] object_ptr<RpWidget> MakeFoursquarePromo() {
 	auto result = object_ptr<RpWidget>((QWidget*)nullptr);
+	const auto skip = st::defaultVerticalListSkip;
 	const auto raw = result.data();
-	raw->resize(0, st::pickLocationPromoHeight);
+	raw->resize(0, skip + st::pickLocationPromoHeight);
 	const auto shadow = CreateChild<PlainShadow>(raw);
 	raw->widthValue() | rpl::start_with_next([=](int width) {
-		shadow->setGeometry(0, 0, width, st::lineWidth);
+		shadow->setGeometry(0, skip, width, st::lineWidth);
 	}, raw->lifetime());
 	raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
 		auto p = QPainter(raw);
@@ -197,7 +201,7 @@ private:
 		p.setPen(st::windowSubTextFg);
 		p.setFont(st::normalFont);
 		p.drawText(
-			raw->rect(),
+			raw->rect().marginsRemoved({ 0, skip, 0, 0 }),
 			tr::lng_maps_venues_source(tr::now),
 			style::al_center);
 	}, raw->lifetime());
@@ -544,7 +548,7 @@ void SetupEmptyView(
 		(query ? Icon::NoResults : Icon::Search),
 		(query
 			? tr::lng_maps_no_places
-			: tr::lng_maps_choose_to_search)(Ui::Text::WithEntities));
+			: tr::lng_maps_choose_to_search)(Text::WithEntities));
 	view->setMinimalHeight(st::recentPeersEmptyHeightMin);
 	view->show();
 
@@ -636,6 +640,96 @@ void SetupVenues(
 	return result;
 }
 
+not_null<RpWidget*> SetupMapPlaceholder(
+		not_null<RpWidget*> parent,
+		int minHeight,
+		int maxHeight,
+		Fn<void()> choose) {
+	const auto result = CreateChild<RpWidget>(parent);
+
+	const auto top = CreateChild<BoxContentDivider>(result);
+	const auto bottom = CreateChild<BoxContentDivider>(result);
+
+	const auto icon = CreateChild<RpWidget>(result);
+	const auto iconSize = st::settingsCloudPasswordIconSize;
+	auto ownedLottie = Lottie::MakeIcon({
+		.name = u"location"_q,
+		.sizeOverride = { iconSize, iconSize },
+		.limitFps = true,
+	});
+	const auto lottie = ownedLottie.get();
+	icon->lifetime().add([kept = std::move(ownedLottie)] {});
+
+	icon->paintRequest(
+	) | rpl::start_with_next([=] {
+		auto p = QPainter(icon);
+		const auto left = (icon->width() - iconSize) / 2;
+		const auto scale = icon->height() / float64(iconSize);
+		auto hq = std::optional<PainterHighQualityEnabler>();
+		if (scale < 1.) {
+			const auto center = QPointF(
+				icon->width() / 2.,
+				icon->height() / 2.);
+			hq.emplace(p);
+			p.translate(center);
+			p.scale(scale, scale);
+			p.translate(-center);
+			p.setOpacity(scale);
+		}
+		lottie->paint(p, left, 0);
+	}, icon->lifetime());
+
+	InvokeQueued(icon, [=] {
+		const auto till = lottie->framesCount() - 1;
+		lottie->animate([=] { icon->update(); }, 0, till);
+	});
+
+	const auto button = CreateChild<RoundButton>(
+		result,
+		tr::lng_maps_select_on_map(),
+		st::pickLocationChooseOnMap);
+	button->setFullRadius(true);
+	button->setTextTransform(RoundButton::TextTransform::NoTransform);
+	button->setClickedCallback(choose);
+
+	parent->sizeValue() | rpl::start_with_next([=](QSize size) {
+		result->setGeometry(QRect(QPoint(), size));
+
+		const auto width = size.width();
+		top->setGeometry(0, 0, width, top->height());
+		bottom->setGeometry(QRect(
+			QPoint(0, size.height() - bottom->height()),
+			QSize(width, bottom->height())));
+		const auto dividers = top->height() + bottom->height();
+
+		const auto ratio = (size.height() - minHeight)
+			/ float64(maxHeight - minHeight);
+		const auto iconHeight = int(base::SafeRound(ratio * iconSize));
+
+		const auto available = size.height() - dividers;
+		const auto maxDelta = (maxHeight
+			- dividers
+			- iconSize
+			- button->height()) / 2;
+		const auto minDelta = (minHeight - dividers - button->height()) / 2;
+
+		const auto delta = anim::interpolate(minDelta, maxDelta, ratio);
+		button->move(
+			(width - button->width()) / 2,
+			size.height() - bottom->height() - delta - button->height());
+		const auto wide = available - delta - button->height();
+		const auto skip = (wide - iconHeight) / 2;
+		icon->setGeometry(0, top->height() + skip, width, iconHeight);
+	}, result->lifetime());
+
+	top->show();
+	icon->show();
+	bottom->show();
+	result->show();
+
+	return result;
+}
+
 } // namespace
 
 LocationPicker::LocationPicker(Descriptor &&descriptor)
@@ -646,6 +740,8 @@ LocationPicker::LocationPicker(Descriptor &&descriptor)
 , _body((_window->setInnerSize(st::pickLocationWindow)
 	, _window->showInner(base::make_unique_q<RpWidget>(_window.get()))
 	, _window->inner()))
+, _chooseButtonLabel(std::move(descriptor.chooseLabel))
+, _webviewStorageId(descriptor.storageId)
 , _updateStyles([=] {
 	const auto str = EscapeForScriptString(ComputeStyles());
 	if (_webview) {
@@ -684,7 +780,6 @@ bool LocationPicker::Available(const LocationPickerConfig &config) {
 
 void LocationPicker::setup(const Descriptor &descriptor) {
 	setupWindow(descriptor);
-	setupWebview(descriptor);
 
 	_initialProvided = descriptor.initial;
 	const auto initial = _initialProvided.exact()
@@ -695,6 +790,9 @@ void LocationPicker::setup(const Descriptor &descriptor) {
 		resolveAddress(initial);
 		venuesSearchEnableAt(initial);
 	}
+	if (!_initialProvided) {
+		resolveCurrentLocation();
+	}
 }
 
 void LocationPicker::setupWindow(const Descriptor &descriptor) {
@@ -714,6 +812,15 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 		parent.y() + (parent.height() - window->height()) / 2);
 
 	_container = CreateChild<RpWidget>(_body.get());
+	_mapPlaceholderAdded = st::pickLocationButtonSkip
+		+ st::pickLocationButton.height
+		+ st::pickLocationButtonSkip
+		+ st::boxDividerHeight;
+	const auto min = st::pickLocationCollapsedHeight + _mapPlaceholderAdded;
+	const auto max = st::pickLocationMapHeight + _mapPlaceholderAdded;
+	_mapPlaceholder = SetupMapPlaceholder(_container, min, max, [=] {
+		setupWebview();
+	});
 	_scroll = CreateChild<ScrollArea>(_body.get());
 	const auto controls = _scroll->setOwnedWidget(
 		object_ptr<VerticalLayout>(_scroll));
@@ -727,17 +834,6 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 
 	const auto toppad = mapControls->add(object_ptr<RpWidget>(controls));
 
-	const auto button = mapControls->add(
-		MakeChooseLocationButton(
-			mapControls,
-			std::move(descriptor.chooseLabel),
-			_geocoderAddress.value()),
-		{ 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip });
-	button->setClickedCallback([=] {
-		_webview->eval("LocationPicker.send();");
-	});
-
-	AddDivider(mapControls);
 	AddSkip(mapControls);
 	AddSubsectionTitle(mapControls, tr::lng_maps_or_choose());
 
@@ -757,7 +853,9 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 		const auto sub = std::min(
 			(st::pickLocationMapHeight - st::pickLocationCollapsedHeight),
 			scrollTop);
-		const auto mapHeight = st::pickLocationMapHeight - sub;
+		const auto mapHeight = st::pickLocationMapHeight
+			- sub
+			+ (_mapPlaceholder ? _mapPlaceholderAdded : 0);
 		_container->setGeometry(0, 0, width, mapHeight);
 		const auto scrollWidgetTop = search ? 0 : mapHeight;
 		const auto scrollHeight = height - scrollWidgetTop;
@@ -771,21 +869,52 @@ void LocationPicker::setupWindow(const Descriptor &descriptor) {
 	}, _container->lifetime());
 
 	_container->show();
-	_scroll->hide();
+	_scroll->show();
 	controls->show();
-	button->show();
 	window->show();
 }
 
-void LocationPicker::setupWebview(const Descriptor &descriptor) {
+void LocationPicker::setupWebview() {
 	Expects(!_webview);
 
+	delete base::take(_mapPlaceholder);
+
+	const auto mapControls = _mapControlsWrap->entity();
+	mapControls->insert(
+		1,
+		object_ptr<BoxContentDivider>(mapControls)
+	)->show();
+
+	_mapButton = mapControls->insert(
+		1,
+		MakeChooseLocationButton(
+			mapControls,
+			_chooseButtonLabel.value(),
+			_geocoderAddress.value()),
+		{ 0, st::pickLocationButtonSkip, 0, st::pickLocationButtonSkip });
+	_mapButton->setClickedCallback([=] {
+		_webview->eval("LocationPicker.send();");
+	});
+	_mapButton->hide();
+
+	_scroll->scrollToY(0);
+	_venuesSearchShown.force_assign(_venuesSearchShown.current());
+
+	_mapLoading = CreateChild<RpWidget>(_body.get());
+
+	_container->geometryValue() | rpl::start_with_next([=](QRect rect) {
+		_mapLoading->setGeometry(rect);
+	}, _mapLoading->lifetime());
+
+	SetupLoadingView(_mapLoading);
+	_mapLoading->show();
+
 	const auto window = _window.get();
 	_webview = std::make_unique<Webview::Window>(
 		_container,
 		Webview::WindowConfig{
 			.opaqueBg = st::windowBg->c,
-			.storageId = descriptor.storageId,
+			.storageId = _webviewStorageId,
 			.dataProtocolOverride = kProtocolOverride,
 		});
 	const auto raw = _webview.get();
@@ -823,12 +952,6 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
 			const auto event = object.value("event").toString();
 			if (event == u"ready"_q) {
 				mapReady();
-				if (!_initialProvided) {
-					resolveCurrentLocation();
-				}
-				if (_webview) {
-					_webview->focus();
-				}
 			} else if (event == u"keydown"_q) {
 				const auto key = object.value("key").toString();
 				const auto modifier = object.value("modifier").toString();
@@ -968,6 +1091,8 @@ void LocationPicker::resolveAddress(Core::GeoLocation location) {
 void LocationPicker::mapReady() {
 	Expects(_scroll != nullptr);
 
+	delete base::take(_mapLoading);
+
 	const auto token = _config.mapsToken.toUtf8();
 	const auto center = DefaultCenter(_initialProvided);
 	const auto bounds = DefaultBounds();
@@ -980,7 +1105,11 @@ void LocationPicker::mapReady() {
 		+ ", protocol: " + protocol;
 	_webview->eval("LocationPicker.init({ " + params + " });");
 
-	_scroll->show();
+	const auto handle = _window->window()->windowHandle();
+	if (handle && QGuiApplication::focusWindow() == handle) {
+		_webview->focus();
+	}
+	_mapButton->show();
 }
 
 bool LocationPicker::venuesFromCache(
@@ -1163,6 +1292,12 @@ void LocationPicker::processKey(
 	}
 }
 
+void LocationPicker::activate() {
+	if (_window) {
+		_window->activateWindow();
+	}
+}
+
 void LocationPicker::close() {
 	crl::on_main(this, [=] {
 		_window = nullptr;
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.h b/Telegram/SourceFiles/ui/controls/location_picker.h
index 97ce724f3..943e7ac75 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.h
+++ b/Telegram/SourceFiles/ui/controls/location_picker.h
@@ -29,6 +29,7 @@ class Window;
 
 namespace Ui {
 
+class AbstractButton;
 class SeparatePanel;
 class RpWidget;
 class ScrollArea;
@@ -93,6 +94,7 @@ public:
 	[[nodiscard]] static bool Available(const LocationPickerConfig &config);
 	static not_null<LocationPicker*> Show(Descriptor &&descriptor);
 
+	void activate();
 	void close();
 	void minimize();
 	void quit();
@@ -109,7 +111,7 @@ private:
 
 	void setup(const Descriptor &descriptor);
 	void setupWindow(const Descriptor &descriptor);
-	void setupWebview(const Descriptor &descriptor);
+	void setupWebview();
 	void processKey(const QString &key, const QString &modifier);
 	void resolveCurrentLocation();
 	void resolveAddressByTimer();
@@ -129,11 +131,17 @@ private:
 	std::unique_ptr<SeparatePanel> _window;
 	not_null<RpWidget*> _body;
 	RpWidget *_container = nullptr;
+	RpWidget *_mapPlaceholder = nullptr;
+	RpWidget *_mapLoading = nullptr;
+	AbstractButton *_mapButton = nullptr;
 	SlideWrap<VerticalLayout> *_mapControlsWrap = nullptr;
+	rpl::variable<QString> _chooseButtonLabel;
 	ScrollArea *_scroll = nullptr;
+	Webview::StorageId _webviewStorageId;
 	std::unique_ptr<Webview::Window> _webview;
 	SingleQueuedInvokation _updateStyles;
 	Core::GeoLocation _initialProvided;
+	int _mapPlaceholderAdded = 0;
 	bool _subscribedToColors = false;
 
 	base::Timer _geocoderResolveTimer;

From 37907636e618311234fa9ea4ef5204434fe76d57 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 27 Jul 2024 07:43:47 +0200
Subject: [PATCH 092/163] Fix build with Xcode.

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

diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls
index 0d88a47c7..9bf4065ea 160000
--- a/Telegram/ThirdParty/tgcalls
+++ b/Telegram/ThirdParty/tgcalls
@@ -1 +1 @@
-Subproject commit 0d88a47c7f6dcebf4e48c16d253bb3db5a15e2e6
+Subproject commit 9bf4065ea00cbed5e63cec348457ed13143459d0

From ee6edf9caa032f891345facc037c46210329d259 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 Jul 2024 19:24:01 +0200
Subject: [PATCH 093/163] Force venue icon format.

---
 Telegram/SourceFiles/ui/controls/location_picker.cpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index b6e3dc441..945e5cf8f 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -298,6 +298,10 @@ void VenuesController::rowPaintIcon(
 					QSize(inner, inner) * ratio,
 					Qt::IgnoreAspectRatio,
 					Qt::SmoothTransformation);
+				if (!data.icon.isNull()) {
+					data.icon = data.icon.convertToFormat(
+						QImage::Format_ARGB32_Premultiplied);
+				}
 			}
 		}
 

From 6a000207ee3b29f0f43d0923b1528e77ff1cd4a1 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Sat, 27 Jul 2024 07:48:35 +0200
Subject: [PATCH 094/163] Beta version 5.2.5.

- Fix media viewer context menu.
- Fix blockquotes layout in messages.
---
 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                                | 5 +++++
 6 files changed, 20 insertions(+), 15 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 27e1bbfe4..4716432a8 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="5.2.4.0" />
+    Version="5.2.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 a256817c4..91720d235 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,4,0
- PRODUCTVERSION 5,2,4,0
+ FILEVERSION 5,2,5,0
+ PRODUCTVERSION 5,2,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", "5.2.4.0"
+            VALUE "FileVersion", "5.2.5.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.4.0"
+            VALUE "ProductVersion", "5.2.5.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 8f3f05d48..829055b26 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,4,0
- PRODUCTVERSION 5,2,4,0
+ FILEVERSION 5,2,5,0
+ PRODUCTVERSION 5,2,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", "5.2.4.0"
+            VALUE "FileVersion", "5.2.5.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.4.0"
+            VALUE "ProductVersion", "5.2.5.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 45302d07a..fcfe70dd8 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 = 5002004;
-constexpr auto AppVersionStr = "5.2.4";
+constexpr auto AppVersion = 5002005;
+constexpr auto AppVersionStr = "5.2.5";
 constexpr auto AppBetaVersion = true;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index 446ec84d7..3e6807e8b 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         5002004
+AppVersion         5002005
 AppVersionStrMajor 5.2
-AppVersionStrSmall 5.2.4
-AppVersionStr      5.2.4
+AppVersionStrSmall 5.2.5
+AppVersionStr      5.2.5
 BetaChannel        1
 AlphaVersion       0
-AppVersionOriginal 5.2.4.beta
+AppVersionOriginal 5.2.5.beta
diff --git a/changelog.txt b/changelog.txt
index 8d5ac75b8..09adb5835 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,8 @@
+5.2.5 beta (27.07.24)
+
+- Fix media viewer context menu.
+- Fix blockquotes layout in messages.
+
 5.2.4 beta (24.07.24)
 
 - Allow opening several web apps.

From a141d01a23c7e0fbdf99f017202e0c3559b3f8fb Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Sat, 27 Jul 2024 13:54:38 +0400
Subject: [PATCH 095/163] Fix macOS packaged action

---
 .github/workflows/mac_packaged.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml
index bd51bb74a..3a37311e6 100644
--- a/.github/workflows/mac_packaged.yml
+++ b/.github/workflows/mac_packaged.yml
@@ -69,7 +69,7 @@ jobs:
         run: |
           brew update
           brew upgrade || true
-          brew install autoconf automake boost cmake ffmpeg@6 openal-soft openssl opus ninja pkg-config python qt yasm xz
+          brew install autoconf automake boost cmake ffmpeg@6 openal-soft openh264 openssl opus ninja pkg-config python qt yasm xz
           sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
 
           xcodebuild -version > CACHE_KEY.txt
@@ -108,7 +108,7 @@ jobs:
         run: |
           cd $LibrariesPath
 
-          git clone --recursive --depth=1 $GIT/desktop-app/tg_owt.git
+          git clone --depth=1 --recursive --shallow-submodules $GIT/desktop-app/tg_owt.git
           cd tg_owt
 
           cmake -B build . -GNinja -DCMAKE_BUILD_TYPE=Debug

From b0981ea8e341ae6d71432a237ab086a9ab6d4b44 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 29 Jul 2024 08:05:28 +0200
Subject: [PATCH 096/163] Beta version 5.2.6.

- Fix launching on X11. (Linux)
---
 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/docker/centos_env/Dockerfile  | 2 +-
 Telegram/build/prepare/prepare.py            | 2 +-
 Telegram/build/version                       | 8 ++++----
 changelog.txt                                | 4 ++++
 snap/snapcraft.yaml                          | 2 +-
 9 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 4716432a8..45ec14889 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="5.2.5.0" />
+    Version="5.2.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 91720d235..83eecf0a7 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,5,0
- PRODUCTVERSION 5,2,5,0
+ FILEVERSION 5,2,6,0
+ PRODUCTVERSION 5,2,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", "5.2.5.0"
+            VALUE "FileVersion", "5.2.6.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.5.0"
+            VALUE "ProductVersion", "5.2.6.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 829055b26..40bb30f11 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,5,0
- PRODUCTVERSION 5,2,5,0
+ FILEVERSION 5,2,6,0
+ PRODUCTVERSION 5,2,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", "5.2.5.0"
+            VALUE "FileVersion", "5.2.6.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.5.0"
+            VALUE "ProductVersion", "5.2.6.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index fcfe70dd8..111881fd4 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 = 5002005;
-constexpr auto AppVersionStr = "5.2.5";
+constexpr auto AppVersion = 5002006;
+constexpr auto AppVersionStr = "5.2.6";
 constexpr auto AppBetaVersion = true;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 99816a198..568d6663a 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -771,7 +771,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache /
 RUN git init tg_owt \
 	&& cd tg_owt \
 	&& git remote add origin {{ GIT }}/desktop-app/tg_owt.git \
-	&& git fetch --depth=1 origin c0ba7b391c0fd1d408fc0d58f4fbecb68a6fcd55 \
+	&& git fetch --depth=1 origin 4a60ce1ab9fdb962004c6a959f682ace3db50cbd \
 	&& git reset --hard FETCH_HEAD \
 	&& git submodule update --init --recursive --depth=1 \
 	&& rm -rf .git \
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index b7c447019..83e16284f 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -1718,7 +1718,7 @@ win:
 stage('tg_owt', """
     git clone https://github.com/desktop-app/tg_owt.git
     cd tg_owt
-    git checkout c0ba7b391c
+    git checkout 4a60ce1ab9
     git submodule init
     git submodule update
 win:
diff --git a/Telegram/build/version b/Telegram/build/version
index 3e6807e8b..f10750e38 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         5002005
+AppVersion         5002006
 AppVersionStrMajor 5.2
-AppVersionStrSmall 5.2.5
-AppVersionStr      5.2.5
+AppVersionStrSmall 5.2.6
+AppVersionStr      5.2.6
 BetaChannel        1
 AlphaVersion       0
-AppVersionOriginal 5.2.5.beta
+AppVersionOriginal 5.2.6.beta
diff --git a/changelog.txt b/changelog.txt
index 09adb5835..5ec213f7e 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,7 @@
+5.2.6 beta (29.07.24)
+
+- Fix launching on X11. (Linux)
+
 5.2.5 beta (27.07.24)
 
 - Fix media viewer context menu.
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 12fa23acb..b5f71d99f 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -433,7 +433,7 @@ parts:
   webrtc:
     source: https://github.com/desktop-app/tg_owt.git
     source-depth: 1
-    source-commit: c0ba7b391c0fd1d408fc0d58f4fbecb68a6fcd55
+    source-commit: 4a60ce1ab9fdb962004c6a959f682ace3db50cbd
     plugin: cmake
     build-environment:
       - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s

From f55584b160329536503804c70095780fc32b8cd8 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Mon, 29 Jul 2024 10:03:50 +0300
Subject: [PATCH 097/163] Fixed position of click handler for via bot header in
 message view.

---
 Telegram/SourceFiles/history/view/history_view_message.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index 4c6d4970d..e1d67952e 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -2406,10 +2406,10 @@ TextState Message::textState(
 			if (getStateForwardedInfo(point, trect, &result, request)) {
 				return result;
 			}
-			if (getStateReplyInfo(point, trect, &result)) {
+			if (getStateViaBotIdInfo(point, trect, &result)) {
 				return result;
 			}
-			if (getStateViaBotIdInfo(point, trect, &result)) {
+			if (getStateReplyInfo(point, trect, &result)) {
 				return result;
 			}
 		}

From e9650385adc153e8754235b961e9a76fed337ac1 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Mon, 29 Jul 2024 10:26:47 +0300
Subject: [PATCH 098/163] Added ability to provide custom style to widget with
 infinite animation.

---
 .../boosts/giveaway/boost_badge.cpp             | 17 +++++++++++------
 .../boosts/giveaway/boost_badge.h               |  4 +++-
 2 files changed, 14 insertions(+), 7 deletions(-)

diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp
index feb6d395e..aea01c592 100644
--- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp
+++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.cpp
@@ -20,12 +20,17 @@ namespace Info::Statistics {
 
 not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
 		not_null<Ui::RpWidget*> parent,
-		int size) {
+		int size,
+		const style::InfiniteRadialAnimation *st) {
 	class Widget final : public Ui::RpWidget {
 	public:
-		Widget(not_null<Ui::RpWidget*> p, int size)
+		Widget(
+			not_null<Ui::RpWidget*> p,
+			int size,
+			const style::InfiniteRadialAnimation *st)
 		: Ui::RpWidget(p)
-		, _animation([=] { update(); }, st::startGiveawayButtonLoading) {
+		, _st(st ? st : &st::startGiveawayButtonLoading)
+		, _animation([=] { update(); }, *_st) {
 			resize(size, size);
 			shownValue() | rpl::start_with_next([=](bool v) {
 				return v
@@ -39,17 +44,17 @@ not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
 			auto p = QPainter(this);
 			p.setPen(st::activeButtonFg);
 			p.setBrush(st::activeButtonFg);
-			const auto r = rect()
-				- Margins(st::startGiveawayButtonLoading.thickness);
+			const auto r = rect() - Margins(_st->thickness);
 			_animation.draw(p, r.topLeft(), r.size(), width());
 		}
 
 	private:
+		const style::InfiniteRadialAnimation *_st;
 		Ui::InfiniteRadialAnimation _animation;
 
 	};
 
-	return Ui::CreateChild<Widget>(parent.get(), size);
+	return Ui::CreateChild<Widget>(parent.get(), size, st);
 }
 
 void AddChildToWidgetCenter(
diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h
index 221f6017f..bc42a9d1c 100644
--- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h
+++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/boost_badge.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 namespace style {
+struct InfiniteRadialAnimation;
 struct TextStyle;
 } // namespace style
 
@@ -30,7 +31,8 @@ namespace Info::Statistics {
 
 [[nodiscard]] not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
 	not_null<Ui::RpWidget*> parent,
-	int size);
+	int size,
+	const style::InfiniteRadialAnimation *st = nullptr);
 
 void AddChildToWidgetCenter(
 	not_null<Ui::RpWidget*> parent,

From caef698e5406bc63bbcc422ee6855643b8123bf7 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Tue, 30 Jul 2024 13:34:00 +0300
Subject: [PATCH 099/163] Fixed recursive invoking of Application::windowFor.

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

diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index 19f488b0b..b2c2ec4db 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -1341,7 +1341,7 @@ Window::Controller *Application::ensureSeparateWindowFor(
 Window::Controller *Application::windowFor(Window::SeparateId id) const {
 	if (const auto separate = separateWindowFor(id)) {
 		return separate;
-	} else if (id && id.primary()) {
+	} else if (id && !id.primary()) {
 		return windowFor(not_null(id.account));
 	}
 	return activePrimaryWindow();

From a32b781e4966939c3072d7a1297d05ae4755e82c Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Tue, 30 Jul 2024 13:51:07 +0300
Subject: [PATCH 100/163] Improved Controller::invokeForSessionController for
 separate windows.

---
 Telegram/SourceFiles/window/window_controller.cpp | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp
index 8ef614d21..ec9fa548e 100644
--- a/Telegram/SourceFiles/window/window_controller.cpp
+++ b/Telegram/SourceFiles/window/window_controller.cpp
@@ -499,6 +499,15 @@ void Controller::invokeForSessionController(
 	if (separateSession) {
 		return callback(separateSession);
 	}
+	const auto accountWindow = account
+		? Core::App().separateWindowFor(not_null(account))
+		: nullptr;
+	const auto accountSession = accountWindow
+		? accountWindow->sessionController()
+		: nullptr;
+	if (accountSession) {
+		return callback(accountSession);
+	}
 	_id.account->domain().activate(std::move(account));
 	if (_sessionController) {
 		callback(_sessionController.get());

From aeb5e57061137c0fddf6601461c49e53612f203e Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Tue, 30 Jul 2024 13:51:53 +0300
Subject: [PATCH 101/163] Fixed profile opening of group call participant in
 separate windows.

---
 .../calls/group/calls_group_members.cpp       | 30 ++++++-------------
 1 file changed, 9 insertions(+), 21 deletions(-)

diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp
index ce806928c..2853340a5 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp
@@ -1195,24 +1195,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
 	const auto addVolumeItem = (!muted || isMe(participantPeer));
 	const auto admin = IsGroupCallAdmin(_peer, participantPeer);
 	const auto session = &_peer->session();
-	const auto getCurrentWindow = [=]() -> Window::SessionController* {
-		if (const auto window = Core::App().windowFor(participantPeer)) {
-			if (const auto controller = window->sessionController()) {
-				if (&controller->session() == session) {
-					return controller;
-				}
-			}
-		}
-		return nullptr;
-	};
-	const auto getWindow = [=] {
-		if (const auto current = getCurrentWindow()) {
-			return current;
-		} else if (&Core::App().domain().active() != &session->account()) {
-			Core::App().domain().activate(&session->account());
-		}
-		return getCurrentWindow();
-	};
+	const auto account = &session->account();
 
 	auto result = base::make_unique_q<Ui::PopupMenu>(
 		parent,
@@ -1223,7 +1206,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
 			: st::groupCallPopupMenu));
 	const auto weakMenu = Ui::MakeWeak(result.get());
 	const auto withActiveWindow = [=](auto callback) {
-		if (const auto window = getWindow()) {
+		if (const auto window = Core::App().activePrimaryWindow()) {
 			if (const auto menu = weakMenu.data()) {
 				menu->discardParentReActivate();
 
@@ -1232,8 +1215,13 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
 				// PopupMenu::hide activates back the group call panel :(
 				delete weakMenu;
 			}
-			callback(window);
-			window->widget()->activate();
+			window->invokeForSessionController(
+				account,
+				participantPeer,
+				[&](not_null<Window::SessionController*> newController) {
+					callback(newController);
+					newController->widget()->activate();
+				});
 		}
 	};
 	const auto showProfile = [=] {

From 0046bae53f0888d8d739ea627b691f2e519df110 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Fri, 26 Jul 2024 00:05:25 +0300
Subject: [PATCH 102/163] Added ability to change name of sticker set from
 sticker set box.

---
 Telegram/Resources/langs/lang.strings         |  3 +
 .../SourceFiles/boxes/sticker_set_box.cpp     | 97 ++++++++++++++++++-
 .../chat_helpers/chat_helpers.style           |  5 +
 Telegram/lib_ui                               |  2 +-
 4 files changed, 104 insertions(+), 3 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index b902aa4f6..47b46c6e9 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2929,6 +2929,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_masks_has_been_archived" = "Mask pack has been archived.";
 "lng_masks_installed" = "Mask pack has been installed.";
 "lng_emoji_nothing_found" = "No emoji found";
+"lng_stickers_context_edit_name" = "Edit name";
+"lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
+"lng_stickers_box_edit_name_about" = "Choose a name for your set.";
 
 "lng_in_dlg_photo" = "Photo";
 "lng_in_dlg_album" = "Album";
diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
index b3c589be7..afd4d233f 100644
--- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp
+++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/scroll_area.h"
 #include "ui/widgets/gradient_round_button.h"
+#include "ui/widgets/fields/input_field.h"
 #include "ui/image/image.h"
 #include "ui/image/image_location_factory.h"
 #include "ui/text/text_utilities.h"
@@ -271,6 +272,10 @@ public:
 			: Data::StickersType::Stickers;
 	}
 
+	[[nodiscard]] bool amSetCreator() const {
+		return _amSetCreator;
+	}
+
 	~Inner();
 
 protected:
@@ -366,6 +371,7 @@ private:
 	TimeId _setInstallDate = TimeId(0);
 	StickerType _setThumbnailType = StickerType::Webp;
 	ImageWithLocation _setThumbnail;
+	bool _amSetCreator = false;
 
 	const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
 	mutable StickerPremiumMark _premiumMark;
@@ -538,6 +544,70 @@ void StickerSetBox::updateTitleAndButtons() {
 	updateButtons();
 }
 
+void ChangeSetNameBox(
+		not_null<Ui::GenericBox*> box,
+		not_null<Data::Session*> data,
+		const StickerSetIdentifier &input) {
+	box->setTitle(tr::lng_stickers_box_edit_name_title());
+	box->addRow(
+		object_ptr<Ui::FlatLabel>(
+			box,
+			tr::lng_stickers_box_edit_name_about(),
+			st::boxLabel));
+
+	const auto wasName = [&] {
+		const auto &sets = data->stickers().sets();
+		const auto it = sets.find(input.id);
+		return (it == sets.end()) ? QString() : it->second->title;
+	}();
+	const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
+		box,
+		st::editStickerSetNameField.heightMin));
+	auto owned = object_ptr<Ui::InputField>(
+		wrap,
+		st::editStickerSetNameField,
+		tr::lng_stickers_context_edit_name(),
+		wasName);
+	const auto field = owned.data();
+	wrap->widthValue() | rpl::start_with_next([=](int width) {
+		field->move(0, 0);
+		field->resize(width, field->height());
+		wrap->resize(width, field->height());
+	}, wrap->lifetime());
+	field->selectAll();
+	constexpr auto kMaxSetNameLength = 50;
+	field->setMaxLength(kMaxSetNameLength);
+	Ui::AddLengthLimitLabel(field, kMaxSetNameLength, kMaxSetNameLength + 1);
+	box->setFocusCallback([=] { field->setFocusFast(); });
+	const auto close = crl::guard(box, [=] { box->closeBox(); });
+	const auto save = [=, show = box->uiShow()] {
+		const auto text = field->getLastText().trimmed();
+		if ((Ui::ComputeRealUnicodeCharactersCount(text) > kMaxSetNameLength)
+			|| text.isEmpty()) {
+			field->showError();
+			return;
+		}
+		data->session().api().request(
+			MTPstickers_RenameStickerSet(
+				Data::InputStickerSet(input),
+				MTP_string(text))
+		).done([=](const MTPmessages_StickerSet &result) {
+			result.match([&](const MTPDmessages_stickerSet &d) {
+				data->stickers().feedSetFull(d);
+				data->stickers().notifyUpdated(Data::StickersType::Stickers);
+			}, [](const auto &) {
+			});
+			close();
+		}).fail([=](const MTP::Error &error) {
+			show->showToast(error.type());
+			close();
+		}).send();
+	};
+
+	box->addButton(tr::lng_box_done(), save);
+	box->addButton(tr::lng_cancel(), close);
+}
+
 void StickerSetBox::updateButtons() {
 	clearButtons();
 	if (_inner->loaded()) {
@@ -548,6 +618,20 @@ void StickerSetBox::updateButtons() {
 					? tr::lng_stickers_copied_emoji(tr::now)
 					: tr::lng_stickers_copied(tr::now));
 		};
+		const auto fillSetCreatorMenu = [&] {
+			using Filler = Fn<void(not_null<Ui::PopupMenu*>)>;
+			if (!_inner->amSetCreator()) {
+				return Filler(nullptr);
+			}
+			const auto data = &_session->data();
+			return Filler([=, show = _show, set = _set](
+					not_null<Ui::PopupMenu*> menu) {
+				menu->addAction(
+					tr::lng_stickers_context_edit_name(tr::now),
+					[=] { show->showBox(Box(ChangeSetNameBox, data, set)); },
+					&st::menuIconEdit);
+			});
+		}();
 		if (_inner->notInstalled()) {
 			if (!_session->premium()
 				&& _session->premiumPossible()
@@ -586,6 +670,9 @@ void StickerSetBox::updateButtons() {
 					*menu = base::make_unique_q<Ui::PopupMenu>(
 						top,
 						st::popupMenuWithIcons);
+					if (fillSetCreatorMenu) {
+						fillSetCreatorMenu(*menu);
+					}
 					(*menu)->addAction(
 						((type == Data::StickersType::Emoji)
 							? tr::lng_stickers_share_emoji
@@ -636,6 +723,9 @@ void StickerSetBox::updateButtons() {
 							remove,
 							&st::menuIconRemove);
 					} else {
+						if (fillSetCreatorMenu) {
+							fillSetCreatorMenu(*menu);
+						}
 						(*menu)->addAction(
 							(type == Data::StickersType::Masks
 								? tr::lng_masks_archive_pack(tr::now)
@@ -748,7 +838,9 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) {
 				}
 			});
 		}
-		data.vset().match([&](const MTPDstickerSet &set) {
+
+		{
+			const auto &set = data.vset().data();
 			_setTitle = _session->data().stickers().getSetTitle(
 				set);
 			_setShortName = qs(set.vshort_name());
@@ -759,6 +851,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) {
 			_setFlags = Data::ParseStickersSetFlags(set);
 			_setInstallDate = set.vinstalled_date().value_or(0);
 			_setThumbnailDocumentId = set.vthumb_document_id().value_or_empty();
+			_amSetCreator = set.is_creator();
 			_setThumbnail = [&] {
 				if (const auto thumbs = set.vthumbs()) {
 					for (const auto &thumb : thumbs->v) {
@@ -791,7 +884,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) {
 				set->emoji = _emoji;
 				set->setThumbnail(_setThumbnail, _setThumbnailType);
 			}
-		});
+		};
 	}, [&](const MTPDmessages_stickerSetNotModified &data) {
 		LOG(("API Error: Unexpected messages.stickerSetNotModified."));
 	});
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 47c1e8b47..be9d5f8e4 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1409,6 +1409,11 @@ editTagLimit: FlatLabel(defaultFlatLabel) {
 	textFg: windowSubTextFg;
 }
 
+editStickerSetNameField: InputField(defaultInputField) {
+	textMargins: margins(0px, 28px, 26px, 4px);
+	heightMax: 55px;
+}
+
 paidStarIcon: icon {{ "settings/premium/star", creditsBg1 }};
 paidStarIconTop: 7px;
 paidAmountAbout: FlatLabel(defaultFlatLabel) {
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 99d914213..97bfa6cef 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 99d9142130025e817fe7f1ac278078c34a509a1c
+Subproject commit 97bfa6cef474b3b311a178ff1a1042d09972a7c7

From 06fc813e9509b3abfb33f1710c494aa3f17f7881 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Mon, 29 Jul 2024 11:02:17 +0300
Subject: [PATCH 103/163] Added loading state to box for renaming of stickers
 set.

---
 .../SourceFiles/boxes/sticker_set_box.cpp     | 64 ++++++++++++++++---
 .../chat_helpers/chat_helpers.style           |  4 ++
 2 files changed, 58 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
index afd4d233f..e1906b0c8 100644
--- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp
+++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/stickers/data_stickers.h"
 #include "data/stickers/data_custom_emoji.h"
 #include "menu/menu_send.h"
+#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
 #include "lang/lang_keys.h"
 #include "ui/boxes/confirm_box.h"
 #include "boxes/premium_preview_box.h"
@@ -73,6 +74,7 @@ constexpr auto kGrayLockOpacity = 0.3;
 using Data::StickersSet;
 using Data::StickersPack;
 using SetFlag = Data::StickersSetFlag;
+using TLStickerSet = MTPmessages_StickerSet;
 
 [[nodiscard]] std::optional<QColor> ComputeImageColor(
 		const style::icon &lockIcon,
@@ -276,6 +278,8 @@ public:
 		return _amSetCreator;
 	}
 
+	void applySet(const TLStickerSet &set);
+
 	~Inner();
 
 protected:
@@ -327,7 +331,6 @@ private:
 	void startOverAnimation(int index, float64 from, float64 to);
 	int stickerFromGlobalPos(const QPoint &p) const;
 
-	void gotSet(const MTPmessages_StickerSet &set);
 	void installDone(const MTPmessages_StickerSetInstallResult &result);
 
 	void chosen(
@@ -547,13 +550,19 @@ void StickerSetBox::updateTitleAndButtons() {
 void ChangeSetNameBox(
 		not_null<Ui::GenericBox*> box,
 		not_null<Data::Session*> data,
-		const StickerSetIdentifier &input) {
+		const StickerSetIdentifier &input,
+		Fn<void(TLStickerSet)> done) {
+	struct State final {
+		rpl::variable<mtpRequestId> requestId = 0;
+		Ui::RpWidget* saveButton = nullptr;
+	};
 	box->setTitle(tr::lng_stickers_box_edit_name_title());
 	box->addRow(
 		object_ptr<Ui::FlatLabel>(
 			box,
 			tr::lng_stickers_box_edit_name_about(),
 			st::boxLabel));
+	const auto state = box->lifetime().make_state<State>();
 
 	const auto wasName = [&] {
 		const auto &sets = data->stickers().sets();
@@ -581,31 +590,59 @@ void ChangeSetNameBox(
 	box->setFocusCallback([=] { field->setFocusFast(); });
 	const auto close = crl::guard(box, [=] { box->closeBox(); });
 	const auto save = [=, show = box->uiShow()] {
+		if (state->requestId.current()) {
+			return;
+		}
 		const auto text = field->getLastText().trimmed();
 		if ((Ui::ComputeRealUnicodeCharactersCount(text) > kMaxSetNameLength)
 			|| text.isEmpty()) {
 			field->showError();
 			return;
 		}
-		data->session().api().request(
+		const auto buttonWidth = state->saveButton
+			? state->saveButton->width()
+			: 0;
+		state->requestId = data->session().api().request(
 			MTPstickers_RenameStickerSet(
 				Data::InputStickerSet(input),
 				MTP_string(text))
-		).done([=](const MTPmessages_StickerSet &result) {
+		).done([=](const TLStickerSet &result) {
 			result.match([&](const MTPDmessages_stickerSet &d) {
 				data->stickers().feedSetFull(d);
 				data->stickers().notifyUpdated(Data::StickersType::Stickers);
 			}, [](const auto &) {
 			});
+			done(result);
 			close();
 		}).fail([=](const MTP::Error &error) {
 			show->showToast(error.type());
 			close();
 		}).send();
+		if (state->saveButton) {
+			state->saveButton->resizeToWidth(buttonWidth);
+		}
 	};
 
-	box->addButton(tr::lng_box_done(), save);
-	box->addButton(tr::lng_cancel(), close);
+	state->saveButton = box->addButton(
+		rpl::conditional(
+			state->requestId.value() | rpl::map(rpl::mappers::_1 > 0),
+			rpl::single(QString()),
+			tr::lng_box_done()),
+		save);
+	if (const auto saveButton = state->saveButton) {
+		using namespace Info::Statistics;
+		const auto loadingAnimation = InfiniteRadialAnimationWidget(
+			saveButton,
+			saveButton->height() / 2,
+			&st::editStickerSetNameLoading);
+		AddChildToWidgetCenter(saveButton, loadingAnimation);
+		loadingAnimation->showOn(
+			state->requestId.value() | rpl::map(rpl::mappers::_1 > 0));
+	}
+	box->addButton(tr::lng_cancel(), [=] {
+		data->session().api().request(state->requestId.current()).cancel();
+		close();
+	});
 }
 
 void StickerSetBox::updateButtons() {
@@ -626,9 +663,16 @@ void StickerSetBox::updateButtons() {
 			const auto data = &_session->data();
 			return Filler([=, show = _show, set = _set](
 					not_null<Ui::PopupMenu*> menu) {
+				const auto done = [inner = _inner](const TLStickerSet &set) {
+					if (const auto raw = inner.data()) {
+						raw->applySet(set);
+					}
+				};
 				menu->addAction(
 					tr::lng_stickers_context_edit_name(tr::now),
-					[=] { show->showBox(Box(ChangeSetNameBox, data, set)); },
+					[=] {
+						show->showBox(Box(ChangeSetNameBox, data, set, done));
+					},
 					&st::menuIconEdit);
 			});
 		}();
@@ -777,8 +821,8 @@ StickerSetBox::Inner::Inner(
 	_api.request(MTPmessages_GetStickerSet(
 		Data::InputStickerSet(_input),
 		MTP_int(0) // hash
-	)).done([=](const MTPmessages_StickerSet &result) {
-		gotSet(result);
+	)).done([=](const TLStickerSet &result) {
+		applySet(result);
 	}).fail([=] {
 		_loaded = true;
 		_errors.fire(Error::NotFound);
@@ -794,7 +838,7 @@ StickerSetBox::Inner::Inner(
 	setMouseTracking(true);
 }
 
-void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) {
+void StickerSetBox::Inner::applySet(const TLStickerSet &set) {
 	_pack.clear();
 	_emoji.clear();
 	_elements.clear();
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index be9d5f8e4..40ab1d705 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1413,6 +1413,10 @@ editStickerSetNameField: InputField(defaultInputField) {
 	textMargins: margins(0px, 28px, 26px, 4px);
 	heightMax: 55px;
 }
+editStickerSetNameLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
+	color: lightButtonFg;
+	thickness: 2px;
+}
 
 paidStarIcon: icon {{ "settings/premium/star", creditsBg1 }};
 paidStarIconTop: 7px;

From 54214ff2ad311daaa62982800c60856b7d5b74b9 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Sat, 27 Jul 2024 11:22:39 +0300
Subject: [PATCH 104/163] Added initial ability to drag stickers in sticker set
 box.

---
 Telegram/Resources/langs/lang.strings         |   1 +
 .../SourceFiles/boxes/sticker_set_box.cpp     | 249 +++++++++++++++++-
 Telegram/lib_base                             |   2 +-
 3 files changed, 243 insertions(+), 9 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 47b46c6e9..3aa6ac586 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2929,6 +2929,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_masks_has_been_archived" = "Mask pack has been archived.";
 "lng_masks_installed" = "Mask pack has been installed.";
 "lng_emoji_nothing_found" = "No emoji found";
+"lng_stickers_context_reorder" = "Reorder";
 "lng_stickers_context_edit_name" = "Edit name";
 "lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
 "lng_stickers_box_edit_name_about" = "Choose a name for your set.";
diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
index e1906b0c8..3cc095421 100644
--- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp
+++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
@@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/path_shift_gradient.h"
 #include "ui/emoji_config.h"
 #include "ui/painter.h"
+#include "ui/rect.h"
 #include "ui/power_saving.h"
 #include "ui/toast/toast.h"
 #include "ui/widgets/popup_menu.h"
@@ -70,6 +71,7 @@ constexpr auto kEmojiPerRow = 8;
 constexpr auto kMinRepaintDelay = crl::time(33);
 constexpr auto kMinAfterScrollDelay = crl::time(33);
 constexpr auto kGrayLockOpacity = 0.3;
+constexpr auto kStickerMoveDuration = crl::time(200);
 
 using Data::StickersSet;
 using Data::StickersPack;
@@ -262,6 +264,13 @@ public:
 	[[nodiscard]] rpl::producer<uint64> setArchived() const;
 	[[nodiscard]] rpl::producer<> updateControls() const;
 
+	void setReorderState(bool enabled) {
+		_dragging.enabled = enabled;
+	}
+	[[nodiscard]] bool reorderState() const {
+		return _dragging.enabled;
+	}
+
 	[[nodiscard]] rpl::producer<Error> errors() const;
 
 	void archiveStickers();
@@ -338,6 +347,9 @@ private:
 		not_null<DocumentData*> sticker,
 		Api::SendOptions options);
 
+	[[nodiscard]] QPoint posFromIndex(int index) const;
+	[[nodiscard]] bool isDraggedAnimating() const;
+
 	not_null<Lottie::MultiPlayer*> getLottiePlayer();
 
 	void showPreview();
@@ -376,6 +388,20 @@ private:
 	ImageWithLocation _setThumbnail;
 	bool _amSetCreator = false;
 
+	struct {
+		bool enabled = false;
+		int index = -1;
+		int lastSelected = -1;
+		QPoint point;
+	} _dragging;
+
+	struct ShiftAnimation final {
+		Ui::Animations::Simple animation;
+		Ui::Animations::Simple yAnimation;
+		int shift = 0;
+	};
+	base::flat_map<int, ShiftAnimation> _shiftAnimations;
+
 	const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
 	mutable StickerPremiumMark _premiumMark;
 
@@ -647,7 +673,12 @@ void ChangeSetNameBox(
 
 void StickerSetBox::updateButtons() {
 	clearButtons();
-	if (_inner->loaded()) {
+	if (_inner->reorderState()) {
+		addButton(tr::lng_box_done(), [=] {
+			_inner->setReorderState(false);
+			updateButtons();
+		});
+	} else if (_inner->loaded()) {
 		const auto type = _inner->setType();
 		const auto share = [=] {
 			copyStickersLink();
@@ -674,6 +705,13 @@ void StickerSetBox::updateButtons() {
 						show->showBox(Box(ChangeSetNameBox, data, set, done));
 					},
 					&st::menuIconEdit);
+				menu->addAction(
+					tr::lng_stickers_context_reorder(tr::now),
+					[=] {
+						_inner->setReorderState(true);
+						updateButtons();
+					},
+					&st::menuIconManage);
 			});
 		}();
 		if (_inner->notInstalled()) {
@@ -1069,11 +1107,100 @@ void StickerSetBox::Inner::mousePressEvent(QMouseEvent *e) {
 	if (index < 0 || index >= _pack.size()) {
 		return;
 	}
+	if (_dragging.enabled) {
+		_previewTimer.cancel();
+		if (isDraggedAnimating()) {
+			return;
+		}
+		_dragging.index = index;
+		_dragging.point = mapFromGlobal(QCursor::pos()) - posFromIndex(index);
+		return;
+	}
 	_previewTimer.callOnce(QApplication::startDragTime());
 }
 
 void StickerSetBox::Inner::mouseMoveEvent(QMouseEvent *e) {
 	updateSelected();
+	const auto draggedAnimating = isDraggedAnimating();
+	if (_selected >= 0 && !draggedAnimating) {
+		_dragging.lastSelected = _selected;
+	}
+	if (_dragging.index >= 0
+		&& _dragging.index < _pack.size()
+		&& _dragging.lastSelected >= 0
+		&& !draggedAnimating) {
+		for (auto i = 0; i < _pack.size(); i++) {
+			if (i == _dragging.index) {
+				continue;
+			}
+			auto &entry = _shiftAnimations[i];
+			const auto wasShift = entry.shift;
+			if ((i >= _dragging.index) && (i <= _dragging.lastSelected)) {
+				if (entry.shift == 0) {
+					entry.shift = -1;
+				} else if (entry.shift == 1) {
+					entry.shift = 0;
+				}
+			} else if ((i < _dragging.index)
+					&& (i >= _dragging.lastSelected)) {
+				if (entry.shift == 0) {
+					entry.shift = 1;
+				} else if (entry.shift == -1) {
+					entry.shift = 0;
+				}
+			}
+			if ((i < std::min(_dragging.index, _dragging.lastSelected))
+				|| (i > std::max(_dragging.index, _dragging.lastSelected))) {
+				entry.shift = 0;
+			}
+			if (wasShift != entry.shift) {
+				const auto fromPoint = posFromIndex(i + wasShift);
+				const auto toPoint = posFromIndex(i + entry.shift);
+				const auto toX = float64(toPoint.x());
+				const auto toY = float64(toPoint.y());
+				const auto ratio = [&] {
+					const auto fromX = entry.animation.value(toX);
+					const auto ratioX = std::min(toX, fromX)
+						/ std::max(toX, fromX);
+					const auto fromY = entry.yAnimation.value(toY);
+					const auto ratioY = std::min(toY, fromY)
+						/ std::max(toY, fromY);
+					return (ratioX == 1.)
+						? ratioY
+						: (ratioY == 1.)
+						? ratioX
+						: std::max(ratioX, ratioY);
+				}();
+				if (!entry.animation.animating()) {
+					entry.animation.stop();
+					entry.animation.start(
+						[=] { update(); },
+						fromPoint.x(),
+						toX,
+						kStickerMoveDuration);
+				} else {
+					entry.animation.change(
+						toX,
+						kStickerMoveDuration * (1. - ratio),
+						anim::linear);
+				}
+				if (!entry.yAnimation.animating()) {
+					entry.yAnimation.stop();
+					entry.yAnimation.start(
+						[=] { update(); },
+						fromPoint.y(),
+						toY,
+						kStickerMoveDuration);
+				} else {
+					entry.yAnimation.change(
+						toY,
+						kStickerMoveDuration * (1. - ratio),
+						anim::linear);
+				}
+			}
+		}
+		update();
+	}
 	if (_previewShown >= 0) {
 		showPreviewAt(e->globalPos());
 	}
@@ -1096,6 +1223,46 @@ void StickerSetBox::Inner::leaveEventHook(QEvent *e) {
 }
 
 void StickerSetBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
+	if (_dragging.index >= 0 && !isDraggedAnimating()) {
+		const auto fromPos = mapFromGlobal(e->globalPos()) - _dragging.point;
+		const auto toPos = posFromIndex(_dragging.lastSelected);
+		const auto finish = [=] {
+			base::reorder(_pack, _dragging.index, _dragging.lastSelected);
+			base::reorder(_elements, _dragging.index, _dragging.lastSelected);
+			_dragging = {};
+			_dragging.enabled = true;
+			_shiftAnimations.clear();
+		};
+		auto &entry = _shiftAnimations[_dragging.index];
+		entry.animation.stop();
+		entry.yAnimation.stop();
+		entry.animation.start(
+			[finish, toPos, this](float64 value) {
+				const auto index = _dragging.index;
+				if (value >= toPos.x()
+					&& index >= 0
+					&& !_shiftAnimations[index].yAnimation.animating()) {
+					finish();
+				}
+				update();
+			},
+			fromPos.x(),
+			toPos.x(),
+			kStickerMoveDuration);
+		entry.yAnimation.start(
+			[finish, toPos, this](float64 value) {
+				const auto index = _dragging.index;
+				if (value >= toPos.y()
+					&& index >= 0
+					&& !_shiftAnimations[index].animation.animating()) {
+					finish();
+				}
+				update();
+			},
+			fromPos.y(),
+			toPos.y(),
+			kStickerMoveDuration);
+	}
 	if (_previewShown >= 0) {
 		_previewShown = -1;
 		return;
@@ -1216,7 +1383,11 @@ void StickerSetBox::Inner::setSelected(int selected) {
 		startOverAnimation(_selected, 1., 0.);
 		_selected = selected;
 		startOverAnimation(_selected, 0., 1.);
-		setCursor(_selected >= 0 ? style::cur_pointer : style::cur_default);
+		setCursor((_selected < 0)
+			? style::cur_default
+			: _dragging.enabled
+			? style::cur_sizeall
+			: style::cur_pointer);
 	}
 }
 
@@ -1238,6 +1409,24 @@ void StickerSetBox::Inner::showPreview() {
 	showPreviewAt(QCursor::pos());
 }
 
+QPoint StickerSetBox::Inner::posFromIndex(int index) const {
+	return {
+		_padding.left() + (index % _perRow) * _singleSize.width(),
+		_padding.top() + (index / _perRow) * _singleSize.height(),
+	};
+}
+
+bool StickerSetBox::Inner::isDraggedAnimating() const {
+	if (_dragging.index < 0) {
+		return false;
+	}
+	const auto it = _shiftAnimations.find(_dragging.index);
+	return (it == _shiftAnimations.end())
+		? false
+		: (it->second.animation.animating()
+			|| it->second.yAnimation.animating());
+}
+
 not_null<Lottie::MultiPlayer*> StickerSetBox::Inner::getLottiePlayer() {
 	if (!_lottiePlayer) {
 		_lottiePlayer = std::make_unique<Lottie::MultiPlayer>(
@@ -1277,12 +1466,36 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) {
 
 	_pathGradient->startFrame(0, width(), width() / 2);
 
+	const auto indexUnderCursor = (_dragging.index >= 0
+			&& _dragging.index < _elements.size())
+		? stickerFromGlobalPos(QCursor::pos())
+		: -2;
+	const auto lastIndex = indexUnderCursor >= 0
+		? indexUnderCursor
+		: _dragging.lastSelected;
+
 	const auto now = crl::now();
 	const auto paused = On(PowerSaving::kStickersPanel)
 		|| _show->paused(ChatHelpers::PauseReason::Layer);
 	for (int32 i = from; i < to; ++i) {
 		for (int32 j = 0; j < _perRow; ++j) {
 			int32 index = i * _perRow + j;
+
+			if (lastIndex >= 0) {
+				if (_dragging.index == index) {
+					continue;
+				}
+				const auto it = _shiftAnimations.find(index);
+				if (it != _shiftAnimations.end()) {
+					const auto &entry = it->second;
+					const auto toPos = posFromIndex(index + entry.shift);
+					const auto pos = QPoint(
+						entry.animation.value(toPos.x()),
+						entry.yAnimation.value(toPos.y()));
+					paintSticker(p, index, pos, paused, now);
+					continue;
+				}
+			}
 			if (index >= _elements.size()) {
 				break;
 			}
@@ -1292,6 +1505,14 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) {
 			paintSticker(p, index, pos, paused, now);
 		}
 	}
+	if (_dragging.index >= 0 && _dragging.index < _elements.size()) {
+		const auto pos = isDraggedAnimating()
+			? QPoint(
+				_shiftAnimations[_dragging.index].animation.value(0),
+				_shiftAnimations[_dragging.index].yAnimation.value(0))
+			: (mapFromGlobal(QCursor::pos()) - _dragging.point);
+		paintSticker(p, _dragging.index, pos, paused, now);
+	}
 
 	if (_lottiePlayer && !paused) {
 		_lottiePlayer->markFrameShown();
@@ -1453,12 +1674,24 @@ void StickerSetBox::Inner::paintSticker(
 		QPoint position,
 		bool paused,
 		crl::time now) const {
-	if (const auto over = _elements[index].overAnimation.value((index == _selected) ? 1. : 0.)) {
-		p.setOpacity(over);
-		auto tl = position;
-		if (rtl()) tl.setX(width() - tl.x() - _singleSize.width());
-		Ui::FillRoundRect(p, QRect(tl, _singleSize), st::emojiPanHover, Ui::StickerHoverCorners);
-		p.setOpacity(1);
+	if (_dragging.index != index) {
+		const auto over = _elements[index].overAnimation.value(
+			(index == _selected) ? 1. : 0.);
+		if (over) {
+			p.setOpacity(over);
+			Ui::FillRoundRect(
+				p,
+				QRect(
+					rtl()
+						? QPoint(
+							width() - position.x() - _singleSize.width(),
+							position.y())
+						: position,
+					_singleSize),
+				st::emojiPanHover,
+				Ui::StickerHoverCorners);
+			p.setOpacity(1);
+		}
 	}
 
 	const auto &element = _elements[index];
diff --git a/Telegram/lib_base b/Telegram/lib_base
index 21a8611ab..ca4503b30 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit 21a8611ab764acc7cb622859ea4c97bd259570af
+Subproject commit ca4503b3075fcaed5719b6ff1f40e40d14d08d95

From 358e58680190fdc7d94cbf6bc8bee797bb57b8f9 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Mon, 29 Jul 2024 14:17:29 +0300
Subject: [PATCH 105/163] Added api support to reorder stickers from sticker
 set box.

---
 .../SourceFiles/boxes/sticker_set_box.cpp     | 49 +++++++++++++++++--
 1 file changed, 46 insertions(+), 3 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
index 3cc095421..48fe9828b 100644
--- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp
+++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
@@ -342,6 +342,8 @@ private:
 
 	void installDone(const MTPmessages_StickerSetInstallResult &result);
 
+	void requestReorder(not_null<DocumentData*> document, int index);
+
 	void chosen(
 		int index,
 		not_null<DocumentData*> sticker,
@@ -394,6 +396,8 @@ private:
 		int lastSelected = -1;
 		QPoint point;
 	} _dragging;
+	std::deque<Fn<void()>> _reorderRequests;
+	std::optional<MTP::Sender> _apiReorder;
 
 	struct ShiftAnimation final {
 		Ui::Animations::Simple animation;
@@ -1222,13 +1226,52 @@ void StickerSetBox::Inner::leaveEventHook(QEvent *e) {
 	setSelected(-1);
 }
 
+void StickerSetBox::Inner::requestReorder(
+		not_null<DocumentData*> document,
+		int index) {
+	if (!_apiReorder) {
+		_apiReorder.emplace(&_session->mtp());
+	}
+	_reorderRequests.emplace_back([document, index, this] {
+		_apiReorder->request(
+			MTPstickers_ChangeStickerPosition(
+				document->mtpInput(),
+				MTP_int(index))
+			).done([this, document](const TLStickerSet &result) {
+				result.match([&](const MTPDmessages_stickerSet &d) {
+					document->owner().stickers().feedSetFull(d);
+					document->owner().stickers().notifyUpdated(
+						Data::StickersType::Stickers);
+				}, [](const auto &) {
+				});
+				if (!_reorderRequests.empty()) {
+					_reorderRequests.pop_front();
+				}
+				if (_reorderRequests.empty()) {
+					// applySet(result); // Causes stickers blink.
+				} else {
+					_reorderRequests.front()();
+				}
+			}).fail([show = _show](const MTP::Error &error) {
+				show->showToast(error.type());
+			}).send();
+	});
+	if (_reorderRequests.size() == 1) {
+		_reorderRequests.front()();
+	}
+}
+
 void StickerSetBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
 	if (_dragging.index >= 0 && !isDraggedAnimating()) {
 		const auto fromPos = mapFromGlobal(e->globalPos()) - _dragging.point;
 		const auto toPos = posFromIndex(_dragging.lastSelected);
-		const auto finish = [=] {
-			base::reorder(_pack, _dragging.index, _dragging.lastSelected);
-			base::reorder(_elements, _dragging.index, _dragging.lastSelected);
+		const auto document = _pack[_dragging.index];
+		const auto wasPosition = _dragging.index;
+		const auto nowPosition = _dragging.lastSelected;
+		const auto finish = [=, this] {
+			requestReorder(document, nowPosition);
+			base::reorder(_pack, wasPosition, nowPosition);
+			base::reorder(_elements, wasPosition, nowPosition);
 			_dragging = {};
 			_dragging.enabled = true;
 			_shiftAnimations.clear();

From 850155b3be7440ac0e545045f1f9ae32539c17be Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Tue, 30 Jul 2024 02:37:30 +0300
Subject: [PATCH 106/163] Added shake animation while reordering to sticker set
 box.

---
 .../SourceFiles/boxes/sticker_set_box.cpp     | 86 +++++++++++++++++++
 1 file changed, 86 insertions(+)

diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
index 48fe9828b..dd5a01183 100644
--- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp
+++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
@@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/image/image_location_factory.h"
 #include "ui/text/text_utilities.h"
 #include "ui/text/custom_emoji_instance.h"
+#include "ui/effects/animation_value_f.h"
 #include "ui/effects/path_shift_gradient.h"
 #include "ui/emoji_config.h"
 #include "ui/painter.h"
@@ -266,6 +267,13 @@ public:
 
 	void setReorderState(bool enabled) {
 		_dragging.enabled = enabled;
+		if (enabled) {
+			_shakeAnimation.init([=] { update(); });
+			_shakeAnimation.start();
+		} else {
+			_shakeAnimation.stop();
+			update();
+		}
 	}
 	[[nodiscard]] bool reorderState() const {
 		return _dragging.enabled;
@@ -324,6 +332,11 @@ private:
 		QPoint position,
 		bool paused,
 		crl::time now) const;
+	void shakeTransform(
+		QPainter &p,
+		int index,
+		QPoint position,
+		crl::time now) const;
 	void setupLottie(int index);
 	void setupWebm(int index);
 	void clipCallback(
@@ -396,6 +409,7 @@ private:
 		int lastSelected = -1;
 		QPoint point;
 	} _dragging;
+	Ui::Animations::Basic _shakeAnimation;
 	std::deque<Fn<void()>> _reorderRequests;
 	std::optional<MTP::Sender> _apiReorder;
 
@@ -1711,6 +1725,70 @@ void StickerSetBox::Inner::customEmojiRepaint() {
 	update();
 }
 
+void StickerSetBox::Inner::shakeTransform(
+		QPainter &p,
+		int index,
+		QPoint position,
+		crl::time now) const {
+	constexpr auto kShakeADuration = crl::time(400);
+	constexpr auto kShakeXDuration = crl::time(kShakeADuration * 1.2);
+	constexpr auto kShakeYDuration = kShakeADuration;
+	const auto diff = ((index % 2) ? 0 : kShakeYDuration / 2)
+		+ (now - _shakeAnimation.started());
+	const auto pX = (diff % kShakeXDuration)
+		/ float64(kShakeXDuration);
+	const auto pY = (diff % kShakeYDuration)
+		/ float64(kShakeYDuration);
+	const auto pA = (diff % kShakeADuration)
+		/ float64(kShakeADuration);
+
+	constexpr auto kMaxA = 2.;
+	constexpr auto kMaxTranslation = .5;
+	constexpr auto kAStep = 1. / 5;
+	constexpr auto kXStep = 1. / 5;
+	constexpr auto kYStep = 1. / 4;
+
+	// 0, -kMaxA, 0, kMaxA, 0.
+	const auto angle = (pA < kAStep)
+		? anim::interpolateF(0., -kMaxA, pA / kAStep)
+		: (pA < kAStep * 2.)
+		? anim::interpolateF(-kMaxA, 0, (pA - kAStep) / kAStep)
+		: (pA < kAStep * 3.)
+		? anim::interpolateF(0, kMaxA, (pA - kAStep * 2.) / kAStep)
+		: (pA < kAStep * 4.)
+		? anim::interpolateF(kMaxA, 0, (pA - kAStep * 3.) / kAStep)
+		: anim::interpolateF(0, 0., (pA - kAStep * 4.) / kAStep);
+
+	// 0, kMaxTranslation, 0, -kMaxTranslation, 0.
+	const auto x = (pX < kXStep)
+		? anim::interpolateF(0., kMaxTranslation, pX / kXStep)
+		: (pX < kXStep * 2.)
+		? anim::interpolateF(kMaxTranslation, 0, (pX - kXStep) / kXStep)
+		: (pX < kXStep * 3.)
+		? anim::interpolateF(0, -kMaxTranslation, (pX - kXStep * 2.) / kXStep)
+		: (pX < kXStep * 4.)
+		? anim::interpolateF(-kMaxTranslation, 0, (pX - kXStep * 3.) / kXStep)
+		: anim::interpolateF(0, 0., (pX - kXStep * 4.) / kXStep);
+
+	// 0, kMaxTranslation, -kMaxTranslation, 0.
+	const auto y = (pY < kYStep)
+		? anim::interpolateF(0., kMaxTranslation, pY / kYStep)
+		: (pY < kYStep * 2.)
+		? anim::interpolateF(kMaxTranslation, 0, (pY - kYStep) / kYStep)
+		: (pY < kYStep * 3.)
+		? anim::interpolateF(0, -kMaxTranslation, (pY - kYStep * 2.) / kYStep)
+		: anim::interpolateF(-kMaxTranslation, 0, (pY - kYStep * 3) / kYStep);
+
+	const auto center = position + QPoint(
+		_singleSize.width() / 2,
+		_singleSize.height() / 2);
+
+	p.translate(center);
+	p.rotate(angle);
+	p.translate(-center);
+	p.translate(x, y);
+}
+
 void StickerSetBox::Inner::paintSticker(
 		Painter &p,
 		int index,
@@ -1737,6 +1815,11 @@ void StickerSetBox::Inner::paintSticker(
 		}
 	}
 
+	const auto hasShake = _shakeAnimation.animating();
+	if (hasShake) {
+		shakeTransform(p, index, position, now);
+	}
+
 	const auto &element = _elements[index];
 	const auto document = element.document;
 	const auto &media = element.documentMedia;
@@ -1803,6 +1886,9 @@ void StickerSetBox::Inner::paintSticker(
 			_singleSize,
 			width());
 	}
+	if (hasShake) {
+		p.resetTransform();
+	}
 }
 
 bool StickerSetBox::Inner::loaded() const {

From 6a167b33f5222556d18fd505d258e79158fb31d2 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Tue, 30 Jul 2024 15:41:43 +0300
Subject: [PATCH 107/163] Added ability to delete sticker from sticker set box.

---
 Telegram/Resources/langs/lang.strings         |   2 +
 .../SourceFiles/boxes/sticker_set_box.cpp     | 205 +++++++++++++++---
 2 files changed, 174 insertions(+), 33 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 3aa6ac586..57dbaad87 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2931,6 +2931,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_emoji_nothing_found" = "No emoji found";
 "lng_stickers_context_reorder" = "Reorder";
 "lng_stickers_context_edit_name" = "Edit name";
+"lng_stickers_context_delete" = "Delete sticker";
+"lng_stickers_context_delete_sure" = "Are you sure you want to delete the sticker from your sticker set?";
 "lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
 "lng_stickers_box_edit_name_about" = "Choose a name for your set.";
 
diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
index dd5a01183..aef0dd941 100644
--- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp
+++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
@@ -7,54 +7,55 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "boxes/sticker_set_box.h"
 
+#include "api/api_common.h"
+#include "api/api_toggling_media.h"
+#include "apiwrap.h"
+#include "base/unixtime.h"
+#include "boxes/premium_preview_box.h"
+#include "chat_helpers/compose/compose_show.h"
+#include "chat_helpers/stickers_list_widget.h"
+#include "chat_helpers/stickers_lottie.h"
+#include "core/application.h"
 #include "data/data_document.h"
-#include "data/data_session.h"
-#include "data/data_file_origin.h"
 #include "data/data_document_media.h"
+#include "data/data_file_origin.h"
 #include "data/data_peer_values.h"
-#include "data/stickers/data_stickers.h"
+#include "data/data_session.h"
 #include "data/stickers/data_custom_emoji.h"
-#include "menu/menu_send.h"
+#include "data/stickers/data_stickers.h"
+#include "dialogs/ui/dialogs_layout.h"
 #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
 #include "lang/lang_keys.h"
-#include "ui/boxes/confirm_box.h"
-#include "boxes/premium_preview_box.h"
-#include "core/application.h"
+#include "lottie/lottie_animation.h"
+#include "lottie/lottie_multi_player.h"
+#include "main/main_session.h"
+#include "mainwindow.h"
+#include "media/clip/media_clip_reader.h"
+#include "menu/menu_send.h"
 #include "mtproto/sender.h"
+#include "settings/settings_premium.h"
 #include "storage/storage_account.h"
-#include "dialogs/ui/dialogs_layout.h"
-#include "ui/widgets/buttons.h"
-#include "ui/widgets/scroll_area.h"
-#include "ui/widgets/gradient_round_button.h"
-#include "ui/widgets/fields/input_field.h"
-#include "ui/image/image.h"
-#include "ui/image/image_location_factory.h"
-#include "ui/text/text_utilities.h"
-#include "ui/text/custom_emoji_instance.h"
+#include "ui/boxes/confirm_box.h"
+#include "ui/cached_round_corners.h"
 #include "ui/effects/animation_value_f.h"
 #include "ui/effects/path_shift_gradient.h"
 #include "ui/emoji_config.h"
+#include "ui/image/image.h"
+#include "ui/image/image_location_factory.h"
 #include "ui/painter.h"
-#include "ui/rect.h"
 #include "ui/power_saving.h"
+#include "ui/rect.h"
+#include "ui/text/custom_emoji_instance.h"
+#include "ui/text/text_utilities.h"
 #include "ui/toast/toast.h"
+#include "ui/vertical_list.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/fields/input_field.h"
+#include "ui/widgets/gradient_round_button.h"
+#include "ui/widgets/menu/menu_add_action_callback.h"
+#include "ui/widgets/menu/menu_add_action_callback_factory.h"
 #include "ui/widgets/popup_menu.h"
-#include "ui/cached_round_corners.h"
-#include "lottie/lottie_multi_player.h"
-#include "lottie/lottie_animation.h"
-#include "chat_helpers/compose/compose_show.h"
-#include "chat_helpers/stickers_lottie.h"
-#include "chat_helpers/stickers_list_widget.h"
-#include "media/clip/media_clip_reader.h"
-#include "window/window_controller.h"
-#include "settings/settings_premium.h"
-#include "base/unixtime.h"
-#include "main/main_session.h"
-#include "apiwrap.h"
-#include "api/api_toggling_media.h"
-#include "api/api_common.h"
-#include "mainwidget.h"
-#include "mainwindow.h"
+#include "ui/widgets/scroll_area.h"
 #include "styles/style_layers.h"
 #include "styles/style_chat_helpers.h"
 #include "styles/style_info.h"
@@ -356,6 +357,7 @@ private:
 	void installDone(const MTPmessages_StickerSetInstallResult &result);
 
 	void requestReorder(not_null<DocumentData*> document, int index);
+	void fillDeleteStickerBox(not_null<Ui::GenericBox*> box, int index);
 
 	void chosen(
 		int index,
@@ -1422,6 +1424,20 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
 			(isFaved
 				? &st::menuIconUnfave
 				: &st::menuIconFave));
+		if (amSetCreator()) {
+			const auto addAction = Ui::Menu::CreateAddActionCallback(
+				_menu.get());
+			addAction({
+				.text = tr::lng_stickers_context_delete(tr::now),
+				.handler = [index, this, show = _show] {
+					show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
+						fillDeleteStickerBox(box, index);
+					}));
+				},
+				.icon = &st::menuIconDeleteAttention,
+				.isAttention = true,
+			});
+		}
 	}
 	if (_menu->empty()) {
 		_menu = nullptr;
@@ -1430,6 +1446,129 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
 	}
 }
 
+void StickerSetBox::Inner::fillDeleteStickerBox(
+		not_null<Ui::GenericBox*> box,
+		int index) {
+	Expects(index >= 0 || index < _pack.size());
+	const auto document = _pack[index];
+	const auto weak = Ui::MakeWeak(this);
+	const auto show = _show;
+
+	const auto container = box->verticalLayout();
+	Ui::AddSkip(container);
+	Ui::AddSkip(container);
+	const auto line = container->add(object_ptr<Ui::RpWidget>(container));
+	line->resize(line->width(), _singleSize.height());
+
+	const auto sticker = Ui::CreateChild<Ui::RpWidget>(line);
+	auto &lifetime = sticker->lifetime();
+	struct State final {
+		rpl::variable<mtpRequestId> requestId = 0;
+		Ui::RpWidget* saveButton = nullptr;
+	};
+	const auto state = lifetime.make_state<State>();
+	sticker->resize(_singleSize);
+	{
+		const auto animation = lifetime.make_state<Ui::Animations::Basic>();
+		animation->init([=] { sticker->update(); });
+		animation->start();
+	}
+	sticker->paintRequest(
+	) | rpl::start_with_next([=] {
+		auto p = Painter(sticker);
+		if (const auto strong = weak.get()) {
+			const auto paused = On(PowerSaving::kStickersPanel)
+				|| show->paused(ChatHelpers::PauseReason::Layer);
+			paintSticker(p, index, QPoint(), paused, crl::now());
+			if (_lottiePlayer && !paused) {
+				_lottiePlayer->markFrameShown();
+			}
+		}
+	}, sticker->lifetime());
+	const auto label = Ui::CreateChild<Ui::FlatLabel>(
+		line,
+		tr::lng_stickers_context_delete(),
+		box->getDelegate()->style().title);
+	line->widthValue(
+	) | rpl::start_with_next([=](int width) {
+		sticker->moveToLeft(st::boxRowPadding.left(), 0);
+		const auto skip = st::defaultBoxCheckbox.textPosition.x();
+		label->resizeToWidth(width
+			- rect::right(sticker)
+			- skip
+			- st::boxRowPadding.right());
+		label->moveToLeft(
+			rect::right(sticker) + skip,
+			((sticker->height() - label->height()) / 2));
+	}, label->lifetime());
+
+	sticker->setAttribute(Qt::WA_TransparentForMouseEvents);
+	label->setAttribute(Qt::WA_TransparentForMouseEvents);
+
+	Ui::AddSkip(container);
+	Ui::AddSkip(container);
+
+	box->addRow(
+		object_ptr<Ui::FlatLabel>(
+			container,
+			tr::lng_stickers_context_delete_sure(),
+			st::boxLabel));
+	const auto save = [=] {
+		if (state->requestId.current()) {
+			return;
+		}
+		const auto weakBox = Ui::MakeWeak(box);
+		const auto buttonWidth = state->saveButton
+			? state->saveButton->width()
+			: 0;
+		state->requestId = document->owner().session().api().request(
+			MTPstickers_RemoveStickerFromSet(document->mtpInput()
+		)).done([=](const TLStickerSet &result) {
+			result.match([&](const MTPDmessages_stickerSet &d) {
+				document->owner().stickers().feedSetFull(d);
+				document->owner().stickers().notifyUpdated(
+					Data::StickersType::Stickers);
+			}, [](const auto &) {
+			});
+			if (const auto strong = weak.get()) {
+				applySet(result);
+			}
+			if (const auto strongBox = weakBox.get()) {
+				strongBox->closeBox();
+			}
+		}).fail([=](const MTP::Error &error) {
+			if (const auto strongBox = weakBox.get()) {
+				strongBox->uiShow()->showToast(error.type());
+			}
+		}).send();
+		if (state->saveButton) {
+			state->saveButton->resizeToWidth(buttonWidth);
+		}
+	};
+	state->saveButton = box->addButton(
+		rpl::conditional(
+			state->requestId.value() | rpl::map(rpl::mappers::_1 > 0),
+			rpl::single(QString()),
+			tr::lng_selected_delete()),
+		save,
+		st::attentionBoxButton);
+	if (const auto saveButton = state->saveButton) {
+		using namespace Info::Statistics;
+		const auto loadingAnimation = InfiniteRadialAnimationWidget(
+			saveButton,
+			saveButton->height() / 2,
+			&st::editStickerSetNameLoading);
+		AddChildToWidgetCenter(saveButton, loadingAnimation);
+		loadingAnimation->showOn(
+			state->requestId.value() | rpl::map(rpl::mappers::_1 > 0));
+	}
+	box->addButton(tr::lng_close(), [=] {
+		document->owner().session().api().request(
+			state->requestId.current()).cancel();
+		box->closeBox();
+	});
+}
+
 void StickerSetBox::Inner::updateSelected() {
 	auto selected = stickerFromGlobalPos(QCursor::pos());
 	setSelected(setType() == Data::StickersType::Masks ? -1 : selected);

From 06f2b23687a9f653ba4e5bc82de15f118afa5acf Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 31 Jul 2024 13:05:29 +0300
Subject: [PATCH 108/163] Added badge to header for owned sticker sets in
 stickers list.

---
 Telegram/Resources/langs/lang.strings         |  1 +
 .../chat_helpers/chat_helpers.style           |  4 ++
 .../chat_helpers/stickers_list_widget.cpp     | 42 +++++++++++++++++++
 .../data/stickers/data_stickers_set.cpp       |  3 +-
 .../data/stickers/data_stickers_set.h         |  1 +
 5 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 57dbaad87..7005a219b 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2935,6 +2935,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_stickers_context_delete_sure" = "Are you sure you want to delete the sticker from your sticker set?";
 "lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
 "lng_stickers_box_edit_name_about" = "Choose a name for your set.";
+"lng_stickers_creator_badge" = "edit";
 
 "lng_in_dlg_photo" = "Photo";
 "lng_in_dlg_album" = "Album";
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 40ab1d705..d48f32c00 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -284,6 +284,10 @@ stickersTrendingSubheaderFont: normalFont;
 stickersTrendingSubheaderFg: windowSubTextFg;
 stickersTrendingSubheaderTop: 31px;
 
+stickersHeaderBadgeFont: font(10px);
+stickersHeaderBadgeFontTop: 12px;
+stickersHeaderBadgeFontSkip: 12px;
+
 emojiPanButtonRight: 7px;
 emojiPanButtonTop: 8px;
 emojiPanButton: RoundButton(defaultActiveButton) {
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
index 337658af1..148f4e7d9 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
@@ -932,6 +932,9 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
 	if (sets.empty() && _section == Section::Search) {
 		paintEmptySearchResults(p);
 	}
+	const auto badgeText = tr::lng_stickers_creator_badge(tr::now);
+	const auto &badgeFont = st::stickersHeaderBadgeFont;
+	const auto badgeWidth = badgeFont->width(badgeText);
 	enumerateSections([&](const SectionInfo &info) {
 		if (clip.top() >= info.rowsBottom) {
 			return true;
@@ -1050,6 +1053,12 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
 
 				widthForTitle -= remove.width();
 			}
+			const auto amCreator = (set.flags & Data::StickersSetFlag::AmCreator);
+			if (amCreator) {
+				widthForTitle -= badgeWidth
+					+ st::stickersFeaturedUnreadSkip
+					+ st::stickersHeaderBadgeFontSkip;
+			}
 			if (titleWidth > widthForTitle) {
 				titleText = st::stickersTrendingHeaderFont->elided(titleText, widthForTitle);
 				titleWidth = st::stickersTrendingHeaderFont->width(titleText);
@@ -1057,6 +1066,39 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
 			p.setFont(st::emojiPanHeaderFont);
 			p.setPen(st().headerFg);
 			p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st().headerTop, width(), titleText, titleWidth);
+			if (amCreator) {
+				const auto badgeLeft = st().headerLeft
+					- st().margin.left()
+					+ titleWidth
+					+ st::stickersFeaturedUnreadSkip;
+				{
+					auto color = st().headerFg->c;
+					color.setAlphaF(st().headerFg->c.alphaF() * 0.15);
+					p.setPen(Qt::NoPen);
+					p.setBrush(color);
+					auto hq = PainterHighQualityEnabler(p);
+					p.drawRoundedRect(
+						style::rtlrect(
+							badgeLeft,
+							info.top + st::stickersHeaderBadgeFontTop,
+							badgeWidth + badgeFont->height,
+							badgeFont->height,
+							width()),
+						badgeFont->height / 2.,
+						badgeFont->height / 2.);
+				}
+				p.setPen(st().headerFg);
+				p.setBrush(Qt::NoBrush);
+				p.setFont(badgeFont);
+				p.drawText(
+					QRect(
+						badgeLeft + badgeFont->height / 2,
+						info.top + st::stickersHeaderBadgeFontTop,
+						badgeWidth,
+						badgeFont->height),
+					badgeText,
+					style::al_center);
+			}
 		}
 		if (clip.top() + clip.height() <= info.rowsTop) {
 			return true;
diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp
index 324377242..75d98404e 100644
--- a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp
+++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp
@@ -55,7 +55,8 @@ StickersSetFlags ParseStickersSetFlags(const MTPDstickerSet &data) {
 		| (data.vinstalled_date() ? Flag::Installed : Flag())
 		//| (data.is_videos() ? Flag::Webm : Flag())
 		| (data.is_text_color() ? Flag::TextColor : Flag())
-		| (data.is_channel_emoji_status() ? Flag::ChannelStatus : Flag());
+		| (data.is_channel_emoji_status() ? Flag::ChannelStatus : Flag())
+		| (data.is_creator() ? Flag::AmCreator : Flag());
 }
 
 StickersSet::StickersSet(
diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.h b/Telegram/SourceFiles/data/stickers/data_stickers_set.h
index e77ee46b5..c218ce2fa 100644
--- a/Telegram/SourceFiles/data/stickers/data_stickers_set.h
+++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.h
@@ -59,6 +59,7 @@ enum class StickersSetFlag : ushort {
 	Emoji = (1 << 9),
 	TextColor = (1 << 10),
 	ChannelStatus = (1 << 11),
+	AmCreator = (1 << 12),
 };
 inline constexpr bool is_flag_type(StickersSetFlag) { return true; };
 using StickersSetFlags = base::flags<StickersSetFlag>;

From 3cc92e01fe41e24e297a74b700b80c33242223a7 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 31 Jul 2024 16:23:51 +0300
Subject: [PATCH 109/163] Added ability to force request favorite stickers.

---
 Telegram/SourceFiles/api/api_editing.cpp      |  2 +-
 Telegram/SourceFiles/apiwrap.cpp              | 46 +++++++++++--------
 Telegram/SourceFiles/apiwrap.h                | 12 +++--
 .../data/stickers/data_stickers.cpp           |  4 +-
 4 files changed, 38 insertions(+), 26 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp
index d5f76bf86..662bfc980 100644
--- a/Telegram/SourceFiles/api/api_editing.cpp
+++ b/Telegram/SourceFiles/api/api_editing.cpp
@@ -128,7 +128,7 @@ mtpRequestId EditMessage(
 		}
 
 		if (updateRecentStickers) {
-			api->requestRecentStickersForce(true);
+			api->requestSpecialStickersForce(false, false, true);
 		}
 	}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
 		if constexpr (ErrorWithId<FailCallback>) {
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 04a2e25f1..0f0fa264b 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -2585,7 +2585,7 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &resu
 void ApiWrap::updateStickers() {
 	const auto now = crl::now();
 	requestStickers(now);
-	requestRecentStickers(now);
+	requestRecentStickers(now, false);
 	requestFavedStickers(now);
 	requestFeaturedStickers(now);
 }
@@ -2607,8 +2607,15 @@ void ApiWrap::updateCustomEmoji() {
 	requestFeaturedEmoji(now);
 }
 
-void ApiWrap::requestRecentStickersForce(bool attached) {
-	requestRecentStickersWithHash(0, attached);
+void ApiWrap::requestSpecialStickersForce(
+		bool faved,
+		bool recent,
+		bool attached) {
+	if (faved) {
+		requestFavedStickers(std::nullopt);
+	} else if (recent || attached) {
+		requestRecentStickers(std::nullopt, attached);
+	}
 }
 
 void ApiWrap::setGroupStickerSet(
@@ -2761,18 +2768,17 @@ void ApiWrap::requestCustomEmoji(TimeId now) {
 	}).send();
 }
 
-void ApiWrap::requestRecentStickers(TimeId now, bool attached) {
-	const auto needed = attached
-		? _session->data().stickers().recentAttachedUpdateNeeded(now)
-		: _session->data().stickers().recentUpdateNeeded(now);
+void ApiWrap::requestRecentStickers(
+		std::optional<TimeId> now,
+		bool attached) {
+	const auto needed = !now
+		? true
+		: attached
+		? _session->data().stickers().recentAttachedUpdateNeeded(*now)
+		: _session->data().stickers().recentUpdateNeeded(*now);
 	if (!needed) {
 		return;
 	}
-	requestRecentStickersWithHash(
-		Api::CountRecentStickersHash(_session, attached), attached);
-}
-
-void ApiWrap::requestRecentStickersWithHash(uint64 hash, bool attached) {
 	const auto requestId = [=]() -> mtpRequestId & {
 		return attached
 			? _recentAttachedStickersUpdateRequest
@@ -2795,7 +2801,7 @@ void ApiWrap::requestRecentStickersWithHash(uint64 hash, bool attached) {
 		: MTPmessages_getRecentStickers::Flags(0);
 	requestId() = request(MTPmessages_GetRecentStickers(
 		MTP_flags(flags),
-		MTP_long(hash)
+		MTP_long(now ? Api::CountRecentStickersHash(_session, attached) : 0)
 	)).done([=](const MTPmessages_RecentStickers &result) {
 		finish();
 
@@ -2822,13 +2828,15 @@ void ApiWrap::requestRecentStickersWithHash(uint64 hash, bool attached) {
 	}).send();
 }
 
-void ApiWrap::requestFavedStickers(TimeId now) {
-	if (!_session->data().stickers().favedUpdateNeeded(now)
-		|| _favedStickersUpdateRequest) {
-		return;
+void ApiWrap::requestFavedStickers(std::optional<TimeId> now) {
+	if (now) {
+		if (!_session->data().stickers().favedUpdateNeeded(*now)
+			|| _favedStickersUpdateRequest) {
+			return;
+		}
 	}
 	_favedStickersUpdateRequest = request(MTPmessages_GetFavedStickers(
-		MTP_long(Api::CountFavedStickersHash(_session))
+		MTP_long(now ? Api::CountFavedStickersHash(_session) : 0)
 	)).done([=](const MTPmessages_FavedStickers &result) {
 		_session->data().stickers().setLastFavedUpdate(crl::now());
 		_favedStickersUpdateRequest = 0;
@@ -4204,7 +4212,7 @@ void ApiWrap::sendMediaWithRandomId(
 		), [=](const MTPUpdates &result, const MTP::Response &response) {
 		if (done) done(true);
 		if (updateRecentStickers) {
-			requestRecentStickersForce(true);
+			requestRecentStickers(std::nullopt, true);
 		}
 	}, [=](const MTP::Error &error, const MTP::Response &response) {
 		if (done) done(false);
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 15c0941c3..7259c410d 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -244,7 +244,10 @@ public:
 	void updateSavedGifs();
 	void updateMasks();
 	void updateCustomEmoji();
-	void requestRecentStickersForce(bool attached = false);
+	void requestSpecialStickersForce(
+		bool faved,
+		bool recent,
+		bool attached);
 	void setGroupStickerSet(
 		not_null<ChannelData*> megagroup,
 		const StickerSetIdentifier &set);
@@ -477,9 +480,10 @@ private:
 	void requestStickers(TimeId now);
 	void requestMasks(TimeId now);
 	void requestCustomEmoji(TimeId now);
-	void requestRecentStickers(TimeId now, bool attached = false);
-	void requestRecentStickersWithHash(uint64 hash, bool attached = false);
-	void requestFavedStickers(TimeId now);
+	void requestRecentStickers(
+		std::optional<TimeId> now,
+		bool attached);
+	void requestFavedStickers(std::optional<TimeId> now);
 	void requestFeaturedStickers(TimeId now);
 	void requestFeaturedEmoji(TimeId now);
 	void requestSavedGifs(TimeId now);
diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp
index 9ed24fb38..8000355c2 100644
--- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp
+++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp
@@ -228,7 +228,7 @@ void Stickers::incrementSticker(not_null<DocumentData*> document) {
 	auto index = set->stickers.indexOf(document);
 	if (index > 0) {
 		if (set->dates.empty()) {
-			session().api().requestRecentStickersForce();
+			session().api().requestSpecialStickersForce(false, true, false);
 		} else {
 			Assert(set->dates.size() == set->stickers.size());
 			set->dates.erase(set->dates.begin() + index);
@@ -260,7 +260,7 @@ void Stickers::incrementSticker(not_null<DocumentData*> document) {
 				set->emoji[emoji].push_front(document);
 			}
 		} else {
-			session().api().requestRecentStickersForce();
+			session().api().requestSpecialStickersForce(false, true, false);
 		}
 
 		writeRecentStickers = true;

From f749616dd8851a8bf2471f6da4de9d6d49ae9282 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 31 Jul 2024 16:48:36 +0300
Subject: [PATCH 110/163] Added additional request of special sticker pack on
 failing view pack.

---
 .../chat_helpers/stickers_list_widget.cpp     | 29 ++++++++++++++++---
 .../chat_helpers/stickers_list_widget.h       |  4 ++-
 2 files changed, 28 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
index 148f4e7d9..4258edbac 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 "base/timer_rpl.h"
 #include "core/application.h"
 #include "data/data_document.h"
 #include "data/data_document_media.h"
@@ -1717,12 +1718,32 @@ QPoint StickersListWidget::buttonRippleTopLeft(int section) const {
 		+ st().removeSet.rippleAreaPosition;
 }
 
-void StickersListWidget::showStickerSetBox(not_null<DocumentData*> document) {
+void StickersListWidget::showStickerSetBox(
+		not_null<DocumentData*> document,
+		uint64 setId) {
 	if (document->sticker() && document->sticker()->set) {
 		checkHideWithBox(Box<StickerSetBox>(
 			_show,
 			document->sticker()->set,
 			document->sticker()->setType));
+	} else if ((setId == Data::Stickers::FavedSetId)
+			|| (setId == Data::Stickers::RecentSetId)) {
+		const auto lifetime = std::make_shared<rpl::lifetime>();
+		constexpr auto kTimeout = 10000;
+		rpl::merge(
+			base::timer_once(kTimeout),
+			document->owner().stickers().updated(
+				Data::StickersType::Stickers)
+		) | rpl::start_with_next([=, weak = Ui::MakeWeak(this)] {
+			if (weak.get()) {
+				showStickerSetBox(document, setId);
+			}
+			lifetime->destroy();
+		}, *lifetime);
+		document->owner().session().api().requestSpecialStickersForce(
+			setId == Data::Stickers::FavedSetId,
+			setId == Data::Stickers::RecentSetId,
+			false);
 	}
 }
 
@@ -1779,8 +1800,8 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
 		isFaved ? &icons->menuUnfave : &icons->menuFave);
 
 	if (_features.openStickerSets) {
-		menu->addAction(tr::lng_context_pack_info(tr::now), [=] {
-			showStickerSetBox(document);
+		menu->addAction(tr::lng_context_pack_info(tr::now), [=, id = set.id] {
+			showStickerSetBox(document, id);
 		}, &icons->menuStickerSet);
 	}
 
@@ -1850,7 +1871,7 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) {
 			const auto document = set.stickers[sticker->index].document;
 			if (_features.openStickerSets
 				&& (e->modifiers() & Qt::ControlModifier)) {
-				showStickerSetBox(document);
+				showStickerSetBox(document, set.id);
 			} else {
 				_chosen.fire({
 					.document = document,
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
index f1a89b210..c436273fc 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
@@ -350,7 +350,9 @@ private:
 	void refreshFooterIcons();
 	void refreshIcons(ValidateIconAnimations animations);
 
-	void showStickerSetBox(not_null<DocumentData*> document);
+	void showStickerSetBox(
+		not_null<DocumentData*> document,
+		uint64 setId);
 
 	void cancelSetsSearch();
 	void showSearchResults();

From 5c797d1f316f7e9cca5a3a18f7b9712b22c9df3f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 17 Jul 2024 10:38:50 +0200
Subject: [PATCH 111/163] Update API scheme to layer 185.

---
 Telegram/Resources/langs/lang.strings         |  5 +++
 Telegram/SourceFiles/api/api_media.cpp        |  3 +-
 .../SourceFiles/data/data_media_types.cpp     |  5 ++-
 Telegram/SourceFiles/data/data_media_types.h  | 11 +++++-
 Telegram/SourceFiles/data/data_session.cpp    |  3 +-
 Telegram/SourceFiles/data/data_story.cpp      | 32 ++++++++++++++++
 Telegram/SourceFiles/data/data_story.h        | 11 ++++++
 .../export/data/export_data_types.cpp         |  7 ++++
 .../export/data/export_data_types.h           | 10 ++++-
 .../export/output/export_output_html.cpp      | 10 +++++
 .../export/output/export_output_json.cpp      |  9 +++++
 Telegram/SourceFiles/history/history_item.cpp | 32 +++++++++++++++-
 .../history/view/history_view_fake_items.cpp  |  3 +-
 .../view/media/history_view_premium_gift.cpp  | 30 ++++++++++++---
 .../view/media/history_view_premium_gift.h    |  1 +
 Telegram/SourceFiles/main/main_account.cpp    |  3 +-
 Telegram/SourceFiles/mtproto/scheme/api.tl    | 37 +++++++++++++++----
 .../SourceFiles/payments/payments_form.cpp    | 19 +++-------
 .../SourceFiles/storage/localimageloader.cpp  |  3 +-
 .../storage/serialize_document.cpp            |  3 +-
 20 files changed, 196 insertions(+), 41 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 7005a219b..2311102df 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2854,6 +2854,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_gift_link_pending_toast" = "Only the recipient can see the link.";
 "lng_gift_link_pending_footer" = "This link hasn't been activated yet.";
 
+"lng_gift_stars_title#one" = "{count} Star";
+"lng_gift_stars_title#other" = "{count} Stars";
+"lng_gift_stars_outgoing" = "With Stars, {user} will be able to unlock content and services on Telegram.";
+"lng_gift_stars_incoming" = "Use Stars to unlock content and services on Telegram.";
+
 "lng_accounts_limit_title" = "Limit Reached";
 "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts.";
 "lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts.";
diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp
index a76cb4b67..46a5b7fa3 100644
--- a/Telegram/SourceFiles/api/api_media.cpp
+++ b/Telegram/SourceFiles/api/api_media.cpp
@@ -36,7 +36,8 @@ MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
 				MTP_double(document->duration() / 1000.),
 				MTP_int(dimensions.width()),
 				MTP_int(dimensions.height()),
-				MTPint())); // preload_prefix_size
+				MTPint(), // preload_prefix_size
+				MTPdouble())); // video_start_ts
 		} else {
 			attributes.push_back(MTP_documentAttributeImageSize(
 				MTP_int(dimensions.width()),
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index da16410b0..70d29f28a 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -2303,8 +2303,9 @@ ClickHandlerPtr MediaDice::MakeHandler(
 MediaGiftBox::MediaGiftBox(
 	not_null<HistoryItem*> parent,
 	not_null<PeerData*> from,
-	int months)
-: MediaGiftBox(parent, from, GiftCode{ .months = months }) {
+	GiftType type,
+	int count)
+: MediaGiftBox(parent, from, GiftCode{ .count = count, .type = type }) {
 }
 
 MediaGiftBox::MediaGiftBox(
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 424e2c448..b8c943edc 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -125,10 +125,16 @@ struct GiveawayResults {
 	bool all = false;
 };
 
+enum class GiftType : uchar {
+	Premium, // count - months
+	Stars, // count - stars
+};
+
 struct GiftCode {
 	QString slug;
 	ChannelData *channel = nullptr;
-	int months = 0;
+	int count = 0;
+	GiftType type = GiftType::Premium;
 	bool viaGiveaway = false;
 	bool unclaimed = false;
 };
@@ -591,7 +597,8 @@ public:
 	MediaGiftBox(
 		not_null<HistoryItem*> parent,
 		not_null<PeerData*> from,
-		int months);
+		GiftType type,
+		int count);
 	MediaGiftBox(
 		not_null<HistoryItem*> parent,
 		not_null<PeerData*> from,
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index e43e6423a..cf772208b 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -4533,7 +4533,8 @@ void Session::serviceNotification(
 			MTPVector<MTPUsername>(),
 			MTPint(), // stories_max_id
 			MTPPeerColor(), // color
-			MTPPeerColor())); // profile_color
+			MTPPeerColor(), // profile_color
+			MTPint())); // bot_active_users
 	}
 	const auto history = this->history(PeerData::kServiceNotificationsId);
 	const auto insert = [=] {
diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index 38ea28aae..edd4670d8 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "storage/download_manager_mtproto.h"
 #include "storage/file_download.h" // kMaxFileInMemory
 #include "ui/text/text_utilities.h"
+#include "ui/color_int_conversion.h"
 
 namespace Data {
 namespace {
@@ -43,6 +44,10 @@ using UpdateFlag = StoryUpdate::Flag;
 	};
 }
 
+[[nodiscard]] uint32 ParseMilliKelvin(double celcius) {
+	return uint32(std::clamp(celcius + 273.15, 0., 1'000'000.) * 1000.);
+}
+
 [[nodiscard]] TextWithEntities StripLinks(TextWithEntities text) {
 	const auto link = [&](const EntityInText &entity) {
 		return (entity.type() == EntityType::CustomUrl)
@@ -83,6 +88,7 @@ using UpdateFlag = StoryUpdate::Flag;
 	}, [&](const MTPDmediaAreaSuggestedReaction &data) {
 	}, [&](const MTPDmediaAreaChannelPost &data) {
 	}, [&](const MTPDmediaAreaUrl &data) {
+	}, [&](const MTPDmediaAreaWeather &data) {
 	}, [&](const MTPDinputMediaAreaChannelPost &data) {
 		LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
 	}, [&](const MTPDinputMediaAreaVenue &data) {
@@ -105,6 +111,7 @@ using UpdateFlag = StoryUpdate::Flag;
 		});
 	}, [&](const MTPDmediaAreaChannelPost &data) {
 	}, [&](const MTPDmediaAreaUrl &data) {
+	}, [&](const MTPDmediaAreaWeather &data) {
 	}, [&](const MTPDinputMediaAreaChannelPost &data) {
 		LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
 	}, [&](const MTPDinputMediaAreaVenue &data) {
@@ -127,6 +134,7 @@ using UpdateFlag = StoryUpdate::Flag;
 				data.vmsg_id().v),
 		});
 	}, [&](const MTPDmediaAreaUrl &data) {
+	}, [&](const MTPDmediaAreaWeather &data) {
 	}, [&](const MTPDinputMediaAreaChannelPost &data) {
 		LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
 	}, [&](const MTPDinputMediaAreaVenue &data) {
@@ -147,6 +155,30 @@ using UpdateFlag = StoryUpdate::Flag;
 			.area = ParseArea(data.vcoordinates()),
 			.url = qs(data.vurl()),
 		});
+	}, [&](const MTPDmediaAreaWeather &data) {
+	}, [&](const MTPDinputMediaAreaChannelPost &data) {
+		LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
+	}, [&](const MTPDinputMediaAreaVenue &data) {
+		LOG(("API Error: Unexpected inputMediaAreaVenue from API."));
+	});
+	return result;
+}
+
+[[nodiscard]] auto ParseWeatherArea(const MTPMediaArea &area)
+-> std::optional<WeatherArea> {
+	auto result = std::optional<WeatherArea>();
+	area.match([&](const MTPDmediaAreaVenue &data) {
+	}, [&](const MTPDmediaAreaGeoPoint &data) {
+	}, [&](const MTPDmediaAreaSuggestedReaction &data) {
+	}, [&](const MTPDmediaAreaChannelPost &data) {
+	}, [&](const MTPDmediaAreaUrl &data) {
+	}, [&](const MTPDmediaAreaWeather &data) {
+		result.emplace(WeatherArea{
+			.area = ParseArea(data.vcoordinates()),
+			.emoji = qs(data.vemoji()),
+			.color = Ui::ColorFromSerialized(data.vcolor().v),
+			.millicelcius = int(data.vtemperature_c().v * 1000.),
+		});
 	}, [&](const MTPDinputMediaAreaChannelPost &data) {
 		LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
 	}, [&](const MTPDinputMediaAreaVenue &data) {
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
index b318ce9fe..4dfc7a712 100644
--- a/Telegram/SourceFiles/data/data_story.h
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -131,6 +131,17 @@ struct UrlArea {
 		const UrlArea &) = default;
 };
 
+struct WeatherArea {
+	StoryArea area;
+	QString emoji;
+	QColor color;
+	int millicelcius = 0;
+
+	friend inline bool operator==(
+		const WeatherArea &,
+		const WeatherArea &) = default;
+};
+
 class Story final {
 public:
 	Story(
diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp
index 9e65eac39..f9c9806d0 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.cpp
+++ b/Telegram/SourceFiles/export/data/export_data_types.cpp
@@ -1522,6 +1522,13 @@ ServiceAction ParseServiceAction(
 		content.peerId = ParsePeerId(data.vpeer());
 		content.transactionId = data.vcharge().data().vid().v;
 		result.content = content;
+	}, [&](const MTPDmessageActionGiftStars &data) {
+		auto content = ActionGiftStars();
+		content.cost = Ui::FillAmountAndCurrency(
+			data.vamount().v,
+			qs(data.vcurrency())).toUtf8();
+		content.stars = data.vstars().v;
+		result.content = content;
 	}, [](const MTPDmessageActionEmpty &data) {});
 	return result;
 }
diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h
index 72824a2ee..9639730e4 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.h
+++ b/Telegram/SourceFiles/export/data/export_data_types.h
@@ -529,7 +529,7 @@ struct ActionWebViewDataSent {
 
 struct ActionGiftPremium {
 	Utf8String cost;
-	int months;
+	int months = 0;
 };
 
 struct ActionTopicCreate {
@@ -583,6 +583,11 @@ struct ActionPaymentRefunded {
 	Utf8String transactionId;
 };
 
+struct ActionGiftStars {
+	Utf8String cost;
+	int stars = 0;
+};
+
 struct ServiceAction {
 	std::variant<
 		v::null_t,
@@ -625,7 +630,8 @@ struct ServiceAction {
 		ActionGiveawayLaunch,
 		ActionGiveawayResults,
 		ActionBoostApply,
-		ActionPaymentRefunded> content;
+		ActionPaymentRefunded,
+		ActionGiftStars> content;
 };
 
 ServiceAction ParseServiceAction(
diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp
index d988fc00c..4d3fa397c 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html.cpp
@@ -1321,6 +1321,16 @@ auto HtmlWriter::Wrap::pushMessage(
 			+ " refunded back "
 			+ amount;
 		return result;
+	}, [&](const ActionGiftStars &data) {
+		if (!data.stars || data.cost.isEmpty()) {
+			return serviceFrom + " sent you a gift.";
+		}
+		return serviceFrom
+			+ " sent you a gift for "
+			+ data.cost
+			+ ": "
+			+ QString::number(data.stars).toUtf8()
+			+ " Telegram Stars.";
 	}, [](v::null_t) { return QByteArray(); });
 
 	if (!serviceText.isEmpty()) {
diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp
index 5d4c7e42d..62aaba5f7 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_json.cpp
@@ -632,6 +632,15 @@ QByteArray SerializeMessage(
 		pushBare("peer_name", wrapPeerName(data.peerId));
 		push("peer_id", data.peerId);
 		push("charge_id", data.transactionId);
+	}, [&](const ActionGiftStars &data) {
+		pushActor();
+		pushAction("send_stars_gift");
+		if (!data.cost.isEmpty()) {
+			push("cost", data.cost);
+		}
+		if (data.stars) {
+			push("stars", data.stars);
+		}
 	}, [](v::null_t) {});
 
 	if (v::is_null(message.action.content)) {
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index d2386e7e6..9cee2262e 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -4977,6 +4977,27 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 		return result;
 	};
 
+	auto prepareGiftStars = [&](
+			const MTPDmessageActionGiftStars &action) {
+		auto result = PreparedServiceText();
+		const auto isSelf = (_from->id == _from->session().userPeerId());
+		const auto peer = isSelf ? _history->peer : _from;
+		_history->session().giftBoxStickersPacks().load();
+		const auto amount = action.vamount().v;
+		const auto currency = qs(action.vcurrency());
+		result.links.push_back(peer->createOpenLink());
+		result.text = (isSelf
+			? tr::lng_action_gift_received_me
+			: tr::lng_action_gift_received)(
+				tr::now,
+				lt_user,
+				Ui::Text::Link(peer->name(), 1), // Link 1.
+				lt_cost,
+				{ Ui::FillAmountAndCurrency(amount, currency) },
+				Ui::Text::WithEntities);
+		return result;
+	};
+
 	setServiceText(action.match(
 		prepareChatAddUserText,
 		prepareChatJoinedByLink,
@@ -5020,6 +5041,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 		prepareGiveawayResults,
 		prepareBoostApply,
 		preparePaymentRefunded,
+		prepareGiftStars,
 		PrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>,
 		PrepareErrorText<MTPDmessageActionEmpty>));
 
@@ -5072,6 +5094,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
 		_media = std::make_unique<Data::MediaGiftBox>(
 			this,
 			_from,
+			Data::GiftType::Premium,
 			data.vmonths().v);
 	}, [&](const MTPDmessageActionSuggestProfilePhoto &data) {
 		data.vphoto().match([&](const MTPDphoto &photo) {
@@ -5106,10 +5129,17 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
 				.channel = (boostedId
 					? history()->owner().channel(boostedId).get()
 					: nullptr),
-				.months = data.vmonths().v,
+				.count = data.vmonths().v,
+				.type = Data::GiftType::Premium,
 				.viaGiveaway = data.is_via_giveaway(),
 				.unclaimed = data.is_unclaimed(),
 			});
+	}, [&](const MTPDmessageActionGiftStars &data) {
+		_media = std::make_unique<Data::MediaGiftBox>(
+			this,
+			_from,
+			Data::GiftType::Stars,
+			data.vstars().v);
 	}, [](const auto &) {
 	});
 }
diff --git a/Telegram/SourceFiles/history/view/history_view_fake_items.cpp b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp
index 5a6538036..8390d09fa 100644
--- a/Telegram/SourceFiles/history/view/history_view_fake_items.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp
@@ -59,7 +59,8 @@ PeerId GenerateUser(not_null<History*> history, const QString &name) {
 		MTPVector<MTPUsername>(),
 		MTPint(), // stories_max_id
 		MTPPeerColor(), // color
-		MTPPeerColor())); // profile_color
+		MTPPeerColor(), // profile_color
+		MTPint())); // bot_active_users
 	return peerId;
 }
 
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 b832a6dc3..885e74a62 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/view/history_view_element.h"
 #include "lang/lang_keys.h"
 #include "main/main_session.h"
+#include "settings/settings_credits.h" // Settings::CreditsId
 #include "settings/settings_premium.h" // Settings::ShowGiftPremium
 #include "ui/text/text_utilities.h"
 #include "window/window_session_controller.h"
@@ -45,6 +46,9 @@ QSize PremiumGift::size() {
 }
 
 QString PremiumGift::title() {
+	if (const auto count = stars()) {
+		return tr::lng_gift_stars_title(tr::now, lt_count, count);
+	}
 	return gift()
 		? tr::lng_premium_summary_title(tr::now)
 		: _data.unclaimed
@@ -53,8 +57,16 @@ QString PremiumGift::title() {
 }
 
 TextWithEntities PremiumGift::subtitle() {
-	if (gift()) {
-		return { GiftDuration(_data.months) };
+	if (const auto count = stars()) {
+		return outgoingGift()
+			? tr::lng_gift_stars_outgoing(
+				tr::now,
+				lt_user,
+				Ui::Text::Bold(_parent->history()->peer->shortName()),
+				Ui::Text::WithEntities)
+			: tr::lng_gift_stars_incoming(tr::now, Ui::Text::WithEntities);
+	} else if (gift()) {
+		return { GiftDuration(_data.count) };
 	}
 	const auto name = _data.channel ? _data.channel->name() : "channel";
 	auto result = (_data.unclaimed
@@ -74,7 +86,7 @@ TextWithEntities PremiumGift::subtitle() {
 		: tr::lng_prize_gift_duration)(
 			tr::now,
 			lt_duration,
-			Ui::Text::Bold(GiftDuration(_data.months)),
+			Ui::Text::Bold(GiftDuration(_data.count)),
 			Ui::Text::RichLangValue));
 	return result;
 }
@@ -94,9 +106,11 @@ ClickHandlerPtr PremiumGift::createViewLink() {
 		if (const auto controller = my.sessionWindow.get()) {
 			const auto selfId = controller->session().userPeerId();
 			const auto self = (from->id == selfId);
-			if (data.slug.isEmpty()) {
+			if (data.type == Data::GiftType::Stars) {
+				controller->showSettings(Settings::CreditsId());
+			} else if (data.slug.isEmpty()) {
 				const auto peer = self ? to : from;
-				const auto months = data.months;
+				const auto months = data.count;
 				Settings::ShowGiftPremium(controller, peer, months, self);
 			} else {
 				const auto fromId = from->id;
@@ -162,12 +176,16 @@ bool PremiumGift::gift() const {
 	return _data.slug.isEmpty() || !_data.channel;
 }
 
+int PremiumGift::stars() const {
+	return (_data.type == Data::GiftType::Stars) ? _data.count : 0;
+}
+
 void PremiumGift::ensureStickerCreated() const {
 	if (_sticker) {
 		return;
 	}
 	const auto &session = _parent->history()->session();
-	const auto months = _gift->data().months;
+	const auto months = stars() ? 1 : _data.count;
 	auto &packs = session.giftBoxStickersPacks();
 	if (const auto document = packs.lookup(months)) {
 		if (const auto sticker = document->sticker()) {
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 af5724dfa..7240dd49e 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h
@@ -49,6 +49,7 @@ private:
 	[[nodiscard]] bool incomingGift() const;
 	[[nodiscard]] bool outgoingGift() const;
 	[[nodiscard]] bool gift() const;
+	[[nodiscard]] int stars() const;
 	void ensureStickerCreated() const;
 
 	const not_null<Element*> _parent;
diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp
index 9e366ca54..bfcab3fc7 100644
--- a/Telegram/SourceFiles/main/main_account.cpp
+++ b/Telegram/SourceFiles/main/main_account.cpp
@@ -171,7 +171,8 @@ void Account::createSession(
 			MTPVector<MTPUsername>(),
 			MTPint(), // stories_max_id
 			MTPPeerColor(), // color
-			MTPPeerColor()), // profile_color
+			MTPPeerColor(), // profile_color
+			MTPint()), // bot_active_users
 		serialized,
 		streamVersion,
 		std::move(settings));
diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl
index 394a50efa..2d35a0c52 100644
--- a/Telegram/SourceFiles/mtproto/scheme/api.tl
+++ b/Telegram/SourceFiles/mtproto/scheme/api.tl
@@ -83,7 +83,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType;
 storage.fileWebp#1081464c = storage.FileType;
 
 userEmpty#d3bc4b7a id:long = User;
-user#215c4438 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 contact_require_premium:flags2.10?true bot_business:flags2.11?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 color:flags2.8?PeerColor profile_color:flags2.9?PeerColor = User;
+user#83314fca 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 contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?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 color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?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;
@@ -180,6 +180,7 @@ messageActionGiveawayResults#2a9fadc5 winners_count:int unclaimed_count:int = Me
 messageActionBoostApply#cc02aa6d boosts:int = MessageAction;
 messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector<RequestedPeer> = MessageAction;
 messageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction;
+messageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long crypto_currency:flags.0?string crypto_amount:flags.0?long transaction_id:flags.1?string = MessageAction;
 
 dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog;
 dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
@@ -568,7 +569,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 nosound:flags.3?true duration:double w:int h:int preload_prefix_size:flags.2?int = DocumentAttribute;
+documentAttributeVideo#17399fad 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 video_start_ts:flags.4?double = 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;
@@ -629,7 +630,7 @@ messages.stickerSetNotModified#d3f924eb = messages.StickerSet;
 
 botCommand#c27ac8c7 command:string description:string = BotCommand;
 
-botInfo#8f300b57 flags:# user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector<BotCommand> menu_button:flags.3?BotMenuButton = BotInfo;
+botInfo#8f300b57 flags:# has_preview_medias:flags.6?true user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector<BotCommand> menu_button:flags.3?BotMenuButton = BotInfo;
 
 keyboardButton#a2fa4880 text:string = KeyboardButton;
 keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton;
@@ -789,6 +790,7 @@ topPeerCategoryChannels#161d9628 = TopPeerCategory;
 topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory;
 topPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory;
 topPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory;
+topPeerCategoryBotsApp#fd9e7bec = TopPeerCategory;
 
 topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector<TopPeer> = TopPeerCategoryPeers;
 
@@ -1453,7 +1455,7 @@ attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType;
 inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice;
 inputInvoiceSlug#c326caef slug:string = InputInvoice;
 inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice;
-inputInvoiceStars#1da33ad8 option:StarsTopupOption = InputInvoice;
+inputInvoiceStars#65f00ce3 purpose:InputStorePaymentPurpose = InputInvoice;
 
 payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice;
 
@@ -1465,7 +1467,8 @@ inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgra
 inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose;
 inputStorePaymentPremiumGiftCode#a3805f3f flags:# users:Vector<InputUser> boost_peer:flags.0?InputPeer currency:string amount:long = InputStorePaymentPurpose;
 inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector<InputPeer> countries_iso2:flags.2?Vector<string> prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose;
-inputStorePaymentStars#4f0ee8df flags:# stars:long currency:string amount:long = InputStorePaymentPurpose;
+inputStorePaymentStarsTopup#dddd0f56 stars:long currency:string amount:long = InputStorePaymentPurpose;
+inputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose;
 
 premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption;
 
@@ -1613,6 +1616,7 @@ mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?tr
 mediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea;
 inputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea;
 mediaAreaUrl#37381085 coordinates:MediaAreaCoordinates url:string = MediaArea;
+mediaAreaWeather#49a6549c coordinates:MediaAreaCoordinates emoji:string temperature_c:double color:int = MediaArea;
 
 peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector<StoryItem> = PeerStories;
 
@@ -1806,7 +1810,7 @@ starsTransactionPeerAds#60682812 = StarsTransactionPeer;
 
 starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption;
 
-starsTransaction#2db5418f flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> = StarsTransaction;
+starsTransaction#2db5418f flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> = StarsTransaction;
 
 payments.starsStatus#8cf4ee60 flags:# balance:long history:Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus;
 
@@ -1826,6 +1830,14 @@ payments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAd
 
 inputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction;
 
+starsGiftOption#5e0589f1 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsGiftOption;
+
+bots.popularAppBots#1991b13b flags:# next_offset:flags.0?string users:Vector<User> = bots.PopularAppBots;
+
+botPreviewMedia#23e91ba3 date:int media:MessageMedia = BotPreviewMedia;
+
+bots.previewInfo#ca71d64 media:Vector<BotPreviewMedia> lang_codes:Vector<string> = bots.PreviewInfo;
+
 ---functions---
 
 invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@@ -1992,7 +2004,7 @@ contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bo
 contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked;
 contacts.search#11f812d8 q:string limit:int = contacts.Found;
 contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer;
-contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers;
+contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true bots_app:flags.16?true offset:int limit:int hash:long = contacts.TopPeers;
 contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
 contacts.resetSaved#879537f1 = Bool;
 contacts.getSaved#82f1e39f = Vector<SavedContact>;
@@ -2221,6 +2233,7 @@ messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects;
 messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates;
 messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates;
 messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;
+messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult;
 
 updates.getState#edd4882a = updates.State;
 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;
@@ -2349,6 +2362,13 @@ bots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool;
 bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
 bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
 bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;
+bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots;
+bots.addPreviewMedia#17aeb75a bot:InputUser lang_code:string media:InputMedia = BotPreviewMedia;
+bots.editPreviewMedia#8525606f bot:InputUser lang_code:string media:InputMedia new_media:InputMedia = BotPreviewMedia;
+bots.deletePreviewMedia#2d0135b3 bot:InputUser lang_code:string media:Vector<InputMedia> = Bool;
+bots.reorderPreviewMedias#b627f3aa bot:InputUser lang_code:string order:Vector<InputMedia> = Bool;
+bots.getPreviewInfo#423ab3ad bot:InputUser lang_code:string = bots.PreviewInfo;
+bots.getPreviewMedias#a2a5594d bot:InputUser = Vector<BotPreviewMedia>;
 
 payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm;
 payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;
@@ -2375,6 +2395,7 @@ payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true peer:InputPeer
 payments.getStarsRevenueWithdrawalUrl#13bbe8b3 peer:InputPeer stars:long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl;
 payments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl;
 payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
+payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;
 
 stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector<InputStickerSetItem> software:flags.3?string = messages.StickerSet;
 stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet;
@@ -2494,4 +2515,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool;
 
 fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;
 
-// LAYER 184
+// LAYER 185
diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp
index c09b41c36..e9dec016b 100644
--- a/Telegram/SourceFiles/payments/payments_form.cpp
+++ b/Telegram/SourceFiles/payments/payments_form.cpp
@@ -318,20 +318,11 @@ MTPInputInvoice Form::inputInvoice() const {
 	} else if (const auto slug = std::get_if<InvoiceSlug>(&_id.value)) {
 		return MTP_inputInvoiceSlug(MTP_string(slug->slug));
 	} else if (const auto credits = std::get_if<InvoiceCredits>(&_id.value)) {
-		using Flag = MTPDstarsTopupOption::Flag;
-		const auto emptyFlag = MTPDstarsTopupOption::Flags(0);
-		return MTP_inputInvoiceStars(MTP_starsTopupOption(
-			MTP_flags(emptyFlag
-				| (credits->product.isEmpty()
-					? Flag::f_store_product
-					: emptyFlag)
-				| (credits->extended
-					? Flag::f_extended
-					: emptyFlag)),
-			MTP_long(credits->credits),
-			MTP_string(credits->product),
-			MTP_string(credits->currency),
-			MTP_long(credits->amount)));
+		return MTP_inputInvoiceStars(
+			MTP_inputStorePaymentStarsTopup(
+				MTP_long(credits->credits),
+				MTP_string(credits->currency),
+				MTP_long(credits->amount)));
 	}
 	const auto &giftCode = v::get<InvoicePremiumGiftCode>(_id.value);
 	using Flag = MTPDpremiumGiftCodeOption::Flag;
diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp
index 59f2fb505..1895a8703 100644
--- a/Telegram/SourceFiles/storage/localimageloader.cpp
+++ b/Telegram/SourceFiles/storage/localimageloader.cpp
@@ -844,7 +844,8 @@ void FileLoadTask::process(Args &&args) {
 				MTP_double(realSeconds),
 				MTP_int(coverWidth),
 				MTP_int(coverHeight),
-				MTPint())); // preload_prefix_size
+				MTPint(), // preload_prefix_size
+				MTPdouble())); // video_start_ts
 
 			if (args.generateGoodThumbnail) {
 				goodThumbnail = video->thumbnail;
diff --git a/Telegram/SourceFiles/storage/serialize_document.cpp b/Telegram/SourceFiles/storage/serialize_document.cpp
index dd87b0c5f..efb527506 100644
--- a/Telegram/SourceFiles/storage/serialize_document.cpp
+++ b/Telegram/SourceFiles/storage/serialize_document.cpp
@@ -207,7 +207,8 @@ DocumentData *Document::readFromStreamHelper(
 				MTP_double(duration / 1000.),
 				MTP_int(width),
 				MTP_int(height),
-				MTPint())); // preload_prefix_size
+				MTPint(), // preload_prefix_size
+				MTPdouble())); // video_start_ts
 		} else {
 			attributes.push_back(MTP_documentAttributeImageSize(
 				MTP_int(width),

From 847d66c97349543400133d6339955d22606622cd Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 12:13:54 +0300
Subject: [PATCH 112/163] Added initial support of received gifts in list of
 credits history.

---
 Telegram/Resources/langs/lang.strings         |  6 +++
 Telegram/SourceFiles/api/api_credits.cpp      |  1 +
 .../SourceFiles/boxes/gift_premium_box.cpp    |  6 ++-
 Telegram/SourceFiles/data/data_credits.h      |  2 +-
 .../info_statistics_list_controllers.cpp      |  4 +-
 .../settings/settings_credits_graphics.cpp    | 38 ++++++++++++++++++-
 .../ui/effects/credits_graphics.cpp           |  4 +-
 7 files changed, 55 insertions(+), 6 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 2311102df..931bbb1c0 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2369,6 +2369,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_credits_box_history_entry_play_market" = "Play Market";
 "lng_credits_box_history_entry_app_store" = "App Store";
 "lng_credits_box_history_entry_fragment" = "Fragment";
+"lng_credits_box_history_entry_anonymous" = "Unknown User";
+"lng_credits_box_history_entry_gift_name" = "Received Gift";
+"lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}";
+"lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}";
+"lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}";
+"lng_credits_box_history_entry_gift_about_url" = "https://telegram.org/blog/telegram-stars";
 "lng_credits_box_history_entry_ads" = "Ads Platform";
 "lng_credits_box_history_entry_premium_bot" = "Stars Top-Up";
 "lng_credits_box_history_entry_via_premium_bot" = "Premium Bot";
diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp
index 356e2cffa..0b35a04ee 100644
--- a/Telegram/SourceFiles/api/api_credits.cpp
+++ b/Telegram/SourceFiles/api/api_credits.cpp
@@ -102,6 +102,7 @@ constexpr auto kTransactionsLimit = 100;
 			: QDateTime(),
 		.successLink = qs(tl.data().vtransaction_url().value_or_empty()),
 		.in = (int64(tl.data().vstars().v) >= 0),
+		.gift = tl.data().is_gift(),
 	};
 }
 
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index fdf1eda03..5730d9b40 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -1695,8 +1695,10 @@ void AddCreditsHistoryEntryTable(
 		AddTableRow(
 			table,
 			tr::lng_credits_box_history_entry_via(),
-			tr::lng_credits_box_history_entry_fragment(
-				Ui::Text::RichLangValue));
+			(entry.gift
+				? tr::lng_credits_box_history_entry_anonymous
+				: tr::lng_credits_box_history_entry_fragment)(
+					Ui::Text::RichLangValue));
 	} else if (entry.peerType == Type::Ads) {
 		AddTableRow(
 			table,
diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h
index 75da4db5b..d01becf07 100644
--- a/Telegram/SourceFiles/data/data_credits.h
+++ b/Telegram/SourceFiles/data/data_credits.h
@@ -57,7 +57,7 @@ struct CreditsHistoryEntry final {
 	QDateTime successDate;
 	QString successLink;
 	bool in = false;
-
+	bool gift = false;
 };
 
 struct CreditsStatusSlice final {
diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
index 9c2a692e7..86640fe5b 100644
--- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
+++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp
@@ -800,7 +800,9 @@ void CreditsRow::init() {
 			: _entry.failed
 			? (joiner + tr::lng_channel_earn_history_failed(tr::now))
 			: QString())
-		+ (_entry.title.isEmpty() ? QString() : (joiner + _name)));
+		+ ((_entry.gift && PeerListRow::special())
+			? (joiner + tr::lng_credits_box_history_entry_anonymous(tr::now))
+			: (_entry.title.isEmpty() ? QString() : (joiner + _name))));
 	{
 		constexpr auto kMinus = QChar(0x2212);
 		_rightText.setText(
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index 1eaef4568..1f774b499 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -488,6 +488,8 @@ void ReceiptCreditsBox(
 			rpl::single(
 				!e.title.isEmpty()
 				? e.title
+				: e.gift
+				? tr::lng_credits_box_history_entry_gift_name(tr::now)
 				: peer
 				? peer->name()
 				: Ui::GenerateEntryName(e).text),
@@ -593,7 +595,41 @@ void ReceiptCreditsBox(
 			object_ptr<Ui::FlatLabel>(
 				box,
 				rpl::single(e.description),
-				st::defaultFlatLabel)));
+				st::creditsBoxAbout)));
+	}
+	if (e.gift) {
+		Ui::AddSkip(content);
+		const auto arrow = Ui::Text::SingleCustomEmoji(
+			session->data().customEmojiManager().registerInternalEmoji(
+				st::topicButtonArrow,
+				st::channelEarnLearnArrowMargins,
+				false));
+		auto link = tr::lng_credits_box_history_entry_gift_about_link(
+			lt_emoji,
+			rpl::single(arrow),
+			Ui::Text::RichLangValue
+		) | rpl::map([](TextWithEntities text) {
+			return Ui::Text::Link(
+				std::move(text),
+				tr::lng_credits_box_history_entry_gift_about_url(tr::now));
+		});
+		box->addRow(object_ptr<Ui::CenterWrap<>>(
+			box,
+			Ui::CreateLabelWithCustomEmoji(
+				box,
+				(!e.in && peer)
+					? tr::lng_credits_box_history_entry_gift_out_about(
+						lt_user,
+						rpl::single(TextWithEntities{ peer->shortName() }),
+						lt_link,
+						std::move(link),
+						Ui::Text::RichLangValue)
+					: tr::lng_credits_box_history_entry_gift_in_about(
+						lt_link,
+						std::move(link),
+						Ui::Text::RichLangValue),
+				{ .session = session },
+				st::creditsBoxAbout)));
 	}
 
 	Ui::AddSkip(content);
diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
index e99a4d4eb..61b43b8fc 100644
--- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
+++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
@@ -455,7 +455,9 @@ Fn<PaintRoundImageCallback(Fn<void()>)> PaintPreviewCallback(
 }
 
 TextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) {
-	return ((entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment)
+	return (entry.gift
+		? tr::lng_credits_box_history_entry_gift_name
+		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment)
 		? tr::lng_bot_username_description1_link
 		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot)
 		? tr::lng_credits_box_history_entry_premium_bot

From bcb6e9e1af2e56d9d7b5e205e0b716fd4d9c8441 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 12:52:40 +0300
Subject: [PATCH 113/163] Removed unused include directives from
 settings_common_session.

---
 .../settings/settings_common_session.cpp      | 21 -------------------
 1 file changed, 21 deletions(-)

diff --git a/Telegram/SourceFiles/settings/settings_common_session.cpp b/Telegram/SourceFiles/settings/settings_common_session.cpp
index 8fac1a399..d72c74d07 100644
--- a/Telegram/SourceFiles/settings/settings_common_session.cpp
+++ b/Telegram/SourceFiles/settings/settings_common_session.cpp
@@ -7,30 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "settings/settings_common_session.h"
 
-#include "api/api_cloud_password.h"
-#include "apiwrap.h"
-#include "core/application.h"
-#include "core/core_cloud_password.h"
-#include "lang/lang_keys.h"
-#include "main/main_domain.h"
-#include "main/main_session.h"
 #include "settings/cloud_password/settings_cloud_password_email_confirm.h"
-#include "settings/settings_advanced.h"
-#include "settings/settings_calls.h"
 #include "settings/settings_chat.h"
-#include "settings/settings_experimental.h"
-#include "settings/settings_folders.h"
-#include "settings/settings_information.h"
 #include "settings/settings_main.h"
-#include "settings/settings_notifications.h"
-#include "settings/settings_privacy_security.h"
-#include "ui/widgets/menu/menu_add_action_callback.h"
-#include "window/themes/window_theme_editor_box.h"
-#include "window/window_controller.h"
-#include "window/window_session_controller.h"
-#include "styles/style_menu_icons.h"
-
-#include <QAction>
 
 namespace Settings {
 

From 3f2cb8f8c94275f46231ccd17e635df6da7178ea Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 14:30:19 +0300
Subject: [PATCH 114/163] Added file origin to sticker pack for gifts.

---
 .../SourceFiles/chat_helpers/stickers_gift_box_pack.cpp     | 6 ++++++
 Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h  | 6 ++++++
 2 files changed, 12 insertions(+)

diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
index f54b1f1d3..2e810dd6c 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "apiwrap.h"
 #include "data/data_document.h"
+#include "data/data_file_origin.h"
 #include "data/data_session.h"
 #include "main/main_session.h"
 
@@ -38,6 +39,10 @@ DocumentData *GiftBoxPack::lookup(int months) const {
 	return (index >= _documents.size()) ? fallback : _documents[index];
 }
 
+Data::FileOrigin GiftBoxPack::origin() const {
+	return Data::FileOriginStickerSet(_setId, _accessHash);
+}
+
 void GiftBoxPack::load() {
 	if (_requestId || !_documents.empty()) {
 		return;
@@ -59,6 +64,7 @@ void GiftBoxPack::load() {
 
 void GiftBoxPack::applySet(const MTPDmessages_stickerSet &data) {
 	_setId = data.vset().data().vid().v;
+	_accessHash = data.vset().data().vaccess_hash().v;
 	auto documents = base::flat_map<DocumentId, not_null<DocumentData*>>();
 	for (const auto &sticker : data.vdocuments().v) {
 		const auto document = _session->data().processDocument(sticker);
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
index e722c1979..4c480fba3 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
+++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
@@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 class DocumentData;
 
+namespace Data {
+struct FileOrigin;
+} // namespace Data
+
 namespace Main {
 class Session;
 } // namespace Main
@@ -22,6 +26,7 @@ public:
 
 	void load();
 	[[nodiscard]] DocumentData *lookup(int months) const;
+	[[nodiscard]] Data::FileOrigin origin() const;
 
 private:
 	using SetId = uint64;
@@ -32,6 +37,7 @@ private:
 
 	std::vector<DocumentData*> _documents;
 	SetId _setId = 0;
+	uint64 _accessHash = 0;
 	mtpRequestId _requestId = 0;
 
 };

From e760a0983f7b32ae61f330988106df93f50ccb8f Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 14:30:53 +0300
Subject: [PATCH 115/163] Added gift sticker to ReceiptCreditsBox for gifts.

---
 .../settings/settings_credits_graphics.cpp    | 95 +++++++++++++++----
 Telegram/SourceFiles/ui/effects/credits.style |  5 +
 2 files changed, 82 insertions(+), 18 deletions(-)

diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index 1f774b499..fdb7b74fe 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -12,11 +12,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/timer_rpl.h"
 #include "base/unixtime.h"
 #include "boxes/gift_premium_box.h"
+#include "chat_helpers/stickers_gift_box_pack.h"
+#include "chat_helpers/stickers_lottie.h"
 #include "core/click_handler_types.h"
+#include "core/click_handler_types.h" // UrlClickHandler
 #include "core/ui_integration.h"
 #include "data/data_document.h"
+#include "data/data_document_media.h"
 #include "data/data_file_origin.h"
-#include "core/click_handler_types.h" // UrlClickHandler
 #include "data/data_photo_media.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
@@ -27,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData.
 #include "info/statistics/info_statistics_list_controllers.h"
 #include "lang/lang_keys.h"
+#include "lottie/lottie_single_player.h"
 #include "main/main_app_config.h"
 #include "main/main_session.h"
 #include "payments/payments_checkout_process.h"
@@ -457,24 +461,82 @@ void ReceiptCreditsBox(
 			content,
 			GenericEntryPhoto(content, callback, stUser.photoSize)));
 		AddViewMediaHandler(thumb->entity(), controller, e);
-	} else if (peer) {
+	} else if (peer && !e.gift) {
 		content->add(object_ptr<Ui::CenterWrap<>>(
 			content,
 			object_ptr<Ui::UserpicButton>(content, peer, stUser)));
 	} else {
-		const auto widget = content->add(
-			object_ptr<Ui::CenterWrap<>>(
+		if (e.gift) {
+			using PlayerPtr = std::unique_ptr<Lottie::SinglePlayer>;
+			struct State final {
+				DocumentData *sticker = nullptr;
+				std::shared_ptr<Data::DocumentMedia> media;
+				std::unique_ptr<Lottie::SinglePlayer> lottie;
+				rpl::lifetime downloadLifetime;
+			};
+			Ui::AddSkip(
 				content,
-				object_ptr<Ui::RpWidget>(content)))->entity();
-		using Draw = Fn<void(Painter &, int, int, int, int)>;
-		const auto draw = widget->lifetime().make_state<Draw>(
-			Ui::GenerateCreditsPaintUserpicCallback(e));
-		widget->resize(Size(stUser.photoSize));
-		widget->paintRequest(
-		) | rpl::start_with_next([=] {
-			auto p = Painter(widget);
-			(*draw)(p, 0, 0, stUser.photoSize, stUser.photoSize);
-		}, widget->lifetime());
+				st::creditsHistoryEntryGiftStickerSpace);
+			const auto icon = Ui::CreateChild<Ui::RpWidget>(content);
+			icon->resize(Size(st::creditsHistoryEntryGiftStickerSize));
+			const auto state = icon->lifetime().make_state<State>();
+			auto &packs = session->giftBoxStickersPacks();
+			const auto document = packs.lookup(1);
+			if (document && document->sticker()) {
+				state->sticker = document;
+				state->media = document->createMediaView();
+				state->media->thumbnailWanted(packs.origin());
+				state->media->automaticLoad(packs.origin(), nullptr);
+				session->downloaderTaskFinished(
+				) | rpl::start_with_next([=] {
+					if (state->media->loaded()) {
+						state->lottie = ChatHelpers::LottiePlayerFromDocument(
+							state->media.get(),
+							ChatHelpers::StickerLottieSize::MessageHistory,
+							icon->size(),
+							Lottie::Quality::High);
+						state->lottie->updates() | rpl::start_with_next([=] {
+							icon->update();
+						}, icon->lifetime());
+						state->downloadLifetime.destroy();
+					}
+				}, state->downloadLifetime);
+			}
+			icon->paintRequest(
+			) | rpl::start_with_next([=] {
+				auto p = Painter(icon);
+				const auto &lottie = state->lottie;
+				const auto frame = (lottie && lottie->ready())
+					? lottie->frameInfo({ .box = icon->size() })
+					: Lottie::Animation::FrameInfo();
+				if (!frame.image.isNull()) {
+					p.drawImage(0, 0, frame.image);
+					if (lottie->frameIndex() < lottie->framesCount() - 1) {
+						lottie->markFrameShown();
+					}
+				}
+			}, icon->lifetime());
+			content->sizeValue(
+			) | rpl::start_with_next([=](const QSize &size) {
+				icon->move(
+					(size.width() - icon->width()) / 2,
+					st::creditsHistoryEntryGiftStickerSkip);
+			}, icon->lifetime());
+		} else {
+			const auto widget = content->add(
+				object_ptr<Ui::CenterWrap<>>(
+					content,
+					object_ptr<Ui::RpWidget>(content)))->entity();
+			using Draw = Fn<void(Painter &, int, int, int, int)>;
+			const auto draw = widget->lifetime().make_state<Draw>(
+				Ui::GenerateCreditsPaintUserpicCallback(e));
+			widget->resize(Size(stUser.photoSize));
+			widget->paintRequest(
+			) | rpl::start_with_next([=] {
+				auto p = Painter(widget);
+				(*draw)(p, 0, 0, stUser.photoSize, stUser.photoSize);
+			}, widget->lifetime());
+		}
 	}
 
 	Ui::AddSkip(content);
@@ -635,10 +697,7 @@ void ReceiptCreditsBox(
 	Ui::AddSkip(content);
 	Ui::AddSkip(content);
 
-	AddCreditsHistoryEntryTable(
-		controller,
-		box->verticalLayout(),
-		e);
+	AddCreditsHistoryEntryTable(controller, content, e);
 
 	Ui::AddSkip(content);
 
diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style
index baab38fb7..cebc110c5 100644
--- a/Telegram/SourceFiles/ui/effects/credits.style
+++ b/Telegram/SourceFiles/ui/effects/credits.style
@@ -49,3 +49,8 @@ starIconSmall: icon{{ "payments/small_star", windowFg }};
 starIconSmallPadding: margins(0px, -2px, 0px, 0px);
 
 creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }};
+
+creditsHistoryEntryGiftStickerSkip: -20px;
+creditsHistoryEntryGiftStickerSize: 150px;
+creditsHistoryEntryGiftStickerSpace: 105px;
+

From 127f651d5e14139faf52178fb1f69802c9c420b8 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 15:29:05 +0300
Subject: [PATCH 116/163] Added api support to get credits gift options.

---
 Telegram/SourceFiles/api/api_credits.cpp | 34 +++++++++++++++++-------
 1 file changed, 25 insertions(+), 9 deletions(-)

diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp
index 0b35a04ee..3ef1d721f 100644
--- a/Telegram/SourceFiles/api/api_credits.cpp
+++ b/Telegram/SourceFiles/api/api_credits.cpp
@@ -134,12 +134,10 @@ rpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {
 	return [=](auto consumer) {
 		auto lifetime = rpl::lifetime();
 
-		using TLOption = MTPStarsTopupOption;
-		_api.request(MTPpayments_GetStarsTopupOptions(
-		)).done([=](const MTPVector<TLOption> &result) {
-			_options = ranges::views::all(
-				result.v
-			) | ranges::views::transform([](const TLOption &option) {
+		const auto optionsFromTL = [](const auto &options) {
+			return ranges::views::all(
+				options
+			) | ranges::views::transform([=](const auto &option) {
 				return Data::CreditTopupOption{
 					.credits = option.data().vstars().v,
 					.product = qs(
@@ -149,10 +147,28 @@ rpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {
 					.extended = option.data().is_extended(),
 				};
 			}) | ranges::to_vector;
-			consumer.put_done();
-		}).fail([=](const MTP::Error &error) {
+		};
+		const auto fail = [=](const MTP::Error &error) {
 			consumer.put_error_copy(error.type());
-		}).send();
+		};
+
+		if (_peer->isSelf()) {
+			using TLOption = MTPStarsTopupOption;
+			_api.request(MTPpayments_GetStarsTopupOptions(
+			)).done([=](const MTPVector<TLOption> &result) {
+				_options = optionsFromTL(result.v);
+				consumer.put_done();
+			}).fail(fail).send();
+		} else if (const auto user = _peer->asUser()) {
+			using TLOption = MTPStarsGiftOption;
+			_api.request(MTPpayments_GetStarsGiftOptions(
+				MTP_flags(MTPpayments_GetStarsGiftOptions::Flag::f_user_id),
+				user->inputUser
+			)).done([=](const MTPVector<TLOption> &result) {
+				_options = optionsFromTL(result.v);
+				consumer.put_done();
+			}).fail(fail).send();
+		}
 
 		return lifetime;
 	};

From 24b93a5effe80a66ba86f3db15801054de4fdd81 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 15:32:16 +0300
Subject: [PATCH 117/163] Added support for credits gift options to list of
 credit options.

---
 Telegram/SourceFiles/settings/settings_credits.cpp     |  3 ++-
 .../SourceFiles/settings/settings_credits_graphics.cpp | 10 +++++++---
 .../SourceFiles/settings/settings_credits_graphics.h   |  1 +
 3 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp
index e80f82a62..235253652 100644
--- a/Telegram/SourceFiles/settings/settings_credits.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits.cpp
@@ -289,7 +289,8 @@ void Credits::setupContent() {
 			Ui::StartFireworks(_parent);
 		}
 	};
-	FillCreditOptions(_controller->uiShow(), content, 0, paid);
+	const auto self = _controller->session().user();
+	FillCreditOptions(_controller->uiShow(), content, self, 0, paid);
 	setupHistory(content);
 
 	Ui::ResizeFitChild(this, content);
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index fdb7b74fe..e9e015849 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -231,6 +231,7 @@ void AddViewMediaHandler(
 void FillCreditOptions(
 		std::shared_ptr<Main::SessionShow> show,
 		not_null<Ui::VerticalLayout*> container,
+		not_null<PeerData*> peer,
 		int minimumCredits,
 		Fn<void()> paid) {
 	const auto options = container->add(
@@ -351,8 +352,7 @@ void FillCreditOptions(
 	};
 
 	using ApiOptions = Api::CreditsTopupOptions;
-	const auto apiCredits = content->lifetime().make_state<ApiOptions>(
-		show->session().user());
+	const auto apiCredits = content->lifetime().make_state<ApiOptions>(peer);
 
 	if (show->session().premiumPossible()) {
 		apiCredits->request(
@@ -864,7 +864,11 @@ void SmallBalanceBox(
 			}));
 	}();
 
-	FillCreditOptions(show, box->verticalLayout(), creditsNeeded, done);
+	{
+		const auto content = box->verticalLayout();
+		const auto self = show->session().user();
+		FillCreditOptions(show, content, self, creditsNeeded, done);
+	}
 
 	content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight);
 	content->setMinimumHeight(st::infoLayerTopBarHeight);
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h
index 47d5323c7..701c60b7f 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.h
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h
@@ -35,6 +35,7 @@ namespace Settings {
 void FillCreditOptions(
 	std::shared_ptr<Main::SessionShow> show,
 	not_null<Ui::VerticalLayout*> container,
+	not_null<PeerData*> peer,
 	int minCredits,
 	Fn<void()> paid);
 

From b8a19b56b667dddff625e8086c3232c6816be0bc Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 16:09:43 +0300
Subject: [PATCH 118/163] Removed window session controller usage from list of
 credit options.

---
 .../settings/settings_credits_graphics.cpp            | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index e9e015849..613bf9169 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -864,11 +864,12 @@ void SmallBalanceBox(
 			}));
 	}();
 
-	{
-		const auto content = box->verticalLayout();
-		const auto self = show->session().user();
-		FillCreditOptions(show, content, self, creditsNeeded, done);
-	}
+	FillCreditOptions(
+		show,
+		box->verticalLayout(),
+		show->session().user(),
+		creditsNeeded,
+		done);
 
 	content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight);
 	content->setMinimumHeight(st::infoLayerTopBarHeight);

From 8ad2d3d39a1539feb0d91abb89f07086470254d8 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 17:22:22 +0300
Subject: [PATCH 119/163] Added api support to create invoice for credit gifts.

---
 Telegram/SourceFiles/api/api_credits.cpp               |  5 ++++-
 Telegram/SourceFiles/data/data_credits.h               |  1 +
 Telegram/SourceFiles/payments/payments_form.cpp        | 10 ++++++++++
 Telegram/SourceFiles/payments/payments_form.h          |  1 +
 .../SourceFiles/settings/settings_credits_graphics.cpp |  1 +
 5 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp
index 3ef1d721f..727cee565 100644
--- a/Telegram/SourceFiles/api/api_credits.cpp
+++ b/Telegram/SourceFiles/api/api_credits.cpp
@@ -134,7 +134,9 @@ rpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {
 	return [=](auto consumer) {
 		auto lifetime = rpl::lifetime();
 
-		const auto optionsFromTL = [](const auto &options) {
+		const auto giftBarePeerId = !_peer->isSelf() ? _peer->id.value : 0;
+
+		const auto optionsFromTL = [giftBarePeerId](const auto &options) {
 			return ranges::views::all(
 				options
 			) | ranges::views::transform([=](const auto &option) {
@@ -145,6 +147,7 @@ rpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {
 					.currency = qs(option.data().vcurrency()),
 					.amount = option.data().vamount().v,
 					.extended = option.data().is_extended(),
+					.giftBarePeerId = giftBarePeerId,
 				};
 			}) | ranges::to_vector;
 		};
diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h
index d01becf07..ee8d948a7 100644
--- a/Telegram/SourceFiles/data/data_credits.h
+++ b/Telegram/SourceFiles/data/data_credits.h
@@ -15,6 +15,7 @@ struct CreditTopupOption final {
 	QString currency;
 	uint64 amount = 0;
 	bool extended = false;
+	uint64 giftBarePeerId = 0;
 };
 
 using CreditTopupOptions = std::vector<CreditTopupOption>;
diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp
index e9dec016b..8c0a44e10 100644
--- a/Telegram/SourceFiles/payments/payments_form.cpp
+++ b/Telegram/SourceFiles/payments/payments_form.cpp
@@ -318,6 +318,16 @@ MTPInputInvoice Form::inputInvoice() const {
 	} else if (const auto slug = std::get_if<InvoiceSlug>(&_id.value)) {
 		return MTP_inputInvoiceSlug(MTP_string(slug->slug));
 	} else if (const auto credits = std::get_if<InvoiceCredits>(&_id.value)) {
+		if (const auto userId = peerToUser(credits->giftPeerId)) {
+			if (const auto user = _session->data().user(userId)) {
+				return MTP_inputInvoiceStars(
+					MTP_inputStorePaymentStarsGift(
+						user->inputUser,
+						MTP_long(credits->credits),
+						MTP_string(credits->currency),
+						MTP_long(credits->amount)));
+			}
+		}
 		return MTP_inputInvoiceStars(
 			MTP_inputStorePaymentStarsTopup(
 				MTP_long(credits->credits),
diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h
index c85d946ed..42b6d00b3 100644
--- a/Telegram/SourceFiles/payments/payments_form.h
+++ b/Telegram/SourceFiles/payments/payments_form.h
@@ -167,6 +167,7 @@ struct InvoiceCredits {
 	QString currency;
 	uint64 amount = 0;
 	bool extended = false;
+	PeerId giftPeerId = PeerId(0);
 };
 
 struct InvoiceId {
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index 613bf9169..954c07c7b 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -314,6 +314,7 @@ void FillCreditOptions(
 					.currency = option.currency,
 					.amount = option.amount,
 					.extended = option.extended,
+					.giftPeerId = PeerId(option.giftBarePeerId),
 				};
 
 				const auto weak = Ui::MakeWeak(button);

From 0bfb0fd04530af5e40ccde949a5e9ad6da218f84 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Wed, 17 Jul 2024 17:22:49 +0300
Subject: [PATCH 120/163] Added initial ability to gift credits to users.

---
 Telegram/CMakeLists.txt                       |   2 +
 Telegram/Resources/langs/lang.strings         |   3 +
 .../SourceFiles/boxes/gift_credits_box.cpp    | 180 ++++++++++++++++++
 Telegram/SourceFiles/boxes/gift_credits_box.h |  20 ++
 .../SourceFiles/settings/settings_credits.cpp |  14 ++
 Telegram/SourceFiles/ui/effects/credits.style |   3 +
 6 files changed, 222 insertions(+)
 create mode 100644 Telegram/SourceFiles/boxes/gift_credits_box.cpp
 create mode 100644 Telegram/SourceFiles/boxes/gift_credits_box.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index b9ee4785c..8923211da 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -272,6 +272,8 @@ PRIVATE
     boxes/edit_caption_box.h
     boxes/edit_privacy_box.cpp
     boxes/edit_privacy_box.h
+    boxes/gift_credits_box.cpp
+    boxes/gift_credits_box.h
     boxes/gift_premium_box.cpp
     boxes/gift_premium_box.h
     boxes/language_box.cpp
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 931bbb1c0..fad51ea32 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -2342,6 +2342,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_credits_summary_history_tab_out" = "Outgoing";
 "lng_credits_summary_history_entry_inner_in" = "In-App Purchase";
 "lng_credits_summary_balance" = "Balance";
+"lng_credits_gift_button" = "Gift Stars to Friends";
 "lng_credits_box_out_title" = "Confirm Your Purchase";
 "lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?";
 "lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?";
@@ -2390,6 +2391,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps.";
 "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
 
+"lng_credits_gift_title" = "Gift Telegram Stars";
+
 "lng_location_title" = "Location";
 "lng_location_about" = "Display the location of your business on your account.";
 "lng_location_address" = "Enter Address";
diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp
new file mode 100644
index 000000000..d03a73641
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp
@@ -0,0 +1,180 @@
+/*
+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 "boxes/gift_credits_box.h"
+
+#include "api/api_credits.h"
+#include "boxes/peer_list_controllers.h"
+#include "data/data_peer.h"
+#include "data/data_session.h"
+#include "data/data_user.h"
+#include "data/stickers/data_custom_emoji.h"
+#include "lang/lang_keys.h"
+#include "main/session/session_show.h"
+#include "settings/settings_credits_graphics.h"
+#include "ui/controls/userpic_button.h"
+#include "ui/effects/premium_graphics.h"
+#include "ui/effects/premium_stars_colored.h"
+#include "ui/layers/generic_box.h"
+#include "ui/rect.h"
+#include "ui/text/text_utilities.h"
+#include "ui/vertical_list.h"
+#include "ui/widgets/label_with_custom_emoji.h"
+#include "window/window_session_controller.h"
+#include "styles/style_boxes.h"
+#include "styles/style_channel_earn.h"
+#include "styles/style_chat.h"
+#include "styles/style_credits.h"
+#include "styles/style_giveaway.h"
+#include "styles/style_layers.h"
+#include "styles/style_premium.h"
+
+namespace Ui {
+
+void GiftCreditsBox(
+		not_null<Ui::GenericBox*> box,
+		not_null<PeerData*> peer,
+		Fn<void()> gifted) {
+	box->setStyle(st::creditsGiftBox);
+	box->setNoContentMargin(true);
+	box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
+
+	const auto content = box->setPinnedToTopContent(
+		object_ptr<Ui::VerticalLayout>(box));
+
+	Ui::AddSkip(content);
+	Ui::AddSkip(content);
+	const auto &stUser = st::premiumGiftsUserpicButton;
+	const auto userpicWrap = content->add(
+		object_ptr<Ui::CenterWrap<>>(
+			content,
+			object_ptr<Ui::UserpicButton>(content, peer, stUser)));
+	userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
+	Ui::AddSkip(content);
+	Ui::AddSkip(content);
+
+	{
+		const auto widget = Ui::CreateChild<Ui::RpWidget>(content);
+		using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
+		const auto stars = widget->lifetime().make_state<ColoredMiniStars>(
+			widget,
+			false,
+			Ui::Premium::MiniStars::Type::BiStars);
+		stars->setColorOverride(Ui::Premium::CreditsIconGradientStops());
+		widget->resize(
+			st::boxWidth - stUser.photoSize,
+			stUser.photoSize * 2);
+		content->sizeValue(
+		) | rpl::start_with_next([=](const QSize &size) {
+			widget->moveToLeft(stUser.photoSize / 2, 0);
+			const auto starsRect = Rect(widget->size());
+			stars->setPosition(starsRect.topLeft());
+			stars->setSize(starsRect.size());
+			widget->lower();
+		}, widget->lifetime());
+		widget->paintRequest(
+		) | rpl::start_with_next([=](const QRect &r) {
+			auto p = QPainter(widget);
+			p.fillRect(r, Qt::transparent);
+			stars->paint(p);
+		}, widget->lifetime());
+	}
+	{
+		Ui::AddSkip(content);
+		const auto arrow = Ui::Text::SingleCustomEmoji(
+			peer->owner().customEmojiManager().registerInternalEmoji(
+				st::topicButtonArrow,
+				st::channelEarnLearnArrowMargins,
+				false));
+		auto link = tr::lng_credits_box_history_entry_gift_about_link(
+			lt_emoji,
+			rpl::single(arrow),
+			Ui::Text::RichLangValue
+		) | rpl::map([](TextWithEntities text) {
+			return Ui::Text::Link(
+				std::move(text),
+				tr::lng_credits_box_history_entry_gift_about_url(tr::now));
+		});
+		content->add(
+			object_ptr<Ui::CenterWrap<>>(
+				content,
+				Ui::CreateLabelWithCustomEmoji(
+					content,
+					tr::lng_credits_box_history_entry_gift_out_about(
+						lt_user,
+						rpl::single(TextWithEntities{ peer->shortName() }),
+						lt_link,
+						std::move(link),
+						Ui::Text::RichLangValue),
+					{ .session = &peer->session() },
+					st::creditsBoxAbout)),
+			st::boxRowPadding);
+	}
+	Ui::AddSkip(content);
+	Ui::AddSkip(box->verticalLayout());
+
+	Settings::FillCreditOptions(
+		Main::MakeSessionShow(box->uiShow(), &peer->session()),
+		box->verticalLayout(),
+		peer,
+		0,
+		[=] { gifted(); box->uiShow()->hideLayer(); });
+
+	const auto bottom = box->setPinnedToBottomContent(
+		object_ptr<Ui::VerticalLayout>(box));
+}
+
+void ShowGiftCreditsBox(
+		not_null<Window::SessionController*> controller,
+		Fn<void()> gifted) {
+
+	class Controller final : public ContactsBoxController {
+	public:
+		Controller(
+			not_null<Main::Session*> session,
+			Fn<void(not_null<PeerData*>)> choose)
+		: ContactsBoxController(session)
+		, _choose(std::move(choose)) {
+		}
+
+	protected:
+		std::unique_ptr<PeerListRow> createRow(
+				not_null<UserData*> user) override {
+			if (user->isSelf()
+				|| user->isBot()
+				|| user->isServiceUser()
+				|| user->isInaccessible()) {
+				return nullptr;
+			}
+			return ContactsBoxController::createRow(user);
+		}
+
+		void rowClicked(not_null<PeerListRow*> row) override {
+			_choose(row->peer());
+		}
+
+	private:
+		const Fn<void(not_null<PeerData*>)> _choose;
+
+	};
+	auto initBox = [=](not_null<PeerListBox*> peersBox) {
+		peersBox->setTitle(tr::lng_credits_gift_title());
+		peersBox->addButton(tr::lng_cancel(), [=] { peersBox->closeBox(); });
+	};
+
+	const auto show = controller->uiShow();
+	auto listController = std::make_unique<Controller>(
+		&controller->session(),
+		[=](not_null<PeerData*> peer) {
+			show->showBox(Box(GiftCreditsBox, peer, gifted));
+		});
+	show->showBox(
+		Box<PeerListBox>(std::move(listController), std::move(initBox)),
+		Ui::LayerOption::KeepOther);
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.h b/Telegram/SourceFiles/boxes/gift_credits_box.h
new file mode 100644
index 000000000..43b8556f3
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/gift_credits_box.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 Window {
+class SessionController;
+} // namespace Window
+
+namespace Ui {
+
+void ShowGiftCreditsBox(
+	not_null<Window::SessionController*> controller,
+	Fn<void()> gifted);
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp
index 235253652..0d7d12490 100644
--- a/Telegram/SourceFiles/settings/settings_credits.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits.cpp
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "settings/settings_credits.h"
 
 #include "api/api_credits.h"
+#include "boxes/gift_credits_box.h"
 #include "boxes/gift_premium_box.h"
 #include "core/click_handler_types.h"
 #include "data/data_file_origin.h"
@@ -291,6 +292,19 @@ void Credits::setupContent() {
 	};
 	const auto self = _controller->session().user();
 	FillCreditOptions(_controller->uiShow(), content, self, 0, paid);
+	{
+		Ui::AddSkip(content);
+		const auto giftButton = AddButtonWithIcon(
+			content,
+			tr::lng_credits_gift_button(),
+			st::settingsButtonLightNoIcon);
+		Ui::AddSkip(content);
+		Ui::AddDivider(content);
+		giftButton->setClickedCallback([=] {
+			Ui::ShowGiftCreditsBox(_controller, paid);
+		});
+	}
+
 	setupHistory(content);
 
 	Ui::ResizeFitChild(this, content);
diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style
index cebc110c5..b982687d6 100644
--- a/Telegram/SourceFiles/ui/effects/credits.style
+++ b/Telegram/SourceFiles/ui/effects/credits.style
@@ -54,3 +54,6 @@ creditsHistoryEntryGiftStickerSkip: -20px;
 creditsHistoryEntryGiftStickerSize: 150px;
 creditsHistoryEntryGiftStickerSpace: 105px;
 
+creditsGiftBox: Box(defaultBox) {
+	shadowIgnoreTopSkip: true;
+}

From 54ce85f8e6d1797dd8816a7fe3db96b7fcb0e3cd Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 23 Jul 2024 07:51:04 +0200
Subject: [PATCH 121/163] Show nice star in stars payments.

---
 Telegram/SourceFiles/history/history.cpp      | 12 +++++-
 Telegram/SourceFiles/history/history_item.cpp | 38 ++++++++++++++-----
 .../history/history_item_components.h         |  2 +-
 .../SourceFiles/ui/text/format_values.cpp     |  2 +-
 4 files changed, 41 insertions(+), 13 deletions(-)

diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index 754791e80..35643637e 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/history_item_helpers.h"
 #include "history/history_translation.h"
 #include "history/history_unread_things.h"
+#include "core/ui_integration.h"
 #include "dialogs/ui/dialogs_layout.h"
 #include "data/business/data_shortcut_messages.h"
 #include "data/components/scheduled_messages.h"
@@ -1128,14 +1129,23 @@ void History::applyServiceChanges(
 			}
 			if (paid) {
 				// Toast on a current active window.
+				const auto context = [=](not_null<QWidget*> toast) {
+					return Core::MarkedTextContext{
+						.session = &session(),
+						.customEmojiRepaint = [=] { toast->update(); },
+					};
+				};
 				Ui::Toast::Show({
 					.text = tr::lng_payments_success(
 						tr::now,
 						lt_amount,
-						Ui::Text::Bold(payment->amount),
+						Ui::Text::Wrapped(
+							payment->amount,
+							EntityType::Bold),
 						lt_title,
 						Ui::Text::Bold(paid->title),
 						Ui::Text::WithEntities),
+					.textContext = context,
 				});
 			}
 		}
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 9cee2262e..4a8a413cc 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/click_handler_types.h"
 #include "base/unixtime.h"
 #include "base/timer_rpl.h"
+#include "boxes/send_credits_box.h"
 #include "api/api_text_entities.h"
 #include "api/api_updates.h"
 #include "data/components/scheduled_messages.h"
@@ -137,6 +138,17 @@ template <typename T>
 	return fields;
 }
 
+[[nodiscard]] TextWithEntities AmountAndStarCurrency(
+		not_null<Main::Session*> session,
+		int64 amount,
+		const QString &currency) {
+	if (currency == Ui::kCreditsCurrency) {
+		return Ui::CreditsEmojiSmall(session).append(
+			Lang::FormatCountDecimal(std::abs(amount)));
+	}
+	return { Ui::FillAmountAndCurrency(amount, currency) };
+}
+
 } // namespace
 
 void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) {
@@ -3957,7 +3969,10 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
 		payment->recurringInit = data.is_recurring_init();
 		payment->recurringUsed = data.is_recurring_used();
 		payment->isCreditsCurrency = (currency == Ui::kCreditsCurrency);
-		payment->amount = Ui::FillAmountAndCurrency(amount, currency);
+		payment->amount = AmountAndStarCurrency(
+			&_history->session(),
+			amount,
+			currency);
 		payment->invoiceLink = std::make_shared<LambdaClickHandler>([=](
 				ClickContext context) {
 			using namespace Payments;
@@ -4692,7 +4707,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 				lt_user,
 				Ui::Text::Link(peer->name(), 1), // Link 1.
 				lt_cost,
-				{ Ui::FillAmountAndCurrency(amount, currency) },
+				AmountAndStarCurrency(&peer->session(), amount, currency),
 				Ui::Text::WithEntities);
 		return result;
 	};
@@ -4905,9 +4920,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 					lt_user,
 					Ui::Text::Link(peer->name(), 1), // Link 1.
 					lt_cost,
-					{ Ui::FillAmountAndCurrency(
+					AmountAndStarCurrency(
+						&_history->session(),
 						action.vamount().value_or_empty(),
-						qs(action.vcurrency().value_or_empty())) },
+						qs(action.vcurrency().value_or_empty())),
 					Ui::Text::WithEntities);
 
 		}
@@ -4957,7 +4973,6 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 			Ui::Text::WithEntities);
 		return result;
 	};
-
 	auto preparePaymentRefunded = [&](const MTPDmessageActionPaymentRefunded &action) {
 		auto result = PreparedServiceText();
 		const auto refund = Get<HistoryServicePaymentRefund>();
@@ -4972,7 +4987,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 			lt_peer,
 			Ui::Text::Link(refund->peer->name(), 1), // Link 1.
 			lt_amount,
-			{ Ui::FillAmountAndCurrency(amount, currency) },
+			AmountAndStarCurrency(&_history->session(), amount, currency),
 			Ui::Text::WithEntities);
 		return result;
 	};
@@ -4993,7 +5008,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 				lt_user,
 				Ui::Text::Link(peer->name(), 1), // Link 1.
 				lt_cost,
-				{ Ui::FillAmountAndCurrency(amount, currency) },
+				AmountAndStarCurrency(
+					&_history->session(),
+					amount,
+					currency),
 				Ui::Text::WithEntities);
 		return result;
 	};
@@ -5412,7 +5430,7 @@ PreparedServiceText HistoryItem::preparePaymentSentText() {
 			result.text = tr::lng_action_payment_used_recurring(
 				tr::now,
 				lt_amount,
-				{ .text = payment->amount },
+				payment->amount,
 				Ui::Text::WithEntities);
 		} else {
 			result.text = (payment->recurringInit
@@ -5420,7 +5438,7 @@ PreparedServiceText HistoryItem::preparePaymentSentText() {
 				: tr::lng_action_payment_done)(
 					tr::now,
 					lt_amount,
-					{ .text = payment->amount },
+					payment->amount,
 					lt_user,
 					{ .text = _history->peer->name() },
 					Ui::Text::WithEntities);
@@ -5431,7 +5449,7 @@ PreparedServiceText HistoryItem::preparePaymentSentText() {
 			: tr::lng_action_payment_done_for)(
 				tr::now,
 				lt_amount,
-				{ .text = payment->amount },
+				payment->amount,
 				lt_user,
 				{ .text = _history->peer->name() },
 				lt_invoice,
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 7e9fb6396..1a5e14014 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -649,7 +649,7 @@ struct HistoryServicePayment
 : public RuntimeComponent<HistoryServicePayment, HistoryItem>
 , public HistoryServiceDependentData {
 	QString slug;
-	QString amount;
+	TextWithEntities amount;
 	ClickHandlerPtr invoiceLink;
 	bool recurringInit = false;
 	bool recurringUsed = false;
diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp
index d95062313..7ebd961f3 100644
--- a/Telegram/SourceFiles/ui/text/format_values.cpp
+++ b/Telegram/SourceFiles/ui/text/format_values.cpp
@@ -146,11 +146,11 @@ QString FillAmountAndCurrency(
 	// std::abs doesn't work on that one :/
 	Expects(amount != std::numeric_limits<int64>::min());
 
-	const auto rule = LookupCurrencyRule(currency);
 	if (currency == kCreditsCurrency) {
 		return QChar(0x2B50) + Lang::FormatCountDecimal(std::abs(amount));
 	}
 
+	const auto rule = LookupCurrencyRule(currency);
 	const auto prefix = (amount < 0)
 		? QString::fromUtf8("\xe2\x88\x92")
 		: QString();

From 5fdd4eba80d26699749a1ee194aac454d0c3bdf5 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 23 Jul 2024 14:56:06 +0200
Subject: [PATCH 122/163] Implement weather area in stories.

---
 Telegram/SourceFiles/data/data_story.cpp      |  26 +-
 Telegram/SourceFiles/data/data_story.h        |   6 +-
 .../stories/media_stories_controller.cpp      |  42 ++-
 .../media/stories/media_stories_controller.h  |   7 +-
 .../media/stories/media_stories_reactions.cpp | 298 +++++++++++++++++-
 .../media/stories/media_stories_reactions.h   |  15 +-
 .../SourceFiles/ui/color_int_conversion.cpp   |   8 +
 .../SourceFiles/ui/color_int_conversion.h     |   1 +
 8 files changed, 369 insertions(+), 34 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp
index edd4670d8..d9c37de4e 100644
--- a/Telegram/SourceFiles/data/data_story.cpp
+++ b/Telegram/SourceFiles/data/data_story.cpp
@@ -41,13 +41,10 @@ using UpdateFlag = StoryUpdate::Flag;
 	return {
 		.geometry = { corner / 100., size / 100. },
 		.rotation = data.vrotation().v,
+		.radius = data.vradius().value_or_empty(),
 	};
 }
 
-[[nodiscard]] uint32 ParseMilliKelvin(double celcius) {
-	return uint32(std::clamp(celcius + 273.15, 0., 1'000'000.) * 1000.);
-}
-
 [[nodiscard]] TextWithEntities StripLinks(TextWithEntities text) {
 	const auto link = [&](const EntityInText &entity) {
 		return (entity.type() == EntityType::CustomUrl)
@@ -176,8 +173,11 @@ using UpdateFlag = StoryUpdate::Flag;
 		result.emplace(WeatherArea{
 			.area = ParseArea(data.vcoordinates()),
 			.emoji = qs(data.vemoji()),
-			.color = Ui::ColorFromSerialized(data.vcolor().v),
-			.millicelcius = int(data.vtemperature_c().v * 1000.),
+			.color = Ui::Color32FromSerialized(data.vcolor().v),
+			.millicelsius = int(1000. * std::clamp(
+				data.vtemperature_c().v,
+				-274.,
+				1'000'000.)),
 		});
 	}, [&](const MTPDinputMediaAreaChannelPost &data) {
 		LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
@@ -721,6 +721,10 @@ const std::vector<UrlArea> &Story::urlAreas() const {
 	return _urlAreas;
 }
 
+const std::vector<WeatherArea> &Story::weatherAreas() const {
+	return _weatherAreas;
+}
+
 void Story::applyChanges(
 		StoryMedia media,
 		const MTPDstoryItem &data,
@@ -825,6 +829,7 @@ void Story::applyFields(
 	auto suggestedReactions = std::vector<SuggestedReaction>();
 	auto channelPosts = std::vector<ChannelPost>();
 	auto urlAreas = std::vector<UrlArea>();
+	auto weatherAreas = std::vector<WeatherArea>();
 	if (const auto areas = data.vmedia_areas()) {
 		for (const auto &area : areas->v) {
 			if (const auto location = ParseLocation(area)) {
@@ -840,6 +845,8 @@ void Story::applyFields(
 				channelPosts.push_back(*post);
 			} else if (auto url = ParseUrlArea(area)) {
 				urlAreas.push_back(*url);
+			} else if (auto weather = ParseWeatherArea(area)) {
+				weatherAreas.push_back(*weather);
 			}
 		}
 	}
@@ -853,6 +860,7 @@ void Story::applyFields(
 		= (_suggestedReactions != suggestedReactions);
 	const auto channelPostsChanged = (_channelPosts != channelPosts);
 	const auto urlAreasChanged = (_urlAreas != urlAreas);
+	const auto weatherAreasChanged = (_weatherAreas != weatherAreas);
 	const auto reactionChanged = (_sentReactionId != reaction);
 
 	_out = out;
@@ -881,6 +889,9 @@ void Story::applyFields(
 	if (urlAreasChanged) {
 		_urlAreas = std::move(urlAreas);
 	}
+	if (weatherAreasChanged) {
+		_weatherAreas = std::move(weatherAreas);
+	}
 	if (reactionChanged) {
 		_sentReactionId = reaction;
 	}
@@ -891,7 +902,8 @@ void Story::applyFields(
 		|| mediaChanged
 		|| locationsChanged
 		|| channelPostsChanged
-		|| urlAreasChanged;
+		|| urlAreasChanged
+		|| weatherAreasChanged;
 	const auto reactionsChanged = reactionChanged
 		|| suggestedReactionsChanged;
 	if (!initial && (changed || reactionsChanged)) {
diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h
index 4dfc7a712..bd508591c 100644
--- a/Telegram/SourceFiles/data/data_story.h
+++ b/Telegram/SourceFiles/data/data_story.h
@@ -81,6 +81,7 @@ struct StoryViews {
 struct StoryArea {
 	QRectF geometry;
 	float64 rotation = 0;
+	float64 radius = 0;
 
 	friend inline bool operator==(
 		const StoryArea &,
@@ -135,7 +136,7 @@ struct WeatherArea {
 	StoryArea area;
 	QString emoji;
 	QColor color;
-	int millicelcius = 0;
+	int millicelsius = 0;
 
 	friend inline bool operator==(
 		const WeatherArea &,
@@ -219,6 +220,8 @@ public:
 		-> const std::vector<ChannelPost> &;
 	[[nodiscard]] auto urlAreas() const
 		-> const std::vector<UrlArea> &;
+	[[nodiscard]] auto weatherAreas() const
+		-> const std::vector<WeatherArea> &;
 
 	void applyChanges(
 		StoryMedia media,
@@ -270,6 +273,7 @@ private:
 	std::vector<SuggestedReaction> _suggestedReactions;
 	std::vector<ChannelPost> _channelPosts;
 	std::vector<UrlArea> _urlAreas;
+	std::vector<WeatherArea> _weatherAreas;
 	StoryViews _views;
 	StoryViews _channelReactions;
 	const TimeId _date = 0;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index c5fd13992..ca558abfd 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -536,8 +536,9 @@ void Controller::rebuildActiveAreas(const Layout &layout) const {
 			int(base::SafeRound(general.width() * scale.width())),
 			int(base::SafeRound(general.height() * scale.height()))
 		).translated(origin);
-		if (const auto reaction = area.reaction.get()) {
-			reaction->setAreaGeometry(area.geometry);
+		area.radius = scale.width() * area.radiusOriginal / 100.;
+		if (const auto view = area.view.get()) {
+			view->setAreaGeometry(area.geometry, area.radius);
 		}
 	}
 }
@@ -1050,6 +1051,9 @@ void Controller::updateAreas(Data::Story *story) {
 	const auto &urlAreas = story
 		? story->urlAreas()
 		: std::vector<Data::UrlArea>();
+	const auto &weatherAreas = story
+		? story->weatherAreas()
+		: std::vector<Data::WeatherArea>();
 	if (_locations != locations) {
 		_locations = locations;
 		_areas.clear();
@@ -1062,13 +1066,18 @@ void Controller::updateAreas(Data::Story *story) {
 		_urlAreas = urlAreas;
 		_areas.clear();
 	}
+	if (_weatherAreas != weatherAreas) {
+		_weatherAreas = weatherAreas;
+		_areas.clear();
+	}
 	const auto reactionsCount = int(suggestedReactions.size());
 	if (_suggestedReactions.size() == reactionsCount && !_areas.empty()) {
 		for (auto i = 0; i != reactionsCount; ++i) {
 			const auto count = suggestedReactions[i].count;
 			if (_suggestedReactions[i].count != count) {
 				_suggestedReactions[i].count = count;
-				_areas[i + _locations.size()].reaction->updateCount(count);
+				const auto view = _areas[i + _locations.size()].view.get();
+				view->updateReactionsCount(count);
 			}
 			if (_suggestedReactions[i] != suggestedReactions[i]) {
 				_suggestedReactions = suggestedReactions;
@@ -1206,7 +1215,8 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
 		|| (_locations.empty()
 			&& _suggestedReactions.empty()
 			&& _channelPosts.empty()
-			&& _urlAreas.empty())) {
+			&& _urlAreas.empty()
+			&& _weatherAreas.empty())) {
 		return nullptr;
 	} else if (_areas.empty()) {
 		const auto now = story();
@@ -1240,7 +1250,7 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
 						}
 					}
 				}),
-				.reaction = std::move(widget),
+				.view = std::move(widget),
 			});
 		}
 		if (const auto session = now ? &now->session() : nullptr) {
@@ -1261,19 +1271,27 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
 				.handler = std::make_shared<HiddenUrlClickHandler>(url.url),
 			});
 		}
+		for (const auto &weather : _weatherAreas) {
+			auto widget = _reactions->makeWeatherAreaWidget(weather);
+			const auto raw = widget.get();
+			_areas.push_back({
+				.original = weather.area.geometry,
+				.radiusOriginal = weather.area.radius,
+				.rotation = weather.area.rotation,
+				.handler = std::make_shared<LambdaClickHandler>([=] {
+					raw->toggleMode();
+				}),
+				.view = std::move(widget),
+			});
+		}
 		rebuildActiveAreas(*layout);
 	}
 
-	const auto circleContains = [&](QRect circle) {
-		const auto radius = std::min(circle.width(), circle.height()) / 2;
-		const auto delta = circle.center() - point;
-		return QPoint::dotProduct(delta, delta) < (radius * radius);
-	};
 	for (const auto &area : _areas) {
 		const auto center = area.geometry.center();
 		const auto angle = -area.rotation;
-		const auto contains = area.reaction
-			? circleContains(area.geometry)
+		const auto contains = area.view
+			? area.view->contains(point)
 			: area.geometry.contains(Rotated(point, center, angle));
 		if (contains) {
 			return area.handler;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 3d486fa87..590dee3f6 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -68,7 +68,7 @@ struct ContentLayout;
 class CaptionFullView;
 class RepostView;
 enum class ReactionsMode;
-class SuggestedReactionView;
+class StoryAreaView;
 struct RepostClickHandler;
 
 enum class HeaderLayout {
@@ -208,10 +208,12 @@ private:
 	};
 	struct ActiveArea {
 		QRectF original;
+		float64 radiusOriginal = 0.;
 		QRect geometry;
 		float64 rotation = 0.;
+		float64 radius = 0.;
 		ClickHandlerPtr handler;
-		std::unique_ptr<SuggestedReactionView> reaction;
+		std::unique_ptr<StoryAreaView> view;
 	};
 
 	void initLayout();
@@ -303,6 +305,7 @@ private:
 	std::vector<Data::SuggestedReaction> _suggestedReactions;
 	std::vector<Data::ChannelPost> _channelPosts;
 	std::vector<Data::UrlArea> _urlAreas;
+	std::vector<Data::WeatherArea> _weatherAreas;
 	mutable std::vector<ActiveArea> _areas;
 
 	std::vector<CachedSource> _cachedSourcesList;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
index af45d9150..f5e2582ef 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/unixtime.h"
 #include "boxes/premium_preview_box.h"
 #include "chat_helpers/compose/compose_show.h"
+#include "chat_helpers/stickers_lottie.h"
+#include "chat_helpers/stickers_emoji_pack.h"
 #include "data/data_changes.h"
 #include "data/data_document.h"
 #include "data/data_document_media.h"
@@ -20,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "history/admin_log/history_admin_log_item.h"
 #include "history/view/media/history_view_custom_emoji.h"
 #include "history/view/media/history_view_media_unwrapped.h"
+#include "history/view/media/history_view_sticker_player.h"
 #include "history/view/reactions/history_view_reactions_selector.h"
 #include "history/view/history_view_element.h"
 #include "history/history_item_reply_markup.h"
@@ -61,7 +64,7 @@ constexpr auto kStoppingFadeDuration = crl::time(150);
 
 class ReactionView final
 	: public Ui::RpWidget
-	, public SuggestedReactionView
+	, public StoryAreaView
 	, public HistoryView::DefaultElementDelegate {
 public:
 	ReactionView(
@@ -69,9 +72,11 @@ public:
 		not_null<Main::Session*> session,
 		const Data::SuggestedReaction &reaction);
 
-	void setAreaGeometry(QRect geometry) override;
-	void updateCount(int count) override;
+	void setAreaGeometry(QRect geometry, float64 radius) override;
+	void updateReactionsCount(int count) override;
 	void playEffect() override;
+	void toggleMode() override;
+	bool contains(QPoint point) override;
 
 private:
 	using Element = HistoryView::Element;
@@ -108,6 +113,7 @@ private:
 	Ui::Text::String _counter;
 	Ui::Animations::Simple _counterAnimation;
 	QRectF _bubbleGeometry;
+	QRect _apiGeometry;
 	int _size = 0;
 	int _mediaLeft = 0;
 	int _mediaTop = 0;
@@ -126,6 +132,58 @@ private:
 
 };
 
+class WeatherView final : public Ui::RpWidget, public StoryAreaView {
+public:
+	WeatherView(
+		QWidget *parent,
+		not_null<Main::Session*> session,
+		const Data::WeatherArea &data);
+
+	void setAreaGeometry(QRect geometry, float64 radius) override;
+	void updateReactionsCount(int count) override;
+	void playEffect() override;
+	void toggleMode() override;
+	bool contains(QPoint point) override;
+
+private:
+	void paintEvent(QPaintEvent *e) override;
+
+	void cacheBackground();
+	void watchForSticker();
+	void setStickerFrom(not_null<DocumentData*> document);
+	[[nodiscard]] QSize stickerSize() const;
+
+	const not_null<Main::Session*> _session;
+	Data::WeatherArea _data;
+	EmojiPtr _emoji;
+	QColor _fg;
+	QImage _background;
+	QFont _font;
+	QRectF _rect;
+	QRect _wrapped;
+	float64 _radius = 0.;
+	int _emojiSize = 0;
+	int _padding = 0;
+	bool _celsius = true;
+
+	std::shared_ptr<HistoryView::StickerPlayer> _sticker;
+	rpl::lifetime _lifetime;
+
+};
+
+[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {
+	if (std::abs(angle) < 1.) {
+		return point;
+	}
+	const auto alpha = angle / 180. * M_PI;
+	const auto acos = cos(alpha);
+	const auto asin = sin(alpha);
+	point -= origin;
+	return origin + QPoint(
+		int(base::SafeRound(acos * point.x() - asin * point.y())),
+		int(base::SafeRound(asin * point.x() + acos * point.y())));
+}
+
 [[nodiscard]] AdminLog::OwnedItem GenerateFakeItem(
 		not_null<HistoryView::ElementDelegate*> delegate,
 		not_null<History*> history) {
@@ -140,6 +198,13 @@ private:
 	return AdminLog::OwnedItem(delegate, item);
 }
 
+[[nodiscard]] QColor ChooseWeatherFg(const QColor &bg) {
+	const auto luminance = (0.2126 * bg.redF())
+		+ (0.7152 * bg.greenF())
+		+ (0.0722 * bg.blueF());
+	return (luminance > 0.705) ? QColor(0, 0, 0) : QColor(255, 255, 255);
+}
+
 ReactionView::ReactionView(
 	QWidget *parent,
 	not_null<Main::Session*> session,
@@ -198,7 +263,7 @@ ReactionView::ReactionView(
 	}, lifetime());
 
 	_data.count = 0;
-	updateCount(reaction.count);
+	updateReactionsCount(reaction.count);
 	_counterAnimation.stop();
 
 	setupCustomChatStylePalette();
@@ -212,7 +277,8 @@ void ReactionView::setupCustomChatStylePalette() {
 	_chatStyle->applyCustomPalette(_chatStyle.get());
 }
 
-void ReactionView::setAreaGeometry(QRect geometry) {
+void ReactionView::setAreaGeometry(QRect geometry, float64 radius) {
+	_apiGeometry = geometry;
 	_size = std::min(geometry.width(), geometry.height());
 	_bubble = _size * kSuggestedBubbleSize;
 	_bigOffset = _bubble * kSuggestedTailBigOffset;
@@ -228,7 +294,7 @@ void ReactionView::setAreaGeometry(QRect geometry) {
 	updateEffectGeometry();
 }
 
-void ReactionView::updateCount(int count) {
+void ReactionView::updateReactionsCount(int count) {
 	if (_data.count == count) {
 		return;
 	}
@@ -283,6 +349,17 @@ void ReactionView::playEffect() {
 	}
 }
 
+void ReactionView::toggleMode() {
+	Unexpected("ReactionView::toggleMode.");
+}
+
+bool ReactionView::contains(QPoint point) {
+	const auto circle = _apiGeometry;
+	const auto radius = std::min(circle.width(), circle.height()) / 2;
+	const auto delta = circle.center() - point;
+	return QPoint::dotProduct(delta, delta) < (radius * radius);
+}
+
 void ReactionView::paintEffectFrame(
 		QPainter &p,
 		not_null<Ui::ReactionFlyAnimation*> effect,
@@ -457,6 +534,205 @@ void ReactionView::cacheBackground() {
 	paintShape(_data.dark ? dark : QColor(255, 255, 255));
 }
 
+WeatherView::WeatherView(
+	QWidget *parent,
+	not_null<Main::Session*> session,
+	const Data::WeatherArea &data)
+: RpWidget(parent)
+, _session(session)
+, _data(data)
+, _emoji(Ui::Emoji::Find(_data.emoji))
+, _fg(ChooseWeatherFg(_data.color)) {
+	watchForSticker();
+	setAttribute(Qt::WA_TransparentForMouseEvents);
+	show();
+}
+
+void WeatherView::watchForSticker() {
+	if (!_emoji) {
+		return;
+	}
+	const auto emojiStickers = &_session->emojiStickersPack();
+	if (const auto sticker = emojiStickers->stickerForEmoji(_emoji)) {
+		setStickerFrom(sticker.document);
+	} else {
+		emojiStickers->refreshed() | rpl::map([=] {
+			return emojiStickers->stickerForEmoji(_emoji).document;
+		}) | rpl::filter([=](DocumentData *document) {
+			return document != nullptr;
+		}) | rpl::take(
+			1
+		) | rpl::start_with_next([=](not_null<DocumentData*> document) {
+			setStickerFrom(document);
+			update();
+		}, _lifetime);
+	}
+}
+
+void WeatherView::setAreaGeometry(QRect geometry, float64 radius) {
+	const auto diagxdiag = (geometry.width() * geometry.width())
+		+ (geometry.height() * geometry.height());
+	const auto diag = std::sqrt(diagxdiag);
+	const auto topleft = QRectF(geometry).center()
+		- QPointF(diag / 2., diag / 2.);
+	const auto bottomright = topleft + QPointF(diag, diag);
+	const auto left = int(std::floor(topleft.x()));
+	const auto top = int(std::floor(topleft.y()));
+	const auto right = int(std::ceil(bottomright.x()));
+	const auto bottom = int(std::ceil(bottomright.y()));
+	setGeometry(left, top, right - left, bottom - top);
+	_rect = QRectF(geometry).translated(-left, -top);
+	_radius = radius;
+
+	_emojiSize = int(base::SafeRound(_rect.height() * 2 / 3.));
+	_font = st::semiboldFont->f;
+	_font.setPixelSize(_emojiSize);
+	_background = {};
+}
+
+void WeatherView::updateReactionsCount(int count) {
+	Unexpected("WeatherView::updateRactionsCount.");
+}
+
+void WeatherView::playEffect() {
+	Unexpected("WeatherView::playEffect.");
+}
+
+void WeatherView::toggleMode() {
+	_celsius = !_celsius;
+	_background = {};
+	update();
+}
+
+bool WeatherView::contains(QPoint point) {
+	const auto geometry = _rect.translated(pos()).toRect();
+	const auto angle = -_data.area.rotation;
+	return geometry.contains(Rotated(point, geometry.center(), angle));
+}
+
+void WeatherView::paintEvent(QPaintEvent *e) {
+	auto p = Painter(this);
+	if (_background.size() != size() * style::DevicePixelRatio()) {
+		cacheBackground();
+	}
+	p.drawImage(0, 0, _background);
+	if (_sticker && _sticker->ready()) {
+		auto hq = PainterHighQualityEnabler(p);
+		const auto rcenter = _wrapped.center();
+		p.translate(rcenter);
+		p.rotate(_data.area.rotation);
+		p.translate(-rcenter);
+
+		const auto image = _sticker->frame(
+			stickerSize(),
+			QColor(0, 0, 0, 0),
+			false,
+			crl::now(),
+			false).image;
+		const auto size = image.size() / style::DevicePixelRatio();
+		const auto rect = QRectF(
+			_wrapped.x() + _padding + (_emojiSize - size.width()) / 2.,
+			_wrapped.y() + (_wrapped.height() - size.height()) / 2.,
+			size.width(),
+			size.height());
+		const auto scenter = rect.center();
+		const auto scale = (_emojiSize * 1.) / stickerSize().width();
+		p.translate(scenter);
+		p.scale(scale, scale);
+		p.translate(-scenter);
+		p.drawImage(rect, image);
+		_sticker->markFrameShown();
+	}
+}
+
+QSize WeatherView::stickerSize() const {
+	return QSize(st::chatIntroStickerSize, st::chatIntroStickerSize);
+}
+
+void WeatherView::setStickerFrom(not_null<DocumentData*> document) {
+	if (_sticker || !_emoji) {
+		return;
+	}
+	const auto media = document->createMediaView();
+	media->checkStickerLarge();
+	media->goodThumbnailWanted();
+
+	rpl::single() | rpl::then(
+		document->owner().session().downloaderTaskFinished()
+	) | rpl::filter([=] {
+		return media->loaded();
+	}) | rpl::take(1) | rpl::start_with_next([=] {
+		const auto sticker = document->sticker();
+		if (sticker->isLottie()) {
+			_sticker = std::make_shared<HistoryView::LottiePlayer>(
+				ChatHelpers::LottiePlayerFromDocument(
+					media.get(),
+					ChatHelpers::StickerLottieSize::StickerSet,
+					stickerSize(),
+					Lottie::Quality::High));
+		} else if (sticker->isWebm()) {
+			_sticker = std::make_shared<HistoryView::WebmPlayer>(
+				media->owner()->location(),
+				media->bytes(),
+				stickerSize());
+		} else {
+			_sticker = std::make_shared<HistoryView::StaticStickerPlayer>(
+				media->owner()->location(),
+				media->bytes(),
+				stickerSize());
+		}
+		_sticker->setRepaintCallback([=] { update(); });
+		update();
+	}, _lifetime);
+}
+
+void WeatherView::cacheBackground() {
+	const auto ratio = style::DevicePixelRatio();
+	_background = QImage(
+		size() * ratio,
+		QImage::Format_ARGB32_Premultiplied);
+	_background.setDevicePixelRatio(ratio);
+	_background.fill(Qt::transparent);
+
+	auto p = QPainter(&_background);
+	auto hq = PainterHighQualityEnabler(p);
+	p.setBrush(_data.color);
+	p.setPen(Qt::NoPen);
+	const auto center = _rect.center();
+	p.translate(center);
+	p.rotate(_data.area.rotation);
+	p.translate(-center);
+
+	const auto format = [](float64 value) {
+		return QString::number(int(base::SafeRound(value * 10)) / 10.);
+	};
+	const auto text = [&] {
+		const auto celsius = _data.millicelsius / 1000.;
+		if (_celsius) {
+			return format(celsius);
+		}
+		const auto fahrenheit = (celsius * 9.0 / 5.0) + 32;
+		return format(fahrenheit);
+	}().append(QChar(0xb0)).append(_celsius ? "C" : "F");
+	const auto metrics = QFontMetrics(_font);
+	const auto textWidth = metrics.horizontalAdvance(text);
+	_padding = int(_rect.height() / 6);
+	const auto fullWidth = (_emoji ? _emojiSize : 0)
+		+ textWidth
+		+ (2 * _padding);
+	const auto left = _rect.x() + (_rect.width() - fullWidth) / 2;
+	_wrapped = QRect(left, _rect.y(), fullWidth, _rect.height());
+
+	p.drawRoundedRect(_wrapped, _radius, _radius);
+
+	p.setPen(_fg);
+	p.setFont(_font);
+	p.drawText(_wrapped.marginsRemoved(
+		{ _padding + (_emoji ? _emojiSize : 0), 0, _padding, 0 }),
+		text,
+		style::al_center);
+}
+
 [[nodiscard]] Data::ReactionId HeartReactionId() {
 	return { QString() + QChar(10084) };
 }
@@ -804,13 +1080,21 @@ auto Reactions::chosen() const -> rpl::producer<Chosen> {
 
 auto Reactions::makeSuggestedReactionWidget(
 	const Data::SuggestedReaction &reaction)
--> std::unique_ptr<SuggestedReactionView> {
+-> std::unique_ptr<StoryAreaView> {
 	return std::make_unique<ReactionView>(
 		_controller->wrap(),
 		&_controller->uiShow()->session(),
 		reaction);
 }
 
+auto Reactions::makeWeatherAreaWidget(const Data::WeatherArea &data)
+-> std::unique_ptr<StoryAreaView> {
+	return std::make_unique<WeatherView>(
+		_controller->wrap(),
+		&_controller->uiShow()->session(),
+		data);
+}
+
 void Reactions::setReplyFieldState(
 		rpl::producer<bool> focused,
 		rpl::producer<bool> hasSendText) {
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
index de9f0e0ce..b17e2e19d 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
@@ -16,6 +16,7 @@ struct ReactionId;
 class Session;
 class Story;
 struct SuggestedReaction;
+struct WeatherArea;
 } // namespace Data
 
 namespace HistoryView::Reactions {
@@ -41,13 +42,15 @@ enum class ReactionsMode {
 	Reaction,
 };
 
-class SuggestedReactionView {
+class StoryAreaView {
 public:
-	virtual ~SuggestedReactionView() = default;
+	virtual ~StoryAreaView() = default;
 
-	virtual void setAreaGeometry(QRect geometry) = 0;
-	virtual void updateCount(int count) = 0;
+	virtual void setAreaGeometry(QRect geometry, float64 radius) = 0;
+	virtual void updateReactionsCount(int count) = 0;
 	virtual void playEffect() = 0;
+	virtual void toggleMode() = 0;
+	virtual bool contains(QPoint point) = 0;
 };
 
 class Reactions final {
@@ -79,7 +82,9 @@ public:
 
 	[[nodiscard]] auto makeSuggestedReactionWidget(
 		const Data::SuggestedReaction &reaction)
-	-> std::unique_ptr<SuggestedReactionView>;
+	-> std::unique_ptr<StoryAreaView>;
+	[[nodiscard]] auto makeWeatherAreaWidget(const Data::WeatherArea &data)
+	-> std::unique_ptr<StoryAreaView>;
 
 	void setReplyFieldState(
 		rpl::producer<bool> focused,
diff --git a/Telegram/SourceFiles/ui/color_int_conversion.cpp b/Telegram/SourceFiles/ui/color_int_conversion.cpp
index a1b0bcb45..5c9c57f07 100644
--- a/Telegram/SourceFiles/ui/color_int_conversion.cpp
+++ b/Telegram/SourceFiles/ui/color_int_conversion.cpp
@@ -22,4 +22,12 @@ std::optional<QColor> MaybeColorFromSerialized(quint32 serialized) {
 		: std::make_optional(ColorFromSerialized(serialized));
 }
 
+QColor Color32FromSerialized(quint32 serialized) {
+	return QColor(
+		int((serialized >> 24) & 0xFFU),
+		int((serialized >> 16) & 0xFFU),
+		int((serialized >> 8) & 0xFFU),
+		int(serialized & 0xFFU));
+}
+
 } // namespace Ui
diff --git a/Telegram/SourceFiles/ui/color_int_conversion.h b/Telegram/SourceFiles/ui/color_int_conversion.h
index ed2bb6a18..1102863b2 100644
--- a/Telegram/SourceFiles/ui/color_int_conversion.h
+++ b/Telegram/SourceFiles/ui/color_int_conversion.h
@@ -12,5 +12,6 @@ namespace Ui {
 [[nodiscard]] QColor ColorFromSerialized(quint32 serialized);
 [[nodiscard]] std::optional<QColor> MaybeColorFromSerialized(
 	quint32 serialized);
+[[nodiscard]] QColor Color32FromSerialized(quint32 serialized);
 
 } // namespace Ui

From a5ffd8b7cf7ecbe60f5e413a7698e189b6c302b9 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 24 Jul 2024 13:44:15 +0200
Subject: [PATCH 123/163] Request new main web app.

---
 .../inline_bots/bot_attach_web_view.cpp       | 33 +++++++++++++++++--
 .../inline_bots/bot_attach_web_view.h         |  1 +
 2 files changed, 32 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index e7d842c64..e0a609f7a 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -651,7 +651,9 @@ void WebViewInstance::resolve() {
 	}, [&](WebViewSourceLinkApp data) {
 		resolveApp(data.appname, data.token, !_context.maySkipConfirmation);
 	}, [&](WebViewSourceLinkBotProfile) {
-		requestWithMenuAdd();
+		confirmOpen([=] {
+			requestMain();
+		});
 	}, [&](WebViewSourceLinkAttachMenu data) {
 		requestWithMenuAdd();
 	}, [&](WebViewSourceMainMenu) {
@@ -667,7 +669,9 @@ void WebViewInstance::resolve() {
 	}, [&](WebViewSourceGame game) {
 		showGame();
 	}, [&](WebViewSourceBotProfile) {
-		requestWithMenuAdd();
+		confirmOpen([=] {
+			requestMain();
+		});
 	});
 }
 
@@ -868,6 +872,31 @@ void WebViewInstance::requestSimple() {
 	}).send();
 }
 
+void WebViewInstance::requestMain() {
+	using Flag = MTPmessages_RequestMainWebView::Flag;
+	_requestId = _session->api().request(MTPmessages_RequestMainWebView(
+		MTP_flags(Flag::f_theme_params
+			| (_button.startCommand.isEmpty()
+						? Flag()
+						: Flag::f_start_param)
+			| (v::is<WebViewSourceLinkBotProfile>(_source)
+				? (v::get<WebViewSourceLinkBotProfile>(_source).compact
+					? Flag::f_compact
+					: Flag(0))
+				: Flag(0))),
+		_context.action->history->peer->input,
+		_bot->inputUser,
+		MTP_string(_button.startCommand),
+		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
+		MTP_string("tdesktop")
+	)).done([=](const MTPWebViewResult &result) {
+		show(qs(result.data().vurl()));
+	}).fail([=](const MTP::Error &error) {
+		_parentShow->showToast(error.type());
+		close();
+	}).send();
+}
+
 void WebViewInstance::requestApp(bool allowWrite) {
 	Expects(_app != nullptr);
 	Expects(_context.action.has_value());
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
index 6e11c87cd..ecccdd042 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
@@ -239,6 +239,7 @@ private:
 
 	void requestButton();
 	void requestSimple();
+	void requestMain();
 	void requestApp(bool allowWrite);
 	void requestWithMainMenuDisclaimer();
 	void requestWithMenuAdd();

From 992c876930169c1d0fe6dfd1d075a8dd1c71273e Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 25 Jul 2024 10:30:39 +0200
Subject: [PATCH 124/163] Show correct presents in Stars gifts.

---
 .../chat_helpers/stickers_gift_box_pack.cpp            | 10 ++++++++++
 .../SourceFiles/chat_helpers/stickers_gift_box_pack.h  |  1 +
 .../history/view/media/history_view_premium_gift.cpp   |  3 ++-
 3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
index 2e810dd6c..f510540c2 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
@@ -22,6 +22,16 @@ GiftBoxPack::GiftBoxPack(not_null<Main::Session*> session)
 
 GiftBoxPack::~GiftBoxPack() = default;
 
+int GiftBoxPack::monthsForStars(int stars) const {
+	if (stars <= 1000) {
+		return 3;
+	} else if (stars < 2500) {
+		return 6;
+	} else {
+		return 12;
+	}
+}
+
 DocumentData *GiftBoxPack::lookup(int months) const {
 	const auto it = ranges::lower_bound(_localMonths, months);
 	const auto fallback = _documents.empty() ? nullptr : _documents[0];
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
index 4c480fba3..1e6c0e934 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
+++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
@@ -25,6 +25,7 @@ public:
 	~GiftBoxPack();
 
 	void load();
+	[[nodiscard]] int monthsForStars(int stars) const;
 	[[nodiscard]] DocumentData *lookup(int months) const;
 	[[nodiscard]] Data::FileOrigin origin() const;
 
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 885e74a62..2a7a2f4b1 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
@@ -185,8 +185,9 @@ void PremiumGift::ensureStickerCreated() const {
 		return;
 	}
 	const auto &session = _parent->history()->session();
-	const auto months = stars() ? 1 : _data.count;
 	auto &packs = session.giftBoxStickersPacks();
+	const auto count = stars();
+	const auto months = count ? packs.monthsForStars(count) : _data.count;
 	if (const auto document = packs.lookup(months)) {
 		if (const auto sticker = document->sticker()) {
 			const auto skipPremiumEffect = false;

From 4b09050061ad5783f416d3b25374e46a85d29f19 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 25 Jul 2024 11:40:22 +0200
Subject: [PATCH 125/163] Implement Stars gift view from service messages.

---
 Telegram/Resources/langs/lang.strings         |   3 +
 .../SourceFiles/boxes/gift_premium_box.cpp    |   4 +-
 Telegram/SourceFiles/calls/calls_panel.cpp    |   4 +-
 Telegram/SourceFiles/history/history_item.cpp |  29 ++-
 .../view/media/history_view_premium_gift.cpp  |  22 ++-
 .../settings/settings_credits_graphics.cpp    | 178 ++++++++++--------
 .../settings/settings_credits_graphics.h      |   7 +
 .../ui/effects/credits_graphics.cpp           |   4 +-
 8 files changed, 154 insertions(+), 97 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index fad51ea32..fedb32957 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1845,6 +1845,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_action_webview_data_done" = "You have just successfully transferred data from the «{text}» button to the bot.";
 "lng_action_gift_received" = "{user} sent you a gift for {cost}";
 "lng_action_gift_received_me" = "You sent to {user} a gift for {cost}";
+"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}";
 "lng_action_suggested_photo_me" = "You suggested {user} to use this profile photo.";
 "lng_action_suggested_photo" = "{user} suggests you to use this profile photo.";
 "lng_action_suggested_photo_button" = "View Photo";
@@ -2358,6 +2359,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_credits_box_out_confirm#one" = "Confirm and Pay {emoji} {count} Star";
 "lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars";
 "lng_credits_box_out_about" = "Review the {link} for Stars.";
+"lng_credits_box_out_about_link" = "https://telegram.org/tos/stars";
 "lng_credits_media_done_title" = "Media Unlocked";
 "lng_credits_media_done_text#one" = "**{count} Star** transferred to {chat}.";
 "lng_credits_media_done_text#other" = "**{count} Stars** transferred to {chat}.";
@@ -2372,6 +2374,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_credits_box_history_entry_fragment" = "Fragment";
 "lng_credits_box_history_entry_anonymous" = "Unknown User";
 "lng_credits_box_history_entry_gift_name" = "Received Gift";
+"lng_credits_box_history_entry_gift_sent" = "Sent Gift";
 "lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}";
 "lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}";
 "lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}";
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index 5730d9b40..3b8f6f1b5 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -1694,7 +1694,9 @@ void AddCreditsHistoryEntryTable(
 	} else if (entry.peerType == Type::Fragment) {
 		AddTableRow(
 			table,
-			tr::lng_credits_box_history_entry_via(),
+			(entry.gift
+				? tr::lng_credits_box_history_entry_peer_in
+				: tr::lng_credits_box_history_entry_via)(),
 			(entry.gift
 				? tr::lng_credits_box_history_entry_anonymous
 				: tr::lng_credits_box_history_entry_fragment)(
diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp
index ea204a770..6a816af96 100644
--- a/Telegram/SourceFiles/calls/calls_panel.cpp
+++ b/Telegram/SourceFiles/calls/calls_panel.cpp
@@ -276,8 +276,8 @@ void Panel::initControls() {
 				_layerBg->showBox(std::move(box));
 			}
 		} else if (const auto source = env->uniqueDesktopCaptureSource()) {
-			if (_call->isSharingScreen()) {
-				_call->toggleScreenSharing(std::nullopt);
+			if (!chooseSourceActiveDeviceId().isEmpty()) {
+				chooseSourceStop();
 			} else {
 				chooseSourceAccepted(*source, false);
 			}
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 4a8a413cc..a38b9b33f 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -4694,21 +4694,32 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
 	auto prepareGiftPremium = [&](
 			const MTPDmessageActionGiftPremium &action) {
 		auto result = PreparedServiceText();
-		const auto isSelf = (_from->id == _from->session().userPeerId());
+		const auto session = &_history->session();
+		const auto isSelf = _from->isSelf();
 		const auto peer = isSelf ? _history->peer : _from;
-		_history->session().giftBoxStickersPacks().load();
+		session->giftBoxStickersPacks().load();
 		const auto amount = action.vamount().v;
 		const auto currency = qs(action.vcurrency());
-		result.links.push_back(peer->createOpenLink());
-		result.text = (isSelf
-			? tr::lng_action_gift_received_me
-			: tr::lng_action_gift_received)(
+		const auto cost = AmountAndStarCurrency(session, amount, currency);
+		const auto anonymous = _from->isServiceUser();
+		if (anonymous) {
+			result.text = tr::lng_action_gift_received_anonymous(
 				tr::now,
-				lt_user,
-				Ui::Text::Link(peer->name(), 1), // Link 1.
 				lt_cost,
-				AmountAndStarCurrency(&peer->session(), amount, currency),
+				cost,
 				Ui::Text::WithEntities);
+		} else {
+			result.links.push_back(peer->createOpenLink());
+			result.text = (isSelf
+				? tr::lng_action_gift_received_me
+				: tr::lng_action_gift_received)(
+					tr::now,
+					lt_user,
+					Ui::Text::Link(peer->name(), 1), // Link 1.
+					lt_cost,
+					cost,
+					Ui::Text::WithEntities);
+		}
 		return result;
 	};
 
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 2a7a2f4b1..23d483c1f 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp
@@ -12,13 +12,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/click_handler_types.h" // ClickHandlerContext
 #include "data/data_document.h"
 #include "data/data_channel.h"
+#include "data/data_user.h"
 #include "history/history.h"
 #include "history/history_item.h"
 #include "history/view/history_view_element.h"
 #include "lang/lang_keys.h"
 #include "main/main_session.h"
 #include "settings/settings_credits.h" // Settings::CreditsId
+#include "settings/settings_credits_graphics.h" // GiftedCreditsBox
 #include "settings/settings_premium.h" // Settings::ShowGiftPremium
+#include "ui/layers/generic_box.h"
 #include "ui/text/text_utilities.h"
 #include "window/window_session_controller.h"
 #include "styles/style_chat.h"
@@ -99,22 +102,29 @@ rpl::producer<QString> PremiumGift::button() {
 
 ClickHandlerPtr PremiumGift::createViewLink() {
 	const auto from = _gift->from();
-	const auto to = _parent->history()->peer;
+	const auto peer = _parent->history()->peer;
+	const auto date = _parent->data()->date();
 	const auto data = _gift->data();
 	return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
 		const auto my = context.other.value<ClickHandlerContext>();
 		if (const auto controller = my.sessionWindow.get()) {
 			const auto selfId = controller->session().userPeerId();
-			const auto self = (from->id == selfId);
+			const auto sent = (from->id == selfId);
 			if (data.type == Data::GiftType::Stars) {
-				controller->showSettings(Settings::CreditsId());
+				const auto to = sent ? peer : peer->session().user();
+				controller->show(Box(
+					Settings::GiftedCreditsBox,
+					controller,
+					from,
+					to,
+					data.count,
+					date));
 			} else if (data.slug.isEmpty()) {
-				const auto peer = self ? to : from;
 				const auto months = data.count;
-				Settings::ShowGiftPremium(controller, peer, months, self);
+				Settings::ShowGiftPremium(controller, peer, months, sent);
 			} else {
 				const auto fromId = from->id;
-				const auto toId = self ? to->id : selfId;
+				const auto toId = sent ? peer->id : selfId;
 				ResolveGiftCode(controller, data.slug, fromId, toId);
 			}
 		}
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
index 954c07c7b..a8c63b5f8 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp
@@ -466,84 +466,81 @@ void ReceiptCreditsBox(
 		content->add(object_ptr<Ui::CenterWrap<>>(
 			content,
 			object_ptr<Ui::UserpicButton>(content, peer, stUser)));
-	} else {
-		if (e.gift) {
-			using PlayerPtr = std::unique_ptr<Lottie::SinglePlayer>;
-			struct State final {
-				DocumentData *sticker = nullptr;
-				std::shared_ptr<Data::DocumentMedia> media;
-				std::unique_ptr<Lottie::SinglePlayer> lottie;
-				rpl::lifetime downloadLifetime;
-			};
-			Ui::AddSkip(
-				content,
-				st::creditsHistoryEntryGiftStickerSpace);
-			const auto icon = Ui::CreateChild<Ui::RpWidget>(content);
-			icon->resize(Size(st::creditsHistoryEntryGiftStickerSize));
-			const auto state = icon->lifetime().make_state<State>();
-			auto &packs = session->giftBoxStickersPacks();
-			const auto document = packs.lookup(1);
-			if (document && document->sticker()) {
-				state->sticker = document;
-				state->media = document->createMediaView();
-				state->media->thumbnailWanted(packs.origin());
-				state->media->automaticLoad(packs.origin(), nullptr);
-				session->downloaderTaskFinished(
-				) | rpl::start_with_next([=] {
-					if (state->media->loaded()) {
-						state->lottie = ChatHelpers::LottiePlayerFromDocument(
-							state->media.get(),
-							ChatHelpers::StickerLottieSize::MessageHistory,
-							icon->size(),
-							Lottie::Quality::High);
-						state->lottie->updates() | rpl::start_with_next([=] {
-							icon->update();
-						}, icon->lifetime());
-						state->downloadLifetime.destroy();
-					}
-				}, state->downloadLifetime);
-			}
-			icon->paintRequest(
-			) | rpl::start_with_next([=] {
-				auto p = Painter(icon);
-				const auto &lottie = state->lottie;
-				const auto frame = (lottie && lottie->ready())
-					? lottie->frameInfo({ .box = icon->size() })
-					: Lottie::Animation::FrameInfo();
-				if (!frame.image.isNull()) {
-					p.drawImage(0, 0, frame.image);
-					if (lottie->frameIndex() < lottie->framesCount() - 1) {
-						lottie->markFrameShown();
-					}
-				}
-			}, icon->lifetime());
-			content->sizeValue(
-			) | rpl::start_with_next([=](const QSize &size) {
-				icon->move(
-					(size.width() - icon->width()) / 2,
-					st::creditsHistoryEntryGiftStickerSkip);
-			}, icon->lifetime());
-		} else {
-			const auto widget = content->add(
-				object_ptr<Ui::CenterWrap<>>(
-					content,
-					object_ptr<Ui::RpWidget>(content)))->entity();
-			using Draw = Fn<void(Painter &, int, int, int, int)>;
-			const auto draw = widget->lifetime().make_state<Draw>(
-				Ui::GenerateCreditsPaintUserpicCallback(e));
-			widget->resize(Size(stUser.photoSize));
-			widget->paintRequest(
-			) | rpl::start_with_next([=] {
-				auto p = Painter(widget);
-				(*draw)(p, 0, 0, stUser.photoSize, stUser.photoSize);
-			}, widget->lifetime());
+	} else if (e.gift) {
+		struct State final {
+			DocumentData *sticker = nullptr;
+			std::shared_ptr<Data::DocumentMedia> media;
+			std::unique_ptr<Lottie::SinglePlayer> lottie;
+			rpl::lifetime downloadLifetime;
+		};
+		Ui::AddSkip(
+			content,
+			st::creditsHistoryEntryGiftStickerSpace);
+		const auto icon = Ui::CreateChild<Ui::RpWidget>(content);
+		icon->resize(Size(st::creditsHistoryEntryGiftStickerSize));
+		const auto state = icon->lifetime().make_state<State>();
+		auto &packs = session->giftBoxStickersPacks();
+		const auto document = packs.lookup(packs.monthsForStars(e.credits));
+		if (document && document->sticker()) {
+			state->sticker = document;
+			state->media = document->createMediaView();
+			state->media->thumbnailWanted(packs.origin());
+			state->media->automaticLoad(packs.origin(), nullptr);
+			rpl::single() | rpl::then(
+				session->downloaderTaskFinished()
+			) | rpl::filter([=] {
+				return state->media->loaded();
+			}) | rpl::start_with_next([=] {
+				state->lottie = ChatHelpers::LottiePlayerFromDocument(
+					state->media.get(),
+					ChatHelpers::StickerLottieSize::MessageHistory,
+					icon->size(),
+					Lottie::Quality::High);
+				state->lottie->updates() | rpl::start_with_next([=] {
+					icon->update();
+				}, icon->lifetime());
+				state->downloadLifetime.destroy();
+			}, state->downloadLifetime);
 		}
+		icon->paintRequest(
+		) | rpl::start_with_next([=] {
+			auto p = Painter(icon);
+			const auto &lottie = state->lottie;
+			const auto frame = (lottie && lottie->ready())
+				? lottie->frameInfo({ .box = icon->size() })
+				: Lottie::Animation::FrameInfo();
+			if (!frame.image.isNull()) {
+				p.drawImage(0, 0, frame.image);
+				if (lottie->frameIndex() < lottie->framesCount() - 1) {
+					lottie->markFrameShown();
+				}
+			}
+		}, icon->lifetime());
+		content->sizeValue(
+		) | rpl::start_with_next([=](const QSize &size) {
+			icon->move(
+				(size.width() - icon->width()) / 2,
+				st::creditsHistoryEntryGiftStickerSkip);
+		}, icon->lifetime());
+	} else {
+		const auto widget = content->add(
+			object_ptr<Ui::CenterWrap<>>(
+				content,
+				object_ptr<Ui::RpWidget>(content)))->entity();
+		using Draw = Fn<void(Painter &, int, int, int, int)>;
+		const auto draw = widget->lifetime().make_state<Draw>(
+			Ui::GenerateCreditsPaintUserpicCallback(e));
+		widget->resize(Size(stUser.photoSize));
+		widget->paintRequest(
+		) | rpl::start_with_next([=] {
+			auto p = Painter(widget);
+			(*draw)(p, 0, 0, stUser.photoSize, stUser.photoSize);
+		}, widget->lifetime());
 	}
 
 	Ui::AddSkip(content);
 	Ui::AddSkip(content);
 
-
 	box->addRow(object_ptr<Ui::CenterWrap<>>(
 		box,
 		object_ptr<Ui::FlatLabel>(
@@ -565,7 +562,7 @@ void ReceiptCreditsBox(
 		auto &lifetime = content->lifetime();
 		const auto text = lifetime.make_state<Ui::Text::String>(
 			st::semiboldTextStyle,
-			(e.in ? QChar('+') : kMinus)
+			(e.in ? u"+"_q : e.gift ? QString() : QString(kMinus))
 				+ Lang::FormatCountDecimal(std::abs(int64(e.credits))));
 		const auto roundedText = e.refunded
 			? tr::lng_channel_earn_history_return(tr::now)
@@ -605,6 +602,8 @@ void ReceiptCreditsBox(
 				? st::creditsStroke
 				: e.in
 				? st::boxTextFgGood
+				: e.gift
+				? st::windowBoldFg
 				: st::menuIconAttentionColor);
 			const auto x = (amount->width() - fullWidth) / 2;
 			text->draw(p, Ui::Text::PaintContext{
@@ -709,10 +708,9 @@ void ReceiptCreditsBox(
 			tr::lng_credits_box_out_about(
 				lt_link,
 				tr::lng_payments_terms_link(
-				) | rpl::map([](const QString &t) {
-					using namespace Ui::Text;
-					return Link(t, u"https://telegram.org/tos"_q);
-				}),
+				) | Ui::Text::ToLink(
+					tr::lng_credits_box_out_about_link(tr::now)
+				),
 				Ui::Text::WithEntities),
 			st::creditsBoxAboutDivider)));
 
@@ -757,6 +755,32 @@ void ReceiptCreditsBox(
 	}, button->lifetime());
 }
 
+void GiftedCreditsBox(
+		not_null<Ui::GenericBox*> box,
+		not_null<Window::SessionController*> controller,
+		not_null<PeerData*> from,
+		not_null<PeerData*> to,
+		int count,
+		TimeId date) {
+	const auto received = to->isSelf();
+	const auto anonymous = from->isServiceUser();
+	const auto peer = received ? from : to;
+	using PeerType = Data::CreditsHistoryEntry::PeerType;
+	Settings::ReceiptCreditsBox(box, controller, nullptr, {
+		.id = QString(),
+		.title = (received
+			? tr::lng_credits_box_history_entry_gift_name
+			: tr::lng_credits_box_history_entry_gift_sent)(tr::now),
+		.date = base::unixtime::parse(date),
+		.credits = uint64(count),
+		.bareMsgId = uint64(),
+		.barePeerId = (anonymous ? uint64() : peer->id.value),
+		.peerType = (anonymous ? PeerType::Fragment : PeerType::Peer),
+		.in = received,
+		.gift = true,
+	});
+}
+
 void ShowRefundInfoBox(
 		not_null<Window::SessionController*> controller,
 		FullMsgId refundItemId) {
diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h
index 701c60b7f..e07262d6d 100644
--- a/Telegram/SourceFiles/settings/settings_credits_graphics.h
+++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h
@@ -59,6 +59,13 @@ void ReceiptCreditsBox(
 	not_null<Window::SessionController*> controller,
 	PeerData *premiumBot,
 	const Data::CreditsHistoryEntry &e);
+void GiftedCreditsBox(
+	not_null<Ui::GenericBox*> box,
+	not_null<Window::SessionController*> controller,
+	not_null<PeerData*> from,
+	not_null<PeerData*> to,
+	int count,
+	TimeId date);
 void ShowRefundInfoBox(
 	not_null<Window::SessionController*> controller,
 	FullMsgId refundItemId);
diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
index 61b43b8fc..018188cb3 100644
--- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
+++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp
@@ -217,7 +217,7 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback(
 		case Data::CreditsHistoryEntry::PeerType::PlayMarket:
 			return { st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 };
 		case Data::CreditsHistoryEntry::PeerType::Fragment:
-			return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 };
+			return { st::windowSubTextFg, st::imageBg };
 		case Data::CreditsHistoryEntry::PeerType::PremiumBot:
 			return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 };
 		case Data::CreditsHistoryEntry::PeerType::Ads:
@@ -458,7 +458,7 @@ TextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) {
 	return (entry.gift
 		? tr::lng_credits_box_history_entry_gift_name
 		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment)
-		? tr::lng_bot_username_description1_link
+		? tr::lng_credits_box_history_entry_fragment
 		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot)
 		? tr::lng_credits_box_history_entry_premium_bot
 		: (entry.peerType == Data::CreditsHistoryEntry::PeerType::Ads)

From 031233ea987c924555e5642c7f00f3c8950f6905 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 25 Jul 2024 16:09:46 +0200
Subject: [PATCH 126/163] Remove some code duplication.

---
 .../dialogs/ui/dialogs_suggestions.cpp        | 796 +++++++++---------
 .../dialogs/ui/dialogs_suggestions.h          |  70 +-
 2 files changed, 431 insertions(+), 435 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
index a19200d8a..84b3f8b89 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
@@ -87,66 +87,6 @@ private:
 
 };
 
-class ControllerWithPreviews
-	: public PeerListController
-	, public base::has_weak_ptr {
-public:
-	explicit ControllerWithPreviews(
-		not_null<Window::SessionController*> window);
-
-	[[nodiscard]] not_null<Window::SessionController*> window() const {
-		return _window;
-	}
-
-	bool rowTrackPress(not_null<PeerListRow*> row) override;
-	void rowTrackPressCancel() override;
-	bool rowTrackPressSkipMouseSelection() override;
-
-	bool processTouchEvent(not_null<QTouchEvent*> e);
-	void setupTouchChatPreview(not_null<Ui::ElasticScroll*> scroll);
-
-private:
-	const not_null<Window::SessionController*> _window;
-
-	std::optional<QPoint> _chatPreviewTouchGlobal;
-	rpl::event_stream<> _touchCancelRequests;
-
-};
-
-class RecentsController final : public ControllerWithPreviews {
-public:
-	RecentsController(
-		not_null<Window::SessionController*> window,
-		RecentPeersList list);
-
-	[[nodiscard]] rpl::producer<int> count() const {
-		return _count.value();
-	}
-	[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
-		return _chosen.events();
-	}
-
-	void prepare() override;
-	void rowClicked(not_null<PeerListRow*> row) override;
-	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
-		QWidget *parent,
-		not_null<PeerListRow*> row) override;
-	Main::Session &session() const override;
-
-	QString savedMessagesChatStatus() const override;
-
-private:
-	void setupDivider();
-	void subscribeToEvents();
-	[[nodiscard]] Fn<void()> removeAllCallback();
-
-	RecentPeersList _recent;
-	rpl::variable<int> _count;
-	rpl::event_stream<not_null<PeerData*>> _chosen;
-	rpl::lifetime _lifetime;
-
-};
-
 class ChannelRow final : public PeerListRow {
 public:
 	using PeerListRow::PeerListRow;
@@ -161,73 +101,6 @@ private:
 
 };
 
-class MyChannelsController final : public ControllerWithPreviews {
-public:
-	explicit MyChannelsController(
-		not_null<Window::SessionController*> window);
-
-	[[nodiscard]] rpl::producer<int> count() const {
-		return _count.value();
-	}
-	[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
-		return _chosen.events();
-	}
-
-	void prepare() override;
-	void rowClicked(not_null<PeerListRow*> row) override;
-	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
-		QWidget *parent,
-		not_null<PeerListRow*> row) override;
-	Main::Session &session() const override;
-
-private:
-	void setupDivider();
-	void appendRow(not_null<ChannelData*> channel);
-	void fill(bool force = false);
-
-	std::vector<not_null<History*>> _channels;
-	rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
-	rpl::variable<int> _count = 0;
-	rpl::variable<bool> _expanded = false;
-	rpl::event_stream<not_null<PeerData*>> _chosen;
-	rpl::lifetime _lifetime;
-
-};
-
-class RecommendationsController final : public ControllerWithPreviews {
-public:
-	explicit RecommendationsController(
-		not_null<Window::SessionController*> window);
-
-	[[nodiscard]] rpl::producer<int> count() const {
-		return _count.value();
-	}
-	[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
-		return _chosen.events();
-	}
-
-	void prepare() override;
-	void rowClicked(not_null<PeerListRow*> row) override;
-	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
-		QWidget *parent,
-		not_null<PeerListRow*> row) override;
-	Main::Session &session() const override;
-
-	void load();
-
-private:
-	void fill();
-	void setupDivider();
-	void appendRow(not_null<ChannelData*> channel);
-
-	rpl::variable<int> _count;
-	History *_activeHistory = nullptr;
-	bool _requested = false;
-	rpl::event_stream<not_null<PeerData*>> _chosen;
-	rpl::lifetime _lifetime;
-
-};
-
 struct EntryMenuDescriptor {
 	not_null<Window::SessionController*> controller;
 	not_null<PeerData*> peer;
@@ -422,12 +295,131 @@ const style::PeerListItem &ChannelRow::computeSt(
 	return _active ? st::recentPeersItemActive : st::recentPeersItem;
 }
 
-ControllerWithPreviews::ControllerWithPreviews(
+} // namespace
+
+
+class Suggestions::ObjectListController
+	: public PeerListController
+	, public base::has_weak_ptr {
+public:
+	explicit ObjectListController(
+		not_null<Window::SessionController*> window);
+
+	[[nodiscard]] not_null<Window::SessionController*> window() const {
+		return _window;
+	}
+	[[nodiscard]] rpl::producer<int> count() const {
+		return _count.value();
+	}
+	[[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
+		return _chosen.events();
+	}
+
+	bool rowTrackPress(not_null<PeerListRow*> row) override;
+	void rowTrackPressCancel() override;
+	bool rowTrackPressSkipMouseSelection() override;
+
+	bool processTouchEvent(not_null<QTouchEvent*> e);
+	void setupTouchChatPreview(not_null<Ui::ElasticScroll*> scroll);
+
+protected:
+	[[nodiscard]] int countCurrent() const;
+	void setCount(int count);
+	void choose(not_null<PeerData*> peer);
+
+private:
+	const not_null<Window::SessionController*> _window;
+
+	std::optional<QPoint> _chatPreviewTouchGlobal;
+	rpl::event_stream<> _touchCancelRequests;
+	rpl::event_stream<not_null<PeerData*>> _chosen;
+	rpl::variable<int> _count;
+
+};
+
+class RecentsController final : public Suggestions::ObjectListController {
+public:
+	RecentsController(
+		not_null<Window::SessionController*> window,
+		RecentPeersList list);
+
+	void prepare() override;
+	void rowClicked(not_null<PeerListRow*> row) override;
+	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
+		QWidget *parent,
+		not_null<PeerListRow*> row) override;
+	Main::Session &session() const override;
+
+	QString savedMessagesChatStatus() const override;
+
+private:
+	void setupDivider();
+	void subscribeToEvents();
+	[[nodiscard]] Fn<void()> removeAllCallback();
+
+	RecentPeersList _recent;
+	rpl::lifetime _lifetime;
+
+};
+
+class MyChannelsController final
+	: public Suggestions::ObjectListController {
+public:
+	explicit MyChannelsController(
+		not_null<Window::SessionController*> window);
+
+	void prepare() override;
+	void rowClicked(not_null<PeerListRow*> row) override;
+	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
+		QWidget *parent,
+		not_null<PeerListRow*> row) override;
+	Main::Session &session() const override;
+
+private:
+	void setupDivider();
+	void appendRow(not_null<ChannelData*> channel);
+	void fill(bool force = false);
+
+	std::vector<not_null<History*>> _channels;
+	rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
+	rpl::variable<bool> _expanded = false;
+	rpl::lifetime _lifetime;
+
+};
+
+class RecommendationsController final
+	: public Suggestions::ObjectListController {
+public:
+	explicit RecommendationsController(
+		not_null<Window::SessionController*> window);
+
+	void prepare() override;
+	void rowClicked(not_null<PeerListRow*> row) override;
+	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
+		QWidget *parent,
+		not_null<PeerListRow*> row) override;
+	Main::Session &session() const override;
+
+	void load();
+
+private:
+	void fill();
+	void setupDivider();
+	void appendRow(not_null<ChannelData*> channel);
+
+	History *_activeHistory = nullptr;
+	bool _requested = false;
+	rpl::lifetime _lifetime;
+
+};
+
+Suggestions::ObjectListController::ObjectListController(
 	not_null<Window::SessionController*> window)
 : _window(window) {
 }
 
-bool ControllerWithPreviews::rowTrackPress(not_null<PeerListRow*> row) {
+bool Suggestions::ObjectListController::rowTrackPress(
+		not_null<PeerListRow*> row) {
 	const auto peer = row->peer();
 	const auto history = peer->owner().history(peer);
 	const auto callback = crl::guard(this, [=](bool shown) {
@@ -454,16 +446,17 @@ bool ControllerWithPreviews::rowTrackPress(not_null<PeerListRow*> row) {
 	return false;
 }
 
-void ControllerWithPreviews::rowTrackPressCancel() {
+void Suggestions::ObjectListController::rowTrackPressCancel() {
 	_chatPreviewTouchGlobal = {};
 	_window->cancelScheduledPreview();
 }
 
-bool ControllerWithPreviews::rowTrackPressSkipMouseSelection() {
+bool Suggestions::ObjectListController::rowTrackPressSkipMouseSelection() {
 	return _chatPreviewTouchGlobal.has_value();
 }
 
-bool ControllerWithPreviews::processTouchEvent(not_null<QTouchEvent*> e) {
+bool Suggestions::ObjectListController::processTouchEvent(
+		not_null<QTouchEvent*> e) {
 	const auto point = e->touchPoints().empty()
 		? std::optional<QPoint>()
 		: e->touchPoints().front().screenPos().toPoint();
@@ -500,7 +493,7 @@ bool ControllerWithPreviews::processTouchEvent(not_null<QTouchEvent*> e) {
 	return false;
 }
 
-void ControllerWithPreviews::setupTouchChatPreview(
+void Suggestions::ObjectListController::setupTouchChatPreview(
 		not_null<Ui::ElasticScroll*> scroll) {
 	_touchCancelRequests.events() | rpl::start_with_next([=] {
 		QTouchEvent ev(QEvent::TouchCancel);
@@ -509,10 +502,22 @@ void ControllerWithPreviews::setupTouchChatPreview(
 	}, lifetime());
 }
 
+int Suggestions::ObjectListController::countCurrent() const {
+	return _count.current();
+}
+
+void Suggestions::ObjectListController::setCount(int count) {
+	_count = count;
+}
+
+void Suggestions::ObjectListController::choose(not_null<PeerData*> peer) {
+	_chosen.fire_copy(peer);
+}
+
 RecentsController::RecentsController(
 	not_null<Window::SessionController*> window,
 	RecentPeersList list)
-: ControllerWithPreviews(window)
+: ObjectListController(window)
 , _recent(std::move(list)) {
 }
 
@@ -523,13 +528,13 @@ void RecentsController::prepare() {
 		delegate()->peerListAppendRow(std::make_unique<RecentRow>(peer));
 	}
 	delegate()->peerListRefreshRows();
-	_count = _recent.list.size();
+	setCount(_recent.list.size());
 
 	subscribeToEvents();
 }
 
 void RecentsController::rowClicked(not_null<PeerListRow*> row) {
-	_chosen.fire(row->peer());
+	choose(row->peer());
 }
 
 Fn<void()> RecentsController::removeAllCallback() {
@@ -537,7 +542,7 @@ Fn<void()> RecentsController::removeAllCallback() {
 	const auto session = &this->session();
 	return crl::guard(session, [=] {
 		if (weak) {
-			_count = 0;
+			setCount(0);
 			while (delegate()->peerListFullRowsCount() > 0) {
 				delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
 			}
@@ -560,7 +565,7 @@ base::unique_qptr<Ui::PopupMenu> RecentsController::rowContextMenu(
 		if (weak) {
 			const auto rowId = peer->id.value;
 			if (const auto row = delegate()->peerListFindRow(rowId)) {
-				_count = std::max(0, _count.current() - 1);
+				setCount(std::max(0, countCurrent() - 1));
 				delegate()->peerListRemoveRow(row);
 				delegate()->peerListRefreshRows();
 			}
@@ -649,7 +654,7 @@ void RecentsController::subscribeToEvents() {
 
 	session().data().unreadBadgeChanges(
 	) | rpl::start_with_next([=] {
-		for (auto i = 0; i != _count.current(); ++i) {
+		for (auto i = 0; i != countCurrent(); ++i) {
 			const auto row = delegate()->peerListRowAt(i);
 			if (static_cast<RecentRow*>(row.get())->refreshBadge()) {
 				delegate()->peerListUpdateRow(row);
@@ -660,7 +665,7 @@ void RecentsController::subscribeToEvents() {
 
 MyChannelsController::MyChannelsController(
 	not_null<Window::SessionController*> window)
-: ControllerWithPreviews(window) {
+: ObjectListController(window) {
 }
 
 void MyChannelsController::prepare() {
@@ -683,7 +688,7 @@ void MyChannelsController::prepare() {
 		if (row) {
 			delegate()->peerListRemoveRow(row);
 		}
-		_count = int(_channels.size());
+		setCount(_channels.size());
 		fill(true);
 	}, _lifetime);
 
@@ -704,7 +709,7 @@ void MyChannelsController::prepare() {
 	}
 
 	ranges::sort(_channels, ranges::greater(), &History::chatListTimeId);
-	_count = int(_channels.size());
+	setCount(_channels.size());
 
 	_expanded.value() | rpl::start_with_next([=] {
 		fill();
@@ -728,17 +733,17 @@ void MyChannelsController::prepare() {
 				}
 			}
 		}
-		const auto was = _count.current();
+		const auto was = countCurrent();
 		const auto now = int(_channels.size());
 		if (was != now) {
-			_count = now;
+			setCount(now);
 			fill();
 		}
 	}, _lifetime);
 }
 
 void MyChannelsController::fill(bool force) {
-	const auto count = _count.current();
+	const auto count = countCurrent();
 	const auto limit = _expanded.current()
 		? count
 		: std::min(count, kCollapsedChannelsCount);
@@ -772,7 +777,7 @@ void MyChannelsController::appendRow(not_null<ChannelData*> channel) {
 }
 
 void MyChannelsController::rowClicked(not_null<PeerListRow*> row) {
-	_chosen.fire(row->peer());
+	choose(row->peer());
 }
 
 base::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(
@@ -806,7 +811,7 @@ void MyChannelsController::setupDivider() {
 		raw,
 		tr::lng_channels_your_title(),
 		st::searchedBarLabel);
-	_count.value(
+	count(
 	) | rpl::map(
 		rpl::mappers::_1 > kCollapsedChannelsCount
 	) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) {
@@ -865,7 +870,7 @@ void MyChannelsController::setupDivider() {
 
 RecommendationsController::RecommendationsController(
 	not_null<Window::SessionController*> window)
-: ControllerWithPreviews(window) {
+: ObjectListController(window) {
 }
 
 void RecommendationsController::prepare() {
@@ -874,7 +879,7 @@ void RecommendationsController::prepare() {
 }
 
 void RecommendationsController::load() {
-	if (_requested || _count.current()) {
+	if (_requested || countCurrent()) {
 		return;
 	}
 	_requested = true;
@@ -898,7 +903,7 @@ void RecommendationsController::fill() {
 		}
 	}
 	delegate()->peerListRefreshRows();
-	_count = delegate()->peerListFullRowsCount();
+	setCount(delegate()->peerListFullRowsCount());
 
 	window()->activeChatValue() | rpl::start_with_next([=](const Key &key) {
 		const auto history = key.history();
@@ -936,7 +941,7 @@ void RecommendationsController::appendRow(not_null<ChannelData*> channel) {
 }
 
 void RecommendationsController::rowClicked(not_null<PeerListRow*> row) {
-	_chosen.fire(row->peer());
+	choose(row->peer());
 }
 
 base::unique_qptr<Ui::PopupMenu> RecommendationsController::rowContextMenu(
@@ -972,8 +977,6 @@ void RecommendationsController::setupDivider() {
 	delegate()->peerListSetAboveWidget(std::move(result));
 }
 
-} // namespace
-
 Suggestions::Suggestions(
 	not_null<QWidget*> parent,
 	not_null<Window::SessionController*> controller,
@@ -990,13 +993,13 @@ Suggestions::Suggestions(
 		this,
 		object_ptr<TopPeersStrip>(this, std::move(topPeers)))))
 , _topPeers(_topPeersWrap->entity())
-, _recentPeers(_chatsContent->add(setupRecentPeers(std::move(recentPeers))))
+, _recent(setupRecentPeers(std::move(recentPeers)))
 , _emptyRecent(_chatsContent->add(setupEmptyRecent()))
 , _channelsScroll(std::make_unique<Ui::ElasticScroll>(this))
 , _channelsContent(
 	_channelsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
-, _myChannels(_channelsContent->add(setupMyChannels()))
-, _recommendations(_channelsContent->add(setupRecommendations()))
+, _myChannels(setupMyChannels())
+, _recommendations(setupRecommendations())
 , _emptyChannels(_channelsContent->add(setupEmptyChannels())) {
 
 	setupTabs();
@@ -1032,10 +1035,10 @@ void Suggestions::setupTabs() {
 }
 
 void Suggestions::setupChats() {
-	_recentCount.value() | rpl::start_with_next([=](int count) {
-		_recentPeers->toggle(count > 0, anim::type::instant);
+	_recent->count.value() | rpl::start_with_next([=](int count) {
+		_recent->wrap->toggle(count > 0, anim::type::instant);
 		_emptyRecent->toggle(count == 0, anim::type::instant);
-	}, _recentPeers->lifetime());
+	}, _recent->wrap->lifetime());
 
 	_topPeers->emptyValue() | rpl::start_with_next([=](bool empty) {
 		_topPeersWrap->toggle(!empty, anim::type::instant);
@@ -1097,7 +1100,7 @@ void Suggestions::setupChats() {
 	}, _topPeers->lifetime());
 
 	_chatsScroll->setVisible(_tab.current() == Tab::Chats);
-	_chatsScroll->setCustomTouchProcess(_recentProcessTouch);
+	_chatsScroll->setCustomTouchProcess(_recent->processTouch);
 }
 
 void Suggestions::handlePressForChatPreview(
@@ -1115,25 +1118,25 @@ void Suggestions::handlePressForChatPreview(
 }
 
 void Suggestions::setupChannels() {
-	_myChannelsCount.value() | rpl::start_with_next([=](int count) {
-		_myChannels->toggle(count > 0, anim::type::instant);
-	}, _myChannels->lifetime());
+	_myChannels->count.value() | rpl::start_with_next([=](int count) {
+		_myChannels->wrap->toggle(count > 0, anim::type::instant);
+	}, _myChannels->wrap->lifetime());
 
-	_recommendationsCount.value() | rpl::start_with_next([=](int count) {
-		_recommendations->toggle(count > 0, anim::type::instant);
-	}, _recommendations->lifetime());
+	_recommendations->count.value() | rpl::start_with_next([=](int count) {
+		_recommendations->wrap->toggle(count > 0, anim::type::instant);
+	}, _recommendations->wrap->lifetime());
 
 	_emptyChannels->toggleOn(
 		rpl::combine(
-			_myChannelsCount.value(),
-			_recommendationsCount.value(),
+			_myChannels->count.value(),
+			_recommendations->count.value(),
 			rpl::mappers::_1 + rpl::mappers::_2 == 0),
 		anim::type::instant);
 
 	_channelsScroll->setVisible(_tab.current() == Tab::Channels);
 	_channelsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
-		const auto myChannels = _myChannelsProcessTouch(e);
-		const auto recommendations = _recommendationsProcessTouch(e);
+		const auto myChannels = _myChannels->processTouch(e);
+		const auto recommendations = _recommendations->processTouch(e);
 		return myChannels || recommendations;
 	});
 }
@@ -1148,26 +1151,27 @@ void Suggestions::selectJump(Qt::Key direction, int pageSize) {
 
 void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) {
 	const auto recentHasSelection = [=] {
-		return _recentSelectJump({}, 0) == JumpResult::Applied;
+		return _recent->selectJump({}, 0) == JumpResult::Applied;
 	};
 	if (pageSize) {
 		if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
 			_topPeers->deselectByKeyboard();
 			if (!recentHasSelection()) {
 				if (direction == Qt::Key_Down) {
-					_recentSelectJump(direction, 0);
+					_recent->selectJump(direction, 0);
 				} else {
 					return;
 				}
 			}
-			if (_recentSelectJump(direction, pageSize) == JumpResult::AppliedAndOut) {
+			if (_recent->selectJump(direction, pageSize)
+				== JumpResult::AppliedAndOut) {
 				if (direction == Qt::Key_Up) {
 					_chatsScroll->scrollTo(0);
 				}
 			}
 		}
 	} else if (direction == Qt::Key_Up) {
-		if (_recentSelectJump(direction, pageSize)
+		if (_recent->selectJump(direction, pageSize)
 			== JumpResult::AppliedAndOut) {
 			_topPeers->selectByKeyboard(direction);
 		} else if (_topPeers->selectedByKeyboard()) {
@@ -1175,12 +1179,12 @@ void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) {
 		}
 	} else if (direction == Qt::Key_Down) {
 		if (!_topPeersWrap->toggled() || recentHasSelection()) {
-			_recentSelectJump(direction, pageSize);
+			_recent->selectJump(direction, pageSize);
 		} else if (_topPeers->selectedByKeyboard()) {
 			if (!_topPeers->selectByKeyboard(direction)
-				&& _recentCount.current() > 0) {
+				&& _recent->count.current() > 0) {
 				_topPeers->deselectByKeyboard();
-				_recentSelectJump(direction, pageSize);
+				_recent->selectJump(direction, pageSize);
 			}
 		} else {
 			_topPeers->selectByKeyboard({});
@@ -1195,62 +1199,62 @@ void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) {
 
 void Suggestions::selectJumpChannels(Qt::Key direction, int pageSize) {
 	const auto myChannelsHasSelection = [=] {
-		return _myChannelsSelectJump({}, 0) == JumpResult::Applied;
+		return _myChannels->selectJump({}, 0) == JumpResult::Applied;
 	};
 	const auto recommendationsHasSelection = [=] {
-		return _recommendationsSelectJump({}, 0) == JumpResult::Applied;
+		return _recommendations->selectJump({}, 0) == JumpResult::Applied;
 	};
 	if (pageSize) {
 		if (direction == Qt::Key_Down) {
 			if (recommendationsHasSelection()) {
-				_recommendationsSelectJump(direction, pageSize);
+				_recommendations->selectJump(direction, pageSize);
 			} else if (myChannelsHasSelection()) {
-				if (_myChannelsSelectJump(direction, pageSize)
+				if (_myChannels->selectJump(direction, pageSize)
 					== JumpResult::AppliedAndOut) {
-					_recommendationsSelectJump(direction, 0);
+					_recommendations->selectJump(direction, 0);
 				}
-			} else if (_myChannelsCount.current()) {
-				_myChannelsSelectJump(direction, 0);
-				_myChannelsSelectJump(direction, pageSize);
-			} else if (_recommendationsCount.current()) {
-				_recommendationsSelectJump(direction, 0);
-				_recommendationsSelectJump(direction, pageSize);
+			} else if (_myChannels->count.current()) {
+				_myChannels->selectJump(direction, 0);
+				_myChannels->selectJump(direction, pageSize);
+			} else if (_recommendations->count.current()) {
+				_recommendations->selectJump(direction, 0);
+				_recommendations->selectJump(direction, pageSize);
 			}
 		} else if (direction == Qt::Key_Up) {
 			if (myChannelsHasSelection()) {
-				if (_myChannelsSelectJump(direction, pageSize)
+				if (_myChannels->selectJump(direction, pageSize)
 					== JumpResult::AppliedAndOut) {
 					_channelsScroll->scrollTo(0);
 				}
 			} else if (recommendationsHasSelection()) {
-				if (_recommendationsSelectJump(direction, pageSize)
+				if (_recommendations->selectJump(direction, pageSize)
 					== JumpResult::AppliedAndOut) {
-					_myChannelsSelectJump(direction, -1);
+					_myChannels->selectJump(direction, -1);
 				}
 			}
 		}
 	} else if (direction == Qt::Key_Up) {
 		if (myChannelsHasSelection()) {
-			_myChannelsSelectJump(direction, 0);
-		} else if (_recommendationsSelectJump(direction, 0)
+			_myChannels->selectJump(direction, 0);
+		} else if (_recommendations->selectJump(direction, 0)
 			== JumpResult::AppliedAndOut) {
-			_myChannelsSelectJump(direction, -1);
+			_myChannels->selectJump(direction, -1);
 		} else if (!recommendationsHasSelection()) {
-			if (_myChannelsSelectJump(direction, 0)
+			if (_myChannels->selectJump(direction, 0)
 				== JumpResult::AppliedAndOut) {
 				_channelsScroll->scrollTo(0);
 			}
 		}
 	} else if (direction == Qt::Key_Down) {
 		if (recommendationsHasSelection()) {
-			_recommendationsSelectJump(direction, 0);
-		} else if (_myChannelsSelectJump(direction, 0)
+			_recommendations->selectJump(direction, 0);
+		} else if (_myChannels->selectJump(direction, 0)
 			== JumpResult::AppliedAndOut) {
-			_recommendationsSelectJump(direction, 0);
+			_recommendations->selectJump(direction, 0);
 		} else if (!myChannelsHasSelection()) {
-			if (_recommendationsSelectJump(direction, 0)
+			if (_recommendations->selectJump(direction, 0)
 				== JumpResult::AppliedAndOut) {
-				_myChannelsSelectJump(direction, 0);
+				_myChannels->selectJump(direction, 0);
 			}
 		}
 	}
@@ -1258,7 +1262,7 @@ void Suggestions::selectJumpChannels(Qt::Key direction, int pageSize) {
 
 void Suggestions::chooseRow() {
 	if (!_topPeers->chooseRow()) {
-		_recentPeersChoose();
+		_recent->choose();
 	}
 }
 
@@ -1272,14 +1276,14 @@ Data::Thread *Suggestions::updateFromChatsDrag(QPoint globalPosition) {
 	if (const auto top = _topPeers->updateFromParentDrag(globalPosition)) {
 		return _controller->session().data().history(PeerId(top));
 	}
-	return fromListId(_recentUpdateFromParentDrag(globalPosition));
+	return fromListId(_recent->updateFromParentDrag(globalPosition));
 }
 
 Data::Thread *Suggestions::updateFromChannelsDrag(QPoint globalPosition) {
-	if (const auto id = _myChannelsUpdateFromParentDrag(globalPosition)) {
+	if (const auto id = _myChannels->updateFromParentDrag(globalPosition)) {
 		return fromListId(id);
 	}
-	return fromListId(_recommendationsUpdateFromParentDrag(globalPosition));
+	return fromListId(_recommendations->updateFromParentDrag(globalPosition));
 }
 
 Data::Thread *Suggestions::fromListId(uint64 peerListRowId) {
@@ -1290,9 +1294,9 @@ Data::Thread *Suggestions::fromListId(uint64 peerListRowId) {
 
 void Suggestions::dragLeft() {
 	_topPeers->dragLeft();
-	_recentDragLeft();
-	_myChannelsDragLeft();
-	_recommendationsDragLeft();
+	_recent->dragLeft();
+	_myChannels->dragLeft();
+	_recommendations->dragLeft();
 }
 
 void Suggestions::show(anim::type animated, Fn<void()> finish) {
@@ -1432,36 +1436,25 @@ void Suggestions::resizeEvent(QResizeEvent *e) {
 	_channelsContent->resizeToWidth(w);
 }
 
-object_ptr<Ui::SlideWrap<>> Suggestions::setupRecentPeers(
-		RecentPeersList recentPeers) {
-	auto &lifetime = _chatsContent->lifetime();
-	const auto delegate = lifetime.make_state<
-		PeerListContentDelegateSimple
-	>();
-	const auto controller = lifetime.make_state<RecentsController>(
+auto Suggestions::setupRecentPeers(RecentPeersList recentPeers)
+-> std::unique_ptr<ObjectList> {
+	const auto controller = lifetime().make_state<RecentsController>(
 		_controller,
 		std::move(recentPeers));
-	controller->setStyleOverrides(&st::recentPeersList);
 
-	_recentCount = controller->count();
-	_recentProcessTouch = [=](not_null<QTouchEvent*> e) {
-		return controller->processTouchEvent(e);
+	const auto addToScroll = [=] {
+		return _topPeersWrap->toggled() ? _topPeers->height() : 0;
 	};
+	auto result = setupObjectList(
+		_chatsScroll.get(),
+		_chatsContent,
+		controller,
+		addToScroll);
+	const auto raw = result.get();
+	const auto list = raw->wrap->entity();
 
-	controller->chosen(
-	) | rpl::start_with_next([=](not_null<PeerData*> peer) {
-		_controller->session().recentPeers().bump(peer);
-		_recentPeerChosen.fire_copy(peer);
-	}, lifetime);
-
-	auto content = object_ptr<PeerListContent>(_chatsContent, controller);
-
-	const auto raw = content.data();
-	_recentPeersChoose = [=] {
-		return raw->submitted();
-	};
-	_recentSelectJump = [raw](Qt::Key direction, int pageSize) {
-		const auto had = raw->hasSelection();
+	raw->selectJump = [list](Qt::Key direction, int pageSize) {
+		const auto had = list->hasSelection();
 		if (direction == Qt::Key()) {
 			return had ? JumpResult::Applied : JumpResult::NotApplied;
 		} else if (direction == Qt::Key_Up && !had) {
@@ -1469,11 +1462,11 @@ object_ptr<Ui::SlideWrap<>> Suggestions::setupRecentPeers(
 		} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
 			const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
 			if (pageSize > 0) {
-				raw->selectSkipPage(pageSize, delta);
+				list->selectSkipPage(pageSize, delta);
 			} else {
-				raw->selectSkip(delta);
+				list->selectSkip(delta);
 			}
-			return raw->hasSelection()
+			return list->hasSelection()
 				? JumpResult::Applied
 				: had
 				? JumpResult::AppliedAndOut
@@ -1481,23 +1474,13 @@ object_ptr<Ui::SlideWrap<>> Suggestions::setupRecentPeers(
 		}
 		return JumpResult::NotApplied;
 	};
-	_recentUpdateFromParentDrag = [=](QPoint globalPosition) {
-		return raw->updateFromParentDrag(globalPosition);
-	};
-	_recentDragLeft = [=] {
-		raw->dragLeft();
-	};
-	raw->scrollToRequests(
-	) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
-		const auto add = _topPeersWrap->toggled() ? _topPeers->height() : 0;
-		_chatsScroll->scrollToY(request.ymin + add, request.ymax + add);
-	}, lifetime);
 
-	delegate->setContent(raw);
-	controller->setDelegate(delegate);
-	controller->setupTouchChatPreview(_chatsScroll.get());
+	raw->chosen.events(
+	) | rpl::start_with_next([=](not_null<PeerData*> peer) {
+		_controller->session().recentPeers().bump(peer);
+	}, list->lifetime());
 
-	return object_ptr<Ui::SlideWrap<>>(this, std::move(content));
+	return result;
 }
 
 object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyRecent() {
@@ -1505,6 +1488,168 @@ object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyRecent() {
 	return setupEmpty(_chatsContent, icon, tr::lng_recent_none());
 }
 
+auto Suggestions::setupMyChannels() -> std::unique_ptr<ObjectList> {
+	const auto controller = lifetime().make_state<MyChannelsController>(
+		_controller);
+
+	auto result = setupObjectList(
+		_channelsScroll.get(),
+		_channelsContent,
+		controller);
+	const auto raw = result.get();
+	const auto list = raw->wrap->entity();
+
+	raw->selectJump = [=](Qt::Key direction, int pageSize) {
+		const auto had = list->hasSelection();
+		if (direction == Qt::Key()) {
+			return had ? JumpResult::Applied : JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Up && !had) {
+			if (pageSize < 0) {
+				list->selectLast();
+				return list->hasSelection()
+					? JumpResult::Applied
+					: JumpResult::NotApplied;
+			}
+			return JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
+			const auto was = list->selectedIndex();
+			const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
+			if (pageSize > 0) {
+				list->selectSkipPage(pageSize, delta);
+			} else {
+				list->selectSkip(delta);
+			}
+			if (had
+				&& delta > 0
+				&& raw->count.current()
+				&& list->selectedIndex() == was) {
+				list->clearSelection();
+				return JumpResult::AppliedAndOut;
+			}
+			return list->hasSelection()
+				? JumpResult::Applied
+				: had
+				? JumpResult::AppliedAndOut
+				: JumpResult::NotApplied;
+		}
+		return JumpResult::NotApplied;
+	};
+
+	raw->chosen.events(
+	) | rpl::start_with_next([=] {
+		_persist = false;
+	}, list->lifetime());
+
+	return result;
+}
+
+auto Suggestions::setupRecommendations() -> std::unique_ptr<ObjectList> {
+	const auto controller = lifetime().make_state<RecommendationsController>(
+		_controller);
+
+	const auto addToScroll = [=] {
+		const auto wrap = _myChannels->wrap;
+		return wrap->toggled() ? wrap->height() : 0;
+	};
+	auto result = setupObjectList(
+		_channelsScroll.get(),
+		_channelsContent,
+		controller,
+		addToScroll);
+	const auto raw = result.get();
+	const auto list = raw->wrap->entity();
+
+	raw->selectJump = [list](Qt::Key direction, int pageSize) {
+		const auto had = list->hasSelection();
+		if (direction == Qt::Key()) {
+			return had ? JumpResult::Applied : JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Up && !had) {
+			return JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
+			const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
+			if (pageSize > 0) {
+				list->selectSkipPage(pageSize, delta);
+			} else {
+				list->selectSkip(delta);
+			}
+			return list->hasSelection()
+				? JumpResult::Applied
+				: had
+				? JumpResult::AppliedAndOut
+				: JumpResult::NotApplied;
+		}
+		return JumpResult::NotApplied;
+	};
+
+	raw->chosen.events(
+	) | rpl::start_with_next([=] {
+		_persist = true;
+	}, list->lifetime());
+
+	_tab.value() | rpl::filter(
+		rpl::mappers::_1 == Tab::Channels
+	) | rpl::start_with_next([=] {
+		controller->load();
+	}, list->lifetime());
+
+	return result;
+}
+
+auto Suggestions::setupObjectList(
+	not_null<Ui::ElasticScroll*> scroll,
+	not_null<Ui::VerticalLayout*> parent,
+	not_null<ObjectListController*> controller,
+	Fn<int()> addToScroll)
+-> std::unique_ptr<ObjectList> {
+	auto &lifetime = parent->lifetime();
+	const auto delegate = lifetime.make_state<
+		PeerListContentDelegateSimple
+	>();
+	controller->setStyleOverrides(&st::recentPeersList);
+
+	auto content = object_ptr<PeerListContent>(parent, controller);
+	const auto list = content.data();
+
+	auto result = std::make_unique<ObjectList>(ObjectList{
+		.wrap = parent->add(object_ptr<Ui::SlideWrap<PeerListContent>>(
+			parent,
+			std::move(content))),
+	});
+	const auto raw = result.get();
+
+	raw->count = controller->count();
+	raw->processTouch = [=](not_null<QTouchEvent*> e) {
+		return controller->processTouchEvent(e);
+	};
+
+	controller->chosen(
+	) | rpl::start_with_next([=](not_null<PeerData*> peer) {
+		raw->chosen.fire_copy(peer);
+	}, lifetime);
+
+	raw->choose = [=] {
+		return list->submitted();
+	};
+	raw->updateFromParentDrag = [=](QPoint globalPosition) {
+		return list->updateFromParentDrag(globalPosition);
+	};
+	raw->dragLeft = [=] {
+		list->dragLeft();
+	};
+
+	list->scrollToRequests(
+	) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
+		const auto add = addToScroll ? addToScroll() : 0;
+		scroll->scrollToY(request.ymin + add, request.ymax + add);
+	}, list->lifetime());
+
+	delegate->setContent(list);
+	controller->setDelegate(delegate);
+	controller->setupTouchChatPreview(scroll);
+
+	return result;
+}
+
 object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyChannels() {
 	const auto icon = SearchEmptyIcon::NoResults;
 	return setupEmpty(_channelsContent, icon, tr::lng_channels_none_about());
@@ -1541,157 +1686,6 @@ object_ptr<Ui::SlideWrap<>> Suggestions::setupEmpty(
 	return result;
 }
 
-object_ptr<Ui::SlideWrap<>> Suggestions::setupMyChannels() {
-	auto &lifetime = _channelsContent->lifetime();
-	const auto delegate = lifetime.make_state<
-		PeerListContentDelegateSimple
-	>();
-	const auto controller = lifetime.make_state<MyChannelsController>(
-		_controller);
-	controller->setStyleOverrides(&st::recentPeersList);
-
-	_myChannelsCount = controller->count();
-	_myChannelsProcessTouch = [=](not_null<QTouchEvent*> e) {
-		return controller->processTouchEvent(e);
-	};
-
-	controller->chosen(
-	) | rpl::start_with_next([=](not_null<PeerData*> peer) {
-		_persist = false;
-		_myChannelChosen.fire_copy(peer);
-	}, lifetime);
-
-	auto content = object_ptr<PeerListContent>(_channelsContent, controller);
-
-	const auto raw = content.data();
-	_myChannelsChoose = [=] {
-		return raw->submitted();
-	};
-	_myChannelsSelectJump = [=](Qt::Key direction, int pageSize) {
-		const auto had = raw->hasSelection();
-		if (direction == Qt::Key()) {
-			return had ? JumpResult::Applied : JumpResult::NotApplied;
-		} else if (direction == Qt::Key_Up && !had) {
-			if (pageSize < 0) {
-				raw->selectLast();
-				return raw->hasSelection()
-					? JumpResult::Applied
-					: JumpResult::NotApplied;
-			}
-			return JumpResult::NotApplied;
-		} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
-			const auto was = raw->selectedIndex();
-			const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
-			if (pageSize > 0) {
-				raw->selectSkipPage(pageSize, delta);
-			} else {
-				raw->selectSkip(delta);
-			}
-			if (had
-				&& delta > 0
-				&& _recommendationsCount.current()
-				&& raw->selectedIndex() == was) {
-				raw->clearSelection();
-				return JumpResult::AppliedAndOut;
-			}
-			return raw->hasSelection()
-				? JumpResult::Applied
-				: had
-				? JumpResult::AppliedAndOut
-				: JumpResult::NotApplied;
-		}
-		return JumpResult::NotApplied;
-	};
-	_myChannelsUpdateFromParentDrag = [=](QPoint globalPosition) {
-		return raw->updateFromParentDrag(globalPosition);
-	};
-	_myChannelsDragLeft = [=] {
-		raw->dragLeft();
-	};
-	raw->scrollToRequests(
-	) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
-		_channelsScroll->scrollToY(request.ymin, request.ymax);
-	}, lifetime);
-
-	delegate->setContent(raw);
-	controller->setDelegate(delegate);
-	controller->setupTouchChatPreview(_channelsScroll.get());
-
-	return object_ptr<Ui::SlideWrap<>>(this, std::move(content));
-}
-
-object_ptr<Ui::SlideWrap<>> Suggestions::setupRecommendations() {
-	auto &lifetime = _channelsContent->lifetime();
-	const auto delegate = lifetime.make_state<
-		PeerListContentDelegateSimple
-	>();
-	const auto controller = lifetime.make_state<RecommendationsController>(
-		_controller);
-	controller->setStyleOverrides(&st::recentPeersList);
-
-	_recommendationsCount = controller->count();
-	_recommendationsProcessTouch = [=](not_null<QTouchEvent*> e) {
-		return controller->processTouchEvent(e);
-	};
-
-	_tab.value() | rpl::filter(
-		rpl::mappers::_1 == Tab::Channels
-	) | rpl::start_with_next([=] {
-		controller->load();
-	}, lifetime);
-
-	controller->chosen(
-	) | rpl::start_with_next([=](not_null<PeerData*> peer) {
-		_persist = true;
-		_recommendationChosen.fire_copy(peer);
-	}, lifetime);
-
-	auto content = object_ptr<PeerListContent>(_channelsContent, controller);
-
-	const auto raw = content.data();
-	_recommendationsChoose = [=] {
-		return raw->submitted();
-	};
-	_recommendationsSelectJump = [raw](Qt::Key direction, int pageSize) {
-		const auto had = raw->hasSelection();
-		if (direction == Qt::Key()) {
-			return had ? JumpResult::Applied : JumpResult::NotApplied;
-		} else if (direction == Qt::Key_Up && !had) {
-			return JumpResult::NotApplied;
-		} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
-			const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
-			if (pageSize > 0) {
-				raw->selectSkipPage(pageSize, delta);
-			} else {
-				raw->selectSkip(delta);
-			}
-			return raw->hasSelection()
-				? JumpResult::Applied
-				: had
-				? JumpResult::AppliedAndOut
-				: JumpResult::NotApplied;
-		}
-		return JumpResult::NotApplied;
-	};
-	_recommendationsUpdateFromParentDrag = [=](QPoint globalPosition) {
-		return raw->updateFromParentDrag(globalPosition);
-	};
-	_recommendationsDragLeft = [=] {
-		raw->dragLeft();
-	};
-	raw->scrollToRequests(
-	) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
-		const auto add = _myChannels->toggled() ? _myChannels->height() : 0;
-		_channelsScroll->scrollToY(request.ymin + add, request.ymax + add);
-	}, lifetime);
-
-	delegate->setContent(raw);
-	controller->setDelegate(delegate);
-	controller->setupTouchChatPreview(_channelsScroll.get());
-
-	return object_ptr<Ui::SlideWrap<>>(this, std::move(content));
-}
-
 bool Suggestions::persist() const {
 	return _persist;
 }
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
index 8b33718e6..438c0b370 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
@@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/animations.h"
 #include "ui/rp_widget.h"
 
+class PeerListContent;
+
 namespace Data {
 class Thread;
 } // namespace Data
@@ -67,17 +69,19 @@ public:
 	}
 	[[nodiscard]] auto recentPeerChosen() const
 	-> rpl::producer<not_null<PeerData*>> {
-		return _recentPeerChosen.events();
+		return _recent->chosen.events();
 	}
 	[[nodiscard]] auto myChannelChosen() const
 	-> rpl::producer<not_null<PeerData*>> {
-		return _myChannelChosen.events();
+		return _myChannels->chosen.events();
 	}
 	[[nodiscard]] auto recommendationChosen() const
 	-> rpl::producer<not_null<PeerData*>> {
-		return _recommendationChosen.events();
+		return _recommendations->chosen.events();
 	}
 
+	class ObjectListController;
+
 private:
 	enum class Tab : uchar {
 		Chats,
@@ -89,6 +93,17 @@ private:
 		AppliedAndOut,
 	};
 
+	struct ObjectList {
+		not_null<Ui::SlideWrap<PeerListContent>*> wrap;
+		rpl::variable<int> count;
+		Fn<bool()> choose;
+		Fn<JumpResult(Qt::Key, int)> selectJump;
+		Fn<uint64(QPoint)> updateFromParentDrag;
+		Fn<void()> dragLeft;
+		Fn<bool(not_null<QTouchEvent*>)> processTouch;
+		rpl::event_stream<not_null<PeerData*>> chosen;
+	};
+
 	void paintEvent(QPaintEvent *e) override;
 	void resizeEvent(QResizeEvent *e) override;
 
@@ -104,14 +119,22 @@ private:
 		QPoint globalPosition);
 	[[nodiscard]] Data::Thread *fromListId(uint64 peerListRowId);
 
-	[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupRecentPeers(
+	[[nodiscard]] std::unique_ptr<ObjectList> setupRecentPeers(
 		RecentPeersList recentPeers);
-	[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupEmptyRecent();
-	[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupMyChannels();
-	[[nodiscard]] auto setupRecommendations()
+	[[nodiscard]] auto setupEmptyRecent()
 		-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
+
+	[[nodiscard]] std::unique_ptr<ObjectList> setupMyChannels();
+	[[nodiscard]] std::unique_ptr<ObjectList> setupRecommendations();
 	[[nodiscard]] auto setupEmptyChannels()
 		-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
+
+	[[nodiscard]] std::unique_ptr<ObjectList> setupObjectList(
+		not_null<Ui::ElasticScroll*> scroll,
+		not_null<Ui::VerticalLayout*> parent,
+		not_null<ObjectListController*> controller,
+		Fn<int()> addToScroll = nullptr);
+
 	[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupEmpty(
 		not_null<QWidget*> parent,
 		SearchEmptyIcon icon,
@@ -131,44 +154,23 @@ private:
 
 	const std::unique_ptr<Ui::ElasticScroll> _chatsScroll;
 	const not_null<Ui::VerticalLayout*> _chatsContent;
+
 	const not_null<Ui::SlideWrap<TopPeersStrip>*> _topPeersWrap;
 	const not_null<TopPeersStrip*> _topPeers;
+	rpl::event_stream<not_null<PeerData*>> _topPeerChosen;
+
+	const std::unique_ptr<ObjectList> _recent;
 
-	rpl::variable<int> _recentCount;
-	Fn<bool()> _recentPeersChoose;
-	Fn<JumpResult(Qt::Key, int)> _recentSelectJump;
-	Fn<uint64(QPoint)> _recentUpdateFromParentDrag;
-	Fn<void()> _recentDragLeft;
-	Fn<bool(not_null<QTouchEvent*>)> _recentProcessTouch;
-	const not_null<Ui::SlideWrap<Ui::RpWidget>*> _recentPeers;
 	const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyRecent;
 
 	const std::unique_ptr<Ui::ElasticScroll> _channelsScroll;
 	const not_null<Ui::VerticalLayout*> _channelsContent;
 
-	rpl::variable<int> _myChannelsCount;
-	Fn<bool()> _myChannelsChoose;
-	Fn<JumpResult(Qt::Key, int)> _myChannelsSelectJump;
-	Fn<uint64(QPoint)> _myChannelsUpdateFromParentDrag;
-	Fn<void()> _myChannelsDragLeft;
-	Fn<bool(not_null<QTouchEvent*>)> _myChannelsProcessTouch;
-	const not_null<Ui::SlideWrap<Ui::RpWidget>*> _myChannels;
-
-	rpl::variable<int> _recommendationsCount;
-	Fn<bool()> _recommendationsChoose;
-	Fn<JumpResult(Qt::Key, int)> _recommendationsSelectJump;
-	Fn<uint64(QPoint)> _recommendationsUpdateFromParentDrag;
-	Fn<void()> _recommendationsDragLeft;
-	Fn<bool(not_null<QTouchEvent*>)> _recommendationsProcessTouch;
-	const not_null<Ui::SlideWrap<Ui::RpWidget>*> _recommendations;
+	const std::unique_ptr<ObjectList> _myChannels;
+	const std::unique_ptr<ObjectList> _recommendations;
 
 	const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyChannels;
 
-	rpl::event_stream<not_null<PeerData*>> _topPeerChosen;
-	rpl::event_stream<not_null<PeerData*>> _recentPeerChosen;
-	rpl::event_stream<not_null<PeerData*>> _myChannelChosen;
-	rpl::event_stream<not_null<PeerData*>> _recommendationChosen;
-
 	Ui::Animations::Simple _shownAnimation;
 	Fn<void()> _showFinished;
 	bool _hidden = false;

From 6a8a85e39514242772a4c9b516bd0a21d23ff847 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 25 Jul 2024 18:08:22 +0200
Subject: [PATCH 127/163] Implement recent/popular apps list.

---
 Telegram/Resources/langs/lang.strings         |   8 +
 .../SourceFiles/data/components/top_peers.cpp |  65 +-
 .../SourceFiles/data/components/top_peers.h   |   9 +-
 Telegram/SourceFiles/data/data_session.cpp    |   2 +
 Telegram/SourceFiles/data/data_user.h         |   2 +
 .../SourceFiles/dialogs/dialogs_widget.cpp    |  33 +
 Telegram/SourceFiles/dialogs/dialogs_widget.h |   1 +
 .../dialogs/ui/dialogs_suggestions.cpp        | 676 ++++++++++++++----
 .../dialogs/ui/dialogs_suggestions.h          |  26 +-
 Telegram/SourceFiles/info/info.style          |   6 +
 .../info/profile/info_profile_actions.cpp     |  58 +-
 .../inline_bots/bot_attach_web_view.cpp       |  54 +-
 .../inline_bots/bot_attach_web_view.h         |  12 +-
 Telegram/SourceFiles/main/main_session.cpp    |   4 +-
 Telegram/SourceFiles/main/main_session.h      |   4 +
 Telegram/SourceFiles/ui/vertical_list.cpp     |  17 +-
 Telegram/SourceFiles/ui/vertical_list.h       |   4 +-
 17 files changed, 792 insertions(+), 189 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index fedb32957..64bbb68d8 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1448,6 +1448,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_info_topic_title" = "Topic Info";
 "lng_profile_enable_notifications" = "Notifications";
 "lng_profile_send_message" = "Send Message";
+"lng_profile_open_app" = "Open App";
+"lng_profile_open_app_about" = "By launching this mini app, you agree to the {terms}.";
+"lng_profile_open_app_terms" = "Terms of Service for Mini Apps";
 "lng_info_add_as_contact" = "Add to contacts";
 "lng_profile_shared_media" = "Shared media";
 "lng_profile_suggest_photo" = "Suggest Profile Photo";
@@ -3186,6 +3189,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_bot_add_to_side_menu_done" = "Bot added to the main menu.";
 "lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps.";
 "lng_bot_click_to_start" = "Click here to use this bot.";
+"lng_bot_status_users#one" = "{count} user";
+"lng_bot_status_users#other" = "{count} users";
 
 "lng_typing" = "typing";
 "lng_user_typing" = "{user} is typing";
@@ -5341,12 +5346,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_recent_none" = "Recent search results\nwill appear here.";
 "lng_recent_chats" = "Chats";
 "lng_recent_channels" = "Channels";
+"lng_recent_apps" = "Apps";
 "lng_channels_none_title" = "No channels yet...";
 "lng_channels_none_about" = "You are not currently subscribed to any channels.";
 "lng_channels_your_title" = "Channels you joined";
 "lng_channels_your_more" = "Show more";
 "lng_channels_your_less" = "Show less";
 "lng_channels_recommended" = "Recommended channels";
+"lng_bot_apps_your" = "Apps you use";
+"lng_bot_apps_popular" = "Popular apps";
 
 "lng_font_box_title" = "Choose font family";
 "lng_font_default" = "Default";
diff --git a/Telegram/SourceFiles/data/components/top_peers.cpp b/Telegram/SourceFiles/data/components/top_peers.cpp
index f476869b6..57974febf 100644
--- a/Telegram/SourceFiles/data/components/top_peers.cpp
+++ b/Telegram/SourceFiles/data/components/top_peers.cpp
@@ -41,12 +41,36 @@ constexpr auto kRequestTimeLimit = 10 * crl::time(1000);
 	) / 1'000'000.;
 }
 
+[[nodiscard]] MTPTopPeerCategory TypeToCategory(TopPeerType type) {
+	switch (type) {
+	case TopPeerType::Chat: return MTP_topPeerCategoryCorrespondents();
+	case TopPeerType::BotApp: return MTP_topPeerCategoryBotsApp();
+	}
+	Unexpected("Type in TypeToCategory.");
+}
+
+[[nodiscard]] auto TypeToGetFlags(TopPeerType type) {
+	using Flag = MTPcontacts_GetTopPeers::Flag;
+	switch (type) {
+	case TopPeerType::Chat: return Flag::f_correspondents;
+	case TopPeerType::BotApp: return Flag::f_bots_app;
+	}
+	Unexpected("Type in TypeToGetFlags.");
+}
+
 } // namespace
 
-TopPeers::TopPeers(not_null<Main::Session*> session)
-: _session(session) {
+TopPeers::TopPeers(not_null<Main::Session*> session, TopPeerType type)
+: _session(session)
+, _type(type) {
+	if (_type == TopPeerType::Chat) {
+		loadAfterChats();
+	}
+}
+
+void TopPeers::loadAfterChats() {
 	using namespace rpl::mappers;
-	crl::on_main(session, [=] {
+	crl::on_main(_session, [=] {
 		_session->data().chatsListLoadedEvents(
 		) | rpl::filter(_1 == nullptr) | rpl::start_with_next([=] {
 			crl::on_main(_session, [=] {
@@ -84,7 +108,7 @@ void TopPeers::remove(not_null<PeerData*> peer) {
 	}
 
 	_requestId = _session->api().request(MTPcontacts_ResetTopPeerRating(
-		MTP_topPeerCategoryCorrespondents(),
+		TypeToCategory(_type),
 		peer->input
 	)).send();
 }
@@ -160,11 +184,13 @@ void TopPeers::request() {
 	}
 
 	_requestId = _session->api().request(MTPcontacts_GetTopPeers(
-		MTP_flags(MTPcontacts_GetTopPeers::Flag::f_correspondents),
+		MTP_flags(TypeToGetFlags(_type)),
 		MTP_int(0),
 		MTP_int(kLimit),
 		MTP_long(countHash())
-	)).done([=](const MTPcontacts_TopPeers &result, const MTP::Response &response) {
+	)).done([=](
+			const MTPcontacts_TopPeers &result,
+			const MTP::Response &response) {
 		_lastReceivedDate = TimeId(response.outerMsgId >> 32);
 		_lastReceived = crl::now();
 		_requestId = 0;
@@ -176,19 +202,22 @@ void TopPeers::request() {
 			owner->processChats(data.vchats());
 			for (const auto &category : data.vcategories().v) {
 				const auto &data = category.data();
-				data.vcategory().match(
-					[&](const MTPDtopPeerCategoryCorrespondents &) {
-					_list = ranges::views::all(
-						data.vpeers().v
-					) | ranges::views::transform([&](const MTPTopPeer &top) {
-						return TopPeer{
-							owner->peer(peerFromMTP(top.data().vpeer())),
-							top.data().vrating().v,
-						};
-					}) | ranges::to_vector;
-				}, [](const auto &) {
+				const auto cons = (_type == TopPeerType::Chat)
+					? mtpc_topPeerCategoryCorrespondents
+					: mtpc_topPeerCategoryBotsApp;
+				if (data.vcategory().type() != cons) {
 					LOG(("API Error: Unexpected top peer category."));
-				});
+					continue;
+				}
+				_list = ranges::views::all(
+					data.vpeers().v
+				) | ranges::views::transform([&](
+						const MTPTopPeer &top) {
+					return TopPeer{
+						owner->peer(peerFromMTP(top.data().vpeer())),
+						top.data().vrating().v,
+					};
+				}) | ranges::to_vector;
 			}
 			updated();
 		}, [&](const MTPDcontacts_topPeersDisabled &) {
diff --git a/Telegram/SourceFiles/data/components/top_peers.h b/Telegram/SourceFiles/data/components/top_peers.h
index 5f1250b53..7f834ad84 100644
--- a/Telegram/SourceFiles/data/components/top_peers.h
+++ b/Telegram/SourceFiles/data/components/top_peers.h
@@ -13,9 +13,14 @@ class Session;
 
 namespace Data {
 
+enum class TopPeerType {
+	Chat,
+	BotApp,
+};
+
 class TopPeers final {
 public:
-	explicit TopPeers(not_null<Main::Session*> session);
+	TopPeers(not_null<Main::Session*> session, TopPeerType type);
 	~TopPeers();
 
 	[[nodiscard]] std::vector<not_null<PeerData*>> list() const;
@@ -36,11 +41,13 @@ private:
 		float64 rating = 0.;
 	};
 
+	void loadAfterChats();
 	void request();
 	[[nodiscard]] uint64 countHash() const;
 	void updated();
 
 	const not_null<Main::Session*> _session;
+	const TopPeerType _type = {};
 
 	std::vector<TopPeer> _list;
 	rpl::event_stream<> _updates;
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index cf772208b..f88fb6abb 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -721,6 +721,8 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
 				result->botInfo->supportsAttachMenu = data.is_bot_attach_menu();
 				result->botInfo->supportsBusiness = data.is_bot_business();
 				result->botInfo->canEditInformation = data.is_bot_can_edit();
+				result->botInfo->activeUsers = data.vbot_active_users().value_or_empty();
+				result->botInfo->hasMainApp = data.is_bot_has_main_app();
 			} else {
 				result->setBotInfoVersion(-1);
 			}
diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h
index 67da9e3f5..8f84555d3 100644
--- a/Telegram/SourceFiles/data/data_user.h
+++ b/Telegram/SourceFiles/data/data_user.h
@@ -40,12 +40,14 @@ struct BotInfo {
 
 	int version = 0;
 	int descriptionVersion = 0;
+	int activeUsers = 0;
 	bool inited : 1 = false;
 	bool readsAllHistory : 1 = false;
 	bool cantJoinGroups : 1 = false;
 	bool supportsAttachMenu : 1 = false;
 	bool canEditInformation : 1 = false;
 	bool supportsBusiness : 1 = false;
+	bool hasMainApp : 1 = false;
 };
 
 enum class UserDataFlag : uint32 {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
index 922b673a0..2e7568fc0 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp
@@ -78,6 +78,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_stories.h"
 #include "info/downloads/info_downloads_widget.h"
 #include "info/info_memento.h"
+#include "inline_bots/bot_attach_web_view.h"
 #include "styles/style_dialogs.h"
 #include "styles/style_chat.h"
 #include "styles/style_chat_helpers.h"
@@ -1265,6 +1266,27 @@ void Widget::updateSuggestions(anim::type animated) {
 			}
 		}, _suggestions->lifetime());
 
+		_suggestions->recentAppChosen(
+		) | rpl::start_with_next([=](not_null<PeerData*> peer) {
+			if (const auto user = peer->asUser()) {
+				if (const auto info = user->botInfo.get()) {
+					if (info->hasMainApp) {
+						openBotMainApp(user);
+						return;
+					}
+				}
+			}
+			chosenRow({
+				.key = peer->owner().history(peer),
+				.newWindow = base::IsCtrlPressed(),
+			});
+		}, _suggestions->lifetime());
+
+		_suggestions->popularAppChosen(
+		) | rpl::start_with_next([=](not_null<PeerData*> peer) {
+			controller()->showPeerInfo(peer);
+		}, _suggestions->lifetime());
+
 		updateControlsGeometry();
 
 		_suggestions->show(animated, [=] {
@@ -1276,6 +1298,17 @@ void Widget::updateSuggestions(anim::type animated) {
 	}
 }
 
+void Widget::openBotMainApp(not_null<UserData*> bot) {
+	session().attachWebView().open({
+		.bot = bot,
+		.context = {
+			.controller = controller(),
+			.maySkipConfirmation = true,
+		},
+		.source = InlineBots::WebViewSourceBotProfile(),
+	});
+}
+
 void Widget::changeOpenedSubsection(
 		FnMut<void()> change,
 		bool fromRight,
diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h
index 347683d0c..a1ee3950d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h
@@ -214,6 +214,7 @@ private:
 	void refreshTopBars();
 	void showSearchInTopBar(anim::type animated);
 	void checkUpdateStatus();
+	void openBotMainApp(not_null<UserData*> bot);
 	void changeOpenedSubsection(
 		FnMut<void()> change,
 		bool fromRight,
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
index 84b3f8b89..18ae94b37 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_user.h"
 #include "dialogs/ui/chat_search_empty.h"
 #include "history/history.h"
+#include "inline_bots/bot_attach_web_view.h"
 #include "lang/lang_keys.h"
 #include "main/main_session.h"
 #include "settings/settings_common.h"
@@ -56,6 +57,8 @@ namespace {
 constexpr auto kCollapsedChannelsCount = 5;
 constexpr auto kProbablyMaxChannels = 1000;
 constexpr auto kProbablyMaxRecommendations = 100;
+constexpr auto kCollapsedAppsCount = 5;
+constexpr auto kProbablyMaxApps = 100;
 
 class RecentRow final : public PeerListRow {
 public:
@@ -315,6 +318,11 @@ public:
 		return _chosen.events();
 	}
 
+	Main::Session &session() const override {
+		return _window->session();
+	}
+
+	void rowClicked(not_null<PeerListRow*> row) override;
 	bool rowTrackPress(not_null<PeerListRow*> row) override;
 	void rowTrackPressCancel() override;
 	bool rowTrackPressSkipMouseSelection() override;
@@ -325,7 +333,12 @@ public:
 protected:
 	[[nodiscard]] int countCurrent() const;
 	void setCount(int count);
-	void choose(not_null<PeerData*> peer);
+
+	[[nodiscard]] bool expandedCurrent() const;
+	[[nodiscard]] rpl::producer<bool> expanded() const;
+
+	void setupPlainDivider(rpl::producer<QString> title);
+	void setupExpandDivider(rpl::producer<QString> title);
 
 private:
 	const not_null<Window::SessionController*> _window;
@@ -334,6 +347,8 @@ private:
 	rpl::event_stream<> _touchCancelRequests;
 	rpl::event_stream<not_null<PeerData*>> _chosen;
 	rpl::variable<int> _count;
+	rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
+	rpl::variable<bool> _expanded = false;
 
 };
 
@@ -344,11 +359,9 @@ public:
 		RecentPeersList list);
 
 	void prepare() override;
-	void rowClicked(not_null<PeerListRow*> row) override;
 	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
 		QWidget *parent,
 		not_null<PeerListRow*> row) override;
-	Main::Session &session() const override;
 
 	QString savedMessagesChatStatus() const override;
 
@@ -369,20 +382,15 @@ public:
 		not_null<Window::SessionController*> window);
 
 	void prepare() override;
-	void rowClicked(not_null<PeerListRow*> row) override;
 	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
 		QWidget *parent,
 		not_null<PeerListRow*> row) override;
-	Main::Session &session() const override;
 
 private:
-	void setupDivider();
 	void appendRow(not_null<ChannelData*> channel);
 	void fill(bool force = false);
 
 	std::vector<not_null<History*>> _channels;
-	rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
-	rpl::variable<bool> _expanded = false;
 	rpl::lifetime _lifetime;
 
 };
@@ -394,17 +402,11 @@ public:
 		not_null<Window::SessionController*> window);
 
 	void prepare() override;
-	void rowClicked(not_null<PeerListRow*> row) override;
-	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
-		QWidget *parent,
-		not_null<PeerListRow*> row) override;
-	Main::Session &session() const override;
 
 	void load();
 
 private:
 	void fill();
-	void setupDivider();
 	void appendRow(not_null<ChannelData*> channel);
 
 	History *_activeHistory = nullptr;
@@ -413,6 +415,45 @@ private:
 
 };
 
+class RecentAppsController final
+	: public Suggestions::ObjectListController {
+public:
+	explicit RecentAppsController(
+		not_null<Window::SessionController*> window);
+
+	void prepare() override;
+
+	void load();
+
+private:
+	void appendRow(not_null<UserData*> bot);
+	void fill();
+
+	std::vector<not_null<UserData*>> _bots;
+	rpl::lifetime _lifetime;
+
+};
+
+class PopularAppsController final
+	: public Suggestions::ObjectListController {
+public:
+	explicit PopularAppsController(
+		not_null<Window::SessionController*> window);
+
+	void prepare() override;
+
+	void load();
+
+private:
+	void fill();
+	void appendRow(not_null<UserData*> bot);
+
+	History *_activeHistory = nullptr;
+	bool _requested = false;
+	rpl::lifetime _lifetime;
+
+};
+
 Suggestions::ObjectListController::ObjectListController(
 	not_null<Window::SessionController*> window)
 : _window(window) {
@@ -510,8 +551,108 @@ void Suggestions::ObjectListController::setCount(int count) {
 	_count = count;
 }
 
-void Suggestions::ObjectListController::choose(not_null<PeerData*> peer) {
-	_chosen.fire_copy(peer);
+bool Suggestions::ObjectListController::expandedCurrent() const {
+	return _expanded.current();
+}
+
+rpl::producer<bool> Suggestions::ObjectListController::expanded() const {
+	return _expanded.value();
+}
+
+void Suggestions::ObjectListController::rowClicked(
+		not_null<PeerListRow*> row) {
+	_chosen.fire(row->peer());
+}
+
+void Suggestions::ObjectListController::setupPlainDivider(
+		rpl::producer<QString> title) {
+	auto result = object_ptr<Ui::FixedHeightWidget>(
+		(QWidget*)nullptr,
+		st::searchedBarHeight);
+	const auto raw = result.data();
+	const auto label = Ui::CreateChild<Ui::FlatLabel>(
+		raw,
+		std::move(title),
+		st::searchedBarLabel);
+	raw->sizeValue(
+	) | rpl::start_with_next([=](QSize size) {
+		const auto x = st::searchedBarPosition.x();
+		const auto y = st::searchedBarPosition.y();
+		label->resizeToWidth(size.width() - x * 2);
+		label->moveToLeft(x, y, size.width());
+	}, raw->lifetime());
+	raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
+		QPainter(raw).fillRect(clip, st::searchedBarBg);
+	}, raw->lifetime());
+
+	delegate()->peerListSetAboveWidget(std::move(result));
+}
+
+void Suggestions::ObjectListController::setupExpandDivider(
+		rpl::producer<QString> title) {
+	auto result = object_ptr<Ui::FixedHeightWidget>(
+		(QWidget*)nullptr,
+		st::searchedBarHeight);
+	const auto raw = result.data();
+	const auto label = Ui::CreateChild<Ui::FlatLabel>(
+		raw,
+		std::move(title),
+		st::searchedBarLabel);
+	count(
+	) | rpl::map(
+		rpl::mappers::_1 > kCollapsedChannelsCount
+	) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) {
+		_expanded = false;
+		if (!more) {
+			const auto toggle = _toggleExpanded.current();
+			_toggleExpanded = nullptr;
+			delete toggle;
+			return;
+		} else if (_toggleExpanded.current()) {
+			return;
+		}
+		const auto toggle = Ui::CreateChild<Ui::LinkButton>(
+			raw,
+			tr::lng_channels_your_more(tr::now),
+			st::searchedBarLink);
+		toggle->show();
+		toggle->setClickedCallback([=] {
+			const auto expand = !_expanded.current();
+			toggle->setText(expand
+				? tr::lng_channels_your_less(tr::now)
+				: tr::lng_channels_your_more(tr::now));
+			_expanded = expand;
+		});
+		rpl::combine(
+			raw->sizeValue(),
+			toggle->widthValue()
+		) | rpl::start_with_next([=](QSize size, int width) {
+			const auto x = st::searchedBarPosition.x();
+			const auto y = st::searchedBarPosition.y();
+			toggle->moveToRight(0, 0, size.width());
+			label->resizeToWidth(size.width() - x - width);
+			label->moveToLeft(x, y, size.width());
+		}, toggle->lifetime());
+		_toggleExpanded = toggle;
+	}, raw->lifetime());
+
+	rpl::combine(
+		raw->sizeValue(),
+		_toggleExpanded.value()
+	) | rpl::filter(
+		rpl::mappers::_2 == nullptr
+	) | rpl::start_with_next([=](QSize size, const auto) {
+		const auto x = st::searchedBarPosition.x();
+		const auto y = st::searchedBarPosition.y();
+		label->resizeToWidth(size.width() - x * 2);
+		label->moveToLeft(x, y, size.width());
+	}, raw->lifetime());
+
+	raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
+		QPainter(raw).fillRect(clip, st::searchedBarBg);
+	}, raw->lifetime());
+
+	delegate()->peerListSetAboveWidget(std::move(result));
 }
 
 RecentsController::RecentsController(
@@ -533,10 +674,6 @@ void RecentsController::prepare() {
 	subscribeToEvents();
 }
 
-void RecentsController::rowClicked(not_null<PeerListRow*> row) {
-	choose(row->peer());
-}
-
 Fn<void()> RecentsController::removeAllCallback() {
 	const auto weak = base::make_weak(this);
 	const auto session = &this->session();
@@ -584,10 +721,6 @@ base::unique_qptr<Ui::PopupMenu> RecentsController::rowContextMenu(
 	return result;
 }
 
-Main::Session &RecentsController::session() const {
-	return window()->session();
-}
-
 QString RecentsController::savedMessagesChatStatus() const {
 	return tr::lng_saved_forward_here(tr::now);
 }
@@ -669,7 +802,7 @@ MyChannelsController::MyChannelsController(
 }
 
 void MyChannelsController::prepare() {
-	setupDivider();
+	setupExpandDivider(tr::lng_channels_your_title());
 
 	session().changes().peerUpdates(
 		Data::PeerUpdate::Flag::ChannelAmIn
@@ -711,7 +844,7 @@ void MyChannelsController::prepare() {
 	ranges::sort(_channels, ranges::greater(), &History::chatListTimeId);
 	setCount(_channels.size());
 
-	_expanded.value() | rpl::start_with_next([=] {
+	expanded() | rpl::start_with_next([=] {
 		fill();
 	}, _lifetime);
 
@@ -744,7 +877,7 @@ void MyChannelsController::prepare() {
 
 void MyChannelsController::fill(bool force) {
 	const auto count = countCurrent();
-	const auto limit = _expanded.current()
+	const auto limit = expandedCurrent()
 		? count
 		: std::min(count, kCollapsedChannelsCount);
 	const auto already = delegate()->peerListFullRowsCount();
@@ -776,10 +909,6 @@ void MyChannelsController::appendRow(not_null<ChannelData*> channel) {
 	delegate()->peerListAppendRow(std::move(row));
 }
 
-void MyChannelsController::rowClicked(not_null<PeerListRow*> row) {
-	choose(row->peer());
-}
-
 base::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(
 		QWidget *parent,
 		not_null<PeerListRow*> row) {
@@ -798,83 +927,13 @@ base::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(
 	return result;
 }
 
-Main::Session &MyChannelsController::session() const {
-	return window()->session();
-}
-
-void MyChannelsController::setupDivider() {
-	auto result = object_ptr<Ui::FixedHeightWidget>(
-		(QWidget*)nullptr,
-		st::searchedBarHeight);
-	const auto raw = result.data();
-	const auto label = Ui::CreateChild<Ui::FlatLabel>(
-		raw,
-		tr::lng_channels_your_title(),
-		st::searchedBarLabel);
-	count(
-	) | rpl::map(
-		rpl::mappers::_1 > kCollapsedChannelsCount
-	) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) {
-		_expanded = false;
-		if (!more) {
-			const auto toggle = _toggleExpanded.current();
-			_toggleExpanded = nullptr;
-			delete toggle;
-			return;
-		} else if (_toggleExpanded.current()) {
-			return;
-		}
-		const auto toggle = Ui::CreateChild<Ui::LinkButton>(
-			raw,
-			tr::lng_channels_your_more(tr::now),
-			st::searchedBarLink);
-		toggle->show();
-		toggle->setClickedCallback([=] {
-			const auto expand = !_expanded.current();
-			toggle->setText(expand
-				? tr::lng_channels_your_less(tr::now)
-				: tr::lng_channels_your_more(tr::now));
-			_expanded = expand;
-		});
-		rpl::combine(
-			raw->sizeValue(),
-			toggle->widthValue()
-		) | rpl::start_with_next([=](QSize size, int width) {
-			const auto x = st::searchedBarPosition.x();
-			const auto y = st::searchedBarPosition.y();
-			toggle->moveToRight(0, 0, size.width());
-			label->resizeToWidth(size.width() - x - width);
-			label->moveToLeft(x, y, size.width());
-		}, toggle->lifetime());
-		_toggleExpanded = toggle;
-	}, raw->lifetime());
-
-	rpl::combine(
-		raw->sizeValue(),
-		_toggleExpanded.value()
-	) | rpl::filter(
-		rpl::mappers::_2 == nullptr
-	) | rpl::start_with_next([=](QSize size, const auto) {
-		const auto x = st::searchedBarPosition.x();
-		const auto y = st::searchedBarPosition.y();
-		label->resizeToWidth(size.width() - x * 2);
-		label->moveToLeft(x, y, size.width());
-	}, raw->lifetime());
-
-	raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
-		QPainter(raw).fillRect(clip, st::searchedBarBg);
-	}, raw->lifetime());
-
-	delegate()->peerListSetAboveWidget(std::move(result));
-}
-
 RecommendationsController::RecommendationsController(
 	not_null<Window::SessionController*> window)
 : ObjectListController(window) {
 }
 
 void RecommendationsController::prepare() {
-	setupDivider();
+	setupPlainDivider(tr::lng_channels_recommended());
 	fill();
 }
 
@@ -940,41 +999,115 @@ void RecommendationsController::appendRow(not_null<ChannelData*> channel) {
 	delegate()->peerListAppendRow(std::move(row));
 }
 
-void RecommendationsController::rowClicked(not_null<PeerListRow*> row) {
-	choose(row->peer());
+RecentAppsController::RecentAppsController(
+	not_null<Window::SessionController*> window)
+: ObjectListController(window) {
 }
 
-base::unique_qptr<Ui::PopupMenu> RecommendationsController::rowContextMenu(
-		QWidget *parent,
-		not_null<PeerListRow*> row) {
-	return nullptr;
+void RecentAppsController::prepare() {
+	setupExpandDivider(tr::lng_bot_apps_your());
+
+	_bots.reserve(kProbablyMaxApps);
+	rpl::single() | rpl::then(
+		session().topBotApps().updates()
+	) | rpl::start_with_next([=] {
+		_bots.clear();
+		for (const auto &peer : session().topBotApps().list()) {
+			if (const auto bot = peer->asUser()) {
+				if (bot->isBot() && !bot->isInaccessible()) {
+					_bots.push_back(bot);
+				}
+			}
+		}
+		setCount(_bots.size());
+		while (delegate()->peerListFullRowsCount()) {
+			delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
+		}
+		fill();
+	}, _lifetime);
+
+	expanded() | rpl::skip(1) | rpl::start_with_next([=] {
+		fill();
+	}, _lifetime);
 }
 
-Main::Session &RecommendationsController::session() const {
-	return window()->session();
+void RecentAppsController::load() {
+	session().topBotApps().reload();
 }
 
-void RecommendationsController::setupDivider() {
-	auto result = object_ptr<Ui::FixedHeightWidget>(
-		(QWidget*)nullptr,
-		st::searchedBarHeight);
-	const auto raw = result.data();
-	const auto label = Ui::CreateChild<Ui::FlatLabel>(
-		raw,
-		tr::lng_channels_recommended(),
-		st::searchedBarLabel);
-	raw->sizeValue(
-	) | rpl::start_with_next([=](QSize size) {
-		const auto x = st::searchedBarPosition.x();
-		const auto y = st::searchedBarPosition.y();
-		label->resizeToWidth(size.width() - x * 2);
-		label->moveToLeft(x, y, size.width());
-	}, raw->lifetime());
-	raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
-		QPainter(raw).fillRect(clip, st::searchedBarBg);
-	}, raw->lifetime());
+void RecentAppsController::fill() {
+	const auto count = countCurrent();
+	const auto limit = expandedCurrent()
+		? count
+		: std::min(count, kCollapsedAppsCount);
+	const auto already = delegate()->peerListFullRowsCount();
+	const auto delta = limit - already;
+	if (!delta) {
+		return;
+	} else if (delta > 0) {
+		for (auto i = already; i != limit; ++i) {
+			appendRow(_bots[i]);
+		}
+	} else if (delta < 0) {
+		for (auto i = already; i != limit;) {
+			delegate()->peerListRemoveRow(delegate()->peerListRowAt(--i));
+		}
+	}
+	delegate()->peerListRefreshRows();
+}
 
-	delegate()->peerListSetAboveWidget(std::move(result));
+void RecentAppsController::appendRow(not_null<UserData*> bot) {
+	auto row = std::make_unique<PeerListRow>(bot);
+	if (const auto count = bot->botInfo->activeUsers) {
+		row->setCustomStatus(
+			tr::lng_bot_status_users(tr::now, lt_count_decimal, count));
+	}
+	delegate()->peerListAppendRow(std::move(row));
+}
+
+PopularAppsController::PopularAppsController(
+	not_null<Window::SessionController*> window)
+: ObjectListController(window) {
+}
+
+void PopularAppsController::prepare() {
+	setupPlainDivider(tr::lng_bot_apps_popular());
+	fill();
+}
+
+void PopularAppsController::load() {
+	if (_requested || countCurrent()) {
+		return;
+	}
+	_requested = true;
+	const auto attachWebView = &session().attachWebView();
+	attachWebView->loadPopularAppBots();
+	attachWebView->popularAppBotsLoaded(
+	) | rpl::take(1) | rpl::start_with_next([=] {
+		fill();
+	}, _lifetime);
+}
+
+void PopularAppsController::fill() {
+	const auto attachWebView = &session().attachWebView();
+	const auto &list = attachWebView->popularAppBots();
+	if (list.empty()) {
+		return;
+	}
+	for (const auto &bot : list) {
+		appendRow(bot);
+	}
+	delegate()->peerListRefreshRows();
+	setCount(delegate()->peerListFullRowsCount());
+}
+
+void PopularAppsController::appendRow(not_null<UserData*> bot) {
+	auto row = std::make_unique<PeerListRow>(bot);
+	if (const auto count = bot->botInfo->activeUsers) {
+		row->setCustomStatus(
+			tr::lng_bot_status_users(tr::now, lt_count_decimal, count));
+	}
+	delegate()->peerListAppendRow(std::move(row));
 }
 
 Suggestions::Suggestions(
@@ -1000,11 +1133,17 @@ Suggestions::Suggestions(
 	_channelsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
 , _myChannels(setupMyChannels())
 , _recommendations(setupRecommendations())
-, _emptyChannels(_channelsContent->add(setupEmptyChannels())) {
+, _emptyChannels(_channelsContent->add(setupEmptyChannels()))
+, _appsScroll(std::make_unique<Ui::ElasticScroll>(this))
+, _appsContent(
+	_appsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
+, _recentApps(setupRecentApps())
+, _popularApps(setupPopularApps()) {
 
 	setupTabs();
 	setupChats();
 	setupChannels();
+	setupApps();
 }
 
 Suggestions::~Suggestions() = default;
@@ -1027,10 +1166,15 @@ void Suggestions::setupTabs() {
 	_tabs->setSections({
 		tr::lng_recent_chats(tr::now),
 		tr::lng_recent_channels(tr::now),
+		tr::lng_recent_apps(tr::now),
 	});
 	_tabs->sectionActivated(
 	) | rpl::start_with_next([=](int section) {
-		switchTab(section ? Tab::Channels : Tab::Chats);
+		switchTab(section == 2
+			? Tab::Apps
+			: section
+			? Tab::Channels
+			: Tab::Chats);
 	}, _tabs->lifetime());
 }
 
@@ -1141,12 +1285,30 @@ void Suggestions::setupChannels() {
 	});
 }
 
+void Suggestions::setupApps() {
+	_recentApps->count.value() | rpl::start_with_next([=](int count) {
+		_recentApps->wrap->toggle(count > 0, anim::type::instant);
+	}, _recentApps->wrap->lifetime());
+
+	_popularApps->count.value() | rpl::start_with_next([=](int count) {
+		_popularApps->wrap->toggle(count > 0, anim::type::instant);
+	}, _popularApps->wrap->lifetime());
+
+	_appsScroll->setVisible(_tab.current() == Tab::Apps);
+	_appsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
+		const auto recentApps = _recentApps->processTouch(e);
+		const auto popularApps = _popularApps->processTouch(e);
+		return recentApps || popularApps;
+	});
+}
+
 void Suggestions::selectJump(Qt::Key direction, int pageSize) {
-	if (_tab.current() == Tab::Chats) {
-		selectJumpChats(direction, pageSize);
-	} else {
-		selectJumpChannels(direction, pageSize);
+	switch (_tab.current()) {
+	case Tab::Chats: selectJumpChats(direction, pageSize); return;
+	case Tab::Channels: selectJumpChannels(direction, pageSize); return;
+	case Tab::Apps: selectJumpApps(direction, pageSize); return;
 	}
+	Unexpected("Tab in Suggestions::selectJump.");
 }
 
 void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) {
@@ -1260,9 +1422,86 @@ void Suggestions::selectJumpChannels(Qt::Key direction, int pageSize) {
 	}
 }
 
+void Suggestions::selectJumpApps(Qt::Key direction, int pageSize) {
+	const auto recentAppsHasSelection = [=] {
+		return _recentApps->selectJump({}, 0) == JumpResult::Applied;
+	};
+	const auto popularAppsHasSelection = [=] {
+		return _popularApps->selectJump({}, 0) == JumpResult::Applied;
+	};
+	if (pageSize) {
+		if (direction == Qt::Key_Down) {
+			if (popularAppsHasSelection()) {
+				_popularApps->selectJump(direction, pageSize);
+			} else if (recentAppsHasSelection()) {
+				if (_recentApps->selectJump(direction, pageSize)
+					== JumpResult::AppliedAndOut) {
+					_popularApps->selectJump(direction, 0);
+				}
+			} else if (_recentApps->count.current()) {
+				_recentApps->selectJump(direction, 0);
+				_recentApps->selectJump(direction, pageSize);
+			} else if (_popularApps->count.current()) {
+				_popularApps->selectJump(direction, 0);
+				_popularApps->selectJump(direction, pageSize);
+			}
+		} else if (direction == Qt::Key_Up) {
+			if (recentAppsHasSelection()) {
+				if (_recentApps->selectJump(direction, pageSize)
+					== JumpResult::AppliedAndOut) {
+					_channelsScroll->scrollTo(0);
+				}
+			} else if (popularAppsHasSelection()) {
+				if (_popularApps->selectJump(direction, pageSize)
+					== JumpResult::AppliedAndOut) {
+					_recentApps->selectJump(direction, -1);
+				}
+			}
+		}
+	} else if (direction == Qt::Key_Up) {
+		if (recentAppsHasSelection()) {
+			_recentApps->selectJump(direction, 0);
+		} else if (_popularApps->selectJump(direction, 0)
+			== JumpResult::AppliedAndOut) {
+			_recentApps->selectJump(direction, -1);
+		} else if (!popularAppsHasSelection()) {
+			if (_recentApps->selectJump(direction, 0)
+				== JumpResult::AppliedAndOut) {
+				_channelsScroll->scrollTo(0);
+			}
+		}
+	} else if (direction == Qt::Key_Down) {
+		if (popularAppsHasSelection()) {
+			_popularApps->selectJump(direction, 0);
+		} else if (_recentApps->selectJump(direction, 0)
+			== JumpResult::AppliedAndOut) {
+			_popularApps->selectJump(direction, 0);
+		} else if (!recentAppsHasSelection()) {
+			if (_popularApps->selectJump(direction, 0)
+				== JumpResult::AppliedAndOut) {
+				_recentApps->selectJump(direction, 0);
+			}
+		}
+	}
+}
+
 void Suggestions::chooseRow() {
-	if (!_topPeers->chooseRow()) {
-		_recent->choose();
+	switch (_tab.current()) {
+	case Tab::Chats:
+		if (!_topPeers->chooseRow()) {
+			_recent->choose();
+		}
+		break;
+	case Tab::Channels:
+		if (!_myChannels->choose()) {
+			_recommendations->choose();
+		}
+		break;
+	case Tab::Apps:
+		if (!_recentApps->choose()) {
+			_popularApps->choose();
+		}
+		break;
 	}
 }
 
@@ -1286,6 +1525,13 @@ Data::Thread *Suggestions::updateFromChannelsDrag(QPoint globalPosition) {
 	return fromListId(_recommendations->updateFromParentDrag(globalPosition));
 }
 
+Data::Thread *Suggestions::updateFromAppsDrag(QPoint globalPosition) {
+	if (const auto id = _recentApps->updateFromParentDrag(globalPosition)) {
+		return fromListId(id);
+	}
+	return fromListId(_popularApps->updateFromParentDrag(globalPosition));
+}
+
 Data::Thread *Suggestions::fromListId(uint64 peerListRowId) {
 	return peerListRowId
 		? _controller->session().data().history(PeerId(peerListRowId)).get()
@@ -1297,6 +1543,8 @@ void Suggestions::dragLeft() {
 	_recent->dragLeft();
 	_myChannels->dragLeft();
 	_recommendations->dragLeft();
+	_recentApps->dragLeft();
+	_popularApps->dragLeft();
 }
 
 void Suggestions::show(anim::type animated, Fn<void()> finish) {
@@ -1322,7 +1570,8 @@ void Suggestions::hide(anim::type animated, Fn<void()> finish) {
 }
 
 void Suggestions::switchTab(Tab tab) {
-	if (_tab.current() == tab) {
+	const auto was = _tab.current();
+	if (was == tab) {
 		return;
 	}
 	_tab = tab;
@@ -1330,19 +1579,29 @@ void Suggestions::switchTab(Tab tab) {
 	if (_tabs->isHidden()) {
 		return;
 	}
-	startSlideAnimation();
+	startSlideAnimation(was, tab);
 }
 
-void Suggestions::startSlideAnimation() {
+void Suggestions::startSlideAnimation(Tab was, Tab now) {
 	if (!_slideAnimation.animating()) {
-		_slideLeft = Ui::GrabWidget(_chatsScroll.get());
-		_slideRight = Ui::GrabWidget(_channelsScroll.get());
+		_slideLeft = (was == Tab::Chats || now == Tab::Chats)
+			? Ui::GrabWidget(_chatsScroll.get())
+			: Ui::GrabWidget(_channelsScroll.get());
+		_slideLeftTop = (was == Tab::Chats || now == Tab::Chats)
+			? _chatsScroll->y()
+			: _channelsScroll->y();
+		_slideRight = (was == Tab::Apps || now == Tab::Apps)
+			? Ui::GrabWidget(_appsScroll.get())
+			: Ui::GrabWidget(_channelsScroll.get());
+		_slideRightTop = (was == Tab::Apps || now == Tab::Apps)
+			? _appsScroll->y()
+			: _channelsScroll->y();
 		_chatsScroll->hide();
 		_channelsScroll->hide();
+		_appsScroll->hide();
 	}
-	const auto channels = (_tab.current() == Tab::Channels);
-	const auto from = channels ? 0. : 1.;
-	const auto to = channels ? 1. : 0.;
+	const auto from = (now > was) ? 0. : 1.;
+	const auto to = (now > was) ? 1. : 0.;
 	_slideAnimation.start([=] {
 		update();
 		if (!_slideAnimation.animating() && !_shownAnimation.animating()) {
@@ -1376,20 +1635,23 @@ void Suggestions::startShownAnimation(bool shown, Fn<void()> finish) {
 	_tabs->hide();
 	_chatsScroll->hide();
 	_channelsScroll->hide();
+	_appsScroll->hide();
 	_slideAnimation.stop();
 }
 
 void Suggestions::finishShow() {
 	_slideAnimation.stop();
 	_slideLeft = _slideRight = QPixmap();
+	_slideLeftTop = _slideRightTop = 0;
 
 	_shownAnimation.stop();
 	_cache = QPixmap();
 
 	_tabs->show();
-	const auto channels = (_tab.current() == Tab::Channels);
-	_chatsScroll->setVisible(!channels);
-	_channelsScroll->setVisible(channels);
+	const auto tab = _tab.current();
+	_chatsScroll->setVisible(tab == Tab::Chats);
+	_channelsScroll->setVisible(tab == Tab::Channels);
+	_appsScroll->setVisible(tab == Tab::Apps);
 }
 
 float64 Suggestions::shownOpacity() const {
@@ -1414,12 +1676,12 @@ void Suggestions::paintEvent(QPaintEvent *e) {
 		p.setOpacity(1. - progress);
 		p.drawPixmap(
 			anim::interpolate(0, -slide, progress),
-			_chatsScroll->y(),
+			_slideLeftTop,
 			_slideLeft);
 		p.setOpacity(progress);
 		p.drawPixmap(
 			anim::interpolate(slide, 0, progress),
-			_channelsScroll->y(),
+			_slideRightTop,
 			_slideRight);
 	}
 }
@@ -1434,6 +1696,9 @@ void Suggestions::resizeEvent(QResizeEvent *e) {
 
 	_channelsScroll->setGeometry(0, tabs, w, height() - tabs);
 	_channelsContent->resizeToWidth(w);
+
+	_appsScroll->setGeometry(0, tabs, w, height() - tabs);
+	_appsContent->resizeToWidth(w);
 }
 
 auto Suggestions::setupRecentPeers(RecentPeersList recentPeers)
@@ -1595,6 +1860,115 @@ auto Suggestions::setupRecommendations() -> std::unique_ptr<ObjectList> {
 	return result;
 }
 
+auto Suggestions::setupRecentApps() -> std::unique_ptr<ObjectList> {
+	const auto controller = lifetime().make_state<RecentAppsController>(
+		_controller);
+
+	auto result = setupObjectList(
+		_appsScroll.get(),
+		_appsContent,
+		controller);
+	const auto raw = result.get();
+	const auto list = raw->wrap->entity();
+
+	raw->selectJump = [=](Qt::Key direction, int pageSize) {
+		const auto had = list->hasSelection();
+		if (direction == Qt::Key()) {
+			return had ? JumpResult::Applied : JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Up && !had) {
+			if (pageSize < 0) {
+				list->selectLast();
+				return list->hasSelection()
+					? JumpResult::Applied
+					: JumpResult::NotApplied;
+			}
+			return JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
+			const auto was = list->selectedIndex();
+			const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
+			if (pageSize > 0) {
+				list->selectSkipPage(pageSize, delta);
+			} else {
+				list->selectSkip(delta);
+			}
+			if (had
+				&& delta > 0
+				&& raw->count.current()
+				&& list->selectedIndex() == was) {
+				list->clearSelection();
+				return JumpResult::AppliedAndOut;
+			}
+			return list->hasSelection()
+				? JumpResult::Applied
+				: had
+				? JumpResult::AppliedAndOut
+				: JumpResult::NotApplied;
+		}
+		return JumpResult::NotApplied;
+	};
+
+	raw->chosen.events(
+	) | rpl::start_with_next([=] {
+		_persist = false;
+	}, list->lifetime());
+
+	controller->load();
+
+	return result;
+}
+
+auto Suggestions::setupPopularApps() -> std::unique_ptr<ObjectList> {
+	const auto controller = lifetime().make_state<PopularAppsController>(
+		_controller);
+
+	const auto addToScroll = [=] {
+		const auto wrap = _recentApps->wrap;
+		return wrap->toggled() ? wrap->height() : 0;
+	};
+	auto result = setupObjectList(
+		_appsScroll.get(),
+		_appsContent,
+		controller,
+		addToScroll);
+	const auto raw = result.get();
+	const auto list = raw->wrap->entity();
+
+	raw->selectJump = [list](Qt::Key direction, int pageSize) {
+		const auto had = list->hasSelection();
+		if (direction == Qt::Key()) {
+			return had ? JumpResult::Applied : JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Up && !had) {
+			return JumpResult::NotApplied;
+		} else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
+			const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
+			if (pageSize > 0) {
+				list->selectSkipPage(pageSize, delta);
+			} else {
+				list->selectSkip(delta);
+			}
+			return list->hasSelection()
+				? JumpResult::Applied
+				: had
+				? JumpResult::AppliedAndOut
+				: JumpResult::NotApplied;
+		}
+		return JumpResult::NotApplied;
+	};
+
+	raw->chosen.events(
+	) | rpl::start_with_next([=] {
+		_persist = true;
+	}, list->lifetime());
+
+	_tab.value() | rpl::filter(
+		rpl::mappers::_1 == Tab::Apps
+	) | rpl::start_with_next([=] {
+		controller->load();
+	}, list->lifetime());
+
+	return result;
+}
+
 auto Suggestions::setupObjectList(
 	not_null<Ui::ElasticScroll*> scroll,
 	not_null<Ui::VerticalLayout*> parent,
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
index 438c0b370..549851d72 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
@@ -79,6 +79,14 @@ public:
 	-> rpl::producer<not_null<PeerData*>> {
 		return _recommendations->chosen.events();
 	}
+	[[nodiscard]] auto recentAppChosen() const
+	-> rpl::producer<not_null<PeerData*>> {
+		return _recentApps->chosen.events();
+	}
+	[[nodiscard]] auto popularAppChosen() const
+	-> rpl::producer<not_null<PeerData*>> {
+		return _popularApps->chosen.events();
+	}
 
 	class ObjectListController;
 
@@ -86,6 +94,7 @@ private:
 	enum class Tab : uchar {
 		Chats,
 		Channels,
+		Apps,
 	};
 	enum class JumpResult : uchar {
 		NotApplied,
@@ -110,13 +119,16 @@ private:
 	void setupTabs();
 	void setupChats();
 	void setupChannels();
+	void setupApps();
 
 	void selectJumpChats(Qt::Key direction, int pageSize);
 	void selectJumpChannels(Qt::Key direction, int pageSize);
+	void selectJumpApps(Qt::Key direction, int pageSize);
 
 	[[nodiscard]] Data::Thread *updateFromChatsDrag(QPoint globalPosition);
 	[[nodiscard]] Data::Thread *updateFromChannelsDrag(
 		QPoint globalPosition);
+	[[nodiscard]] Data::Thread *updateFromAppsDrag(QPoint globalPosition);
 	[[nodiscard]] Data::Thread *fromListId(uint64 peerListRowId);
 
 	[[nodiscard]] std::unique_ptr<ObjectList> setupRecentPeers(
@@ -129,6 +141,9 @@ private:
 	[[nodiscard]] auto setupEmptyChannels()
 		-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
 
+	[[nodiscard]] std::unique_ptr<ObjectList> setupRecentApps();
+	[[nodiscard]] std::unique_ptr<ObjectList> setupPopularApps();
+
 	[[nodiscard]] std::unique_ptr<ObjectList> setupObjectList(
 		not_null<Ui::ElasticScroll*> scroll,
 		not_null<Ui::VerticalLayout*> parent,
@@ -142,7 +157,7 @@ private:
 
 	void switchTab(Tab tab);
 	void startShownAnimation(bool shown, Fn<void()> finish);
-	void startSlideAnimation();
+	void startSlideAnimation(Tab was, Tab now);
 	void finishShow();
 
 	void handlePressForChatPreview(PeerId id, Fn<void(bool)> callback);
@@ -171,6 +186,12 @@ private:
 
 	const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyChannels;
 
+	const std::unique_ptr<Ui::ElasticScroll> _appsScroll;
+	const not_null<Ui::VerticalLayout*> _appsContent;
+
+	const std::unique_ptr<ObjectList> _recentApps;
+	const std::unique_ptr<ObjectList> _popularApps;
+
 	Ui::Animations::Simple _shownAnimation;
 	Fn<void()> _showFinished;
 	bool _hidden = false;
@@ -181,6 +202,9 @@ private:
 	QPixmap _slideLeft;
 	QPixmap _slideRight;
 
+	int _slideLeftTop = 0;
+	int _slideRightTop = 0;
+
 };
 
 [[nodiscard]] rpl::producer<TopPeersList> TopPeersContent(
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index 8e44a7a79..4d5a9bc04 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -469,6 +469,12 @@ infoSharedMediaButtonIconPosition: point(20px, 3px);
 infoGroupMembersIconPosition: point(20px, 10px);
 infoChannelMembersIconPosition: point(20px, 19px);
 
+infoOpenApp: RoundButton(defaultActiveButton) {
+	textTop: 11px;
+	height: 40px;
+}
+infoOpenAppMargin: margins(16px, 12px, 16px, 12px);
+
 infoPersonalChannelIconPosition: point(25px, 20px);
 infoPersonalChannelNameLabel: 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 6e17ed549..3c0af7c0d 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
@@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "info/profile/info_profile_text.h"
 #include "info/profile/info_profile_values.h"
 #include "info/profile/info_profile_widget.h"
+#include "inline_bots/bot_attach_web_view.h"
 #include "lang/lang_keys.h"
 #include "main/main_session.h"
 #include "menu/menu_mute.h"
@@ -776,6 +777,7 @@ private:
 	object_ptr<Ui::RpWidget> setupPersonalChannel(not_null<UserData*> user);
 	object_ptr<Ui::RpWidget> setupInfo();
 	object_ptr<Ui::RpWidget> setupMuteToggle();
+	void setupMainApp();
 	void setupMainButtons();
 	Ui::MultiSlideTracker fillTopicButtons();
 	Ui::MultiSlideTracker fillUserButtons(
@@ -1209,10 +1211,21 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
 	}
 	if (!_peer->isSelf()) {
 		// No notifications toggle for Self => no separator.
+
+		const auto user = _peer->asUser();
+		const auto app = user && user->botInfo && user->botInfo->hasMainApp;
+		const auto padding = app
+			? QMargins(
+				st::infoOpenAppMargin.left(),
+				st::infoProfileSeparatorPadding.top(),
+				st::infoOpenAppMargin.right(),
+				0)
+			: st::infoProfileSeparatorPadding;
+
 		result->add(object_ptr<Ui::SlideWrap<>>(
 			result,
 			object_ptr<Ui::PlainShadow>(result),
-			st::infoProfileSeparatorPadding)
+			padding)
 		)->setDuration(
 			st::infoSlideDuration
 		)->toggleOn(
@@ -1548,6 +1561,42 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupMuteToggle() {
 	return result;
 }
 
+void DetailsFiller::setupMainApp() {
+	const auto button = _wrap->add(
+		object_ptr<Ui::RoundButton>(
+			_wrap,
+			tr::lng_profile_open_app(),
+			st::infoOpenApp),
+		st::infoOpenAppMargin);
+	button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
+
+	const auto user = _peer->asUser();
+	const auto controller = _controller->parentController();
+	button->setClickedCallback([=] {
+		user->session().attachWebView().open({
+			.bot = user,
+			.context = {
+				.controller = controller,
+				.maySkipConfirmation = true,
+			},
+			.source = InlineBots::WebViewSourceBotProfile(),
+		});
+	});
+
+	const auto url = tr::lng_mini_apps_tos_url(tr::now);
+	Ui::AddDividerText(
+		_wrap,
+		tr::lng_profile_open_app_about(
+			lt_terms,
+			tr::lng_profile_open_app_terms() | Ui::Text::ToLink(url),
+			Ui::Text::WithEntities)
+	)->setClickHandlerFilter([=](const auto &...) {
+		UrlClickHandler::Open(url);
+		return false;
+	});
+	Ui::AddSkip(_wrap);
+}
+
 void DetailsFiller::setupMainButtons() {
 	auto wrapButtons = [=](auto &&callback) {
 		auto topSkip = _wrap->add(CreateSlideSkipWidget(_wrap));
@@ -1737,6 +1786,13 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() {
 	add(object_ptr<Ui::BoxContentDivider>(_wrap));
 	add(CreateSkipWidget(_wrap));
 	add(setupInfo());
+	if (const auto user = _peer->asUser()) {
+		if (const auto info = user->botInfo.get()) {
+			if (info->hasMainApp) {
+				setupMainApp();
+			}
+		}
+	}
 	if (!_peer->isSelf()) {
 		add(setupMuteToggle());
 	}
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index e0a609f7a..5810310fa 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -69,6 +69,7 @@ namespace {
 
 constexpr auto kProlongTimeout = 60 * crl::time(1000);
 constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000);
+constexpr auto kPopularAppBotsLimit = 100;
 
 [[nodiscard]] DocumentData *ResolveIcon(
 		not_null<Main::Session*> session,
@@ -669,9 +670,13 @@ void WebViewInstance::resolve() {
 	}, [&](WebViewSourceGame game) {
 		showGame();
 	}, [&](WebViewSourceBotProfile) {
-		confirmOpen([=] {
+		if (_context.maySkipConfirmation) {
 			requestMain();
-		});
+		} else {
+			confirmOpen([=] {
+				requestMain();
+			});
+		}
 	});
 }
 
@@ -757,6 +762,10 @@ void WebViewInstance::confirmOpen(Fn<void()> done) {
 		close();
 		done();
 	};
+	const auto cancel = [=](Fn<void()> close) {
+		botClose();
+		close();
+	};
 	_parentShow->show(Ui::MakeConfirmBox({
 		.text = tr::lng_allow_bot_webview(
 			tr::now,
@@ -764,7 +773,7 @@ void WebViewInstance::confirmOpen(Fn<void()> done) {
 			Ui::Text::Bold(_bot->name()),
 			Ui::Text::RichLangValue),
 		.confirmed = crl::guard(this, callback),
-		.cancelled = crl::guard(this, [=] { botClose(); }),
+		.cancelled = crl::guard(this, cancel),
 		.confirmText = tr::lng_box_ok(),
 	}));
 }
@@ -1444,7 +1453,10 @@ AttachWebView::AttachWebView(not_null<Main::Session*> session)
 	_refreshTimer.callEach(kRefreshBotsTimeout);
 }
 
-AttachWebView::~AttachWebView() = default;
+AttachWebView::~AttachWebView() {
+	closeAll();
+	_session->api().request(_popularAppBotsRequestId).cancel();
+}
 
 void AttachWebView::openByUsername(
 		not_null<Window::SessionController*> controller,
@@ -1501,6 +1513,40 @@ void AttachWebView::closeAll() {
 	base::take(_instances);
 }
 
+void AttachWebView::loadPopularAppBots() {
+	if (_popularAppBotsLoaded.current() || _popularAppBotsRequestId) {
+		return;
+	}
+	_popularAppBotsRequestId = _session->api().request(
+		MTPbots_GetPopularAppBots(
+			MTP_string(),
+			MTP_int(kPopularAppBotsLimit))
+	).done([=](const MTPbots_PopularAppBots &result) {
+		_popularAppBotsRequestId = 0;
+
+		const auto &list = result.data().vusers().v;
+		auto parsed = std::vector<not_null<UserData*>>();
+		parsed.reserve(list.size());
+		for (const auto &user : list) {
+			const auto bot = _session->data().processUser(user);
+			if (bot->isBot()) {
+				parsed.push_back(bot);
+			}
+		}
+		_popularAppBots = std::move(parsed);
+		_popularAppBotsLoaded = true;
+	}).send();
+}
+
+auto AttachWebView::popularAppBots() const
+-> const std::vector<not_null<UserData*>> & {
+	return _popularAppBots;
+}
+
+rpl::producer<> AttachWebView::popularAppBotsLoaded() const {
+	return _popularAppBotsLoaded.changes() | rpl::to_empty;
+}
+
 void AttachWebView::cancel() {
 	_session->api().request(base::take(_requestId)).cancel();
 	_botUsername = QString();
diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
index ecccdd042..4a4f5ca08 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h
@@ -256,9 +256,6 @@ private:
 	void showGame();
 	void started(uint64 queryId);
 
-	[[nodiscard]] Window::SessionController *windowForThread(
-		not_null<Data::Thread*> thread);
-
 	auto nonPanelPaymentFormFactory(
 		Fn<void(Payments::CheckoutResult)> reactivate)
 	-> Fn<void(Payments::NonPanelPaymentForm)>;
@@ -352,6 +349,11 @@ public:
 	void close(not_null<WebViewInstance*> instance);
 	void closeAll();
 
+	void loadPopularAppBots();
+	[[nodiscard]] auto popularAppBots() const
+		-> const std::vector<not_null<UserData*>> &;
+	[[nodiscard]] rpl::producer<> popularAppBotsLoaded() const;
+
 private:
 	void resolveUsername(
 		std::shared_ptr<Ui::Show> show,
@@ -395,6 +397,10 @@ private:
 
 	std::vector<std::unique_ptr<WebViewInstance>> _instances;
 
+	std::vector<not_null<UserData*>> _popularAppBots;
+	mtpRequestId _popularAppBotsRequestId = 0;
+	rpl::variable<bool> _popularAppBotsLoaded = false;
+
 };
 
 [[nodiscard]] std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp
index cc09dacf5..5dd80e988 100644
--- a/Telegram/SourceFiles/main/main_session.cpp
+++ b/Telegram/SourceFiles/main/main_session.cpp
@@ -110,7 +110,9 @@ Session::Session(
 , _recentPeers(std::make_unique<Data::RecentPeers>(this))
 , _scheduledMessages(std::make_unique<Data::ScheduledMessages>(this))
 , _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this))
-, _topPeers(std::make_unique<Data::TopPeers>(this))
+, _topPeers(std::make_unique<Data::TopPeers>(this, Data::TopPeerType::Chat))
+, _topBotApps(
+	std::make_unique<Data::TopPeers>(this, Data::TopPeerType::BotApp))
 , _factchecks(std::make_unique<Data::Factchecks>(this))
 , _locationPickers(std::make_unique<Data::LocationPickers>())
 , _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h
index a69373a36..ba5dbcd99 100644
--- a/Telegram/SourceFiles/main/main_session.h
+++ b/Telegram/SourceFiles/main/main_session.h
@@ -129,6 +129,9 @@ public:
 	[[nodiscard]] Data::TopPeers &topPeers() const {
 		return *_topPeers;
 	}
+	[[nodiscard]] Data::TopPeers &topBotApps() const {
+		return *_topBotApps;
+	}
 	[[nodiscard]] Data::Factchecks &factchecks() const {
 		return *_factchecks;
 	}
@@ -262,6 +265,7 @@ private:
 	const std::unique_ptr<Data::ScheduledMessages> _scheduledMessages;
 	const std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages;
 	const std::unique_ptr<Data::TopPeers> _topPeers;
+	const std::unique_ptr<Data::TopPeers> _topBotApps;
 	const std::unique_ptr<Data::Factchecks> _factchecks;
 	const std::unique_ptr<Data::LocationPickers> _locationPickers;
 
diff --git a/Telegram/SourceFiles/ui/vertical_list.cpp b/Telegram/SourceFiles/ui/vertical_list.cpp
index 11347aa61..6666077dd 100644
--- a/Telegram/SourceFiles/ui/vertical_list.cpp
+++ b/Telegram/SourceFiles/ui/vertical_list.cpp
@@ -28,31 +28,34 @@ void AddDivider(not_null<Ui::VerticalLayout*> container) {
 	container->add(object_ptr<Ui::BoxContentDivider>(container));
 }
 
-void AddDividerText(
+not_null<Ui::FlatLabel*> AddDividerText(
 		not_null<Ui::VerticalLayout*> container,
 		rpl::producer<QString> text,
 		const style::margins &margins,
 		RectParts parts) {
-	AddDividerText(
+	return AddDividerText(
 		container,
 		std::move(text) | Ui::Text::ToWithEntities(),
 		margins,
 		parts);
 }
 
-void AddDividerText(
+not_null<Ui::FlatLabel*> AddDividerText(
 		not_null<Ui::VerticalLayout*> container,
 		rpl::producer<TextWithEntities> text,
 		const style::margins &margins,
 		RectParts parts) {
+	auto label = object_ptr<Ui::FlatLabel>(
+		container,
+		std::move(text),
+		st::boxDividerLabel);
+	const auto result = label.data();
 	container->add(object_ptr<Ui::DividerLabel>(
 		container,
-		object_ptr<Ui::FlatLabel>(
-			container,
-			std::move(text),
-			st::boxDividerLabel),
+		std::move(label),
 		margins,
 		parts));
+	return result;
 }
 
 not_null<Ui::FlatLabel*> AddSubsectionTitle(
diff --git a/Telegram/SourceFiles/ui/vertical_list.h b/Telegram/SourceFiles/ui/vertical_list.h
index 7ab743bd3..82934367f 100644
--- a/Telegram/SourceFiles/ui/vertical_list.h
+++ b/Telegram/SourceFiles/ui/vertical_list.h
@@ -25,12 +25,12 @@ class VerticalLayout;
 void AddSkip(not_null<Ui::VerticalLayout*> container);
 void AddSkip(not_null<Ui::VerticalLayout*> container, int skip);
 void AddDivider(not_null<Ui::VerticalLayout*> container);
-void AddDividerText(
+not_null<Ui::FlatLabel*> AddDividerText(
 	not_null<Ui::VerticalLayout*> container,
 	rpl::producer<QString> text,
 	const style::margins &margins = st::defaultBoxDividerLabelPadding,
 	RectParts parts = RectPart::Top | RectPart::Bottom);
-void AddDividerText(
+not_null<Ui::FlatLabel*> AddDividerText(
 	not_null<Ui::VerticalLayout*> container,
 	rpl::producer<TextWithEntities> text,
 	const style::margins &margins = st::defaultBoxDividerLabelPadding,

From 3eeb01be61640c0062715fde3b851ea6b79d9a36 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 Jul 2024 14:44:06 +0200
Subject: [PATCH 128/163] Save Celsius/Fahrenheit in Settings.

---
 Telegram/SourceFiles/core/core_settings.cpp   | 12 +++++--
 Telegram/SourceFiles/core/core_settings.h     |  8 +++++
 .../stories/media_stories_controller.cpp      | 26 +++++++++++---
 .../media/stories/media_stories_controller.h  |  2 ++
 .../media/stories/media_stories_reactions.cpp | 35 +++++++++----------
 .../media/stories/media_stories_reactions.h   |  5 +--
 6 files changed, 61 insertions(+), 27 deletions(-)

diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp
index efd0ba4d5..16b2595de 100644
--- a/Telegram/SourceFiles/core/core_settings.cpp
+++ b/Telegram/SourceFiles/core/core_settings.cpp
@@ -220,7 +220,7 @@ QByteArray Settings::serialize() const {
 		+ Serialize::bytearraySize(ivPosition)
 		+ Serialize::stringSize(noWarningExtensions)
 		+ Serialize::stringSize(_customFontFamily)
-		+ sizeof(qint32) * 2;
+		+ sizeof(qint32) * 3;
 
 	auto result = QByteArray();
 	result.reserve(size);
@@ -372,7 +372,8 @@ QByteArray Settings::serialize() const {
 				qRound(_dialogsNoChatWidthRatio.current() * 1000000),
 				0,
 				1000000))
-			<< qint32(_systemUnlockEnabled ? 1 : 0);
+			<< qint32(_systemUnlockEnabled ? 1 : 0)
+			<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2);
 	}
 
 	Ensures(result.size() == size);
@@ -493,6 +494,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	QByteArray ivPosition;
 	QString customFontFamily = _customFontFamily;
 	qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
+	qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
 
 	stream >> themesAccentColors;
 	if (!stream.atEnd()) {
@@ -793,6 +795,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	if (!stream.atEnd()) {
 		stream >> systemUnlockEnabled;
 	}
+	if (!stream.atEnd()) {
+		stream >> weatherInCelsius;
+	}
 	if (stream.status() != QDataStream::Ok) {
 		LOG(("App Error: "
 			"Bad data for Core::Settings::constructFromSerialized()"));
@@ -1001,6 +1006,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	}
 	_customFontFamily = customFontFamily;
 	_systemUnlockEnabled = (systemUnlockEnabled == 1);
+	_weatherInCelsius = !weatherInCelsius
+		? std::optional<bool>()
+		: (weatherInCelsius == 1);
 }
 
 QString Settings::getSoundPath(const QString &key) const {
diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h
index 3412c4588..0ac4e94a5 100644
--- a/Telegram/SourceFiles/core/core_settings.h
+++ b/Telegram/SourceFiles/core/core_settings.h
@@ -891,6 +891,13 @@ public:
 		_systemUnlockEnabled = enabled;
 	}
 
+	[[nodiscard]] std::optional<bool> weatherInCelsius() const {
+		return _weatherInCelsius;
+	}
+	void setWeatherInCelsius(bool value) {
+		_weatherInCelsius = value;
+	}
+
 	[[nodiscard]] static bool ThirdColumnByDefault();
 	[[nodiscard]] static float64 DefaultDialogsWidthRatio();
 
@@ -1022,6 +1029,7 @@ private:
 	WindowPosition _ivPosition;
 	QString _customFontFamily;
 	bool _systemUnlockEnabled = false;
+	std::optional<bool> _weatherInCelsius;
 
 	bool _tabbedReplacedWithInfo = false; // per-window
 	rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
index ca558abfd..fc75e366d 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "media/stories/media_stories_controller.h"
 
+#include "base/platform/base_platform_info.h"
 #include "base/power_save_blocker.h"
 #include "base/qt_signal_producer.h"
 #include "base/unixtime.h"
@@ -128,6 +129,13 @@ struct SameDayRange {
 		int(base::SafeRound(asin * point.x() + acos * point.y())));
 }
 
+[[nodiscard]] bool ResolveWeatherInCelsius() {
+	const auto saved = Core::App().settings().weatherInCelsius();
+	return saved.value_or(!ranges::contains(
+		std::array{ u"US"_q, u"BS"_q, u"KY"_q, u"LR"_q, u"BZ"_q },
+		Platform::SystemCountry().toUpper()));
+}
+
 } // namespace
 
 class Controller::PhotoPlayback final {
@@ -284,7 +292,8 @@ Controller::Controller(not_null<Delegate*> delegate)
 , _slider(std::make_unique<Slider>(this))
 , _replyArea(std::make_unique<ReplyArea>(this))
 , _reactions(std::make_unique<Reactions>(this))
-, _recentViews(std::make_unique<RecentViews>(this)) {
+, _recentViews(std::make_unique<RecentViews>(this))
+, _weatherInCelsius(ResolveWeatherInCelsius()){
 	initLayout();
 
 	using namespace rpl::mappers;
@@ -1272,16 +1281,16 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
 			});
 		}
 		for (const auto &weather : _weatherAreas) {
-			auto widget = _reactions->makeWeatherAreaWidget(weather);
-			const auto raw = widget.get();
 			_areas.push_back({
 				.original = weather.area.geometry,
 				.radiusOriginal = weather.area.radius,
 				.rotation = weather.area.rotation,
 				.handler = std::make_shared<LambdaClickHandler>([=] {
-					raw->toggleMode();
+					toggleWeatherMode();
 				}),
-				.view = std::move(widget),
+				.view = _reactions->makeWeatherAreaWidget(
+					weather,
+					_weatherInCelsius.value()),
 			});
 		}
 		rebuildActiveAreas(*layout);
@@ -1300,6 +1309,13 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
 	return nullptr;
 }
 
+void Controller::toggleWeatherMode() const {
+	const auto now = !_weatherInCelsius.current();
+	Core::App().settings().setWeatherInCelsius(now);
+	Core::App().saveSettingsDelayed();
+	_weatherInCelsius = now;
+}
+
 void Controller::maybeMarkAsRead(const Player::TrackState &state) {
 	const auto length = state.length;
 	const auto position = Player::IsStoppedAtEnd(state.state)
diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h
index 590dee3f6..860908b96 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_controller.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h
@@ -229,6 +229,7 @@ private:
 	void updatePlayingAllowed();
 	void setPlayingAllowed(bool allowed);
 	void rebuildActiveAreas(const Layout &layout) const;
+	void toggleWeatherMode() const;
 
 	void hideSiblings();
 	void showSiblings(not_null<Main::Session*> session);
@@ -307,6 +308,7 @@ private:
 	std::vector<Data::UrlArea> _urlAreas;
 	std::vector<Data::WeatherArea> _weatherAreas;
 	mutable std::vector<ActiveArea> _areas;
+	mutable rpl::variable<bool> _weatherInCelsius;
 
 	std::vector<CachedSource> _cachedSourcesList;
 	int _cachedSourceIndex = -1;
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
index f5e2582ef..a8e044cc9 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -75,7 +75,6 @@ public:
 	void setAreaGeometry(QRect geometry, float64 radius) override;
 	void updateReactionsCount(int count) override;
 	void playEffect() override;
-	void toggleMode() override;
 	bool contains(QPoint point) override;
 
 private:
@@ -137,12 +136,12 @@ public:
 	WeatherView(
 		QWidget *parent,
 		not_null<Main::Session*> session,
-		const Data::WeatherArea &data);
+		const Data::WeatherArea &data,
+		rpl::producer<bool> weatherInCelsius);
 
 	void setAreaGeometry(QRect geometry, float64 radius) override;
 	void updateReactionsCount(int count) override;
 	void playEffect() override;
-	void toggleMode() override;
 	bool contains(QPoint point) override;
 
 private:
@@ -349,10 +348,6 @@ void ReactionView::playEffect() {
 	}
 }
 
-void ReactionView::toggleMode() {
-	Unexpected("ReactionView::toggleMode.");
-}
-
 bool ReactionView::contains(QPoint point) {
 	const auto circle = _apiGeometry;
 	const auto radius = std::min(circle.width(), circle.height()) / 2;
@@ -537,7 +532,8 @@ void ReactionView::cacheBackground() {
 WeatherView::WeatherView(
 	QWidget *parent,
 	not_null<Main::Session*> session,
-	const Data::WeatherArea &data)
+	const Data::WeatherArea &data,
+	rpl::producer<bool> weatherInCelsius)
 : RpWidget(parent)
 , _session(session)
 , _data(data)
@@ -546,6 +542,12 @@ WeatherView::WeatherView(
 	watchForSticker();
 	setAttribute(Qt::WA_TransparentForMouseEvents);
 	show();
+
+	std::move(weatherInCelsius) | rpl::start_with_next([=](bool celsius) {
+		_celsius = celsius;
+		_background = {};
+		update();
+	}, lifetime());
 }
 
 void WeatherView::watchForSticker() {
@@ -565,7 +567,7 @@ void WeatherView::watchForSticker() {
 		) | rpl::start_with_next([=](not_null<DocumentData*> document) {
 			setStickerFrom(document);
 			update();
-		}, _lifetime);
+		}, lifetime());
 	}
 }
 
@@ -598,12 +600,6 @@ void WeatherView::playEffect() {
 	Unexpected("WeatherView::playEffect.");
 }
 
-void WeatherView::toggleMode() {
-	_celsius = !_celsius;
-	_background = {};
-	update();
-}
-
 bool WeatherView::contains(QPoint point) {
 	const auto geometry = _rect.translated(pos()).toRect();
 	const auto angle = -_data.area.rotation;
@@ -683,7 +679,7 @@ void WeatherView::setStickerFrom(not_null<DocumentData*> document) {
 		}
 		_sticker->setRepaintCallback([=] { update(); });
 		update();
-	}, _lifetime);
+	}, lifetime());
 }
 
 void WeatherView::cacheBackground() {
@@ -1087,12 +1083,15 @@ auto Reactions::makeSuggestedReactionWidget(
 		reaction);
 }
 
-auto Reactions::makeWeatherAreaWidget(const Data::WeatherArea &data)
+auto Reactions::makeWeatherAreaWidget(
+	const Data::WeatherArea &data,
+	rpl::producer<bool> weatherInCelsius)
 -> std::unique_ptr<StoryAreaView> {
 	return std::make_unique<WeatherView>(
 		_controller->wrap(),
 		&_controller->uiShow()->session(),
-		data);
+		data,
+		std::move(weatherInCelsius));
 }
 
 void Reactions::setReplyFieldState(
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
index b17e2e19d..11da4c456 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.h
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h
@@ -49,7 +49,6 @@ public:
 	virtual void setAreaGeometry(QRect geometry, float64 radius) = 0;
 	virtual void updateReactionsCount(int count) = 0;
 	virtual void playEffect() = 0;
-	virtual void toggleMode() = 0;
 	virtual bool contains(QPoint point) = 0;
 };
 
@@ -83,7 +82,9 @@ public:
 	[[nodiscard]] auto makeSuggestedReactionWidget(
 		const Data::SuggestedReaction &reaction)
 	-> std::unique_ptr<StoryAreaView>;
-	[[nodiscard]] auto makeWeatherAreaWidget(const Data::WeatherArea &data)
+	[[nodiscard]] auto makeWeatherAreaWidget(
+		const Data::WeatherArea &data,
+		rpl::producer<bool> weatherInCelsius)
 	-> std::unique_ptr<StoryAreaView>;
 
 	void setReplyFieldState(

From 2dcf40817e39e52e744b423310362c2997c6658c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 26 Jul 2024 19:25:47 +0200
Subject: [PATCH 129/163] Initial tonsite:// show in IV window.

---
 Telegram/SourceFiles/core/ui_integration.cpp |   3 +
 Telegram/SourceFiles/iv/iv_controller.cpp    |  20 +++
 Telegram/SourceFiles/iv/iv_controller.h      |   2 +
 Telegram/SourceFiles/iv/iv_instance.cpp      | 156 +++++++++++++++++--
 Telegram/SourceFiles/iv/iv_instance.h        |   6 +
 5 files changed, 177 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp
index 1bc5a1f85..bf1b49432 100644
--- a/Telegram/SourceFiles/core/ui_integration.cpp
+++ b/Telegram/SourceFiles/core/ui_integration.cpp
@@ -238,6 +238,9 @@ bool UiIntegration::handleUrlClick(
 	} else if (local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) {
 		Core::App().openLocalUrl(local, context);
 		return true;
+	} else if (local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) {
+		Core::App().iv().showTonSite(url, context);
+		return true;
 	} else if (local.startsWith(u"internal:"_q, Qt::CaseInsensitive)) {
 		Core::App().openInternalUrl(local, context);
 		return true;
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index b3fb82a0d..6720f3c93 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -251,6 +251,26 @@ void Controller::update(Prepared page) {
 	}
 }
 
+void Controller::showTonSite(
+		const Webview::StorageId &storageId,
+		QString uri) {
+	auto part = uri.mid(u"tonsite://"_q.size());
+	part = part.replace('-', "-h");
+	part = part.replace('.', "-d");
+	const auto url = "https://" + part + ".magic.org";
+	if (!_webview) {
+		createWebview(storageId);
+	}
+	if (_webview && _webview->widget()) {
+		_webview->navigate(url);
+		activate();
+	} else {
+		_events.fire({ Event::Type::Close });
+	}
+	_subtitleText = uri;
+	_menuToggle->hide();
+}
+
 QByteArray Controller::fillInChannelValuesScript(
 		base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues) {
 	auto result = QByteArray();
diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h
index 473813fca..1f0ecaf9a 100644
--- a/Telegram/SourceFiles/iv/iv_controller.h
+++ b/Telegram/SourceFiles/iv/iv_controller.h
@@ -76,6 +76,8 @@ public:
 		base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues);
 	void update(Prepared page);
 
+	void showTonSite(const Webview::StorageId &storageId, QString uri);
+
 	[[nodiscard]] bool active() const;
 	void showJoinedTooltip();
 	void minimize();
diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp
index a49ba8887..00cfaa615 100644
--- a/Telegram/SourceFiles/iv/iv_instance.cpp
+++ b/Telegram/SourceFiles/iv/iv_instance.cpp
@@ -171,6 +171,39 @@ private:
 
 };
 
+class TonSite final : public base::has_weak_ptr {
+public:
+	TonSite(not_null<Delegate*> delegate, QString uri);
+
+	[[nodiscard]] bool active() const;
+
+	void moveTo(QString uri);
+
+	void minimize();
+
+	[[nodiscard]] rpl::producer<Controller::Event> events() const {
+		return _events.events();
+	}
+
+	[[nodiscard]] rpl::lifetime &lifetime() {
+		return _lifetime;
+	}
+
+private:
+	void createController();
+
+	void showWindowed();
+
+	const not_null<Delegate*> _delegate;
+	QString _uri;
+	std::unique_ptr<Controller> _controller;
+
+	rpl::event_stream<Controller::Event> _events;
+
+	rpl::lifetime _lifetime;
+
+};
+
 Shown::Shown(
 	not_null<Delegate*> delegate,
 	not_null<Main::Session*> session,
@@ -742,6 +775,50 @@ void Shown::minimize() {
 	}
 }
 
+TonSite::TonSite(not_null<Delegate*> delegate, QString uri)
+: _delegate(delegate)
+, _uri(uri) {
+	showWindowed();
+}
+
+void TonSite::createController() {
+	Expects(!_controller);
+
+	const auto showShareBox = [=](ShareBoxDescriptor &&descriptor) {
+		return ShareBoxResult();
+	};
+	_controller = std::make_unique<Controller>(
+		_delegate,
+		std::move(showShareBox));
+
+	_controller->events(
+	) | rpl::start_to_stream(_events, _controller->lifetime());
+}
+
+void TonSite::showWindowed() {
+	if (!_controller) {
+		createController();
+	}
+
+	_controller->showTonSite(
+		{},//_session->local().resolveStorageIdOther(),
+		_uri);
+}
+
+bool TonSite::active() const {
+	return _controller && _controller->active();
+}
+
+void TonSite::moveTo(QString uri) {
+	_controller->showTonSite({}, uri);
+}
+
+void TonSite::minimize() {
+	if (_controller) {
+		_controller->minimize();
+	}
+}
+
 Instance::Instance(not_null<Delegate*> delegate) : _delegate(delegate) {
 }
 
@@ -785,6 +862,7 @@ void Instance::show(
 		const auto lower = event.url.toLower();
 		const auto urlChecked = lower.startsWith("http://")
 			|| lower.startsWith("https://");
+		const auto tonsite = lower.startsWith("tonsite://");
 		switch (event.type) {
 		case Type::Close:
 			_shown = nullptr;
@@ -801,8 +879,10 @@ void Instance::show(
 		case Type::OpenLinkExternal:
 			if (urlChecked) {
 				File::OpenUrl(event.url);
+				closeAll();
+			} else if (tonsite) {
+				showTonSite(event.url);
 			}
-			closeAll();
 			break;
 		case Type::OpenMedia:
 			if (const auto window = Core::App().activeWindow()) {
@@ -840,7 +920,10 @@ void Instance::show(
 			break;
 		case Type::OpenPage:
 		case Type::OpenLink: {
-			if (!urlChecked) {
+			if (tonsite) {
+				showTonSite(event.url);
+				break;
+			} else if (!urlChecked) {
 				break;
 			}
 			const auto session = _shownSession;
@@ -990,6 +1073,52 @@ void Instance::openWithIvPreferred(
 	}).send();
 }
 
+void Instance::showTonSite(
+		const QString &uri,
+		QVariant context) {
+	if (Platform::IsMac()) {
+		// Otherwise IV is not visible under the media viewer.
+		Core::App().hideMediaView();
+	}
+	if (_tonSite) {
+		_tonSite->moveTo(uri);
+		return;
+	}
+	_tonSite = std::make_unique<TonSite>(_delegate, uri);
+	_tonSite->events() | rpl::start_with_next([=](Controller::Event event) {
+		using Type = Controller::Event::Type;
+		const auto lower = event.url.toLower();
+		const auto urlChecked = lower.startsWith("http://")
+			|| lower.startsWith("https://");
+		const auto tonsite = lower.startsWith("tonsite://");
+		switch (event.type) {
+		case Type::Close:
+			_tonSite = nullptr;
+			break;
+		case Type::Quit:
+			Shortcuts::Launch(Shortcuts::Command::Quit);
+			break;
+		case Type::OpenLinkExternal:
+			if (urlChecked) {
+				File::OpenUrl(event.url);
+				closeAll();
+			} else if (tonsite) {
+				showTonSite(event.url);
+			}
+			break;
+		case Type::OpenPage:
+		case Type::OpenLink:
+			if (urlChecked) {
+				File::OpenUrl(event.url);
+				closeAll();
+			} else if (tonsite) {
+				showTonSite(event.url);
+			}
+			break;
+		}
+	}, _tonSite->lifetime());
+}
+
 void Instance::requestFull(
 		not_null<Main::Session*> session,
 		const QString &id) {
@@ -1085,23 +1214,30 @@ bool Instance::hasActiveWindow(not_null<Main::Session*> session) const {
 }
 
 bool Instance::closeActive() {
-	if (!_shown || !_shown->active()) {
-		return false;
+	if (_shown && _shown->active()) {
+		_shown = nullptr;
+		return true;
+	} else if (_tonSite && _tonSite->active()) {
+		_tonSite = nullptr;
+		return true;
 	}
-	_shown = nullptr;
-	return true;
+	return false;
 }
 
 bool Instance::minimizeActive() {
-	if (!_shown || !_shown->active()) {
-		return false;
+	if (_shown && _shown->active()) {
+		_shown->minimize();
+		return true;
+	} else if (_tonSite && _tonSite->active()) {
+		_tonSite->minimize();
+		return true;
 	}
-	_shown->minimize();
-	return true;
+	return false;
 }
 
 void Instance::closeAll() {
 	_shown = nullptr;
+	_tonSite = nullptr;
 }
 
 bool PreferForUri(const QString &uri) {
diff --git a/Telegram/SourceFiles/iv/iv_instance.h b/Telegram/SourceFiles/iv/iv_instance.h
index 9083734ad..c48cf569e 100644
--- a/Telegram/SourceFiles/iv/iv_instance.h
+++ b/Telegram/SourceFiles/iv/iv_instance.h
@@ -22,6 +22,7 @@ namespace Iv {
 
 class Data;
 class Shown;
+class TonSite;
 
 class Instance final {
 public:
@@ -50,6 +51,10 @@ public:
 		QString uri,
 		QVariant context = {});
 
+	void showTonSite(
+		const QString &uri,
+		QVariant context = {});
+
 	[[nodiscard]] bool hasActiveWindow(
 		not_null<Main::Session*> session) const;
 
@@ -97,6 +102,7 @@ private:
 	QString _ivRequestUri;
 	mtpRequestId _ivRequestId = 0;
 
+	std::unique_ptr<TonSite> _tonSite;
 
 	rpl::lifetime _lifetime;
 

From 4f37343e8b09d59bcb74b803708040c4c5c82d0f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 29 Jul 2024 12:26:22 +0200
Subject: [PATCH 130/163] Use special webview storage for tonsite.

---
 Telegram/SourceFiles/core/core_settings.cpp   | 11 ++++++++--
 Telegram/SourceFiles/core/core_settings.h     |  8 +++++++
 Telegram/SourceFiles/iv/iv_controller.cpp     | 22 +++++++++++++++----
 Telegram/SourceFiles/iv/iv_instance.cpp       |  4 +---
 .../main/main_session_settings.cpp            |  4 +++-
 .../SourceFiles/storage/storage_account.cpp   | 14 ++++++++++++
 .../SourceFiles/storage/storage_account.h     |  2 ++
 7 files changed, 55 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp
index 16b2595de..782f59156 100644
--- a/Telegram/SourceFiles/core/core_settings.cpp
+++ b/Telegram/SourceFiles/core/core_settings.cpp
@@ -220,7 +220,8 @@ QByteArray Settings::serialize() const {
 		+ Serialize::bytearraySize(ivPosition)
 		+ Serialize::stringSize(noWarningExtensions)
 		+ Serialize::stringSize(_customFontFamily)
-		+ sizeof(qint32) * 3;
+		+ sizeof(qint32) * 3
+		+ Serialize::bytearraySize(_tonsiteStorageToken);
 
 	auto result = QByteArray();
 	result.reserve(size);
@@ -373,7 +374,8 @@ QByteArray Settings::serialize() const {
 				0,
 				1000000))
 			<< qint32(_systemUnlockEnabled ? 1 : 0)
-			<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2);
+			<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
+			<< _tonsiteStorageToken;
 	}
 
 	Ensures(result.size() == size);
@@ -495,6 +497,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	QString customFontFamily = _customFontFamily;
 	qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
 	qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
+	QByteArray tonsiteStorageToken = _tonsiteStorageToken;
 
 	stream >> themesAccentColors;
 	if (!stream.atEnd()) {
@@ -798,6 +801,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	if (!stream.atEnd()) {
 		stream >> weatherInCelsius;
 	}
+	if (!stream.atEnd()) {
+		stream >> tonsiteStorageToken;
+	}
 	if (stream.status() != QDataStream::Ok) {
 		LOG(("App Error: "
 			"Bad data for Core::Settings::constructFromSerialized()"));
@@ -1009,6 +1015,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	_weatherInCelsius = !weatherInCelsius
 		? std::optional<bool>()
 		: (weatherInCelsius == 1);
+	_tonsiteStorageToken = tonsiteStorageToken;
 }
 
 QString Settings::getSoundPath(const QString &key) const {
diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h
index 0ac4e94a5..73f0f5eb0 100644
--- a/Telegram/SourceFiles/core/core_settings.h
+++ b/Telegram/SourceFiles/core/core_settings.h
@@ -898,6 +898,13 @@ public:
 		_weatherInCelsius = value;
 	}
 
+	[[nodiscard]] QByteArray tonsiteStorageToken() const {
+		return _tonsiteStorageToken;
+	}
+	void setTonsiteStorageToken(const QByteArray &value) {
+		_tonsiteStorageToken = value;
+	}
+
 	[[nodiscard]] static bool ThirdColumnByDefault();
 	[[nodiscard]] static float64 DefaultDialogsWidthRatio();
 
@@ -1030,6 +1037,7 @@ private:
 	QString _customFontFamily;
 	bool _systemUnlockEnabled = false;
 	std::optional<bool> _weatherInCelsius;
+	QByteArray _tonsiteStorageToken;
 
 	bool _tabbedReplacedWithInfo = false; // per-window
 	rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 6720f3c93..07ddd4e9d 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -254,10 +254,24 @@ void Controller::update(Prepared page) {
 void Controller::showTonSite(
 		const Webview::StorageId &storageId,
 		QString uri) {
-	auto part = uri.mid(u"tonsite://"_q.size());
-	part = part.replace('-', "-h");
-	part = part.replace('.', "-d");
-	const auto url = "https://" + part + ".magic.org";
+	const auto url = [&] {
+		auto parsed = QUrl(uri);
+		if (parsed.isValid()) {
+			auto host = parsed.host();
+			host = host.replace('-', "-h");
+			host = host.replace('.', "-d");
+			parsed.setHost(host + ".magic.org");
+			parsed.setScheme("https");
+			return parsed.toString();
+		}
+		auto part = uri.mid(u"tonsite://"_q.size());
+		const auto split = part.indexOf('/');
+		auto host = (split < 0) ? part : part.left(split);
+		host = host.replace('-', "-h");
+		host = host.replace('.', "-d");
+		part = (split < 0) ? QString() : part.mid(split);
+		return "https://" + host + ".magic.org" + part;
+	}();
 	if (!_webview) {
 		createWebview(storageId);
 	}
diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp
index 00cfaa615..480d73573 100644
--- a/Telegram/SourceFiles/iv/iv_instance.cpp
+++ b/Telegram/SourceFiles/iv/iv_instance.cpp
@@ -800,9 +800,7 @@ void TonSite::showWindowed() {
 		createController();
 	}
 
-	_controller->showTonSite(
-		{},//_session->local().resolveStorageIdOther(),
-		_uri);
+	_controller->showTonSite(Storage::TonSiteStorageId(), _uri);
 }
 
 bool TonSite::active() const {
diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp
index 6349f6b6a..253116dea 100644
--- a/Telegram/SourceFiles/main/main_session_settings.cpp
+++ b/Telegram/SourceFiles/main/main_session_settings.cpp
@@ -34,7 +34,7 @@ SessionSettings::SessionSettings()
 
 QByteArray SessionSettings::serialize() const {
 	const auto autoDownload = _autoDownload.serialize();
-	auto size = sizeof(qint32) * 4
+	const auto size = sizeof(qint32) * 4
 		+ _groupStickersSectionHidden.size() * sizeof(quint64)
 		+ sizeof(qint32) * 4
 		+ Serialize::bytearraySize(autoDownload)
@@ -103,6 +103,8 @@ QByteArray SessionSettings::serialize() const {
 			<< qint32(_lastNonPremiumLimitDownload)
 			<< qint32(_lastNonPremiumLimitUpload);
 	}
+
+	Ensures(result.size() == size);
 	return result;
 }
 
diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp
index 83d8e52f4..bfac71560 100644
--- a/Telegram/SourceFiles/storage/storage_account.cpp
+++ b/Telegram/SourceFiles/storage/storage_account.cpp
@@ -3198,4 +3198,18 @@ bool Account::decrypt(
 	return true;
 }
 
+Webview::StorageId TonSiteStorageId() {
+	auto result = Webview::StorageId{
+		.path = BaseGlobalPath() + u"webview-tonsite"_q,
+		.token = Core::App().settings().tonsiteStorageToken(),
+	};
+	if (result.token.isEmpty()) {
+		result.token = QByteArray::fromStdString(
+			Webview::GenerateStorageToken());
+		Core::App().settings().setTonsiteStorageToken(result.token);
+		Core::App().saveSettingsDelayed();
+	}
+	return result;
+}
+
 } // namespace Storage
diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h
index d1314cde2..84533f93d 100644
--- a/Telegram/SourceFiles/storage/storage_account.h
+++ b/Telegram/SourceFiles/storage/storage_account.h
@@ -327,4 +327,6 @@ private:
 
 };
 
+[[nodiscard]] Webview::StorageId TonSiteStorageId();
+
 } // namespace Storage

From 8b2bbfba6aaaa764f83a35ca0c1b26d7bde59187 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2024 10:19:59 +0200
Subject: [PATCH 131/163] Navigate back/forward in tonsite pages.

---
 Telegram/Resources/iv_html/page.js            |   3 -
 Telegram/SourceFiles/iv/iv.style              |  13 +-
 Telegram/SourceFiles/iv/iv_controller.cpp     | 146 ++++++++++++++----
 Telegram/SourceFiles/iv/iv_controller.h       |   5 +-
 .../settings/settings_experimental.cpp        |   2 +
 Telegram/lib_webview                          |   2 +-
 6 files changed, 130 insertions(+), 41 deletions(-)

diff --git a/Telegram/Resources/iv_html/page.js b/Telegram/Resources/iv_html/page.js
index bae02fe48..fda34772f 100644
--- a/Telegram/Resources/iv_html/page.js
+++ b/Telegram/Resources/iv_html/page.js
@@ -618,9 +618,6 @@ var IV = {
 		element.getAnimations().forEach(
 			(animation) => animation.finish());
 	},
-	back: function () {
-        window.history.back();
-	},
 	menuShown: function (shown) {
 		var already = document.getElementById('menu_page_blocker');
 		if (already && shown) {
diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style
index c8c9c6b69..6a5757276 100644
--- a/Telegram/SourceFiles/iv/iv.style
+++ b/Telegram/SourceFiles/iv/iv.style
@@ -22,12 +22,21 @@ ivMenuToggle: IconButton(defaultIconButton) {
 	}
 }
 ivMenuPosition: point(-2px, 40px);
+ivBackIcon: icon {{ "box_button_back", menuIconColor }};
 ivBack: IconButton(ivMenuToggle) {
 	width: 60px;
-	icon: icon {{ "box_button_back", menuIconColor }};
-	iconOver: icon {{ "box_button_back", menuIconColor }};
+	icon: ivBackIcon;
+	iconOver: ivBackIcon;
 	rippleAreaPosition: point(12px, 6px);
 }
+ivBackIconDisabled: icon {{ "box_button_back", menuIconFg }};
+ivForwardIcon: icon {{ "box_button_back-flip_horizontal", menuIconColor }};
+ivForward: IconButton(ivBack) {
+	width: 48px;
+	icon: ivForwardIcon;
+	iconOver: ivForwardIcon;
+	rippleAreaPosition: point(0px, 6px);
+}
 ivSubtitleFont: font(16px semibold);
 ivSubtitle: FlatLabel(defaultFlatLabel) {
 	textFg: boxTitleFg;
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 07ddd4e9d..1870011dc 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -127,6 +127,51 @@ namespace {
 	return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
 }
 
+[[nodiscard]] QString TonsiteToHttps(QString value) {
+	const auto ChangeHost = [](QString tonsite) {
+		tonsite = tonsite.replace('-', "-h");
+		tonsite = tonsite.replace('.', "-d");
+		return tonsite + ".magic.org";
+	};
+	auto parsed = QUrl(value);
+	if (parsed.isValid()) {
+		parsed.setScheme("https");
+		parsed.setHost(ChangeHost(parsed.host()));
+		if (parsed.path().isEmpty()) {
+			parsed.setPath(u"/"_q);
+		}
+		return parsed.toString();
+	}
+	const auto part = value.mid(u"tonsite://"_q.size());
+	const auto split = part.indexOf('/');
+	return "https://"
+		+ ChangeHost((split < 0) ? part : part.left(split))
+		+ ((split < 0) ? u"/"_q : part.mid(split));
+}
+
+[[nodiscard]] QString HttpsToTonsite(QString value) {
+	const auto ChangeHost = [](QString https) {
+		https.replace(".magic.org", QString());
+		https = https.replace("-d", ".");
+		https = https.replace("-h", "-");
+		return https;
+	};
+	auto parsed = QUrl(value);
+	if (parsed.isValid()) {
+		parsed.setScheme("tonsite");
+		parsed.setHost(ChangeHost(parsed.host()));
+		if (parsed.path().isEmpty()) {
+			parsed.setPath(u"/"_q);
+		}
+		return parsed.toString();
+	}
+	const auto part = value.mid(u"https://"_q.size());
+	const auto split = part.indexOf('/');
+	return "tonsite://"
+		+ ChangeHost((split < 0) ? part : part.left(split))
+		+ ((split < 0) ? u"/"_q : part.mid(split));
+}
+
 } // namespace
 
 Controller::Controller(
@@ -151,6 +196,7 @@ Controller::~Controller() {
 	_ready = false;
 	_webview = nullptr;
 	_back.destroy();
+	_forward.destroy();
 	_menu = nullptr;
 	_menuToggle.destroy();
 	_subtitle = nullptr;
@@ -168,15 +214,22 @@ void Controller::updateTitleGeometry(int newWidth) const {
 		QPainter(_subtitleWrap.get()).fillRect(clip, st::windowBg);
 	}, _subtitleWrap->lifetime());
 
-	const auto progress = _subtitleLeft.value(_back->toggled() ? 1. : 0.);
-	const auto left = anim::interpolate(
-		st::ivSubtitleLeft,
-		_back->width() + st::ivSubtitleSkip,
-		progress);
+	const auto progressBack = _subtitleBackShift.value(
+		_back->toggled() ? 1. : 0.);
+	const auto progressForward = _subtitleForwardShift.value(
+		_forward->toggled() ? 1. : 0.);
+	const auto backAdded = _back->width()
+		+ st::ivSubtitleSkip
+		- st::ivSubtitleLeft;
+	const auto forwardAdded = _forward->width();
+	const auto left = st::ivSubtitleLeft
+		+ anim::interpolate(0, backAdded, progressBack)
+		+ anim::interpolate(0, forwardAdded, progressForward);
 	_subtitle->resizeToWidth(newWidth - left - _menuToggle->width());
 	_subtitle->moveToLeft(left, st::ivSubtitleTop);
 
 	_back->moveToLeft(0, 0);
+	_forward->moveToLeft(_back->width() * progressBack, 0);
 	_menuToggle->moveToRight(0, 0);
 }
 
@@ -191,12 +244,12 @@ void Controller::initControls() {
 		_subtitleWrap.get(),
 		_subtitleText.value(),
 		st::ivSubtitle);
+	_subtitle->setSelectable(true);
 	_subtitleText.value(
 	) | rpl::start_with_next([=](const QString &subtitle) {
 		const auto prefix = tr::lng_iv_window_title(tr::now);
 		_window->setWindowTitle(prefix + ' ' + QChar(0x2014) + ' ' + subtitle);
 	}, _subtitle->lifetime());
-	_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
 
 	_menuToggle.create(_subtitleWrap.get(), st::ivMenuToggle);
 	_menuToggle->setClickedCallback([=] { showMenu(); });
@@ -206,15 +259,25 @@ void Controller::initControls() {
 		object_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivBack));
 	_back->entity()->setClickedCallback([=] {
 		if (_webview) {
-			_webview->eval("IV.back();");
+			_webview->eval("window.history.back();");
 		} else {
 			_back->hide(anim::type::normal);
 		}
 	});
+	_forward.create(
+		_subtitleWrap.get(),
+		object_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivForward));
+	_forward->entity()->setClickedCallback([=] {
+		if (_webview) {
+			_webview->eval("window.history.forward();");
+		} else {
+			_forward->hide(anim::type::normal);
+		}
+	});
 
 	_back->toggledValue(
 	) | rpl::start_with_next([=](bool toggled) {
-		_subtitleLeft.start(
+		_subtitleBackShift.start(
 			[=] { updateTitleGeometry(_window->body()->width()); },
 			toggled ? 0. : 1.,
 			toggled ? 1. : 0.,
@@ -222,7 +285,18 @@ void Controller::initControls() {
 	}, _back->lifetime());
 	_back->hide(anim::type::instant);
 
-	_subtitleLeft.stop();
+	_forward->toggledValue(
+	) | rpl::start_with_next([=](bool toggled) {
+		_subtitleForwardShift.start(
+			[=] { updateTitleGeometry(_window->body()->width()); },
+			toggled ? 0. : 1.,
+			toggled ? 1. : 0.,
+			st::fadeWrapDuration);
+	}, _forward->lifetime());
+	_forward->hide(anim::type::instant);
+
+	_subtitleBackShift.stop();
+	_subtitleForwardShift.stop();
 }
 
 void Controller::show(
@@ -254,24 +328,7 @@ void Controller::update(Prepared page) {
 void Controller::showTonSite(
 		const Webview::StorageId &storageId,
 		QString uri) {
-	const auto url = [&] {
-		auto parsed = QUrl(uri);
-		if (parsed.isValid()) {
-			auto host = parsed.host();
-			host = host.replace('-', "-h");
-			host = host.replace('.', "-d");
-			parsed.setHost(host + ".magic.org");
-			parsed.setScheme("https");
-			return parsed.toString();
-		}
-		auto part = uri.mid(u"tonsite://"_q.size());
-		const auto split = part.indexOf('/');
-		auto host = (split < 0) ? part : part.left(split);
-		host = host.replace('-', "-h");
-		host = host.replace('.', "-d");
-		part = (split < 0) ? QString() : part.mid(split);
-		return "https://" + host + ".magic.org" + part;
-	}();
+	const auto url = TonsiteToHttps(uri);
 	if (!_webview) {
 		createWebview(storageId);
 	}
@@ -281,7 +338,13 @@ void Controller::showTonSite(
 	} else {
 		_events.fire({ Event::Type::Close });
 	}
-	_subtitleText = uri;
+	_url = url;
+	_subtitleText = _url.value(
+	) | rpl::filter([=](const QString &url) {
+		return !url.isEmpty() && url != u"about:blank"_q;
+	}) | rpl::map([=](QString value) {
+		return HttpsToTonsite(value);
+	});
 	_menuToggle->hide();
 }
 
@@ -438,12 +501,12 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 				if (!script.isEmpty()) {
 					_webview->eval(script);
 				}
-			} else if (event == u"location_change"_q) {
-				_index = object.value("index").toInt();
-				_hash = object.value("hash").toString();
-				_back->toggle(
-					(object.value("position").toInt() > 0),
-					anim::type::normal);
+			//} else if (event == u"location_change"_q) {
+			//	_index = object.value("index").toInt();
+			//	_hash = object.value("hash").toString();
+			//	_back->toggle(
+			//		(object.value("position").toInt() > 0),
+			//		anim::type::normal);
 			}
 		});
 	});
@@ -524,6 +587,21 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 		return Webview::DataResult::Failed;
 	});
 
+	raw->navigationHistoryState(
+	) | rpl::start_with_next([=](Webview::NavigationHistoryState state) {
+		_back->toggle(
+			state.canGoBack || state.canGoForward,
+			anim::type::normal);
+		_forward->toggle(state.canGoForward, anim::type::normal);
+		_back->entity()->setDisabled(!state.canGoBack);
+		_back->entity()->setIconOverride(
+			state.canGoBack ? nullptr : &st::ivBackIconDisabled,
+			state.canGoBack ? nullptr : &st::ivBackIconDisabled);
+		_back->setAttribute(
+			Qt::WA_TransparentForMouseEvents,
+			!state.canGoBack);
+	}, _webview->lifetime());
+
 	raw->init(R"()");
 }
 
diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h
index 1f0ecaf9a..b732ec57a 100644
--- a/Telegram/SourceFiles/iv/iv_controller.h
+++ b/Telegram/SourceFiles/iv/iv_controller.h
@@ -127,11 +127,14 @@ private:
 
 	std::unique_ptr<Ui::RpWindow> _window;
 	std::unique_ptr<Ui::RpWidget> _subtitleWrap;
+	rpl::variable<QString> _url;
 	rpl::variable<QString> _subtitleText;
 	std::unique_ptr<Ui::FlatLabel> _subtitle;
-	Ui::Animations::Simple _subtitleLeft;
+	Ui::Animations::Simple _subtitleBackShift;
+	Ui::Animations::Simple _subtitleForwardShift;
 	object_ptr<Ui::IconButton> _menuToggle = { nullptr };
 	object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _back = { nullptr };
+	object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _forward = { nullptr };
 	base::unique_qptr<Ui::PopupMenu> _menu;
 	Ui::RpWidget *_container = nullptr;
 	std::unique_ptr<Webview::Window> _webview;
diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp
index f578bce82..69e0ef89e 100644
--- a/Telegram/SourceFiles/settings/settings_experimental.cpp
+++ b/Telegram/SourceFiles/settings/settings_experimental.cpp
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "lang/lang_keys.h"
 #include "mainwindow.h"
 #include "media/player/media_player_instance.h"
+#include "webview/platform/win/webview_windows_edge_chromium.h"
 #include "webview/webview_embed.h"
 #include "window/main_window.h"
 #include "window/window_peer_menu.h"
@@ -149,6 +150,7 @@ void SetupExperimental(
 	addToggle(Media::Player::kOptionDisableAutoplayNext);
 	addToggle(kOptionSendLargePhotos);
 	addToggle(Webview::kOptionWebviewDebugEnabled);
+	addToggle(Webview::EdgeChromium::kOptionWebviewLegacyEdge);
 	addToggle(kOptionAutoScrollInactiveChat);
 	addToggle(Window::Notifications::kOptionGNotification);
 	addToggle(Core::kOptionFreeType);
diff --git a/Telegram/lib_webview b/Telegram/lib_webview
index 363db4e49..cc8f41d10 160000
--- a/Telegram/lib_webview
+++ b/Telegram/lib_webview
@@ -1 +1 @@
-Subproject commit 363db4e49a0b78e5dd08bd922e09cf8810318c09
+Subproject commit cc8f41d10b66c1e66f16728f95f217a62d2dc7ac

From 4108debca0f671dc15d2689270bf2f92d5b9fa5a Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2024 12:40:33 +0200
Subject: [PATCH 132/163] Fix possible crash in web apps.

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

diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index a8dee5f76..b2159ebb1 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -746,8 +746,9 @@ postEvent: function(eventType, eventData) {
 		&QGuiApplication::focusWindowChanged
 	) | rpl::filter([=](QWindow *focused) {
 		const auto handle = _widget->window()->windowHandle();
-		return _webview
-			&& !_webview->window.widget()->isHidden()
+		const auto widget = _webview ? _webview->window.widget() : nullptr;
+		return widget
+			&& !widget->isHidden()
 			&& handle
 			&& (focused == handle);
 	}) | rpl::start_with_next([=] {

From 699a7bdc58fc12711f331313c94d417075c550e1 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2024 15:25:11 +0200
Subject: [PATCH 133/163] Fix possible crash in message field.

Fixes #28203.
---
 Telegram/SourceFiles/chat_helpers/message_field.cpp | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp
index 8ee5170fb..b77fe3e3c 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.cpp
+++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp
@@ -392,6 +392,9 @@ void InitMessageFieldHandlers(
 		Fn<bool()> customEmojiPaused,
 		Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji,
 		const style::InputField *fieldStyle) {
+	const auto paused = [customEmojiPaused] {
+		return customEmojiPaused && customEmojiPaused();
+	};
 	field->setTagMimeProcessor(
 		FieldTagMimeProcessor(session, allowPremiumEmoji));
 	field->setCustomTextContext([=](Fn<void()> repaint) {
@@ -399,10 +402,10 @@ void InitMessageFieldHandlers(
 			.session = session,
 			.customEmojiRepaint = std::move(repaint),
 		});
-	}, [customEmojiPaused] {
-		return On(PowerSaving::kEmojiChat) || customEmojiPaused();
-	}, [customEmojiPaused] {
-		return On(PowerSaving::kChatSpoiler) || customEmojiPaused();
+	}, [paused] {
+		return On(PowerSaving::kEmojiChat) || paused();
+	}, [paused] {
+		return On(PowerSaving::kChatSpoiler) || paused();
 	});
 	field->setInstantReplaces(Ui::InstantReplaces::Default());
 	field->setInstantReplacesEnabled(

From a25b2e9700a2d06d91e32d37bf51955eb80b819f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2024 15:53:06 +0200
Subject: [PATCH 134/163] Show webview error in Iv::Controller.

---
 Telegram/SourceFiles/iv/iv_controller.cpp     | 68 +++++++++++++++++--
 Telegram/SourceFiles/iv/iv_controller.h       |  3 +
 .../payments/ui/payments_panel.cpp            | 30 ++------
 .../settings/settings_experimental.cpp        |  3 +-
 .../ui/chat/attach/attach_bot_webview.cpp     | 36 +++++-----
 .../ui/chat/attach/attach_bot_webview.h       |  2 +
 6 files changed, 91 insertions(+), 51 deletions(-)

diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 1870011dc..487f9295c 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -11,8 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "base/invoke_queued.h"
 #include "base/qt_signal_producer.h"
 #include "base/qthelp_url.h"
+#include "core/file_utilities.h"
 #include "iv/iv_data.h"
 #include "lang/lang_keys.h"
+#include "ui/chat/attach/attach_bot_webview.h"
 #include "ui/platform/ui_platform_window_title.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
@@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/palette.h"
 #include "styles/style_iv.h"
 #include "styles/style_menu_icons.h"
+#include "styles/style_payments.h" // paymentsCriticalError
 #include "styles/style_widgets.h"
 #include "styles/style_window.h"
 
@@ -194,7 +197,7 @@ Controller::~Controller() {
 		_window->hide();
 	}
 	_ready = false;
-	_webview = nullptr;
+	base::take(_webview);
 	_back.destroy();
 	_forward.destroy();
 	_menu = nullptr;
@@ -335,8 +338,6 @@ void Controller::showTonSite(
 	if (_webview && _webview->widget()) {
 		_webview->navigate(url);
 		activate();
-	} else {
-		_events.fire({ Event::Type::Close });
 	}
 	_url = url;
 	_subtitleText = _url.value(
@@ -435,7 +436,7 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 
 	window->lifetime().add([=] {
 		_ready = false;
-		_webview = nullptr;
+		base::take(_webview);
 	});
 
 	window->events(
@@ -449,11 +450,32 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 			}
 		}
 	}, window->lifetime());
-	raw->widget()->show();
+
+	const auto widget = raw->widget();
+	if (!widget) {
+		base::take(_webview);
+		showWebviewError();
+		return;
+	}
+	widget->show();
+
+	QObject::connect(widget, &QObject::destroyed, [=] {
+		if (!_webview) {
+			// If we destroyed _webview ourselves,
+			// we don't show any message, nothing crashed.
+			return;
+		}
+		crl::on_main(window, [=] {
+			showWebviewError({ "Error: WebView has crashed." });
+		});
+		base::take(_webview);
+	});
 
 	_container->sizeValue(
 	) | rpl::start_with_next([=](QSize size) {
-		raw->widget()->setGeometry(QRect(QPoint(), size));
+		if (const auto widget = raw->widget()) {
+			widget->setGeometry(QRect(QPoint(), size));
+		}
 	}, _container->lifetime());
 
 	raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
@@ -600,11 +622,45 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 		_back->setAttribute(
 			Qt::WA_TransparentForMouseEvents,
 			!state.canGoBack);
+		_url = QString::fromStdString(state.url);
 	}, _webview->lifetime());
 
 	raw->init(R"()");
 }
 
+void Controller::showWebviewError() {
+	const auto available = Webview::Availability();
+	if (available.error != Webview::Available::Error::None) {
+		showWebviewError(Ui::BotWebView::ErrorText(available));
+	} else {
+		showWebviewError({ "Error: Could not initialize WebView." });
+	}
+}
+
+void Controller::showWebviewError(TextWithEntities text) {
+	auto error = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
+		_container,
+		object_ptr<Ui::FlatLabel>(
+			_container,
+			rpl::single(text),
+			st::paymentsCriticalError),
+		st::paymentsCriticalErrorPadding);
+	error->entity()->setClickHandlerFilter([=](
+			const ClickHandlerPtr &handler,
+			Qt::MouseButton) {
+		const auto entity = handler->getTextEntity();
+		if (entity.type != EntityType::CustomUrl) {
+			return true;
+		}
+		File::OpenUrl(entity.data);
+		return false;
+	});
+	error->show();
+	_container->sizeValue() | rpl::start_with_next([=](QSize size) {
+		error->setGeometry(0, 0, size.width(), size.height() * 2 / 3);
+	}, error->lifetime());
+}
+
 void Controller::showInWindow(
 		const Webview::StorageId &storageId,
 		Prepared page) {
diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h
index b732ec57a..c92f4c4b2 100644
--- a/Telegram/SourceFiles/iv/iv_controller.h
+++ b/Telegram/SourceFiles/iv/iv_controller.h
@@ -123,6 +123,9 @@ private:
 	void showShareMenu();
 	void destroyShareMenu();
 
+	void showWebviewError();
+	void showWebviewError(TextWithEntities text);
+
 	const not_null<Delegate*> _delegate;
 
 	std::unique_ptr<Ui::RpWindow> _window;
diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp
index d97628285..2bc023e8c 100644
--- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp
+++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/checkbox.h"
 #include "ui/wrap/fade_wrap.h"
 #include "ui/boxes/single_choice_box.h"
+#include "ui/chat/attach/attach_bot_webview.h"
 #include "ui/text/format_values.h"
 #include "ui/text/text_utilities.h"
 #include "ui/effects/radial_animation.h"
@@ -908,32 +909,9 @@ std::shared_ptr<Show> Panel::uiShow() {
 void Panel::showWebviewError(
 		const QString &text,
 		const Webview::Available &information) {
-	using Error = Webview::Available::Error;
-	Expects(information.error != Error::None);
-
-	auto rich = TextWithEntities{ text };
-	rich.append("\n\n");
-	switch (information.error) {
-	case Error::NoWebview2: {
-		rich.append(tr::lng_payments_webview_install_edge(
-			tr::now,
-			lt_link,
-			Text::Link(
-				"Microsoft Edge WebView2 Runtime",
-				"https://go.microsoft.com/fwlink/p/?LinkId=2124703"),
-			Ui::Text::WithEntities));
-	} break;
-	case Error::NoWebKitGTK:
-		rich.append(tr::lng_payments_webview_install_webkit(tr::now));
-		break;
-	case Error::OldWindows:
-		rich.append(tr::lng_payments_webview_update_windows(tr::now));
-		break;
-	default:
-		rich.append(QString::fromStdString(information.details));
-		break;
-	}
-	showCriticalError(rich);
+	showCriticalError(TextWithEntities{ text }.append(
+		"\n\n"
+	).append(BotWebView::ErrorText(information)));
 }
 
 void Panel::updateThemeParams(const Webview::ThemeParams &params) {
diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp
index 69e0ef89e..c17eda5b8 100644
--- a/Telegram/SourceFiles/settings/settings_experimental.cpp
+++ b/Telegram/SourceFiles/settings/settings_experimental.cpp
@@ -24,7 +24,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "lang/lang_keys.h"
 #include "mainwindow.h"
 #include "media/player/media_player_instance.h"
-#include "webview/platform/win/webview_windows_edge_chromium.h"
 #include "webview/webview_embed.h"
 #include "window/main_window.h"
 #include "window/window_peer_menu.h"
@@ -150,7 +149,7 @@ void SetupExperimental(
 	addToggle(Media::Player::kOptionDisableAutoplayNext);
 	addToggle(kOptionSendLargePhotos);
 	addToggle(Webview::kOptionWebviewDebugEnabled);
-	addToggle(Webview::EdgeChromium::kOptionWebviewLegacyEdge);
+	addToggle(Webview::kOptionWebviewLegacyEdge);
 	addToggle(kOptionAutoScrollInactiveChat);
 	addToggle(Window::Notifications::kOptionGNotification);
 	addToggle(Core::kOptionFreeType);
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
index b2159ebb1..7b57464f7 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
@@ -1401,32 +1401,34 @@ if (window.TelegramGameProxy) {
 )");
 }
 
-void Panel::showWebviewError(
-		const QString &text,
-		const Webview::Available &information) {
-	using Error = Webview::Available::Error;
-	Expects(information.error != Error::None);
+TextWithEntities ErrorText(const Webview::Available &info) {
+	Expects(info.error != Webview::Available::Error::None);
 
-	auto rich = TextWithEntities{ text };
-	rich.append("\n\n");
-	switch (information.error) {
-	case Error::NoWebview2: {
-		rich.append(tr::lng_payments_webview_install_edge(
+	using Error = Webview::Available::Error;
+	switch (info.error) {
+	case Error::NoWebview2:
+		return tr::lng_payments_webview_install_edge(
 			tr::now,
 			lt_link,
 			Text::Link(
 				"Microsoft Edge WebView2 Runtime",
 				"https://go.microsoft.com/fwlink/p/?LinkId=2124703"),
-			Ui::Text::WithEntities));
-	} break;
+			Ui::Text::WithEntities);
 	case Error::NoWebKitGTK:
-		rich.append(tr::lng_payments_webview_install_webkit(tr::now));
-		break;
+		return { tr::lng_payments_webview_install_webkit(tr::now) };
+	case Error::OldWindows:
+		return { tr::lng_payments_webview_update_windows(tr::now) };
 	default:
-		rich.append(QString::fromStdString(information.details));
-		break;
+		return { QString::fromStdString(info.details) };
 	}
-	showCriticalError(rich);
+}
+
+void Panel::showWebviewError(
+		const QString &text,
+		const Webview::Available &information) {
+	showCriticalError(TextWithEntities{ text }.append(
+		"\n\n"
+	).append(ErrorText(information)));
 }
 
 rpl::lifetime &Panel::lifetime() {
diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
index e25483b89..f687c24e6 100644
--- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
+++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
@@ -30,6 +30,8 @@ struct Available;
 
 namespace Ui::BotWebView {
 
+[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info);
+
 struct MainButtonArgs {
 	bool isActive = false;
 	bool isVisible = false;

From dac4389e371e001c89a790de6a10c96d7d774dbb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2024 18:37:57 +0300
Subject: [PATCH 135/163] Fix build on Linux.

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

diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp
index d03a73641..a2e36038a 100644
--- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp
@@ -124,7 +124,7 @@ void GiftCreditsBox(
 		0,
 		[=] { gifted(); box->uiShow()->hideLayer(); });
 
-	const auto bottom = box->setPinnedToBottomContent(
+	box->setPinnedToBottomContent(
 		object_ptr<Ui::VerticalLayout>(box));
 }
 

From fb9ce6d3a877f4773b40bb8d5c67eeec920fb887 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2024 18:38:17 +0300
Subject: [PATCH 136/163] Provide canGo[Back|Forward] for tonsite-s.

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

diff --git a/Telegram/lib_webview b/Telegram/lib_webview
index cc8f41d10..2c95b169b 160000
--- a/Telegram/lib_webview
+++ b/Telegram/lib_webview
@@ -1 +1 @@
-Subproject commit cc8f41d10b66c1e66f16728f95f217a62d2dc7ac
+Subproject commit 2c95b169b30035a6e85d0f5534c4477adff393e2

From bb6c94ef4f1623b4c19b5f48bc67e2e046f51f2b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Jul 2024 19:19:30 +0200
Subject: [PATCH 137/163] Handle tonsite:// links from the system.

---
 Telegram/Resources/uwp/AppX/AppxManifest.xml |  3 ++
 Telegram/SourceFiles/core/application.cpp    | 38 ++++++++++++++------
 Telegram/SourceFiles/iv/iv_controller.cpp    | 10 +++---
 Telegram/SourceFiles/iv/iv_instance.cpp      |  3 +-
 Telegram/Telegram.plist                      |  1 +
 5 files changed, 37 insertions(+), 18 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 45ec14889..3d4040dd7 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -37,6 +37,9 @@
       <Extensions>
         <uap3:Extension Category="windows.protocol">
           <uap3:Protocol Name="tg" Parameters="-- &quot;%1&quot;" />
+        <uap3:Extension Category="windows.protocol">
+        </uap3:Extension>
+          <uap3:Protocol Name="tonsite" Parameters="-- &quot;%1&quot;" />
         </uap3:Extension>
         <desktop:Extension
           Category="windows.startupTask"
diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index b2c2ec4db..94fa2ae4d 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -683,7 +683,8 @@ bool Application::eventFilter(QObject *object, QEvent *e) {
 			if (const auto file = event->file(); !file.isEmpty()) {
 				_filesToOpen.append(file);
 				_fileOpenTimer.callOnce(kFileOpenTimeoutMs);
-			} else if (event->url().scheme() == u"tg"_q) {
+			} else if (event->url().scheme() == u"tg"_q
+				|| event->url().scheme() == u"tonsite"_q) {
 				const auto url = QString::fromUtf8(
 					event->url().toEncoded().trimmed());
 				cSetStartUrl(url.mid(0, 8192));
@@ -1084,13 +1085,17 @@ void Application::checkSendPaths() {
 }
 
 void Application::checkStartUrl() {
-	if (!cStartUrl().isEmpty()
-		&& _lastActivePrimaryWindow
-		&& !_lastActivePrimaryWindow->locked()) {
+	if (!cStartUrl().isEmpty()) {
 		const auto url = cStartUrl();
-		cSetStartUrl(QString());
-		if (!openLocalUrl(url, {})) {
-			cSetStartUrl(url);
+		if (url.startsWith("tonsite://", Qt::CaseInsensitive)) {
+			cSetStartUrl(QString());
+			iv().showTonSite(url, {});
+		} else if (_lastActivePrimaryWindow
+			&& !_lastActivePrimaryWindow->locked()) {
+			cSetStartUrl(QString());
+			if (!openLocalUrl(url, {})) {
+				cSetStartUrl(url);
+			}
 		}
 	}
 }
@@ -1798,11 +1803,13 @@ void Application::startShortcuts() {
 }
 
 void Application::RegisterUrlScheme() {
+	const auto arguments = Launcher::Instance().customWorkingDir()
+		? u"-workdir \"%1\""_q.arg(cWorkingDir())
+		: QString();
+
 	base::Platform::RegisterUrlScheme(base::Platform::UrlSchemeDescriptor{
 		.executable = Platform::ExecutablePathForShortcuts(),
-		.arguments = Launcher::Instance().customWorkingDir()
-			? u"-workdir \"%1\""_q.arg(cWorkingDir())
-			: QString(),
+		.arguments = arguments,
 		.protocol = u"tg"_q,
 		.protocolName = u"Telegram Link"_q,
 		.shortAppName = u"tdesktop"_q,
@@ -1810,6 +1817,17 @@ void Application::RegisterUrlScheme() {
 		.displayAppName = AppName.utf16(),
 		.displayAppDescription = AppName.utf16(),
 	});
+
+	base::Platform::RegisterUrlScheme(base::Platform::UrlSchemeDescriptor{
+		.executable = Platform::ExecutablePathForShortcuts(),
+		.arguments = arguments,
+		.protocol = u"tonsite"_q,
+		.protocolName = u"TonSite Link"_q,
+		.shortAppName = u"tdesktop"_q,
+		.longAppName = QCoreApplication::applicationName(),
+		.displayAppName = AppName.utf16(),
+		.displayAppDescription = AppName.utf16(),
+	});
 }
 
 bool IsAppLaunched() {
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 487f9295c..00b645505 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -523,12 +523,10 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 				if (!script.isEmpty()) {
 					_webview->eval(script);
 				}
-			//} else if (event == u"location_change"_q) {
-			//	_index = object.value("index").toInt();
-			//	_hash = object.value("hash").toString();
-			//	_back->toggle(
-			//		(object.value("position").toInt() > 0),
-			//		anim::type::normal);
+			} else if (event == u"location_change"_q) {
+				_index = object.value("index").toInt();
+				_hash = object.value("hash").toString();
+				_webview->refreshNavigationHistoryState();
 			}
 		});
 	});
diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp
index 480d73573..3b327e5a6 100644
--- a/Telegram/SourceFiles/iv/iv_instance.cpp
+++ b/Telegram/SourceFiles/iv/iv_instance.cpp
@@ -1107,8 +1107,7 @@ void Instance::showTonSite(
 		case Type::OpenPage:
 		case Type::OpenLink:
 			if (urlChecked) {
-				File::OpenUrl(event.url);
-				closeAll();
+				UrlClickHandler::Open(event.url);
 			} else if (tonsite) {
 				showTonSite(event.url);
 			}
diff --git a/Telegram/Telegram.plist b/Telegram/Telegram.plist
index b68e2109c..72913e85e 100644
--- a/Telegram/Telegram.plist
+++ b/Telegram/Telegram.plist
@@ -38,6 +38,7 @@
 			<key>CFBundleURLSchemes</key>
 			<array>
 				<string>tg</string>
+				<string>tonsite</string>
 			</array>
 		</dict>
 	</array>

From 8959679b3cfe29f756cb7437ff6fd4eeeab05987 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 13:05:54 +0200
Subject: [PATCH 138/163] Make IV internal links bg semi-transparent.

---
 Telegram/SourceFiles/iv/iv_controller.cpp   |  9 ++-
 Telegram/SourceFiles/ui/webview_helpers.cpp | 66 +++++++++++++++++----
 Telegram/SourceFiles/ui/webview_helpers.h   |  4 ++
 3 files changed, 64 insertions(+), 15 deletions(-)

diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 00b645505..e369ab5c0 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -65,7 +65,7 @@ namespace {
 		{ "box-divider-bg", &st::boxDividerBg },
 		{ "box-divider-fg", &st::boxDividerFg },
 		{ "light-button-fg", &st::lightButtonFg },
-		{ "light-button-bg-over", &st::lightButtonBgOver },
+		//{ "light-button-bg-over", &st::lightButtonBgOver },
 		{ "menu-icon-fg", &st::menuIconFg },
 		{ "menu-icon-fg-over", &st::menuIconFgOver },
 		{ "menu-bg", &st::menuBg },
@@ -82,7 +82,12 @@ namespace {
 	static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
 		{ "iv-join-channel", tr::lng_iv_join_channel },
 	};
-	return Ui::ComputeStyles(map, phrases);
+	return Ui::ComputeStyles(map, phrases)
+		+ ';'
+		+ Ui::ComputeSemiTransparentOverStyle(
+			"light-button-bg-over",
+			st::lightButtonBgOver,
+			st::windowBg);
 }
 
 [[nodiscard]] QByteArray WrapPage(const Prepared &page) {
diff --git a/Telegram/SourceFiles/ui/webview_helpers.cpp b/Telegram/SourceFiles/ui/webview_helpers.cpp
index 24b7155ce..3be7a6359 100644
--- a/Telegram/SourceFiles/ui/webview_helpers.cpp
+++ b/Telegram/SourceFiles/ui/webview_helpers.cpp
@@ -10,24 +10,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "lang/lang_keys.h"
 
 namespace Ui {
+namespace {
 
-[[nodiscard]] QByteArray ComputeStyles(
+[[nodiscard]] QByteArray Serialize(const QColor &qt) {
+	if (qt.alpha() == 255) {
+		return '#'
+			+ QByteArray::number(qt.red(), 16).right(2)
+			+ QByteArray::number(qt.green(), 16).right(2)
+			+ QByteArray::number(qt.blue(), 16).right(2);
+	}
+	return "rgba("
+		+ QByteArray::number(qt.red()) + ","
+		+ QByteArray::number(qt.green()) + ","
+		+ QByteArray::number(qt.blue()) + ","
+		+ QByteArray::number(qt.alpha() / 255.) + ")";
+}
+
+} // namespace
+
+QByteArray ComputeStyles(
 		const base::flat_map<QByteArray, const style::color*> &colors,
 		const base::flat_map<QByteArray, tr::phrase<>> &phrases,
 		bool nightTheme) {
 	static const auto serialize = [](const style::color *color) {
-		const auto qt = (*color)->c;
-		if (qt.alpha() == 255) {
-			return '#'
-				+ QByteArray::number(qt.red(), 16).right(2)
-				+ QByteArray::number(qt.green(), 16).right(2)
-				+ QByteArray::number(qt.blue(), 16).right(2);
-		}
-		return "rgba("
-			+ QByteArray::number(qt.red()) + ","
-			+ QByteArray::number(qt.green()) + ","
-			+ QByteArray::number(qt.blue()) + ","
-			+ QByteArray::number(qt.alpha() / 255.) + ")";
+		return Serialize((*color)->c);
 	};
 	static const auto escape = [](tr::phrase<> phrase) {
 		const auto text = phrase(tr::now);
@@ -63,6 +69,40 @@ namespace Ui {
 	return result;
 }
 
+QByteArray ComputeSemiTransparentOverStyle(
+		const QByteArray &name,
+		const style::color &over,
+		const style::color &bg) {
+	const auto result = [&](const QColor &c) {
+		return "--td-"_q + name + ':' + Serialize(c) + ';';
+	};
+	if (over->c.alpha() < 255) {
+		return result(over->c);
+	}
+	// The most transparent color that will still give the same result.
+	const auto r0 = bg->c.red();
+	const auto g0 = bg->c.green();
+	const auto b0 = bg->c.blue();
+	const auto r1 = over->c.red();
+	const auto g1 = over->c.green();
+	const auto b1 = over->c.blue();
+	const auto mina = [](int c0, int c1) {
+		return (c0 == c1)
+			? 0
+			: (c0 > c1)
+			? (((c0 - c1) * 255) / c0)
+			: (((c1 - c0) * 255) / (255 - c0));
+	};
+	const auto rmina = mina(r0, r1);
+	const auto gmina = mina(g0, g1);
+	const auto bmina = mina(b0, b1);
+	const auto a = std::max({ rmina, gmina, bmina });
+	const auto r = (r1 * 255 - r0 * (255 - a)) / a;
+	const auto g = (g1 * 255 - g0 * (255 - a)) / a;
+	const auto b = (b1 * 255 - b0 * (255 - a)) / a;
+	return result(QColor(r, g, b, a));
+}
+
 QByteArray EscapeForAttribute(QByteArray value) {
 	return value
 		.replace('&', "&amp;")
diff --git a/Telegram/SourceFiles/ui/webview_helpers.h b/Telegram/SourceFiles/ui/webview_helpers.h
index 3d8a51066..518096479 100644
--- a/Telegram/SourceFiles/ui/webview_helpers.h
+++ b/Telegram/SourceFiles/ui/webview_helpers.h
@@ -20,6 +20,10 @@ namespace Ui {
 	const base::flat_map<QByteArray, const style::color*> &colors,
 	const base::flat_map<QByteArray, tr::phrase<>> &phrases,
 	bool nightTheme = false);
+[[nodiscard]] QByteArray ComputeSemiTransparentOverStyle(
+	const QByteArray &name,
+	const style::color &over,
+	const style::color &bg);
 
 [[nodiscard]] QByteArray EscapeForAttribute(QByteArray value);
 [[nodiscard]] QByteArray EscapeForScriptString(QByteArray value);

From 76314e3c03267dc06b9c6d4cd63894f0ffdd0bbb Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 13:43:55 +0200
Subject: [PATCH 139/163] Close additional windows on passcode lock.

---
 Telegram/SourceFiles/core/application.cpp | 40 ++++++++++++++---------
 Telegram/SourceFiles/core/application.h   |  1 +
 2 files changed, 25 insertions(+), 16 deletions(-)

diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index 94fa2ae4d..39b7ac32f 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -188,8 +188,11 @@ Application::Application()
 	_platformIntegration->init();
 
 	passcodeLockChanges(
-	) | rpl::start_with_next([=] {
+	) | rpl::start_with_next([=](bool locked) {
 		_shouldLockAt = 0;
+		if (locked) {
+			closeAdditionalWindows();
+		}
 	}, _lifetime);
 
 	passcodeLockChanges(
@@ -211,6 +214,16 @@ Application::Application()
 	}, _lifetime);
 }
 
+void Application::closeAdditionalWindows() {
+	Payments::CheckoutProcess::ClearAll();
+	for (const auto &[index, account] : _domain->accounts()) {
+		if (account->sessionExists()) {
+			account->session().attachWebView().closeAll();
+		}
+	}
+	_iv->closeAll();
+}
+
 Application::~Application() {
 	if (_saveSettingsTimer && _saveSettingsTimer->isActive()) {
 		Local::writeSettings();
@@ -230,13 +243,7 @@ Application::~Application() {
 	//
 	// For example Domain::removeRedundantAccounts() is called from
 	// Domain::finish() and there is a violation on Ensures(started()).
-	Payments::CheckoutProcess::ClearAll();
-	for (const auto &[index, account] : _domain->accounts()) {
-		if (account->sessionExists()) {
-			account->session().attachWebView().closeAll();
-		}
-	}
-	_iv->closeAll();
+	closeAdditionalWindows();
 
 	_domain->finish();
 
@@ -1087,14 +1094,15 @@ void Application::checkSendPaths() {
 void Application::checkStartUrl() {
 	if (!cStartUrl().isEmpty()) {
 		const auto url = cStartUrl();
-		if (url.startsWith("tonsite://", Qt::CaseInsensitive)) {
-			cSetStartUrl(QString());
-			iv().showTonSite(url, {});
-		} else if (_lastActivePrimaryWindow
-			&& !_lastActivePrimaryWindow->locked()) {
-			cSetStartUrl(QString());
-			if (!openLocalUrl(url, {})) {
-				cSetStartUrl(url);
+		if (!Core::App().passcodeLocked()) {
+			if (url.startsWith("tonsite://", Qt::CaseInsensitive)) {
+				cSetStartUrl(QString());
+				iv().showTonSite(url, {});
+			} else if (_lastActivePrimaryWindow) {
+				cSetStartUrl(QString());
+				if (!openLocalUrl(url, {})) {
+					cSetStartUrl(url);
+				}
 			}
 		}
 	}
diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h
index eca07e751..373ba3546 100644
--- a/Telegram/SourceFiles/core/application.h
+++ b/Telegram/SourceFiles/core/application.h
@@ -378,6 +378,7 @@ private:
 
 	void showOpenGLCrashNotification();
 	void clearPasscodeLock();
+	void closeAdditionalWindows();
 
 	bool openCustomUrl(
 		const QString &protocol,

From cf896aeb13a28141b3a2866495b4f1e964749d08 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 13:44:07 +0200
Subject: [PATCH 140/163] Improve focusing of shown layers.

---
 Telegram/SourceFiles/window/section_widget.cpp | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp
index 4ec94b0ab..5a72a6984 100644
--- a/Telegram/SourceFiles/window/section_widget.cpp
+++ b/Telegram/SourceFiles/window/section_widget.cpp
@@ -457,7 +457,11 @@ void SectionWidget::showFinished() {
 	showChildren();
 	showFinishedHook();
 
-	controller()->widget()->setInnerFocus();
+	if (isAncestorOf(window()->focusWidget())) {
+		setInnerFocus();
+	} else {
+		controller()->widget()->setInnerFocus();
+	}
 }
 
 rpl::producer<int> SectionWidget::desiredHeight() const {

From db80096e6b5d9b839d29abcabc6ad56e68884003 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 14:08:18 +0200
Subject: [PATCH 141/163] Use corporate sign certificate identity.

---
 Telegram/build/build.sh   | 2 +-
 Telegram/build/updates.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/build/build.sh b/Telegram/build/build.sh
index 08bd287ad..aa2dcc1ec 100755
--- a/Telegram/build/build.sh
+++ b/Telegram/build/build.sh
@@ -327,7 +327,7 @@ if [ "$BuildTarget" == "mac" ] || [ "$BuildTarget" == "macstore" ]; then
 
     echo "Signing the application.."
     if [ "$BuildTarget" == "mac" ]; then
-      codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: John Preston" "$ReleasePath/$BundleName" --entitlements "$HomePath/Telegram/Telegram.entitlements"
+      codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: Telegram FZ-LLC (C67CF9S4VU)" "$ReleasePath/$BundleName" --entitlements "$HomePath/Telegram/Telegram.entitlements"
     elif [ "$BuildTarget" == "macstore" ]; then
       codesign --force --timestamp --options runtime --sign "3rd Party Mac Developer Application: Telegram FZ-LLC (C67CF9S4VU)" "$ReleasePath/$BundleName/Contents/Frameworks/Breakpad.framework/Versions/A/Resources/breakpadUtilities.dylib" --entitlements "$HomePath/Telegram/Breakpad.entitlements"
       codesign --force --deep --timestamp --options runtime --sign "3rd Party Mac Developer Application: Telegram FZ-LLC (C67CF9S4VU)" "$ReleasePath/$BundleName" --entitlements "$HomePath/Telegram/Telegram Lite.entitlements"
diff --git a/Telegram/build/updates.py b/Telegram/build/updates.py
index 7a05070e7..f6f701eda 100644
--- a/Telegram/build/updates.py
+++ b/Telegram/build/updates.py
@@ -81,7 +81,7 @@ if building:
         if result != 0:
             finish(1, 'While stripping Telegram.')
 
-        result = subprocess.call('codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: John Preston" Telegram.app --entitlements "../../Telegram/Telegram/Telegram.entitlements"', shell=True)
+        result = subprocess.call('codesign --force --deep --timestamp --options runtime --sign "Developer ID Application: Telegram FZ-LLC (C67CF9S4VU)" Telegram.app --entitlements "../../Telegram/Telegram/Telegram.entitlements"', shell=True)
         if result != 0:
             finish(1, 'While signing Telegram.')
 

From 813d0501dafde8da484c6f324ddbbfff51ea0f4c Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 17:03:50 +0200
Subject: [PATCH 142/163] Fix build on Windows.

---
 Telegram/SourceFiles/boxes/sticker_set_box.cpp            | 8 ++++----
 .../SourceFiles/chat_helpers/stickers_list_widget.cpp     | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
index aef0dd941..d9d3b9a6f 100644
--- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp
+++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp
@@ -1476,7 +1476,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
 	sticker->paintRequest(
 	) | rpl::start_with_next([=] {
 		auto p = Painter(sticker);
-		if (const auto strong = weak.get()) {
+		if (const auto strong = weak.data()) {
 			const auto paused = On(PowerSaving::kStickersPanel)
 				|| show->paused(ChatHelpers::PauseReason::Layer);
 			paintSticker(p, index, QPoint(), paused, crl::now());
@@ -1530,14 +1530,14 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
 					Data::StickersType::Stickers);
 			}, [](const auto &) {
 			});
-			if (const auto strong = weak.get()) {
+			if (const auto strong = weak.data()) {
 				applySet(result);
 			}
-			if (const auto strongBox = weakBox.get()) {
+			if (const auto strongBox = weakBox.data()) {
 				strongBox->closeBox();
 			}
 		}).fail([=](const MTP::Error &error) {
-			if (const auto strongBox = weakBox.get()) {
+			if (const auto strongBox = weakBox.data()) {
 				strongBox->uiShow()->showToast(error.type());
 			}
 		}).send();
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
index 4258edbac..e1e40cc62 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
@@ -1735,7 +1735,7 @@ void StickersListWidget::showStickerSetBox(
 			document->owner().stickers().updated(
 				Data::StickersType::Stickers)
 		) | rpl::start_with_next([=, weak = Ui::MakeWeak(this)] {
-			if (weak.get()) {
+			if (weak.data()) {
 				showStickerSetBox(document, setId);
 			}
 			lifetime->destroy();

From a422aec99a08044b29606d904551fbbe76af4a37 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 18:59:13 +0200
Subject: [PATCH 143/163] Fix cancel of bot app confirm.

---
 Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index 5810310fa..e5c9ac6e3 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -787,6 +787,10 @@ void WebViewInstance::confirmAppOpen(
 			done((*allowed) && (*allowed)->checked());
 			close();
 		};
+		const auto cancelled = [=](Fn<void()> close) {
+			botClose();
+			close();
+		};
 		Ui::ConfirmBox(box, {
 			tr::lng_allow_bot_webview(
 				tr::now,
@@ -794,7 +798,7 @@ void WebViewInstance::confirmAppOpen(
 				Ui::Text::Bold(_bot->name()),
 				Ui::Text::RichLangValue),
 			crl::guard(this, callback),
-			crl::guard(this, [=] { botClose(); }),
+			crl::guard(this, cancelled),
 		});
 		if (writeAccess) {
 			(*allowed) = box->addRow(

From 148690d8b1a2ea1c65ee2e7311388370ef0920c8 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 18:59:31 +0200
Subject: [PATCH 144/163] Open .ton as a tonsite.

---
 Telegram/SourceFiles/core/click_handler_types.cpp |  4 +++-
 Telegram/SourceFiles/core/local_url_handlers.cpp  |  7 +++++++
 Telegram/SourceFiles/iv/iv_controller.cpp         | 10 +++++++---
 Telegram/lib_ui                                   |  2 +-
 4 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp
index 915b12f4f..b1ba3d166 100644
--- a/Telegram/SourceFiles/core/click_handler_types.cpp
+++ b/Telegram/SourceFiles/core/click_handler_types.cpp
@@ -122,7 +122,9 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
 			return result;
 		}()));
 	} else {
-		const auto parsedUrl = QUrl::fromUserInput(url);
+		const auto parsedUrl = url.startsWith(u"tonsite://"_q)
+			? QUrl(url)
+			: QUrl::fromUserInput(url);
 		if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) {
 			const auto my = context.value<ClickHandlerContext>();
 			if (!my.show) {
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index 6777464c1..28aa99822 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -1337,6 +1337,13 @@ QString TryConvertUrlToLocal(QString url) {
 
 	using namespace qthelp;
 	auto matchOptions = RegExOption::CaseInsensitive;
+	auto tonsiteMatch = (url.indexOf(u".ton") >= 0)
+		? regex_match(u"^(https?://)?[^/@:]+\\.ton($|/)"_q, url, matchOptions)
+		: RegularExpressionMatch(QRegularExpressionMatch());
+	if (tonsiteMatch) {
+		const auto protocol = tonsiteMatch->captured(1);
+		return u"tonsite://"_q + url.mid(protocol.size());
+	}
 	auto subdomainMatch = regex_match(u"^(https?://)?([a-zA-Z0-9\\_]+)\\.t\\.me(/\\d+)?/?(\\?.+)?"_q, url, matchOptions);
 	if (subdomainMatch) {
 		const auto name = subdomainMatch->captured(2);
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index e369ab5c0..067e90b48 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -166,12 +166,16 @@ namespace {
 	};
 	auto parsed = QUrl(value);
 	if (parsed.isValid()) {
+		const auto host = ChangeHost(parsed.host());
+		const auto emptyPath = parsed.path().isEmpty();
 		parsed.setScheme("tonsite");
-		parsed.setHost(ChangeHost(parsed.host()));
-		if (parsed.path().isEmpty()) {
+		parsed.setHost(host);
+		if (emptyPath) {
 			parsed.setPath(u"/"_q);
 		}
-		return parsed.toString();
+		if (parsed.isValid()) {
+			return parsed.toString();
+		}
 	}
 	const auto part = value.mid(u"https://"_q.size());
 	const auto split = part.indexOf('/');
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 97bfa6cef..8db5d1aa5 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 97bfa6cef474b3b311a178ff1a1042d09972a7c7
+Subproject commit 8db5d1aa533334c75ed2598ecf3607768ae9b418

From 0b6bd7075ae82aa05cb05e769855e9a507e327d9 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 19:06:47 +0200
Subject: [PATCH 145/163] Version 5.3.

- View recent and popular web apps in chats search.
- Open several web apps in different windows.
- Gift Telegram Stars to your friends.
- Send location marks and venues.
- Open tonsite:// links in webview.
- Edit order of stickers in your packs.
---
 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/build/version                       | 12 ++++++------
 changelog.txt                                |  9 +++++++++
 6 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 3d4040dd7..cffa28fb5 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="5.2.6.0" />
+    Version="5.3.0.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 83eecf0a7..b9e1fb916 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,6,0
- PRODUCTVERSION 5,2,6,0
+ FILEVERSION 5,3,0,0
+ PRODUCTVERSION 5,3,0,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "5.2.6.0"
+            VALUE "FileVersion", "5.3.0.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.6.0"
+            VALUE "ProductVersion", "5.3.0.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 40bb30f11..0f70bef0e 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,2,6,0
- PRODUCTVERSION 5,2,6,0
+ FILEVERSION 5,3,0,0
+ PRODUCTVERSION 5,3,0,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", "5.2.6.0"
+            VALUE "FileVersion", "5.3.0.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.2.6.0"
+            VALUE "ProductVersion", "5.3.0.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 111881fd4..bb5f9b405 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 = 5002006;
-constexpr auto AppVersionStr = "5.2.6";
-constexpr auto AppBetaVersion = true;
+constexpr auto AppVersion = 5003000;
+constexpr auto AppVersionStr = "5.3";
+constexpr auto AppBetaVersion = false;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index f10750e38..4829ec4b2 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         5002006
-AppVersionStrMajor 5.2
-AppVersionStrSmall 5.2.6
-AppVersionStr      5.2.6
-BetaChannel        1
+AppVersion         5003000
+AppVersionStrMajor 5.3
+AppVersionStrSmall 5.3
+AppVersionStr      5.3.0
+BetaChannel        0
 AlphaVersion       0
-AppVersionOriginal 5.2.6.beta
+AppVersionOriginal 5.3
diff --git a/changelog.txt b/changelog.txt
index 5ec213f7e..f8929141f 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,12 @@
+5.3 (31.07.24)
+
+- View recent and popular web apps in chats search.
+- Open several web apps in different windows.
+- Gift Telegram Stars to your friends.
+- Send location marks and venues.
+- Open tonsite:// links in webview.
+- Edit order of stickers in your packs.
+
 5.2.6 beta (29.07.24)
 
 - Fix launching on X11. (Linux)

From b7c14f17a7b42272d164834af5e82d33cfab46c6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Wed, 31 Jul 2024 23:06:39 +0200
Subject: [PATCH 146/163] Version 5.3: Fix build with Xcode.

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

diff --git a/Telegram/lib_webview b/Telegram/lib_webview
index 2c95b169b..4b2399232 160000
--- a/Telegram/lib_webview
+++ b/Telegram/lib_webview
@@ -1 +1 @@
-Subproject commit 2c95b169b30035a6e85d0f5534c4477adff393e2
+Subproject commit 4b239923219d6a186b96de913506a0777e86c68e

From 503c3c7b00409acb4b76893a40b07f11fdf4474b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 00:20:34 +0300
Subject: [PATCH 147/163] Version 5.3: Update cmake_helpers submodule.

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

diff --git a/cmake b/cmake
index 17c758e2b..721383d0b 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 17c758e2b9f09a58f14582bde3561873ce026039
+Subproject commit 721383d0b901cc5d5cbeec29386671d145f5a1e5

From 993c0ee64817597b0cdaa09e394ab086c9bd61a0 Mon Sep 17 00:00:00 2001
From: Ilya Fedin <fedin-ilja2010@ya.ru>
Date: Thu, 1 Aug 2024 04:49:52 +0400
Subject: [PATCH 148/163] Ensure fake modal widget is a window

---
 Telegram/SourceFiles/platform/linux/specific_linux.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
index 32437fb77..b49cef3bb 100644
--- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp
@@ -109,6 +109,7 @@ void PortalAutostart(bool enabled, Fn<void(bool)> done) {
 
 			auto &raw = **window;
 			raw.setAttribute(Qt::WA_DontShowOnScreen);
+			raw.setWindowFlag(Qt::Window);
 			raw.setWindowModality(Qt::WindowModal);
 			raw.show();
 

From 7c1510b6112d07cf0ff2e87720e2269fb82e5294 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 12:55:23 +0200
Subject: [PATCH 149/163] Fix crash on empty gift receivers list.

---
 Telegram/SourceFiles/boxes/gift_premium_box.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index 3b8f6f1b5..d478a7c6e 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -1014,6 +1014,7 @@ void GiftPremiumValidator::showChoosePeerBox(const QString &ref) {
 				if (users.empty()) {
 					show->showToast(
 						tr::lng_settings_gift_premium_choose(tr::now));
+					return;
 				}
 				const auto giftBox = show->show(
 					Box(GiftsBox, _controller, users, api, ref));

From 6b96466c5e7b79bdd2fabd4fbd822739d49e1987 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 12:55:34 +0200
Subject: [PATCH 150/163] Fix possible crash in forward preview.

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

diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
index a0d3d6066..ee24c7eb4 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
@@ -370,7 +370,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
 				userpicTop,
 				width(),
 				st::msgPhotoSize);
-		} else if (const auto info = item->originalHiddenSenderInfo()) {
+		} else if (const auto info = item->displayHiddenSenderInfo()) {
 			if (info->customUserpic.empty()) {
 				info->emptyUserpic.paintCircle(
 					p,

From 51fc104c60efe0e697af8f0c75098a218b067753 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 13:28:03 +0200
Subject: [PATCH 151/163] Show bot active users count in status.

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

diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp
index e4694f88d..a0578000f 100644
--- a/Telegram/SourceFiles/data/data_peer_values.cpp
+++ b/Telegram/SourceFiles/data/data_peer_values.cpp
@@ -53,6 +53,12 @@ std::optional<QString> OnlineTextSpecial(not_null<UserData*> user) {
 	} else if (user->isSupport()) {
 		return tr::lng_status_support(tr::now);
 	} else if (user->isBot()) {
+		if (const auto count = user->botInfo->activeUsers) {
+			return tr::lng_bot_status_users(
+				tr::now,
+				lt_count_decimal,
+				count);
+		}
 		return tr::lng_status_bot(tr::now);
 	} else if (user->isServiceUser()) {
 		return tr::lng_status_support(tr::now);

From 7f3dc27aa9207aa869c76176763b93988cab85af Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 13:28:25 +0200
Subject: [PATCH 152/163] Allow removing mini apps from recents.

---
 .../dialogs/ui/dialogs_suggestions.cpp        | 53 +++++++++++++++----
 1 file changed, 43 insertions(+), 10 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
index 18ae94b37..a1391e3ba 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
@@ -166,16 +166,17 @@ void FillEntryMenu(
 		.icon = &st::menuIconDeleteAttention,
 		.isAttention = true,
 	});
-
-	add({
-		.text = descriptor.removeAllText,
-		.handler = RemoveAllConfirm(
-			descriptor.controller,
-			descriptor.removeAllConfirm,
-			descriptor.removeAll),
-		.icon = &st::menuIconCancelAttention,
-		.isAttention = true,
-	});
+	if (!descriptor.removeAllText.isEmpty()) {
+		add({
+			.text = descriptor.removeAllText,
+			.handler = RemoveAllConfirm(
+				descriptor.controller,
+				descriptor.removeAllConfirm,
+				descriptor.removeAll),
+			.icon = &st::menuIconCancelAttention,
+			.isAttention = true,
+		});
+	}
 }
 
 RecentRow::RecentRow(not_null<PeerData*> peer)
@@ -422,6 +423,9 @@ public:
 		not_null<Window::SessionController*> window);
 
 	void prepare() override;
+	base::unique_qptr<Ui::PopupMenu> rowContextMenu(
+		QWidget *parent,
+		not_null<PeerListRow*> row) override;
 
 	void load();
 
@@ -1031,6 +1035,35 @@ void RecentAppsController::prepare() {
 	}, _lifetime);
 }
 
+base::unique_qptr<Ui::PopupMenu> RecentAppsController::rowContextMenu(
+		QWidget *parent,
+		not_null<PeerListRow*> row) {
+	auto result = base::make_unique_q<Ui::PopupMenu>(
+		parent,
+		st::popupMenuWithIcons);
+	const auto peer = row->peer();
+	const auto weak = base::make_weak(this);
+	const auto session = &this->session();
+	const auto removeOne = crl::guard(session, [=] {
+		if (weak) {
+			const auto rowId = peer->id.value;
+			if (const auto row = delegate()->peerListFindRow(rowId)) {
+				setCount(std::max(0, countCurrent() - 1));
+				delegate()->peerListRemoveRow(row);
+				delegate()->peerListRefreshRows();
+			}
+		}
+		session->topBotApps().remove(peer);
+	});
+	FillEntryMenu(Ui::Menu::CreateAddActionCallback(result), {
+		.controller = window(),
+		.peer = peer,
+		.removeOneText = tr::lng_recent_remove(tr::now),
+		.removeOne = removeOne,
+	});
+	return result;
+}
+
 void RecentAppsController::load() {
 	session().topBotApps().reload();
 }

From 11c91c1a42ada13d2b3af04960cf823cb6a017c7 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 14:13:27 +0200
Subject: [PATCH 153/163] Don't show recent apps in popular apps.

---
 .../dialogs/ui/dialogs_suggestions.cpp        | 62 ++++++++++++++-----
 .../dialogs/ui/dialogs_suggestions.h          |  2 +
 2 files changed, 49 insertions(+), 15 deletions(-)

diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
index a1391e3ba..96529c241 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
@@ -429,11 +429,15 @@ public:
 
 	void load();
 
+	[[nodiscard]] rpl::producer<> refreshed() const;
+	[[nodiscard]] bool shown(not_null<PeerData*> peer) const;
+
 private:
 	void appendRow(not_null<UserData*> bot);
 	void fill();
 
 	std::vector<not_null<UserData*>> _bots;
+	rpl::event_stream<> _refreshed;
 	rpl::lifetime _lifetime;
 
 };
@@ -442,7 +446,9 @@ class PopularAppsController final
 	: public Suggestions::ObjectListController {
 public:
 	explicit PopularAppsController(
-		not_null<Window::SessionController*> window);
+		not_null<Window::SessionController*> window,
+		Fn<bool(not_null<PeerData*>)> filterOut,
+		rpl::producer<> filterOutRefreshes);
 
 	void prepare() override;
 
@@ -452,6 +458,8 @@ private:
 	void fill();
 	void appendRow(not_null<UserData*> bot);
 
+	Fn<bool(not_null<PeerData*>)> _filterOut;
+	rpl::producer<> _filterOutRefreshes;
 	History *_activeHistory = nullptr;
 	bool _requested = false;
 	rpl::lifetime _lifetime;
@@ -1068,6 +1076,14 @@ void RecentAppsController::load() {
 	session().topBotApps().reload();
 }
 
+rpl::producer<> RecentAppsController::refreshed() const {
+	return _refreshed.events();
+}
+
+bool RecentAppsController::shown(not_null<PeerData*> peer) const {
+	return delegate()->peerListFindRow(peer->id.value) != nullptr;
+}
+
 void RecentAppsController::fill() {
 	const auto count = countCurrent();
 	const auto limit = expandedCurrent()
@@ -1087,6 +1103,8 @@ void RecentAppsController::fill() {
 		}
 	}
 	delegate()->peerListRefreshRows();
+
+	_refreshed.fire({});
 }
 
 void RecentAppsController::appendRow(not_null<UserData*> bot) {
@@ -1099,13 +1117,21 @@ void RecentAppsController::appendRow(not_null<UserData*> bot) {
 }
 
 PopularAppsController::PopularAppsController(
-	not_null<Window::SessionController*> window)
-: ObjectListController(window) {
+	not_null<Window::SessionController*> window,
+	Fn<bool(not_null<PeerData*>)> filterOut,
+	rpl::producer<> filterOutRefreshes)
+: ObjectListController(window)
+, _filterOut(std::move(filterOut))
+, _filterOutRefreshes(std::move(filterOutRefreshes)) {
 }
 
 void PopularAppsController::prepare() {
 	setupPlainDivider(tr::lng_bot_apps_popular());
-	fill();
+	rpl::single() | rpl::then(
+		std::move(_filterOutRefreshes)
+	) | rpl::start_with_next([=] {
+		fill();
+	}, _lifetime);
 }
 
 void PopularAppsController::load() {
@@ -1122,13 +1148,13 @@ void PopularAppsController::load() {
 }
 
 void PopularAppsController::fill() {
-	const auto attachWebView = &session().attachWebView();
-	const auto &list = attachWebView->popularAppBots();
-	if (list.empty()) {
-		return;
+	while (delegate()->peerListFullRowsCount()) {
+		delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
 	}
-	for (const auto &bot : list) {
-		appendRow(bot);
+	for (const auto &bot : session().attachWebView().popularAppBots()) {
+		if (!_filterOut || !_filterOut(bot)) {
+			appendRow(bot);
+		}
 	}
 	delegate()->peerListRefreshRows();
 	setCount(delegate()->peerListFullRowsCount());
@@ -1136,10 +1162,10 @@ void PopularAppsController::fill() {
 
 void PopularAppsController::appendRow(not_null<UserData*> bot) {
 	auto row = std::make_unique<PeerListRow>(bot);
-	if (const auto count = bot->botInfo->activeUsers) {
-		row->setCustomStatus(
-			tr::lng_bot_status_users(tr::now, lt_count_decimal, count));
-	}
+	//if (const auto count = bot->botInfo->activeUsers) {
+	//	row->setCustomStatus(
+	//		tr::lng_bot_status_users(tr::now, lt_count_decimal, count));
+	//}
 	delegate()->peerListAppendRow(std::move(row));
 }
 
@@ -1896,6 +1922,10 @@ auto Suggestions::setupRecommendations() -> std::unique_ptr<ObjectList> {
 auto Suggestions::setupRecentApps() -> std::unique_ptr<ObjectList> {
 	const auto controller = lifetime().make_state<RecentAppsController>(
 		_controller);
+	_recentAppsShows = [=](not_null<PeerData*> peer) {
+		return controller->shown(peer);
+	};
+	_recentAppsRefreshed = controller->refreshed();
 
 	auto result = setupObjectList(
 		_appsScroll.get(),
@@ -1952,7 +1982,9 @@ auto Suggestions::setupRecentApps() -> std::unique_ptr<ObjectList> {
 
 auto Suggestions::setupPopularApps() -> std::unique_ptr<ObjectList> {
 	const auto controller = lifetime().make_state<PopularAppsController>(
-		_controller);
+		_controller,
+		_recentAppsShows,
+		rpl::duplicate(_recentAppsRefreshed));
 
 	const auto addToScroll = [=] {
 		const auto wrap = _recentApps->wrap;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
index 549851d72..cf8b21780 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
@@ -189,6 +189,8 @@ private:
 	const std::unique_ptr<Ui::ElasticScroll> _appsScroll;
 	const not_null<Ui::VerticalLayout*> _appsContent;
 
+	rpl::producer<> _recentAppsRefreshed;
+	Fn<bool(not_null<PeerData*>)> _recentAppsShows;
 	const std::unique_ptr<ObjectList> _recentApps;
 	const std::unique_ptr<ObjectList> _popularApps;
 

From 0af3028cd6868ad56d1d159e14f00f0ccce1edbd Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 14:47:16 +0200
Subject: [PATCH 154/163] Allow opening tonsite:// from web apps.

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

diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
index e5c9ac6e3..3f549edb1 100644
--- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
+++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp
@@ -1117,9 +1117,10 @@ Webview::ThemeParams WebViewInstance::botThemeParams() {
 
 bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {
 	const auto local = Core::TryConvertUrlToLocal(uri);
-	if (uri == local || Core::InternalPassportLink(local)) {
-		return local.startsWith(u"tg://"_q);
-	} else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) {
+	if (Core::InternalPassportLink(local)) {
+		return true;
+	} else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)
+		&& !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) {
 		return false;
 	}
 	const auto bot = _bot;

From 4864a6996f8f0b06c56dce3b89825eaf7affc009 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 14:47:33 +0200
Subject: [PATCH 155/163] Open links from tonsite:// externally.

---
 Telegram/SourceFiles/iv/iv_controller.cpp | 22 +++++++++++++++++-----
 Telegram/SourceFiles/iv/iv_controller.h   |  1 +
 2 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 067e90b48..11df08527 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -257,10 +257,15 @@ void Controller::initControls() {
 		_subtitleText.value(),
 		st::ivSubtitle);
 	_subtitle->setSelectable(true);
-	_subtitleText.value(
-	) | rpl::start_with_next([=](const QString &subtitle) {
+
+	_windowTitleText = _subtitleText.value(
+	) | rpl::map([=](const QString &subtitle) {
 		const auto prefix = tr::lng_iv_window_title(tr::now);
-		_window->setWindowTitle(prefix + ' ' + QChar(0x2014) + ' ' + subtitle);
+		return prefix + ' ' + QChar(0x2014) + ' ' + subtitle;
+	});
+	_windowTitleText.value(
+	) | rpl::start_with_next([=](const QString &title) {
+		_window->setWindowTitle(title);
 	}, _subtitle->lifetime());
 
 	_menuToggle.create(_subtitleWrap.get(), st::ivMenuToggle);
@@ -355,6 +360,7 @@ void Controller::showTonSite(
 	}) | rpl::map([=](QString value) {
 		return HttpsToTonsite(value);
 	});
+	_windowTitleText = _subtitleText;
 	_menuToggle->hide();
 }
 
@@ -488,7 +494,12 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 	}, _container->lifetime());
 
 	raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
-		return true;
+		if (uri.startsWith(u"http://desktop-app-resource/"_q)
+			|| QUrl(uri).host().toLower().endsWith(u".magic.org"_q)) {
+			return true;
+		}
+		_events.fire({ .type = Event::Type::OpenLink, .url = uri });
+		return false;
 	});
 	raw->setNavigationDoneHandler([=](bool success) {
 	});
@@ -578,7 +589,8 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 				|| index >= _pages.size()) {
 				return Webview::DataResult::Failed;
 			}
-			return finishWith(WrapPage(_pages[index]), "text/html; charset=utf-8");
+			return finishWith(
+				WrapPage(_pages[index]), "text/html; charset=utf-8");
 		} else if (id.starts_with("page") && id.ends_with(".json")) {
 			auto index = 0;
 			const auto result = std::from_chars(
diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h
index c92f4c4b2..30e975d02 100644
--- a/Telegram/SourceFiles/iv/iv_controller.h
+++ b/Telegram/SourceFiles/iv/iv_controller.h
@@ -132,6 +132,7 @@ private:
 	std::unique_ptr<Ui::RpWidget> _subtitleWrap;
 	rpl::variable<QString> _url;
 	rpl::variable<QString> _subtitleText;
+	rpl::variable<QString> _windowTitleText;
 	std::unique_ptr<Ui::FlatLabel> _subtitle;
 	Ui::Animations::Simple _subtitleBackShift;
 	Ui::Animations::Simple _subtitleForwardShift;

From 281ad01b850c07a0dc8c32fdc5cdbb8306c3b500 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 16:35:08 +0200
Subject: [PATCH 156/163] Add ada library.

---
 Telegram/build/docker/centos_env/Dockerfile | 13 +++++++++++
 Telegram/build/prepare/prepare.py           | 24 +++++++++++++++++++++
 cmake                                       |  2 +-
 snap/snapcraft.yaml                         | 18 ++++++++++++++++
 4 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile
index 568d6663a..82f184cb8 100644
--- a/Telegram/build/docker/centos_env/Dockerfile
+++ b/Telegram/build/docker/centos_env/Dockerfile
@@ -801,6 +801,18 @@ RUN cmake --build out --config Debug --parallel \
 	&& find out -mindepth 1 -maxdepth 1 ! -name Debug -exec rm -rf {} \;
 {%- endif %}
 
+FROM builder AS ada
+RUN git clone -b v2.9.0 --depth=1 {{ GIT }}/ada-url/ada.git \
+	&& cd ada \
+	&& cmake -GNinja -B build . \
+		-D CMAKE_BUILD_TYPE=None \
+        -D ADA_TESTING=OFF \
+        -D ADA_TOOLS=OFF \
+	&& cmake --build build --parallel \
+	&& DESTDIR="{{ LibrariesPath }}/ada-cache" cmake --install build \
+	&& cd .. \
+	&& rm -rf ada
+
 FROM builder
 COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache /
 COPY --link --from=xz {{ LibrariesPath }}/xz-cache /
@@ -844,6 +856,7 @@ COPY --link --from=breakpad {{ LibrariesPath }}/breakpad-cache /
 COPY --link --from=webrtc {{ LibrariesPath }}/tg_owt tg_owt
 COPY --link --from=webrtc_release {{ LibrariesPath }}/tg_owt/out/Release tg_owt/out/Release
 COPY --link --from=libwebp {{ LibrariesPath }}/libwebp-cache /
+COPY --link --from=ada {{ LibrariesPath }}/ada-cache /
 
 {%- if DEBUG %}
 COPY --link --from=webrtc_debug {{ LibrariesPath }}/tg_owt/out/Debug tg_owt/out/Debug
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index 83e16284f..67e767003 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -1831,6 +1831,30 @@ release:
     lipo -create Release.arm64/libtg_owt.a Release.x86_64/libtg_owt.a -output Release/libtg_owt.a
 """)
 
+stage('ada', """
+    git clone -b v2.9.0 https://github.com/ada-url/ada.git
+    cd ada
+win:
+    cmake -B out . ^
+        -A %WIN32X64% ^
+        -D ADA_TESTING=OFF ^
+        -D ADA_TOOLS=OFF ^
+        -D CMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$<CONFIG:Debug>:Debug>" ^
+        -D CMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^
+        -D CMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG"
+    cmake --build out --config Debug --parallel
+    cmake --build out --config Release --parallel
+mac:
+    CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake -B build . \\
+        -D ADA_TESTING=OFF \\
+        -D ADA_TOOLS=OFF \\
+        -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\
+        -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\
+        -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX
+    cmake --build build $MAKE_THREADS_CNT
+    cmake --install build
+""")
+
 stage('protobuf', """
 win:
     git clone --recursive -b v21.9 https://github.com/protocolbuffers/protobuf
diff --git a/cmake b/cmake
index 721383d0b..6a1ac8a4e 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 721383d0b901cc5d5cbeec29386671d145f5a1e5
+Subproject commit 6a1ac8a4eebb968ff6ca538088006a3d06d47421
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index b5f71d99f..8378b599f 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -488,3 +488,21 @@ parts:
     after:
       - ffmpeg
       - libjxl
+
+  ada:
+    source: https://github.com/ada-url/ada.git
+    source-depth: 1
+    source-tag: v2.9.0
+    plugin: cmake
+    build-environment:
+      - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s
+    cmake-generator: Ninja
+    cmake-parameters:
+      - -DCMAKE_BUILD_TYPE=Release
+      - -DCMAKE_INSTALL_PREFIX=/usr
+      - -DADA_TESTING=OFF
+      - -DADA_TOOLS=OFF
+    prime:
+      - -./usr/include
+      - -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/cmake
+      - -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/*.a

From 2ff0ed50be10da0f96d530205d032aedffb57642 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 16:35:55 +0200
Subject: [PATCH 157/163] Encode/Decode tonsite:// punycode correctly.

---
 Telegram/Resources/langs/lang.strings     |  1 +
 Telegram/SourceFiles/iv/iv_controller.cpp | 78 ++++++++++++++---------
 Telegram/SourceFiles/iv/iv_controller.h   |  1 +
 Telegram/SourceFiles/iv/iv_instance.cpp   |  6 +-
 Telegram/cmake/td_iv.cmake                |  1 +
 5 files changed, 57 insertions(+), 30 deletions(-)

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 64bbb68d8..3924a85f6 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -5318,6 +5318,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_iv_join_channel" = "Join";
 "lng_iv_window_title" = "Instant View";
 "lng_iv_wrong_layout" = "Wrong layout?";
+"lng_iv_not_supported" = "This link appears to be invalid.";
 
 "lng_limit_download_title" = "Download speed limited";
 "lng_limit_download_subscribe" = "Subscribe to {link} and increase download speed {increase}.";
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index 11df08527..759ef0db3 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -44,6 +44,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include <QtGui/QWindow>
 #include <charconv>
 
+#include <ada.h>
+
 namespace Iv {
 namespace {
 
@@ -137,50 +139,62 @@ namespace {
 
 [[nodiscard]] QString TonsiteToHttps(QString value) {
 	const auto ChangeHost = [](QString tonsite) {
+		const auto fake = "http://" + tonsite.toStdString();
+		const auto parsed = ada::parse<ada::url>(fake);
+		if (!parsed) {
+			return QString();
+		}
+		tonsite = QString::fromStdString(parsed->get_hostname());
 		tonsite = tonsite.replace('-', "-h");
 		tonsite = tonsite.replace('.', "-d");
 		return tonsite + ".magic.org";
 	};
-	auto parsed = QUrl(value);
-	if (parsed.isValid()) {
-		parsed.setScheme("https");
-		parsed.setHost(ChangeHost(parsed.host()));
-		if (parsed.path().isEmpty()) {
-			parsed.setPath(u"/"_q);
-		}
-		return parsed.toString();
+	const auto prefix = u"tonsite://"_q;
+	if (!value.toLower().startsWith(prefix)) {
+		return QString();
 	}
-	const auto part = value.mid(u"tonsite://"_q.size());
+	const auto part = value.mid(prefix.size());
 	const auto split = part.indexOf('/');
-	return "https://"
-		+ ChangeHost((split < 0) ? part : part.left(split))
-		+ ((split < 0) ? u"/"_q : part.mid(split));
+	const auto host = ChangeHost((split < 0) ? part : part.left(split));
+	if (host.isEmpty()) {
+		return QString();
+	}
+	return "https://" + host + ((split < 0) ? u"/"_q : part.mid(split));
 }
 
 [[nodiscard]] QString HttpsToTonsite(QString value) {
 	const auto ChangeHost = [](QString https) {
-		https.replace(".magic.org", QString());
+		const auto dot = https.indexOf('.');
+		if (dot < 0 || https.mid(dot).toLower() != u".magic.org"_q) {
+			return QString();
+		}
+		https = https.mid(0, dot);
 		https = https.replace("-d", ".");
 		https = https.replace("-h", "-");
-		return https;
+		auto parts = https.split('.');
+		for (auto &part : parts) {
+			if (part.startsWith(u"xn--"_q)) {
+				const auto utf8 = part.mid(4).toStdString();
+				auto out = std::u32string();
+				if (ada::idna::punycode_to_utf32(utf8, out)) {
+					part = QString::fromUcs4(out.data(), out.size());
+				}
+			}
+		}
+		return parts.join('.');
 	};
-	auto parsed = QUrl(value);
-	if (parsed.isValid()) {
-		const auto host = ChangeHost(parsed.host());
-		const auto emptyPath = parsed.path().isEmpty();
-		parsed.setScheme("tonsite");
-		parsed.setHost(host);
-		if (emptyPath) {
-			parsed.setPath(u"/"_q);
-		}
-		if (parsed.isValid()) {
-			return parsed.toString();
-		}
+	const auto prefix = u"https://"_q;
+	if (!value.toLower().startsWith(prefix)) {
+		return value;
 	}
-	const auto part = value.mid(u"https://"_q.size());
+	const auto part = value.mid(prefix.size());
 	const auto split = part.indexOf('/');
+	const auto host = ChangeHost((split < 0) ? part : part.left(split));
+	if (host.isEmpty()) {
+		return value;
+	}
 	return "tonsite://"
-		+ ChangeHost((split < 0) ? part : part.left(split))
+		+ host
 		+ ((split < 0) ? u"/"_q : part.mid(split));
 }
 
@@ -342,10 +356,16 @@ void Controller::update(Prepared page) {
 	}
 }
 
+bool Controller::IsGoodTonSiteUrl(const QString &uri) {
+	return !TonsiteToHttps(uri).isEmpty();
+}
+
 void Controller::showTonSite(
 		const Webview::StorageId &storageId,
 		QString uri) {
 	const auto url = TonsiteToHttps(uri);
+	Assert(!url.isEmpty());
+
 	if (!_webview) {
 		createWebview(storageId);
 	}
@@ -360,7 +380,7 @@ void Controller::showTonSite(
 	}) | rpl::map([=](QString value) {
 		return HttpsToTonsite(value);
 	});
-	_windowTitleText = _subtitleText;
+	_windowTitleText = _subtitleText.value();
 	_menuToggle->hide();
 }
 
diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h
index 30e975d02..9b5af4e6b 100644
--- a/Telegram/SourceFiles/iv/iv_controller.h
+++ b/Telegram/SourceFiles/iv/iv_controller.h
@@ -76,6 +76,7 @@ public:
 		base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues);
 	void update(Prepared page);
 
+	[[nodiscard]] static bool IsGoodTonSiteUrl(const QString &uri);
 	void showTonSite(const Webview::StorageId &storageId, QString uri);
 
 	[[nodiscard]] bool active() const;
diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp
index 3b327e5a6..8078828d6 100644
--- a/Telegram/SourceFiles/iv/iv_instance.cpp
+++ b/Telegram/SourceFiles/iv/iv_instance.cpp
@@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/boxes/confirm_box.h"
 #include "ui/layers/layer_widget.h"
 #include "ui/text/text_utilities.h"
+#include "ui/toast/toast.h"
 #include "ui/basic_click_handlers.h"
 #include "webview/webview_data_stream_memory.h"
 #include "webview/webview_interface.h"
@@ -1074,7 +1075,10 @@ void Instance::openWithIvPreferred(
 void Instance::showTonSite(
 		const QString &uri,
 		QVariant context) {
-	if (Platform::IsMac()) {
+	if (!Controller::IsGoodTonSiteUrl(uri)) {
+		Ui::Toast::Show(tr::lng_iv_not_supported(tr::now));
+		return;
+	} else if (Platform::IsMac()) {
 		// Otherwise IV is not visible under the media viewer.
 		Core::App().hideMediaView();
 	}
diff --git a/Telegram/cmake/td_iv.cmake b/Telegram/cmake/td_iv.cmake
index 602abf41c..1d4edf9f5 100644
--- a/Telegram/cmake/td_iv.cmake
+++ b/Telegram/cmake/td_iv.cmake
@@ -38,6 +38,7 @@ PUBLIC
     tdesktop::td_scheme
 PRIVATE
     desktop-app::lib_webview
+    desktop-app::external_ada
     tdesktop::td_lang
     tdesktop::td_ui
 )

From 74f7fa80b7882fb67baa42e0138379aa36e9f455 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 16:36:13 +0200
Subject: [PATCH 158/163] Fix opening .ton links.

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

diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp
index bf1b49432..60c9b0045 100644
--- a/Telegram/SourceFiles/core/ui_integration.cpp
+++ b/Telegram/SourceFiles/core/ui_integration.cpp
@@ -239,7 +239,7 @@ bool UiIntegration::handleUrlClick(
 		Core::App().openLocalUrl(local, context);
 		return true;
 	} else if (local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) {
-		Core::App().iv().showTonSite(url, context);
+		Core::App().iv().showTonSite(local, context);
 		return true;
 	} else if (local.startsWith(u"internal:"_q, Qt::CaseInsensitive)) {
 		Core::App().openInternalUrl(local, context);

From e9bb6f65e3610fd55b3f19b8d2eb6e88ad627bf4 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 16:36:38 +0200
Subject: [PATCH 159/163] Update submodules.

---
 Telegram/lib_webrtc  | 2 +-
 Telegram/lib_webview | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index d9a08df0c..022203200 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit d9a08df0c0b64e4323e23c9ccefe754fd86d0f44
+Subproject commit 0222032008a93fb6f198fdeeafd4ee063d0b624b
diff --git a/Telegram/lib_webview b/Telegram/lib_webview
index 4b2399232..c27c69953 160000
--- a/Telegram/lib_webview
+++ b/Telegram/lib_webview
@@ -1 +1 @@
-Subproject commit 4b239923219d6a186b96de913506a0777e86c68e
+Subproject commit c27c69953db52cfcb56abc3d422764f0fb4c2152

From 65a14bcab4a8fdb6875fc6f3b5ea96f642b0f547 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Thu, 1 Aug 2024 15:01:46 +0300
Subject: [PATCH 160/163] Removed placeholder from input field in box for
 renaming sticker set.

---
 Telegram/SourceFiles/chat_helpers/chat_helpers.style | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index d48f32c00..63cf5048a 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1414,8 +1414,15 @@ editTagLimit: FlatLabel(defaultFlatLabel) {
 }
 
 editStickerSetNameField: InputField(defaultInputField) {
-	textMargins: margins(0px, 28px, 26px, 4px);
-	heightMax: 55px;
+	textMargins: margins(0px, 8px, 26px, 4px);
+	heightMin: 36px;
+	heightMax: 36px;
+	placeholderFg: placeholderFg;
+	placeholderFgActive: placeholderFgActive;
+	placeholderFgError: placeholderFgActive;
+	placeholderMargins: margins(2px, 0px, 2px, 0px);
+	placeholderScale: 0.;
+	placeholderFont: normalFont;
 }
 editStickerSetNameLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
 	color: lightButtonFg;

From 4a5d8046d5de32b1990fcd39dc12c7b04328e8ab Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Thu, 1 Aug 2024 17:18:31 +0200
Subject: [PATCH 161/163] Version 5.3.1.

- Open normal links from tonsite-s in system browser.
- Fix unicode tonsite:// links.
- Fix crash on Linux X11.
---
 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                                | 6 ++++++
 6 files changed, 21 insertions(+), 15 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index cffa28fb5..261fb0911 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="5.3.0.0" />
+    Version="5.3.1.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 b9e1fb916..eb9e2d265 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,3,0,0
- PRODUCTVERSION 5,3,0,0
+ FILEVERSION 5,3,1,0
+ PRODUCTVERSION 5,3,1,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "5.3.0.0"
+            VALUE "FileVersion", "5.3.1.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.3.0.0"
+            VALUE "ProductVersion", "5.3.1.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 0f70bef0e..9e4b2aced 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,3,0,0
- PRODUCTVERSION 5,3,0,0
+ FILEVERSION 5,3,1,0
+ PRODUCTVERSION 5,3,1,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", "5.3.0.0"
+            VALUE "FileVersion", "5.3.1.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.3.0.0"
+            VALUE "ProductVersion", "5.3.1.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index bb5f9b405..146e4b1f0 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 = 5003000;
-constexpr auto AppVersionStr = "5.3";
+constexpr auto AppVersion = 5003001;
+constexpr auto AppVersionStr = "5.3.1";
 constexpr auto AppBetaVersion = false;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index 4829ec4b2..928f3047d 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         5003000
+AppVersion         5003001
 AppVersionStrMajor 5.3
-AppVersionStrSmall 5.3
-AppVersionStr      5.3.0
+AppVersionStrSmall 5.3.1
+AppVersionStr      5.3.1
 BetaChannel        0
 AlphaVersion       0
-AppVersionOriginal 5.3
+AppVersionOriginal 5.3.1
diff --git a/changelog.txt b/changelog.txt
index f8929141f..cf6616399 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,9 @@
+5.3.1 (01.08.24)
+
+- Open normal links from tonsite-s in system browser.
+- Fix unicode tonsite:// links.
+- Fix crash on Linux X11.
+
 5.3 (31.07.24)
 
 - View recent and popular web apps in chats search.

From a982560a62a5cd2932491816cd20239707e1d91b Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 2 Aug 2024 05:31:59 +0300
Subject: [PATCH 162/163] Fix build on Linux.

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

diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc
index 022203200..8751e27d5 160000
--- a/Telegram/lib_webrtc
+++ b/Telegram/lib_webrtc
@@ -1 +1 @@
-Subproject commit 0222032008a93fb6f198fdeeafd4ee063d0b624b
+Subproject commit 8751e27d50d2f26b5d20673e5ddba38e90953570

From 0de50808743cfeaff0925cf7a63fdda207e29e85 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 2 Aug 2024 18:12:03 +0300
Subject: [PATCH 163/163] Version 5.3.2.

- Fix crash on launch by focing non-LTO jemalloc build.

Fixes #28213, fixes #28221.
---
 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 ++++
 cmake                                        | 2 +-
 7 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 261fb0911..89f5f8761 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="5.3.1.0" />
+    Version="5.3.2.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 eb9e2d265..eb4271d8a 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1               ICON                    "..\\art\\icon256.ico"
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,3,1,0
- PRODUCTVERSION 5,3,1,0
+ FILEVERSION 5,3,2,0
+ PRODUCTVERSION 5,3,2,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
         BEGIN
             VALUE "CompanyName", "Telegram FZ-LLC"
             VALUE "FileDescription", "Telegram Desktop"
-            VALUE "FileVersion", "5.3.1.0"
+            VALUE "FileVersion", "5.3.2.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.3.1.0"
+            VALUE "ProductVersion", "5.3.2.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 9e4b2aced..d7e88ba04 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
 //
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 5,3,1,0
- PRODUCTVERSION 5,3,1,0
+ FILEVERSION 5,3,2,0
+ PRODUCTVERSION 5,3,2,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", "5.3.1.0"
+            VALUE "FileVersion", "5.3.2.0"
             VALUE "LegalCopyright", "Copyright (C) 2014-2024"
             VALUE "ProductName", "Telegram Desktop"
-            VALUE "ProductVersion", "5.3.1.0"
+            VALUE "ProductVersion", "5.3.2.0"
         END
     END
     BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 146e4b1f0..9fdc8cf18 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 = 5003001;
-constexpr auto AppVersionStr = "5.3.1";
+constexpr auto AppVersion = 5003002;
+constexpr auto AppVersionStr = "5.3.2";
 constexpr auto AppBetaVersion = false;
 constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/build/version b/Telegram/build/version
index 928f3047d..37a420e7a 100644
--- a/Telegram/build/version
+++ b/Telegram/build/version
@@ -1,7 +1,7 @@
-AppVersion         5003001
+AppVersion         5003002
 AppVersionStrMajor 5.3
-AppVersionStrSmall 5.3.1
-AppVersionStr      5.3.1
+AppVersionStrSmall 5.3.2
+AppVersionStr      5.3.2
 BetaChannel        0
 AlphaVersion       0
-AppVersionOriginal 5.3.1
+AppVersionOriginal 5.3.2
diff --git a/changelog.txt b/changelog.txt
index cf6616399..e43e0dbe2 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,7 @@
+5.3.2 (02.08.24)
+
+- Fix crash on launch in some Linux systems.
+
 5.3.1 (01.08.24)
 
 - Open normal links from tonsite-s in system browser.
diff --git a/cmake b/cmake
index 6a1ac8a4e..08de4f18f 160000
--- a/cmake
+++ b/cmake
@@ -1 +1 @@
-Subproject commit 6a1ac8a4eebb968ff6ca538088006a3d06d47421
+Subproject commit 08de4f18f5e4459689957b3aa115e10d8cbef9d6