From 37067f17e2523256daab3fa16e9d565262586185 Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Tue, 9 Jan 2024 14:49:35 +0300
Subject: [PATCH] Added new viewer widget for voice messages with ttl.

---
 Telegram/CMakeLists.txt                       |   2 +
 Telegram/Resources/langs/lang.strings         |   3 +
 .../chat_helpers/chat_helpers.style           |  14 +
 .../chat_helpers/ttl_media_layer_widget.cpp   | 273 ++++++++++++++++++
 .../chat_helpers/ttl_media_layer_widget.h     |  22 ++
 .../data/data_document_resolver.cpp           |  15 +-
 .../history/view/history_view_element.h       |   1 +
 .../history/view/history_view_message.cpp     |   4 +-
 .../view/media/history_view_document.cpp      |  77 +++--
 .../view/media/history_view_document.h        |   2 +
 10 files changed, 379 insertions(+), 34 deletions(-)
 create mode 100644 Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp
 create mode 100644 Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h

diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 59c701340..b1fc1570d 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -406,6 +406,8 @@ PRIVATE
     chat_helpers/tabbed_section.h
     chat_helpers/tabbed_selector.cpp
     chat_helpers/tabbed_selector.h
+    chat_helpers/ttl_media_layer_widget.cpp
+    chat_helpers/ttl_media_layer_widget.h
     core/application.cpp
     core/application.h
     core/base_integration.cpp
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 347fdbdc7..cec8bf36e 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1719,6 +1719,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_ttl_voice_expired" = "Voice message expired";
 "lng_ttl_round_sent" = "You sent a self-destructing video message.";
 "lng_ttl_round_expired" = "Round message expired";
+"lng_ttl_voice_tooltip_in" = "This voice message can only be played once.";
+"lng_ttl_voice_tooltip_out" = "This message will disappear once **{user}** plays it once.";
+"lng_ttl_voice_close_in" = "Delete and close";
 
 "lng_profile_add_more_after_create" = "You will be able to add more members after you create the group.";
 "lng_profile_camera_title" = "Capture yourself";
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index c86e98b75..a7f94d301 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -1266,3 +1266,17 @@ dragDropColor: windowActiveTextFg;
 dragMargin: margins(0px, 10px, 0px, 10px);
 dragPadding: margins(20px, 10px, 20px, 10px);
 dragHeight: 72px;
