From 221b0d19c7ff0f379b542df1858a3562fdf17581 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 3 Mar 2023 20:52:21 +0400
Subject: [PATCH] Support media covers for bot descriptions.

For that replace custom handling of _botAbout by a fake message.
---
 Telegram/SourceFiles/data/data_types.h        |   3 +
 Telegram/SourceFiles/data/data_user.cpp       |  31 +-
 Telegram/SourceFiles/data/data_user.h         |   8 +-
 .../history/history_inner_widget.cpp          | 306 ++++++++----------
 Telegram/SourceFiles/history/history_item.h   |   3 +
 .../history/view/history_view_element.cpp     |  17 +
 .../history/view/history_view_element.h       |   8 +
 .../history/view/history_view_message.cpp     |  72 ++++-
 .../history/view/media/history_view_gif.cpp   |  41 ++-
 .../history/view/media/history_view_photo.cpp |  41 ++-
 10 files changed, 325 insertions(+), 205 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h
index 375e9da07..aa3ade241 100644
--- a/Telegram/SourceFiles/data/data_types.h
+++ b/Telegram/SourceFiles/data/data_types.h
@@ -293,6 +293,9 @@ enum class MessageFlag : uint64 {
 
 	OnlyEmojiAndSpaces    = (1ULL << 34),
 	OnlyEmojiAndSpacesSet = (1ULL << 35),
+
+	// Fake message with bot cover and information.
+	FakeBotAbout          = (1ULL << 36),
 };
 inline constexpr bool is_flag_type(MessageFlag) { return true; }
 using MessageFlags = base::flags<MessageFlag>;
diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp
index ae0fd3c61..f474f701e 100644
--- a/Telegram/SourceFiles/data/data_user.cpp
+++ b/Telegram/SourceFiles/data/data_user.cpp
@@ -32,8 +32,7 @@ using UpdateFlag = Data::PeerUpdate::Flag;
 
 } // namespace
 
-BotInfo::BotInfo() : text(st::msgMinWidth) {
-}
+BotInfo::BotInfo() = default;
 
 UserData::UserData(not_null<Data::Session*> owner, PeerId id)
 : PeerData(owner, id)
@@ -188,10 +187,30 @@ void UserData::setBotInfo(const MTPBotInfo &info) {
 			return;
 		}
 
