diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp
index e0afbabf4..eb1883ea6 100644
--- a/Telegram/SourceFiles/api/api_chat_invite.cpp
+++ b/Telegram/SourceFiles/api/api_chat_invite.cpp
@@ -171,6 +171,11 @@ void CheckChatInvite(
 
 } // namespace Api
 
+struct ConfirmInviteBox::Participant {
+	not_null<UserData*> user;
+	Ui::PeerUserpicView userpic;
+};
+
 ConfirmInviteBox::ConfirmInviteBox(
 	QWidget*,
 	not_null<Main::Session*> session,
@@ -356,7 +361,7 @@ void ConfirmInviteBox::paintEvent(QPaintEvent *e) {
 					{ .options = Images::Option::RoundCircle }));
 		}
 	} else if (_photoEmpty) {
-		_photoEmpty->paint(
+		_photoEmpty->paintCircle(
 			p,
 			(width() - st::confirmInvitePhotoSize) / 2,
 			st::confirmInvitePhotoTop,
diff --git a/Telegram/SourceFiles/api/api_chat_invite.h b/Telegram/SourceFiles/api/api_chat_invite.h
index 95b42cbf5..1ae6f2529 100644
--- a/Telegram/SourceFiles/api/api_chat_invite.h
+++ b/Telegram/SourceFiles/api/api_chat_invite.h
@@ -21,7 +21,6 @@ class SessionController;
 } // namespace Window
 
 namespace Data {
-class CloudImageView;
 class PhotoMedia;
 } // namespace Data
 
@@ -55,10 +54,7 @@ protected:
 	void paintEvent(QPaintEvent *e) override;
 
 private:
-	struct Participant {
-		not_null<UserData*> user;
-		std::shared_ptr<Data::CloudImageView> userpic;
-	};
+	struct Participant;
 	struct ChatInvite {
 		QString title;
 		QString about;
diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp
index 596a41cbe..7ebabbb72 100644
--- a/Telegram/SourceFiles/api/api_who_reacted.cpp
+++ b/Telegram/SourceFiles/api/api_who_reacted.cpp
@@ -114,7 +114,7 @@ struct Context {
 struct Userpic {
 	not_null<PeerData*> peer;
 	QString customEntityData;
-	mutable std::shared_ptr<Data::CloudImageView> view;
+	mutable Ui::PeerUserpicView view;
 	mutable InMemoryKey uniqueKey;
 };
 
@@ -380,7 +380,7 @@ bool UpdateUserpics(
 		const auto peer = not_null{ resolved.peer };
 		const auto &data = ReactionEntityData(resolved.reaction);
 		const auto i = ranges::find(was, peer, &Userpic::peer);
-		if (i != end(was) && i->view) {
+		if (i != end(was) && i->view.cloud) {
 			now.push_back(std::move(*i));
 			now.back().customEntityData = data;
 			continue;
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
index c5d94f52c..e8fd6a843 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp
@@ -84,7 +84,7 @@ private:
 	};
 	struct PeerButton {
 		not_null<History*> history;
-		std::shared_ptr<Data::CloudImageView> userpic;
+		Ui::PeerUserpicView userpic;
 		Ui::Text::String name;
 		Button button;
 	};
diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
index 4e4507a46..75b1487fa 100644
--- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
+++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp
@@ -157,7 +157,7 @@ PaintRoundImageCallback ExceptionRow::generatePaintUserpicCallback() {
 	const auto peer = this->peer();
 	const auto saved = peer->isSelf();
 	const auto replies = peer->isRepliesChat();
-	auto userpic = saved ? nullptr : ensureUserpicView();
+	auto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView();
 	return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
 		if (saved) {
 			Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index 2ed495a55..c4bd163f5 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -48,7 +48,7 @@ PaintRoundImageCallback PaintUserpicCallback(
 			};
 		}
 	}
-	auto userpic = std::shared_ptr<Data::CloudImageView>();
+	auto userpic = Ui::PeerUserpicView();
 	return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
 		peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
 	};
@@ -635,8 +635,8 @@ QString PeerListRow::generateShortName() {
 		: peer()->shortName();
 }
 
-std::shared_ptr<Data::CloudImageView> &PeerListRow::ensureUserpicView() {
-	if (!_userpic) {
+Ui::PeerUserpicView &PeerListRow::ensureUserpicView() {
+	if (!_userpic.cloud && peer()->hasUserpic()) {
 		_userpic = peer()->createUserpicView();
 	}
 	return _userpic;
@@ -646,7 +646,7 @@ PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback() {
 	const auto saved = _isSavedMessagesChat;
 	const auto replies = _isRepliesMessagesChat;
 	const auto peer = this->peer();
-	auto userpic = saved ? nullptr : ensureUserpicView();
+	auto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView();
 	return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
 		if (saved) {
 			Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
@@ -840,10 +840,10 @@ void PeerListRow::lazyInitialize(const style::PeerListItem &st) {
 void PeerListRow::createCheckbox(
 		const style::RoundImageCheckbox &st,
 		Fn<void()> updateCallback) {
-	const auto generateRadius = [=] {
+	const auto generateRadius = [=](int size) {
 		return (!special() && peer()->isForum())
-			? ImageRoundRadius::Large
-			: ImageRoundRadius::Ellipse;
+			? int(size * Ui::ForumUserpicRadiusMultiplier())
+			: std::optional<int>();
 	};
 	_checkbox = std::make_unique<Ui::RoundImageCheckbox>(
 		st,
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h
index f416d74ba..f0c887d6b 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.h
+++ b/Telegram/SourceFiles/boxes/peer_list_box.h
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/rp_widget.h"
 #include "ui/empty_userpic.h"
 #include "ui/unread_badge.h"
+#include "ui/userpic_view.h"
 #include "boxes/abstract_box.h"
 #include "mtproto/sender.h"
 #include "data/data_cloud_file.h"
@@ -84,7 +85,7 @@ public:
 		return _id;
 	}
 
-	[[nodiscard]] std::shared_ptr<Data::CloudImageView> &ensureUserpicView();
+	[[nodiscard]] Ui::PeerUserpicView &ensureUserpicView();
 
 	[[nodiscard]] virtual QString generateName();
 	[[nodiscard]] virtual QString generateShortName();
@@ -262,7 +263,7 @@ private:
 
 	PeerListRowId _id = 0;
 	PeerData *_peer = nullptr;
-	mutable std::shared_ptr<Data::CloudImageView> _userpic;
+	mutable Ui::PeerUserpicView _userpic;
 	std::unique_ptr<Ui::RippleAnimation> _ripple;
 	std::unique_ptr<Ui::RoundImageCheckbox> _checkbox;
 	Ui::Text::String _name;
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index b1584b0f4..35613e4b8 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -632,7 +632,7 @@ auto ChooseTopicBoxController::Row::generatePaintUserpicCallback()
 			int y,
 			int outerWidth,
 			int size) {
-		auto view = std::shared_ptr<Data::CloudImageView>();
+		auto view = Ui::PeerUserpicView();
 		p.translate(x, y);
 		_topic->paintUserpic(p, view, {
 			.st = &st::forumTopicRow,
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
index 6064d9208..f9a24c72b 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
@@ -1042,11 +1042,12 @@ void AddPermanentLinkBlock(
 		state->allUserpicsLoaded = ranges::all_of(
 			state->list,
 			[](const HistoryView::UserpicInRow &element) {
-				return !element.peer->hasUserpic() || element.view->image();
+				return !element.peer->hasUserpic()
+					|| !Ui::PeerUserpicLoading(element.view);
 			});
 		state->content = Ui::JoinedCountContent{
 			.count = state->count,
-			.userpics = state->cachedUserpics
+			.userpics = state->cachedUserpics,
 		};
 	};
 	value->value(
@@ -1087,7 +1088,7 @@ void AddPermanentLinkBlock(
 			} else if (element.peer->userpicUniqueKey(element.view)
 				!= element.uniqueKey) {
 				pushing = true;
-			} else if (!element.view->image()) {
+			} else if (Ui::PeerUserpicLoading(element.view)) {
 				state->allUserpicsLoaded = false;
 			}
 		}
diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
index a78c4b10e..520b46d4c 100644
--- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
@@ -36,7 +36,7 @@ struct UserpicState {
 	PeerShortInfoUserpic current;
 	std::optional<UserPhotosSlice> userSlice;
 	PhotoId userpicPhotoId = PeerData::kUnknownPhotoId;
-	std::shared_ptr<Data::CloudImageView> userpicView;
+	Ui::PeerUserpicView userpicView;
 	std::shared_ptr<Data::PhotoMedia> photoView;
 	std::vector<std::shared_ptr<Data::PhotoMedia>> photoPreloads;
 	InMemoryKey userpicKey;
@@ -76,27 +76,26 @@ void ProcessUserpic(
 		not_null<UserpicState*> state) {
 	state->current.videoDocument = nullptr;
 	state->userpicKey = peer->userpicUniqueKey(state->userpicView);
-	if (!state->userpicView) {
+	if (!state->userpicView.cloud) {
 		GenerateImage(
 			state,
 			peer->generateUserpicImage(
 				state->userpicView,
 				st::shortInfoWidth * style::DevicePixelRatio(),
-				ImageRoundRadius::None),
+				0),
 			false);
 		state->current.photoLoadingProgress = 1.;
 		state->photoView = nullptr;
 		return;
 	}
 	peer->loadUserpic();
-	const auto image = state->userpicView->image();
-	if (!image) {
+	if (Ui::PeerUserpicLoading(state->userpicView)) {
 		state->current.photoLoadingProgress = 0.;
 		state->current.photo = QImage();
 		state->waitingLoad = true;
 		return;
 	}
-	GenerateImage(state, image, true);
+	GenerateImage(state, *state->userpicView.cloud, true);
 	state->current.photoLoadingProgress = peer->userpicPhotoId() ? 0. : 1.;
 	state->photoView = nullptr;
 }
@@ -411,7 +410,7 @@ bool ProcessCurrent(
 			return state->waitingLoad
 				&& (state->photoView
 					? (!!state->photoView->image(Data::PhotoSize::Large))
-					: (state->userpicView && state->userpicView->image()));
+					: (!Ui::PeerUserpicLoading(state->userpicView)));
 		}) | rpl::start_with_next([=] {
 			push();
 		}, lifetime);
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index 95d9978a2..65a643710 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -536,7 +536,7 @@ void ShareBox::applyFilterUpdate(const QString &query) {
 }
 
 PaintRoundImageCallback ForceRoundUserpicCallback(not_null<PeerData*> peer) {
-	auto userpic = std::shared_ptr<Data::CloudImageView>();
+	auto userpic = Ui::PeerUserpicView();
 	auto cache = std::make_shared<QImage>();
 	return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
 		const auto ratio = style::DevicePixelRatio();
@@ -956,9 +956,9 @@ ShareBox::Inner::Chat::Chat(
 	st.checkbox,
 	updateCallback,
 	PaintUserpicCallback(peer, true),
-	[=] { return peer->isForum()
-	? ImageRoundRadius::Large
-	: ImageRoundRadius::Ellipse; })
+	[=](int size) { return peer->isForum()
+		? int(size * Ui::ForumUserpicRadiusMultiplier())
+		: std::optional<int>(); })
 , name(st.checkbox.imageRadius * 2) {
 }
 
diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h
index 5e448857e..f891835c0 100644
--- a/Telegram/SourceFiles/calls/calls_panel.h
+++ b/Telegram/SourceFiles/calls/calls_panel.h
@@ -25,7 +25,6 @@ class PowerSaveBlocker;
 
 namespace Data {
 class PhotoMedia;
-class CloudImageView;
 } // namespace Data
 
 namespace Ui {
diff --git a/Telegram/SourceFiles/calls/calls_userpic.cpp b/Telegram/SourceFiles/calls/calls_userpic.cpp
index 400299f29..f296ef0c6 100644
--- a/Telegram/SourceFiles/calls/calls_userpic.cpp
+++ b/Telegram/SourceFiles/calls/calls_userpic.cpp
@@ -159,7 +159,12 @@ void Userpic::refreshPhoto() {
 		_userPhotoFull = true;
 		createCache(_photo->image(Data::PhotoSize::Thumbnail));
 	} else if (_userPhoto.isNull()) {
-		createCache(_userpic ? _userpic->image() : nullptr);
+		if (const auto cloud = _peer->userpicCloudImage(_userpic)) {
+			auto image = Image(base::duplicate(*cloud));
+			createCache(&image);
+		} else {
+			createCache(nullptr);
+		}
 	}
 }
 
@@ -200,7 +205,7 @@ void Userpic::createCache(Image *image) {
 				Ui::EmptyUserpic::UserpicColor(
 					Data::PeerColorIndex(_peer->id)),
 				_peer->name()
-			).paint(p, 0, 0, size, size);
+			).paintCircle(p, 0, 0, size, size);
 		}
 		//_userPhoto = Images::PixmapFast(Images::Round(
 		//	std::move(filled),
diff --git a/Telegram/SourceFiles/calls/calls_userpic.h b/Telegram/SourceFiles/calls/calls_userpic.h
index 54bfcd8a5..369635348 100644
--- a/Telegram/SourceFiles/calls/calls_userpic.h
+++ b/Telegram/SourceFiles/calls/calls_userpic.h
@@ -8,13 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "ui/rp_widget.h"
+#include "ui/userpic_view.h"
 #include "ui/effects/animations.h"
 
 class PeerData;
 class Image;
 
 namespace Data {
-class CloudImageView;
 class PhotoMedia;
 } // namespace Data
 
@@ -51,7 +51,7 @@ private:
 	Ui::RpWidget _content;
 
 	not_null<PeerData*> _peer;
-	std::shared_ptr<Data::CloudImageView> _userpic;
+	Ui::PeerUserpicView _userpic;
 	std::shared_ptr<Data::PhotoMedia> _photo;
 	Ui::Animations::Simple _mutedAnimation;
 	QPixmap _userPhoto;
diff --git a/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
index f8e5a8a4c..7081c9dc0 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
@@ -350,7 +350,7 @@ void MembersRow::updateBlobAnimation(crl::time now) {
 }
 
 void MembersRow::ensureUserpicCache(
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int size) {
 	Expects(_blobsAnimation != nullptr);
 
@@ -401,7 +401,7 @@ void MembersRow::paintBlobs(
 
 void MembersRow::paintScaledUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &userpic,
+		Ui::PeerUserpicView &userpic,
 		int x,
 		int y,
 		int outerWidth,
diff --git a/Telegram/SourceFiles/calls/group/calls_group_members_row.h b/Telegram/SourceFiles/calls/group/calls_group_members_row.h
index 123ca3ec8..745b32459 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_members_row.h
+++ b/Telegram/SourceFiles/calls/group/calls_group_members_row.h
@@ -19,6 +19,7 @@ struct GroupCallParticipant;
 
 namespace Ui {
 class RippleAnimation;
+struct PeerUserpicView;
 } // namespace Ui
 
 namespace Calls::Group {
@@ -180,7 +181,7 @@ private:
 	void setVolume(int volume);
 
 	void ensureUserpicCache(
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int size);
 	void paintBlobs(
 		Painter &p,
@@ -190,7 +191,7 @@ private:
 		int sizeh, PanelMode mode);
 	void paintScaledUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &userpic,
+		Ui::PeerUserpicView &userpic,
 		int x,
 		int y,
 		int outerWidth,
diff --git a/Telegram/SourceFiles/calls/group/calls_group_menu.cpp b/Telegram/SourceFiles/calls/group/calls_group_menu.cpp
index d37085b29..aad072371 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_menu.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_menu.cpp
@@ -59,7 +59,7 @@ private:
 	const not_null<QAction*> _dummyAction;
 	const style::Menu &_st;
 	const not_null<PeerData*> _peer;
-	std::shared_ptr<Data::CloudImageView> _userpicView;
+	Ui::PeerUserpicView _userpicView;
 
 	Ui::Text::String _text;
 	Ui::Text::String _name;
diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h
index 6dfca8b28..c1d251a50 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_panel.h
+++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h
@@ -29,7 +29,6 @@ class PowerSaveBlocker;
 
 namespace Data {
 class PhotoMedia;
-class CloudImageView;
 class GroupCall;
 } // namespace Data
 
diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
index 2b6d91d6d..e4a04f7c5 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
@@ -462,7 +462,7 @@ void Viewport::RendererGL::validateUserpicFrame(
 	tileData.userpicFrame = tile->row()->peer()->generateUserpicImage(
 		tile->row()->ensureUserpicView(),
 		size.width(),
-		ImageRoundRadius::None);
+		0);
 }
 
 void Viewport::RendererGL::paintTile(
diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
index 6c67b9fe8..f85929ea1 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
@@ -80,7 +80,7 @@ void Viewport::RendererSW::validateUserpicFrame(
 		tile->row()->peer()->generateUserpicImage(
 			tile->row()->ensureUserpicView(),
 			size.width(),
-			ImageRoundRadius::None),
+			0),
 		kBlurRadius);
 }
 
diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
index 477937cd1..0f21d0121 100644
--- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
+++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
@@ -165,6 +165,20 @@ struct FieldAutocomplete::StickerSuggestion {
 	QImage premiumLock;
 };
 
+struct FieldAutocomplete::MentionRow {
+	not_null<UserData*> user;
+	Ui::Text::String name;
+	Ui::PeerUserpicView userpic;
+};
+
+struct FieldAutocomplete::BotCommandRow {
+	not_null<UserData*> user;
+	QString command;
+	QString description;
+	Ui::PeerUserpicView userpic;
+	Ui::Text::String descriptionText;
+};
+
 FieldAutocomplete::FieldAutocomplete(
 	QWidget *parent,
 	not_null<Window::SessionController*> controller)
diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h
index 270f59438..a03d4c840 100644
--- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h
+++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h
@@ -31,7 +31,6 @@ class SessionController;
 
 namespace Data {
 class DocumentMedia;
-class CloudImageView;
 } // namespace Data
 
 namespace SendMenu {
@@ -131,20 +130,8 @@ private:
 	class Inner;
 	friend class Inner;
 	struct StickerSuggestion;
-
-	struct MentionRow {
-		not_null<UserData*> user;
-		Ui::Text::String name;
-		std::shared_ptr<Data::CloudImageView> userpic;
-	};
-
-	struct BotCommandRow {
-		not_null<UserData*> user;
-		QString command;
-		QString description;
-		std::shared_ptr<Data::CloudImageView> userpic;
-		Ui::Text::String descriptionText;
-	};
+	struct MentionRow;
+	struct BotCommandRow;
 
 	using HashtagRows = std::vector<QString>;
 	using BotCommandRows = std::vector<BotCommandRow>;
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
index e4fd909a7..78448a9cd 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "chat_helpers/tabbed_selector.h"
 #include "mtproto/sender.h"
 #include "ui/round_rect.h"
+#include "ui/userpic_view.h"
 
 namespace Ui {
 class InputField;
@@ -21,7 +22,6 @@ namespace Data {
 class StickersSet;
 class StickersSetThumbnailView;
 class DocumentMedia;
-class CloudImageView;
 } // namespace Data
 
 namespace Lottie {
@@ -73,7 +73,7 @@ struct StickerIcon {
 	ChannelData *megagroup = nullptr;
 	mutable std::shared_ptr<Data::StickersSetThumbnailView> thumbnailMedia;
 	mutable std::shared_ptr<Data::DocumentMedia> stickerMedia;
-	mutable std::shared_ptr<Data::CloudImageView> megagroupUserpic;
+	mutable Ui::PeerUserpicView megagroupUserpic;
 	int pixw = 0;
 	int pixh = 0;
 	mutable rpl::lifetime lifetime;
diff --git a/Telegram/SourceFiles/data/data_cloud_file.cpp b/Telegram/SourceFiles/data/data_cloud_file.cpp
index d99ad6ea3..6e35f2a8d 100644
--- a/Telegram/SourceFiles/data/data_cloud_file.cpp
+++ b/Telegram/SourceFiles/data/data_cloud_file.cpp
@@ -22,13 +22,6 @@ CloudFile::~CloudFile() {
 	base::take(loader);
 }
 
-void CloudImageView::set(
-		not_null<Main::Session*> session,
-		QImage image) {
-	_image.emplace(std::move(image));
-	session->notifyDownloaderTaskFinished();
-}
-
 CloudImage::CloudImage() = default;
 
 CloudImage::CloudImage(
@@ -37,10 +30,6 @@ CloudImage::CloudImage(
 	update(session, data);
 }
 
-Image *CloudImageView::image() {
-	return _image ? &*_image : nullptr;
-}
-
 void CloudImage::set(
 		not_null<Main::Session*> session,
 		const ImageWithLocation &data) {
@@ -52,11 +41,11 @@ void CloudImage::set(
 		_file.location = ImageLocation();
 		_file.byteSize = 0;
 		_file.flags = CloudFile::Flag();
-		_view = std::weak_ptr<CloudImageView>();
+		_view = std::weak_ptr<QImage>();
 	} else if (was != now
 		&& (!v::is<InMemoryLocation>(was) || v::is<InMemoryLocation>(now))) {
 		_file.location = ImageLocation();
-		_view = std::weak_ptr<CloudImageView>();
+		_view = std::weak_ptr<QImage>();
 	}
 	UpdateCloudFile(
 		_file,
@@ -65,9 +54,7 @@ void CloudImage::set(
 		kImageCacheTag,
 		[=](FileOrigin origin) { load(session, origin); },
 		[=](QImage preloaded, QByteArray) {
-			if (const auto view = activeView()) {
-				view->set(session, data.preloaded);
-			}
+			setToActive(session, std::move(preloaded));
 		});
 }
 
@@ -81,9 +68,7 @@ void CloudImage::update(
 		kImageCacheTag,
 		[=](FileOrigin origin) { load(session, origin); },
 		[=](QImage preloaded, QByteArray) {
-			if (const auto view = activeView()) {
-				view->set(session, data.preloaded);
-			}
+			setToActive(session, std::move(preloaded));
 		});
 }
 
@@ -107,16 +92,14 @@ void CloudImage::load(not_null<Main::Session*> session, FileOrigin origin) {
 	const auto autoLoading = false;
 	const auto finalCheck = [=] {
 		if (const auto active = activeView()) {
-			return !active->image();
+			return active->isNull();
 		} else if (_file.flags & CloudFile::Flag::Loaded) {
 			return false;
 		}
 		return !(_file.flags & CloudFile::Flag::Loaded);
 	};
 	const auto done = [=](QImage result, QByteArray) {
-		if (const auto active = activeView()) {
-			active->set(session, std::move(result));
-		}
+		setToActive(session, std::move(result));
 	};
 	LoadCloudFile(
 		session,
@@ -137,27 +120,37 @@ int CloudImage::byteSize() const {
 	return _file.byteSize;
 }
 
-std::shared_ptr<CloudImageView> CloudImage::createView() {
+std::shared_ptr<QImage> CloudImage::createView() {
 	if (auto active = activeView()) {
 		return active;
 	}
-	auto view = std::make_shared<CloudImageView>();
+	auto view = std::make_shared<QImage>();
 	_view = view;
 	return view;
 }
 
-std::shared_ptr<CloudImageView> CloudImage::activeView() const {
+std::shared_ptr<QImage> CloudImage::activeView() const {
 	return _view.lock();
 }
 
-bool CloudImage::isCurrentView(
-		const std::shared_ptr<CloudImageView> &view) const {
+bool CloudImage::isCurrentView(const std::shared_ptr<QImage> &view) const {
 	if (!view) {
 		return empty();
 	}
 	return !view.owner_before(_view) && !_view.owner_before(view);
 }
 
+void CloudImage::setToActive(
+		not_null<Main::Session*> session,
+		QImage image) {
+	if (const auto view = activeView()) {
+		*view = image.isNull()
+			? Image::Empty()->original()
+			: std::move(image);
+		session->notifyDownloaderTaskFinished();
+	}
+}
+
 void UpdateCloudFile(
 		CloudFile &file,
 		const ImageWithLocation &data,
diff --git a/Telegram/SourceFiles/data/data_cloud_file.h b/Telegram/SourceFiles/data/data_cloud_file.h
index 799a1a688..e71cbe462 100644
--- a/Telegram/SourceFiles/data/data_cloud_file.h
+++ b/Telegram/SourceFiles/data/data_cloud_file.h
@@ -44,17 +44,6 @@ struct CloudFile final {
 	base::flags<Flag> flags;
 };
 
-class CloudImageView final {
-public:
-	void set(not_null<Main::Session*> session, QImage image);
-
-	[[nodiscard]] Image *image();
-
-private:
-	std::optional<Image> _image;
-
-};
-
 class CloudImage final {
 public:
 	CloudImage();
@@ -79,14 +68,16 @@ public:
 	[[nodiscard]] const ImageLocation &location() const;
 	[[nodiscard]] int byteSize() const;
 
-	[[nodiscard]] std::shared_ptr<CloudImageView> createView();
-	[[nodiscard]] std::shared_ptr<CloudImageView> activeView() const;
+	[[nodiscard]] std::shared_ptr<QImage> createView();
+	[[nodiscard]] std::shared_ptr<QImage> activeView() const;
 	[[nodiscard]] bool isCurrentView(
-		const std::shared_ptr<CloudImageView> &view) const;
+		const std::shared_ptr<QImage> &view) const;
 
 private:
+	void setToActive(not_null<Main::Session*> session, QImage image);
+
 	CloudFile _file;
-	std::weak_ptr<CloudImageView> _view;
+	std::weak_ptr<QImage> _view;
 
 };
 
diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp
index b8ed32698..bd027837b 100644
--- a/Telegram/SourceFiles/data/data_folder.cpp
+++ b/Telegram/SourceFiles/data/data_folder.cpp
@@ -216,7 +216,7 @@ void Folder::loadUserpic() {
 
 void Folder::paintUserpic(
 		Painter &p,
-		std::shared_ptr<CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Dialogs::Ui::PaintContext &context) const {
 	paintUserpic(
 		p,
diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h
index 77805ef25..779c1ba1b 100644
--- a/Telegram/SourceFiles/data/data_folder.h
+++ b/Telegram/SourceFiles/data/data_folder.h
@@ -57,7 +57,7 @@ public:
 	void loadUserpic() override;
 	void paintUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Dialogs::Ui::PaintContext &context) const override;
 	void paintUserpic(Painter &p, int x, int y, int size) const;
 	void paintUserpic(
diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp
index 5c87c3656..9e56bca29 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.cpp
+++ b/Telegram/SourceFiles/data/data_forum_topic.cpp
@@ -568,7 +568,7 @@ void ForumTopic::loadUserpic() {
 
 void ForumTopic::paintUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Dialogs::Ui::PaintContext &context) const {
 	const auto &st = context.st;
 	auto position = QPoint(st->padding.left(), st->padding.top());
diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h
index 12a4eb26e..e2a631723 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.h
+++ b/Telegram/SourceFiles/data/data_forum_topic.h
@@ -150,7 +150,7 @@ public:
 	void loadUserpic() override;
 	void paintUserpic(
 		Painter &p,
-		std::shared_ptr<CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Dialogs::Ui::PaintContext &context) const override;
 	void clearUserpicLoops();
 
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index a296bc72a..25cf34ad2 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -191,7 +191,7 @@ void PeerData::updateNameDelayed(
 		}
 	}
 	_name = newName;
-	_userpicEmpty = nullptr;
+	invalidateEmptyUserpic();
 
 	auto flags = UpdateFlag::None | UpdateFlag::None;
 	auto oldFirstLetters = base::flat_set<QChar>();
@@ -231,13 +231,17 @@ not_null<Ui::EmptyUserpic*> PeerData::ensureEmptyUserpic() const {
 		const auto user = asUser();
 		_userpicEmpty = std::make_unique<Ui::EmptyUserpic>(
 			Ui::EmptyUserpic::UserpicColor(Data::PeerColorIndex(id)),
-			user && user->isInaccessible()
+			((user && user->isInaccessible())
 				? Ui::EmptyUserpic::InaccessibleName()
-				: name());
+				: name()));
 	}
 	return _userpicEmpty.get();
 }
 
+void PeerData::invalidateEmptyUserpic() {
+	_userpicEmpty = nullptr;
+}
+
 ClickHandlerPtr PeerData::createOpenLink() {
 	return std::make_shared<PeerClickHandler>(this);
 }
@@ -265,50 +269,45 @@ void PeerData::setUserpicPhoto(const MTPPhoto &data) {
 	}
 }
 
-Image *PeerData::currentUserpic(
-		std::shared_ptr<Data::CloudImageView> &view) const {
-	if (!_userpic.isCurrentView(view)) {
-		view = _userpic.createView();
-		_userpic.load(&session(), userpicOrigin());
+QImage *PeerData::userpicCloudImage(Ui::PeerUserpicView &view) const {
+	if (!_userpic.isCurrentView(view.cloud)) {
+		if (!_userpic.empty()) {
+			view.cloud = _userpic.createView();
+			_userpic.load(&session(), userpicOrigin());
+		} else {
+			view.cloud = nullptr;
+			if (view.empty.null()) {
+				view.paletteVersion = 0;
+			}
+		}
 	}
-	const auto image = view ? view->image() : nullptr;
-	if (image) {
+	if (const auto image = view.cloud.get(); image && !image->isNull()) {
 		_userpicEmpty = nullptr;
+		return image;
 	} else if (isNotificationsUser()) {
-		static auto result = Image(
-			Window::LogoNoMargin().scaledToWidth(
-				kUserpicSize,
-				Qt::SmoothTransformation));
+		static auto result = Window::LogoNoMargin().scaledToWidth(
+			kUserpicSize,
+			Qt::SmoothTransformation);
 		return &result;
 	}
-	return image;
+	return nullptr;
 }
 
 void PeerData::paintUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int x,
 		int y,
 		int size) const {
-	if (const auto userpic = currentUserpic(view)) {
-		const auto rounding = isForum()
-			? Images::Option::RoundLarge
-			: Images::Option::RoundCircle;
-		p.drawPixmap(
-			x,
-			y,
-			userpic->pix(size, size, { .options = rounding }));
-	} else if (isForum()) {
-		ensureEmptyUserpic()->paintRounded(
-			p,
-			x,
-			y,
-			x + size + x,
-			size,
-			st::roundRadiusLarge);
-	} else{
-		ensureEmptyUserpic()->paint(p, x, y, x + size + x, size);
-	}
+	const auto cloud = userpicCloudImage(view);
+	const auto ratio = style::DevicePixelRatio();
+	Ui::ValidateUserpicCache(
+		view,
+		cloud,
+		cloud ? nullptr : ensureEmptyUserpic().get(),
+		size * ratio,
+		isForum());
+	p.drawImage(QRect(x, y, size, size), view.cached);
 }
 
 void PeerData::loadUserpic() {
@@ -319,112 +318,76 @@ bool PeerData::hasUserpic() const {
 	return !_userpic.empty();
 }
 
-std::shared_ptr<Data::CloudImageView> PeerData::activeUserpicView() {
-	return _userpic.empty() ? nullptr : _userpic.activeView();
+Ui::PeerUserpicView PeerData::activeUserpicView() {
+	return { .cloud = _userpic.empty() ? nullptr : _userpic.activeView() };
 }
 
-std::shared_ptr<Data::CloudImageView> PeerData::createUserpicView() {
+Ui::PeerUserpicView PeerData::createUserpicView() {
 	if (_userpic.empty()) {
-		return nullptr;
+		return {};
 	}
 	auto result = _userpic.createView();
 	_userpic.load(&session(), userpicPhotoOrigin());
-	return result;
+	return { .cloud = result };
 }
 
-bool PeerData::useEmptyUserpic(
-		std::shared_ptr<Data::CloudImageView> &view) const {
-	return !currentUserpic(view);
+bool PeerData::useEmptyUserpic(Ui::PeerUserpicView &view) const {
+	return !userpicCloudImage(view);
 }
 
-InMemoryKey PeerData::userpicUniqueKey(
-		std::shared_ptr<Data::CloudImageView> &view) const {
+InMemoryKey PeerData::userpicUniqueKey(Ui::PeerUserpicView &view) const {
 	return useEmptyUserpic(view)
 		? ensureEmptyUserpic()->uniqueKey()
 		: inMemoryKey(_userpic.location());
 }
 
-void PeerData::saveUserpic(
-		std::shared_ptr<Data::CloudImageView> &view,
-		const QString &path,
-		int size) const {
-	generateUserpicImage(view, size * cIntRetinaFactor()).save(path, "PNG");
-}
-
-void PeerData::saveUserpicRounded(
-		std::shared_ptr<Data::CloudImageView> &view,
-		const QString &path,
-		int size) const {
-	generateUserpicImage(
-		view,
-		size * cIntRetinaFactor(),
-		ImageRoundRadius::Small).save(path, "PNG");
-}
-
-QPixmap PeerData::genUserpic(
-		std::shared_ptr<Data::CloudImageView> &view,
-		int size) const {
-	if (const auto userpic = currentUserpic(view)) {
-		const auto rounding = isForum()
-			? Images::Option::RoundLarge
-			: Images::Option::RoundCircle;
-		return userpic->pix(size, size, { .options = rounding });
-	}
-	const auto ratio = style::DevicePixelRatio();
-	auto result = QImage(
-		QSize(size, size) * ratio,
-		QImage::Format_ARGB32_Premultiplied);
-	result.setDevicePixelRatio(ratio);
-	result.fill(Qt::transparent);
-	{
-		Painter p(&result);
-		paintUserpic(p, view, 0, 0, size);
-	}
-	return Ui::PixmapFromImage(std::move(result));
-}
-
 QImage PeerData::generateUserpicImage(
-		std::shared_ptr<Data::CloudImageView> &view,
-		int size) const {
-	return generateUserpicImage(
-		view,
-		size,
-		isForum() ? ImageRoundRadius::Large : ImageRoundRadius::Ellipse);
-}
-
-QImage PeerData::generateUserpicImage(
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int size,
-		ImageRoundRadius radius) const {
-	if (const auto userpic = currentUserpic(view)) {
-		const auto options = (radius == ImageRoundRadius::Ellipse)
-			? Images::Option::RoundCircle
-			: (radius == ImageRoundRadius::Large)
-			? Images::Option::RoundLarge
-			: (radius == ImageRoundRadius::Small)
-			? Images::Option::RoundSmall
-			: Images::Option();
-		return userpic->pixNoCache(
+		std::optional<int> radius) const {
+	if (const auto userpic = userpicCloudImage(view)) {
+		auto image = userpic->scaled(
 			{ size, size },
-			{ .options = options }).toImage();
+			Qt::IgnoreAspectRatio,
+			Qt::SmoothTransformation);
+		const auto round = [&](int radius) {
+			return Images::Round(
+				std::move(image),
+				Images::CornersMask(radius));
+		};
+		if (radius == 0) {
+			return image;
+		} else if (radius) {
+			return round(*radius);
+		} else if (isForum()) {
+			return round(size * Ui::ForumUserpicRadiusMultiplier());
+		} else {
+			return Images::Circle(std::move(image));
+		}
 	}
 	auto result = QImage(
 		QSize(size, size),
 		QImage::Format_ARGB32_Premultiplied);
 	result.fill(Qt::transparent);
-	{
-		Painter p(&result);
-		if (radius == ImageRoundRadius::Ellipse) {
-			ensureEmptyUserpic()->paint(p, 0, 0, size, size);
-		} else if (radius == ImageRoundRadius::None) {
-			ensureEmptyUserpic()->paintSquare(p, 0, 0, size, size);
-		} else if (radius == ImageRoundRadius::Large) {
-			const auto radius = st::roundRadiusLarge;
-			ensureEmptyUserpic()->paintRounded(p, 0, 0, size, size, radius);
-		} else {
-			ensureEmptyUserpic()->paintRounded(p, 0, 0, size, size);
-		}
+
+	Painter p(&result);
+	if (radius == 0) {
+		ensureEmptyUserpic()->paintSquare(p, 0, 0, size, size);
+	} else if (radius) {
+		ensureEmptyUserpic()->paintRounded(p, 0, 0, size, size, *radius);
+	} else if (isForum()) {
+		ensureEmptyUserpic()->paintRounded(
+			p,
+			0,
+			0,
+			size,
+			size,
+			size * Ui::ForumUserpicRadiusMultiplier());
+	} else {
+		ensureEmptyUserpic()->paintCircle(p, 0, 0, size, size);
 	}
+	p.end();
+
 	return result;
 }
 
diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h
index fe925ad82..566a1fd4c 100644
--- a/Telegram/SourceFiles/data/data_peer.h
+++ b/Telegram/SourceFiles/data/data_peer.h
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_flags.h"
 #include "data/notify/data_peer_notify_settings.h"
 #include "data/data_cloud_file.h"
+#include "ui/userpic_view.h"
 
 struct BotInfo;
 class PeerData;
@@ -36,13 +37,12 @@ class Forum;
 class ForumTopic;
 class Session;
 class GroupCall;
-class CloudImageView;
 struct ReactionId;
 
-int PeerColorIndex(PeerId peerId);
+[[nodiscard]] int PeerColorIndex(PeerId peerId);
 
 // Must be used only for PeerColor-s.
-PeerId FakePeerIdForJustName(const QString &name);
+[[nodiscard]] PeerId FakePeerIdForJustName(const QString &name);
 
 class RestrictionCheckResult {
 public:
@@ -264,13 +264,13 @@ public:
 	void setUserpicPhoto(const MTPPhoto &data);
 	void paintUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int x,
 		int y,
 		int size) const;
 	void paintUserpicLeft(
 			Painter &p,
-			std::shared_ptr<Data::CloudImageView> &view,
+			Ui::PeerUserpicView &view,
 			int x,
 			int y,
 			int w,
@@ -279,30 +279,14 @@ public:
 	}
 	void loadUserpic();
 	[[nodiscard]] bool hasUserpic() const;
-	[[nodiscard]] std::shared_ptr<Data::CloudImageView> activeUserpicView();
-	[[nodiscard]] std::shared_ptr<Data::CloudImageView> createUserpicView();
-	[[nodiscard]] bool useEmptyUserpic(
-		std::shared_ptr<Data::CloudImageView> &view) const;
-	[[nodiscard]] InMemoryKey userpicUniqueKey(
-		std::shared_ptr<Data::CloudImageView> &view) const;
-	void saveUserpic(
-		std::shared_ptr<Data::CloudImageView> &view,
-		const QString &path,
-		int size) const;
-	void saveUserpicRounded(
-		std::shared_ptr<Data::CloudImageView> &view,
-		const QString &path,
-		int size) const;
-	[[nodiscard]] QPixmap genUserpic(
-		std::shared_ptr<Data::CloudImageView> &view,
-		int size) const;
+	[[nodiscard]] Ui::PeerUserpicView activeUserpicView();
+	[[nodiscard]] Ui::PeerUserpicView createUserpicView();
+	[[nodiscard]] bool useEmptyUserpic(Ui::PeerUserpicView &view) const;
+	[[nodiscard]] InMemoryKey userpicUniqueKey(Ui::PeerUserpicView &view) const;
 	[[nodiscard]] QImage generateUserpicImage(
-		std::shared_ptr<Data::CloudImageView> &view,
-		int size) const;
-	[[nodiscard]] QImage generateUserpicImage(
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int size,
-		ImageRoundRadius radius) const;
+		std::optional<int> radius = {}) const;
 	[[nodiscard]] ImageLocation userpicLocation() const {
 		return _userpic.location();
 	}
@@ -332,8 +316,7 @@ public:
 		return _openLink;
 	}
 
-	[[nodiscard]] Image *currentUserpic(
-		std::shared_ptr<Data::CloudImageView> &view) const;
+	[[nodiscard]] QImage *userpicCloudImage(Ui::PeerUserpicView &view) const;
 
 	[[nodiscard]] bool canPinMessages() const;
 	[[nodiscard]] bool canEditMessagesIndefinitely() const;
@@ -423,6 +406,7 @@ protected:
 		const QString &newUsername);
 	void updateUserpic(PhotoId photoId, MTP::DcId dcId, bool hasVideo);
 	void clearUserpic();
+	void invalidateEmptyUserpic();
 
 private:
 	void fillNames();
diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp
index ae46d9d6f..4b016d9dd 100644
--- a/Telegram/SourceFiles/data/data_peer_values.cpp
+++ b/Telegram/SourceFiles/data/data_peer_values.cpp
@@ -515,17 +515,10 @@ bool ChannelHasActiveCall(not_null<ChannelData*> channel) {
 rpl::producer<QImage> PeerUserpicImageValue(
 		not_null<PeerData*> peer,
 		int size) {
-	return PeerUserpicImageValue(peer, size, ImageRoundRadius::Ellipse);
-}
-
-rpl::producer<QImage> PeerUserpicImageValue(
-		not_null<PeerData*> peer,
-		int size,
-		ImageRoundRadius radius) {
 	return [=](auto consumer) {
 		auto result = rpl::lifetime();
 		struct State {
-			std::shared_ptr<CloudImageView> view;
+			Ui::PeerUserpicView view;
 			rpl::lifetime waiting;
 			InMemoryKey key = {};
 			bool empty = true;
@@ -534,7 +527,7 @@ rpl::producer<QImage> PeerUserpicImageValue(
 		const auto state = result.make_state<State>();
 		state->push = [=] {
 			const auto key = peer->userpicUniqueKey(state->view);
-			const auto loading = state->view && !state->view->image();
+			const auto loading = Ui::PeerUserpicLoading(state->view);
 
 			if (loading && !state->waiting) {
 				peer->session().downloaderTaskFinished(
@@ -548,8 +541,7 @@ rpl::producer<QImage> PeerUserpicImageValue(
 			}
 			state->key = key;
 			state->empty = false;
-			consumer.put_next(
-				peer->generateUserpicImage(state->view, size, radius));
+			consumer.put_next(peer->generateUserpicImage(state->view, size));
 		};
 		peer->session().changes().peerFlagsValue(
 			peer,
diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h
index 72374bf0c..0a5d5f398 100644
--- a/Telegram/SourceFiles/data/data_peer_values.h
+++ b/Telegram/SourceFiles/data/data_peer_values.h
@@ -134,10 +134,6 @@ inline auto PeerFullFlagValue(
 [[nodiscard]] rpl::producer<QImage> PeerUserpicImageValue(
 	not_null<PeerData*> peer,
 	int size);
-[[nodiscard]] rpl::producer<QImage> PeerUserpicImageValue(
-	not_null<PeerData*> peer,
-	int size,
-	ImageRoundRadius radius);
 
 [[nodiscard]] const AllowedReactions &PeerAllowedReactions(
 	not_null<PeerData*> peer);
diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp
index d568987de..7d3ee694b 100644
--- a/Telegram/SourceFiles/data/data_user.cpp
+++ b/Telegram/SourceFiles/data/data_user.cpp
@@ -228,22 +228,27 @@ void UserData::setAccessHash(uint64 accessHash) {
 	if (accessHash == kInaccessibleAccessHashOld) {
 		_accessHash = 0;
 		_flags.add(Flag::Deleted);
+		invalidateEmptyUserpic();
 	} else {
 		_accessHash = accessHash;
 	}
 }
 
 void UserData::setFlags(UserDataFlags which) {
+	if ((which & UserDataFlag::Deleted)
+		!= (flags() & UserDataFlag::Deleted)) {
+		invalidateEmptyUserpic();
+	}
 	_flags.set((flags() & UserDataFlag::Self)
 		| (which & ~UserDataFlag::Self));
 }
 
 void UserData::addFlags(UserDataFlags which) {
-	_flags.add(which & ~UserDataFlag::Self);
+	setFlags(flags() | which);
 }
 
 void UserData::removeFlags(UserDataFlags which) {
-	_flags.remove(which & ~UserDataFlag::Self);
+	setFlags(flags() & ~which);
 }
 
 bool UserData::isVerified() const {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.h b/Telegram/SourceFiles/dialogs/dialogs_entry.h
index a8940d2ae..bfb38d6dc 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_entry.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_entry.h
@@ -25,10 +25,10 @@ class Session;
 class Forum;
 class Folder;
 class ForumTopic;
-class CloudImageView;
 } // namespace Data
 
 namespace Ui {
+struct PeerUserpicView;
 } // namespace Ui
 
 namespace Dialogs::Ui {
@@ -233,7 +233,7 @@ public:
 	virtual void loadUserpic() = 0;
 	virtual void paintUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Ui::PaintContext &context) const = 0;
 
 	[[nodiscard]] TimeId chatListTimeId() const {
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 51b77d469..d41c86164 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -1134,7 +1134,7 @@ void InnerWidget::paintSearchInFilter(
 void InnerWidget::paintSearchInPeer(
 		Painter &p,
 		not_null<PeerData*> peer,
-		std::shared_ptr<Data::CloudImageView> &userpic,
+		Ui::PeerUserpicView &userpic,
 		int top,
 		const Ui::Text::String &text) const {
 	const auto paintUserpic = [&](Painter &p, int x, int y, int size) {
@@ -1168,7 +1168,7 @@ void InnerWidget::paintSearchInTopic(
 		Painter &p,
 		const Ui::PaintContext &context,
 		not_null<Data::ForumTopic*> topic,
-		std::shared_ptr<Data::CloudImageView> &userpic,
+		Ui::PeerUserpicView &userpic,
 		int top,
 		const Ui::Text::String &text) const {
 	const auto paintUserpic = [&](Painter &p, int x, int y, int size) {
@@ -2846,7 +2846,7 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
 		_searchFromUserUserpic = _searchFromPeer->createUserpicView();
 	} else {
 		_cancelSearchFromUser->hide();
-		_searchFromUserUserpic = nullptr;
+		_searchFromUserUserpic = {};
 	}
 	if (_searchInChat || _searchFromPeer) {
 		refreshSearchInChatLabel();
@@ -2855,7 +2855,7 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
 	if (const auto peer = _searchInChat.peer()) {
 		_searchInChatUserpic = peer->createUserpicView();
 	} else {
-		_searchInChatUserpic = nullptr;
+		_searchInChatUserpic = {};
 	}
 	moveCancelSearchButtons();
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
index dc153f5d5..9a78d7784 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/dragging_scroll_manager.h"
 #include "ui/effects/animations.h"
 #include "ui/rp_widget.h"
+#include "ui/userpic_view.h"
 #include "base/flags.h"
 #include "base/object_ptr.h"
 
@@ -39,7 +40,6 @@ class SessionController;
 } // namespace Window
 
 namespace Data {
-class CloudImageView;
 class Thread;
 class Folder;
 class Forum;
@@ -340,7 +340,7 @@ private:
 	void paintSearchInPeer(
 		Painter &p,
 		not_null<PeerData*> peer,
-		std::shared_ptr<Data::CloudImageView> &userpic,
+		Ui::PeerUserpicView &userpic,
 		int top,
 		const Ui::Text::String &text) const;
 	void paintSearchInSaved(
@@ -355,7 +355,7 @@ private:
 		Painter &p,
 		const Ui::PaintContext &context,
 		not_null<Data::ForumTopic*> topic,
-		std::shared_ptr<Data::CloudImageView> &userpic,
+		Ui::PeerUserpicView &userpic,
 		int top,
 		const Ui::Text::String &text) const;
 	template <typename PaintUserpic>
@@ -470,8 +470,8 @@ private:
 	Key _searchInChat;
 	History *_searchInMigrated = nullptr;
 	PeerData *_searchFromPeer = nullptr;
-	mutable std::shared_ptr<Data::CloudImageView> _searchInChatUserpic;
-	mutable std::shared_ptr<Data::CloudImageView> _searchFromUserUserpic;
+	mutable Ui::PeerUserpicView _searchInChatUserpic;
+	mutable Ui::PeerUserpicView _searchFromUserUserpic;
 	Ui::Text::String _searchInChatText;
 	Ui::Text::String _searchFromUserText;
 	RowDescriptor _menuRow;
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
index a557aea79..376ca14fe 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp
@@ -38,11 +38,11 @@ constexpr auto kNoneLayer = 0;
 void PaintCornerBadgeTTLFrame(
 		QPainter &q,
 		not_null<PeerData*> peer,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		float64 progress,
 		int photoSize) {
 	const auto ttl = peer->messagesTTL();
-	if (!ttl || !view) {
+	if (!ttl) {
 		return;
 	}
 	using Radius = ImageRoundRadius;
@@ -54,7 +54,7 @@ void PaintCornerBadgeTTLFrame(
 	const auto ratio = style::DevicePixelRatio();
 	const auto fullSize = photoSize;
 	const auto blurredFull = Images::BlurLargeImage(
-		peer->generateUserpicImage(view, fullSize * ratio, Radius::None),
+		peer->generateUserpicImage(view, fullSize * ratio, 0),
 		kBlurRadius);
 	const auto partRect = CornerBadgeTTLRect(fullSize);
 	const auto &partSize = partRect.width();
@@ -338,7 +338,7 @@ void Row::PaintCornerBadgeFrame(
 		not_null<CornerBadgeUserpic*> data,
 		not_null<PeerData*> peer,
 		Ui::VideoUserpic *videoUserpic,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Ui::PaintContext &context) {
 	data->frame.fill(Qt::transparent);
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h
index a8689bf2a..e1ccc435c 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_row.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_row.h
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/text/text.h"
 #include "ui/effects/animations.h"
 #include "ui/unread_badge.h"
+#include "ui/userpic_view.h"
 #include "dialogs/dialogs_key.h"
 #include "dialogs/ui/dialogs_message_view.h"
 
@@ -20,10 +21,6 @@ namespace style {
 struct DialogRow;
 } // namespace style
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
-
 namespace Ui {
 class RippleAnimation;
 } // namespace Ui
@@ -69,13 +66,12 @@ public:
 		int outerWidth,
 		const QColor *colorOverride = nullptr) const;
 
-	[[nodiscard]] auto userpicView() const
-	-> std::shared_ptr<Data::CloudImageView> & {
+	[[nodiscard]] Ui::PeerUserpicView &userpicView() const {
 		return _userpic;
 	}
 
 private:
-	mutable std::shared_ptr<Data::CloudImageView> _userpic;
+	mutable Ui::PeerUserpicView _userpic;
 	mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
 
 };
@@ -182,7 +178,7 @@ private:
 		not_null<CornerBadgeUserpic*> data,
 		not_null<PeerData*> peer,
 		Ui::VideoUserpic *videoUserpic,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Ui::PaintContext &context);
 
 	Key _id;
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
index 02a022823..32d53834e 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
@@ -285,9 +285,9 @@ void PaintRow(
 		not_null<Entry*> entry,
 		VideoUserpic *videoUserpic,
 		PeerData *from,
-		Ui::PeerBadge &fromBadge,
+		PeerBadge &fromBadge,
 		Fn<void()> customEmojiRepaint,
-		const Ui::Text::String &fromName,
+		const Text::String &fromName,
 		const HiddenSenderInfo *hiddenSenderInfo,
 		HistoryItem *item,
 		const Data::Draft *draft,
@@ -306,8 +306,8 @@ void PaintRow(
 		: context.selected
 		? st::dialogsBgOver
 		: anim::brush(
-			st::dialogsBg, 
-			st::dialogsBgOver, 
+			st::dialogsBg,
+			st::dialogsBgOver,
 			context.childListShown);
 	p.fillRect(geometry, bg);
 	if (!(flags & Flag::TopicJumpRipple)) {
@@ -342,7 +342,7 @@ void PaintRow(
 			(flags & Flag::AllowUserOnline) ? history : nullptr,
 			context);
 	} else if (hiddenSenderInfo) {
-		hiddenSenderInfo->emptyUserpic.paint(
+		hiddenSenderInfo->emptyUserpic.paintCircle(
 			p,
 			context.st->padding.left(),
 			context.st->padding.top(),
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h
index 29ebc80c0..bb637c852 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h
@@ -38,8 +38,8 @@ using namespace ::Ui;
 class VideoUserpic;
 
 struct TopicJumpCorners {
-	Ui::CornersPixmaps normal;
-	Ui::CornersPixmaps inverted;
+	CornersPixmaps normal;
+	CornersPixmaps inverted;
 	QPixmap small;
 	int invertedRadius = 0;
 	int smallKey = 0; // = `-radius` if top right else `radius`.
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
index 353a11300..cdebd9da4 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
@@ -30,7 +30,7 @@ int VideoUserpic::frameIndex() const {
 
 void VideoUserpic::paintLeft(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int x,
 		int y,
 		int w,
@@ -133,7 +133,7 @@ void PaintUserpic(
 		Painter &p,
 		not_null<PeerData*> peer,
 		Ui::VideoUserpic *videoUserpic,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		int x,
 		int y,
 		int outerWidth,
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
index 3cc332ba8..6c285ce97 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
@@ -12,10 +12,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 class Painter;
 
 namespace Data {
-class CloudImageView;
 class PhotoMedia;
 } // namespace Data
 
+namespace Ui {
+struct PeerUserpicView;
+} // namespace Ui
+
 namespace Dialogs::Ui {
 
 using namespace ::Ui;
@@ -29,7 +32,7 @@ public:
 
 	void paintLeft(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		PeerUserpicView &view,
 		int x,
 		int y,
 		int w,
@@ -54,8 +57,8 @@ private:
 void PaintUserpic(
 	Painter &p,
 	not_null<PeerData*> peer,
-	Ui::VideoUserpic *videoUserpic,
-	std::shared_ptr<Data::CloudImageView> &view,
+	VideoUserpic *videoUserpic,
+	PeerUserpicView &view,
 	int x,
 	int y,
 	int outerWidth,
diff --git a/Telegram/SourceFiles/editor/editor_crop.cpp b/Telegram/SourceFiles/editor/editor_crop.cpp
index 9e3885a28..78a91aa6a 100644
--- a/Telegram/SourceFiles/editor/editor_crop.cpp
+++ b/Telegram/SourceFiles/editor/editor_crop.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "editor/editor_crop.h"
 
+#include "ui/userpic_view.h"
 #include "styles/style_editor.h"
 #include "styles/style_basic.h"
 #include "styles/style_dialogs.h"
@@ -149,7 +150,7 @@ void Crop::setCropPaint(QRectF &&rect) {
 		_painterPath.addEllipse(_cropPaint);
 	} else if (_data.cropType == EditorData::CropType::RoundedRect) {
 		const auto radius = std::min(_cropPaint.width(), _cropPaint.height())
-			* st::roundRadiusLarge / float64(st::defaultDialogRow.photoSize);
+			* Ui::ForumUserpicRadiusMultiplier();
 		_painterPath.addRoundedRect(_cropPaint, radius, radius);
 	} else {
 		_painterPath.addRect(_cropPaint);
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp
index 07b062d64..6265c81e4 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp
@@ -20,10 +20,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "styles/style_boxes.h"
 #include "styles/style_chat.h"
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
-
 namespace AdminLog {
 namespace {
 
@@ -64,7 +60,7 @@ private:
 	QRect _checkRect;
 
 	const not_null<UserData*> _user;
-	std::shared_ptr<Data::CloudImageView> _userpic;
+	Ui::PeerUserpicView _userpic;
 	Ui::Text::String _name;
 	QString _statusText;
 	bool _statusOnline = false;
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h
index 1eb60a8b1..cf92d2ceb 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h
@@ -19,10 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 struct ChatRestrictionsInfo;
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
-
 namespace Main {
 class Session;
 } // namespace Main
@@ -38,6 +34,7 @@ enum class PointState : char;
 namespace Ui {
 class PopupMenu;
 class ChatStyle;
+struct PeerUserpicView;
 } // namespace Ui
 
 namespace Window {
@@ -278,9 +275,8 @@ private:
 	std::map<not_null<const HistoryItem*>, not_null<Element*>> _itemsByData;
 	base::flat_map<not_null<const HistoryItem*>, TimeId> _itemDates;
 	base::flat_set<FullMsgId> _animatedStickersPlayed;
-	base::flat_map<
-		not_null<PeerData*>,
-		std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache;
+	base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics;
+	base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpicsCache;
 	int _itemsTop = 0;
 	int _itemsWidth = 0;
 	int _itemsHeight = 0;
diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index 0db425b6d..6e9d5e621 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -2262,7 +2262,7 @@ void History::loadUserpic() {
 
 void History::paintUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Dialogs::Ui::PaintContext &context) const {
 	peer->paintUserpic(
 		p,
diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h
index 9f189af24..e3c408820 100644
--- a/Telegram/SourceFiles/history/history.h
+++ b/Telegram/SourceFiles/history/history.h
@@ -398,7 +398,7 @@ public:
 	void loadUserpic() override;
 	void paintUserpic(
 		Painter &p,
-		std::shared_ptr<Data::CloudImageView> &view,
+		Ui::PeerUserpicView &view,
 		const Dialogs::Ui::PaintContext &context) const override;
 
 	void refreshChatListNameSortKey();
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 3cdcf3091..cc36aee31 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -1135,7 +1135,8 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
 
 		// paint the userpic if it intersects the painted rect
 		if (userpicTop + st::msgPhotoSize > clip.top()) {
-			if (const auto from = view->data()->displayFrom()) {
+			const auto item = view->data();
+			if (const auto from = item->displayFrom()) {
 				Dialogs::Ui::PaintUserpic(
 					p,
 					from,
@@ -1146,28 +1147,25 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
 					width(),
 					st::msgPhotoSize,
 					context.paused);
-			} else if (const auto info = view->data()->hiddenSenderInfo()) {
+			} else if (const auto info = item->hiddenSenderInfo()) {
 				if (info->customUserpic.empty()) {
-					info->emptyUserpic.paint(
+					info->emptyUserpic.paintCircle(
 						p,
 						st::historyPhotoLeft,
 						userpicTop,
 						width(),
 						st::msgPhotoSize);
 				} else {
-					const auto painted = info->paintCustomUserpic(
+					auto &userpic = _hiddenSenderUserpics[item->id];
+					const auto valid = info->paintCustomUserpic(
 						p,
+						userpic,
 						st::historyPhotoLeft,
 						userpicTop,
 						width(),
 						st::msgPhotoSize);
-					if (!painted) {
-						const auto itemId = view->data()->fullId();
-						auto &v = _sponsoredUserpics[itemId.msg];
-						if (!info->customUserpic.isCurrentView(v)) {
-							v = info->customUserpic.createView();
-							info->customUserpic.load(&session(), itemId);
-						}
+					if (!valid) {
+						info->customUserpic.load(&session(), item->fullId());
 					}
 				}
 			} else {
diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h
index 43ff3fb45..7a4430524 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.h
+++ b/Telegram/SourceFiles/history/history_inner_widget.h
@@ -20,7 +20,6 @@ struct ClickHandlerContext;
 
 namespace Data {
 struct Group;
-class CloudImageView;
 } // namespace Data
 
 namespace HistoryView {
@@ -51,6 +50,7 @@ class PopupMenu;
 enum class ReportReason;
 struct ChatPaintContext;
 class PathShiftGradient;
+struct PeerUserpicView;
 } // namespace Ui
 
 namespace Dialogs::Ui {
@@ -458,12 +458,9 @@ private:
 	bool _isChatWide = false;
 
 	base::flat_set<not_null<const HistoryItem*>> _animatedStickersPlayed;
-	base::flat_map<
-		not_null<PeerData*>,
-		std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache;
-	base::flat_map<
-		MsgId,
-		std::shared_ptr<Data::CloudImageView>> _sponsoredUserpics;
+	base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics;
+	base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpicsCache;
+	base::flat_map<MsgId, Ui::PeerUserpicView> _hiddenSenderUserpics;
 	base::flat_map<
 		not_null<PeerData*>,
 		std::unique_ptr<VideoUserpic>> _videoUserpics;
diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp
index 160b05609..c9832365d 100644
--- a/Telegram/SourceFiles/history/history_item_components.cpp
+++ b/Telegram/SourceFiles/history/history_item_components.cpp
@@ -144,22 +144,31 @@ ClickHandlerPtr HiddenSenderInfo::ForwardClickHandler() {
 
 bool HiddenSenderInfo::paintCustomUserpic(
 		Painter &p,
+		Ui::PeerUserpicView &view,
 		int x,
 		int y,
 		int outerWidth,
 		int size) const {
-	const auto view = customUserpic.activeView();
-	if (const auto image = view ? view->image() : nullptr) {
-		const auto circled = Images::Option::RoundCircle;
-		p.drawPixmap(
-			x,
-			y,
-			image->pix(size, size, { .options = circled }));
-		return true;
-	} else {
-		emptyUserpic.paint(p, x, y, outerWidth, size);
-		return false;
+	Expects(!customUserpic.empty());
+
+	auto valid = true;
+	if (!customUserpic.isCurrentView(view.cloud)) {
+		view.cloud = customUserpic.createView();
+		valid = false;
 	}
+	const auto image = *view.cloud;
+	if (image.isNull()) {
+		emptyUserpic.paintCircle(p, x, y, outerWidth, size);
+		return valid;
+	}
+	Ui::ValidateUserpicCache(
+		view,
+		image.isNull() ? nullptr : &image,
+		image.isNull() ? &emptyUserpic : nullptr,
+		size * style::DevicePixelRatio(),
+		false);
+	p.drawImage(QRect(x, y, size, size), view.cached);
+	return valid;
 }
 
 void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 9e69f3335..2b04d7784 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -19,6 +19,7 @@ class VoiceSeekClickHandler;
 namespace Ui {
 struct ChatPaintContext;
 class ChatStyle;
+struct PeerUserpicView;
 } // namespace Ui
 
 namespace Data {
@@ -94,6 +95,7 @@ public:
 	[[nodiscard]] const Ui::Text::String &nameText() const;
 	[[nodiscard]] bool paintCustomUserpic(
 		Painter &p,
+		Ui::PeerUserpicView &view,
 		int x,
 		int y,
 		int outerWidth,
diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp
index 60d7994ba..b8bd52464 100644
--- a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp
@@ -139,12 +139,12 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallBarContentByCall(
 		state->someUserpicsNotLoaded = false;
 		for (auto &userpic : state->userpics) {
 			userpic.peer->loadUserpic();
-			const auto pic = userpic.peer->genUserpic(
+			auto image = userpic.peer->generateUserpicImage(
 				userpic.view,
 				userpicSize);
 			userpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view);
 			state->current.users.push_back({
-				.userpic = pic.toImage(),
+				.userpic = std::move(image),
 				.userpicKey = userpic.uniqueKey,
 				.id = userpic.peer->id.value,
 				.speaking = userpic.speaking,
diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_bar.h b/Telegram/SourceFiles/history/view/history_view_group_call_bar.h
index 90814c7f1..fd96caba4 100644
--- a/Telegram/SourceFiles/history/view/history_view_group_call_bar.h
+++ b/Telegram/SourceFiles/history/view/history_view_group_call_bar.h
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "ui/rp_widget.h"
+#include "ui/userpic_view.h"
 
 namespace Ui {
 struct GroupCallBarContent;
@@ -15,7 +16,6 @@ struct GroupCallBarContent;
 
 namespace Data {
 class GroupCall;
-class CloudImageView;
 } // namespace Data
 
 namespace style {
@@ -27,7 +27,7 @@ namespace HistoryView {
 struct UserpicInRow {
 	not_null<PeerData*> peer;
 	bool speaking = false;
-	mutable std::shared_ptr<Data::CloudImageView> view;
+	mutable Ui::PeerUserpicView view;
 	mutable InMemoryKey uniqueKey;
 };
 
diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
index d808544ef..edb83f268 100644
--- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp
@@ -2132,7 +2132,8 @@ void ListWidget::paintEvent(QPaintEvent *e) {
 
 		// paint the userpic if it intersects the painted rect
 		if (userpicTop + st::msgPhotoSize > clip.top()) {
-			if (const auto from = view->data()->displayFrom()) {
+			const auto item = view->data();
+			if (const auto from = item->displayFrom()) {
 				from->paintUserpicLeft(
 					p,
 					_userpics[from],
@@ -2140,28 +2141,25 @@ void ListWidget::paintEvent(QPaintEvent *e) {
 					userpicTop,
 					view->width(),
 					st::msgPhotoSize);
-			} else if (const auto info = view->data()->hiddenSenderInfo()) {
+			} else if (const auto info = item->hiddenSenderInfo()) {
 				if (info->customUserpic.empty()) {
-					info->emptyUserpic.paint(
+					info->emptyUserpic.paintCircle(
 						p,
 						st::historyPhotoLeft,
 						userpicTop,
 						view->width(),
 						st::msgPhotoSize);
 				} else {
-					const auto painted = info->paintCustomUserpic(
+					auto &userpic = _hiddenSenderUserpics[item->id];
+					const auto valid = info->paintCustomUserpic(
 						p,
+						userpic,
 						st::historyPhotoLeft,
 						userpicTop,
 						view->width(),
 						st::msgPhotoSize);
-					if (!painted) {
-						const auto itemId = view->data()->fullId();
-						auto &v = _sponsoredUserpics[itemId.msg];
-						if (!info->customUserpic.isCurrentView(v)) {
-							v = info->customUserpic.createView();
-							info->customUserpic.load(session, itemId);
-						}
+					if (!valid) {
+						info->customUserpic.load(session, item->fullId());
 					}
 				}
 			} else {
diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h
index 7d8c435c0..0dbeb7eef 100644
--- a/Telegram/SourceFiles/history/view/history_view_list_widget.h
+++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h
@@ -29,6 +29,7 @@ class PopupMenu;
 class ChatTheme;
 struct ChatPaintContext;
 enum class TouchScrollState;
+struct PeerUserpicView;
 } // namespace Ui
 
 namespace Window {
@@ -37,7 +38,6 @@ class SessionController;
 
 namespace Data {
 struct Group;
-class CloudImageView;
 struct Reaction;
 struct AllowedReactions;
 } // namespace Data
@@ -637,12 +637,9 @@ private:
 		ItemRevealAnimation> _itemRevealAnimations;
 	int _itemsRevealHeight = 0;
 	base::flat_set<FullMsgId> _animatedStickersPlayed;
-	base::flat_map<
-		not_null<PeerData*>,
-		std::shared_ptr<Data::CloudImageView>> _userpics, _userpicsCache;
-	base::flat_map<
-		MsgId,
-		std::shared_ptr<Data::CloudImageView>> _sponsoredUserpics;
+	base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics;
+	base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpicsCache;
+	base::flat_map<MsgId, Ui::PeerUserpicView> _hiddenSenderUserpics;
 
 	const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
 
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index abc076a20..99ee3f907 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -1105,10 +1105,10 @@ void Message::paintCommentsButton(
 				auto &entry = list[i];
 				const auto peer = entry.peer;
 				auto &view = entry.view;
-				const auto wasView = view.get();
+				const auto wasView = view.cloud.get();
 				if (views->recentRepliers[i] != peer->id
 					|| peer->userpicUniqueKey(view) != entry.uniqueKey
-					|| view.get() != wasView) {
+					|| view.cloud.get() != wasView) {
 					return true;
 				}
 			}
diff --git a/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp b/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp
index 298f43b2a..88af5ad6e 100644
--- a/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp
@@ -86,12 +86,12 @@ rpl::producer<Ui::RequestsBarContent> RequestsBarContentByPeer(
 		state->someUserpicsNotLoaded = false;
 		for (auto &userpic : state->userpics) {
 			userpic.peer->loadUserpic();
-			const auto pic = userpic.peer->genUserpic(
+			auto image = userpic.peer->generateUserpicImage(
 				userpic.view,
 				userpicSize);
 			userpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view);
 			state->current.users.push_back({
-				.userpic = pic.toImage(),
+				.userpic = std::move(image),
 				.userpicKey = userpic.uniqueKey,
 				.id = userpic.peer->id.value,
 			});
diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp
index c52227287..d33f1ba8a 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp
@@ -96,8 +96,8 @@ Contact::Contact(
 
 Contact::~Contact() {
 	history()->owner().unregisterContactView(_userId, _parent);
-	if (_userpic) {
-		_userpic = nullptr;
+	if (!_userpic.null()) {
+		_userpic = {};
 		_parent->checkHeavyPart();
 	}
 }
@@ -177,13 +177,13 @@ void Contact::draw(Painter &p, const PaintContext &context) const {
 	if (_userId) {
 		QRect rthumb(style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, paintw));
 		if (_contact) {
-			const auto was = (_userpic != nullptr);
+			const auto was = !_userpic.null();
 			_contact->paintUserpic(p, _userpic, rthumb.x(), rthumb.y(), st.thumbSize);
-			if (!was && _userpic) {
+			if (!was && !_userpic.null()) {
 				history()->owner().registerHeavyViewPart(_parent);
 			}
 		} else {
-			_photoEmpty->paint(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize);
+			_photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize);
 		}
 		if (context.selected()) {
 			PainterHighQualityEnabler hq(p);
@@ -197,7 +197,7 @@ void Contact::draw(Painter &p, const PaintContext &context) const {
 		p.setPen(stm->msgFileThumbLinkFg);
 		p.drawTextLeft(nameleft, linktop, paintw, _link, _linkw);
 	} else {
-		_photoEmpty->paint(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize);
+		_photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize);
 	}
 	const auto namewidth = paintw - nameleft - nameright;
 
@@ -232,11 +232,11 @@ TextState Contact::textState(QPoint point, StateRequest request) const {
 }
 
 void Contact::unloadHeavyPart() {
-	_userpic = nullptr;
+	_userpic = {};
 }
 
 bool Contact::hasHeavyPart() const {
-	return (_userpic != nullptr);
+	return !_userpic.null();
 }
 
 } // namespace HistoryView
diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.h b/Telegram/SourceFiles/history/view/media/history_view_contact.h
index 1d7912eb9..ecca595f2 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_contact.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_contact.h
@@ -8,10 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "history/view/media/history_view_media.h"
-
-namespace Data {
-class CloudImageView;
-} // namespace Data
+#include "ui/userpic_view.h"
 
 namespace Ui {
 class EmptyUserpic;
@@ -72,7 +69,7 @@ private:
 	QString _fname, _lname, _phone;
 	Ui::Text::String _name;
 	std::unique_ptr<Ui::EmptyUserpic> _photoEmpty;
-	mutable std::shared_ptr<Data::CloudImageView> _userpic;
+	mutable Ui::PeerUserpicView _userpic;
 
 	ClickHandlerPtr _linkl;
 	int _linkw = 0;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.cpp b/Telegram/SourceFiles/history/view/media/history_view_location.cpp
index 0fc8f5ad8..f8890ed1b 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_location.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_location.cpp
@@ -245,17 +245,16 @@ void Location::draw(Painter &p, const PaintContext &context) const {
 void Location::validateImageCache(
 		QSize outer,
 		Ui::BubbleRounding rounding) const {
+	Expects(_media != nullptr);
+
 	const auto ratio = style::DevicePixelRatio();
 	if (_imageCache.size() == (outer * ratio)
-		&& _imageCacheRounding == rounding) {
-		return;
-	}
-	const auto thumbnail = _media->image();
-	if (!thumbnail) {
+		&& _imageCacheRounding == rounding
+		|| _media->isNull()) {
 		return;
 	}
 	_imageCache = Images::Round(
-		thumbnail->original().scaled(
+		_media->scaled(
 			outer * ratio,
 			Qt::IgnoreAspectRatio,
 			Qt::SmoothTransformation),
diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.h b/Telegram/SourceFiles/history/view/media/history_view_location.h
index 46f49caf9..ec412fd00 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_location.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_location.h
@@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace Data {
 class CloudImage;
-class CloudImageView;
 } // namespace Data
 
 namespace HistoryView {
@@ -81,7 +80,7 @@ private:
 	[[nodiscard]] int fullHeight() const;
 
 	const not_null<Data::CloudImage*> _data;
-	mutable std::shared_ptr<Data::CloudImageView> _media;
+	mutable std::shared_ptr<QImage> _media;
 	Ui::Text::String _title, _description;
 	ClickHandlerPtr _link;
 
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
index 3402ce431..73913a198 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp
@@ -46,6 +46,7 @@ struct Photo::Streamed {
 	::Media::Streaming::Instance instance;
 	::Media::Streaming::FrameRequest frozenRequest;
 	QImage frozenFrame;
+	std::array<QImage, 4> roundingCorners;
 	QImage roundingMask;
 };
 
@@ -352,22 +353,66 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
 	}
 }
 
+void Photo::validateUserpicImageCache(QSize size, bool forum) const {
+	const auto forumValue = forum ? 1 : 0;
+	const auto large = _dataMedia->image(PhotoSize::Large);
+	const auto ratio = style::DevicePixelRatio();
+	const auto blurredValue = large ? 0 : 1;
+	if (_imageCache.size() == (size * ratio)
+		&& _imageCacheForum == forumValue
+		&& _imageCacheBlurred == blurredValue) {
+		return;
+	}
+	auto original = [&] {
+		if (large) {
+			return large->original();
+		} else if (const auto thumbnail = _dataMedia->image(
+				PhotoSize::Thumbnail)) {
+			return thumbnail->original();
+		} else if (const auto small = _dataMedia->image(
+				PhotoSize::Small)) {
+			return small->original();
+		} else if (const auto blurred = _dataMedia->thumbnailInline()) {
+			return blurred->original();
+		} else {
+			return Image::Empty()->original();
+		}
+	}();
+	auto args = Images::PrepareArgs();
+	if (blurredValue) {
+		args = args.blurred();
+	}
+	original = Images::Prepare(std::move(original), size, args);
+	if (forumValue) {
+		original = Images::Round(
+			std::move(original),
+			Images::CornersMask(std::min(size.width(), size.height())
+				* Ui::ForumUserpicRadiusMultiplier()
+				* style::DevicePixelRatio()));
+	} else {
+		original = Images::Circle(std::move(original));
+	}
+	_imageCache = std::move(original);
+	_imageCacheForum = forumValue;
+	_imageCacheBlurred = blurredValue;
+}
+
 void Photo::validateImageCache(
 		QSize outer,
 		std::optional<Ui::BubbleRounding> rounding) const {
 	const auto large = _dataMedia->image(PhotoSize::Large);
 	const auto ratio = style::DevicePixelRatio();
-	const auto shouldBeBlurred = !large;
+	const auto blurredValue = large ? 0 : 1;
 	if (_imageCache.size() == (outer * ratio)
 		&& _imageCacheRounding == rounding
-		&& _imageCacheBlurred == shouldBeBlurred) {
+		&& _imageCacheBlurred == blurredValue) {
 		return;
 	}
 	_imageCache = Images::Round(
 		prepareImageCache(outer),
 		MediaRoundingMask(rounding));
 	_imageCacheRounding = rounding;
-	_imageCacheBlurred = shouldBeBlurred;
+	_imageCacheBlurred = blurredValue;
 }
 
 QImage Photo::prepareImageCache(QSize outer) const {
@@ -405,17 +450,23 @@ void Photo::paintUserpicFrame(
 	const auto rect = QRect(photoPosition, size);
 	const auto st = context.st;
 	const auto sti = context.imageStyle();
+	const auto forum = _parent->data()->history()->isForum();
 
 	if (_streamed
 		&& _streamed->instance.player().ready()
 		&& !_streamed->instance.player().videoSize().isEmpty()) {
+		const auto ratio = style::DevicePixelRatio();
 		auto request = ::Media::Streaming::FrameRequest();
-		request.outer = size * cIntRetinaFactor();
-		request.resize = size * cIntRetinaFactor();
-		const auto forum = _parent->data()->history()->isForum();
+		request.outer = request.resize = size * ratio;
 		if (forum) {
+			const auto radius = int(std::min(size.width(), size.height())
+				* Ui::ForumUserpicRadiusMultiplier()
+				* ratio);
+			if (_streamed->roundingCorners[0].width() != radius) {
+				_streamed->roundingCorners = Images::CornersMask(radius);
+			}
 			request.rounding = Images::CornersMaskRef(
-				Images::CornersMask(ImageRoundRadius::Large));
+				_streamed->roundingCorners);
 		} else {
 			if (_streamed->roundingMask.size() != request.outer) {
 				_streamed->roundingMask = Images::EllipseMask(size);
@@ -438,28 +489,8 @@ void Photo::paintUserpicFrame(
 		}
 		return;
 	}
-	const auto pix = [&] {
-		const auto forum = _parent->data()->history()->isForum();
-		const auto args = Images::PrepareArgs{
-			.options = (forum
-				? Images::Option::RoundLarge
-				: Images::Option::RoundCircle),
-		};
-		if (const auto large = _dataMedia->image(PhotoSize::Large)) {
-			return large->pix(size, args);
-		} else if (const auto thumbnail = _dataMedia->image(
-				PhotoSize::Thumbnail)) {
-			return thumbnail->pix(size, args.blurred());
-		} else if (const auto small = _dataMedia->image(
-				PhotoSize::Small)) {
-			return small->pix(size, args.blurred());
-		} else if (const auto blurred = _dataMedia->thumbnailInline()) {
-			return blurred->pix(size, args.blurred());
-		} else {
-			return QPixmap();
-		}
-	}();
-	p.drawPixmap(rect, pix);
+	validateUserpicImageCache(size, forum);
+	p.drawImage(rect, _imageCache);
 
 	if (_data->videoCanBePlayed() && !_streamed) {
 		const auto innerSize = st::msgFileLayout.thumbSize;
diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h
index c9aa130e9..c436e7f22 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_photo.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h
@@ -129,6 +129,7 @@ private:
 	void validateImageCache(
 		QSize outer,
 		std::optional<Ui::BubbleRounding> rounding) const;
+	void validateUserpicImageCache(QSize size, bool forum) const;
 	[[nodiscard]] QImage prepareImageCache(QSize outer) const;
 
 	bool videoAutoplayEnabled() const;
@@ -150,8 +151,9 @@ private:
 	mutable std::unique_ptr<Streamed> _streamed;
 	mutable QImage _imageCache;
 	mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
-	int _serviceWidth = 0;
-	mutable bool _imageCacheBlurred = false;
+	int _serviceWidth : 30 = 0;
+	mutable int _imageCacheForum : 1 = 0;
+	mutable int _imageCacheBlurred : 1 = 0;
 
 };
 
diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp
index 184705564..b64aaeb14 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp
@@ -181,6 +181,11 @@ struct Poll::CloseInformation {
 	Ui::Animations::Basic radial;
 };
 
+struct Poll::RecentVoter {
+	not_null<UserData*> user;
+	mutable Ui::PeerUserpicView userpic;
+};
+
 template <typename Callback>
 Poll::SendingAnimation::SendingAnimation(
 	const QByteArray &option,
@@ -886,9 +891,9 @@ void Poll::paintRecentVoters(
 
 	auto created = false;
 	for (auto &recent : _recentVoters) {
-		const auto was = (recent.userpic != nullptr);
+		const auto was = !recent.userpic.null();
 		recent.user->paintUserpic(p, recent.userpic, x, y, size);
-		if (!was && recent.userpic) {
+		if (!was && !recent.userpic.null()) {
 			created = true;
 		}
 		const auto paintContent = [&](QPainter &p) {
@@ -1499,13 +1504,13 @@ void Poll::clickHandlerPressedChanged(
 
 void Poll::unloadHeavyPart() {
 	for (auto &recent : _recentVoters) {
-		recent.userpic = nullptr;
+		recent.userpic = {};
 	}
 }
 
 bool Poll::hasHeavyPart() const {
 	for (auto &recent : _recentVoters) {
-		if (recent.userpic) {
+		if (!recent.userpic.null()) {
 			return true;
 		}
 	}
diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.h b/Telegram/SourceFiles/history/view/media/history_view_poll.h
index 08a79d1f2..d32ae664b 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_poll.h
+++ b/Telegram/SourceFiles/history/view/media/history_view_poll.h
@@ -12,10 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_poll.h"
 #include "base/weak_ptr.h"
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
-
 namespace Ui {
 class RippleAnimation;
 class FireworksAnimation;
@@ -75,11 +71,7 @@ private:
 	struct SendingAnimation;
 	struct Answer;
 	struct CloseInformation;
-
-	struct RecentVoter {
-		not_null<UserData*> user;
-		mutable std::shared_ptr<Data::CloudImageView> userpic;
-	};
+	struct RecentVoter;
 
 	QSize countOptimalSize() override;
 	QSize countCurrentSize(int newWidth) override;
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp
index fb9816bf4..616f8741b 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp
@@ -524,9 +524,9 @@ void InlineList::resolveUserpicsImage(const Button &button) const {
 		for (auto &entry : userpics->list) {
 			const auto peer = entry.peer;
 			auto &view = entry.view;
-			const auto wasView = view.get();
+			const auto wasView = view.cloud.get();
 			if (peer->userpicUniqueKey(view) != entry.uniqueKey
-				|| view.get() != wasView) {
+				|| view.cloud.get() != wasView) {
 				return true;
 			}
 		}
diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h
index 88ac5d0ce..bc6a3c37c 100644
--- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h
+++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h
@@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_message_reaction_id.h"
 
 namespace Data {
-class CloudImageView;
 class Reactions;
 } // namespace Data
 
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
index f13e0d0ca..2d77babdb 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp
@@ -879,9 +879,17 @@ void Video::prepareThumbnail(QSize size) const {
 			}
 		}
 	}
+	auto resultThumbnailImage = _documentMedia
+		? nullptr
+		: getResultThumb(fileOrigin());
+	const auto resultThumbnail = Image(resultThumbnailImage
+		? base::duplicate(*resultThumbnailImage)
+		: QImage());
 	const auto thumb = _documentMedia
 		? _documentMedia->thumbnail()
-		: getResultThumb(fileOrigin());
+		: resultThumbnailImage
+		? &resultThumbnail
+		: nullptr;
 	if (!thumb) {
 		return;
 	}
@@ -1245,7 +1253,7 @@ void Contact::prepareThumbnail(int width, int height) const {
 			w = width;
 		}
 	}
-	_thumb = thumb->pixNoCache(
+	_thumb = Image(base::duplicate(*thumb)).pixNoCache(
 		QSize(w, h) * style::DevicePixelRatio(),
 		{
 			.options = Images::Option::TransparentBackground,
@@ -1403,7 +1411,7 @@ void Article::prepareThumbnail(int width, int height) const {
 			w = width;
 		}
 	}
-	_thumb = thumb->pixNoCache(
+	_thumb = Image(base::duplicate(*thumb)).pixNoCache(
 		QSize(w, h) * style::DevicePixelRatio(),
 		{
 			.options = Images::Option::TransparentBackground,
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
index 2d5ba3c26..17eeeed0b 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp
@@ -142,7 +142,7 @@ bool ItemBase::hasResultThumb() const {
 			|| !_result->_locationThumbnail.empty());
 }
 
-Image *ItemBase::getResultThumb(Data::FileOrigin origin) const {
+QImage *ItemBase::getResultThumb(Data::FileOrigin origin) const {
 	if (_result && !_thumbnail) {
 		if (!_result->_thumbnail.empty()) {
 			_thumbnail = _result->_thumbnail.createView();
@@ -152,7 +152,9 @@ Image *ItemBase::getResultThumb(Data::FileOrigin origin) const {
 			_result->_locationThumbnail.load(_result->_session, origin);
 		}
 	}
-	return _thumbnail->image();
+	return (_thumbnail && !_thumbnail->isNull())
+		? _thumbnail.get()
+		: nullptr;
 }
 
 QPixmap ItemBase::getResultContactAvatar(int width, int height) const {
diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
index d989bc91c..67574f7e0 100644
--- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
+++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h
@@ -16,10 +16,6 @@ namespace Ui {
 class PathShiftGradient;
 } // namespace Ui
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
-
 namespace InlineBots {
 
 class Result;
@@ -126,7 +122,7 @@ protected:
 	DocumentData *getResultDocument() const;
 	PhotoData *getResultPhoto() const;
 	bool hasResultThumb() const;
-	Image *getResultThumb(Data::FileOrigin origin) const;
+	QImage *getResultThumb(Data::FileOrigin origin) const;
 	QPixmap getResultContactAvatar(int width, int height) const;
 	int getResultDuration() const;
 	QString getResultUrl() const;
@@ -148,7 +144,7 @@ protected:
 
 private:
 	not_null<Context*> _context;
-	mutable std::shared_ptr<Data::CloudImageView> _thumbnail;
+	mutable std::shared_ptr<QImage> _thumbnail;
 
 };
 
diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp
index 3b95165bd..6cbb29714 100644
--- a/Telegram/SourceFiles/main/main_session.cpp
+++ b/Telegram/SourceFiles/main/main_session.cpp
@@ -110,8 +110,9 @@ Session::Session(
 		_user,
 		Data::PeerUpdate::Flag::Photo
 	) | rpl::start_with_next([=] {
-		[[maybe_unused]] const auto image = _user->currentUserpic(
-			_selfUserpicView);
+		auto view = Ui::PeerUserpicView{ .cloud = _selfUserpicView };
+		[[maybe_unused]] const auto image = _user->userpicCloudImage(view);
+		_selfUserpicView = view.cloud;
 	}, lifetime());
 
 	crl::on_main(this, [=] {
diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h
index 0f4d2f963..05dbb9ce5 100644
--- a/Telegram/SourceFiles/main/main_session.h
+++ b/Telegram/SourceFiles/main/main_session.h
@@ -32,7 +32,6 @@ class Templates;
 namespace Data {
 class Session;
 class Changes;
-class CloudImageView;
 } // namespace Data
 
 namespace Storage {
@@ -216,7 +215,7 @@ private:
 
 	const std::unique_ptr<Support::Helper> _supportHelper;
 
-	std::shared_ptr<Data::CloudImageView> _selfUserpicView;
+	std::shared_ptr<QImage> _selfUserpicView;
 	rpl::variable<bool> _premiumPossible = false;
 
 	rpl::event_stream<bool> _termsLockChanges;
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index b04066a7b..a2ad42ffd 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -2827,14 +2827,16 @@ void OverlayWidget::initStreamingThumbnail() {
 
 	_touchbarDisplay.fire(TouchBarItemType::Video);
 
+	auto userpicImage = std::optional<Image>();
 	const auto computePhotoThumbnail = [&] {
 		const auto thumbnail = _photoMedia->image(Data::PhotoSize::Thumbnail);
 		if (thumbnail) {
 			return thumbnail;
 		} else if (_peer && _peer->userpicPhotoId() == _photo->id) {
-			if (const auto view = _peer->activeUserpicView()) {
-				if (const auto image = view->image()) {
-					return image;
+			if (const auto view = _peer->activeUserpicView(); view.cloud) {
+				if (!view.cloud->isNull()) {
+					userpicImage.emplace(base::duplicate(*view.cloud));
+					return &*userpicImage;
 				}
 			}
 		}
@@ -3455,8 +3457,11 @@ void OverlayWidget::validatePhotoCurrentImage() {
 		&& !_message
 		&& _peer
 		&& _peer->hasUserpic()) {
-		if (const auto view = _peer->activeUserpicView()) {
-			validatePhotoImage(view->image(), true);
+		if (const auto view = _peer->activeUserpicView(); view.cloud) {
+			if (!view.cloud->isNull()) {
+				auto image = Image(base::duplicate(*view.cloud));
+				validatePhotoImage(&image, true);
+			}
 		}
 	}
 	if (_staticContent.isNull()) {
diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
index 8bb32c4b3..e6d87f32c 100644
--- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
+++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "main/main_session.h"
 #include "lang/lang_keys.h"
 #include "base/weak_ptr.h"
+#include "window/notifications_utilities.h"
 #include "styles/style_window.h"
 
 #include <QtCore/QBuffer>
@@ -906,7 +907,7 @@ public:
 	void showNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -1013,7 +1014,7 @@ Manager::Private::Private(not_null<Manager*> manager)
 void Manager::Private::showNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -1041,12 +1042,8 @@ void Manager::Private::showNotification(
 	}
 
 	if (!options.hideNameAndPhoto) {
-		const auto userpic = peer->isSelf()
-			? Ui::EmptyUserpic::GenerateSavedMessages(st::notifyMacPhotoSize)
-			: peer->isRepliesChat()
-			? Ui::EmptyUserpic::GenerateRepliesMessages(st::notifyMacPhotoSize)
-			: peer->genUserpic(userpicView, st::notifyMacPhotoSize);
-		notification->setImage(userpic.toImage());
+		notification->setImage(
+			Window::Notifications::GenerateUserpic(peer, userpicView));
 	}
 
 	auto i = _notifications.find(key);
@@ -1183,7 +1180,7 @@ Manager::~Manager() = default;
 void Manager::doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h
index 98ab2b81c..de0c8cad8 100644
--- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h
+++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h
@@ -22,7 +22,7 @@ protected:
 	void doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h
index 718adc1e0..7a5ed2f15 100644
--- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h
+++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h
@@ -22,7 +22,7 @@ protected:
 	void doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm
index 1cb6a975c..1ab0de030 100644
--- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm
+++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/empty_userpic.h"
 #include "main/main_session.h"
 #include "mainwindow.h"
+#include "window/notification_utilities.h"
 #include "styles/style_window.h"
 
 #include <thread>
@@ -189,7 +190,7 @@ public:
 	void showNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -265,7 +266,7 @@ Manager::Private::Private(Manager *manager)
 void Manager::Private::showNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -302,12 +303,8 @@ void Manager::Private::showNotification(
 	[notification setInformativeText:Q2NSString(msg)];
 	if (!options.hideNameAndPhoto
 		&& [notification respondsToSelector:@selector(setContentImage:)]) {
-		auto userpic = peer->isSelf()
-			? Ui::EmptyUserpic::GenerateSavedMessages(st::notifyMacPhotoSize)
-			: peer->isRepliesChat()
-			? Ui::EmptyUserpic::GenerateRepliesMessages(st::notifyMacPhotoSize)
-			: peer->genUserpic(userpicView, st::notifyMacPhotoSize);
-		NSImage *img = Q2NSImage(userpic.toImage());
+		NSImage *img = Q2NSImage(
+			Window::Notifications::GenerateUserpic(peer, userpicView));
 		[notification setContentImage:img];
 	}
 
@@ -475,7 +472,7 @@ Manager::~Manager() = default;
 void Manager::doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm
index c9978da16..916bf4f2f 100644
--- a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm
+++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm
@@ -84,7 +84,7 @@ QImage ArchiveUserpic(not_null<Data::Folder*> folder) {
 	auto result = PrepareImage();
 	Painter paint(&result);
 
-	auto view = std::shared_ptr<Data::CloudImageView>();
+	auto view = Ui::PeerUserpicView();
 	folder->paintUserpic(paint, 0, 0, result.width());
 	return result;
 }
@@ -161,7 +161,7 @@ TimeId CalculateOnlineTill(not_null<PeerData*> peer) {
 @implementation PinnedDialogsPanel {
 	struct Pin {
 		PeerData *peer = nullptr;
-		std::shared_ptr<Data::CloudImageView> userpicView = nullptr;
+		Ui::PeerUserpicView userpicView;
 		int index = -1;
 		QImage userpic;
 		QImage unreadBadge;
diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp
index a1f273da1..cd4da2862 100644
--- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp
+++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp
@@ -405,15 +405,13 @@ void Create(Window::Notifications::System *system) {
 #ifndef __MINGW32__
 class Manager::Private {
 public:
-	using Type = Window::Notifications::CachedUserpics::Type;
-
-	explicit Private(Manager *instance, Type type);
+	explicit Private(Manager *instance);
 	bool init();
 
 	bool showNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -438,7 +436,7 @@ private:
 	bool showNotificationInTryCatch(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -460,9 +458,8 @@ private:
 
 };
 
-Manager::Private::Private(Manager *instance, Type type)
-: _cachedUserpics(type)
-, _guarded(std::make_shared<Manager*>(instance)) {
+Manager::Private::Private(Manager *instance)
+: _guarded(std::make_shared<Manager*>(instance)) {
 	ToastActivations(
 	) | rpl::start_with_next([=](const ToastActivation &activation) {
 		handleActivation(activation);
@@ -657,7 +654,7 @@ void Manager::Private::handleActivation(const ToastActivation &activation) {
 bool Manager::Private::showNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -692,7 +689,7 @@ std::wstring Manager::Private::ensureSendButtonIcon() {
 bool Manager::Private::showNotificationInTryCatch(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -870,7 +867,7 @@ void Manager::Private::tryHide(const ToastNotification &notification) {
 
 Manager::Manager(Window::Notifications::System *system)
 : NativeManager(system)
-, _private(std::make_unique<Private>(this, Private::Type::Rounded)) {
+, _private(std::make_unique<Private>(this)) {
 }
 
 bool Manager::init() {
@@ -890,7 +887,7 @@ Manager::~Manager() = default;
 void Manager::doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.h b/Telegram/SourceFiles/platform/win/notifications_manager_win.h
index 0eaaa2e83..675597bc1 100644
--- a/Telegram/SourceFiles/platform/win/notifications_manager_win.h
+++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.h
@@ -30,7 +30,7 @@ protected:
 	void doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
diff --git a/Telegram/SourceFiles/profile/profile_block_peer_list.h b/Telegram/SourceFiles/profile/profile_block_peer_list.h
index 0ca1027ca..94e654496 100644
--- a/Telegram/SourceFiles/profile/profile_block_peer_list.h
+++ b/Telegram/SourceFiles/profile/profile_block_peer_list.h
@@ -8,10 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #pragma once
 
 #include "profile/profile_block_widget.h"
-
-namespace Data {
-class CloudImageView;
-} // namespace Data
+#include "ui/userpic_view.h"
 
 namespace Ui {
 class RippleAnimation;
@@ -33,7 +30,7 @@ public:
 		~Item();
 
 		const not_null<PeerData*> peer;
-		std::shared_ptr<Data::CloudImageView> userpic;
+		Ui::PeerUserpicView userpic;
 		Ui::Text::String name;
 		QString statusText;
 		bool statusHasOnlineColor = false;
diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp
index e2ff0fd44..d6a44f4e1 100644
--- a/Telegram/SourceFiles/settings/settings_information.cpp
+++ b/Telegram/SourceFiles/settings/settings_information.cpp
@@ -644,7 +644,7 @@ void SetupAccountsWrap(
 		}
 
 		Ui::RpWidget userpic;
-		std::shared_ptr<Data::CloudImageView> view;
+		Ui::PeerUserpicView view;
 		base::unique_qptr<Ui::PopupMenu> menu;
 	};
 	const auto state = raw->lifetime().make_state<State>(raw);
diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
index f4d13a65d..3677a620f 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp
@@ -360,7 +360,7 @@ RoundImageCheckbox::RoundImageCheckbox(
 	const style::RoundImageCheckbox &st,
 	Fn<void()> updateCallback,
 	PaintRoundImage &&paintRoundImage,
-	Fn<ImageRoundRadius()> roundingRadius)
+	Fn<std::optional<int>(int size)> roundingRadius)
 : _st(st)
 , _updateCallback(updateCallback)
 , _paintRoundImage(std::move(paintRoundImage))
@@ -390,8 +390,8 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 
 	if (selectionLevel > 0) {
 		const auto radius = _roundingRadius
-			? _roundingRadius()
-			: ImageRoundRadius::Ellipse;
+			? _roundingRadius(_st.imageRadius)
+			: std::optional<int>();
 		PainterHighQualityEnabler hq(p);
 		p.setOpacity(std::clamp(selectionLevel, 0., 1.));
 		p.setBrush(Qt::NoBrush);
@@ -405,11 +405,10 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
 			_st.imageRadius * 2,
 			_st.imageRadius * 2,
 			outerWidth);
-		if (radius == ImageRoundRadius::Ellipse) {
+		if (!radius) {
 			p.drawEllipse(rect);
 		} else {
-			const auto pxRadius = st::roundRadiusLarge;
-			p.drawRoundedRect(rect, pxRadius, pxRadius);
+			p.drawRoundedRect(rect, *radius, *radius);
 		}
 		p.setOpacity(1.);
 	}
diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.h b/Telegram/SourceFiles/ui/effects/round_checkbox.h
index b7a788389..461244f95 100644
--- a/Telegram/SourceFiles/ui/effects/round_checkbox.h
+++ b/Telegram/SourceFiles/ui/effects/round_checkbox.h
@@ -52,7 +52,7 @@ public:
 		const style::RoundImageCheckbox &st,
 		Fn<void()> updateCallback,
 		PaintRoundImage &&paintRoundImage,
-		Fn<ImageRoundRadius()> roundingRadius = nullptr);
+		Fn<std::optional<int>(int size)> roundingRadius = nullptr);
 
 	void paint(Painter &p, int x, int y, int outerWidth) const;
 	float64 checkedAnimationRatio() const;
@@ -76,7 +76,7 @@ private:
 	const style::RoundImageCheckbox &_st;
 	Fn<void()> _updateCallback;
 	PaintRoundImage _paintRoundImage;
-	Fn<ImageRoundRadius()> _roundingRadius;
+	Fn<std::optional<int>(int size)> _roundingRadius;
 
 	QPixmap _wideCache;
 	Ui::Animations::Simple _selection;
diff --git a/Telegram/SourceFiles/ui/empty_userpic.cpp b/Telegram/SourceFiles/ui/empty_userpic.cpp
index 143f11e7b..594fe6707 100644
--- a/Telegram/SourceFiles/ui/empty_userpic.cpp
+++ b/Telegram/SourceFiles/ui/empty_userpic.cpp
@@ -193,7 +193,7 @@ void PaintInaccessibleAccountInner(
 	}
 }
 
-[[nodiscard]] QPixmap Generate(int size, Fn<void(QPainter&)> callback) {
+[[nodiscard]] QImage Generate(int size, Fn<void(QPainter&)> callback) {
 	auto result = QImage(
 		QSize(size, size) * style::DevicePixelRatio(),
 		QImage::Format_ARGB32_Premultiplied);
@@ -203,7 +203,7 @@ void PaintInaccessibleAccountInner(
 		Painter p(&result);
 		callback(p);
 	}
-	return Ui::PixmapFromImage(std::move(result));
+	return result;
 }
 
 } // namespace
@@ -286,7 +286,7 @@ void EmptyUserpic::paint(
 	}
 }
 
-void EmptyUserpic::paint(
+void EmptyUserpic::paintCircle(
 		QPainter &p,
 		int x,
 		int y,
@@ -304,9 +304,6 @@ void EmptyUserpic::paintRounded(
 		int outerWidth,
 		int size,
 		int radius) const {
-	if (!radius) {
-		radius = st::roundRadiusSmall;
-	}
 	paint(p, x, y, outerWidth, size, [&] {
 		p.drawRoundedRect(x, y, size, size, radius, radius);
 	});
@@ -338,21 +335,6 @@ void EmptyUserpic::PaintSavedMessages(
 	PaintSavedMessages(p, x, y, outerWidth, size, QBrush(bg), fg);
 }
 
-void EmptyUserpic::PaintSavedMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size) {
-	auto bg = QLinearGradient(x, y, x, y + size);
-	bg.setStops({
-		{ 0., st::historyPeerSavedMessagesBg->c },
-		{ 1., st::historyPeerSavedMessagesBg2->c }
-	});
-	const auto &fg = st::historyPeerUserpicFg;
-	PaintSavedMessagesRounded(p, x, y, outerWidth, size, QBrush(bg), fg);
-}
-
 void EmptyUserpic::PaintSavedMessages(
 		QPainter &p,
 		int x,
@@ -371,42 +353,12 @@ void EmptyUserpic::PaintSavedMessages(
 	PaintSavedMessagesInner(p, x, y, size, fg);
 }
 
-void EmptyUserpic::PaintSavedMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size,
-		QBrush bg,
-		const style::color &fg) {
-	x = style::RightToLeft() ? (outerWidth - x - size) : x;
-
-	PainterHighQualityEnabler hq(p);
-	p.setBrush(std::move(bg));
-	p.setPen(Qt::NoPen);
-	p.drawRoundedRect(
-		x,
-		y,
-		size,
-		size,
-		st::roundRadiusSmall,
-		st::roundRadiusSmall);
-
-	PaintSavedMessagesInner(p, x, y, size, fg);
-}
-
-QPixmap EmptyUserpic::GenerateSavedMessages(int size) {
+QImage EmptyUserpic::GenerateSavedMessages(int size) {
 	return Generate(size, [&](QPainter &p) {
 		PaintSavedMessages(p, 0, 0, size, size);
 	});
 }
 
-QPixmap EmptyUserpic::GenerateSavedMessagesRounded(int size) {
-	return Generate(size, [&](QPainter &p) {
-		PaintSavedMessagesRounded(p, 0, 0, size, size);
-	});
-}
-
 void EmptyUserpic::PaintRepliesMessages(
 		QPainter &p,
 		int x,
@@ -422,21 +374,6 @@ void EmptyUserpic::PaintRepliesMessages(
 	PaintRepliesMessages(p, x, y, outerWidth, size, QBrush(bg), fg);
 }
 
-void EmptyUserpic::PaintRepliesMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size) {
-	auto bg = QLinearGradient(x, y, x, y + size);
-	bg.setStops({
-		{ 0., st::historyPeerSavedMessagesBg->c },
-		{ 1., st::historyPeerSavedMessagesBg2->c }
-	});
-	const auto &fg = st::historyPeerUserpicFg;
-	PaintRepliesMessagesRounded(p, x, y, outerWidth, size, QBrush(bg), fg);
-}
-
 void EmptyUserpic::PaintRepliesMessages(
 		QPainter &p,
 		int x,
@@ -455,42 +392,12 @@ void EmptyUserpic::PaintRepliesMessages(
 	PaintRepliesMessagesInner(p, x, y, size, fg);
 }
 
-void EmptyUserpic::PaintRepliesMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size,
-		QBrush bg,
-		const style::color &fg) {
-	x = style::RightToLeft() ? (outerWidth - x - size) : x;
-
-	PainterHighQualityEnabler hq(p);
-	p.setBrush(bg);
-	p.setPen(Qt::NoPen);
-	p.drawRoundedRect(
-		x,
-		y,
-		size,
-		size,
-		st::roundRadiusSmall,
-		st::roundRadiusSmall);
-
-	PaintRepliesMessagesInner(p, x, y, size, fg);
-}
-
-QPixmap EmptyUserpic::GenerateRepliesMessages(int size) {
+QImage EmptyUserpic::GenerateRepliesMessages(int size) {
 	return Generate(size, [&](QPainter &p) {
 		PaintRepliesMessages(p, 0, 0, size, size);
 	});
 }
 
-QPixmap EmptyUserpic::GenerateRepliesMessagesRounded(int size) {
-	return Generate(size, [&](QPainter &p) {
-		PaintRepliesMessagesRounded(p, 0, 0, size, size);
-	});
-}
-
 std::pair<uint64, uint64> EmptyUserpic::uniqueKey() const {
 	const auto first = (uint64(0xFFFFFFFFU) << 32)
 		| anim::getPremultiplied(_colors.color1->c);
@@ -510,7 +417,7 @@ QPixmap EmptyUserpic::generate(int size) {
 	result.fill(Qt::transparent);
 	{
 		auto p = QPainter(&result);
-		paint(p, 0, 0, size, size);
+		paintCircle(p, 0, 0, size, size);
 	}
 	return Ui::PixmapFromImage(std::move(result));
 }
diff --git a/Telegram/SourceFiles/ui/empty_userpic.h b/Telegram/SourceFiles/ui/empty_userpic.h
index 01539329c..2f9b9856b 100644
--- a/Telegram/SourceFiles/ui/empty_userpic.h
+++ b/Telegram/SourceFiles/ui/empty_userpic.h
@@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #pragma once
 
+#include "base/weak_ptr.h"
+
 namespace Ui {
 
-class EmptyUserpic {
+class EmptyUserpic final : public base::has_weak_ptr {
 public:
 	struct BgColors {
 		const style::color color1;
@@ -24,7 +26,7 @@ public:
 
 	EmptyUserpic(const BgColors &colors, const QString &name);
 
-	void paint(
+	void paintCircle(
 		QPainter &p,
 		int x,
 		int y,
@@ -52,12 +54,6 @@ public:
 		int y,
 		int outerWidth,
 		int size);
-	static void PaintSavedMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size);
 	static void PaintSavedMessages(
 		QPainter &p,
 		int x,
@@ -66,16 +62,7 @@ public:
 		int size,
 		QBrush bg,
 		const style::color &fg);
-	static void PaintSavedMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size,
-		QBrush bg,
-		const style::color &fg);
-	[[nodiscard]] static QPixmap GenerateSavedMessages(int size);
-	[[nodiscard]] static QPixmap GenerateSavedMessagesRounded(int size);
+	[[nodiscard]] static QImage GenerateSavedMessages(int size);
 
 	static void PaintRepliesMessages(
 		QPainter &p,
@@ -83,12 +70,6 @@ public:
 		int y,
 		int outerWidth,
 		int size);
-	static void PaintRepliesMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size);
 	static void PaintRepliesMessages(
 		QPainter &p,
 		int x,
@@ -97,16 +78,7 @@ public:
 		int size,
 		QBrush bg,
 		const style::color &fg);
-	static void PaintRepliesMessagesRounded(
-		QPainter &p,
-		int x,
-		int y,
-		int outerWidth,
-		int size,
-		QBrush bg,
-		const style::color &fg);
-	[[nodiscard]] static QPixmap GenerateRepliesMessages(int size);
-	[[nodiscard]] static QPixmap GenerateRepliesMessagesRounded(int size);
+	[[nodiscard]] static QImage GenerateRepliesMessages(int size);
 
 	~EmptyUserpic();
 
diff --git a/Telegram/SourceFiles/ui/special_buttons.cpp b/Telegram/SourceFiles/ui/special_buttons.cpp
index 6b199630d..610c2ba27 100644
--- a/Telegram/SourceFiles/ui/special_buttons.cpp
+++ b/Telegram/SourceFiles/ui/special_buttons.cpp
@@ -354,7 +354,7 @@ void UserpicButton::setupPeerViewers() {
 	) | rpl::filter([=] {
 		return _waiting;
 	}) | rpl::start_with_next([=] {
-		if (!_userpicView || _userpicView->image()) {
+		if (!Ui::PeerUserpicLoading(_userpicView)) {
 			_waiting = false;
 			startNewPhotoShowing();
 		}
@@ -473,12 +473,17 @@ void UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) {
 			: false;
 		auto request = Media::Streaming::FrameRequest();
 		auto size = QSize{ _st.photoSize, _st.photoSize };
-		request.outer = size * cIntRetinaFactor();
-		request.resize = size * cIntRetinaFactor();
+		const auto ratio = style::DevicePixelRatio();
+		request.outer = request.resize = size * ratio;
 		const auto forum = _peer && _peer->isForum();
 		if (forum) {
-			request.rounding = Images::CornersMaskRef(
-				Images::CornersMask(ImageRoundRadius::Large));
+			const auto radius = int(_st.photoSize
+				* Ui::ForumUserpicRadiusMultiplier()
+				* ratio);
+			if (_roundingCorners[0].width() != radius) {
+				_roundingCorners = Images::CornersMask(radius);
+			}
+			request.rounding = Images::CornersMaskRef(_roundingCorners);
 		} else {
 			if (_ellipseMask.size() != request.outer) {
 				_ellipseMask = Images::EllipseMask(size);
@@ -520,7 +525,7 @@ void UserpicButton::processPeerPhoto() {
 	Expects(_peer != nullptr);
 
 	_userpicView = _peer->createUserpicView();
-	_waiting = _userpicView && !_userpicView->image();
+	_waiting = Ui::PeerUserpicLoading(_userpicView);
 	if (_waiting) {
 		_peer->loadUserpic();
 	}
@@ -798,7 +803,7 @@ void UserpicButton::fillShape(QPainter &p, const style::color &color) const {
 	p.setBrush(color);
 	const auto size = _st.photoSize;
 	if (_peer && _peer->isForum()) {
-		const auto radius = st::roundRadiusLarge;
+		const auto radius = size * Ui::ForumUserpicRadiusMultiplier();
 		p.drawRoundedRect(0, 0, size, size, radius, radius);
 	} else {
 		p.drawEllipse(0, 0, size, size);
@@ -811,8 +816,8 @@ void UserpicButton::prepareUserpicPixmap() {
 	}
 	auto size = _st.photoSize;
 	_userpicHasImage = _peer
-		? (_peer->currentUserpic(_userpicView) || _role != Role::ChangePhoto)
-		: false;
+		&& (_peer->userpicCloudImage(_userpicView)
+			|| _role != Role::ChangePhoto);
 	_userpic = CreateSquarePixmap(size, [&](Painter &p) {
 		if (_userpicHasImage) {
 			_peer->paintUserpic(p, _userpicView, 0, 0, _st.photoSize);
diff --git a/Telegram/SourceFiles/ui/special_buttons.h b/Telegram/SourceFiles/ui/special_buttons.h
index a103f8ba2..4a9d95377 100644
--- a/Telegram/SourceFiles/ui/special_buttons.h
+++ b/Telegram/SourceFiles/ui/special_buttons.h
@@ -11,15 +11,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/widgets/tooltip.h"
 #include "ui/effects/animations.h"
 #include "ui/effects/cross_line.h"
+#include "ui/userpic_view.h"
 #include "styles/style_window.h"
 #include "styles/style_widgets.h"
 
 class PeerData;
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
-
 namespace Window {
 class Controller;
 class SessionController;
@@ -159,7 +156,7 @@ private:
 	::Window::SessionController *_controller = nullptr;
 	::Window::Controller *_window = nullptr;
 	PeerData *_peer = nullptr;
-	std::shared_ptr<Data::CloudImageView> _userpicView;
+	PeerUserpicView _userpicView;
 	QString _cropTitle;
 	Role _role = Role::ChangePhoto;
 	bool _notShownYet = true;
@@ -169,28 +166,29 @@ private:
 	bool _userpicCustom = false;
 	bool _requestToUpload = false;
 	InMemoryKey _userpicUniqueKey;
-	Ui::Animations::Simple _a_appearance;
+	Animations::Simple _a_appearance;
 	QImage _result;
 	QImage _ellipseMask;
+	std::array<QImage, 4> _roundingCorners;
 	std::unique_ptr<Media::Streaming::Instance> _streamed;
 	PhotoData *_streamedPhoto = nullptr;
 
-	base::unique_qptr<Ui::PopupMenu> _menu;
+	base::unique_qptr<PopupMenu> _menu;
 
 	bool _showSavedMessagesOnSelf = false;
 	bool _canOpenPhoto = false;
 	bool _cursorInChangeOverlay = false;
 	bool _changeOverlayEnabled = false;
-	Ui::Animations::Simple _changeOverlayShown;
+	Animations::Simple _changeOverlayShown;
 
 	rpl::event_stream<QImage> _chosenImages;
 	rpl::event_stream<> _uploadPhotoRequests;
 
 };
 
-class SilentToggle
-	: public Ui::RippleButton
-	, public Ui::AbstractTooltipShower {
+class SilentToggle final
+	: public RippleButton
+	, public AbstractTooltipShower {
 public:
 	SilentToggle(QWidget *parent, not_null<ChannelData*> channel);
 
@@ -218,7 +216,7 @@ private:
 	not_null<ChannelData*> _channel;
 	bool _checked = false;
 
-	Ui::Animations::Simple _crossLineAnimation;
+	Animations::Simple _crossLineAnimation;
 
 };
 
diff --git a/Telegram/SourceFiles/ui/userpic_view.cpp b/Telegram/SourceFiles/ui/userpic_view.cpp
new file mode 100644
index 000000000..0e4ef3d13
--- /dev/null
+++ b/Telegram/SourceFiles/ui/userpic_view.cpp
@@ -0,0 +1,80 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "ui/userpic_view.h"
+
+#include "ui/empty_userpic.h"
+#include "ui/image/image_prepare.h"
+
+namespace Ui {
+
+float64 ForumUserpicRadiusMultiplier() {
+	return 0.3;
+}
+
+bool PeerUserpicLoading(const PeerUserpicView &view) {
+	return view.cloud && view.cloud->isNull();
+}
+
+void ValidateUserpicCache(
+		PeerUserpicView &view,
+		const QImage *cloud,
+		const EmptyUserpic *empty,
+		int size,
+		bool forum) {
+	Expects(cloud != nullptr || empty != nullptr);
+
+	const auto full = QSize(size, size);
+	const auto version = style::PaletteVersion();
+	const auto forumValue = forum ? 1 : 0;
+	const auto regenerate = (view.cached.size() != QSize(size, size))
+		|| (view.forum != forumValue)
+		|| (cloud && !view.empty.null())
+		|| (empty && empty != view.empty.get())
+		|| (empty && view.paletteVersion != version);
+	if (!regenerate) {
+		return;
+	}
+	view.empty = empty;
+	view.forum = forumValue;
+	view.paletteVersion = version;
+
+	if (cloud) {
+		view.cached = cloud->scaled(
+			full,
+			Qt::IgnoreAspectRatio,
+			Qt::SmoothTransformation);
+		if (forum) {
+			view.cached = Images::Round(
+				std::move(view.cached),
+				Images::CornersMask(
+					size * Ui::ForumUserpicRadiusMultiplier()));
+		} else {
+			view.cached = Images::Circle(std::move(view.cached));
+		}
+	} else {
+		if (view.cached.size() != full) {
+			view.cached = QImage(full, QImage::Format_ARGB32_Premultiplied);
+		}
+		view.cached.fill(Qt::transparent);
+
+		auto p = QPainter(&view.cached);
+		if (forum) {
+			empty->paintRounded(
+				p,
+				0,
+				0,
+				size,
+				size,
+				size * Ui::ForumUserpicRadiusMultiplier());
+		} else {
+			empty->paintCircle(p, 0, 0, size, size);
+		}
+	}
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/userpic_view.h b/Telegram/SourceFiles/ui/userpic_view.h
new file mode 100644
index 000000000..e05162f95
--- /dev/null
+++ b/Telegram/SourceFiles/ui/userpic_view.h
@@ -0,0 +1,41 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/weak_ptr.h"
+
+#include <QtGui/QImage>
+
+namespace Ui {
+
+class EmptyUserpic;
+
+[[nodiscard]] float64 ForumUserpicRadiusMultiplier();
+
+struct PeerUserpicView {
+	[[nodiscard]] bool null() const {
+		return cached.isNull() && !cloud && empty.null();
+	}
+
+	QImage cached;
+	std::shared_ptr<QImage> cloud;
+	base::weak_ptr<const EmptyUserpic> empty;
+	int paletteVersion : 31 = 0;
+	int forum : 1 = 0;
+};
+
+[[nodiscard]] bool PeerUserpicLoading(const PeerUserpicView &view);
+
+void ValidateUserpicCache(
+	PeerUserpicView &view,
+	const QImage *cloud,
+	const EmptyUserpic *empty,
+	int size,
+	bool forum);
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/window/notifications_manager.h b/Telegram/SourceFiles/window/notifications_manager.h
index 57401671c..c33048fcd 100644
--- a/Telegram/SourceFiles/window/notifications_manager.h
+++ b/Telegram/SourceFiles/window/notifications_manager.h
@@ -15,13 +15,16 @@ class History;
 
 namespace Data {
 class Session;
-class CloudImageView;
 class ForumTopic;
 class Thread;
 struct ItemNotification;
 enum class ItemNotificationType;
 } // namespace Data
 
+namespace Ui {
+struct PeerUserpicView;
+} // namespace Ui
+
 namespace Main {
 class Session;
 } // namespace Main
@@ -392,7 +395,7 @@ protected:
 	virtual void doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
@@ -413,7 +416,7 @@ protected:
 	void doShowNativeNotification(
 		not_null<PeerData*> peer,
 		MsgId topicRootId,
-		std::shared_ptr<Data::CloudImageView> &userpicView,
+		Ui::PeerUserpicView &userpicView,
 		MsgId msgId,
 		const QString &title,
 		const QString &subtitle,
diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp
index 67f03b7c3..3f78b9b7a 100644
--- a/Telegram/SourceFiles/window/notifications_manager_default.cpp
+++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp
@@ -660,7 +660,7 @@ Notification::Notification(
 	auto position = computePosition(st::notifyMinHeight);
 	updateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyMinHeight);
 
-	_userpicLoaded = !_userpicView || (_userpicView->image() != nullptr);
+	_userpicLoaded = !Ui::PeerUserpicLoading(_userpicView);
 	updateNotifyDisplay();
 
 	_hideTimer.setSingleShot(true);
@@ -1004,7 +1004,7 @@ void Notification::updatePeerPhoto() {
 		return;
 	}
 	_userpicView = _peer->createUserpicView();
-	if (_userpicView && !_userpicView->image()) {
+	if (Ui::PeerUserpicLoading(_userpicView)) {
 		return;
 	}
 	_userpicLoaded = true;
@@ -1024,7 +1024,7 @@ void Notification::updatePeerPhoto() {
 		st::notifyPhotoPos.y(),
 		width(),
 		st::notifyPhotoSize);
-	_userpicView = nullptr;
+	_userpicView = {};
 	update();
 }
 
diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h
index 773694f81..ac8b19bda 100644
--- a/Telegram/SourceFiles/window/notifications_manager_default.h
+++ b/Telegram/SourceFiles/window/notifications_manager_default.h
@@ -11,16 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/effects/animations.h"
 #include "ui/text/text.h"
 #include "ui/rp_widget.h"
+#include "ui/userpic_view.h"
 #include "base/timer.h"
 #include "base/binary_guard.h"
 #include "base/object_ptr.h"
 
 #include <QtCore/QTimer>
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
-
 namespace Ui {
 class IconButton;
 class RoundButton;
@@ -290,7 +287,7 @@ private:
 	History *_history = nullptr;
 	Data::ForumTopic *_topic = nullptr;
 	MsgId _topicRootId = 0;
-	std::shared_ptr<Data::CloudImageView> _userpicView;
+	Ui::PeerUserpicView _userpicView;
 	QString _author;
 	Data::ReactionId _reaction;
 	HistoryItem *_item = nullptr;
diff --git a/Telegram/SourceFiles/window/notifications_utilities.cpp b/Telegram/SourceFiles/window/notifications_utilities.cpp
index 8167f2d4b..418848c05 100644
--- a/Telegram/SourceFiles/window/notifications_utilities.cpp
+++ b/Telegram/SourceFiles/window/notifications_utilities.cpp
@@ -15,8 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/empty_userpic.h"
 #include "styles/style_window.h"
 
-namespace Window {
-namespace Notifications {
+namespace Window::Notifications {
 namespace {
 
 // Delete notify photo file after 1 minute of not using.
@@ -24,9 +23,16 @@ constexpr int kNotifyDeletePhotoAfterMs = 60000;
 
 } // namespace
 
-CachedUserpics::CachedUserpics(Type type)
-: _type(type)
-, _clearTimer([=] { clear(); }) {
+QImage GenerateUserpic(not_null<PeerData*> peer, Ui::PeerUserpicView &view) {
+	return peer->isSelf()
+		? Ui::EmptyUserpic::GenerateSavedMessages(st::notifyMacPhotoSize)
+		: peer->isRepliesChat()
+		? Ui::EmptyUserpic::GenerateRepliesMessages(st::notifyMacPhotoSize)
+		: peer->generateUserpicImage(view, st::notifyMacPhotoSize);
+}
+
+CachedUserpics::CachedUserpics()
+: _clearTimer([=] { clear(); }) {
 	QDir().mkpath(cWorkingDir() + u"tdata/temp"_q);
 }
 
@@ -37,14 +43,14 @@ CachedUserpics::~CachedUserpics() {
 		}
 
 		// This works about 1200ms on Windows for a folder with one image O_o
-		//		base::Platform::DeleteDirectory(cWorkingDir() + u"tdata/temp"_q);
+		//base::Platform::DeleteDirectory(cWorkingDir() + u"tdata/temp"_q);
 	}
 }
 
 QString CachedUserpics::get(
 		const InMemoryKey &key,
 		not_null<PeerData*> peer,
-		std::shared_ptr<Data::CloudImageView> &view) {
+		Ui::PeerUserpicView &view) {
 	auto ms = crl::now();
 	auto i = _images.find(key);
 	if (i != _images.cend()) {
@@ -64,21 +70,7 @@ QString CachedUserpics::get(
 			cWorkingDir(),
 			QString::number(base::RandomValue<uint64>(), 16));
 		if (key.first || key.second) {
-			if (peer->isSelf()) {
-				const auto method = (_type == Type::Rounded)
-					? Ui::EmptyUserpic::GenerateSavedMessagesRounded
-					: Ui::EmptyUserpic::GenerateSavedMessages;
-				method(st::notifyMacPhotoSize).save(v.path, "PNG");
-			} else if (peer->isRepliesChat()) {
-				const auto method = (_type == Type::Rounded)
-					? Ui::EmptyUserpic::GenerateRepliesMessagesRounded
-					: Ui::EmptyUserpic::GenerateRepliesMessages;
-				method(st::notifyMacPhotoSize).save(v.path, "PNG");
-			} else if (_type == Type::Rounded) {
-				peer->saveUserpicRounded(view, v.path, st::notifyMacPhotoSize);
-			} else {
-				peer->saveUserpic(view, v.path, st::notifyMacPhotoSize);
-			}
+			GenerateUserpic(peer, view).save(v.path, "PNG");
 		} else {
 			LogoNoMargin().save(v.path, "PNG");
 		}
@@ -128,5 +120,4 @@ void CachedUserpics::clear() {
 	}
 }
 
-} // namespace Notifications
-} // namespace Window
+} // namespace Window::Notifications
diff --git a/Telegram/SourceFiles/window/notifications_utilities.h b/Telegram/SourceFiles/window/notifications_utilities.h
index 321504939..81b45f036 100644
--- a/Telegram/SourceFiles/window/notifications_utilities.h
+++ b/Telegram/SourceFiles/window/notifications_utilities.h
@@ -10,34 +10,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "window/notifications_manager.h"
 #include "base/timer.h"
 
-namespace Data {
-class CloudImageView;
-} // namespace Data
+namespace Ui {
+struct PeerUserpicView;
+} // namespace Ui
 
-namespace Window {
-namespace Notifications {
+namespace Window::Notifications {
+
+[[nodiscard]] QImage GenerateUserpic(
+	not_null<PeerData*> peer,
+	Ui::PeerUserpicView &view);
 
 class CachedUserpics : public QObject {
 public:
-	enum class Type {
-		Rounded,
-		Circled,
-	};
-
-	CachedUserpics(Type type);
+	CachedUserpics();
 	~CachedUserpics();
 
 	[[nodiscard]] QString get(
 		const InMemoryKey &key,
 		not_null<PeerData*> peer,
-		std::shared_ptr<Data::CloudImageView> &view);
+		Ui::PeerUserpicView &view);
 
 private:
 	void clear();
 	void clearInMs(int ms);
 	crl::time clear(crl::time ms);
 
-	Type _type = Type::Rounded;
 	struct Image {
 		crl::time until = 0;
 		QString path;
@@ -49,5 +46,4 @@ private:
 
 };
 
-} // namesapce Notifications
-} // namespace Window
+} // namespace Window::Notifications
diff --git a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp
index c256c1bfc..f30c88d8f 100644
--- a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp
+++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp
@@ -982,7 +982,7 @@ void Generator::paintUserpic(int x, int y, Row::Type type, int index, QString le
 	image.fill(Qt::transparent);
 	{
 		Painter p(&image);
-		userpic.paintRounded(p, 0, 0, size, size, size / 2);
+		userpic.paintCircle(p, 0, 0, size, size);
 	}
 	_p->drawImage(rtl() ? (_rect.width() - x - size) : x, y, image);
 }
diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake
index 1d3717bf4..113f7755e 100644
--- a/Telegram/cmake/td_ui.cmake
+++ b/Telegram/cmake/td_ui.cmake
@@ -278,6 +278,8 @@ PRIVATE
     ui/empty_userpic.h
     ui/grouped_layout.cpp
     ui/grouped_layout.h
+    ui/userpic_view.cpp
+    ui/userpic_view.h
     ui/widgets/fields/special_fields.cpp
     ui/widgets/fields/special_fields.h
     ui/widgets/fields/time_part_input_with_placeholder.cpp
diff --git a/Telegram/lib_base b/Telegram/lib_base
index e8294bbc9..fd3531b70 160000
--- a/Telegram/lib_base
+++ b/Telegram/lib_base
@@ -1 +1 @@
-Subproject commit e8294bbc9c7f85e6909c3ed06fe03cfd39869613
+Subproject commit fd3531b70750ffe7ec2213a899409c08c388ce4c
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 22ceaae4e..59a7b94ef 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 22ceaae4ed958d9711965fbe4c1385a7d8c60212
+Subproject commit 59a7b94ef4bb4d4cf6597d53bdacccbf5720e694