diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp
index c83af953d..072dc34fb 100644
--- a/Telegram/SourceFiles/boxes/background_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp
@@ -175,7 +175,8 @@ BackgroundPreviewBox::BackgroundPreviewBox(
 , _controller(controller)
 , _forPeer(args.forPeer)
 , _fromMessageId(args.fromMessageId)
-, _chatStyle(std::make_unique<Ui::ChatStyle>())
+, _chatStyle(std::make_unique<Ui::ChatStyle>(
+	controller->session().colorIndicesValue()))
 , _serviceHistory(_controller->session().data().history(
 	PeerData::kServiceNotificationsId))
 , _service(nullptr)
diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
index 952aa0c49..2bbb1b4a7 100644
--- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
+++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
@@ -133,7 +133,8 @@ void AddMessage(
 	state->delegate = std::make_unique<Delegate>(
 		controller,
 		crl::guard(widget, [=] { widget->update(); }));
-	state->style = std::make_unique<Ui::ChatStyle>();
+	state->style = std::make_unique<Ui::ChatStyle>(
+		controller->session().colorIndicesValue());
 	state->style->apply(controller->defaultChatTheme().get());
 	state->icons.lifetimes = std::vector<rpl::lifetime>(2);
 
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index 780b6c571..4c03f5d4d 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -889,7 +889,7 @@ bool PeerData::changeColorIndex(uint8 index) {
 	if (_colorIndexCloud && _colorIndex == index) {
 		return false;
 	}
-	_colorIndexCloud = true;
+	_colorIndexCloud = 1;
 	_colorIndex = index;
 	return true;
 }
@@ -898,7 +898,7 @@ bool PeerData::clearColorIndex() {
 	if (!_colorIndexCloud) {
 		return false;
 	}
-	_colorIndexCloud = false;
+	_colorIndexCloud = 0;
 	_colorIndex = Data::DecideColorIndex(id);
 	return true;
 }
diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp
index 1528269e7..5cf6ecb4c 100644
--- a/Telegram/SourceFiles/history/history_item_components.cpp
+++ b/Telegram/SourceFiles/history/history_item_components.cpp
@@ -848,19 +848,20 @@ void HistoryMessageReply::paint(
 		? (resolvedMessage->hiddenSenderInfo()->colorIndex + 1)
 		: 0;
 	const auto useColorIndex = colorIndexPlusOne && !context.outbg;
-	const auto twoColored = colorIndexPlusOne
-		&& Ui::ColorIndexTwoColored(colorIndexPlusOne - 1);
+	const auto colorPattern = colorIndexPlusOne
+		? st->colorPatternIndex(colorIndexPlusOne - 1)
+		: 0;
 	const auto cache = !inBubble
 		? (hasQuote
-			? st->serviceQuoteCache(twoColored)
-			: st->serviceReplyCache(twoColored)).get()
+			? st->serviceQuoteCache(colorPattern)
+			: st->serviceReplyCache(colorPattern)).get()
 		: useColorIndex
 		? (hasQuote
 			? st->coloredQuoteCache(selected, colorIndexPlusOne - 1)
 			: st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get()
 		: (hasQuote
-			? (twoColored ? stm->quoteCacheTwo : stm->quoteCache)
-			: (twoColored ? stm->replyCacheTwo : stm->replyCache)).get();
+			? stm->quoteCache[colorPattern]
+			: stm->replyCache[colorPattern]).get();
 	const auto &quoteSt = hasQuote
 		? st::messageTextStyle.blockquote
 		: st::messageQuoteStyle;
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
index 8d0107157..0b2fad3d7 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp
@@ -196,7 +196,8 @@ PreviewWrap::PreviewWrap(
 , _box(box)
 , _history(history)
 , _theme(DefaultThemeOn(lifetime()))
-, _style(std::make_unique<Ui::ChatStyle>())
+, _style(std::make_unique<Ui::ChatStyle>(
+	history->session().colorIndicesValue()))
 , _delegate(std::make_unique<PreviewDelegate>(
 	box,
 	_style.get(),
diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp
index afffc0b2b..3d9142761 100644
--- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp
@@ -232,13 +232,12 @@ void ViewButton::draw(
 		Painter &p,
 		const QRect &r,
 		const Ui::ChatPaintContext &context) {
+	const auto st = context.st;
 	const auto stm = context.messageStyle();
-
 	const auto selected = context.selected();
-	const auto twoColored = Ui::ColorIndexTwoColored(_inner->colorIndex);
 	const auto cache = context.outbg
-		? (twoColored ? stm->replyCacheTwo : stm->replyCache).get()
-		: context.st->coloredReplyCache(selected, _inner->colorIndex).get();
+		? stm->replyCache[st->colorPatternIndex(_inner->colorIndex)].get()
+		: st->coloredReplyCache(selected, _inner->colorIndex).get();
 	const auto radius = st::historyPagePreview.radius;
 
 	if (_inner->ripple && !_inner->ripple->empty()) {
diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.cpp b/Telegram/SourceFiles/history/view/media/history_view_game.cpp
index 9f09e28db..a91f8350d 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_game.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_game.cpp
@@ -220,9 +220,8 @@ void Game::draw(Painter &p, const PaintContext &context) const {
 
 	const auto colorIndex = parent()->colorIndex();
 	const auto selected = context.selected();
-	const auto twoColored = Ui::ColorIndexTwoColored(colorIndex);
 	const auto cache = context.outbg
-		? (twoColored ? stm->replyCacheTwo : stm->replyCache).get()
+		? stm->replyCache[st->colorPatternIndex(colorIndex)].get()
 		: st->coloredReplyCache(selected, colorIndex).get();
 	Ui::Text::ValidateQuotePaintCache(*cache, _st);
 	Ui::Text::FillQuotePaint(p, outer, *cache, _st);
diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp
index 9079ac6e0..311e9bd88 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp
@@ -344,13 +344,13 @@ void Giveaway::paintChannels(
 
 	const auto size = _channels[0].geometry.height();
 	const auto ratio = style::DevicePixelRatio();
+	const auto st = context.st;
 	const auto stm = context.messageStyle();
 	const auto selected = context.selected();
 	const auto colorIndex = parent()->colorIndex();
-	const auto twoColored = Ui::ColorIndexTwoColored(colorIndex);
 	const auto cache = context.outbg
-		? (twoColored ? stm->replyCacheTwo : stm->replyCache).get()
-		: context.st->coloredReplyCache(selected, colorIndex).get();
+		? stm->replyCache[st->colorPatternIndex(colorIndex)].get()
+		: st->coloredReplyCache(selected, colorIndex).get();
 	if (_channelCorners[0].isNull() || _channelBg != cache->bg) {
 		_channelBg = cache->bg;
 		_channelCorners = Images::CornersMask(size / 2);
diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp
index 7e9ff2b35..f91e7ad55 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp
@@ -523,9 +523,8 @@ void WebPage::draw(Painter &p, const PaintContext &context) const {
 
 	const auto selected = context.selected();
 	const auto colorIndex = parent()->colorIndex();
-	const auto twoColored = Ui::ColorIndexTwoColored(colorIndex);
 	const auto cache = context.outbg
-		? (twoColored ? stm->replyCacheTwo : stm->replyCache).get()
+		? stm->replyCache[st->colorPatternIndex(colorIndex)].get()
 		: st->coloredReplyCache(selected, colorIndex).get();
 	Ui::Text::ValidateQuotePaintCache(*cache, _st);
 	Ui::Text::FillQuotePaint(p, outer, *cache, _st);
diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp
index b2556600b..3b4b788a1 100644
--- a/Telegram/SourceFiles/main/main_app_config.cpp
+++ b/Telegram/SourceFiles/main/main_app_config.cpp
@@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 */
 #include "main/main_app_config.h"
 
-#include "main/main_account.h"
-#include "base/call_delayed.h"
 #include "apiwrap.h"
+#include "base/call_delayed.h"
+#include "main/main_account.h"
+#include "ui/chat/chat_style.h"
 
 namespace Main {
 namespace {
@@ -27,6 +28,8 @@ AppConfig::AppConfig(not_null<Account*> account) : _account(account) {
 	}, _lifetime);
 }
 
+AppConfig::~AppConfig() = default;
+
 void AppConfig::start() {
 	_account->mtpMainSessionValue(
 	) | rpl::start_with_next([=](not_null<MTP::Instance*> instance) {
@@ -58,6 +61,7 @@ void AppConfig::refresh() {
 					_data.emplace_or_assign(qs(data.vkey()), data.vvalue());
 				});
 			}
+			parseColorIndices();
 			DEBUG_LOG(("getAppConfig result handled."));
 			_refreshed.fire({});
 		}, [](const MTPDhelp_appConfigNotModified &) {});
@@ -171,6 +175,27 @@ std::vector<std::map<QString, QString>> AppConfig::getStringMapArray(
 	});
 }
 
+std::vector<int> AppConfig::getIntArray(
+		const QString &key,
+		std::vector<int> &&fallback) const {
+	return getValue(key, [&](const MTPJSONValue &value) {
+		return value.match([&](const MTPDjsonArray &data) {
+			auto result = std::vector<int>();
+			result.reserve(data.vvalue().v.size());
+			for (const auto &entry : data.vvalue().v) {
+				if (entry.type() != mtpc_jsonNumber) {
+					return std::move(fallback);
+				}
+				result.push_back(
+					int(base::SafeRound(entry.c_jsonNumber().vvalue().v)));
+			}
+			return result;
+		}, [&](const auto &data) {
+			return std::move(fallback);
+		});
+	});
+}
+
 bool AppConfig::suggestionCurrent(const QString &key) const {
 	return !_dismissedSuggestions.contains(key)
 		&& ranges::contains(
@@ -199,4 +224,121 @@ void AppConfig::dismissSuggestion(const QString &key) {
 	)).send();
 }
 
+void AppConfig::parseColorIndices() {
+	constexpr auto parseColor = [](const MTPJSONValue &color) {
+		if (color.type() != mtpc_jsonString) {
+			LOG(("API Error: Bad type for color element."));
+			return uint32();
+		}
+		const auto value = color.c_jsonString().vvalue().v;
+		if (value.size() != 6) {
+			LOG(("API Error: Bad length for color element: %1"
+				).arg(qs(value)));
+			return uint32();
+		}
+		const auto hex = [](char ch) {
+			return (ch >= 'a' && ch <= 'f')
+				? (ch - 'a' + 10)
+				: (ch >= 'A' && ch <= 'F')
+				? (ch - 'A' + 10)
+				: (ch >= '0' && ch <= '9')
+				? (ch - '0')
+				: 0;
+		};
+		auto result = (uint32(1) << 24);
+		for (auto i = 0; i != 6; ++i) {
+			result |= (uint32(hex(value[i])) << ((5 - i) * 4));
+		}
+		return result;
+	};
+
+	struct ParsedColor {
+		uint8 colorIndex = Ui::kColorIndexCount;
+		std::array<uint32, Ui::kColorPatternsCount> colors;
+
+		explicit operator bool() const {
+			return colorIndex < Ui::kColorIndexCount;
+		}
+	};
+	constexpr auto parseColors = [](const MTPJSONObjectValue &element) {
+		const auto &data = element.data();
+		if (data.vvalue().type() != mtpc_jsonArray) {
+			LOG(("API Error: Bad value for peer_colors element."));
+			return ParsedColor();
+		}
+		const auto &list = data.vvalue().c_jsonArray().vvalue().v;
+		if (list.empty() || list.size() > Ui::kColorPatternsCount) {
+			LOG(("API Error: Bad count for peer_colors element: %1"
+				).arg(list.size()));
+			return ParsedColor();
+		}
+		const auto index = data.vkey().v.toInt();
+		if (index < Ui::kSimpleColorIndexCount
+			|| index >= Ui::kColorIndexCount) {
+			LOG(("API Error: Bad index for peer_colors element: %1"
+				).arg(qs(data.vkey().v)));
+			return ParsedColor();
+		}
+		auto result = ParsedColor{ .colorIndex = uint8(index) };
+		auto fill = result.colors.data();
+		for (const auto &color : list) {
+			*fill++ = parseColor(color);
+		}
+		return result;
+	};
+	constexpr auto checkColorsObjectType = [](const MTPJSONValue &value) {
+		if (value.type() != mtpc_jsonObject) {
+			if (value.type() != mtpc_jsonArray
+				|| !value.c_jsonArray().vvalue().v.empty()) {
+				LOG(("API Error: Bad value for [dark_]peer_colors."));
+			}
+			return false;
+		}
+		return true;
+	};
+
+	auto colors = std::make_shared<
+		std::array<Ui::ColorIndexData, Ui::kColorIndexCount>>();
+	getValue(u"peer_colors"_q, [&](const MTPJSONValue &value) {
+		if (!checkColorsObjectType(value)) {
+			return;
+		}
+		for (const auto &element : value.c_jsonObject().vvalue().v) {
+			if (const auto parsed = parseColors(element)) {
+				auto &fields = (*colors)[parsed.colorIndex];
+				fields.dark = fields.light = parsed.colors;
+			}
+		}
+	});
+	getValue(u"dark_peer_colors"_q, [&](const MTPJSONValue &value) {
+		if (!checkColorsObjectType(value)) {
+			return;
+		}
+		for (const auto &element : value.c_jsonObject().vvalue().v) {
+			if (const auto parsed = parseColors(element)) {
+				(*colors)[parsed.colorIndex].dark = parsed.colors;
+			}
+		}
+	});
+
+	if (!_colorIndicesCurrent) {
+		_colorIndicesCurrent = std::make_unique<Ui::ColorIndicesCompressed>(
+			Ui::ColorIndicesCompressed{ std::move(colors) });
+		_colorIndicesChanged.fire({});
+	} else if (*_colorIndicesCurrent->colors != *colors) {
+		_colorIndicesCurrent->colors = std::move(colors);
+		_colorIndicesChanged.fire({});
+	}
+}
+
+auto AppConfig::colorIndicesValue() const
+-> rpl::producer<Ui::ColorIndicesCompressed> {
+	return rpl::single(_colorIndicesCurrent
+		? *_colorIndicesCurrent
+		: Ui::ColorIndicesCompressed()
+	) | rpl::then(_colorIndicesChanged.events() | rpl::map([=] {
+		return *_colorIndicesCurrent;
+	}));
+}
+
 } // namespace Main
diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h
index b0273b8de..04c00dcfe 100644
--- a/Telegram/SourceFiles/main/main_app_config.h
+++ b/Telegram/SourceFiles/main/main_app_config.h
@@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "mtproto/sender.h"
 #include "base/algorithm.h"
 
+namespace Ui {
+struct ColorIndicesCompressed;
+} // namespace Ui
+
 namespace Main {
 
 class Account;
@@ -17,6 +21,7 @@ class Account;
 class AppConfig final {
 public:
 	explicit AppConfig(not_null<Account*> account);
+	~AppConfig();
 
 	void start();
 
@@ -30,6 +35,8 @@ public:
 			return getString(key, fallback);
 		} else if constexpr (std::is_same_v<Type, std::vector<QString>>) {
 			return getStringArray(key, std::move(fallback));
+		} else if constexpr (std::is_same_v<Type, std::vector<int>>) {
+			return getIntArray(key, std::move(fallback));
 		} else if constexpr (std::is_same_v<
 				Type,
 				std::vector<std::map<QString, QString>>>) {
@@ -47,10 +54,14 @@ public:
 		const QString &key) const;
 	void dismissSuggestion(const QString &key);
 
+	[[nodiscard]] auto colorIndicesValue() const
+		-> rpl::producer<Ui::ColorIndicesCompressed>;
+
 	void refresh();
 
 private:
 	void refreshDelayed();
+	void parseColorIndices();
 
 	template <typename Extractor>
 	[[nodiscard]] auto getValue(
@@ -72,6 +83,9 @@ private:
 	[[nodiscard]] std::vector<std::map<QString, QString>> getStringMapArray(
 		const QString &key,
 		std::vector<std::map<QString, QString>> &&fallback) const;
+	[[nodiscard]] std::vector<int> getIntArray(
+		const QString &key,
+		std::vector<int> &&fallback) const;
 
 	const not_null<Account*> _account;
 	std::optional<MTP::Sender> _api;
@@ -80,6 +94,10 @@ private:
 	base::flat_map<QString, MTPJSONValue> _data;
 	rpl::event_stream<> _refreshed;
 	base::flat_set<QString> _dismissedSuggestions;
+
+	rpl::event_stream<> _colorIndicesChanged;
+	std::unique_ptr<Ui::ColorIndicesCompressed> _colorIndicesCurrent;
+
 	rpl::lifetime _lifetime;
 
 };
diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp
index a235f729f..84f48106a 100644
--- a/Telegram/SourceFiles/main/main_session.cpp
+++ b/Telegram/SourceFiles/main/main_session.cpp
@@ -476,4 +476,9 @@ Window::SessionController *Session::tryResolveWindow() const {
 	return _windows.front();
 }
 
+auto Session::colorIndicesValue() const
+-> rpl::producer<Ui::ColorIndicesCompressed> {
+	return _account->appConfig().colorIndicesValue();
+}
+
 } // namespace Main
diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h
index 05dbb9ce5..342c7ced6 100644
--- a/Telegram/SourceFiles/main/main_session.h
+++ b/Telegram/SourceFiles/main/main_session.h
@@ -57,6 +57,10 @@ namespace InlineBots {
 class AttachWebView;
 } // namespace InlineBots
 
+namespace Ui {
+struct ColorIndicesCompressed;
+} // namespace Ui
+
 namespace Main {
 
 class Account;
@@ -187,6 +191,9 @@ public:
 	[[nodiscard]] Support::Helper &supportHelper() const;
 	[[nodiscard]] Support::Templates &supportTemplates() const;
 
+	[[nodiscard]] auto colorIndicesValue() const
+		-> rpl::producer<Ui::ColorIndicesCompressed>;
+
 private:
 	static constexpr auto kDefaultSaveDelay = crl::time(1000);
 
@@ -227,6 +234,8 @@ private:
 	QByteArray _tmpPassword;
 	TimeId _tmpPasswordValidUntil = 0;
 
+	rpl::event_stream<Ui::ColorIndicesCompressed> _colorIndicesChanges;
+
 	rpl::lifetime _lifetime;
 
 };
diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
index 2d9e017d5..fd4a0ea8a 100644
--- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
+++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp
@@ -157,7 +157,7 @@ ReactionView::ReactionView(
 	const Data::SuggestedReaction &reaction)
 : RpWidget(parent)
 , _data(reaction)
-, _chatStyle(std::make_unique<Ui::ChatStyle>())
+, _chatStyle(std::make_unique<Ui::ChatStyle>(session->colorIndicesValue()))
 , _pathGradient(
 	std::make_unique<Ui::PathShiftGradient>(
 		st::shadowFg,
diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
index f34691241..aa1e37d09 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp
@@ -839,7 +839,9 @@ ForwardsPrivacyController::ForwardsPrivacyController(
 	not_null<Window::SessionController*> controller)
 : SimpleElementDelegate(controller, [] {})
 , _controller(controller)
-, _chatStyle(std::make_unique<Ui::ChatStyle>()) {
+, _chatStyle(
+	std::make_unique<Ui::ChatStyle>(
+		controller->session().colorIndicesValue())) {
 	_chatStyle->apply(controller->defaultChatTheme().get());
 }
 
diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp
index aaf9fec54..75d42760d 100644
--- a/Telegram/SourceFiles/support/support_autocomplete.cpp
+++ b/Telegram/SourceFiles/support/support_autocomplete.cpp
@@ -506,7 +506,8 @@ ConfirmContactBox::ConfirmContactBox(
 	const Contact &data,
 	Fn<void(Qt::KeyboardModifiers)> submit)
 : SimpleElementDelegate(controller, [=] { update(); })
-, _chatStyle(std::make_unique<Ui::ChatStyle>())
+, _chatStyle(std::make_unique<Ui::ChatStyle>(
+	history->session().colorIndicesValue()))
 , _comment(GenerateCommentItem(this, history, data))
 , _contact(GenerateContactItem(this, history, data))
 , _submit(submit) {
diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style
index 7050667ae..fd5affd42 100644
--- a/Telegram/SourceFiles/ui/chat/chat.style
+++ b/Telegram/SourceFiles/ui/chat/chat.style
@@ -50,6 +50,7 @@ messageQuoteStyle: QuoteStyle(defaultQuoteStyle) {
 	padding: margins(10px, 2px, 4px, 2px);
 	verticalSkip: 4px;
 	outline: 3px;
+	outlineShift: 2px;
 	radius: 5px;
 }
 messageTextStyle: TextStyle(defaultTextStyle) {
diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp
index 1ca60e7a3..b0b23e1d3 100644
--- a/Telegram/SourceFiles/ui/chat/chat_style.cpp
+++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp
@@ -39,8 +39,7 @@ void EnsureBlockquoteCache(
 	cache = std::make_unique<Text::QuotePaintCache>();
 	const auto &colors = values();
 	cache->bg = colors.bg;
-	cache->outline1 = colors.outline1;
-	cache->outline2 = colors.outline2;
+	cache->outlines = colors.outlines;
 	cache->icon = colors.name;
 }
 
@@ -55,15 +54,15 @@ void EnsurePreCache(
 	const auto bg = bgOverride();
 	cache->bg = bg.value_or(color->c);
 	if (!bg) {
-		cache->bg.setAlphaF(0.12);
+		cache->bg.setAlpha(0.12 * 255);
 	}
-	cache->outline1 = color->c;
-	cache->outline1.setAlphaF(0.9);
-	cache->outline2 = cache->outline1;
+	cache->outlines[0] = color->c;
+	cache->outlines[0].setAlpha(0.9 * 255);
+	cache->outlines[1] = cache->outlines[2] = QColor(0, 0, 0, 0);
 	cache->header = color->c;
-	cache->header.setAlphaF(0.25);
-	cache->icon = cache->outline1;
-	cache->icon.setAlphaF(0.6);
+	cache->header.setAlpha(0.25 * 255);
+	cache->icon = cache->outlines[0];
+	cache->icon.setAlpha(0.6 * 255);
 }
 
 } // namespace
@@ -80,9 +79,8 @@ not_null<Text::QuotePaintCache*> ChatPaintContext::quoteCache(
 		uint8 colorIndex) const {
 	return !outbg
 		? st->coloredQuoteCache(selected(), colorIndex).get()
-		: ColorIndexTwoColored(colorIndex)
-		? messageStyle()->quoteCacheTwo.get()
-		: messageStyle()->quoteCache.get();
+		: messageStyle()->quoteCache[
+			st->colorPatternIndex(colorIndex)].get();
 }
 
 int HistoryServiceMsgRadius() {
@@ -110,85 +108,28 @@ int HistoryServiceMsgInvertedShrink() {
 	return result;
 }
 
-ColorIndexValues ComputeColorIndexValues(
-		not_null<const ChatStyle*> st,
-		bool selected,
-		uint8 colorIndex) {
-	if (colorIndex < kSimpleColorIndexCount) {
-		const style::color list[] = {
-			st->historyPeer1NameFg(),
-			st->historyPeer2NameFg(),
-			st->historyPeer3NameFg(),
-			st->historyPeer4NameFg(),
-			st->historyPeer5NameFg(),
-			st->historyPeer6NameFg(),
-			st->historyPeer7NameFg(),
-			st->historyPeer8NameFg(),
-		};
-		const style::color listSelected[] = {
-			st->historyPeer1NameFgSelected(),
-			st->historyPeer2NameFgSelected(),
-			st->historyPeer3NameFgSelected(),
-			st->historyPeer4NameFgSelected(),
-			st->historyPeer5NameFgSelected(),
-			st->historyPeer6NameFgSelected(),
-			st->historyPeer7NameFgSelected(),
-			st->historyPeer8NameFgSelected(),
-		};
-		const auto paletteIndex = ColorIndexToPaletteIndex(colorIndex);
-		auto result = ColorIndexValues{
-			.name = (selected ? listSelected : list)[paletteIndex]->c,
-		};
-		result.bg = result.name;
-		result.bg.setAlphaF(0.12);
-		result.outline1 = result.name;
-		result.outline1.setAlphaF(0.9);
-		result.outline2 = result.outline1;
-		return result;
-	}
-	struct Pair {
-		QColor outline1;
-		QColor outline2;
-	};
-	const Pair list[] = {
-		{ QColor(0xE1, 0x50, 0x52), QColor(0xF9, 0xAE, 0x63) }, // Red
-		{ QColor(0xE0, 0x80, 0x2B), QColor(0xFA, 0xC5, 0x34) }, // Orange
-		{ QColor(0xA0, 0x5F, 0xF3), QColor(0xF4, 0x8F, 0xFF) }, // Violet
-		{ QColor(0x27, 0xA9, 0x10), QColor(0xA7, 0xDC, 0x57) }, // Green
-		{ QColor(0x27, 0xAC, 0xCE), QColor(0x82, 0xE8, 0xD6) }, // Cyan
-		{ QColor(0x33, 0x91, 0xD4), QColor(0x7D, 0xD3, 0xF0) }, // Blue
-		{ QColor(0xD1, 0x48, 0x72), QColor(0xFF, 0xBE, 0xA0) }, // Pink
-	};
-	const auto &pair = list[colorIndex - kSimpleColorIndexCount];
-	auto bg = pair.outline1;
-	bg.setAlphaF(0.12);
-	return {
-		.name = st->dark() ? pair.outline2 : pair.outline1,
-		.bg = bg,
-		.outline1 = pair.outline1,
-		.outline2 = pair.outline2,
-	};
-}
-
-bool ColorIndexTwoColored(uint8 colorIndex) {
-	return (colorIndex >= kSimpleColorIndexCount);
-}
-
-ColorIndexValues SimpleColorIndexValues(QColor color, bool twoColored) {
+ColorIndexValues SimpleColorIndexValues(QColor color, int patternIndex) {
 	auto bg = color;
-	bg.setAlphaF(0.12);
-	auto outline1 = color;
-	outline1.setAlphaF(0.9);
-	auto outline2 = outline1;
-	if (twoColored) {
-		outline2.setAlphaF(0.5);
-	}
-	return {
+	bg.setAlpha(0.12 * 255);
+	auto result = ColorIndexValues{
 		.name = color,
 		.bg = bg,
-		.outline1 = outline1,
-		.outline2 = outline2,
 	};
+	result.outlines[0] = color;
+	result.outlines[0].setAlpha(0.9 * 255);
+	if (patternIndex > 1) {
+		result.outlines[1] = result.outlines[0];
+		result.outlines[1].setAlpha(0.3 * 255);
+		result.outlines[2] = result.outlines[0];
+		result.outlines[2].setAlpha(0.6 * 255);
+	} else if (patternIndex > 0) {
+		result.outlines[1] = result.outlines[0];
+		result.outlines[1].setAlpha(0.5 * 255);
+		result.outlines[2] = QColor(0, 0, 0, 0);
+	} else {
+		result.outlines[1] = result.outlines[2] = QColor(0, 0, 0, 0);
+	}
+	return result;
 }
 
 int BackgroundEmojiData::CacheIndex(
@@ -202,7 +143,15 @@ int BackgroundEmojiData::CacheIndex(
 	return (base * 2) + (selected ? 1 : 0);
 };
 
-ChatStyle::ChatStyle() {
+ChatStyle::ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices) {
+	if (colorIndices) {
+		_colorIndicesLifetime = std::move(
+			colorIndices
+		) | rpl::start_with_next([=](ColorIndicesCompressed &&indices) {
+			_colorIndices = std::move(indices);
+		});
+	}
+
 	finalize();
 	make(_historyPsaForwardPalette, st::historyPsaForwardPalette);
 	make(_imgReplyTextPalette, st::imgReplyTextPalette);
@@ -561,7 +510,7 @@ ChatStyle::ChatStyle() {
 }
 
 ChatStyle::ChatStyle(not_null<const style::palette*> isolated)
-: ChatStyle() {
+: ChatStyle(rpl::producer<ColorIndicesCompressed>()) {
 	assignPalette(isolated);
 }
 
@@ -631,17 +580,41 @@ std::span<Text::SpecialColor> ChatStyle::highlightColors() const {
 	return _highlightColors;
 }
 
+void ChatStyle::clearColorIndexCaches() {
+	for (auto &style : _messageStyles) {
+		for (auto &cache : style.quoteCache) {
+			cache = nullptr;
+		}
+		for (auto &cache : style.replyCache) {
+			cache = nullptr;
+		}
+	}
+	for (auto &values : _coloredValues) {
+		values.reset();
+	}
+	for (auto &palette : _coloredTextPalettes) {
+		palette.linkFg.reset();
+	}
+	for (auto &cache : _coloredReplyCaches) {
+		cache = nullptr;
+	}
+	for (auto &cache : _coloredQuoteCaches) {
+		cache = nullptr;
+	}
+}
+
 void ChatStyle::assignPalette(not_null<const style::palette*> palette) {
 	*static_cast<style::palette*>(this) = *palette;
 	style::internal::resetIcons();
+
+	clearColorIndexCaches();
 	for (auto &style : _messageStyles) {
 		style.msgBgCornersSmall = {};
 		style.msgBgCornersLarge = {};
-		style.quoteCache = nullptr;
-		style.quoteCacheTwo = nullptr;
-		style.replyCache = nullptr;
-		style.replyCacheTwo = nullptr;
 		style.preCache = nullptr;
+		style.textPalette.linkAlwaysActive
+			= style.semiboldPalette.linkAlwaysActive
+			= (style.textPalette.linkFg->c == style.historyTextFg->c);
 	}
 	for (auto &style : _imageStyles) {
 		style.msgDateImgBgCorners = {};
@@ -657,24 +630,6 @@ void ChatStyle::assignPalette(not_null<const style::palette*> palette) {
 	for (auto &corners : _msgSelectOverlayCorners) {
 		corners = {};
 	}
-
-	for (auto &stm : _messageStyles) {
-		stm.textPalette.linkAlwaysActive
-			= stm.semiboldPalette.linkAlwaysActive
-			= (stm.textPalette.linkFg->c == stm.historyTextFg->c);
-	}
-	for (auto &values : _coloredValues) {
-		values.reset();
-	}
-	for (auto &palette : _coloredTextPalettes) {
-		palette.linkFg.reset();
-	}
-	for (auto &cache : _coloredReplyCaches) {
-		cache = nullptr;
-	}
-	for (auto &cache : _coloredQuoteCaches) {
-		cache = nullptr;
-	}
 	updateDarkValue();
 
 	_paletteChanged.fire({});
@@ -710,19 +665,14 @@ const MessageStyle &ChatStyle::messageStyle(bool outbg, bool selected) const {
 		result.msgBg,
 		&result.msgShadow);
 	const auto &replyBar = result.msgReplyBarColor->c;
-	EnsureBlockquoteCache(
-		result.replyCache,
-		[&] { return SimpleColorIndexValues(replyBar, false); });
-	EnsureBlockquoteCache(
-		result.replyCacheTwo,
-		[&] { return SimpleColorIndexValues(replyBar, true); });
-	if (!result.quoteCache) {
-		result.quoteCache = std::make_unique<Text::QuotePaintCache>(
-			*result.replyCache);
-	}
-	if (!result.quoteCacheTwo) {
-		result.quoteCacheTwo = std::make_unique<Text::QuotePaintCache>(
-			*result.replyCacheTwo);
+	for (auto i = 0; i != kColorPatternsCount; ++i) {
+		EnsureBlockquoteCache(
+			result.replyCache[i],
+			[&] { return SimpleColorIndexValues(replyBar, i); });
+		if (!result.quoteCache[i]) {
+			result.quoteCache[i] = std::make_unique<Text::QuotePaintCache>(
+				*result.replyCache[i]);
+		}
 	}
 
 	const auto preBgOverride = [&] {
@@ -763,6 +713,80 @@ const MessageImageStyle &ChatStyle::imageStyle(bool selected) const {
 	return result;
 }
 
+int ChatStyle::colorPatternIndex(uint8 colorIndex) const {
+	Expects(colorIndex >= 0 && colorIndex < kColorIndexCount);
+
+	if (!_colorIndices.colors
+		|| colorIndex < kSimpleColorIndexCount) {
+		return 0;
+	}
+	auto &data = (*_colorIndices.colors)[colorIndex];
+	auto &colors = _dark ? data.dark : data.light;
+	return colors[2] ? 2 : colors[1] ? 1 : 0;
+}
+
+ColorIndexValues ChatStyle::computeColorIndexValues(
+		bool selected,
+		uint8 colorIndex) const {
+	if (!_colorIndices.colors) {
+		colorIndex %= kSimpleColorIndexCount;
+	}
+	if (colorIndex < kSimpleColorIndexCount) {
+		const auto list = std::array{
+			&historyPeer1NameFg(),
+			&historyPeer2NameFg(),
+			&historyPeer3NameFg(),
+			&historyPeer4NameFg(),
+			&historyPeer5NameFg(),
+			&historyPeer6NameFg(),
+			&historyPeer7NameFg(),
+			&historyPeer8NameFg(),
+		};
+		const auto listSelected = std::array{
+			&historyPeer1NameFgSelected(),
+			&historyPeer2NameFgSelected(),
+			&historyPeer3NameFgSelected(),
+			&historyPeer4NameFgSelected(),
+			&historyPeer5NameFgSelected(),
+			&historyPeer6NameFgSelected(),
+			&historyPeer7NameFgSelected(),
+			&historyPeer8NameFgSelected(),
+		};
+		const auto paletteIndex = ColorIndexToPaletteIndex(colorIndex);
+		auto result = ColorIndexValues{
+			.name = (*(selected ? listSelected : list)[paletteIndex])->c,
+		};
+		result.bg = result.name;
+		result.bg.setAlphaF(0.12);
+		result.outlines[0] = result.name;
+		result.outlines[0].setAlphaF(0.9);
+		result.outlines[1] = result.outlines[2] = QColor(0, 0, 0, 0);
+		return result;
+	}
+	auto &data = (*_colorIndices.colors)[colorIndex];
+	auto &colors = _dark ? data.dark : data.light;
+	if (!colors[0]) {
+		return computeColorIndexValues(
+			selected,
+			colorIndex % kSimpleColorIndexCount);
+	}
+	const auto color = [&](int index) {
+		const auto v = colors[index];
+		return v
+			? QColor((v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF)
+			: QColor(0, 0, 0, 0);
+	};
+	auto result = ColorIndexValues{
+		.outlines = { color(0), color(1), color(2) }
+	};
+	result.bg = result.outlines[0];
+	result.bg.setAlpha(0.12 * 255);
+	result.name = (_dark && colorPatternIndex(colorIndex) == 1)
+		? result.outlines[1]
+		: result.outlines[0];
+	return result;
+}
+
 not_null<Text::QuotePaintCache*> ChatStyle::serviceQuoteCache(
 		bool twoColored) const {
 	const auto index = (twoColored ? 1 : 0);
@@ -791,7 +815,7 @@ const ColorIndexValues &ChatStyle::coloredValues(
 	const auto shift = (selected ? kColorIndexCount : 0);
 	auto &result = _coloredValues[shift + colorIndex];
 	if (!result) {
-		result.emplace(ComputeColorIndexValues(this, selected, colorIndex));
+		result.emplace(computeColorIndexValues(selected, colorIndex));
 	}
 	return *result;
 }
diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h
index 8f2b7639d..c60993df6 100644
--- a/Telegram/SourceFiles/ui/chat/chat_style.h
+++ b/Telegram/SourceFiles/ui/chat/chat_style.h
@@ -31,7 +31,8 @@ class ChatTheme;
 class ChatStyle;
 struct BubblePattern;
 
-inline constexpr auto kColorIndexCount = uint8(14);
+inline constexpr auto kColorPatternsCount = Text::kMaxQuoteOutlines;
+inline constexpr auto kColorIndexCount = uint8(1 << 6);
 inline constexpr auto kSimpleColorIndexCount = uint8(7);
 
 struct MessageStyle {
@@ -83,10 +84,12 @@ struct MessageStyle {
 	style::icon historyPollChoiceRight = { Qt::Uninitialized };
 	style::icon historyTranscribeIcon = { Qt::Uninitialized };
 	style::icon historyTranscribeHide = { Qt::Uninitialized };
-	std::unique_ptr<Text::QuotePaintCache> quoteCache;
-	std::unique_ptr<Text::QuotePaintCache> quoteCacheTwo;
-	std::unique_ptr<Text::QuotePaintCache> replyCache;
-	std::unique_ptr<Text::QuotePaintCache> replyCacheTwo;
+	std::array<
+		std::unique_ptr<Text::QuotePaintCache>,
+		kColorPatternsCount> quoteCache;
+	std::array<
+		std::unique_ptr<Text::QuotePaintCache>,
+		kColorPatternsCount> replyCache;
 	std::unique_ptr<Text::QuotePaintCache> preCache;
 };
 
@@ -190,22 +193,31 @@ struct ChatPaintContext {
 [[nodiscard]] int HistoryServiceMsgInvertedRadius();
 [[nodiscard]] int HistoryServiceMsgInvertedShrink();
 
+struct ColorIndexData {
+	std::array<uint32, kColorPatternsCount> light = {};
+	std::array<uint32, kColorPatternsCount> dark = {};
+
+	friend inline bool operator==(
+		const ColorIndexData&,
+		const ColorIndexData&) = default;
+};
+
+struct ColorIndicesCompressed {
+	std::shared_ptr<std::array<ColorIndexData, kColorIndexCount>> colors;
+};
+
 struct ColorIndexValues {
+	std::array<QColor, kColorPatternsCount> outlines;
 	QColor name;
 	QColor bg;
-	QColor outline1;
-	QColor outline2;
 };
-[[nodiscard]] ColorIndexValues ComputeColorIndexValues(
-	not_null<const ChatStyle*> st,
-	bool selected,
-	uint8 colorIndex);
-[[nodiscard]] bool ColorIndexTwoColored(uint8 colorIndex);
 
 class ChatStyle final : public style::palette {
 public:
-	ChatStyle();
+	explicit ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices);
 	explicit ChatStyle(not_null<const style::palette*> isolated);
+	ChatStyle(const ChatStyle &other) = delete;
+	ChatStyle &operator=(const ChatStyle &other) = delete;
 	~ChatStyle();
 
 	void apply(not_null<ChatTheme*> theme);
@@ -246,6 +258,11 @@ public:
 		bool selected) const;
 	[[nodiscard]] const MessageImageStyle &imageStyle(bool selected) const;
 
+	[[nodiscard]] int colorPatternIndex(uint8 colorIndex) const;
+	[[nodiscard]] ColorIndexValues computeColorIndexValues(
+		bool selected,
+		uint8 colorIndex) const;
+
 	[[nodiscard]] auto serviceQuoteCache(bool twoColored) const
 		-> not_null<Text::QuotePaintCache*>;
 	[[nodiscard]] auto serviceReplyCache(bool twoColored) const
@@ -362,6 +379,7 @@ private:
 	};
 
 	void assignPalette(not_null<const style::palette*> palette);
+	void clearColorIndexCaches();
 	void updateDarkValue();
 
 	[[nodiscard]] not_null<Text::QuotePaintCache*> coloredCache(
@@ -462,11 +480,14 @@ private:
 	style::icon _historyPollChoiceRight = { Qt::Uninitialized };
 	style::icon _historyPollChoiceWrong = { Qt::Uninitialized };
 
+	ColorIndicesCompressed _colorIndices;
+
 	bool _dark = false;
 
 	rpl::event_stream<> _paletteChanged;
 
 	rpl::lifetime _defaultPaletteChangeLifetime;
+	rpl::lifetime _colorIndicesLifetime;
 
 };
 
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index 11dae1d94..681604d93 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -1088,7 +1088,7 @@ SessionController::SessionController(
 , _invitePeekTimer([=] { checkInvitePeek(); })
 , _activeChatsFilter(session->data().chatsFilters().defaultId())
 , _defaultChatTheme(std::make_shared<Ui::ChatTheme>())
-, _chatStyle(std::make_unique<Ui::ChatStyle>())
+, _chatStyle(std::make_unique<Ui::ChatStyle>(session->colorIndicesValue()))
 , _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
 , _giftPremiumValidator(this) {
 	init();
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 383b5b8f7..611224c52 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 383b5b8f7e629475e5f22445167aaa7669c5cdd6
+Subproject commit 611224c52f3192f616018fc7a2c5930667531084