diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp
index 0accf6bf7..0265f3a18 100644
--- a/Telegram/SourceFiles/data/data_peer_values.cpp
+++ b/Telegram/SourceFiles/data/data_peer_values.cpp
@@ -366,4 +366,11 @@ bool OnlineTextActive(not_null<UserData*> user, TimeId now) {
 	return OnlineTextActive(user->onlineTill, now);
 }
 
+bool IsPeerAnOnlineUser(not_null<PeerData*> peer) {
+	if (const auto user = peer->asUser()) {
+		return OnlineTextActive(user, unixtime());
+	}
+	return false;
+}
+
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h
index 8744a46ad..339e031df 100644
--- a/Telegram/SourceFiles/data/data_peer_values.h
+++ b/Telegram/SourceFiles/data/data_peer_values.h
@@ -110,13 +110,14 @@ rpl::producer<bool> CanWriteValue(ChatData *chat);
 rpl::producer<bool> CanWriteValue(ChannelData *channel);
 rpl::producer<bool> CanWriteValue(not_null<PeerData*> peer);
 
-TimeId SortByOnlineValue(not_null<UserData*> user, TimeId now);
-crl::time OnlineChangeTimeout(TimeId online, TimeId now);
-crl::time OnlineChangeTimeout(not_null<UserData*> user, TimeId now);
-QString OnlineText(TimeId online, TimeId now);
-QString OnlineText(not_null<UserData*> user, TimeId now);
-QString OnlineTextFull(not_null<UserData*> user, TimeId now);
-bool OnlineTextActive(TimeId online, TimeId now);
-bool OnlineTextActive(not_null<UserData*> user, TimeId now);
+[[nodiscard]] TimeId SortByOnlineValue(not_null<UserData*> user, TimeId now);
+[[nodiscard]] crl::time OnlineChangeTimeout(TimeId online, TimeId now);
+[[nodiscard]] crl::time OnlineChangeTimeout(not_null<UserData*> user, TimeId now);
+[[nodiscard]] QString OnlineText(TimeId online, TimeId now);
+[[nodiscard]] QString OnlineText(not_null<UserData*> user, TimeId now);
+[[nodiscard]] QString OnlineTextFull(not_null<UserData*> user, TimeId now);
+[[nodiscard]] bool OnlineTextActive(TimeId online, TimeId now);
+[[nodiscard]] bool OnlineTextActive(not_null<UserData*> user, TimeId now);
+[[nodiscard]] bool IsPeerAnOnlineUser(not_null<PeerData*> peer);
 
 } // namespace Data
diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style
index 6e0701927..f33e81e50 100644
--- a/Telegram/SourceFiles/dialogs/dialogs.style
+++ b/Telegram/SourceFiles/dialogs/dialogs.style
@@ -37,6 +37,7 @@ dialogsPadding: point(10px, 8px);
 dialogsOnlineBadgeStroke: 2px;
 dialogsOnlineBadgeSize: 10px;
 dialogsOnlineBadgeSkip: point(10px, 12px);
+dialogsOnlineBadgeDuration: 150;
 
 dialogsImportantBarHeight: 37px;
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 3d6460216..e6da771ea 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_channel.h"
 #include "data/data_chat.h"
 #include "data/data_user.h"
+#include "data/data_peer_values.h"
 #include "lang/lang_keys.h"
 #include "mainwindow.h"
 #include "mainwidget.h"
@@ -79,21 +80,21 @@ struct InnerWidget::CollapsedRow {
 	}
 
 	Data::Folder *folder = nullptr;
-	RippleRow row;
+	BasicRow row;
 };
 
 struct InnerWidget::HashtagResult {
 	HashtagResult(const QString &tag) : tag(tag) {
 	}
 	QString tag;
-	RippleRow row;
+	BasicRow row;
 };
 
 struct InnerWidget::PeerSearchResult {
 	PeerSearchResult(not_null<PeerData*> peer) : peer(peer) {
 	}
 	not_null<PeerData*> peer;
-	RippleRow row;
+	BasicRow row;
 };
 
 InnerWidget::InnerWidget(
@@ -177,39 +178,7 @@ InnerWidget::InnerWidget(
 			UpdateRowSection::Default | UpdateRowSection::Filtered);
 	}, lifetime());
 
