From b9b7d9e33709daec348c51e81607e47c56d6e3bc Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 Apr 2024 14:58:32 +0400
Subject: [PATCH] Implement live location view.

---
 .../icons/chat/live_location_long.png         | Bin 0 -> 416 bytes
 .../icons/chat/live_location_long@2x.png      | Bin 0 -> 913 bytes
 .../icons/chat/live_location_long@3x.png      | Bin 0 -> 1588 bytes
 Telegram/Resources/langs/lang.strings         |   9 +
 .../SourceFiles/data/data_media_types.cpp     |  51 ++-
 Telegram/SourceFiles/data/data_media_types.h  |  18 +-
 Telegram/SourceFiles/history/history_item.cpp |   4 +-
 .../view/media/history_view_location.cpp      | 409 ++++++++++++++++--
 .../view/media/history_view_location.h        |  33 +-
 .../history/view/media/history_view_media.cpp |   4 +
 .../history/view/media/history_view_media.h   |   1 +
 Telegram/SourceFiles/ui/chat/chat.style       |   6 +
 Telegram/SourceFiles/ui/chat/chat_style.cpp   |   6 +
 Telegram/SourceFiles/ui/chat/chat_style.h     |   1 +
 Telegram/build/prepare/prepare.py             |  76 +++-
 15 files changed, 555 insertions(+), 63 deletions(-)
 create mode 100644 Telegram/Resources/icons/chat/live_location_long.png
 create mode 100644 Telegram/Resources/icons/chat/live_location_long@2x.png
 create mode 100644 Telegram/Resources/icons/chat/live_location_long@3x.png