-		QString desc = qs(d.vdescription().value_or_empty());
-		if (botInfo->description != desc) {
-			botInfo->description = desc;
-			botInfo->text = Ui::Text::String(st::msgMinWidth);
+		const auto description = qs(d.vdescription().value_or_empty());
+		if (botInfo->description != description) {
+			botInfo->description = description;
+			++botInfo->descriptionVersion;
+		}
+		if (const auto photo = d.vdescription_photo()) {
+			const auto parsed = owner().processPhoto(*photo);
+			if (botInfo->photo != parsed) {
+				botInfo->photo = parsed;
+				++botInfo->descriptionVersion;
+			}
+		} else if (botInfo->photo) {
+			botInfo->photo = nullptr;
+			++botInfo->descriptionVersion;
+		}
+		if (const auto document = d.vdescription_document()) {
+			const auto parsed = owner().processDocument(*document);
+			if (botInfo->document != parsed) {
+				botInfo->document = parsed;
+				++botInfo->descriptionVersion;
+			}
+		} else if (botInfo->document) {
+			botInfo->document = nullptr;
+			++botInfo->descriptionVersion;
 		}
 
 		auto commands = d.vcommands()
diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h
index a51001095..e0355d8e9 100644
--- a/Telegram/SourceFiles/data/data_user.h
+++ b/Telegram/SourceFiles/data/data_user.h
@@ -24,9 +24,13 @@ struct BotInfo {
 	bool cantJoinGroups = false;
 	bool supportsAttachMenu = false;
 	int version = 0;
-	QString description, inlinePlaceholder;
+	int descriptionVersion = 0;
+	QString description;
+	QString inlinePlaceholder;
 	std::vector<Data::BotCommand> commands;
-	Ui::Text::String text;
+
+	PhotoData *photo = nullptr;
+	DocumentData *document = nullptr;
 
 	QString botMenuButtonText;
 	QString botMenuButtonUrl;
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index c8b8416c1..fbd913acc 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "core/crash_reports.h"
 #include "core/click_handler_types.h"
 #include "history/history.h"
+#include "history/admin_log/history_admin_log_item.h"
 #include "history/history_item.h"
 #include "history/history_item_helpers.h"
 #include "history/view/media/history_view_media.h"
@@ -326,41 +327,100 @@ public:
 
 };
 
-class HistoryInner::BotAbout : public ClickHandlerHost {
+class HistoryInner::BotAbout final : public ClickHandlerHost {
 public:
-	BotAbout(not_null<HistoryInner*> parent, not_null<BotInfo*> info);
+	BotAbout(
+		not_null<History*> history,
+		not_null<HistoryView::ElementDelegate*> delegate);
 
-	// ClickHandlerHost interface
-	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
-	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
+	[[nodiscard]] not_null<History*> history() const;
+	[[nodiscard]] HistoryView::Element *view() const;
+	[[nodiscard]] HistoryItem *item() const;
 
-	not_null<BotInfo*> info;
-	int width = 0;
+	bool refresh();
+
+	int top = 0;
 	int height = 0;
-	QRect rect;
 
 private:
-	not_null<HistoryInner*>  _parent;
+	const not_null<History*> _history;
+	const not_null<HistoryView::ElementDelegate*> _delegate;
+	AdminLog::OwnedItem _item;
+	int _version = 0;
 
 };
 
 HistoryInner::BotAbout::BotAbout(
-	not_null<HistoryInner*> parent,
-	not_null<BotInfo*> info)
-: info(info)
-, _parent(parent) {
+	not_null<History*> history,
+	not_null<HistoryView::ElementDelegate*> delegate)
+: _history(history)
+, _delegate(delegate) {
 }
 
-void HistoryInner::BotAbout::clickHandlerActiveChanged(
-		const ClickHandlerPtr &p,
-		bool active) {
-	_parent->update(rect);
+not_null<History*> HistoryInner::BotAbout::history() const {
+	return _history;
 }
 
-void HistoryInner::BotAbout::clickHandlerPressedChanged(
-		const ClickHandlerPtr &p,
-		bool pressed) {
-	_parent->update(rect);
+HistoryView::Element *HistoryInner::BotAbout::view() const {
+	return _item.get();
+}
+
+HistoryItem *HistoryInner::BotAbout::item() const {
+	if (const auto element = view()) {
+		return element->data();
+	}
+	return nullptr;
+}
+
+bool HistoryInner::BotAbout::refresh() {
+	const auto bot = _history->peer->asUser();
+	const auto info = bot ? bot->botInfo.get() : nullptr;
+	if (!info) {
+		if (_item) {
+			_item = {};
+			return true;
+		}
+		_version = 0;
+		return false;
+	}
+	const auto version = info->descriptionVersion;
+	if (_version == version) {
+		return false;
+	}
+	_version = version;
+
+	const auto flags = MessageFlag::FakeBotAbout
+		| MessageFlag::FakeHistoryItem
+		| MessageFlag::Local;
+	const auto postAuthor = QString();
+	const auto date = TimeId(0);
+	const auto replyTo = MsgId(0);
+	const auto viaBotId = UserId(0);
+	const auto groupedId = uint64(0);
+	const auto textWithEntities = TextUtilities::ParseEntities(
+		info->description,
+		Ui::ItemTextBotNoMonoOptions().flags);
+	const auto make = [&](auto &&a, auto &&b, auto &&...other) {
+		return _history->makeMessage(
+			_history->nextNonHistoryEntryId(),
+			flags,
+			replyTo,
+			viaBotId,
+			date,
+			bot->id,
+			postAuthor,
+			std::forward<decltype(a)>(a),
+			std::forward<decltype(b)>(b),
+			HistoryMessageMarkupData(),
+			std::forward<decltype(other)>(other)...);
+	};
+	const auto item = info->document
+		? make(info->document, textWithEntities)
+		: info->photo
+		? make(info->photo, textWithEntities)
+		: make(textWithEntities, MTP_messageMediaEmpty(), groupedId);
+	_item = AdminLog::OwnedItem(_delegate, item);
+	return true;
 }
 
 HistoryInner::HistoryInner(
@@ -971,41 +1031,10 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
 
 	const auto historyDisplayedEmpty = _history->isDisplayedEmpty()
 		&& (!_migrated || _migrated->isDisplayedEmpty());
-	if (_botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) {
-		const auto st = context.st;
-		const auto stm = &st->messageStyle(false, false);
-		if (clip.y() < _botAbout->rect.y() + _botAbout->rect.height() && clip.y() + clip.height() > _botAbout->rect.y()) {
-			p.setTextPalette(stm->textPalette);
-			using Corner = Ui::BubbleCornerRounding;
-			const auto rounding = Ui::BubbleRounding{
-				Corner::Large,
-				Corner::Large,
-				Corner::Large,
-				Corner::Large,
-			};
-			Ui::PaintBubble(p, Ui::SimpleBubble{
-				.st = st,
-				.geometry = _botAbout->rect,
-				.pattern = context.bubblesPattern,
-				.patternViewport = context.viewport,
-				.outerWidth = width(),
-				.selected = false,
-				.outbg = false,
-				.rounding = rounding,
-			});
-
-			auto top = _botAbout->rect.top() + st::msgPadding.top();
-			if (!_history->peer->isRepliesChat()) {
-				p.setFont(st::msgNameFont);
-				p.setPen(st->dialogsNameFg());
-				p.drawText(_botAbout->rect.left() + st::msgPadding.left(), top + st::msgNameFont->ascent, tr::lng_bot_description(tr::now));
-				top += +st::msgNameFont->height + st::botDescSkip;
-			}
-
-			p.setPen(stm->historyTextFg);
-			_botAbout->info->text.draw(p, _botAbout->rect.left() + st::msgPadding.left(), top, _botAbout->width);
-
-			p.restoreTextPalette();
+	if (const auto view = _botAbout ? _botAbout->view() : nullptr) {
+		if (clip.y() < _botAbout->top + _botAbout->height
+			&& clip.y() + clip.height() > _botAbout->top) {
+			view->draw(p, context);
 		}
 	} else if (historyDisplayedEmpty) {
 		paintEmpty(p, context.st, width(), height());
@@ -2939,9 +2968,11 @@ void HistoryInner::recountHistoryGeometry() {
 	}
 
 	const auto visibleHeight = _scroll->height();
-	int oldHistoryPaddingTop = qMax(visibleHeight - historyHeight() - st::historyPaddingBottom, 0);
-	if (_botAbout && !_botAbout->info->text.isEmpty()) {
-		accumulate_max(oldHistoryPaddingTop, st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height);
+	auto oldHistoryPaddingTop = qMax(
+		visibleHeight - historyHeight() - st::historyPaddingBottom,
+		0);
+	if (_botAbout) {
+		accumulate_max(oldHistoryPaddingTop, _botAbout->height);
 	}
 
 	_history->resizeToWidth(_contentWidth);
@@ -2968,39 +2999,20 @@ void HistoryInner::recountHistoryGeometry() {
 	}
 
 	updateBotInfo(false);
-	if (_botAbout && !_botAbout->info->text.isEmpty()) {
-		int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
-		if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
-		tw -= st::msgPadding.left() + st::msgPadding.right();
-		const auto descriptionWidth = _history->peer->isRepliesChat()
-			? 0
-			: st::msgNameFont->width(tr::lng_bot_description(tr::now));
-		int32 mw = qMax(_botAbout->info->text.maxWidth(), descriptionWidth);
-		if (tw > mw) tw = mw;
-
-		_botAbout->width = tw;
-		_botAbout->height = _botAbout->info->text.countHeight(_botAbout->width);
-
-		const auto descriptionHeight = _history->peer->isRepliesChat()
-			? 0
-			: (st::msgNameFont->height + st::botDescSkip);
-		int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
-		int32 descMaxWidth = _scroll->width();
-		if (_isChatWide) {
-			descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
-		}
-		int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left();
-		int32 descAtY = qMin(_historyPaddingTop - descH, qMax(0, (_scroll->height() - descH) / 2)) + st::msgMargin.top();
-
-		_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
+	if (const auto view = _botAbout ? _botAbout->view() : nullptr) {
+		_botAbout->height = view->resizeGetHeight(_contentWidth);
+		_botAbout->top = qMin(
+			_historyPaddingTop - _botAbout->height,
+			qMax(0, (_scroll->height() - _botAbout->height) / 2));
 	} else if (_botAbout) {
-		_botAbout->width = _botAbout->height = 0;
-		_botAbout->rect = QRect();
+		_botAbout->top = _botAbout->height = 0;
 	}
 
-	int newHistoryPaddingTop = qMax(visibleHeight - historyHeight() - st::historyPaddingBottom, 0);
-	if (_botAbout && !_botAbout->info->text.isEmpty()) {
-		accumulate_max(newHistoryPaddingTop, st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height);
+	auto newHistoryPaddingTop = qMax(
+		visibleHeight - historyHeight() - st::historyPaddingBottom,
+		0);
+	if (_botAbout) {
+		accumulate_max(newHistoryPaddingTop, _botAbout->height);
 	}
 
 	auto historyPaddingTopDelta = (newHistoryPaddingTop - oldHistoryPaddingTop);
@@ -3014,48 +3026,15 @@ void HistoryInner::recountHistoryGeometry() {
 }
 
 void HistoryInner::updateBotInfo(bool recount) {
-	int newh = 0;
-	if (_botAbout && !_botAbout->info->description.isEmpty()) {
-		if (_botAbout->info->text.isEmpty()) {
-			_botAbout->info->text.setText(
-				st::messageTextStyle,
-				_botAbout->info->description,
-				Ui::ItemTextBotNoMonoOptions());
-			if (recount) {
-				int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
-				if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
-				tw -= st::msgPadding.left() + st::msgPadding.right();
-				const auto descriptionWidth = _history->peer->isRepliesChat()
-					? 0
-					: st::msgNameFont->width(tr::lng_bot_description(tr::now));
-				int32 mw = qMax(_botAbout->info->text.maxWidth(), descriptionWidth);
-				if (tw > mw) tw = mw;
-
-				_botAbout->width = tw;
-				newh = _botAbout->info->text.countHeight(_botAbout->width);
-			}
-		} else if (recount) {
-			newh = _botAbout->height;
-		}
-	}
-	if (recount && _botAbout) {
-		if (_botAbout->height != newh) {
-			_botAbout->height = newh;
+	if (!_botAbout) {
+		return;
+	} else if (_botAbout->refresh() && recount && _contentWidth > 0) {
+		const auto view = _botAbout->view();
+		const auto now = view ? view->resizeGetHeight(_contentWidth) : 0;
+		if (_botAbout->height != now) {
+			_botAbout->height = now;
 			updateSize();
 		}
-		if (_botAbout->height > 0) {
-			const auto descriptionHeight = _history->peer->isRepliesChat()
-				? 0
-				: (st::msgNameFont->height + st::botDescSkip);
-			int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
-			int32 descAtX = (_scroll->width() - _botAbout->width) / 2 - st::msgPadding.left();
-			int32 descAtY = qMin(_historyPaddingTop - descH, (_scroll->height() - descH) / 2) + st::msgMargin.top();
-
-			_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
-		} else {
-			_botAbout->width = 0;
-			_botAbout->rect = QRect();
-		}
 	}
 }
 
@@ -3200,24 +3179,15 @@ void HistoryInner::changeItemsRevealHeight(int revealHeight) {
 void HistoryInner::updateSize() {
 	const auto visibleHeight = _scroll->height();
 	const auto itemsHeight = historyHeight() - _revealHeight;
-	int newHistoryPaddingTop = qMax(visibleHeight - itemsHeight - st::historyPaddingBottom, 0);
-	if (_botAbout && !_botAbout->info->text.isEmpty()) {
-		accumulate_max(newHistoryPaddingTop, st::msgMargin.top() + st::msgMargin.bottom() + st::msgPadding.top() + st::msgPadding.bottom() + st::msgNameFont->height + st::botDescSkip + _botAbout->height);
+	auto newHistoryPaddingTop = qMax(visibleHeight - itemsHeight - st::historyPaddingBottom, 0);
+	if (_botAbout) {
+		accumulate_max(newHistoryPaddingTop, _botAbout->height);
 	}
 
 	if (_botAbout && _botAbout->height > 0) {
-		const auto descriptionHeight = _history->peer->isRepliesChat()
-			? 0
-			: (st::msgNameFont->height + st::botDescSkip);
-		int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
-		int32 descMaxWidth = _scroll->width();
-		if (_isChatWide) {
-			descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
-		}
-		int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left();
-		int32 descAtY = qMin(newHistoryPaddingTop - descH, qMax(0, (_scroll->height() - descH) / 2)) + st::msgMargin.top();
-
-		_botAbout->rect = QRect(descAtX, descAtY, _botAbout->width + st::msgPadding.left() + st::msgPadding.right(), descH - st::msgMargin.top() - st::msgMargin.bottom());
+		_botAbout->top = qMin(
+			newHistoryPaddingTop - _botAbout->height,
+			qMax(0, (_scroll->height() - _botAbout->height) / 2));
 	}
 
 	if (_historyPaddingTop != newHistoryPaddingTop) {
@@ -3261,6 +3231,7 @@ void HistoryInner::leaveEventHook(QEvent *e) {
 }
 
 HistoryInner::~HistoryInner() {
+	_botAbout = nullptr;
 	for (const auto &item : _animatedStickersPlayed) {
 		if (const auto view = item->mainView()) {
 			if (const auto media = view->media()) {
@@ -3632,12 +3603,15 @@ void HistoryInner::mouseActionUpdate() {
 		dragState = reactionState;
 		lnkhost = reactionView;
 	} else if (point.y() < _historyPaddingTop) {
-		if (_botAbout && !_botAbout->info->text.isEmpty() && _botAbout->height > 0) {
-			dragState = TextState(nullptr, _botAbout->info->text.getState(
-				point - _botAbout->rect.topLeft() - QPoint(st::msgPadding.left(), st::msgPadding.top() + st::botDescSkip + st::msgNameFont->height),
-				_botAbout->width));
+		if (const auto view = _botAbout ? _botAbout->view() : nullptr) {
+			StateRequest request;
+			if (base::IsAltPressed()) {
+				request.flags &= ~Ui::Text::StateRequest::Flag::LookupLink;
+			}
+			const auto relative = point - QPoint(0, _botAbout->top);
+			dragState = view->textState(relative, request);
 			_dragStateItem = session().data().message(dragState.itemId);
-			lnkhost = _botAbout.get();
+			lnkhost = view;
 		}
 	} else if (item) {
 		if (item != _mouseActionItem || (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
@@ -3947,16 +3921,22 @@ void HistoryInner::setCanHaveFromUserpicsSponsored(bool value) {
 int HistoryInner::itemTop(const HistoryItem *item) const {
 	if (!item) {
 		return -2;
+	} else if (_botAbout && item == _botAbout->item()) {
+		return _botAbout->top;
 	}
 	return itemTop(item->mainView());
 }
 
 int HistoryInner::itemTop(const Element *view) const {
-	if (!view || view->data()->mainView() != view) {
+	if (!view) {
+		return -1;
+	} else if (_botAbout && view == _botAbout->view()) {
+		return _botAbout->top;
+	} else if (view->data()->mainView() != view) {
 		return -1;
 	}
 
-	auto top = (view->history() == _history)
+	const auto top = (view->history() == _history)
 		? historyTop()
 		: (view->history() == _migrated
 			? migratedTop()
@@ -3990,21 +3970,17 @@ auto HistoryInner::findViewForPinnedTracking(int top) const
 }
 
 void HistoryInner::notifyIsBotChanged() {
-	const auto newinfo = _peer->isUser()
-		? _peer->asUser()->botInfo.get()
-		: nullptr;
-	if ((!newinfo && !_botAbout)
-		|| (newinfo && _botAbout && _botAbout->info == newinfo)) {
-		return;
-	}
-
-	if (newinfo) {
-		_botAbout = std::make_unique<BotAbout>(this, newinfo);
-		if (newinfo && !newinfo->inited) {
-			session().api().requestFullPeer(_peer);
+	if (const auto user = _peer->asUser()) {
+		if (const auto info = user->botInfo.get()) {
+			if (!_botAbout) {
+				_botAbout = std::make_unique<BotAbout>(
+					_history,
+					_history->delegateMixin()->delegate());
+			}
+			if (!info->inited) {
+				session().api().requestFullPeer(_peer);
+			}
 		}
-	} else {
-		_botAbout = nullptr;
 	}
 }
 
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index 47ab8f01a..629cc7267 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -306,6 +306,9 @@ public:
 	[[nodiscard]] bool isLocal() const {
 		return _flags & MessageFlag::Local;
 	}
+	[[nodiscard]] bool isFakeBotAbout() const {
+		return _flags & MessageFlag::FakeBotAbout;
+	}
 	[[nodiscard]] bool isRegular() const;
 	[[nodiscard]] bool isUploading() const;
 	void sendFailed();
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index 0bab7aee2..8f13011cd 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -354,6 +354,20 @@ void DateBadge::paint(
 	ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide);
 }
 
+void FakeBotAboutTop::init() {
+	if (!text.isEmpty()) {
+		return;
+	}
+	text.setText(
+		st::msgNameStyle,
+		tr::lng_bot_description(tr::now),
+		Ui::NameTextOptions());
+	maxWidth = st::msgPadding.left()
+		+ text.maxWidth()
+		+ st::msgPadding.right();
+	height = st::msgNameStyle.font->height + st::botDescSkip;
+}
+
 Element::Element(
 	not_null<ElementDelegate*> delegate,
 	not_null<HistoryItem*> data,
@@ -376,6 +390,9 @@ Element::Element(
 	if (_context == Context::History) {
 		history()->setHasPendingResizedItems();
 	}
+	if (data->isFakeBotAbout() && !data->history()->peer->isRepliesChat()) {
+		AddComponents(FakeBotAboutTop::Bit());
+	}
 }
 
 not_null<ElementDelegate*> Element::delegate() const {
diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h
index 7c2ab77e7..a2b9cb049 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.h
+++ b/Telegram/SourceFiles/history/view/history_view_element.h
@@ -230,6 +230,14 @@ struct DateBadge : public RuntimeComponent<DateBadge, Element> {
 
 };
 
+struct FakeBotAboutTop : public RuntimeComponent<FakeBotAboutTop, Element> {
+	void init();
+
+	Ui::Text::String text;
+	int maxWidth = 0;
+	int height = 0;
+};
+
 struct TopicButton {
 	std::unique_ptr<Ui::RippleAnimation> ripple;
 	ClickHandlerPtr link;
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index a3aa5e99c..e9c773ff9 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -530,6 +530,12 @@ QSize Message::performCountOptimalSize() {
 	refreshInfoSkipBlock();
 
 	const auto media = this->media();
+	const auto botTop = item->isFakeBotAbout()
+		? Get<FakeBotAboutTop>()
+		: nullptr;
+	if (botTop) {
+		botTop->init();
+	}
 
 	auto maxWidth = 0;
 	auto minHeight = 0;
@@ -560,13 +566,14 @@ QSize Message::performCountOptimalSize() {
 		}
 
 		// Entry page is always a bubble bottom.
+		const auto withVisibleText = hasVisibleText();
 		auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/);
 		auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());
 		maxWidth = plainMaxWidth();
 		if (context() == Context::Replies && item->isDiscussionPost()) {
 			maxWidth = std::max(maxWidth, st::msgMaxWidth);
 		}
-		minHeight = hasVisibleText() ? text().minHeight() : 0;
+		minHeight = withVisibleText ? text().minHeight() : 0;
 		if (reactionsInBubble) {
 			const auto reactionsMaxWidth = st::msgPadding.left()
 				+ _reactions->maxWidth()
@@ -604,9 +611,14 @@ QSize Message::performCountOptimalSize() {
 				const auto innerWidth = maxWidth
 					- st::msgPadding.left()
 					- st::msgPadding.right();
-				if (hasVisibleText() && maxWidth < plainMaxWidth()) {
-					minHeight -= text().minHeight();
-					minHeight += text().countHeight(innerWidth);
+				if (withVisibleText) {
+					if (botTop) {
+						minHeight += botTop->height;
+					}
+					if (maxWidth < plainMaxWidth()) {
+						minHeight -= text().minHeight();
+						minHeight += text().countHeight(innerWidth);
+					}
 				}
 				if (reactionsInBubble) {
 					minHeight -= _reactions->minHeight();
@@ -678,6 +690,10 @@ QSize Message::performCountOptimalSize() {
 				accumulate_max(maxWidth, entry->maxWidth());
 				minHeight += entry->minHeight();
 			}
+			if (withVisibleText && botTop) {
+				accumulate_max(maxWidth, botTop->maxWidth);
+				minHeight += botTop->height;
+			}
 		}
 		accumulate_max(maxWidth, minWidthForMedia());
 	} else if (media) {
@@ -1467,6 +1483,15 @@ void Message::paintText(
 	p.setPen(stm->historyTextFg);
 	p.setFont(st::msgFont);
 	prepareCustomEmojiPaint(p, context, text());
+	if (const auto botTop = Get<FakeBotAboutTop>()) {
+		botTop->text.drawLeftElided(
+			p,
+			trect.x(),
+			trect.y(),
+			trect.width(),
+			width());
+		trect.setY(trect.y() + botTop->height);
+	}
 	text().draw(p, {
 		.position = trect.topLeft(),
 		.availableWidth = trect.width(),
@@ -2281,6 +2306,8 @@ bool Message::getStateText(
 		StateRequest request) const {
 	if (!hasVisibleText()) {
 		return false;
+	} else if (const auto botTop = Get<FakeBotAboutTop>()) {
+		trect.setY(trect.y() + botTop->height);
 	}
 	const auto item = data();
 	if (base::in_range(point.y(), trect.y(), trect.y() + trect.height())) {
@@ -2886,7 +2913,7 @@ bool Message::drawBubble() const {
 	const auto item = data();
 	if (isHidden()) {
 		return false;
-	} else if (logEntryOriginal()) {
+	} else if (logEntryOriginal() || item->isFakeBotAbout()) {
 		return true;
 	}
 	const auto media = this->media();
@@ -3376,8 +3403,9 @@ QRect Message::innerGeometry() const {
 }
 
 QRect Message::countGeometry() const {
-	const auto commentsRoot = (context() == Context::Replies)
-		&& data()->isDiscussionPost();
+	const auto item = data();
+	const auto centeredView = item->isFakeBotAbout()
+		|| (context() == Context::Replies && item->isDiscussionPost());
 	const auto media = this->media();
 	const auto mediaWidth = (media && media->isDisplayed())
 		? media->width()
@@ -3385,7 +3413,7 @@ QRect Message::countGeometry() const {
 	const auto outbg = hasOutLayout();
 	const auto availableWidth = width()
 		- st::msgMargin.left()
-		- (commentsRoot ? st::msgMargin.left() : st::msgMargin.right());
+		- (centeredView ? st::msgMargin.left() : st::msgMargin.right());
 	auto contentLeft = (outbg && !delegate()->elementIsChatWide())
 		? st::msgMargin.right()
 		: st::msgMargin.left();
@@ -3412,10 +3440,10 @@ QRect Message::countGeometry() const {
 	if (contentWidth < availableWidth && !delegate()->elementIsChatWide()) {
 		if (outbg) {
 			contentLeft += availableWidth - contentWidth;
-		} else if (commentsRoot) {
+		} else if (centeredView) {
 			contentLeft += (availableWidth - contentWidth) / 2;
 		}
-	} else if (contentWidth < availableWidth && commentsRoot) {
+	} else if (contentWidth < availableWidth && centeredView) {
 		contentLeft += std::max(
 			((st::msgMaxWidth + 2 * st::msgPhotoSkip) - contentWidth) / 2,
 			0);
@@ -3433,11 +3461,13 @@ Ui::BubbleRounding Message::countMessageRounding() const {
 	const auto smallTop = isBubbleAttachedToPrevious();
 	const auto smallBottom = isBubbleAttachedToNext();
 	const auto media = smallBottom ? nullptr : this->media();
-	const auto keyboard = data()->inlineReplyKeyboard();
+	const auto item = data();
+	const auto keyboard = item->inlineReplyKeyboard();
 	const auto skipTail = smallBottom
 		|| (media && media->skipBubbleTail())
 		|| (keyboard != nullptr)
-		|| (context() == Context::Replies && data()->isDiscussionPost());
+		|| item->isFakeBotAbout()
+		|| (context() == Context::Replies && item->isDiscussionPost());
 	const auto right = !delegate()->elementIsChatWide() && hasOutLayout();
 	using Corner = Ui::BubbleCornerRounding;
 	return Ui::BubbleRounding{
@@ -3480,16 +3510,19 @@ int Message::resizeContentGetHeight(int newWidth) {
 	auto newHeight = minHeight();
 
 	const auto item = data();
+	const auto botTop = item->isFakeBotAbout()
+		? Get<FakeBotAboutTop>()
+		: nullptr;
 	const auto media = this->media();
 	const auto mediaDisplayed = media ? media->isDisplayed() : false;
 	const auto bubble = drawBubble();
 
 	// This code duplicates countGeometry() but also resizes media.
-	const auto commentsRoot = (context() == Context::Replies)
-		&& data()->isDiscussionPost();
+	const auto centeredView = item->isFakeBotAbout()
+		|| (context() == Context::Replies && item->isDiscussionPost());
 	auto contentWidth = newWidth
 		- st::msgMargin.left()
-		- (commentsRoot ? st::msgMargin.left() : st::msgMargin.right());
+		- (centeredView ? st::msgMargin.left() : st::msgMargin.right());
 	if (hasFromPhoto()) {
 		if (const auto size = rightActionSize()) {
 			contentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize);
@@ -3540,7 +3573,14 @@ int Message::resizeContentGetHeight(int newWidth) {
 				entry->resizeGetHeight(contentWidth);
 			}
 		} else {
-			newHeight = hasVisibleText() ? textHeightFor(textWidth) : 0;
+			const auto withVisibleText = hasVisibleText();
+			newHeight = 0;
+			if (withVisibleText) {
+				if (botTop) {
+					newHeight += botTop->height;
+				}
+				newHeight += textHeightFor(textWidth);
+			}
 			if (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) {
 				newHeight += st::msgPadding.bottom();
 				if (mediaDisplayed) {
diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
index caf8c2e7a..a4e8a372d 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp
@@ -199,6 +199,10 @@ QSize Gif::countOptimalSize() {
 			minHeight = adjustHeightForLessCrop(
 				scaled,
 				{ maxWidth, minHeight });
+			if (const auto botTop = _parent->Get<FakeBotAboutTop>()) {
+				accumulate_max(maxWidth, botTop->maxWidth);
+				minHeight += botTop->height;
+			}
 			minHeight += st::mediaCaptionSkip + _caption.minHeight();
 			if (isBubbleBottom()) {
 				minHeight += st::msgPadding.bottom();
@@ -236,11 +240,14 @@ QSize Gif::countCurrentSize(int newWidth) {
 	if (_parent->hasBubble()) {
 		accumulate_max(newWidth, _parent->minWidthForMedia());
 		if (!_caption.isEmpty()) {
-			const auto maxWithCaption = qMin(
-				st::msgMaxWidth,
-				(st::msgPadding.left()
-					+ _caption.maxWidth()
-					+ st::msgPadding.right()));
+			auto captionMaxWidth = st::msgPadding.left()
+				+ _caption.maxWidth()
+				+ st::msgPadding.right();
+			const auto botTop = _parent->Get<FakeBotAboutTop>();
+			if (botTop) {
+				accumulate_max(captionMaxWidth, botTop->maxWidth);
+			}
+			const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
 			newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
 			newHeight = adjustHeightForLessCrop(
 				scaled,
@@ -248,6 +255,9 @@ QSize Gif::countCurrentSize(int newWidth) {
 			const auto captionw = newWidth
 				- st::msgPadding.left()
 				- st::msgPadding.right();
+			if (botTop) {
+				newHeight += botTop->height;
+			}
 			newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw);
 			if (isBubbleBottom()) {
 				newHeight += st::msgPadding.bottom();
@@ -349,12 +359,16 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
 	const auto outbg = context.outbg;
 	const auto inWebPage = (_parent->media() != this);
 	const auto isRound = _data->isVideoMessage();
+	const auto botTop = _parent->Get<FakeBotAboutTop>();
 
 	const auto rounding = inWebPage
 		? std::optional<Ui::BubbleRounding>()
 		: adjustedBubbleRoundingWithCaption(_caption);
 	if (bubble) {
 		if (!_caption.isEmpty()) {
+			if (botTop) {
+				painth -= botTop->height;
+			}
 			painth -= st::mediaCaptionSkip + _caption.countHeight(captionw);
 			if (isBubbleBottom()) {
 				painth -= st::msgPadding.bottom();
@@ -674,10 +688,18 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
 	if (!unwrapped && !_caption.isEmpty()) {
 		p.setPen(stm->historyTextFg);
 		_parent->prepareCustomEmojiPaint(p, context, _caption);
-		_caption.draw(p, {
-			.position = QPoint(
+		auto top = painty + painth + st::mediaCaptionSkip;
+		if (botTop) {
+			botTop->text.drawLeftElided(
+				p,
 				st::msgPadding.left(),
-				painty + painth + st::mediaCaptionSkip),
+				top,
+				captionw,
+				_parent->width());
+			top += botTop->height;
+		}
+		_caption.draw(p, {
+			.position = QPoint(st::msgPadding.left(), top),
 			.availableWidth = captionw,
 			.palette = &stm->textPalette,
 			.spoiler = Ui::Text::DefaultSpoilerCache(),
@@ -956,6 +978,9 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
 				request.forText()));
 			return result;
 		}
+		if (const auto botTop = _parent->Get<FakeBotAboutTop>()) {
+			painth -= botTop->height;
+		}
 		painth -= st::mediaCaptionSkip;
 	}
 	const auto outbg = _parent->hasOutLayout();
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
index b65b946e9..78c4be6f3 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
@@ -187,6 +187,10 @@ QSize Photo::countOptimalSize() {
 		minHeight = adjustHeightForLessCrop(
 			dimensions,
 			{ maxWidth, minHeight });
+		if (const auto botTop = _parent->Get<FakeBotAboutTop>()) {
+			accumulate_max(maxWidth, botTop->maxWidth);
+			minHeight += botTop->height;
+		}
 		minHeight += st::mediaCaptionSkip + _caption.minHeight();
 		if (isBubbleBottom()) {
 			minHeight += st::msgPadding.bottom();
@@ -214,11 +218,14 @@ QSize Photo::countCurrentSize(int newWidth) {
 	newWidth = qMax(pix.width(), minWidth);
 	auto newHeight = qMax(pix.height(), st::minPhotoSize);
 	if (_parent->hasBubble() && !_caption.isEmpty()) {
-		const auto maxWithCaption = qMin(
-			st::msgMaxWidth,
-			(st::msgPadding.left()
-				+ _caption.maxWidth()
-				+ st::msgPadding.right()));
+		auto captionMaxWidth = st::msgPadding.left()
+			+ _caption.maxWidth()
+			+ st::msgPadding.right();
+		const auto botTop = _parent->Get<FakeBotAboutTop>();
+		if (botTop) {
+			accumulate_max(captionMaxWidth, botTop->maxWidth);
+		}
+		const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
 		newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
 		newHeight = adjustHeightForLessCrop(
 			dimensions,
@@ -226,6 +233,9 @@ QSize Photo::countCurrentSize(int newWidth) {
 		const auto captionw = newWidth
 			- st::msgPadding.left()
 			- st::msgPadding.right();
+		if (botTop) {
+			newHeight += botTop->height;
+		}
 		newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw);
 		if (isBubbleBottom()) {
 			newHeight += st::msgPadding.bottom();
@@ -268,6 +278,7 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
 		}
 	}
 	const auto radial = isRadialAnimation();
+	const auto botTop = _parent->Get<FakeBotAboutTop>();
 
 	auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
 	if (_serviceWidth > 0) {
@@ -279,6 +290,9 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
 		if (bubble) {
 			if (!_caption.isEmpty()) {
 				painth -= st::mediaCaptionSkip + _caption.countHeight(captionw);
+				if (botTop) {
+					painth -= botTop->height;
+				}
 				if (isBubbleBottom()) {
 					painth -= st::msgPadding.bottom();
 				}
@@ -348,10 +362,18 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
 	if (!_caption.isEmpty()) {
 		p.setPen(stm->historyTextFg);
 		_parent->prepareCustomEmojiPaint(p, context, _caption);
-		_caption.draw(p, {
-			.position = QPoint(
+		auto top = painty + painth + st::mediaCaptionSkip;
+		if (botTop) {
+			botTop->text.drawLeftElided(
+				p,
 				st::msgPadding.left(),
-				painty + painth + st::mediaCaptionSkip),
+				top,
+				captionw,
+				_parent->width());
+			top += botTop->height;
+		}
+		_caption.draw(p, {
+			.position = QPoint(st::msgPadding.left(), top),
 			.availableWidth = captionw,
 			.palette = &stm->textPalette,
 			.spoiler = Ui::Text::DefaultSpoilerCache(),
@@ -592,6 +614,9 @@ TextState Photo::textState(QPoint point, StateRequest request) const {
 				request.forText()));
 			return result;
 		}
+		if (const auto botTop = _parent->Get<FakeBotAboutTop>()) {
+			painth -= botTop->height;
+		}
 		painth -= st::mediaCaptionSkip;
 	}
 	if (QRect(paintx, painty, paintw, painth).contains(point)) {