-	const auto handleUserOnline = [=](const Notify::PeerUpdate &peerUpdate) {
-		if (peerUpdate.peer->isSelf()) {
-			return;
-		}
-		const auto history = session().data().historyLoaded(peerUpdate.peer);
-		if (!history) {
-			return;
-		}
-		const auto size = st::dialogsOnlineBadgeSize;
-		const auto stroke = st::dialogsOnlineBadgeStroke;
-		const auto skip = st::dialogsOnlineBadgeSkip;
-		const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize;
-		const auto updateRect = QRect(
-			edge - skip.x() - size,
-			edge - skip.y() - size,
-			size,
-			size
-		).marginsAdded(
-			{ stroke, stroke, stroke, stroke }
-		).translated(
-			st::dialogsPadding
-		);
-		updateDialogRow(
-			RowDescriptor(
-				history,
-				FullMsgId()),
-			updateRect,
-			UpdateRowSection::Default | UpdateRowSection::Filtered);
-	};
-
-	subscribe(Notify::PeerUpdated(), Notify::PeerUpdatedHandler(
-		Notify::PeerUpdate::Flag::UserOnlineChanged,
-		handleUserOnline));
+	setupOnlineStatusCheck();
 
 	session().data().chatsListChanges(
 	) | rpl::filter([=](Data::Folder *folder) {
@@ -1507,6 +1476,15 @@ void InnerWidget::repaintCollapsedFolderRow(not_null<Data::Folder*> folder) {
 	}
 }
 
+int InnerWidget::defaultRowTop(not_null<Row*> row) const {
+	const auto position = row->pos();
+	auto top = dialogsOffset();
+	if (base::in_range(position, 0, _pinnedRows.size())) {
+		top += qRound(_pinnedRows[position].yadd.current());
+	}
+	return top + position * st::dialogsRowHeight;
+}
+
 void InnerWidget::repaintDialogRow(
 		Mode list,
 		not_null<Row*> row) {
@@ -1515,12 +1493,7 @@ void InnerWidget::repaintDialogRow(
 			if (const auto folder = row->folder()) {
 				repaintCollapsedFolderRow(folder);
 			}
-			auto position = row->pos();
-			auto top = dialogsOffset();
-			if (base::in_range(position, 0, _pinnedRows.size())) {
-				top += qRound(_pinnedRows[position].yadd.current());
-			}
-			update(0, top + position * st::dialogsRowHeight, width(), st::dialogsRowHeight);
+			update(0, defaultRowTop(row), width(), st::dialogsRowHeight);
 		}
 	} else if (_state == WidgetState::Filtered) {
 		if (list == Mode::All) {
@@ -2816,6 +2789,71 @@ MsgId InnerWidget::lastSearchMigratedId() const {
 	return _lastSearchMigratedId;
 }
 
+void InnerWidget::setupOnlineStatusCheck() {
+	using namespace Notify;
+	subscribe(PeerUpdated(), PeerUpdatedHandler(
+		PeerUpdate::Flag::UserOnlineChanged,
+		[=](const PeerUpdate &update) { userOnlineUpdated(update); }));
+}
+
+void InnerWidget::userOnlineUpdated(const Notify::PeerUpdate &update) {
+	const auto user = update.peer->isSelf()
+		? nullptr
+		: update.peer->asUser();
+	if (!user) {
+		return;
+	}
+	const auto history = session().data().historyLoaded(user);
+	if (!history) {
+		return;
+	}
+	const auto size = st::dialogsOnlineBadgeSize;
+	const auto stroke = st::dialogsOnlineBadgeStroke;
+	const auto skip = st::dialogsOnlineBadgeSkip;
+	const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize;
+	const auto updateRect = QRect(
+		edge - skip.x() - size,
+		edge - skip.y() - size,
+		size,
+		size
+	).marginsAdded(
+		{ stroke, stroke, stroke, stroke }
+	).translated(
+		st::dialogsPadding
+	);
+	const auto repaint = [=] {
+		updateDialogRow(
+			RowDescriptor(
+				history,
+				FullMsgId()),
+			updateRect,
+			UpdateRowSection::Default | UpdateRowSection::Filtered);
+	};
+	repaint();
+
+	const auto findRow = [&](not_null<History*> history)
+	-> std::pair<Row*, int> {
+		if (state() == WidgetState::Default) {
+			const auto row = shownDialogs()->getRow({ history });
+			return { row, row ? defaultRowTop(row) : 0 };
+		}
+		const auto i = ranges::find(
+			_filterResults,
+			history.get(),
+			[](not_null<Row*> row) { return row->history(); });
+		const auto index = (i - begin(_filterResults));
+		const auto row = (i == end(_filterResults)) ? nullptr : i->get();
+		return { row, filteredOffset() + index * st::dialogsRowHeight };
+	};
+	if (const auto &[row, top] = findRow(history); row != nullptr) {
+		const auto visible = (top < _visibleBottom)
+			&& (top + st::dialogsRowHeight > _visibleTop);
+		row->setOnline(
+			Data::OnlineTextActive(user, unixtime()),
+			visible ? Fn<void()>(crl::guard(this, repaint)) : nullptr);
+	}
+}
+
 void InnerWidget::setupShortcuts() {
 	Shortcuts::Requests(
 	) | rpl::filter([=] {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index 342603d47..e275dcfc5 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -25,6 +25,10 @@ namespace Window {
 class SessionController;
 } // namespace Window
 
+namespace Notify {
+struct PeerUpdate;
+} // namespace Notify
+
 namespace Dialogs {
 
 class Row;
@@ -204,6 +208,10 @@ private:
 	bool uniqueSearchResults() const;
 	bool hasHistoryInResults(not_null<History*> history) const;
 
+	int defaultRowTop(not_null<Row*> row) const;
+	void setupOnlineStatusCheck();
+	void userOnlineUpdated(const Notify::PeerUpdate &update);
+
 	void setupShortcuts();
 	RowDescriptor computeJump(
 		const RowDescriptor &to,
diff --git a/Telegram/SourceFiles/dialogs/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/dialogs_layout.cpp
index 7e0871f10..9f6582d4c 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_layout.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_layout.cpp
@@ -204,7 +204,7 @@ enum class Flag {
 	Selected         = 0x02,
 	SearchResult     = 0x04,
 	SavedMessages    = 0x08,
-	UserOnline       = 0x10,
+	AllowUserOnline  = 0x10,
 	//FeedSearchResult = 0x10, // #feed
 };
 inline constexpr bool is_flag_type(Flag) { return true; }
@@ -212,7 +212,7 @@ inline constexpr bool is_flag_type(Flag) { return true; }
 template <typename PaintItemCallback, typename PaintCounterCallback>
 void paintRow(
 		Painter &p,
-		not_null<const RippleRow*> row,
+		not_null<const BasicRow*> row,
 		not_null<Entry*> entry,
 		Dialogs::Key chat,
 		PeerData *from,
@@ -252,51 +252,12 @@ void paintRow(
 			fullWidth,
 			st::dialogsPhotoSize);
 	} else if (from) {
-		if (flags & Flag::UserOnline) {
-			auto frame = QImage(
-				st::dialogsPhotoSize * cRetinaFactor(),
-				st::dialogsPhotoSize * cRetinaFactor(),
-				QImage::Format_ARGB32_Premultiplied);
-			frame.setDevicePixelRatio(cRetinaFactor());
-			frame.fill(Qt::transparent);
-			{
-				Painter q(&frame);
-				from->paintUserpicLeft(
-						q,
-						0,
-						0,
-						fullWidth,
-						st::dialogsPhotoSize);
-
-				PainterHighQualityEnabler hq(q);
-				q.setCompositionMode(QPainter::CompositionMode_Source);
-
-				const auto size = st::dialogsOnlineBadgeSize;
-				const auto stroke = st::dialogsOnlineBadgeStroke;
-				const auto skip = st::dialogsOnlineBadgeSkip;
-				const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize;
-
-				auto pen = QPen(Qt::transparent);
-				pen.setWidth(stroke);
-				q.setPen(pen);
-				q.setBrush(active
-					? st::dialogsOnlineBadgeFgActive
-					: st::dialogsOnlineBadgeFg);
-				q.drawEllipse(
-					edge - skip.x() - size,
-					edge - skip.y() - size,
-					size,
-					size);
-			}
-			p.drawImage(st::dialogsPadding, frame);
-		} else {
-			from->paintUserpicLeft(
-				p,
-				st::dialogsPadding.x(),
-				st::dialogsPadding.y(),
-				fullWidth,
-				st::dialogsPhotoSize);
-		}
+		row->paintUserpic(
+			p,
+			from,
+			(flags & Flag::AllowUserOnline),
+			active,
+			fullWidth);
 	} else if (hiddenSenderInfo) {
 		hiddenSenderInfo->userpic.paint(
 			p,
@@ -690,14 +651,11 @@ void RowPainter::paint(
 			? history->peer->migrateTo()
 			: history->peer.get())
 		: nullptr;
-	const auto showUserOnline = peer
-		&& peer->isUser()
-		&& Data::OnlineTextActive(peer->asUser(), unixtime())
-		&& !(fullWidth < st::columnMinimalWidthLeft
-			&& (displayUnreadCounter || displayUnreadMark));
+	const auto allowUserOnline = (fullWidth >= st::columnMinimalWidthLeft)
+		|| (!displayUnreadCounter && !displayUnreadMark);
 	const auto flags = (active ? Flag::Active : Flag(0))
 		| (selected ? Flag::Selected : Flag(0))
-		| (showUserOnline ? Flag::UserOnline : Flag(0))
+		| (allowUserOnline ? Flag::AllowUserOnline : Flag(0))
 		| (peer && peer->isSelf() ? Flag::SavedMessages : Flag(0));
 	const auto paintItemCallback = [&](int nameleft, int namewidth) {
 		const auto texttop = st::dialogsPadding.y()
@@ -908,7 +866,7 @@ QRect RowPainter::sendActionAnimationRect(int animationWidth, int animationHeigh
 
 void PaintCollapsedRow(
 		Painter &p,
-		const RippleRow &row,
+		const BasicRow &row,
 		Data::Folder *folder,
 		const QString &text,
 		int unread,
diff --git a/Telegram/SourceFiles/dialogs/dialogs_layout.h b/Telegram/SourceFiles/dialogs/dialogs_layout.h
index b0ca4843f..c8b03e792 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_layout.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_layout.h
@@ -11,7 +11,7 @@ namespace Dialogs {
 
 class Row;
 class FakeRow;
-class RippleRow;
+class BasicRow;
 
 namespace Layout {
 
@@ -51,7 +51,7 @@ public:
 
 void PaintCollapsedRow(
 	Painter &p,
-	const RippleRow &row,
+	const BasicRow &row,
 	Data::Folder *folder,
 	const QString &text,
 	int unread,
diff --git a/Telegram/SourceFiles/dialogs/dialogs_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_list.cpp
index 00e1ac599..37302ace8 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_list.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_list.cpp
@@ -31,7 +31,7 @@ not_null<Row*> List::addToEnd(Key key) {
 		key,
 		std::make_unique<Row>(key, _rows.size())
 	).first->second.get();
-	_rows.push_back(result);
+	_rows.emplace_back(result);
 	if (_sortMode == SortMode::Date) {
 		adjustByDate(result);
 	}
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index 156fcf7c1..ed280b55c 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text_options.h"
 #include "dialogs/dialogs_entry.h"
 #include "data/data_folder.h"
+#include "data/data_peer_values.h"
 #include "history/history.h"
 #include "lang/lang_keys.h"
 #include "mainwidget.h"
@@ -66,24 +67,59 @@ QString ComposeFolderListEntryText(not_null<Data::Folder*> folder) {
 
 } // namespace
 
-RippleRow::RippleRow() = default;
-RippleRow::~RippleRow() = default;
+BasicRow::BasicRow() = default;
+BasicRow::~BasicRow() = default;
 
-void RippleRow::addRipple(QPoint origin, QSize size, Fn<void()> updateCallback) {
+void BasicRow::setOnline(bool online, Fn<void()> updateCallback) const {
+	if (_online == online) {
+		return;
+	}
+	_online = online;
+	if (_onlineUserpic && _onlineUserpic->animation.animating()) {
+		_onlineUserpic->animation.change(
+			_online ? 1. : 0.,
+			st::dialogsOnlineBadgeDuration);
+	} else if (updateCallback) {
+		ensureOnlineUserpic();
+		_onlineUserpic->animation.start(
+			std::move(updateCallback),
+			_online ? 0. : 1.,
+			_online ? 1. : 0.,
+			st::dialogsOnlineBadgeDuration);
+	}
+	if (!_online
+		&& _onlineUserpic
+		&& !_onlineUserpic->animation.animating()) {
+		_onlineUserpic = nullptr;
+	}
+}
+
+void BasicRow::addRipple(
+		QPoint origin,
+		QSize size,
+		Fn<void()> updateCallback) {
 	if (!_ripple) {
 		auto mask = Ui::RippleAnimation::rectMask(size);
-		_ripple = std::make_unique<Ui::RippleAnimation>(st::dialogsRipple, std::move(mask), std::move(updateCallback));
+		_ripple = std::make_unique<Ui::RippleAnimation>(
+			st::dialogsRipple,
+			std::move(mask),
+			std::move(updateCallback));
 	}
 	_ripple->add(origin);
 }
 
-void RippleRow::stopLastRipple() {
+void BasicRow::stopLastRipple() {
 	if (_ripple) {
 		_ripple->lastStop();
 	}
 }
 
-void RippleRow::paintRipple(Painter &p, int x, int y, int outerWidth, const QColor *colorOverride) const {
+void BasicRow::paintRipple(
+		Painter &p,
+		int x,
+		int y,
+		int outerWidth,
+		const QColor *colorOverride) const {
 	if (_ripple) {
 		_ripple->paint(p, x, y, outerWidth, colorOverride);
 		if (_ripple->empty()) {
@@ -92,6 +128,97 @@ void RippleRow::paintRipple(Painter &p, int x, int y, int outerWidth, const QCol
 	}
 }
 
+void BasicRow::ensureOnlineUserpic() const {
+	if (_onlineUserpic) {
+		return;
+	}
+	_onlineUserpic = std::make_unique<OnlineUserpic>();
+}
+
+void BasicRow::PaintOnlineFrame(
+		not_null<OnlineUserpic*> data,
+		not_null<PeerData*> peer) {
+	data->frame.fill(Qt::transparent);
+
+	Painter q(&data->frame);
+	peer->paintUserpic(
+		q,
+		0,
+		0,
+		st::dialogsPhotoSize);
+
+	PainterHighQualityEnabler hq(q);
+	q.setCompositionMode(QPainter::CompositionMode_Source);
+
+	const auto size = st::dialogsOnlineBadgeSize;
+	const auto stroke = st::dialogsOnlineBadgeStroke;
+	const auto skip = st::dialogsOnlineBadgeSkip;
+	const auto edge = st::dialogsPadding.x() + st::dialogsPhotoSize;
+	const auto shrink = (size / 2) * (1. - data->online);
+
+	auto pen = QPen(Qt::transparent);
+	pen.setWidthF(stroke * data->online);
+	q.setPen(pen);
+	q.setBrush(data->active
+		? st::dialogsOnlineBadgeFgActive
+		: st::dialogsOnlineBadgeFg);
+	q.drawEllipse(QRectF(
+		edge - skip.x() - size,
+		edge - skip.y() - size,
+		size,
+		size
+	).marginsRemoved({ shrink, shrink, shrink, shrink }));
+}
+
+void BasicRow::paintUserpic(
+		Painter &p,
+		not_null<PeerData*> peer,
+		bool allowOnline,
+		bool active,
+		int fullWidth) const {
+	setOnline(Data::IsPeerAnOnlineUser(peer));
+
+	const auto online = _onlineUserpic
+		? _onlineUserpic->animation.value(_online ? 1. : 0.)
+		: (_online ? 1. : 0.);
+	if (!allowOnline || online == 0.) {
+		peer->paintUserpicLeft(
+			p,
+			st::dialogsPadding.x(),
+			st::dialogsPadding.y(),
+			fullWidth,
+			st::dialogsPhotoSize);
+		if (!allowOnline || !_online) {
+			_onlineUserpic = nullptr;
+		}
+		return;
+	}
+	ensureOnlineUserpic();
+	if (_onlineUserpic->frame.isNull()) {
+		_onlineUserpic->frame = QImage(
+			st::dialogsPhotoSize * cRetinaFactor(),
+			st::dialogsPhotoSize * cRetinaFactor(),
+			QImage::Format_ARGB32_Premultiplied);
+		_onlineUserpic->frame.setDevicePixelRatio(cRetinaFactor());
+	}
+	const auto key = peer->userpicUniqueKey();
+	if (_onlineUserpic->online != online
+		|| _onlineUserpic->key != key
+		|| _onlineUserpic->active != active) {
+		_onlineUserpic->online = online;
+		_onlineUserpic->key = key;
+		_onlineUserpic->active = active;
+		PaintOnlineFrame(_onlineUserpic.get(), peer);
+	}
+	p.drawImage(st::dialogsPadding, _onlineUserpic->frame);
+}
+
+Row::Row(Key key, int pos) : _id(key), _pos(pos) {
+	if (const auto history = key.history()) {
+		setOnline(Data::IsPeerAnOnlineUser(history->peer));
+	}
+}
+
 uint64 Row::sortKey() const {
 	return _id.entry()->sortKeyInChatList();
 }
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h
index d081ce8ef..189211718 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "ui/text/text.h"
+#include "ui/effects/animations.h"
 #include "dialogs/dialogs_key.h"
 
 class History;
@@ -22,28 +23,55 @@ namespace Layout {
 class RowPainter;
 } // namespace Layout
 
-class RippleRow {
+class BasicRow {
 public:
-	RippleRow();
-	~RippleRow();
+	BasicRow();
+	~BasicRow();
+
+	void setOnline(bool online, Fn<void()> updateCallback = nullptr) const;
+	void paintUserpic(
+		Painter &p,
+		not_null<PeerData*> peer,
+		bool allowOnline,
+		bool active,
+		int fullWidth) const;
 
 	void addRipple(QPoint origin, QSize size, Fn<void()> updateCallback);
 	void stopLastRipple();
 
-	void paintRipple(Painter &p, int x, int y, int outerWidth, const QColor *colorOverride = nullptr) const;
+	void paintRipple(
+		Painter &p,
+		int x,
+		int y,
+		int outerWidth,
+		const QColor *colorOverride = nullptr) const;
 
 private:
+	struct OnlineUserpic {
+		InMemoryKey key;
+		float64 online = 0.;
+		bool active = false;
+		QImage frame;
+		Ui::Animations::Simple animation;
+	};
+
+	void ensureOnlineUserpic() const;
+	static void PaintOnlineFrame(
+		not_null<OnlineUserpic*> data,
+		not_null<PeerData*> peer);
+
 	mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
+	mutable std::unique_ptr<OnlineUserpic> _onlineUserpic;
+	mutable bool _online = false;
 
 };
 
 class List;
-class Row : public RippleRow {
+class Row : public BasicRow {
 public:
 	explicit Row(std::nullptr_t) {
 	}
-	Row(Key key, int pos) : _id(key), _pos(pos) {
-	}
+	Row(Key key, int pos);
 
 	Key key() const {
 		return _id;
@@ -80,7 +108,7 @@ private:
 
 };
 
-class FakeRow : public RippleRow {
+class FakeRow : public BasicRow {
 public:
 	FakeRow(Key searchInChat, not_null<HistoryItem*> item);
 
diff --git a/Telegram/SourceFiles/platform/mac/mac_touchbar.mm b/Telegram/SourceFiles/platform/mac/mac_touchbar.mm
index 3c4b7cf89..ca7d0bdff 100644
--- a/Telegram/SourceFiles/platform/mac/mac_touchbar.mm
+++ b/Telegram/SourceFiles/platform/mac/mac_touchbar.mm
@@ -96,13 +96,7 @@ inline bool UseEmptyUserpic(PeerData *peer) {
 }
 
 inline bool IsSelfPeer(PeerData *peer) {
-	return (peer && peer->id == Auth().userPeerId());
-}
-
-inline bool IsUserOnline(PeerData *peer) {
-	return peer
-		&& peer->isUser()
-		&& Data::OnlineTextActive(peer->asUser(), unixtime());
+	return (peer && peer->isSelf());
 }
 
 inline int UnreadCount(PeerData *peer) {
@@ -266,7 +260,7 @@ void SendKeyEvent(int command) {
 		themeChanged
 	) | rpl::filter([=](const Update &update) {
 		return update.type == Update::Type::ApplyingTheme
-			&& (UnreadCount(_peer) || IsUserOnline(_peer));
+			&& (UnreadCount(_peer) || Data::IsPeerAnOnlineUser(_peer));
 	}) | rpl::start_with_next([=] {
 		[self updateBadge];
 	}, _lifetime);
@@ -373,7 +367,7 @@ void SendKeyEvent(int command) {
 	// Draw unread or online badge.
 	auto pixmap = App::pixmapFromImageInPlace(_userpic.toImage());
 	Painter p(&pixmap);
-	if (!PaintUnreadBadge(p, _peer) && IsUserOnline(_peer)) {
+	if (!PaintUnreadBadge(p, _peer) && Data::IsPeerAnOnlineUser(_peer)) {
 		PaintOnlineCircle(p);
 	}
 	[self updateImage:pixmap];
diff --git a/Telegram/SourceFiles/ui/effects/animations.h b/Telegram/SourceFiles/ui/effects/animations.h
index 7c1cacc7f..96bf6e511 100644
--- a/Telegram/SourceFiles/ui/effects/animations.h
+++ b/Telegram/SourceFiles/ui/effects/animations.h
@@ -58,6 +58,10 @@ public:
 		float64 to,
 		crl::time duration,
 		anim::transition transition = anim::linear);
+	void change(
+		float64 to,
+		crl::time duration,
+		anim::transition transition = anim::linear);
 	void stop();
 	[[nodiscard]] bool animating() const;
 	[[nodiscard]] float64 value(float64 final) const;
@@ -328,6 +332,16 @@ inline void Simple::start(
 	startPrepared(to, duration, transition);
 }
 
+inline void Simple::change(
+		float64 to,
+		crl::time duration,
+		anim::transition transition) {
+	Expects(_data != nullptr);
+
+	prepare(0. /* ignored */, duration);
+	startPrepared(to, duration, transition);
+}
+
 inline void Simple::prepare(float64 from, crl::time duration) {
 	const auto isLong = (duration > kLongAnimationDuration);
 	if (!_data) {