diff --git a/Telegram/Resources/icons/chat/live_location_long.png b/Telegram/Resources/icons/chat/live_location_long.png
new file mode 100644
index 0000000000000000000000000000000000000000..847930f549c6a70c90798213cbfdc6becb5414f6
GIT binary patch
literal 416
zcmV;R0bl-!P)<h;3K|Lk000e1NJLTq000~S000~a0ssI2{clLa00009a7bBm000XU
z000XU0RWnu7ytkPQb|NXR7i>Kk-zFeVHkkl4mpwhD`tbFT!3US%YbWe0jHGgR;3hK
zT!f8HXCadO36nC|+<=_)7{2-?2FLt6&-OljJ>Ofeh=YT}2b!k&{r*qycDsE(-_JQ+
z*W2y(?REpk<8eNp*ECH;qS0u--?uCa7!HT2RO*KX=kqymxm;GO6#$4xC=|M0ufX|y
z-fT9&{eDj*61Jj-VE~;@M~#(ArCzU#$bP>Es@1CUrBVr)&1SZu>-Ac#_DZkU1Deg|
zt29j$2#3SpqucHFEt<>afWzVN_Hr_r0Fg-Kd$d-o0j*X`jhRg5@pu5SSgc$wHyRD)
zi^U?aSS-F5Ma1QDEtgB+bUIC^Q+239p&%l=-3~Y&kLr?^WhIkITTv0wb-ml|ss&Bc
z%x1I7$K&zAV4%JkjYjEo+HUa=kH-@X20y*m>kR|~Kj$1A{sB+<w7b<9L_kLX0000<
KMNUMnLSTYS?!6lT

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/chat/live_location_long@2x.png b/Telegram/Resources/icons/chat/live_location_long@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4c0182008a996804ac48b4c201e86fc628f551f
GIT binary patch
literal 913
zcmV;C18)3@P)<h;3K|Lk000e1NJLTq001}u001}$0ssI2C*-V200009a7bBm000XU
z000XU0RWnu7ytkRLrFwIRA_<imO&^yQ5?s|Y`curOe7^a7)C5@q2{92$+}Pu4pNkw
zlqm<<rJS~i8wUr{W@C3L%E7@ZCwr1DrQNimrnIpvk~MZx)B7J@|M%Z4V{G$xdY{wm
z+t2)d-`?)~%@70zgTY`h7z_r3!TdU=yzeY7E@l`;RaI40_4qVRmz0#ezrO<j@@Q9(
z$321|baZqyG&ImOeSd!+i^X<#cMlH_Kb5Uks~`wG&)aM^ilP#UL^K-p`~9b<r+Kv2
zOLup7BoYBRsH(cZzu(&0`oR}PF&>ZS`Y$gpUayy7nE#Tpv$Io{<=n1_+uPfwrY7Xi
z&(CYf0Dw>^WVKoiPRViH<KrXhadL7pH#g^UxmH(KB}qd2R99CA{e^{v>`X^TN3*lD
zZnxX#^QBTLwC?x&4Gy}ov4N`3%*<r(M0Iub_VzXaU~Fs*t_%$gAz7B?{{DWG>EEff
z+wB(@7f5JtZ#OI@%d(kFMq>{sit6d<u~;kwLA12AD2f8*o12?YbJWz-B$G+#_<TOY
zg7)?G!J?T=hT}MG`S|!4?bhJn;8&W*<AF{j5}|3@n4nWrQ>du6IXO9rQU(G64b$m#
zLQRsS=H}*aGmv_M78K|jw>F#2<MF`jySuyb@o^0q{2luwNs{FEF?0o$Bnd@fS@u)M
zWHPO-tx*&OKMoHMr&1}6nmo_Li-(7Y>+5ShF}|m^wicZauh*L^kR-XZw3Pd!^E}_x
z)s^d4RaGSt2`I0xuN#vRvbnj5a=6{@>_0<gWo0lJMBkpBofQ=o5d^_73_RKc0|VKm
zIF5_OV#s$m97YChY-~)Y)2Mnl9G;$@76f5&aq;Tv3azKpX*3pAR#wpZ!NI}A#Kg$R
z$nx?s8cYCyt*tF1Q$n0h=iA#G_?mb;-qzNJe3#3mAp-zHp^#yN=>xsJz0qj&tFCst
z{ev%xB6`4PqbN!s5GXAz)!+KZuXz@WWps4Z;c(Q~*B2HRK0Q5MUS96(?CkCBed?E&
zmy4pv^E}J4X0th&OrD>g2ZO<IIQ*l&`Mi%p%F4>jX0t5IudlCq{ECVSk|dv>pA|*P
nyD0{P!C){L3<iV2{3?F{foZu_t%C1000000NkvXXu0mjfp}n~Z

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/icons/chat/live_location_long@3x.png b/Telegram/Resources/icons/chat/live_location_long@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a8cd36b53d105ad14199abfbe61d56ff057b0c8
GIT binary patch
literal 1588
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

literal 0
HcmV?d00001

diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 52c9ed8d3..d0a709c6d 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3092,6 +3092,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_unread_bar_some" = "Unread messages";
 
 "lng_maps_point" = "Location";
+"lng_live_location" = "Live Location";
+"lng_live_location_now" = "updated just now";
+"lng_live_location_minutes#one" = "updated {count} minute ago";
+"lng_live_location_minutes#other" = "updated {count} minutes ago";
+"lng_live_location_hours#one" = "updated {count} hour ago";
+"lng_live_location_hours#other" = "updated {count} hours ago";
+"lng_live_location_today" = "updated today at {time}";
+"lng_live_location_yesterday" = "updated yesterday at {time}";
+"lng_live_location_date_time" = "updated {date} at {time}";
 "lng_save_photo" = "Save image";
 "lng_save_video" = "Save video";
 "lng_save_audio_file" = "Save audio file";
diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp
index e6dd5006a..50e063da5 100644
--- a/Telegram/SourceFiles/data/data_media_types.cpp
+++ b/Telegram/SourceFiles/data/data_media_types.cpp
@@ -1311,8 +1311,9 @@ std::unique_ptr<HistoryView::Media> MediaContact::createView(
 
 MediaLocation::MediaLocation(
 	not_null<HistoryItem*> parent,
-	const LocationPoint &point)
-: MediaLocation(parent, point, QString(), QString()) {
+	const LocationPoint &point,
+	TimeId livePeriod)
+: MediaLocation({}, parent, point, livePeriod, QString(), QString()) {
 }
 
 MediaLocation::MediaLocation(
@@ -1320,17 +1321,30 @@ MediaLocation::MediaLocation(
 	const LocationPoint &point,
 	const QString &title,
 	const QString &description)
+: MediaLocation({}, parent, point, TimeId(), title, description) {
+}
+
+MediaLocation::MediaLocation(
+	PrivateTag,
+	not_null<HistoryItem*> parent,
+	const LocationPoint &point,
+	TimeId livePeriod,
+	const QString &title,
+	const QString &description)
 : Media(parent)
 , _point(point)
 , _location(parent->history()->owner().location(point))
+, _livePeriod(livePeriod)
 , _title(title)
 , _description(description) {
 }
 
 std::unique_ptr<Media> MediaLocation::clone(not_null<HistoryItem*> parent) {
 	return std::make_unique<MediaLocation>(
+		PrivateTag(),
 		parent,
 		_point,
+		_livePeriod,
 		_title,
 		_description);
 }
@@ -1339,8 +1353,14 @@ CloudImage *MediaLocation::location() const {
 	return _location;
 }
 
+QString MediaLocation::typeString() const {
+	return _livePeriod
+		? tr::lng_live_location(tr::now)
+		: tr::lng_maps_point(tr::now);
+}
+
 ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const {
-	const auto type = tr::lng_maps_point(tr::now);
+	const auto type = typeString();
 	const auto hasMiniImages = false;
 	const auto text = TextWithEntities{ .text = _title };
 	return {
@@ -1349,9 +1369,7 @@ ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const {
 }
 
 TextWithEntities MediaLocation::notificationText() const {
-	return WithCaptionNotificationText(
-		tr::lng_maps_point(tr::now),
-		{ .text = _title });
+	return WithCaptionNotificationText(typeString(), { .text = _title });
 }
 
 QString MediaLocation::pinnedTextSubstring() const {
@@ -1360,7 +1378,7 @@ QString MediaLocation::pinnedTextSubstring() const {
 
 TextForMimeData MediaLocation::clipboardText() const {
 	auto result = TextForMimeData::Simple(
-		u"[ "_q + tr::lng_maps_point(tr::now) + u" ]\n"_q);
+		u"[ "_q + typeString() + u" ]\n"_q);
 	auto titleResult = TextUtilities::ParseEntities(
 		_title,
 		Ui::WebpageTextTitleOptions().flags);
@@ -1389,12 +1407,19 @@ std::unique_ptr<HistoryView::Media> MediaLocation::createView(
 		not_null<HistoryView::Element*> message,
 		not_null<HistoryItem*> realParent,
 		HistoryView::Element *replacing) {
-	return std::make_unique<HistoryView::Location>(
-		message,
-		_location,
-		_point,
-		_title,
-		_description);
+	return _livePeriod
+		? std::make_unique<HistoryView::Location>(
+			message,
+			_location,
+			_point,
+			replacing,
+			_livePeriod)
+		: std::make_unique<HistoryView::Location>(
+			message,
+			_location,
+			_point,
+			_title,
+			_description);
 }
 
 MediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call)
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 1aae67928..c486799cd 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -331,10 +331,14 @@ private:
 };
 
 class MediaLocation final : public Media {
+	struct PrivateTag {
+	};
+
 public:
 	MediaLocation(
 		not_null<HistoryItem*> parent,
-		const LocationPoint &point);
+		const LocationPoint &point,
+		TimeId livePeriod = 0);
 	MediaLocation(
 		not_null<HistoryItem*> parent,
 		const LocationPoint &point,
@@ -356,9 +360,21 @@ public:
 		not_null<HistoryItem*> realParent,
 		HistoryView::Element *replacing = nullptr) override;
 
+	MediaLocation(
+		PrivateTag,
+		not_null<HistoryItem*> parent,
+		const LocationPoint &point,
+		TimeId livePeriod,
+		const QString &title,
+		const QString &description);
+
 private:
+
+	[[nodiscard]] QString typeString() const;
+
 	LocationPoint _point;
 	not_null<CloudImage*> _location;
+	TimeId _livePeriod = 0;
 	QString _title;
 	QString _description;
 
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 0695742bb..171480ab6 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -224,10 +224,12 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
 			return nullptr;
 		});
 	}, [&](const MTPDmessageMediaGeoLive &media) -> Result {
+		const auto period = media.vperiod().v;
 		return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result {
 			return std::make_unique<Data::MediaLocation>(
 				item,
-				Data::LocationPoint(point));
+				Data::LocationPoint(point),
+				media.vperiod().v);
 		}, [](const MTPDgeoPointEmpty &) -> Result {
 			return nullptr;
 		});
diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.cpp b/Telegram/SourceFiles/history/view/media/history_view_location.cpp
index 5fab03d15..f16d65141 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_location.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_location.cpp
@@ -7,12 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "history/view/media/history_view_location.h"
 
+#include "base/unixtime.h"
 #include "history/history.h"
 #include "history/history_item_components.h"
 #include "history/history_item.h"
 #include "history/history_location_manager.h"
 #include "history/view/history_view_element.h"
 #include "history/view/history_view_cursor_state.h"
+#include "lang/lang_keys.h"
 #include "ui/chat/chat_style.h"
 #include "ui/image/image.h"
 #include "ui/text/text_options.h"
@@ -24,6 +26,98 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/style_chat.h"
 
 namespace HistoryView {
+namespace {
+
+constexpr auto kUntilOffPeriod = std::numeric_limits<TimeId>::max();
+constexpr auto kLiveElapsedPartOpacity = 0.2;
+
+[[nodiscard]] TimeId ResolveUpdateDate(not_null<Element*> view) {
+	const auto item = view->data();
+	const auto edited = item->Get<HistoryMessageEdited>();
+	return edited ? edited->date : item->date();
+}
+
+[[nodiscard]] QString RemainingTimeText(
+		not_null<Element*> view,
+		TimeId period) {
+	if (period == kUntilOffPeriod) {
+		return QString(1, QChar(0x221E));
+	}
+	const auto elapsed = base::unixtime::now() - view->data()->date();
+	const auto remaining = std::clamp(period - elapsed, 0, period);
+	if (remaining < 10) {
+		return tr::lng_seconds_tiny(tr::now, lt_count, remaining);
+	} else if (remaining < 600) {
+		return tr::lng_minutes_tiny(tr::now, lt_count, remaining / 60);
+	} else if (remaining < 3600) {
+		return QString::number(remaining / 60);
+	} else if (remaining < 86400) {
+		return tr::lng_hours_tiny(tr::now, lt_count, remaining / 3600);
+	}
+	return tr::lng_days_tiny(tr::now, lt_count, remaining / 86400);
+}
+
+[[nodiscard]] float64 RemainingTimeProgress(
+		not_null<Element*> view,
+		TimeId period) {
+	if (period == kUntilOffPeriod) {
+		return 1.;
+	} else if (period < 1) {
+		return 0.;
+	}
+	const auto elapsed = base::unixtime::now() - view->data()->date();
+	return std::clamp(period - elapsed, 0, period) / float64(period);
+}
+
+} // namespace
+
+struct Location::Live {
+	explicit Live(TimeId period) : period(period) {
+	}
+
+	base::Timer updateStatusTimer;
+	base::Timer updateRemainingTimer;
+	QImage previous;
+	QImage previousCache;
+	Ui::BubbleRounding previousRounding;
+	Ui::Animations::Simple crossfade;
+	TimeId period = 0;
+	int thumbnailHeight = 0;
+};
+
+Location::Location(
+	not_null<Element*> parent,
+	not_null<Data::CloudImage*> data,
+	Data::LocationPoint point,
+	Element *replacing,
+	TimeId livePeriod)
+: Media(parent)
+, _data(data)
+, _live(CreateLiveTracker(parent, livePeriod))
+, _title(st::msgMinWidth)
+, _description(st::msgMinWidth)
+, _link(std::make_shared<LocationClickHandler>(point)) {
+	if (_live) {
+		_title.setText(
+			st::webPageTitleStyle,
+			tr::lng_live_location(tr::now),
+			Ui::WebpageTextTitleOptions());
+		_live->updateStatusTimer.setCallback([=] {
+			updateLiveStatus();
+			checkLiveFinish();
+		});
+		_live->updateRemainingTimer.setCallback([=] {
+			checkLiveFinish();
+		});
+		updateLiveStatus();
+		if (const auto media = replacing ? replacing->media() : nullptr) {
+			_live->previous = media->locationTakeImage();
+			if (!_live->previous.isNull()) {
+				history()->owner().registerHeavyViewPart(_parent);
+			}
+		}
+	}
+}
 
 Location::Location(
 	not_null<Element*> parent,
@@ -53,18 +147,106 @@ Location::Location(
 }
 
 Location::~Location() {
-	if (_media) {
-		_media = nullptr;
+	if (hasHeavyPart()) {
+		unloadHeavyPart();
 		_parent->checkHeavyPart();
 	}
 }
 
+void Location::checkLiveFinish() {
+	Expects(_live != nullptr);
+
+	const auto now = base::unixtime::now();
+	const auto item = _parent->data();
+	const auto start = item->date();
+	if (_live->period != kUntilOffPeriod && now - start >= _live->period) {
+		_live = nullptr;
+		item->history()->owner().requestViewResize(_parent);
+	} else {
+		_parent->repaint();
+	}
+}
+
+std::unique_ptr<Location::Live> Location::CreateLiveTracker(
+		not_null<Element*> parent,
+		TimeId period) {
+	if (!period) {
+		return nullptr;
+	}
+	const auto now = base::unixtime::now();
+	const auto date = parent->data()->date();
+	return (now < date || now - date < period)
+		? std::make_unique<Live>(period)
+		: nullptr;
+}
+
+void Location::updateLiveStatus() {
+	const auto date = ResolveUpdateDate(_parent);
+	const auto now = base::unixtime::now();
+	const auto elapsed = now - date;
+	auto next = TimeId();
+	const auto text = [&] {
+		if (elapsed < 60) {
+			next = 60 - elapsed;
+			return tr::lng_live_location_now(tr::now);
+		} else if (const auto minutes = elapsed / 60; minutes < 60) {
+			next = 60 - (elapsed % 60);
+			return tr::lng_live_location_minutes(tr::now, lt_count, minutes);
+		} else if (const auto hours = elapsed / 3600; hours < 12) {
+			next = 3600 - (elapsed % 3600);
+			return tr::lng_live_location_hours(tr::now, lt_count, hours);
+		}
+		const auto dateFull = base::unixtime::parse(date);
+		const auto nowFull = base::unixtime::parse(now);
+		const auto nextTomorrow = [&] {
+			const auto tomorrow = nowFull.date().addDays(1);
+			next = nowFull.secsTo(QDateTime(tomorrow, QTime(0, 0)));
+		};
+		const auto locale = QLocale();
+		const auto format = QLocale::ShortFormat;
+		if (dateFull.date() == nowFull.date()) {
+			nextTomorrow();
+			const auto time = locale.toString(dateFull.time(), format);
+			return tr::lng_live_location_today(tr::now, lt_time, time);
+		} else if (dateFull.date().addDays(1) == nowFull.date()) {
+			nextTomorrow();
+			const auto time = locale.toString(dateFull.time(), format);
+			return tr::lng_live_location_yesterday(tr::now, lt_time, time);
+		}
+		return tr::lng_live_location_date_time(
+			tr::now,
+			lt_date,
+			locale.toString(dateFull.date(), format),
+			lt_time,
+			locale.toString(dateFull.time(), format));
+	}();
+	_description.setMarkedText(
+		st::webPageDescriptionStyle,
+		{ text },
+		Ui::WebpageTextDescriptionOptions());
+	if (next > 0 && next < 86400) {
+		_live->updateStatusTimer.callOnce(next * crl::time(1000));
+	}
+}
+
+QImage Location::locationTakeImage() {
+	if (_media && !_media->isNull()) {
+		return *_media;
+	} else if (_live && !_live->previous.isNull()) {
+		return _live->previous;
+	}
+	return {};
+}
+
 void Location::unloadHeavyPart() {
 	_media = nullptr;
+	if (_live) {
+		_live->previous = QImage();
+	}
 }
 
 bool Location::hasHeavyPart() const {
-	return (_media != nullptr);
+	return (_media != nullptr) || (_live && !_live->previous.isNull());
 }
 
 void Location::ensureMediaCreated() const {
@@ -99,8 +281,14 @@ QSize Location::countOptimalSize() {
 		}
 		if (!_title.isEmpty() || !_description.isEmpty()) {
 			minHeight += st::mediaInBubbleSkip;
-			if (isBubbleTop()) {
-				minHeight += st::msgPadding.top();
+			if (_live) {
+				if (isBubbleBottom()) {
+					minHeight += st::msgPadding.bottom();
+				}
+			} else {
+				if (isBubbleTop()) {
+					minHeight += st::msgPadding.top();
+				}
 			}
 		}
 	}
@@ -128,6 +316,9 @@ QSize Location::countCurrentSize(int newWidth) {
 		std::min(newWidth, st::maxMediaSize));
 	accumulate_max(newWidth, minWidth);
 	accumulate_max(newHeight, st::minPhotoSize);
+	if (_live) {
+		_live->thumbnailHeight = newHeight;
+	}
 	if (_parent->hasBubble()) {
 		if (!_title.isEmpty()) {
 			newHeight += qMin(_title.countHeight(newWidth - st::msgPadding.left() - st::msgPadding.right()), st::webPageTitleFont->height * 2);
@@ -137,8 +328,14 @@ QSize Location::countCurrentSize(int newWidth) {
 		}
 		if (!_title.isEmpty() || !_description.isEmpty()) {
 			newHeight += st::mediaInBubbleSkip;
-			if (isBubbleTop()) {
-				newHeight += st::msgPadding.top();
+			if (_live) {
+				if (isBubbleBottom()) {
+					newHeight += st::msgPadding.bottom();
+				}
+			} else {
+				if (isBubbleTop()) {
+					newHeight += st::msgPadding.top();
+				}
 			}
 		}
 	}
@@ -156,20 +353,27 @@ TextSelection Location::fromDescriptionSelection(
 }
 
 void Location::draw(Painter &p, const PaintContext &context) const {
-	if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
+	if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
+		return;
+	}
 	auto paintx = 0, painty = 0, paintw = width(), painth = height();
 	bool bubble = _parent->hasBubble();
 	const auto st = context.st;
 	const auto stm = context.messageStyle();
 
 	const auto hasText = !_title.isEmpty() || !_description.isEmpty();
-	const auto rounding = adjustedBubbleRounding(
-		hasText ? RectPart::FullTop : RectPart());
-	if (bubble) {
-		if (hasText) {
-			if (isBubbleTop()) {
-				painty += st::msgPadding.top();
-			}
+	const auto rounding = adjustedBubbleRounding(_live
+		? RectPart::FullBottom
+		: hasText
+		? RectPart::FullTop
+		: RectPart());
+	const auto paintText = [&] {
+		if (_live) {
+			painty += st::mediaInBubbleSkip;
+		} else if (!hasText) {
+			return;
+		} else if (isBubbleTop()) {
+			painty += st::msgPadding.top();
 		}
 
 		auto textw = width() - st::msgPadding.left() - st::msgPadding.right();
@@ -180,24 +384,45 @@ void Location::draw(Painter &p, const PaintContext &context) const {
 			painty += qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height);
 		}
 		if (!_description.isEmpty()) {
+			if (_live) {
+				p.setPen(stm->msgDateFg);
+			}
 			_description.drawLeftElided(p, paintx + st::msgPadding.left(), painty, textw, width(), 3, style::al_left, 0, -1, 0, false, toDescriptionSelection(context.selection));
 			painty += qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height);
 		}
-		if (!_title.isEmpty() || !_description.isEmpty()) {
+		if (!_live) {
 			painty += st::mediaInBubbleSkip;
+			painth -= painty;
 		}
-		painth -= painty;
+	};
+	if (!_live) {
+		paintText();
 	}
-	auto rthumb = QRect(paintx, painty, paintw, painth);
+	const auto thumbh = _live ? _live->thumbnailHeight : painth;
+	auto rthumb = QRect(paintx, painty, paintw, thumbh);
 	if (!bubble) {
 		fillImageShadow(p, rthumb, rounding, context);
 	}
 
 	ensureMediaCreated();
 	validateImageCache(rthumb.size(), rounding);
-	if (!_imageCache.isNull()) {
+	const auto paintPrevious = _live && !_live->previous.isNull();
+	auto opacity = _imageCache.isNull() ? 0. : 1.;
+	if (paintPrevious) {
+		opacity = _live->crossfade.value(opacity);
+		if (opacity < 1.) {
+			p.drawImage(rthumb.topLeft(), _live->previousCache);
+			if (opacity > 0.) {
+				p.setOpacity(opacity);
+			}
+		}
+	}
+	if (!_imageCache.isNull() && opacity > 0.) {
 		p.drawImage(rthumb.topLeft(), _imageCache);
-	} else if (!bubble) {
+		if (opacity < 1.) {
+			p.setOpacity(1.);
+		}
+	} else if (!bubble && !paintPrevious) {
 		Ui::PaintBubble(
 			p,
 			Ui::SimpleBubble{
@@ -223,8 +448,12 @@ void Location::draw(Painter &p, const PaintContext &context) const {
 	if (context.selected()) {
 		fillImageOverlay(p, rthumb, rounding, context);
 	}
-
-	if (_parent->media() == this) {
+	if (_live) {
+		painty += _live->thumbnailHeight;
+		painth -= _live->thumbnailHeight;
+		paintLiveRemaining(p, context, { paintx, painty, paintw, painth });
+		paintText();
+	} else if (_parent->media() == this) {
 		auto fullRight = paintx + paintw;
 		auto fullBottom = height();
 		_parent->drawInfo(
@@ -244,25 +473,114 @@ void Location::draw(Painter &p, const PaintContext &context) const {
 	}
 }
 
+void Location::paintLiveRemaining(
+		QPainter &p,
+		const PaintContext &context,
+		QRect bottom) const {
+	const auto size = st::liveLocationRemainingSize;
+	const auto skip = (bottom.height() - size) / 2;
+	const auto rect = QRect(
+		bottom.x() + bottom.width() - size - skip,
+		bottom.y() + skip,
+		size,
+		size);
+	auto hq = PainterHighQualityEnabler(p);
+	const auto stm = context.messageStyle();
+	const auto color = stm->msgServiceFg->c;
+	const auto untiloff = (_live->period == kUntilOffPeriod);
+	const auto progress = RemainingTimeProgress(_parent, _live->period);
+	const auto part = 1. / 360;
+	const auto full = (progress >= 1. - part);
+	auto elapsed = color;
+	if (!full) {
+		elapsed.setAlphaF(elapsed.alphaF() * kLiveElapsedPartOpacity);
+	}
+	auto pen = QPen(elapsed);
+	const auto stroke = style::ConvertScaleExact(2.);
+	pen.setWidthF(stroke);
+	p.setPen(pen);
+	p.setBrush(Qt::NoBrush);
+	p.drawEllipse(rect);
+
+	if (untiloff) {
+		stm->liveLocationLongIcon.paintInCenter(p, rect);
+	} else {
+		if (!full && progress > part) {
+			auto pen = QPen(color);
+			pen.setWidthF(stroke);
+			p.setPen(pen);
+			p.drawArc(rect, 90 * 16, int(base::SafeRound(360 * 16 * progress)));
+		}
+
+		p.setPen(stm->msgServiceFg);
+		p.setFont(st::semiboldFont);
+		const auto text = RemainingTimeText(_parent, _live->period);
+		p.drawText(rect, text, style::al_center);
+		const auto each = std::clamp(_live->period / 360, 1, 86400);
+		_live->updateRemainingTimer.callOnce(each * crl::time(1000));
+	}
+}
+
 void Location::validateImageCache(
 		QSize outer,
 		Ui::BubbleRounding rounding) const {
 	Expects(_media != nullptr);
 
-	const auto ratio = style::DevicePixelRatio();
-	if ((_imageCache.size() == (outer * ratio)
-			&& _imageCacheRounding == rounding)
-		|| _media->isNull()) {
+	if (_live && !_live->previous.isNull()) {
+		validateImageCache(
+			_live->previous,
+			_live->previousCache,
+			_live->previousRounding,
+			outer,
+			rounding);
+	}
+	validateImageCache(
+		*_media,
+		_imageCache,
+		_imageCacheRounding,
+		outer,
+		rounding);
+	checkLiveCrossfadeStart();
+}
+
+void Location::checkLiveCrossfadeStart() const {
+	if (!_live
+		|| _live->previous.isNull()
+		|| !_media
+		|| _media->isNull()
+		|| _live->crossfade.animating()) {
 		return;
 	}
-	_imageCache = Images::Round(
-		_media->scaled(
+	_live->crossfade.start([=] {
+		if (!_live->crossfade.animating()) {
+			_live->previous = QImage();
+			_live->previousCache = QImage();
+		}
+		_parent->repaint();
+	}, 0., 1., st::fadeWrapDuration);
+}
+
+void Location::validateImageCache(
+		const QImage &source,
+		QImage &cache,
+		Ui::BubbleRounding &cacheRounding,
+		QSize outer,
+		Ui::BubbleRounding rounding) const {
+	if (source.isNull()) {
+		return;
+	}
+	const auto ratio = style::DevicePixelRatio();
+	if (cache.size() == (outer * ratio) && cacheRounding == rounding) {
+		return;
+	}
+	cache = Images::Round(
+		source.scaled(
 			outer * ratio,
 			Qt::IgnoreAspectRatio,
 			Qt::SmoothTransformation),
 		MediaRoundingMask(rounding));
-	_imageCache.setDevicePixelRatio(ratio);
-	_imageCacheRounding = rounding;
+	cache.setDevicePixelRatio(ratio);
+	cacheRounding = rounding;
 }
 
 TextState Location::textState(QPoint point, StateRequest request) const {
@@ -275,11 +593,13 @@ TextState Location::textState(QPoint point, StateRequest request) const {
 	auto paintx = 0, painty = 0, paintw = width(), painth = height();
 	bool bubble = _parent->hasBubble();
 
-	if (bubble) {
-		if (!_title.isEmpty() || !_description.isEmpty()) {
-			if (isBubbleTop()) {
-				painty += st::msgPadding.top();
-			}
+	auto checkText = [&] {
+		if (_live) {
+			painty += st::mediaInBubbleSkip;
+		} else if (_title.isEmpty() && _description.isEmpty()) {
+			return false;
+		} else if (isBubbleTop()) {
+			painty += st::msgPadding.top();
 		}
 
 		auto textw = width() - st::msgPadding.left() - st::msgPadding.right();
@@ -292,7 +612,7 @@ TextState Location::textState(QPoint point, StateRequest request) const {
 					textw,
 					width(),
 					request.forText()));
-				return result;
+				return true;
 			} else if (point.y() >= painty + titleh) {
 				symbolAdd += _title.length();
 			}
@@ -306,6 +626,8 @@ TextState Location::textState(QPoint point, StateRequest request) const {
 					textw,
 					width(),
 					request.forText()));
+				result.symbol += symbolAdd;
+				return true;
 			} else if (point.y() >= painty + descriptionh) {
 				symbolAdd += _description.length();
 			}
@@ -315,11 +637,22 @@ TextState Location::textState(QPoint point, StateRequest request) const {
 			painty += st::mediaInBubbleSkip;
 		}
 		painth -= painty;
+		return false;
+	};
+	if (!_live && checkText()) {
+		return result;
 	}
-	if (QRect(paintx, painty, paintw, painth).contains(point) && _data) {
+	const auto thumbh = _live ? _live->thumbnailHeight : painth;
+	if (QRect(paintx, painty, paintw, thumbh).contains(point) && _data) {
 		result.link = _link;
 	}
-	if (_parent->media() == this) {
+	if (_live) {
+		painty += _live->thumbnailHeight;
+		painth -= _live->thumbnailHeight;
+		if (checkText()) {
+			return result;
+		}
+	} else if (_parent->media() == this) {
 		auto fullRight = paintx + paintw;
 		auto fullBottom = height();
 		const auto bottomInfoResult = _parent->bottomInfoTextState(
diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.h b/Telegram/SourceFiles/history/view/media/history_view_location.h
index ec412fd00..9e431ef74 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_location.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_location.h
@@ -22,8 +22,14 @@ public:
 		not_null<Element*> parent,
 		not_null<Data::CloudImage*> data,
 		Data::LocationPoint point,
-		const QString &title = QString(),
-		const QString &description = QString());
+		Element *replacing = nullptr,
+		TimeId livePeriod = 0);
+	Location(
+		not_null<Element*> parent,
+		not_null<Data::CloudImage*> data,
+		Data::LocationPoint point,
+		const QString &title,
+		const QString &description);
 	~Location();
 
 	void draw(Painter &p, const PaintContext &context) const override;
@@ -58,15 +64,33 @@ public:
 		return isRoundedInBubbleBottom();
 	}
 
+	QImage locationTakeImage() override;
+
 	void unloadHeavyPart() override;
 	bool hasHeavyPart() const override;
 
 private:
+	struct Live;
+	[[nodiscard]] static std::unique_ptr<Live> CreateLiveTracker(
+		not_null<Element*> parent,
+		TimeId period);
+
 	void ensureMediaCreated() const;
 
 	void validateImageCache(
 		QSize outer,
 		Ui::BubbleRounding rounding) const;
+	void validateImageCache(
+		const QImage &source,
+		QImage &cache,
+		Ui::BubbleRounding &cacheRounding,
+		QSize outer,
+		Ui::BubbleRounding rounding) const;
+
+	void paintLiveRemaining(
+		QPainter &p,
+		const PaintContext &context,
+		QRect bottom) const;
 
 	QSize countOptimalSize() override;
 	QSize countCurrentSize(int newWidth) override;
@@ -79,7 +103,12 @@ private:
 	[[nodiscard]] int fullWidth() const;
 	[[nodiscard]] int fullHeight() const;
 
+	void checkLiveCrossfadeStart() const;
+	void updateLiveStatus();
+	void checkLiveFinish();
+
 	const not_null<Data::CloudImage*> _data;
+	mutable std::unique_ptr<Live> _live;
 	mutable std::shared_ptr<QImage> _media;
 	Ui::Text::String _title, _description;
 	ClickHandlerPtr _link;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp
index f64cb808b..56720e83f 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp
@@ -356,6 +356,10 @@ std::unique_ptr<StickerPlayer> Media::stickerTakePlayer(
 	return nullptr;
 }
 
+QImage Media::locationTakeImage() {
+	return QImage();
+}
+
 TextState Media::getStateGrouped(
 		const QRect &geometry,
 		RectParts sides,
diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h
index 1b835dcce..f9ad5c987 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_media.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_media.h
@@ -190,6 +190,7 @@ public:
 	virtual std::unique_ptr<StickerPlayer> stickerTakePlayer(
 		not_null<DocumentData*> data,
 		const Lottie::ColorReplacements *replacements);
+	virtual QImage locationTakeImage();
 	virtual void checkAnimation() {
 	}
 
diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style
index 2cab84839..5dd77998a 100644
--- a/Telegram/SourceFiles/ui/chat/chat.style
+++ b/Telegram/SourceFiles/ui/chat/chat.style
@@ -1070,3 +1070,9 @@ chatIntroWidth: 224px;
 chatIntroTitleMargin: margins(11px, 16px, 11px, 4px);
 chatIntroMargin: margins(11px, 0px, 11px, 0px);
 chatIntroStickerPadding: margins(10px, 8px, 10px, 16px);
+
+liveLocationLongInIcon: icon {{ "chat/live_location_long", msgInServiceFg }};
+liveLocationLongInIconSelected: icon {{ "chat/live_location_long", msgInServiceFgSelected }};
+liveLocationLongOutIcon: icon {{ "chat/live_location_long", msgOutServiceFg }};
+liveLocationLongOutIconSelected: icon {{ "chat/live_location_long", msgOutServiceFgSelected }};
+liveLocationRemainingSize: 28px;
diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp
index 9470d71d6..a532a3180 100644
--- a/Telegram/SourceFiles/ui/chat/chat_style.cpp
+++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp
@@ -543,6 +543,12 @@ ChatStyle::ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices) {
 		st::historyVoiceMessageInTTLSelected,
 		st::historyVoiceMessageOutTTL,
 		st::historyVoiceMessageOutTTLSelected);
+	make(
+		&MessageStyle::liveLocationLongIcon,
+		st::liveLocationLongInIcon,
+		st::liveLocationLongInIconSelected,
+		st::liveLocationLongOutIcon,
+		st::liveLocationLongOutIconSelected);
 
 	updateDarkValue();
 }
diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h
index 1053d630a..6b8cc459b 100644
--- a/Telegram/SourceFiles/ui/chat/chat_style.h
+++ b/Telegram/SourceFiles/ui/chat/chat_style.h
@@ -92,6 +92,7 @@ struct MessageStyle {
 	style::icon historyTranscribeLock = { Qt::Uninitialized };
 	style::icon historyTranscribeHide = { Qt::Uninitialized };
 	style::icon historyVoiceMessageTTL = { Qt::Uninitialized };
+	style::icon liveLocationLongIcon = { Qt::Uninitialized };
 	std::array<
 		std::unique_ptr<Text::QuotePaintCache>,
 		kColorPatternsCount> quoteCache;
diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py
index ce00e278e..6351f95c6 100644
--- a/Telegram/build/prepare/prepare.py
+++ b/Telegram/build/prepare/prepare.py
@@ -1510,22 +1510,82 @@ mac:
 """)
 
 if buildQt6:
-    stage('qt_6_2_8', """
+    qt6version = '6.2.8' if mac else '6.7.0'
+    qt6version_ = '6_2_8' if mac else '6_7_0'
+    arg0 = 'qt_' + qt6version_
+    arg1 = qt6version
+
+    stage('qt_' + qt6version_, """
+win:
+    git clone -b v{1} https://github.com/qt/qt5.git {0}
 mac:
-    git clone -b v6.2.8-lts-lgpl https://github.com/qt/qt5.git qt_6_2_8
-    cd qt_6_2_8
+    git clone -b v{1}-lts-lgpl https://github.com/qt/qt5.git {0}
+common:
+    cd {0}
     git submodule update --init --recursive qtbase qtimageformats qtsvg
-depends:patches/qtbase_6.2.8/*.patch
+depends:patches/qtbase_{1}/*.patch
     cd qtbase
-    find ../../patches/qtbase_6.2.8 -type f -print0 | sort -z | xargs -0 git apply -v
+win:
+    for /r %%i in (..\\..\\patches\\qtbase_{1}\\*) do git apply %%i -v
     cd ..
-    sed -i.bak 's/tqtc-//' {qtimageformats,qtsvg}/dependencies.yaml
+
+    SET CONFIGURATIONS=-debug
+release:
+    SET CONFIGURATIONS=-debug-and-release
+win:
+    """.format(arg0, arg1) + removeDir("\"%LIBS_DIR%\\Qt-\"" + qt6version) + """
+    SET ANGLE_DIR=%LIBS_DIR%\\tg_angle
+    SET ANGLE_LIBS_DIR=%ANGLE_DIR%\\out
+    SET MOZJPEG_DIR=%LIBS_DIR%\\mozjpeg
+    SET OPENSSL_DIR=%LIBS_DIR%\\openssl3
+    SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\out
+    SET ZLIB_LIBS_DIR=%LIBS_DIR%\\zlib
+    SET WEBP_DIR=%LIBS_DIR%\\libwebp
+    configure -prefix "%LIBS_DIR%\\Qt-{1}" ^
+        %CONFIGURATIONS% ^
+        -force-debug-info ^
+        -opensource ^
+        -confirm-license ^
+        -static ^
+        -static-runtime ^
+        -opengl es2 -no-angle ^
+        -I "%ANGLE_DIR%\\include" ^
+        -D "KHRONOS_STATIC=" ^
+        -D "DESKTOP_APP_QT_STATIC_ANGLE=" ^
+        QMAKE_LIBS_OPENGL_ES2_DEBUG="%ANGLE_LIBS_DIR%\\Debug\\tg_angle.lib %ZLIB_LIBS_DIR%\\Debug\\zlibstaticd.lib d3d9.lib dxgi.lib dxguid.lib" ^
+        QMAKE_LIBS_OPENGL_ES2_RELEASE="%ANGLE_LIBS_DIR%\\Release\\tg_angle.lib %ZLIB_LIBS_DIR%\\Release\\zlibstatic.lib d3d9.lib dxgi.lib dxguid.lib" ^
+        -egl ^
+        QMAKE_LIBS_EGL_DEBUG="%ANGLE_LIBS_DIR%\\Debug\\tg_angle.lib %ZLIB_LIBS_DIR%\\Debug\\zlibstaticd.lib d3d9.lib dxgi.lib dxguid.lib Gdi32.lib User32.lib" ^
+        QMAKE_LIBS_EGL_RELEASE="%ANGLE_LIBS_DIR%\\Release\\tg_angle.lib %ZLIB_LIBS_DIR%\\Release\\zlibstatic.lib d3d9.lib dxgi.lib dxguid.lib Gdi32.lib User32.lib" ^
+        -openssl-linked ^
+        -I "%OPENSSL_DIR%\\include" ^
+        OPENSSL_LIBS_DEBUG="%OPENSSL_LIBS_DIR%.dbg\\libssl.lib %OPENSSL_LIBS_DIR%.dbg\\libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib" ^
+        OPENSSL_LIBS_RELEASE="%OPENSSL_LIBS_DIR%\\libssl.lib %OPENSSL_LIBS_DIR%\\libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib" ^
+        -I "%MOZJPEG_DIR%" ^
+        LIBJPEG_LIBS_DEBUG="%MOZJPEG_DIR%\\Debug\\jpeg-static.lib" ^
+        LIBJPEG_LIBS_RELEASE="%MOZJPEG_DIR%\\Release\\jpeg-static.lib" ^
+        -system-webp ^
+        -I "%WEBP_DIR%\\src" ^
+        -L "%WEBP_DIR%\\out\\release-static\\$X8664\\lib" ^
+        -mp ^
+        -no-feature-netlistmgr ^
+        -nomake examples ^
+        -nomake tests ^
+        -platform win32-msvc
+
+    jom -j%NUMBER_OF_PROCESSORS%
+    jom -j%NUMBER_OF_PROCESSORS% install
+
+mac:
+    find ../../patches/qtbase_{1} -type f -print0 | sort -z | xargs -0 git apply -v
+    cd ..
+    sed -i.bak 's/tqtc-//' {{qtimageformats,qtsvg}}/dependencies.yaml
 
     CONFIGURATIONS=-debug
 release:
     CONFIGURATIONS=-debug-and-release
 mac:
-    ./configure -prefix "$USED_PREFIX/Qt-6.2.8" \
+    ./configure -prefix "$USED_PREFIX/Qt-{1}" \
         $CONFIGURATIONS \
         -force-debug-info \
         -opensource \
@@ -1546,7 +1606,7 @@ mac:
 
     ninja
     ninja install
-""")
+""".format(arg0, arg1))
 
 stage('tg_owt', """
     git clone https://github.com/desktop-app/tg_owt.git