+
+ttlMediaImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) {
+	style: TextStyle(defaultTextStyle) {
+		font: font(14px);
+	}
+}
+ttlMediaButton: RoundButton(defaultActiveButton) {
+	textBg: shadowFg;
+	textBgOver: shadowFg;
+	ripple: universalRippleAnimation;
+	height: 31px;
+	textTop: 6px;
+}
+ttlMediaButtonBottomSkip: 14px;
diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp
new file mode 100644
index 000000000..91403a2c2
--- /dev/null
+++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp
@@ -0,0 +1,273 @@
+/*
+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 "chat_helpers/ttl_media_layer_widget.h"
+
+#include "base/event_filter.h"
+#include "data/data_session.h"
+#include "editor/editor_layer_widget.h"
+#include "history/history.h"
+#include "history/history_item.h"
+#include "history/view/history_view_element.h"
+#include "history/view/media/history_view_document.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "mainwidget.h"
+#include "media/audio/media_audio.h"
+#include "media/player/media_player_instance.h"
+#include "ui/chat/chat_style.h"
+#include "ui/chat/chat_theme.h"
+#include "ui/effects/path_shift_gradient.h"
+#include "ui/painter.h"
+#include "ui/text/text_utilities.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/labels.h"
+#include "ui/widgets/tooltip.h"
+#include "window/themes/window_theme.h"
+#include "window/window_session_controller.h"
+#include "styles/style_chat.h"
+#include "styles/style_chat_helpers.h"
+#include "styles/style_dialogs.h"
+
+namespace ChatHelpers {
+namespace {
+
+class PreviewDelegate final : public HistoryView::DefaultElementDelegate {
+public:
+	PreviewDelegate(
+		not_null<QWidget*> parent,
+		not_null<Ui::ChatStyle*> st,
+		Fn<void()> update);
+
+	bool elementAnimationsPaused() override;
+	not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
+	HistoryView::Context elementContext() override;
+	bool elementIsChatWide() override;
+
+private:
+	const not_null<QWidget*> _parent;
+	const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
+
+};
+
+PreviewDelegate::PreviewDelegate(
+	not_null<QWidget*> parent,
+	not_null<Ui::ChatStyle*> st,
+	Fn<void()> update)
+: _parent(parent)
+, _pathGradient(HistoryView::MakePathShiftGradient(st, update)) {
+}
+
+bool PreviewDelegate::elementAnimationsPaused() {
+	return _parent->window()->isActiveWindow();
+}
+
+not_null<Ui::PathShiftGradient*> PreviewDelegate::elementPathShiftGradient() {
+	return _pathGradient.get();
+}
+
+HistoryView::Context PreviewDelegate::elementContext() {
+	return HistoryView::Context::TTLViewer;
+}
+
+bool PreviewDelegate::elementIsChatWide() {
+	return true;
+}
+
+class PreviewWrap final : public Ui::RpWidget {
+public:
+	PreviewWrap(not_null<Ui::RpWidget*> parent, not_null<HistoryItem*> item);
+	~PreviewWrap();
+
+	[[nodiscard]] rpl::producer<> closeRequests() const;
+
+private:
+	void paintEvent(QPaintEvent *e) override;
+	[[nodiscard]] QRect elementRect() const;
+
+	const not_null<HistoryItem*> _item;
+	const std::unique_ptr<Ui::ChatTheme> _theme;
+	const std::unique_ptr<Ui::ChatStyle> _style;
+	const std::unique_ptr<PreviewDelegate> _delegate;
+	std::unique_ptr<HistoryView::Element> _element;
+	rpl::lifetime _elementLifetime;
+
+	rpl::event_stream<> _closeRequests;
+
+};
+
+PreviewWrap::PreviewWrap(
+	not_null<Ui::RpWidget*> parent,
+	not_null<HistoryItem*> item)
+: RpWidget(parent)
+, _item(item)
+, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
+, _style(std::make_unique<Ui::ChatStyle>(
+	item->history()->session().colorIndicesValue()))
+, _delegate(std::make_unique<PreviewDelegate>(
+	parent,
+	_style.get(),
+	[=] { update(elementRect()); })) {
+	_style->apply(_theme.get());
+
+	const auto session = &_item->history()->session();
+	session->data().viewRepaintRequest(
+	) | rpl::start_with_next([=](not_null<const HistoryView::Element*> view) {
+		if (view == _element.get()) {
+			update(elementRect());
+		}
+	}, lifetime());
+
+	const auto closeCallback = [=] { _closeRequests.fire({}); };
+
+	{
+		const auto close = Ui::CreateChild<Ui::RoundButton>(
+			this,
+			item->out()
+				? tr::lng_close()
+				: tr::lng_ttl_voice_close_in(),
+			st::ttlMediaButton);
+		close->setFullRadius(true);
+		close->setClickedCallback(closeCallback);
+		close->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
+
+		sizeValue(
+		) | rpl::start_with_next([=](const QSize &s) {
+			close->moveToLeft(
+				(s.width() - close->width()) / 2,
+				s.height() - close->height() - st::ttlMediaButtonBottomSkip);
+		}, close->lifetime());
+	}
+
+	QWidget::setAttribute(Qt::WA_OpaquePaintEvent, false);
+	_element = _item->createView(_delegate.get());
+
+	{
+		_element->initDimensions();
+		widthValue(
+		) | rpl::filter([=](int width) {
+			return width > st::msgMinWidth;
+		}) | rpl::start_with_next([=](int width) {
+			_element->resizeGetHeight(width);
+		}, _elementLifetime);
+	}
+
+	{
+		auto text = item->out()
+			? tr::lng_ttl_voice_tooltip_out(
+				lt_user,
+				rpl::single(
+					item->history()->peer->name()
+				) | rpl::map(Ui::Text::RichLangValue),
+				Ui::Text::RichLangValue)
+			: tr::lng_ttl_voice_tooltip_in(Ui::Text::RichLangValue);
+		const auto tooltip = Ui::CreateChild<Ui::ImportantTooltip>(
+			this,
+			object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
+				this,
+				Ui::MakeNiceTooltipLabel(
+					parent,
+					std::move(text),
+					st::dialogsStoriesTooltipMaxWidth,
+					st::ttlMediaImportantTooltipLabel),
+				st::defaultImportantTooltip.padding),
+			st::dialogsStoriesTooltip);
+		tooltip->toggleFast(true);
+		sizeValue(
+		) | rpl::filter(
+			[](const QSize &s) { return !s.isNull(); }
+		) | rpl::take(1) | rpl::start_with_next([=](const QSize &s) {
+			if (s.isEmpty()) {
+				return;
+			}
+			auto area = elementRect();
+			area.setWidth(_element->media()
+				? _element->media()->width()
+				: _element->width());
+			tooltip->pointAt(area, RectPart::Top, [=](QSize size) {
+				return QPoint{
+					(area.width() - size.width()) / 2,
+					(s.height() - size.height() * 2 - _element->height()) / 2
+						- st::defaultImportantTooltip.padding.top(),
+				};
+			});
+		}, tooltip->lifetime());
+	}
+
+	HistoryView::TTLVoiceStops(
+		item->fullId()
+	) | rpl::start_with_next(closeCallback, lifetime());
+}
+
+QRect PreviewWrap::elementRect() const {
+	return QRect(
+		(width() - _element->width()) / 2,
+		(height() - _element->height()) / 2,
+		_element->width(),
+		_element->height());
+}
+
+rpl::producer<> PreviewWrap::closeRequests() const {
+	return _closeRequests.events();
+}
+
+PreviewWrap::~PreviewWrap() {
+	_elementLifetime.destroy();
+	_element = nullptr;
+}
+
+void PreviewWrap::paintEvent(QPaintEvent *e) {
+	if (!_element) {
+		return;
+	}
+
+	auto p = Painter(this);
+	const auto r = rect();
+
+	auto context = _theme->preparePaintContext(
+		_style.get(),
+		r,
+		e->rect(),
+		!window()->isActiveWindow());
+	context.outbg = _element->hasOutLayout();
+
+	p.translate(
+		(r.width() - _element->width()) / 2,
+		(r.height() - _element->height()) / 2);
+	_element->draw(p, context);
+}
+
+} // namespace
+
+void ShowTTLMediaLayerWidget(
+		not_null<Window::SessionController*> controller,
+		not_null<HistoryItem*> item) {
+	const auto parent = controller->content();
+	const auto show = controller->uiShow();
+	auto preview = base::make_unique_q<PreviewWrap>(parent, item);
+	preview->closeRequests(
+	) | rpl::start_with_next([=] {
+		show->hideLayer();
+	}, preview->lifetime());
+	auto layer = std::make_unique<Editor::LayerWidget>(
+		parent,
+		std::move(preview));
+	layer->lifetime().add([] { ::Media::Player::instance()->stop(); });
+	base::install_event_filter(layer.get(), [=](not_null<QEvent*> e) {
+		if (e->type() == QEvent::KeyPress) {
+			const auto k = static_cast<QKeyEvent*>(e.get());
+			if (k->key() == Qt::Key_Escape) {
+				show->hideLayer();
+			}
+			return base::EventFilterResult::Cancel;
+		}
+		return base::EventFilterResult::Continue;
+	});
+	controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
+}
+
+} // namespace ChatHelpers
diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h
new file mode 100644
index 000000000..f31d04982
--- /dev/null
+++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h
@@ -0,0 +1,22 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+class HistoryItem;
+
+namespace Window {
+class SessionController;
+} // namespace Window
+
+namespace ChatHelpers {
+
+void ShowTTLMediaLayerWidget(
+	not_null<Window::SessionController*> controller,
+	not_null<HistoryItem*> item);
+
+} // namespace ChatHelpers
diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp
index 2c34719ff..8ab502564 100644
--- a/Telegram/SourceFiles/data/data_document_resolver.cpp
+++ b/Telegram/SourceFiles/data/data_document_resolver.cpp
@@ -9,7 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "base/options.h"
 #include "base/platform/base_platform_info.h"
-#include "ui/boxes/confirm_box.h"
+#include "boxes/abstract_box.h" // Ui::show().
+#include "chat_helpers/ttl_media_layer_widget.h"
 #include "core/application.h"
 #include "core/core_settings.h"
 #include "core/mime_type.h"
@@ -17,17 +18,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_document_media.h"
 #include "data/data_file_click_handler.h"
 #include "data/data_session.h"
-#include "history/view/media/history_view_gif.h"
 #include "history/history.h"
 #include "history/history_item.h"
-#include "media/player/media_player_instance.h"
+#include "history/view/media/history_view_gif.h"
 #include "lang/lang_keys.h"
+#include "media/player/media_player_instance.h"
 #include "platform/platform_file_utilities.h"
+#include "ui/boxes/confirm_box.h"
 #include "ui/chat/chat_theme.h"
 #include "ui/text/text_utilities.h"
 #include "ui/widgets/checkbox.h"
 #include "window/window_session_controller.h"
-#include "boxes/abstract_box.h" // Ui::show().
 #include "styles/style_layers.h"
 
 #include <QtCore/QBuffer>
@@ -298,6 +299,12 @@ void ResolveDocument(
 			|| document->isVoiceMessage()
 			|| document->isVideoMessage()) {
 			::Media::Player::instance()->playPause({ document, msgId });
+			if (controller
+				&& item
+				&& item->media()
+				&& item->media()->ttlSeconds()) {
+				ChatHelpers::ShowTTLMediaLayerWidget(controller, item);
+			}
 		} else {
 			showDocument();
 		}
diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h
index 3b524915f..134a7bf70 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.h
+++ b/Telegram/SourceFiles/history/view/history_view_element.h
@@ -58,6 +58,7 @@ enum class Context : char {
 	AdminLog,
 	ContactPreview,
 	SavedSublist,
+	TTLViewer,
 };
 
 enum class OnlyEmojiAndSpaces : char {
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index b53fb1454..d7c9e2f16 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -2005,6 +2005,7 @@ bool Message::hasFromPhoto() const {
 		return !item->out() && !item->history()->peer->isUser();
 	} break;
 	case Context::ContactPreview:
+	case Context::TTLViewer:
 		return false;
 	}
 	Unexpected("Context in Message::hasFromPhoto.");
@@ -3200,9 +3201,10 @@ bool Message::hasFromName() const {
 		return false;
 	} break;
 	case Context::ContactPreview:
+	case Context::TTLViewer:
 		return false;
 	}
-	Unexpected("Context in Message::hasFromPhoto.");
+	Unexpected("Context in Message::hasFromName.");
 }
 
 bool Message::displayFromName() const {
diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp
index 7ca7bc747..2dc91de74 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp
@@ -134,7 +134,11 @@ void DrawCornerBadgeTTL(
 			});
 			animate(animate);
 		});
+	const auto weak = std::weak_ptr(lifetime);
 	return [=](QPainter &p, QRect r, QColor c) {
+		if (weak.expired()) {
+			return;
+		}
 		(state->idle ? state->idle : state->start)->paintInCenter(p, r, c);
 	};
 }
@@ -326,41 +330,36 @@ Document::Document(
 	}
 
 	if ((_data->isVoiceMessage() || isRound)
-		&& IsVoiceOncePlayable(_parent->data())) {
-		_parent->data()->removeFromSharedMediaIndex();
-		setDocumentLinks(_data, realParent, [=] {
-			_openl = nullptr;
-
+		&& _parent->data()->media()->ttlSeconds()) {
+		const auto fullId = _realParent->fullId();
+		if (_parent->delegate()->elementContext() == Context::TTLViewer) {
 			auto lifetime = std::make_shared<rpl::lifetime>();
-			rpl::merge(
-				::Media::Player::instance()->updatedNotifier(
-				) | rpl::filter([=](::Media::Player::TrackState state) {
-					using State = ::Media::Player::State;
-					const auto badState = state.state == State::Stopped
-						|| state.state == State::StoppedAtEnd
-						|| state.state == State::StoppedAtError
-						|| state.state == State::StoppedAtStart;
-					return (state.id.contextId() != _realParent->fullId())
-						&& !badState;
-				}) | rpl::to_empty,
-				::Media::Player::instance()->tracksFinished(
-				) | rpl::filter([=](AudioMsgId::Type type) {
-					return (type == AudioMsgId::Type::Voice);
-				}) | rpl::to_empty,
-				::Media::Player::instance()->stops(AudioMsgId::Type::Voice)
-			) | rpl::start_with_next([=]() mutable {
-				_drawTtl = nullptr;
-				const auto item = _parent->data();
+			TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable {
 				if (lifetime) {
 					base::take(lifetime)->destroy();
 				}
-				// Destroys this.
-				ClearMediaAsExpired(item);
 			}, *lifetime);
 			_drawTtl = CreateTtlPaintCallback(lifetime, [=] { repaint(); });
+		} else if (!_parent->data()->out()) {
+			_parent->data()->removeFromSharedMediaIndex();
+			setDocumentLinks(_data, realParent, [=] {
+				_openl = nullptr;
 
-			return false;
-		});
+				auto lifetime = std::make_shared<rpl::lifetime>();
+				TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable {
+					const auto item = _parent->data();
+					if (lifetime) {
+						base::take(lifetime)->destroy();
+					}
+					// Destroys this.
+					ClearMediaAsExpired(item);
+				}, *lifetime);
+
+				return false;
+			});
+		} else {
+			setDocumentLinks(_data, realParent);
+		}
 	} else {
 		setDocumentLinks(_data, realParent);
 	}
@@ -918,7 +917,8 @@ void Document::draw(
 			.highlight = highlightRequest ? &*highlightRequest : nullptr,
 		});
 	}
-	if (_parent->data()->media() && _parent->data()->media()->ttlSeconds()) {
+	if ((_parent->data()->media() && _parent->data()->media()->ttlSeconds())
+		&& _openl) {
 		const auto &fg = context.outbg
 			? st::historyFileOutIconFg
 			: st::historyFileInIconFg;
@@ -1739,4 +1739,23 @@ bool DrawThumbnailAsSongCover(
 	return true;
 }
 
+rpl::producer<> TTLVoiceStops(FullMsgId fullId) {
+	return rpl::merge(
+		::Media::Player::instance()->updatedNotifier(
+		) | rpl::filter([=](::Media::Player::TrackState state) {
+			using State = ::Media::Player::State;
+			const auto badState = state.state == State::Stopped
+				|| state.state == State::StoppedAtEnd
+				|| state.state == State::StoppedAtError
+				|| state.state == State::StoppedAtStart;
+			return (state.id.contextId() != fullId) && !badState;
+		}) | rpl::to_empty,
+		::Media::Player::instance()->tracksFinished(
+		) | rpl::filter([=](AudioMsgId::Type type) {
+			return (type == AudioMsgId::Type::Voice);
+		}) | rpl::to_empty,
+		::Media::Player::instance()->stops(AudioMsgId::Type::Voice)
+	);
+}
+
 } // namespace HistoryView
diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h
index 5fa8dc1da..3d29651c4 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_document.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_document.h
@@ -181,4 +181,6 @@ bool DrawThumbnailAsSongCover(
 	const QRect &rect,
 	bool selected = false);
 
+rpl::producer<> TTLVoiceStops(FullMsgId fullId);
+
 } // namespace HistoryView