From 89d0a71591d0d82d4b2645b4c9e610962a6bc422 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Mon, 17 Oct 2022 20:29:48 +0400
Subject: [PATCH] Update API scheme on layer 148: Drafts in topics.

---
 Telegram/Resources/tl/api.tl                  |  27 ++-
 Telegram/SourceFiles/api/api_bot.cpp          |   4 +
 Telegram/SourceFiles/api/api_common.h         |   1 +
 Telegram/SourceFiles/api/api_polls.cpp        |  17 +-
 Telegram/SourceFiles/api/api_sending.cpp      |  25 ++-
 Telegram/SourceFiles/api/api_updates.cpp      |   4 +-
 Telegram/SourceFiles/api/api_who_reacted.cpp  |   2 +-
 Telegram/SourceFiles/apiwrap.cpp              | 158 +++++++++-------
 Telegram/SourceFiles/apiwrap.h                |   6 +-
 .../boxes/peers/add_bot_to_chat_box.cpp       |   3 +
 .../chat_helpers/stickers_dice_pack.cpp       |   2 +-
 Telegram/SourceFiles/data/data_changes.h      |   3 +-
 Telegram/SourceFiles/data/data_drafts.cpp     |  32 ++--
 Telegram/SourceFiles/data/data_drafts.h       | 113 +++++++-----
 .../SourceFiles/data/data_forum_topic.cpp     |  55 +++++-
 Telegram/SourceFiles/data/data_forum_topic.h  |  15 ++
 Telegram/SourceFiles/data/data_histories.cpp  |  45 ++---
 Telegram/SourceFiles/data/data_histories.h    |  20 +-
 .../SourceFiles/data/data_replies_list.cpp    |   8 +
 Telegram/SourceFiles/data/data_replies_list.h |   3 +
 .../SourceFiles/data/data_send_action.cpp     |   2 +-
 Telegram/SourceFiles/data/data_send_action.h  |   4 +-
 Telegram/SourceFiles/data/data_session.cpp    |   2 +-
 Telegram/SourceFiles/data/data_thread.cpp     |   7 +
 Telegram/SourceFiles/data/data_thread.h       |  11 ++
 .../dialogs/dialogs_inner_widget.cpp          |   8 +-
 Telegram/SourceFiles/dialogs/dialogs_key.cpp  |   6 +
 Telegram/SourceFiles/dialogs/dialogs_key.h    |   2 +
 .../SourceFiles/dialogs/ui/dialogs_layout.cpp |  44 +++--
 .../admin_log/history_admin_log_inner.cpp     |   2 +-
 .../admin_log/history_admin_log_item.cpp      | 160 ++++++++--------
 Telegram/SourceFiles/history/history.cpp      | 173 +++++++++++-------
 Telegram/SourceFiles/history/history.h        |  67 ++++---
 .../history/history_inner_widget.cpp          |   4 +-
 Telegram/SourceFiles/history/history_item.cpp |  28 ++-
 Telegram/SourceFiles/history/history_item.h   |   2 +-
 .../SourceFiles/history/history_message.cpp   |   3 +
 .../SourceFiles/history/history_service.cpp   |  11 +-
 .../SourceFiles/history/history_widget.cpp    |  71 +++----
 .../history_view_compose_controls.cpp         |  21 ++-
 .../history/view/history_view_bottom_info.cpp |   2 +-
 .../history/view/history_view_element.cpp     |   2 +-
 .../view/history_view_pinned_section.cpp      |   2 +-
 .../view/history_view_replies_section.cpp     |  23 ++-
 .../history/view/history_view_send_action.cpp |  12 +-
 .../history/view/history_view_send_action.h   |  10 +-
 .../view/history_view_top_bar_widget.cpp      |   4 +-
 Telegram/SourceFiles/info/info_controller.cpp |  38 ++--
 Telegram/SourceFiles/info/info_controller.h   |   2 +
 .../info/profile/info_profile_actions.cpp     |   3 +
 .../profile/info_profile_inner_widget.cpp     |   3 +
 Telegram/SourceFiles/mainwidget.cpp           |  12 +-
 .../passport/passport_form_controller.cpp     |   2 +-
 .../SourceFiles/storage/localimageloader.h    |   3 +
 .../SourceFiles/storage/storage_account.cpp   |  75 ++++----
 .../SourceFiles/support/support_helper.cpp    |  16 +-
 .../window/notifications_manager.cpp          |   9 +-
 .../SourceFiles/window/window_peer_menu.cpp   |  10 +-
 .../SourceFiles/window/window_peer_menu.h     |   1 +
 Telegram/lib_ui                               |   2 +-
 60 files changed, 861 insertions(+), 541 deletions(-)

diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl
index a5d651135..1fd39b08e 100644
--- a/Telegram/Resources/tl/api.tl
+++ b/Telegram/Resources/tl/api.tl
@@ -336,7 +336,7 @@ updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg
 updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update;
 updateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update;
 updateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update;
-updateDraftMessage#ee2bb969 peer:Peer draft:DraftMessage = Update;
+updateDraftMessage#1b49ec6d flags:# peer:Peer top_msg_id:flags.0?int draft:DraftMessage = Update;
 updateReadFeaturedStickers#571d2742 = Update;
 updateRecentStickers#9a422c20 = Update;
 updateConfig#a229dd06 = Update;
@@ -389,7 +389,7 @@ updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJ
 updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector<BotCommand> = Update;
 updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector<long> = Update;
 updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update;
-updateMessageReactions#154798c3 peer:Peer msg_id:int reactions:MessageReactions = Update;
+updateMessageReactions#5e1b3cb8 flags:# peer:Peer msg_id:int top_msg_id:flags.0?int reactions:MessageReactions = Update;
 updateAttachMenuBots#17b7a20b = Update;
 updateWebViewResultSent#1592b79d query_id:long = Update;
 updateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update;
@@ -593,6 +593,7 @@ inputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet;
 inputStickerSetPremiumGifts#c88b3b02 = InputStickerSet;
 inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet;
 inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet;
+inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet;
 
 stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true emojis:flags.7?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector<PhotoSize> thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet;
 
@@ -958,12 +959,17 @@ channelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdmi
 channelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction;
 channelAdminLogEventActionChangeAvailableReactions#be4e0ef8 prev_value:ChatReactions new_value:ChatReactions = ChannelAdminLogEventAction;
 channelAdminLogEventActionChangeUsernames#f04fb3a9 prev_value:Vector<string> new_value:Vector<string> = ChannelAdminLogEventAction;
+channelAdminLogEventActionToggleForum#2cc6383 new_value:Bool = ChannelAdminLogEventAction;
+channelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction;
+channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic = ChannelAdminLogEventAction;
+channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction;
+channelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic = ChannelAdminLogEventAction;
 
 channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent;
 
 channels.adminLogResults#ed8af74d events:Vector<ChannelAdminLogEvent> chats:Vector<Chat> users:Vector<User> = channels.AdminLogResults;
 
-channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true = ChannelAdminLogEventsFilter;
+channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true forums:flags.17?true = ChannelAdminLogEventsFilter;
 
 popularContact#5ce14175 client_id:long importers:int = PopularContact;
 
@@ -1322,7 +1328,7 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR
 account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult;
 account.resetPasswordOk#e926d63e = account.ResetPasswordResult;
 
-sponsoredMessage#3a836df8 flags:# recommended:flags.5?true random_id:bytes from_id:flags.3?Peer chat_invite:flags.4?ChatInvite chat_invite_hash:flags.4?string channel_post:flags.2?int start_param:flags.0?string message:string entities:flags.1?Vector<MessageEntity> = SponsoredMessage;
+sponsoredMessage#3a836df8 flags:# recommended:flags.5?true show_peer_photo:flags.6?true random_id:bytes from_id:flags.3?Peer chat_invite:flags.4?ChatInvite chat_invite_hash:flags.4?string channel_post:flags.2?int start_param:flags.0?string message:string entities:flags.1?Vector<MessageEntity> = SponsoredMessage;
 
 messages.sponsoredMessages#65a4c7d5 messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages;
 
@@ -1459,7 +1465,7 @@ stickerKeyword#fcfeb29c document_id:long keyword:Vector<string> = StickerKeyword
 username#b4073647 flags:# editable:flags.0?true active:flags.1?true username:string = Username;
 
 forumTopicDeleted#23f109b id:int = ForumTopic;
-forumTopic#5920d6dc flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true id:int date:int title:string icon_color:int icon_emoji_id:flags.0?long top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int from_id:Peer notify_settings:PeerNotifySettings = ForumTopic;
+forumTopic#71701da9 flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true id:int date:int title:string icon_color:int icon_emoji_id:flags.0?long top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int from_id:Peer notify_settings:PeerNotifySettings draft:flags.4?DraftMessage = ForumTopic;
 
 messages.forumTopics#367617d3 flags:# order_by_create_date:flags.0?true count:int topics:Vector<ForumTopic> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> pts:int = messages.ForumTopics;
 
@@ -1611,8 +1617,8 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t
 messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;
 messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;
 messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool;
-messages.sendMessage#d9d75a4 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
-messages.sendMedia#e25ff8e0 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
+messages.sendMessage#6460114f flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.8?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
+messages.sendMedia#a2e8e1de flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.8?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
 messages.forwardMessages#cc30290b flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
 messages.reportSpam#cf1592db peer:InputPeer = Bool;
 messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings;
@@ -1656,14 +1662,14 @@ messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs;
 messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;
 messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
 messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool;
-messages.sendInlineBotResult#7aa11297 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
+messages.sendInlineBotResult#15b11c73 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.8?int random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
 messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData;
 messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates;
 messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool;
 messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;
 messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool;
 messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;
-messages.saveDraft#bc39e14b flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> = Bool;
+messages.saveDraft#b4331e3f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int top_msg_id:flags.2?int peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> = Bool;
 messages.getAllDrafts#6a3f8d65 = Updates;
 messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers;
 messages.readFeaturedStickers#5b118126 id:Vector<long> = Bool;
@@ -1692,7 +1698,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;
 messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
 messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;
 messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages;
-messages.sendMultiMedia#f803138f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
+messages.sendMultiMedia#68463a19 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.8?int multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
 messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;
 messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets;
 messages.getSplitRanges#1cff7e08 = Vector<MessageRange>;
@@ -1865,6 +1871,7 @@ channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates;
 channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates;
 channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector<string> = Bool;
 channels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool;
+channels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool;
 channels.toggleForum#a4298b29 channel:InputChannel enabled:Bool = Updates;
 channels.createForumTopic#f40c0224 flags:# channel:InputChannel title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates;
 channels.getForumTopics#de560d1 flags:# channel:InputChannel q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics;
diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index 03883c946..19ef1c06a 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -351,6 +351,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
 	case ButtonType::RequestPhone: {
 		HideSingleUseKeyboard(controller, item);
 		const auto itemId = item->id;
+		const auto topicRootId = item->topicRootId();
 		const auto history = item->history();
 		controller->show(Ui::MakeConfirmBox({
 			.text = tr::lng_bot_share_phone(),
@@ -362,6 +363,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
 				auto action = Api::SendAction(history);
 				action.clearDraft = false;
 				action.replyTo = itemId;
+				action.topicRootId = topicRootId;
 				history->session().api().shareContact(
 					history->session().user(),
 					action);
@@ -381,10 +383,12 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
 			}
 		}
 		const auto replyToId = MsgId(0);
+		const auto topicRootId = MsgId(0);
 		Window::PeerMenuCreatePoll(
 			controller,
 			item->history()->peer,
 			replyToId,
+			topicRootId,
 			chosen,
 			disabled);
 	} break;
diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h
index 6e2ab6e80..65b3e7bce 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -37,6 +37,7 @@ struct SendAction {
 	not_null<History*> history;
 	SendOptions options;
 	MsgId replyTo = 0;
+	MsgId topicRootId = 0;
 	bool clearDraft = true;
 	bool generateLocal = true;
 	MsgId replaceMediaOf = 0;
diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp
index 4d4727470..85f34e2c2 100644
--- a/Telegram/SourceFiles/api/api_polls.cpp
+++ b/Telegram/SourceFiles/api/api_polls.cpp
@@ -42,16 +42,20 @@ void Polls::create(
 
 	const auto history = action.history;
 	const auto peer = history->peer;
+	const auto topicRootId = action.replyTo ? action.topicRootId : 0;
 	auto sendFlags = MTPmessages_SendMedia::Flags(0);
 	if (action.replyTo) {
 		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
+		if (topicRootId) {
+			sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id;
+		}
 	}
 	const auto clearCloudDraft = action.clearDraft;
 	if (clearCloudDraft) {
 		sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
-		history->clearLocalDraft();
-		history->clearCloudDraft();
-		history->startSavingCloudDraft();
+		history->clearLocalDraft(topicRootId);
+		history->clearCloudDraft(topicRootId);
+		history->startSavingCloudDraft(topicRootId);
 	}
 	const auto silentPost = ShouldSendSilent(peer, action.options);
 	if (silentPost) {
@@ -65,16 +69,17 @@ void Polls::create(
 		sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
 	}
 	auto &histories = history->owner().histories();
-	const auto replyTo = action.replyTo;
 	const auto randomId = base::RandomValue<uint64>();
 	histories.sendPreparedMessage(
 		history,
-		replyTo,
+		action.replyTo,
+		topicRootId,
 		randomId,
 		Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
 			MTP_flags(sendFlags),
 			peer->input,
 			Data::Histories::ReplyToPlaceholder(),
+			Data::Histories::TopicRootPlaceholder(),
 			PollDataToInputMedia(&data),
 			MTP_string(),
 			MTP_long(randomId),
@@ -85,6 +90,7 @@ void Polls::create(
 		), [=](const MTPUpdates &result, const MTP::Response &response) {
 		if (clearCloudDraft) {
 			history->finishSavingCloudDraft(
+				topicRootId,
 				UnixtimeFromMsgId(response.outerMsgId));
 		}
 		_session->changes().historyUpdated(
@@ -96,6 +102,7 @@ void Polls::create(
 	}, [=](const MTP::Error &error, const MTP::Response &response) {
 		if (clearCloudDraft) {
 			history->finishSavingCloudDraft(
+				topicRootId,
 				UnixtimeFromMsgId(response.outerMsgId));
 		}
 		fail();
diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index 791bd2166..64d28c770 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -86,6 +86,9 @@ void SendExistingMedia(
 	if (message.action.replyTo) {
 		flags |= MessageFlag::HasReplyInfo;
 		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
+		if (message.action.topicRootId) {
+			sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id;
+		}
 	}
 	const auto anonymousPost = peer->amAnonymous();
 	const auto silentPost = ShouldSendSilent(peer, message.action.options);
@@ -118,7 +121,6 @@ void SendExistingMedia(
 	if (!sentEntities.v.isEmpty()) {
 		sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
 	}
-	const auto replyTo = message.action.replyTo;
 	const auto captionText = caption.text;
 
 	if (message.action.options.scheduled) {
@@ -133,7 +135,7 @@ void SendExistingMedia(
 		newId.msg,
 		flags,
 		viaBotId,
-		replyTo,
+		message.action.replyTo,
 		HistoryItem::NewMessageDate(message.action.options.scheduled),
 		messageFromId,
 		messagePostAuthor,
@@ -146,12 +148,14 @@ void SendExistingMedia(
 		const auto usedFileReference = media->fileReference();
 		histories.sendPreparedMessage(
 			history,
-			replyTo,
+			message.action.replyTo,
+			message.action.topicRootId,
 			randomId,
 			Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
 				MTP_flags(sendFlags),
 				peer->input,
 				Data::Histories::ReplyToPlaceholder(),
+				Data::Histories::TopicRootPlaceholder(),
 				inputMedia(),
 				MTP_string(captionText),
 				MTP_long(randomId),
@@ -269,6 +273,9 @@ bool SendDice(MessageToSend &message) {
 	if (message.action.replyTo) {
 		flags |= MessageFlag::HasReplyInfo;
 		sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
+		if (message.action.topicRootId) {
+			sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id;
+		}
 	}
 	const auto replyHeader = NewMessageReplyHeader(message.action);
 	const auto anonymousPost = peer->amAnonymous();
@@ -289,7 +296,6 @@ bool SendDice(MessageToSend &message) {
 	const auto messagePostAuthor = peer->isBroadcast()
 		? session->user()->name()
 		: QString();
-	const auto replyTo = message.action.replyTo;
 
 	if (message.action.options.scheduled) {
 		flags |= MessageFlag::IsOrWasScheduled;
@@ -312,12 +318,14 @@ bool SendDice(MessageToSend &message) {
 		HistoryMessageMarkupData());
 	histories.sendPreparedMessage(
 		history,
-		replyTo,
+		message.action.replyTo,
+		message.action.topicRootId,
 		randomId,
 		Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
 			MTP_flags(sendFlags),
 			peer->input,
 			Data::Histories::ReplyToPlaceholder(),
+			Data::Histories::TopicRootPlaceholder(),
 			MTP_inputMediaDice(MTP_string(emoji)),
 			MTP_string(),
 			MTP_long(randomId),
@@ -368,9 +376,13 @@ void SendConfirmedFile(
 	const auto peer = history->peer;
 
 	if (!isEditing) {
-		file->to.replyTo = session->data().histories().convertTopicReplyTo(
+		const auto histories = &session->data().histories();
+		file->to.replyTo = histories->convertTopicReplyTo(
 			history,
 			file->to.replyTo);
+		file->to.topicRootId = histories->convertTopicReplyTo(
+			history,
+			file->to.topicRootId);
 	}
 
 	session->uploader().upload(newId, file);
@@ -378,6 +390,7 @@ void SendConfirmedFile(
 	auto action = SendAction(history, file->to.options);
 	action.clearDraft = false;
 	action.replyTo = file->to.replyTo;
+	action.topicRootId = file->to.topicRootId;
 	action.generateLocal = true;
 	session->api().sendAction(action);
 
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index b759af4ba..07e3ce501 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -2443,12 +2443,14 @@ void Updates::feedUpdate(const MTPUpdate &update) {
 	case mtpc_updateDraftMessage: {
 		const auto &data = update.c_updateDraftMessage();
 		const auto peerId = peerFromMTP(data.vpeer());
+		const auto topicRootId = data.vtop_msg_id().value_or_empty();
 		data.vdraft().match([&](const MTPDdraftMessage &data) {
-			Data::ApplyPeerCloudDraft(&session(), peerId, data);
+			Data::ApplyPeerCloudDraft(&session(), peerId, topicRootId, data);
 		}, [&](const MTPDdraftMessageEmpty &data) {
 			Data::ClearPeerCloudDraft(
 				&session(),
 				peerId,
+				topicRootId,
 				data.vdate().value_or_empty());
 		});
 	} break;
diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp
index a802787ff..26a99ce1f 100644
--- a/Telegram/SourceFiles/api/api_who_reacted.cpp
+++ b/Telegram/SourceFiles/api/api_who_reacted.cpp
@@ -562,7 +562,7 @@ bool WhoReadExists(not_null<HistoryItem*> item) {
 	}
 	const auto type = DetectSeenType(item);
 	const auto unseen = (type == Ui::WhoReadType::Seen)
-		? item->unread()
+		? item->unread(item->history())
 		: item->isUnreadMedia();
 	if (unseen) {
 		return false;
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 214443d67..248dd230f 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -1812,8 +1812,8 @@ void ApiWrap::sendNotifySettingsUpdates() {
 	session().mtp().sendAnything();
 }
 
-void ApiWrap::saveDraftToCloudDelayed(not_null<History*> history) {
-	_draftsSaveRequestIds.emplace(history, 0);
+void ApiWrap::saveDraftToCloudDelayed(not_null<Data::Thread*> thread) {
+	_draftsSaveRequestIds.emplace(base::make_weak(thread.get()), 0);
 	if (!_draftsSaveTimer.isActive()) {
 		_draftsSaveTimer.callOnce(kSaveCloudDraftTimeout);
 	}
@@ -2004,33 +2004,46 @@ void ApiWrap::saveCurrentDraftToCloud() {
 	Core::App().saveCurrentDraftsToHistories();
 
 	for (const auto &controller : _session->windows()) {
-		if (const auto history = controller->activeChatCurrent().history()) {
+		if (const auto thread = controller->activeChatCurrent().thread()) {
+			const auto history = thread->owningHistory();
 			_session->local().writeDrafts(history);
 
-			const auto localDraft = history->localDraft();
-			const auto cloudDraft = history->cloudDraft();
-			if (!Data::draftsAreEqual(localDraft, cloudDraft)
+			const auto topicRootId = thread->topicRootId();
+			const auto localDraft = history->localDraft(topicRootId);
+			const auto cloudDraft = history->cloudDraft(topicRootId);
+			if (!Data::DraftsAreEqual(localDraft, cloudDraft)
 				&& !_session->supportMode()) {
-				saveDraftToCloudDelayed(history);
+				saveDraftToCloudDelayed(thread);
 			}
 		}
 	}
 }
 
 void ApiWrap::saveDraftsToCloud() {
-	for (auto i = _draftsSaveRequestIds.begin(), e = _draftsSaveRequestIds.end(); i != e; ++i) {
-		if (i->second) continue; // sent already
+	for (auto i = begin(_draftsSaveRequestIds); i != end(_draftsSaveRequestIds);) {
+		const auto weak = i->first;
+		const auto thread = weak.get();
+		if (!thread) {
+			i = _draftsSaveRequestIds.erase(i);
+			continue;
+		}
+		auto &requestId = i->second;
+		++i;
+		if (requestId) {
+			continue; // sent already
+		}
 
-		auto history = i->first;
-		auto cloudDraft = history->cloudDraft();
-		auto localDraft = history->localDraft();
+		const auto history = thread->owningHistory();
+		const auto topicRootId = thread->topicRootId();
+		auto cloudDraft = history->cloudDraft(topicRootId);
+		auto localDraft = history->localDraft(topicRootId);
 		if (cloudDraft && cloudDraft->saveRequestId) {
 			request(base::take(cloudDraft->saveRequestId)).cancel();
 		}
 		if (!_session->supportMode()) {
-			cloudDraft = history->createCloudDraft(localDraft);
+			cloudDraft = history->createCloudDraft(topicRootId, localDraft);
 		} else if (!cloudDraft) {
-			cloudDraft = history->createCloudDraft(nullptr);
+			cloudDraft = history->createCloudDraft(topicRootId, nullptr);
 		}
 
 		auto flags = MTPmessages_SaveDraft::Flags(0);
@@ -2040,6 +2053,9 @@ void ApiWrap::saveDraftsToCloud() {
 		}
 		if (cloudDraft->msgId) {
 			flags |= MTPmessages_SaveDraft::Flag::f_reply_to_msg_id;
+			if (cloudDraft->topicRootId) {
+				flags |= MTPmessages_SaveDraft::Flag::f_top_msg_id;
+			}
 		}
 		if (!textWithTags.tags.isEmpty()) {
 			flags |= MTPmessages_SaveDraft::Flag::f_entities;
@@ -2049,44 +2065,45 @@ void ApiWrap::saveDraftsToCloud() {
 			TextUtilities::ConvertTextTagsToEntities(textWithTags.tags),
 			Api::ConvertOption::SkipLocal);
 
-		history->startSavingCloudDraft();
+		history->startSavingCloudDraft(topicRootId);
 		cloudDraft->saveRequestId = request(MTPmessages_SaveDraft(
 			MTP_flags(flags),
 			MTP_int(cloudDraft->msgId),
+			MTP_int(cloudDraft->topicRootId),
 			history->peer->input,
 			MTP_string(textWithTags.text),
 			entities
 		)).done([=](const MTPBool &result, const MTP::Response &response) {
-			history->finishSavingCloudDraft(
-				UnixtimeFromMsgId(response.outerMsgId));
-
 			const auto requestId = response.requestId;
-			if (const auto cloudDraft = history->cloudDraft()) {
+			history->finishSavingCloudDraft(
+				topicRootId,
+				UnixtimeFromMsgId(response.outerMsgId));
+			if (const auto cloudDraft = history->cloudDraft(topicRootId)) {
 				if (cloudDraft->saveRequestId == requestId) {
 					cloudDraft->saveRequestId = 0;
-					history->draftSavedToCloud();
+					history->draftSavedToCloud(topicRootId);
 				}
 			}
-			auto i = _draftsSaveRequestIds.find(history);
+			const auto i = _draftsSaveRequestIds.find(weak);
 			if (i != _draftsSaveRequestIds.cend()
 				&& i->second == requestId) {
-				_draftsSaveRequestIds.erase(history);
+				_draftsSaveRequestIds.erase(i);
 				checkQuitPreventFinished();
 			}
 		}).fail([=](const MTP::Error &error, const MTP::Response &response) {
-			history->finishSavingCloudDraft(
-				UnixtimeFromMsgId(response.outerMsgId));
-
 			const auto requestId = response.requestId;
-			if (const auto cloudDraft = history->cloudDraft()) {
+			history->finishSavingCloudDraft(
+				topicRootId,
+				UnixtimeFromMsgId(response.outerMsgId));
+			if (const auto cloudDraft = history->cloudDraft(topicRootId)) {
 				if (cloudDraft->saveRequestId == requestId) {
-					history->clearCloudDraft();
+					history->clearCloudDraft(topicRootId);
 				}
 			}
-			auto i = _draftsSaveRequestIds.find(history);
+			const auto i = _draftsSaveRequestIds.find(weak);
 			if (i != _draftsSaveRequestIds.cend()
 				&& i->second == requestId) {
-				_draftsSaveRequestIds.erase(history);
+				_draftsSaveRequestIds.erase(i);
 				checkQuitPreventFinished();
 			}
 		}).send();
@@ -3420,6 +3437,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 		if (action.replyTo) {
 			flags |= MessageFlag::HasReplyInfo;
 			sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to_msg_id;
+			if (action.topicRootId) {
+				sendFlags |= MTPmessages_SendMessage::Flag::f_top_msg_id;
+			}
 		}
 		const auto replyHeader = NewMessageReplyHeader(action);
 		MTPMessageMedia media = MTP_messageMediaEmpty();
@@ -3446,10 +3466,11 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 			sendFlags |= MTPmessages_SendMessage::Flag::f_entities;
 		}
 		const auto clearCloudDraft = action.clearDraft;
+		const auto topicRootId = action.topicRootId;
 		if (clearCloudDraft) {
 			sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft;
-			history->clearCloudDraft();
-			history->startSavingCloudDraft();
+			history->clearCloudDraft(topicRootId);
+			history->startSavingCloudDraft(topicRootId);
 		}
 		const auto sendAs = action.options.sendAs;
 		const auto messageFromId = sendAs
@@ -3468,12 +3489,11 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 			sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date;
 		}
 		const auto viaBotId = UserId();
-		const auto replyTo = action.replyTo;
 		lastMessage = history->addNewLocalMessage(
 			newId.msg,
 			flags,
 			viaBotId,
-			replyTo,
+			action.replyTo,
 			HistoryItem::NewMessageDate(action.options.scheduled),
 			messageFromId,
 			messagePostAuthor,
@@ -3482,12 +3502,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 			HistoryMessageMarkupData());
 		histories.sendPreparedMessage(
 			history,
-			replyTo,
+			action.replyTo,
+			topicRootId,
 			randomId,
 			Data::Histories::PrepareMessage<MTPmessages_SendMessage>(
 				MTP_flags(sendFlags),
 				peer->input,
 				Data::Histories::ReplyToPlaceholder(),
+				Data::Histories::TopicRootPlaceholder(),
 				msgText,
 				MTP_long(randomId),
 				MTPReplyMarkup(),
@@ -3497,6 +3519,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 			), [=](const MTPUpdates &result, const MTP::Response &response) {
 			if (clearCloudDraft) {
 				history->finishSavingCloudDraft(
+					topicRootId,
 					UnixtimeFromMsgId(response.outerMsgId));
 			}
 		}, [=](const MTP::Error &error, const MTP::Response &response) {
@@ -3507,6 +3530,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
 			}
 			if (clearCloudDraft) {
 				history->finishSavingCloudDraft(
+					topicRootId,
 					UnixtimeFromMsgId(response.outerMsgId));
 			}
 		});
@@ -3573,12 +3597,16 @@ void ApiWrap::sendInlineResult(
 			? (*localMessageId)
 			: _session->data().nextLocalMessageId());
 	const auto randomId = base::RandomValue<uint64>();
+	const auto topicRootId = action.replyTo ? action.topicRootId : 0;
 
 	auto flags = NewMessageFlags(peer);
 	auto sendFlags = MTPmessages_SendInlineBotResult::Flag::f_clear_draft | 0;
 	if (action.replyTo) {
 		flags |= MessageFlag::HasReplyInfo;
 		sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_reply_to_msg_id;
+		if (topicRootId) {
+			sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_top_msg_id;
+		}
 	}
 	const auto anonymousPost = peer->amAnonymous();
 	const auto silentPost = ShouldSendSilent(peer, action.options);
@@ -3618,19 +3646,20 @@ void ApiWrap::sendInlineResult(
 		action.replyTo,
 		messagePostAuthor);
 
-	history->clearCloudDraft();
-	history->startSavingCloudDraft();
+	history->clearCloudDraft(topicRootId);
+	history->startSavingCloudDraft(topicRootId);
 
 	auto &histories = history->owner().histories();
-	const auto replyTo = action.replyTo;
 	histories.sendPreparedMessage(
 		history,
-		replyTo,
+		action.replyTo,
+		topicRootId,
 		randomId,
 		Data::Histories::PrepareMessage<MTPmessages_SendInlineBotResult>(
 			MTP_flags(sendFlags),
 			peer->input,
 			Data::Histories::ReplyToPlaceholder(),
+			Data::Histories::TopicRootPlaceholder(),
 			MTP_long(randomId),
 			MTP_long(data->getQueryId()),
 			MTP_string(data->getId()),
@@ -3638,10 +3667,12 @@ void ApiWrap::sendInlineResult(
 			(sendAs ? sendAs->input : MTP_inputPeerEmpty())
 		), [=](const MTPUpdates &result, const MTP::Response &response) {
 		history->finishSavingCloudDraft(
+			topicRootId,
 			UnixtimeFromMsgId(response.outerMsgId));
 	}, [=](const MTP::Error &error, const MTP::Response &response) {
 		sendMessageFail(error, peer, randomId, newId);
 		history->finishSavingCloudDraft(
+			topicRootId,
 			UnixtimeFromMsgId(response.outerMsgId));
 	});
 	finishForwarding(action);
@@ -3740,6 +3771,7 @@ void ApiWrap::sendMediaWithRandomId(
 		uint64 randomId) {
 	const auto history = item->history();
 	const auto replyTo = item->replyToId();
+	const auto topicRootId = item->topicRootId();
 
 	auto caption = item->originalText();
 	TextUtilities::Trim(caption);
@@ -3750,22 +3782,16 @@ void ApiWrap::sendMediaWithRandomId(
 
 	const auto updateRecentStickers = Api::HasAttachedStickers(media);
 
-	const auto flags = MTPmessages_SendMedia::Flags(0)
-		| (replyTo
-			? MTPmessages_SendMedia::Flag::f_reply_to_msg_id
-			: MTPmessages_SendMedia::Flag(0))
+	using Flag = MTPmessages_SendMedia::Flag;
+	const auto flags = Flag(0)
+		| (replyTo ? Flag::f_reply_to_msg_id : Flag(0))
+		| (topicRootId ? Flag::f_top_msg_id : Flag(0))
 		| (ShouldSendSilent(history->peer, options)
-			? MTPmessages_SendMedia::Flag::f_silent
-			: MTPmessages_SendMedia::Flag(0))
-		| (!sentEntities.v.isEmpty()
-			? MTPmessages_SendMedia::Flag::f_entities
-			: MTPmessages_SendMedia::Flag(0))
-		| (options.scheduled
-			? MTPmessages_SendMedia::Flag::f_schedule_date
-			: MTPmessages_SendMedia::Flag(0))
-		| (options.sendAs
-			? MTPmessages_SendMedia::Flag::f_send_as
-			: MTPmessages_SendMedia::Flag(0));
+			? Flag::f_silent
+			: Flag(0))
+		| (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0))
+		| (options.scheduled ? Flag::f_schedule_date : Flag(0))
+		| (options.sendAs ? Flag::f_send_as : Flag(0));
 
 	auto &histories = history->owner().histories();
 	const auto peer = history->peer;
@@ -3773,11 +3799,13 @@ void ApiWrap::sendMediaWithRandomId(
 	histories.sendPreparedMessage(
 		history,
 		replyTo,
+		topicRootId,
 		randomId,
 		Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
 			MTP_flags(flags),
 			peer->input,
 			Data::Histories::ReplyToPlaceholder(),
+			Data::Histories::TopicRootPlaceholder(),
 			media,
 			MTP_string(caption.text),
 			MTP_long(randomId),
@@ -3859,30 +3887,29 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
 	}
 	const auto history = sample->history();
 	const auto replyTo = sample->replyToId();
+	const auto topicRootId = sample->topicRootId();
 	const auto sendAs = album->options.sendAs;
-	const auto flags = MTPmessages_SendMultiMedia::Flags(0)
-		| (replyTo
-			? MTPmessages_SendMultiMedia::Flag::f_reply_to_msg_id
-			: MTPmessages_SendMultiMedia::Flag(0))
+	using Flag = MTPmessages_SendMultiMedia::Flag;
+	const auto flags = Flag(0)
+		| (replyTo ? Flag::f_reply_to_msg_id : Flag(0))
+		| (topicRootId ? Flag::f_top_msg_id : Flag(0))
 		| (ShouldSendSilent(history->peer, album->options)
-			? MTPmessages_SendMultiMedia::Flag::f_silent
-			: MTPmessages_SendMultiMedia::Flag(0))
-		| (album->options.scheduled
-			? MTPmessages_SendMultiMedia::Flag::f_schedule_date
-			: MTPmessages_SendMultiMedia::Flag(0))
-		| (sendAs
-			? MTPmessages_SendMultiMedia::Flag::f_send_as
-			: MTPmessages_SendMultiMedia::Flag(0));
+			? Flag::f_silent
+			: Flag(0))
+		| (album->options.scheduled ? Flag::f_schedule_date : Flag(0))
+		| (sendAs ? Flag::f_send_as : Flag(0));
 	auto &histories = history->owner().histories();
 	const auto peer = history->peer;
 	histories.sendPreparedMessage(
 		history,
 		replyTo,
+		topicRootId,
 		uint64(0), // randomId
 		Data::Histories::PrepareMessage<MTPmessages_SendMultiMedia>(
 			MTP_flags(flags),
 			peer->input,
 			Data::Histories::ReplyToPlaceholder(),
+			Data::Histories::TopicRootPlaceholder(),
 			MTP_vector<MTPInputSingleMedia>(medias),
 			MTP_int(album->options.scheduled),
 			(sendAs ? sendAs->input : MTP_inputPeerEmpty())
@@ -3905,6 +3932,7 @@ FileLoadTo ApiWrap::fileLoadTaskOptions(const SendAction &action) const {
 		peer->id,
 		action.options,
 		action.replyTo,
+		action.topicRootId,
 		action.replaceMediaOf);
 }
 
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 7cbfc2ab6..ddbf75568 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -245,7 +245,7 @@ public:
 	void updateNotifySettingsDelayed(not_null<const Data::Thread*> thread);
 	void updateNotifySettingsDelayed(not_null<const PeerData*> peer);
 	void updateNotifySettingsDelayed(Data::DefaultNotify type);
-	void saveDraftToCloudDelayed(not_null<History*> history);
+	void saveDraftToCloudDelayed(not_null<Data::Thread*> thread);
 
 	static int OnlineTillFromStatus(
 		const MTPUserStatus &status,
@@ -563,7 +563,9 @@ private:
 	};
 	base::flat_map<NotifySettingsKey, mtpRequestId> _notifySettingRequests;
 
-	base::flat_map<not_null<History*>, mtpRequestId> _draftsSaveRequestIds;
+	base::flat_map<
+		base::weak_ptr<Data::Thread>,
+		mtpRequestId> _draftsSaveRequestIds;
 	base::Timer _draftsSaveTimer;
 
 	base::flat_set<mtpRequestId> _stickerSetDisenableRequests;
diff --git a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
index b220fa84d..4e636da93 100644
--- a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp
@@ -65,14 +65,17 @@ void ShareBotGame(
 	auto &histories = history->owner().histories();
 	const auto randomId = base::RandomValue<uint64>();
 	const auto replyTo = 0;
+	const auto topicRootId = 0;
 	histories.sendPreparedMessage(
 		history,
 		replyTo,
+		topicRootId,
 		randomId,
 		Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
 			MTP_flags(0),
 			chat->input,
 			Data::Histories::ReplyToPlaceholder(),
+			Data::Histories::TopicRootPlaceholder(),
 			MTP_inputMediaGame(
 				MTP_inputGameShortName(
 					bot->inputUser,
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp
index 23ea9d72e..2c95462ad 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp
@@ -127,7 +127,7 @@ void DicePack::generateLocal(int index, const QString &name) {
 		QByteArray(),
 		nullptr,
 		SendMediaType::File,
-		FileLoadTo(0, {}, 0, 0),
+		FileLoadTo(0, {}, 0, 0, 0),
 		{});
 	task.process({ .generateGoodThumbnail = false });
 	const auto result = task.peekResult();
diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h
index 1a7b31099..87a0588b7 100644
--- a/Telegram/SourceFiles/data/data_changes.h
+++ b/Telegram/SourceFiles/data/data_changes.h
@@ -151,8 +151,9 @@ struct TopicUpdate {
 		Notifications = (1U << 4),
 		Title = (1U << 5),
 		Icon = (1U << 6),
+		CloudDraft = (1U << 7),
 
-		LastUsedBit = (1U << 4),
+		LastUsedBit = (1U << 7),
 	};
 	using Flags = base::flags<Flag>;
 	friend inline constexpr auto is_flag_type(Flag) { return true; }
diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp
index 88d614f0b..474822a48 100644
--- a/Telegram/SourceFiles/data/data_drafts.cpp
+++ b/Telegram/SourceFiles/data/data_drafts.cpp
@@ -19,29 +19,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 namespace Data {
 
-DraftKey DraftKey::FromSerializedOld(int32 value) {
-	return !value
-		? DraftKey::None()
-		: (value == kLocalDraftIndex + kEditDraftShiftOld)
-		? DraftKey::LocalEdit()
-		: (value == kScheduledDraftIndex + kEditDraftShiftOld)
-		? DraftKey::ScheduledEdit()
-		: (value > 0 && value < 0x4000'0000)
-		? DraftKey::Replies(int64(value))
-		: (value > kEditDraftShiftOld
-			&& value < kEditDraftShiftOld + 0x4000'000)
-		? DraftKey::RepliesEdit(int64(value - kEditDraftShiftOld))
-		: DraftKey::None();
-}
-
 Draft::Draft(
 	const TextWithTags &textWithTags,
 	MsgId msgId,
+	MsgId topicRootId,
 	const MessageCursor &cursor,
 	PreviewState previewState,
 	mtpRequestId saveRequestId)
 : textWithTags(textWithTags)
 , msgId(msgId)
+, topicRootId(topicRootId)
 , cursor(cursor)
 , previewState(previewState)
 , saveRequestId(saveRequestId) {
@@ -50,10 +37,12 @@ Draft::Draft(
 Draft::Draft(
 	not_null<const Ui::InputField*> field,
 	MsgId msgId,
+	MsgId topicRootId,
 	PreviewState previewState,
 	mtpRequestId saveRequestId)
 : textWithTags(field->getTextWithTags())
 , msgId(msgId)
+, topicRootId(topicRootId)
 , cursor(field)
 , previewState(previewState) {
 }
@@ -61,10 +50,11 @@ Draft::Draft(
 void ApplyPeerCloudDraft(
 		not_null<Main::Session*> session,
 		PeerId peerId,
+		MsgId topicRootId,
 		const MTPDdraftMessage &draft) {
 	const auto history = session->data().history(peerId);
 	const auto date = draft.vdate().v;
-	if (history->skipCloudDraftUpdate(date)) {
+	if (history->skipCloudDraftUpdate(topicRootId, date)) {
 		return;
 	}
 	const auto textWithTags = TextWithTags{
@@ -78,6 +68,7 @@ void ApplyPeerCloudDraft(
 	auto cloudDraft = std::make_unique<Draft>(
 		textWithTags,
 		replyTo,
+		topicRootId,
 		MessageCursor(QFIXED_MAX, QFIXED_MAX, QFIXED_MAX),
 		(draft.is_no_webpage()
 			? Data::PreviewState::Cancelled
@@ -85,20 +76,21 @@ void ApplyPeerCloudDraft(
 	cloudDraft->date = date;
 
 	history->setCloudDraft(std::move(cloudDraft));
-	history->applyCloudDraft();
+	history->applyCloudDraft(topicRootId);
 }
 
 void ClearPeerCloudDraft(
 		not_null<Main::Session*> session,
 		PeerId peerId,
+		MsgId topicRootId,
 		TimeId date) {
 	const auto history = session->data().history(peerId);
-	if (history->skipCloudDraftUpdate(date)) {
+	if (history->skipCloudDraftUpdate(topicRootId, date)) {
 		return;
 	}
 
-	history->clearCloudDraft();
-	history->applyCloudDraft();
+	history->clearCloudDraft(topicRootId);
+	history->applyCloudDraft(topicRootId);
 }
 
 } // namespace Data
diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h
index 533c74167..5af044019 100644
--- a/Telegram/SourceFiles/data/data_drafts.h
+++ b/Telegram/SourceFiles/data/data_drafts.h
@@ -20,10 +20,12 @@ namespace Data {
 void ApplyPeerCloudDraft(
 	not_null<Main::Session*> session,
 	PeerId peerId,
+	MsgId topicRootId,
 	const MTPDdraftMessage &draft);
 void ClearPeerCloudDraft(
 	not_null<Main::Session*> session,
 	PeerId peerId,
+	MsgId topicRootId,
 	TimeId date);
 
 enum class PreviewState : char {
@@ -37,18 +39,21 @@ struct Draft {
 	Draft(
 		const TextWithTags &textWithTags,
 		MsgId msgId,
+		MsgId topicRootId,
 		const MessageCursor &cursor,
 		PreviewState previewState,
 		mtpRequestId saveRequestId = 0);
 	Draft(
 		not_null<const Ui::InputField*> field,
 		MsgId msgId,
+		MsgId topicRootId,
 		PreviewState previewState,
 		mtpRequestId saveRequestId = 0);
 
 	TimeId date = 0;
 	TextWithTags textWithTags;
 	MsgId msgId = 0; // replyToId for message draft, editMsgId for edit draft
+	MsgId topicRootId = 0;
 	MessageCursor cursor;
 	PreviewState previewState = PreviewState::Allowed;
 	mtpRequestId saveRequestId = 0;
@@ -56,70 +61,93 @@ struct Draft {
 
 class DraftKey {
 public:
-	[[nodiscard]] static DraftKey None() {
+	[[nodiscard]] static constexpr DraftKey None() {
 		return 0;
 	}
-	[[nodiscard]] static DraftKey Local() {
-		return kLocalDraftIndex;
+	[[nodiscard]] static constexpr DraftKey Local(MsgId topicRootId) {
+		return (topicRootId < 0 || topicRootId >= ServerMaxMsgId)
+			? None()
+			: (topicRootId ? topicRootId.bare : kLocalDraftIndex);
 	}
-	[[nodiscard]] static DraftKey LocalEdit() {
-		return kLocalDraftIndex + kEditDraftShift;
+	[[nodiscard]] static constexpr DraftKey LocalEdit(MsgId topicRootId) {
+		return (topicRootId < 0 || topicRootId >= ServerMaxMsgId)
+			? None()
+			: ((topicRootId ? topicRootId.bare : kLocalDraftIndex)
+				+ kEditDraftShift);
 	}
-	[[nodiscard]] static DraftKey Cloud() {
-		return kCloudDraftIndex;
+	[[nodiscard]] static constexpr DraftKey Cloud(MsgId topicRootId) {
+		return (topicRootId < 0 || topicRootId >= ServerMaxMsgId)
+			? None()
+			: topicRootId
+			? (kCloudDraftShift + topicRootId.bare)
+			: kCloudDraftIndex;
 	}
-	[[nodiscard]] static DraftKey Scheduled() {
+	[[nodiscard]] static constexpr DraftKey Scheduled() {
 		return kScheduledDraftIndex;
 	}
-	[[nodiscard]] static DraftKey ScheduledEdit() {
+	[[nodiscard]] static constexpr DraftKey ScheduledEdit() {
 		return kScheduledDraftIndex + kEditDraftShift;
 	}
-	[[nodiscard]] static DraftKey Replies(MsgId rootId) {
-		return rootId.bare;
-	}
-	[[nodiscard]] static DraftKey RepliesEdit(MsgId rootId) {
-		return rootId.bare + kEditDraftShift;
-	}
 
-	[[nodiscard]] static DraftKey FromSerialized(qint64 value) {
+	[[nodiscard]] static constexpr DraftKey FromSerialized(qint64 value) {
 		return value;
 	}
-	[[nodiscard]] qint64 serialize() const {
+	[[nodiscard]] constexpr qint64 serialize() const {
 		return _value;
 	}
 
-	[[nodiscard]] static DraftKey FromSerializedOld(int32 value);
+	[[nodiscard]] static constexpr DraftKey FromSerializedOld(int32 value) {
+		return !value
+			? None()
+			: (value == kLocalDraftIndex + kEditDraftShiftOld)
+			? LocalEdit(0)
+			: (value == kScheduledDraftIndex + kEditDraftShiftOld)
+			? ScheduledEdit()
+			: (value > 0 && value < 0x4000'0000)
+			? Local(MsgId(value))
+			: (value > kEditDraftShiftOld
+				&& value < kEditDraftShiftOld + 0x4000'000)
+			? LocalEdit(int64(value - kEditDraftShiftOld))
+			: None();
+	}
+	[[nodiscard]] constexpr bool isLocal() const {
+		return (_value == kLocalDraftIndex)
+			|| (_value > 0 && _value < ServerMaxMsgId.bare);
+	}
+	[[nodiscard]] constexpr bool isCloud() const {
+		return (_value == kCloudDraftIndex)
+			|| (_value > kCloudDraftShift
+				&& _value < kCloudDraftShift + ServerMaxMsgId.bare);
+	}
 
-	inline bool operator<(const DraftKey &other) const {
-		return _value < other._value;
-	}
-	inline bool operator==(const DraftKey &other) const {
-		return _value == other._value;
-	}
-	inline bool operator>(const DraftKey &other) const {
-		return (other < *this);
-	}
-	inline bool operator<=(const DraftKey &other) const {
-		return !(other < *this);
-	}
-	inline bool operator>=(const DraftKey &other) const {
-		return !(*this < other);
-	}
-	inline bool operator!=(const DraftKey &other) const {
-		return !(*this == other);
+	[[nodiscard]] constexpr MsgId topicRootId() const {
+		const auto max = ServerMaxMsgId.bare;
+		if (_value > kCloudDraftShift && _value < kCloudDraftShift + max) {
+			return (_value - kCloudDraftShift);
+		} else if (_value > kEditDraftShift && _value < kEditDraftShift + max) {
+			return (_value - kEditDraftShift);
+		} else if (_value > 0 && _value < max) {
+			return _value;
+		}
+		return 0;
 	}
+
+
+	friend inline constexpr auto operator<=>(DraftKey, DraftKey) = default;
+
 	inline explicit operator bool() const {
 		return _value != 0;
 	}
 
 private:
-	DraftKey(int64 value) : _value(value) {
+	constexpr DraftKey(int64 value) : _value(value) {
 	}
 
 	static constexpr auto kLocalDraftIndex = -1;
 	static constexpr auto kCloudDraftIndex = -2;
 	static constexpr auto kScheduledDraftIndex = -3;
 	static constexpr auto kEditDraftShift = ServerMaxMsgId.bare;
+	static constexpr auto kCloudDraftShift = 2 * ServerMaxMsgId.bare;
 	static constexpr auto kEditDraftShiftOld = 0x3FFF'FFFF;
 
 	int64 _value = 0;
@@ -128,7 +156,7 @@ private:
 
 using HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>;
 
-inline bool draftStringIsEmpty(const QString &text) {
+[[nodiscard]] inline bool DraftStringIsEmpty(const QString &text) {
 	for (const auto &ch : text) {
 		if (!ch.isSpace()) {
 			return false;
@@ -137,20 +165,19 @@ inline bool draftStringIsEmpty(const QString &text) {
 	return true;
 }
 
-inline bool draftIsNull(const Draft *draft) {
+[[nodiscard]] inline bool DraftIsNull(const Draft *draft) {
 	return !draft
-		|| (draftStringIsEmpty(draft->textWithTags.text) && !draft->msgId);
+		|| (!draft->msgId && DraftStringIsEmpty(draft->textWithTags.text));
 }
 
-inline bool draftsAreEqual(const Draft *a, const Draft *b) {
-	bool aIsNull = draftIsNull(a);
-	bool bIsNull = draftIsNull(b);
+[[nodiscard]] inline bool DraftsAreEqual(const Draft *a, const Draft *b) {
+	const auto aIsNull = DraftIsNull(a);
+	const auto bIsNull = DraftIsNull(b);
 	if (aIsNull) {
 		return bIsNull;
 	} else if (bIsNull) {
 		return false;
 	}
-
 	return (a->textWithTags == b->textWithTags)
 		&& (a->msgId == b->msgId)
 		&& (a->previewState == b->previewState);
diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp
index 5ea18d8d2..4ef0754dc 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.cpp
+++ b/Telegram/SourceFiles/data/data_forum_topic.cpp
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "data/data_forum.h"
 #include "data/data_histories.h"
 #include "data/data_replies_list.h"
+#include "data/data_send_action.h"
 #include "data/notify/data_notify_settings.h"
 #include "data/data_session.h"
 #include "data/stickers/data_custom_emoji.h"
@@ -142,10 +143,15 @@ ForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId)
 , _forum(forum)
 , _list(_forum->topicsList())
 , _replies(std::make_shared<RepliesList>(history(), rootId))
+, _sendActionPainter(owner().sendActionManager().repliesPainter(
+	history(),
+	rootId))
 , _rootId(rootId)
 , _lastKnownServerMessageId(rootId) {
 	Thread::setMuted(owner().notifySettings().isMuted(this));
 
+	_sendActionPainter->setTopic(this);
+
 	_replies->unreadCountValue(
 	) | rpl::combine_previous(
 	) | rpl::filter([=] {
@@ -161,6 +167,7 @@ ForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId)
 }
 
 ForumTopic::~ForumTopic() {
+	_sendActionPainter->setTopic(nullptr);
 	session().api().unreadThings().cancelRequests(this);
 }
 
@@ -191,11 +198,24 @@ MsgId ForumTopic::rootId() const {
 	return _rootId;
 }
 
+bool ForumTopic::creating() const {
+	return _forum->creating(_rootId);
+}
+
+void ForumTopic::discard() {
+	Expects(creating());
+
+	_forum->discardCreatingId(_rootId);
+}
+
 void ForumTopic::setRealRootId(MsgId realId) {
 	if (_rootId != realId) {
 		_rootId = realId;
 		_lastKnownServerMessageId = realId;
 		_replies = std::make_shared<RepliesList>(history(), _rootId);
+		_sendActionPainter = owner().sendActionManager().repliesPainter(
+			history(),
+			_rootId);
 	}
 }
 
@@ -221,6 +241,15 @@ void ForumTopic::applyTopic(const MTPDforumTopic &data) {
 
 	owner().notifySettings().apply(this, data.vnotify_settings());
 
+	const auto draft = data.vdraft();
+	if (draft && draft->type() == mtpc_draftMessage) {
+		Data::ApplyPeerCloudDraft(
+			&session(),
+			channel()->id,
+			_rootId,
+			draft->c_draftMessage());
+	}
+
 	_replies->setInboxReadTill(
 		data.vread_inbox_max_id().v,
 		data.vunread_count().v);
@@ -390,13 +419,11 @@ void ForumTopic::requestChatListMessage() {
 
 TimeId ForumTopic::adjustedChatListTimeId() const {
 	const auto result = chatListTimeId();
-#if 0 // #TODO forum draft
-	if (const auto draft = cloudDraft()) {
-		if (!Data::draftIsNull(draft) && !session().supportMode()) {
+	if (const auto draft = history()->cloudDraft(_rootId)) {
+		if (!Data::DraftIsNull(draft) && !session().supportMode()) {
 			return std::max(result, draft->date);
 		}
 	}
-#endif
 	return result;
 }
 
@@ -480,6 +507,17 @@ void ForumTopic::applyItemAdded(not_null<HistoryItem*> item) {
 	setLastMessage(item);
 }
 
+void ForumTopic::maybeSetLastMessage(not_null<HistoryItem*> item) {
+	Expects(item->topicRootId() == _rootId);
+
+	if (!_lastMessage
+		|| ((*_lastMessage)->date() < item->date())
+		|| ((*_lastMessage)->date() == item->date()
+			&& (*_lastMessage)->id < item->id)) {
+		setLastMessage(item);
+	}
+}
+
 void ForumTopic::applyItemRemoved(MsgId id) {
 	if (const auto lastItem = lastMessage()) {
 		if (lastItem->id == id) {
@@ -499,6 +537,11 @@ void ForumTopic::applyItemRemoved(MsgId id) {
 	}
 }
 
+bool ForumTopic::isServerSideUnread(
+		not_null<const HistoryItem*> item) const {
+	return _replies->isServerSideUnread(item);
+}
+
 int ForumTopic::unreadCount() const {
 	return _replies->unreadCountCurrent();
 }
@@ -544,6 +587,10 @@ void ForumTopic::setUnreadMark(bool unread) {
 	Thread::setUnreadMark(unread);
 }
 
+not_null<HistoryView::SendActionPainter*> ForumTopic::sendActionPainter() {
+	return _sendActionPainter.get();
+}
+
 int ForumTopic::chatListUnreadCount() const {
 	const auto state = chatListUnreadState();
 	return state.marks
diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h
index c687abef7..d18d1a900 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.h
+++ b/Telegram/SourceFiles/data/data_forum_topic.h
@@ -25,6 +25,10 @@ namespace Main {
 class Session;
 } // namespace Main
 
+namespace HistoryView {
+class SendActionPainter;
+} // namespace HistoryView
+
 namespace Data {
 
 class RepliesList;
@@ -57,6 +61,9 @@ public:
 	[[nodiscard]] rpl::producer<> destroyed() const;
 	[[nodiscard]] MsgId rootId() const;
 
+	[[nodiscard]] bool creating() const;
+	void discard();
+
 	void setRealRootId(MsgId realId);
 
 	void applyTopic(const MTPDforumTopic &data);
@@ -91,6 +98,7 @@ public:
 	void applyColorId(int32 colorId);
 	void applyItemAdded(not_null<HistoryItem*> item);
 	void applyItemRemoved(MsgId id);
+	void maybeSetLastMessage(not_null<HistoryItem*> item);
 
 	[[nodiscard]] PeerNotifySettings &notify() {
 		return _notify;
@@ -105,6 +113,9 @@ public:
 		std::shared_ptr<CloudImageView> &view,
 		const Dialogs::Ui::PaintContext &context) const override;
 
+	[[nodiscard]] bool isServerSideUnread(
+		not_null<const HistoryItem*> item) const override;
+
 	[[nodiscard]] int unreadCount() const;
 	[[nodiscard]] bool unreadCountKnown() const;
 
@@ -113,6 +124,9 @@ public:
 	void setMuted(bool muted) override;
 	void setUnreadMark(bool unread) override;
 
+	[[nodiscard]] auto sendActionPainter()
+		->not_null<HistoryView::SendActionPainter*> override;
+
 private:
 	void indexTitleParts();
 	void validateDefaultIcon() const;
@@ -132,6 +146,7 @@ private:
 	const not_null<Forum*> _forum;
 	const not_null<Dialogs::MainList*> _list;
 	std::shared_ptr<RepliesList> _replies;
+	std::shared_ptr<HistoryView::SendActionPainter> _sendActionPainter;
 	MsgId _rootId = 0;
 	MsgId _lastKnownServerMessageId = 0;
 
diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp
index 0d49f4016..665e9030c 100644
--- a/Telegram/SourceFiles/data/data_histories.cpp
+++ b/Telegram/SourceFiles/data/data_histories.cpp
@@ -247,7 +247,7 @@ void Histories::readInboxOnNewMessage(not_null<HistoryItem*> item) {
 }
 
 void Histories::readClientSideMessage(not_null<HistoryItem*> item) {
-	if (item->out() || !item->unread()) {
+	if (item->out() || !item->unread(item->history())) {
 		return;
 	}
 	const auto history = item->history();
@@ -886,20 +886,22 @@ bool Histories::isCreatingTopic(
 int Histories::sendPreparedMessage(
 		not_null<History*> history,
 		MsgId replyTo,
+		MsgId topicRootId,
 		uint64 randomId,
-		Fn<PreparedMessage(MsgId replyTo)> message,
+		Fn<PreparedMessage(MsgId replyTo, MsgId topicRootId)> message,
 		Fn<void(const MTPUpdates&, const MTP::Response&)> done,
 		Fn<void(const MTP::Error&, const MTP::Response&)> fail) {
-	if (isCreatingTopic(history, replyTo)) {
+	if (isCreatingTopic(history, topicRootId)) {
 		const auto id = ++_requestAutoincrement;
-		const auto creatingId = FullMsgId(history->peer->id, replyTo);
+		const auto creatingId = FullMsgId(history->peer->id, topicRootId);
 		auto i = _creatingTopics.find(creatingId);
 		if (i == end(_creatingTopics)) {
-			sendCreateTopicRequest(history, replyTo);
+			sendCreateTopicRequest(history, topicRootId);
 			i = _creatingTopics.emplace(creatingId).first;
 		}
 		i->second.push_back({
 			.randomId = randomId,
+			.replyTo = replyTo,
 			.message = std::move(message),
 			.done = std::move(done),
 			.fail = std::move(fail),
@@ -908,8 +910,9 @@ int Histories::sendPreparedMessage(
 		_creatingTopicRequests.emplace(id);
 		return id;
 	}
-	const auto realTo = convertTopicReplyTo(history, replyTo);
-	return v::match(message(realTo), [&](const auto &request) {
+	const auto realReply = convertTopicReplyTo(history, replyTo);
+	const auto realRoot = convertTopicReplyTo(history, topicRootId);
+	return v::match(message(realReply, realRoot), [&](const auto &request) {
 		const auto type = RequestType::Send;
 		return sendRequest(history, type, [=](Fn<void()> finish) {
 			const auto session = &_owner->session();
@@ -935,38 +938,38 @@ int Histories::sendPreparedMessage(
 	});
 }
 
-void Histories::checkTopicCreated(FullMsgId rootId, MsgId realId) {
+void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) {
 	const auto i = _creatingTopics.find(rootId);
 	if (i != end(_creatingTopics)) {
 		auto scheduled = base::take(i->second);
 		_creatingTopics.erase(i);
 
-		_createdTopicIds.emplace(rootId, realId);
+		_createdTopicIds.emplace(rootId, realRoot);
 
 		if (const auto forum = _owner->peer(rootId.peer)->forum()) {
-			forum->created(rootId.msg, realId);
+			forum->created(rootId.msg, realRoot);
 		}
 
 		const auto history = _owner->history(rootId.peer);
 		for (auto &entry : scheduled) {
 			_creatingTopicRequests.erase(entry.requestId);
-			AssertIsDebug();
-			//sendPreparedMessage(
-			//	history,
-			//	realId,
-			//	entry.randomId,
-			//	std::move(entry.message),
-			//	std::move(entry.done),
-			//	std::move(entry.fail));
+			sendPreparedMessage(
+				history,
+				entry.replyTo,
+				realRoot,
+				entry.randomId,
+				std::move(entry.message),
+				std::move(entry.done),
+				std::move(entry.fail));
 		}
 		for (const auto &item : history->clientSideMessages()) {
 			const auto replace = [&](MsgId nowId) {
-				return (nowId == rootId.msg) ? realId : nowId;
+				return (nowId == rootId.msg) ? realRoot : nowId;
 			};
-			if (item->replyToTop() == rootId.msg) {
+			if (item->topicRootId() == rootId.msg) {
 				item->setReplyFields(
 					replace(item->replyToId()),
-					realId,
+					realRoot,
 					true);
 			}
 		}
diff --git a/Telegram/SourceFiles/data/data_histories.h b/Telegram/SourceFiles/data/data_histories.h
index 780867f3c..05f7534f4 100644
--- a/Telegram/SourceFiles/data/data_histories.h
+++ b/Telegram/SourceFiles/data/data_histories.h
@@ -103,22 +103,25 @@ public:
 	int sendPreparedMessage(
 		not_null<History*> history,
 		MsgId replyTo,
+		MsgId topicRootId,
 		uint64 randomId,
-		Fn<PreparedMessage(MsgId replyTo)> message,
+		Fn<PreparedMessage(MsgId replyTo, MsgId topicRootId)> message,
 		Fn<void(const MTPUpdates&, const MTP::Response&)> done,
 		Fn<void(const MTP::Error&, const MTP::Response&)> fail);
 
 	struct ReplyToPlaceholder {
 	};
+	struct TopicRootPlaceholder {
+	};
 	template <typename RequestType, typename ...Args>
-	static Fn<Histories::PreparedMessage(MsgId)> PrepareMessage(
+	static Fn<Histories::PreparedMessage(MsgId, MsgId)> PrepareMessage(
 			const Args &...args) {
-		return [=](MsgId replyTo) {
-			return RequestType(ReplaceReplyTo(args, replyTo)...);
+		return [=](MsgId replyTo, MsgId topicRootId) -> RequestType {
+			return { ReplaceReplyIds(args, replyTo, topicRootId)... };
 		};
 	}
 
-	void checkTopicCreated(FullMsgId rootId, MsgId realId);
+	void checkTopicCreated(FullMsgId rootId, MsgId realRoot);
 	[[nodiscard]] MsgId convertTopicReplyTo(
 		not_null<History*> history,
 		MsgId replyTo) const;
@@ -147,7 +150,8 @@ private:
 	};
 	struct DelayedByTopicMessage {
 		uint64 randomId = 0;
-		Fn<PreparedMessage(MsgId replyTo)> message;
+		MsgId replyTo = 0;
+		Fn<PreparedMessage(MsgId replyTo, MsgId topicRootId)> message;
 		Fn<void(const MTPUpdates&, const MTP::Response&)> done;
 		Fn<void(const MTP::Error&, const MTP::Response&)> fail;
 		int requestId = 0;
@@ -162,9 +166,11 @@ private:
 	};
 
 	template <typename Arg>
-	static auto ReplaceReplyTo(Arg arg, MsgId replyTo) {
+	static auto ReplaceReplyIds(Arg arg, MsgId replyTo, MsgId topicRootId) {
 		if constexpr (std::is_same_v<Arg, ReplyToPlaceholder>) {
 			return MTP_int(replyTo);
+		} else if constexpr (std::is_same_v<Arg, TopicRootPlaceholder>) {
+			return MTP_int(topicRootId);
 		} else {
 			return arg;
 		}
diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp
index 292190b5e..065e25284 100644
--- a/Telegram/SourceFiles/data/data_replies_list.cpp
+++ b/Telegram/SourceFiles/data/data_replies_list.cpp
@@ -765,6 +765,14 @@ void RepliesList::setUnreadCount(std::optional<int> count) {
 	}
 }
 
+bool RepliesList::isServerSideUnread(
+		not_null<const HistoryItem*> item) const {
+	const auto till = item->out()
+		? computeOutboxReadTillFull()
+		: computeInboxReadTillFull();
+	return (item->id > till);
+}
+
 void RepliesList::checkReadTillEnd() {
 	if (_unreadCount.current() != 0
 		&& _skippedAfter == 0
diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h
index 8c30c912d..2d5690c36 100644
--- a/Telegram/SourceFiles/data/data_replies_list.h
+++ b/Telegram/SourceFiles/data/data_replies_list.h
@@ -43,6 +43,9 @@ public:
 	void setOutboxReadTill(MsgId readTillId);
 	[[nodiscard]] MsgId computeOutboxReadTillFull() const;
 
+	[[nodiscard]] bool isServerSideUnread(
+		not_null<const HistoryItem*> item) const;
+
 	[[nodiscard]] std::optional<int> computeUnreadCountLocally(
 		MsgId afterId) const;
 	void requestUnreadCount();
diff --git a/Telegram/SourceFiles/data/data_send_action.cpp b/Telegram/SourceFiles/data/data_send_action.cpp
index 77333acf6..ea8f92bac 100644
--- a/Telegram/SourceFiles/data/data_send_action.cpp
+++ b/Telegram/SourceFiles/data/data_send_action.cpp
@@ -75,7 +75,7 @@ auto SendActionManager::repliesPainter(
 	if (auto strong = weak.lock()) {
 		return strong;
 	}
-	auto result = std::make_shared<SendActionPainter>(history);
+	auto result = std::make_shared<SendActionPainter>(history, rootId);
 	weak = result;
 	return result;
 }
diff --git a/Telegram/SourceFiles/data/data_send_action.h b/Telegram/SourceFiles/data/data_send_action.h
index a209e1263..227ef6569 100644
--- a/Telegram/SourceFiles/data/data_send_action.h
+++ b/Telegram/SourceFiles/data/data_send_action.h
@@ -17,10 +17,12 @@ class SendActionPainter;
 
 namespace Data {
 
+class Thread;
+
 class SendActionManager final {
 public:
 	struct AnimationUpdate {
-		not_null<History*> history;
+		not_null<Thread*> thread;
 		int left = 0;
 		int width = 0;
 		int height = 0;
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index f5dadc374..d1fd7b983 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -3820,7 +3820,7 @@ void Session::refreshChatListEntry(Dialogs::Key key) {
 	const auto mainList = chatsListFor(entry);
 	auto event = ChatListEntryRefresh{ .key = key };
 	const auto creating = event.existenceChanged = !entry->inChatList();
-	if (creating && topic && topic->forum()->creating(topic->rootId())) {
+	if (creating && topic && topic->creating()) {
 		return;
 	} else if (event.existenceChanged) {
 		const auto mainRow = entry->addToChatList(0, mainList);
diff --git a/Telegram/SourceFiles/data/data_thread.cpp b/Telegram/SourceFiles/data/data_thread.cpp
index 602eaf062..7c42a92b9 100644
--- a/Telegram/SourceFiles/data/data_thread.cpp
+++ b/Telegram/SourceFiles/data/data_thread.cpp
@@ -17,6 +17,13 @@ namespace Data {
 
 Thread::~Thread() = default;
 
+MsgId Thread::topicRootId() const {
+	if (const auto topic = asTopic()) {
+		return topic->rootId();
+	}
+	return MsgId();
+}
+
 not_null<PeerData*> Thread::peer() const {
 	return owningHistory()->peer;
 }
diff --git a/Telegram/SourceFiles/data/data_thread.h b/Telegram/SourceFiles/data/data_thread.h
index e3c8c08d9..58df8b7f4 100644
--- a/Telegram/SourceFiles/data/data_thread.h
+++ b/Telegram/SourceFiles/data/data_thread.h
@@ -24,6 +24,10 @@ class Proxy;
 class ConstProxy;
 } // namespace HistoryUnreadThings
 
+namespace HistoryView {
+class SendActionPainter;
+} // namespace HistoryView
+
 namespace st {
 extern const int &dialogsTextWidthMin;
 } // namespace st
@@ -57,6 +61,7 @@ public:
 	[[nodiscard]] not_null<const History*> owningHistory() const {
 		return const_cast<Thread*>(this)->owningHistory();
 	}
+	[[nodiscard]] MsgId topicRootId() const;
 	[[nodiscard]] not_null<PeerData*> peer() const;
 	[[nodiscard]] PeerNotifySettings &notify();
 	[[nodiscard]] const PeerNotifySettings &notify() const;
@@ -87,6 +92,9 @@ public:
 	}
 	virtual void setUnreadMark(bool unread);
 
+	[[nodiscard]] virtual bool isServerSideUnread(
+		not_null<const HistoryItem*> item) const = 0;
+
 	[[nodiscard]] const base::flat_set<MsgId> &unreadMentionsIds() const;
 	[[nodiscard]] const base::flat_set<MsgId> &unreadReactionsIds() const;
 
@@ -97,6 +105,9 @@ public:
 		return _lastItemDialogsView;
 	}
 
+	[[nodiscard]] virtual auto sendActionPainter()
+		-> not_null<HistoryView::SendActionPainter*> = 0;
+
 private:
 	enum class Flag : uchar {
 		UnreadMark = (1 << 0),
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 340267d82..94eee7e94 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -185,7 +185,7 @@ InnerWidget::InnerWidget(
 			width(),
 			update.textUpdated);
 		updateDialogRow(
-			RowDescriptor(update.history, FullMsgId()),
+			RowDescriptor(update.thread, FullMsgId()),
 			updateRect,
 			UpdateRowSection::Default | UpdateRowSection::Filtered);
 	}, lifetime());
@@ -2394,8 +2394,10 @@ void InnerWidget::refreshEmptyLabel() {
 	const auto data = &session().data();
 	const auto state = !shownDialogs()->empty()
 		? EmptyState::None
-		: (_openedForum && _openedForum->topicsList()->loaded())
-		? EmptyState::EmptyForum
+		: _openedForum
+		? (_openedForum->topicsList()->loaded()
+			? EmptyState::EmptyForum
+			: EmptyState::Loading)
 		: (!_filterId && data->contactsLoaded().current())
 		? EmptyState::NoContacts
 		: (_filterId > 0) && data->chatsList()->loaded()
diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp
index 1156daf43..fe96a5a7d 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp
@@ -19,12 +19,18 @@ Key::Key(History *history) : _value(history) {
 Key::Key(Data::Folder *folder) : _value(folder) {
 }
 
+Key::Key(Data::Thread *thread) : _value(thread) {
+}
+
 Key::Key(Data::ForumTopic *topic) : _value(topic) {
 }
 
 Key::Key(not_null<History*> history) : _value(history) {
 }
 
+Key::Key(not_null<Data::Thread*> thread) : _value(thread) {
+}
+
 Key::Key(not_null<Data::Folder*> folder) : _value(folder) {
 }
 
diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h
index b644188b5..8ac12ec64 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_key.h
+++ b/Telegram/SourceFiles/dialogs/dialogs_key.h
@@ -27,10 +27,12 @@ public:
 	}
 	Key(History *history);
 	Key(Data::Folder *folder);
+	Key(Data::Thread *thread);
 	Key(Data::ForumTopic *topic);
 	Key(not_null<Entry*> entry) : _value(entry) {
 	}
 	Key(not_null<History*> history);
+	Key(not_null<Data::Thread*> thread);
 	Key(not_null<Data::Folder*> folder);
 	Key(not_null<Data::ForumTopic*> topic);
 
diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
index 4d886f556..5b079c732 100644
--- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
+++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
@@ -48,7 +48,8 @@ const auto kPsaBadgePrefix = "cloud_lng_badge_psa_";
 	return user->isBot() && !user->isSupport() && !user->isRepliesChat();
 }
 
-[[nodiscard]] bool ShowSendActionInDialogs(History *history) {
+[[nodiscard]] bool ShowSendActionInDialogs(Data::Thread *thread) {
+	const auto history = thread ? thread->owningHistory().get() : nullptr;
 	return history
 		&& (!history->peer->isUser()
 			|| history->peer->asUser()->onlineTill > 0);
@@ -465,8 +466,8 @@ void PaintRow(
 			: context.selected
 			? st::dialogsTextFgServiceOver
 			: st::dialogsTextFgService;
-		if (!ShowSendActionInDialogs(history)
-			|| !history->sendActionPainter()->paint(
+		if (!ShowSendActionInDialogs(thread)
+			|| !thread->sendActionPainter()->paint(
 				p,
 				nameleft,
 				texttop,
@@ -474,7 +475,8 @@ void PaintRow(
 				context.width,
 				color,
 				context.paused)) {
-			if (history->cloudDraftTextCache().isEmpty()) {
+			auto &cache = thread->cloudDraftTextCache();
+			if (cache.isEmpty()) {
 				using namespace TextUtilities;
 				auto draftWrapped = Text::PlainLink(
 					tr::lng_dialogs_text_from_wrapped(
@@ -496,10 +498,10 @@ void PaintRow(
 						}),
 						Text::WithEntities);
 				const auto context = Core::MarkedTextContext{
-					.session = &history->session(),
+					.session = &thread->session(),
 					.customEmojiRepaint = customEmojiRepaint,
 				};
-				history->cloudDraftTextCache().setMarkedText(
+				cache.setMarkedText(
 					st::dialogsTextStyle,
 					draftText,
 					DialogTextOptions(),
@@ -510,7 +512,7 @@ void PaintRow(
 				: context.selected
 				? st::dialogsTextFgOver
 				: st::dialogsTextFg);
-			history->cloudDraftTextCache().draw(p, {
+			cache.draw(p, {
 				.position = { nameleft, texttop },
 				.availableWidth = availableWidth,
 				.palette = &(supportMode
@@ -549,8 +551,8 @@ void PaintRow(
 			? st::dialogsTextFgServiceOver
 			: st::dialogsTextFgService;
 		p.setFont(st::dialogsTextFont);
-		if (!ShowSendActionInDialogs(history)
-			|| !history->sendActionPainter()->paint(
+		if (!ShowSendActionInDialogs(thread)
+			|| !thread->sendActionPainter()->paint(
 				p,
 				nameleft,
 				texttop,
@@ -575,8 +577,10 @@ void PaintRow(
 			: st::dialogsPinnedIcon;
 		icon.paint(p, context.width - context.st->padding.right() - icon.width(), texttop, context.width);
 	}
-	auto sendStateIcon = [&]() -> const style::icon* {
-		if (draft) {
+	const auto sendStateIcon = [&]() -> const style::icon* {
+		if (!thread) {
+			return nullptr;
+		} else if (draft) {
 			if (draft->saveRequestId) {
 				return &(context.active
 					? st::dialogsSendingIconActive
@@ -586,7 +590,7 @@ void PaintRow(
 			}
 		} else if (item && !item->isEmpty() && item->needCheck()) {
 			if (!item->isSending() && !item->hasFailed()) {
-				if (item->unread()) {
+				if (item->unread(thread)) {
 					return &(context.active
 						? st::dialogsSentIconActive
 						: context.selected
@@ -607,7 +611,7 @@ void PaintRow(
 		}
 		return nullptr;
 	}();
-	if (sendStateIcon && history) {
+	if (sendStateIcon) {
 		rectForName.setWidth(rectForName.width() - st::dialogsSendStateSkip);
 		sendStateIcon->paint(p, rectForName.topLeft() + QPoint(rectForName.width(), 0), context.width);
 	}
@@ -892,12 +896,12 @@ void RowPainter::Paint(
 	const auto unreadMuted = entry->chatListMutedBadge();
 	const auto item = entry->chatListMessage();
 	const auto cloudDraft = [&]() -> const Data::Draft*{
-		if (history && (!item || (!unreadCount && !unreadMark))) {
+		if (thread && (!item || (!unreadCount && !unreadMark))) {
 			// Draw item, if there are unread messages.
-			if (const auto draft = history->cloudDraft()) {
-				if (!Data::draftIsNull(draft)) {
-					return draft;
-				}
+			const auto draft = thread->owningHistory()->cloudDraft(
+				thread->topicRootId());
+			if (!Data::DraftIsNull(draft)) {
+				return draft;
 			}
 		}
 		return nullptr;
@@ -977,8 +981,8 @@ void RowPainter::Paint(
 			texttop,
 			availableWidth,
 			st::dialogsTextFont->height);
-		const auto actionWasPainted = ShowSendActionInDialogs(history)
-			? history->sendActionPainter()->paint(
+		const auto actionWasPainted = ShowSendActionInDialogs(thread)
+			? thread->sendActionPainter()->paint(
 				p,
 				rect.x(),
 				rect.y(),
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
index 48e015844..a450d3f21 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
@@ -654,7 +654,7 @@ bool InnerWidget::elementHideReply(not_null<const Element*> view) {
 }
 
 bool InnerWidget::elementShownUnread(not_null<const Element*> view) {
-	return view->data()->unread();
+	return false;
 }
 
 void InnerWidget::elementSendBotCommand(
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp
index 2df368381..92de1bd43 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp
@@ -695,9 +695,14 @@ void GenerateItems(
 	using LogJoinByRequest =
 		MTPDchannelAdminLogEventActionParticipantJoinByRequest;
 	using LogNoForwards = MTPDchannelAdminLogEventActionToggleNoForwards;
-	using LogActionSendMessage = MTPDchannelAdminLogEventActionSendMessage;
-	using LogEventActionChangeAvailableReactions = MTPDchannelAdminLogEventActionChangeAvailableReactions;
-	using LogEventActionChangeUsernames = MTPDchannelAdminLogEventActionChangeUsernames;
+	using LogSendMessage = MTPDchannelAdminLogEventActionSendMessage;
+	using LogChangeAvailableReactions = MTPDchannelAdminLogEventActionChangeAvailableReactions;
+	using LogChangeUsernames = MTPDchannelAdminLogEventActionChangeUsernames;
+	using LogToggleForum = MTPDchannelAdminLogEventActionToggleForum;
+	using LogCreateTopic = MTPDchannelAdminLogEventActionCreateTopic;
+	using LogEditTopic = MTPDchannelAdminLogEventActionEditTopic;
+	using LogDeleteTopic = MTPDchannelAdminLogEventActionDeleteTopic;
+	using LogPinTopic = MTPDchannelAdminLogEventActionPinTopic;
 
 	const auto session = &history->session();
 	const auto id = event.vid().v;
@@ -972,7 +977,7 @@ void GenerateItems(
 			ExtractSentDate(action.vmessage()));
 	};
 
-	const auto createParticipantJoin = [&]() {
+	const auto createParticipantJoin = [&](const LogJoin&) {
 		const auto text = (channel->isMegagroup()
 			? tr::lng_admin_log_participant_joined
 			: tr::lng_admin_log_participant_joined_channel)(
@@ -983,7 +988,7 @@ void GenerateItems(
 		addSimpleServiceMessage(text);
 	};
 
-	const auto createParticipantLeave = [&]() {
+	const auto createParticipantLeave = [&](const LogLeave&) {
 		const auto text = (channel->isMegagroup()
 			? tr::lng_admin_log_participant_left
 			: tr::lng_admin_log_participant_left_channel)(
@@ -1457,7 +1462,7 @@ void GenerateItems(
 		addSimpleServiceMessage(text);
 	};
 
-	const auto createSendMessage = [&](const LogActionSendMessage &data) {
+	const auto createSendMessage = [&](const LogSendMessage &data) {
 		const auto text = tr::lng_admin_log_sent_message(
 			tr::now,
 			lt_from,
@@ -1476,7 +1481,7 @@ void GenerateItems(
 	};
 
 	const auto createChangeAvailableReactions = [&](
-			const LogEventActionChangeAvailableReactions &data) {
+			const LogChangeAvailableReactions &data) {
 		const auto text = data.vnew_value().match([&](
 				const MTPDchatReactionsNone&) {
 			return tr::lng_admin_log_reactions_disabled(
@@ -1514,87 +1519,74 @@ void GenerateItems(
 		addSimpleServiceMessage(text);
 	};
 
-	const auto createChangeUsernames = [&](
-			const LogEventActionChangeUsernames &data) {
+	const auto createChangeUsernames = [&](const LogChangeUsernames &data) {
 		// #TODO usernames
 		addSimpleServiceMessage({ "changed usernames" });
 	};
 
-	action.match([&](const LogTitle &data) {
-		createChangeTitle(data);
-	}, [&](const LogAbout &data) {
-		createChangeAbout(data);
-	}, [&](const LogUsername &data) {
-		createChangeUsername(data);
-	}, [&](const LogPhoto &data) {
-		createChangePhoto(data);
-	}, [&](const LogInvites &data) {
-		createToggleInvites(data);
-	}, [&](const LogSign &data) {
-		createToggleSignatures(data);
-	}, [&](const LogPin &data) {
-		createUpdatePinned(data);
-	}, [&](const LogEdit &data) {
-		createEditMessage(data);
-	}, [&](const LogDelete &data) {
-		createDeleteMessage(data);
-	}, [&](const LogJoin &) {
-		createParticipantJoin();
-	}, [&](const LogLeave &) {
-		createParticipantLeave();
-	}, [&](const LogInvite &data) {
-		createParticipantInvite(data);
-	}, [&](const LogBan &data) {
-		createParticipantToggleBan(data);
-	}, [&](const LogPromote &data) {
-		createParticipantToggleAdmin(data);
-	}, [&](const LogSticker &data) {
-		createChangeStickerSet(data);
-	}, [&](const LogPreHistory &data) {
-		createTogglePreHistoryHidden(data);
-	}, [&](const LogPermissions &data) {
-		createDefaultBannedRights(data);
-	}, [&](const LogPoll &data) {
-		createStopPoll(data);
-	}, [&](const LogDiscussion &data) {
-		createChangeLinkedChat(data);
-	}, [&](const LogLocation &data) {
-		createChangeLocation(data);
-	}, [&](const LogSlowMode &data) {
-		createToggleSlowMode(data);
-	}, [&](const LogStartCall &data) {
-		createStartGroupCall(data);
-	}, [&](const LogDiscardCall &data) {
-		createDiscardGroupCall(data);
-	}, [&](const LogMute &data) {
-		createParticipantMute(data);
-	}, [&](const LogUnmute &data) {
-		createParticipantUnmute(data);
-	}, [&](const LogCallSetting &data) {
-		createToggleGroupCallSetting(data);
-	}, [&](const LogJoinByInvite &data) {
-		createParticipantJoinByInvite(data);
-	}, [&](const LogInviteDelete &data) {
-		createExportedInviteDelete(data);
-	}, [&](const LogInviteRevoke &data) {
-		createExportedInviteRevoke(data);
-	}, [&](const LogInviteEdit &data) {
-		createExportedInviteEdit(data);
-	}, [&](const LogVolume &data) {
-		createParticipantVolume(data);
-	}, [&](const LogTTL &data) {
-		createChangeHistoryTTL(data);
-	}, [&](const LogJoinByRequest &data) {
-		createParticipantJoinByRequest(data);
-	}, [&](const LogNoForwards &data) {
-		createToggleNoForwards(data);
-	}, [&](const LogActionSendMessage &data) {
-		createSendMessage(data);
-	}, [&](const LogEventActionChangeAvailableReactions &data) {
-		createChangeAvailableReactions(data);
-	}, [&](const LogEventActionChangeUsernames &data) {
-		createChangeUsernames(data);
-	});
+	const auto createToggleForum = [&](const LogToggleForum &data) {
+
+	};
+
+	const auto createCreateTopic = [&](const LogCreateTopic &data) {
+
+	};
+
+	const auto createEditTopic = [&](const LogEditTopic &data) {
+
+	};
+
+	const auto createDeleteTopic = [&](const LogDeleteTopic &data) {
+
+	};
+
+	const auto createPinTopic = [&](const LogPinTopic &data) {
+
+	};
+
+	action.match(
+		createChangeTitle,
+		createChangeAbout,
+		createChangeUsername,
+		createChangePhoto,
+		createToggleInvites,
+		createToggleSignatures,
+		createUpdatePinned,
+		createEditMessage,
+		createDeleteMessage,
+		createParticipantJoin,
+		createParticipantLeave,
+		createParticipantInvite,
+		createParticipantToggleBan,
+		createParticipantToggleAdmin,
+		createChangeStickerSet,
+		createTogglePreHistoryHidden,
+		createDefaultBannedRights,
+		createStopPoll,
+		createChangeLinkedChat,
+		createChangeLocation,
+		createToggleSlowMode,
+		createStartGroupCall,
+		createDiscardGroupCall,
+		createParticipantMute,
+		createParticipantUnmute,
+		createToggleGroupCallSetting,
+		createParticipantJoinByInvite,
+		createExportedInviteDelete,
+		createExportedInviteRevoke,
+		createExportedInviteEdit,
+		createParticipantVolume,
+		createChangeHistoryTTL,
+		createParticipantJoinByRequest,
+		createToggleNoForwards,
+		createSendMessage,
+		createChangeAvailableReactions,
+		createChangeUsernames,
+		createToggleForum,
+		createCreateTopic,
+		createEditTopic,
+		createDeleteTopic,
+		createPinTopic);
 }
 
 } // namespace AdminLog
diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp
index 25e1bb00a..2507f9989 100644
--- a/Telegram/SourceFiles/history/history.cpp
+++ b/Telegram/SourceFiles/history/history.cpp
@@ -157,53 +157,56 @@ void History::itemVanished(not_null<HistoryItem*> item) {
 		clearLastKeyboard();
 	}
 	if ((!item->out() || item->isPost())
-		&& item->unread()
+		&& item->unread(this)
 		&& unreadCount() > 0) {
 		setUnreadCount(unreadCount() - 1);
 	}
 }
 
 void History::takeLocalDraft(not_null<History*> from) {
-	const auto i = from->_drafts.find(Data::DraftKey::Local());
+	const auto topicRootId = MsgId(0);
+	const auto i = from->_drafts.find(Data::DraftKey::Local(topicRootId));
 	if (i == end(from->_drafts)) {
 		return;
 	}
 	auto &draft = i->second;
 	if (!draft->textWithTags.text.isEmpty()
-		&& !_drafts.contains(Data::DraftKey::Local())) {
+		&& !_drafts.contains(Data::DraftKey::Local(topicRootId))) {
 		// Edit and reply to drafts can't migrate.
 		// Cloud drafts do not migrate automatically.
 		draft->msgId = 0;
 
 		setLocalDraft(std::move(draft));
 	}
-	from->clearLocalDraft();
+	from->clearLocalDraft(topicRootId);
 	session().api().saveDraftToCloudDelayed(from);
 }
 
-void History::createLocalDraftFromCloud() {
-	const auto draft = cloudDraft();
+void History::createLocalDraftFromCloud(MsgId topicRootId) {
+	const auto draft = cloudDraft(topicRootId);
 	if (!draft) {
-		clearLocalDraft();
+		clearLocalDraft(topicRootId);
 		return;
-	} else if (Data::draftIsNull(draft) || !draft->date) {
+	} else if (Data::DraftIsNull(draft) || !draft->date) {
 		return;
 	}
 
-	auto existing = localDraft();
-	if (Data::draftIsNull(existing)
+	auto existing = localDraft(topicRootId);
+	if (Data::DraftIsNull(existing)
 		|| !existing->date
 		|| draft->date >= existing->date) {
 		if (!existing) {
 			setLocalDraft(std::make_unique<Data::Draft>(
 				draft->textWithTags,
 				draft->msgId,
+				topicRootId,
 				draft->cursor,
 				draft->previewState));
-			existing = localDraft();
+			existing = localDraft(topicRootId);
 		} else if (existing != draft) {
 			existing->textWithTags = draft->textWithTags;
 			existing->msgId = draft->msgId;
+			existing->topicRootId = draft->topicRootId;
 			existing->cursor = draft->cursor;
 			existing->previewState = draft->previewState;
 		}
@@ -219,18 +222,22 @@ Data::Draft *History::draft(Data::DraftKey key) const {
 	return (i != _drafts.end()) ? i->second.get() : nullptr;
 }
 
-void History::setDraft(Data::DraftKey key, std::unique_ptr<Data::Draft> &&draft) {
+void History::setDraft(
+		Data::DraftKey key,
+		std::unique_ptr<Data::Draft> &&draft) {
 	if (!key) {
 		return;
 	}
-	const auto changingCloudDraft = (key == Data::DraftKey::Cloud());
-	if (changingCloudDraft) {
-		cloudDraftTextCache().clear();
+	const auto cloudThread = key.isCloud()
+		? threadFor(key.topicRootId())
+		: nullptr;
+	if (cloudThread) {
+		cloudThread->cloudDraftTextCache().clear();
 	}
 	if (draft) {
 		_drafts[key] = std::move(draft);
-	} else if (_drafts.remove(key) && changingCloudDraft) {
-		updateChatListSortPosition();
+	} else if (_drafts.remove(key) && cloudThread) {
+		cloudThread->updateChatListSortPosition();
 	}
 }
 
@@ -250,31 +257,38 @@ void History::clearDraft(Data::DraftKey key) {
 }
 
 void History::clearDrafts() {
-	const auto changingCloudDraft = _drafts.contains(Data::DraftKey::Cloud());
-	_drafts.clear();
-	if (changingCloudDraft) {
-		cloudDraftTextCache().clear();
-		updateChatListSortPosition();
+	for (auto &[key, draft] : base::take(_drafts)) {
+		const auto cloudThread = key.isCloud()
+			? threadFor(key.topicRootId())
+			: nullptr;
+		if (cloudThread) {
+			cloudThread->cloudDraftTextCache().clear();
+			cloudThread->updateChatListSortPosition();
+		}
 	}
 }
 
-Data::Draft *History::createCloudDraft(const Data::Draft *fromDraft) {
-	if (Data::draftIsNull(fromDraft)) {
+Data::Draft *History::createCloudDraft(
+		MsgId topicRootId,
+		const Data::Draft *fromDraft) {
+	if (Data::DraftIsNull(fromDraft)) {
 		setCloudDraft(std::make_unique<Data::Draft>(
 			TextWithTags(),
 			0,
+			topicRootId,
 			MessageCursor(),
 			Data::PreviewState::Allowed));
-		cloudDraft()->date = TimeId(0);
+		cloudDraft(topicRootId)->date = TimeId(0);
 	} else {
-		auto existing = cloudDraft();
+		auto existing = cloudDraft(topicRootId);
 		if (!existing) {
 			setCloudDraft(std::make_unique<Data::Draft>(
 				fromDraft->textWithTags,
 				fromDraft->msgId,
+				topicRootId,
 				fromDraft->cursor,
 				fromDraft->previewState));
-			existing = cloudDraft();
+			existing = cloudDraft(topicRootId);
 		} else if (existing != fromDraft) {
 			existing->textWithTags = fromDraft->textWithTags;
 			existing->msgId = fromDraft->msgId;
@@ -284,42 +298,60 @@ Data::Draft *History::createCloudDraft(const Data::Draft *fromDraft) {
 		existing->date = base::unixtime::now();
 	}
 
-	cloudDraftTextCache().clear();
-	updateChatListSortPosition();
-
-	return cloudDraft();
-}
-
-bool History::skipCloudDraftUpdate(TimeId date) const {
-	return (_savingCloudDraftRequests > 0)
-		|| (date < _acceptCloudDraftsAfter);
-}
-
-void History::startSavingCloudDraft() {
-	++_savingCloudDraftRequests;
-}
-
-void History::finishSavingCloudDraft(TimeId savedAt) {
-	if (_savingCloudDraftRequests > 0) {
-		--_savingCloudDraftRequests;
+	if (const auto thread = threadFor(topicRootId)) {
+		thread->cloudDraftTextCache().clear();
+		thread->updateChatListSortPosition();
 	}
-	const auto acceptAfter = savedAt + kSkipCloudDraftsFor;
-	_acceptCloudDraftsAfter = std::max(_acceptCloudDraftsAfter, acceptAfter);
+
+	return cloudDraft(topicRootId);
 }
 
-void History::applyCloudDraft() {
-	if (session().supportMode()) {
+bool History::skipCloudDraftUpdate(MsgId topicRootId, TimeId date) const {
+	const auto i = _acceptCloudDraftsAfter.find(topicRootId);
+	return _savingCloudDraftRequests.contains(topicRootId)
+		|| (i != _acceptCloudDraftsAfter.end() && date < i->second);
+}
+
+void History::startSavingCloudDraft(MsgId topicRootId) {
+	++_savingCloudDraftRequests[topicRootId];
+}
+
+void History::finishSavingCloudDraft(MsgId topicRootId, TimeId savedAt) {
+	const auto i = _savingCloudDraftRequests.find(topicRootId);
+	if (i != _savingCloudDraftRequests.end()) {
+		if (--i->second <= 0) {
+			_savingCloudDraftRequests.erase(i);
+		}
+	}
+	auto &after = _acceptCloudDraftsAfter[topicRootId];
+	after = std::max(after, savedAt + kSkipCloudDraftsFor);
+}
+
+void History::applyCloudDraft(MsgId topicRootId) {
+	if (!topicRootId && session().supportMode()) {
 		updateChatListEntry();
 		session().supportHelper().cloudDraftChanged(this);
 	} else {
-		createLocalDraftFromCloud();
-		updateChatListSortPosition();
-		session().changes().historyUpdated(this, UpdateFlag::CloudDraft);
+		createLocalDraftFromCloud(topicRootId);
+		if (const auto thread = threadFor(topicRootId)) {
+			thread->updateChatListSortPosition();
+			if (!topicRootId) {
+				session().changes().historyUpdated(
+					this,
+					UpdateFlag::CloudDraft);
+			} else {
+				session().changes().topicUpdated(
+					thread->asTopic(),
+					Data::TopicUpdate::Flag::CloudDraft);
+			}
+		}
 	}
 }
 
-void History::draftSavedToCloud() {
-	updateChatListEntry();
+void History::draftSavedToCloud(MsgId topicRootId) {
+	if (const auto thread = threadFor(topicRootId)) {
+		thread->updateChatListEntry();
+	}
 	session().local().writeDrafts(this);
 }
 
@@ -1143,20 +1175,22 @@ void History::newItemAdded(not_null<HistoryItem*> item) {
 	const auto stillShow = item->showNotification(); // Could be read already.
 	if (stillShow) {
 		Core::App().notifications().schedule(notification);
-		if (!item->out() && item->unread()) {
+	}
+	if (item->out()) {
+		destroyUnreadBar();
+		if (!item->unread(this)) {
+			outboxRead(item);
+		}
+	} else {
+		if (item->unread(this)) {
 			if (unreadCountKnown()) {
 				setUnreadCount(unreadCount() + 1);
 			} else {
 				owner().histories().requestDialogEntry(this);
 			}
+		} else {
+			inboxRead(item);
 		}
-	} else if (item->out()) {
-		destroyUnreadBar();
-	} else if (!item->unread()) {
-		inboxRead(item);
-	}
-	if (item->out() && !item->unread()) {
-		outboxRead(item);
 	}
 	item->incrementReplyToTopCounter();
 	if (!folderKnown()) {
@@ -1879,8 +1913,8 @@ void History::applyPinnedUpdate(const MTPDupdateDialogPinned &data) {
 
 TimeId History::adjustedChatListTimeId() const {
 	const auto result = chatListTimeId();
-	if (const auto draft = cloudDraft()) {
-		if (!Data::draftIsNull(draft) && !session().supportMode()) {
+	if (const auto draft = cloudDraft(MsgId(0))) {
+		if (!Data::DraftIsNull(draft) && !session().supportMode()) {
 			return std::max(result, draft->date);
 		}
 	}
@@ -2601,6 +2635,7 @@ void History::applyDialog(
 		Data::ApplyPeerCloudDraft(
 			&session(),
 			peer->id,
+			MsgId(0), // topicRootId
 			draft->c_draftMessage());
 	}
 	owner().histories().dialogEntryApplied(this);
@@ -2823,6 +2858,16 @@ void History::forceFullResize() {
 	_flags |= Flag::HasPendingResizedItems;
 }
 
+Data::Thread *History::threadFor(MsgId topicRootId) {
+	return topicRootId
+		? peer->forumTopicFor(topicRootId)
+		: static_cast<Data::Thread*>(this);
+}
+
+const Data::Thread *History::threadFor(MsgId topicRootId) const {
+	return const_cast<History*>(this)->threadFor(topicRootId);
+}
+
 not_null<History*> History::migrateToOrMe() const {
 	if (const auto to = peer->migrateTo()) {
 		return owner().history(to);
diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h
index 9bda7c26f..121d604fc 100644
--- a/Telegram/SourceFiles/history/history.h
+++ b/Telegram/SourceFiles/history/history.h
@@ -80,6 +80,8 @@ public:
 	not_null<History*> owningHistory() override {
 		return this;
 	}
+	[[nodiscard]] Data::Thread *threadFor(MsgId topicRootId);
+	[[nodiscard]] const Data::Thread *threadFor(MsgId topicRootId) const;
 
 	[[nodiscard]] auto delegateMixin() const
 			-> not_null<HistoryMainElementDelegateMixin*> {
@@ -229,12 +231,13 @@ public:
 	void inboxRead(not_null<const HistoryItem*> wasRead);
 	void outboxRead(MsgId upTo);
 	void outboxRead(not_null<const HistoryItem*> wasRead);
-	[[nodiscard]] bool isServerSideUnread(
-		not_null<const HistoryItem*> item) const;
 	[[nodiscard]] MsgId loadAroundId() const;
 	[[nodiscard]] MsgId inboxReadTillId() const;
 	[[nodiscard]] MsgId outboxReadTillId() const;
 
+	[[nodiscard]] bool isServerSideUnread(
+		not_null<const HistoryItem*> item) const override;
+
 	[[nodiscard]] bool trackUnreadMessages() const;
 	[[nodiscard]] int unreadCount() const;
 	[[nodiscard]] bool unreadCountKnown() const;
@@ -301,7 +304,7 @@ public:
 	void setHasPendingResizedItems();
 
 	[[nodiscard]] auto sendActionPainter()
-	-> not_null<HistoryView::SendActionPainter*> {
+	-> not_null<HistoryView::SendActionPainter*> override {
 		return &_sendActionPainter;
 	}
 
@@ -316,41 +319,51 @@ public:
 	[[nodiscard]] const Data::HistoryDrafts &draftsMap() const;
 	void setDraftsMap(Data::HistoryDrafts &&map);
 
-	Data::Draft *localDraft() const {
-		return draft(Data::DraftKey::Local());
+	Data::Draft *localDraft(MsgId topicRootId) const {
+		return draft(Data::DraftKey::Local(topicRootId));
 	}
-	Data::Draft *localEditDraft() const {
-		return draft(Data::DraftKey::LocalEdit());
+	Data::Draft *localEditDraft(MsgId topicRootId) const {
+		return draft(Data::DraftKey::LocalEdit(topicRootId));
 	}
-	Data::Draft *cloudDraft() const {
-		return draft(Data::DraftKey::Cloud());
+	Data::Draft *cloudDraft(MsgId topicRootId) const {
+		return draft(Data::DraftKey::Cloud(topicRootId));
 	}
 	void setLocalDraft(std::unique_ptr<Data::Draft> &&draft) {
-		setDraft(Data::DraftKey::Local(), std::move(draft));
+		setDraft(
+			Data::DraftKey::Local(draft->topicRootId),
+			std::move(draft));
 	}
 	void setLocalEditDraft(std::unique_ptr<Data::Draft> &&draft) {
-		setDraft(Data::DraftKey::LocalEdit(), std::move(draft));
+		setDraft(
+			Data::DraftKey::LocalEdit(draft->topicRootId),
+			std::move(draft));
 	}
 	void setCloudDraft(std::unique_ptr<Data::Draft> &&draft) {
-		setDraft(Data::DraftKey::Cloud(), std::move(draft));
+		setDraft(
+			Data::DraftKey::Cloud(draft->topicRootId),
+			std::move(draft));
 	}
-	void clearLocalDraft() {
-		clearDraft(Data::DraftKey::Local());
+	void clearLocalDraft(MsgId topicRootId) {
+		clearDraft(Data::DraftKey::Local(topicRootId));
 	}
-	void clearCloudDraft() {
-		clearDraft(Data::DraftKey::Cloud());
+	void clearCloudDraft(MsgId topicRootId) {
+		clearDraft(Data::DraftKey::Cloud(topicRootId));
 	}
-	void clearLocalEditDraft() {
-		clearDraft(Data::DraftKey::LocalEdit());
+	void clearLocalEditDraft(MsgId topicRootId) {
+		clearDraft(Data::DraftKey::LocalEdit(topicRootId));
 	}
 	void clearDrafts();
-	Data::Draft *createCloudDraft(const Data::Draft *fromDraft);
-	bool skipCloudDraftUpdate(TimeId date) const;
-	void startSavingCloudDraft();
-	void finishSavingCloudDraft(TimeId savedAt);
+	Data::Draft *createCloudDraft(
+		MsgId topicRootId,
+		const Data::Draft *fromDraft);
+	[[nodiscard]] bool skipCloudDraftUpdate(
+		MsgId topicRootId,
+		TimeId date) const;
+	void startSavingCloudDraft(MsgId topicRootId);
+	void finishSavingCloudDraft(MsgId topicRootId, TimeId savedAt);
 	void takeLocalDraft(not_null<History*> from);
-	void applyCloudDraft();
-	void draftSavedToCloud();
+	void applyCloudDraft(MsgId topicRootId);
+	void draftSavedToCloud(MsgId topicRootId);
 
 	[[nodiscard]] const Data::ForwardDraft &forwardDraft() const {
 		return _forwardDraft;
@@ -553,7 +566,7 @@ private:
 
 	void viewReplaced(not_null<const Element*> was, Element *now);
 
-	void createLocalDraftFromCloud();
+	void createLocalDraftFromCloud(MsgId topicRootId);
 
 	HistoryService *insertJoinedMessage();
 	void insertMessageToBlocks(not_null<HistoryItem*> item);
@@ -603,8 +616,8 @@ private:
 	std::unique_ptr<BuildingBlock> _buildingFrontBlock;
 
 	Data::HistoryDrafts _drafts;
-	TimeId _acceptCloudDraftsAfter = 0;
-	int _savingCloudDraftRequests = 0;
+	base::flat_map<MsgId, TimeId> _acceptCloudDraftsAfter;
+	base::flat_map<MsgId, int> _savingCloudDraftRequests;
 	Data::ForwardDraft _forwardDraft;
 
 	QString _topPromotedMessage;
diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp
index 299a75c52..24ca77c40 100644
--- a/Telegram/SourceFiles/history/history_inner_widget.cpp
+++ b/Telegram/SourceFiles/history/history_inner_widget.cpp
@@ -227,7 +227,7 @@ public:
 		return false;
 	}
 	bool elementShownUnread(not_null<const Element*> view) override {
-		return view->data()->unread();
+		return view->data()->unread(view->data()->history());
 	}
 	void elementSendBotCommand(
 			const QString &command,
@@ -983,7 +983,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
 		const auto item = view->data();
 		const auto isSponsored = item->isSponsored();
 		const auto isUnread = !item->out()
-			&& item->unread()
+			&& item->unread(_history)
 			&& (item->history() == _history);
 		const auto withReaction = item->hasUnreadReaction();
 		const auto yShown = [&](int y) {
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 12b4abca8..fe62d0bb0 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -1217,22 +1217,22 @@ bool HistoryItem::needCheck() const {
 	return (out() && !isEmpty()) || (!isRegular() && history()->peer->isSelf());
 }
 
-bool HistoryItem::unread() const {
+bool HistoryItem::unread(not_null<Data::Thread*> thread) const {
 	// Messages from myself are always read, unless scheduled.
 	if (history()->peer->isSelf() && !isFromScheduled()) {
 		return false;
 	}
 
-	if (out()) {
-		// Outgoing messages in converted chats are always read.
-		if (history()->peer->migrateTo()) {
+	// All messages in converted chats are always read.
+	if (history()->peer->migrateTo()) {
+		return false;
+	}
+
+	if (isRegular()) {
+		if (!thread->isServerSideUnread(this)) {
 			return false;
 		}
-
-		if (isRegular()) {
-			if (!history()->isServerSideUnread(this)) {
-				return false;
-			}
+		if (out()) {
 			if (const auto user = history()->peer->asUser()) {
 				if (user->isBot() && !user->isSupport()) {
 					return false;
@@ -1246,13 +1246,7 @@ bool HistoryItem::unread() const {
 		return true;
 	}
 
-	if (isRegular()) {
-		if (!history()->isServerSideUnread(this)) {
-			return false;
-		}
-		return true;
-	}
-	return (_flags & MessageFlag::ClientSideUnread);
+	return out() || (_flags & MessageFlag::ClientSideUnread);
 }
 
 bool HistoryItem::showNotification() const {
@@ -1262,7 +1256,7 @@ bool HistoryItem::showNotification() const {
 	}
 	return (out() || _history->peer->isSelf())
 		? isFromScheduled()
-		: unread();
+		: unread(notificationThread());
 }
 
 void HistoryItem::markClientSideAsRead() {
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index 053f69629..504db9fe1 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -150,7 +150,7 @@ public:
 	[[nodiscard]] bool isPinned() const {
 		return _flags & MessageFlag::Pinned;
 	}
-	[[nodiscard]] bool unread() const;
+	[[nodiscard]] bool unread(not_null<Data::Thread*> thread) const;
 	[[nodiscard]] bool showNotification() const;
 	void markClientSideAsRead();
 	[[nodiscard]] bool mentionsMe() const;
diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp
index bdde8a8df..cf6d6457b 100644
--- a/Telegram/SourceFiles/history/history_message.cpp
+++ b/Telegram/SourceFiles/history/history_message.cpp
@@ -1773,6 +1773,9 @@ void HistoryMessage::setReplyFields(
 		&& !IsServerMsgId(reply->replyToMsgTop)) {
 		reply->replyToMsgTop = replyToTop;
 		changeReplyToTopCounter(reply, 1);
+		if (const auto topic = this->topic()) {
+			topic->maybeSetLastMessage(this);
+		}
 	}
 }
 
diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp
index ac209140f..bbfcc1272 100644
--- a/Telegram/SourceFiles/history/history_service.cpp
+++ b/Telegram/SourceFiles/history/history_service.cpp
@@ -1342,10 +1342,19 @@ void HistoryService::setReplyFields(
 		MsgId replyToTop,
 		bool isForumPost) {
 	const auto data = GetDependentData();
-	if (!data || IsServerMsgId(data->topId) || isScheduled()) {
+	if (!data
+		|| (data->topId == replyToTop)
+		|| IsServerMsgId(data->topId)
+		|| isScheduled()) {
 		return;
 	}
 	data->topId = replyToTop;
+	if (isForumPost) {
+		data->topicPost = true;
+	}
+	if (const auto topic = this->topic()) {
+		topic->maybeSetLastMessage(this);
+	}
 }
 
 std::unique_ptr<HistoryView::Element> HistoryService::createView(
diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp
index d9ed05694..0f80dc1ab 100644
--- a/Telegram/SourceFiles/history/history_widget.cpp
+++ b/Telegram/SourceFiles/history/history_widget.cpp
@@ -948,7 +948,7 @@ void HistoryWidget::initVoiceRecordBar() {
 	});
 
 	const auto applyLocalDraft = [=] {
-		if (_history && _history->localDraft()) {
+		if (_history && _history->localDraft({})) {
 			applyDraft();
 		}
 	};
@@ -1657,10 +1657,12 @@ void HistoryWidget::saveDraft(bool delayed) {
 void HistoryWidget::saveFieldToHistoryLocalDraft() {
 	if (!_history) return;
 
+	const auto topicRootId = MsgId();
 	if (_editMsgId) {
 		_history->setLocalEditDraft(std::make_unique<Data::Draft>(
 			_field,
 			_editMsgId,
+			topicRootId,
 			_previewState,
 			_saveEditMsgRequestId));
 	} else {
@@ -1668,11 +1670,12 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() {
 			_history->setLocalDraft(std::make_unique<Data::Draft>(
 				_field,
 				_replyToId,
+				topicRootId,
 				_previewState));
 		} else {
-			_history->clearLocalDraft();
+			_history->clearLocalDraft({});
 		}
-		_history->clearLocalEditDraft();
+		_history->clearLocalEditDraft({});
 	}
 }
 
@@ -1760,7 +1763,8 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query, U
 			MessageCursor cursor = { int(textWithTags.text.size()), int(textWithTags.text.size()), QFIXED_MAX };
 			_history->setLocalDraft(std::make_unique<Data::Draft>(
 				textWithTags,
-				0,
+				0, // replyTo
+				0, // topicRootId
 				cursor,
 				Data::PreviewState::Allowed));
 			applyDraft();
@@ -1782,21 +1786,19 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query, U
 		auto draft = std::make_unique<Data::Draft>(
 			textWithTags,
 			to.currentReplyToId,
+			to.rootId,
 			cursor,
 			Data::PreviewState::Allowed);
 
-		if (to.section == Section::Replies) {
-			history->setDraft(
-				Data::DraftKey::Replies(to.rootId),
-				std::move(draft));
-			controller()->showRepliesForMessage(history, to.rootId);
-		} else if (to.section == Section::Scheduled) {
+		if (to.section == Section::Scheduled) {
 			history->setDraft(Data::DraftKey::Scheduled(), std::move(draft));
 			controller()->showSection(
 				std::make_shared<HistoryView::ScheduledMemento>(history));
 		} else {
 			history->setLocalDraft(std::move(draft));
-			if (history == _history) {
+			if (to.section == Section::Replies) {
+				controller()->showRepliesForMessage(history, to.rootId);
+			} else if (history == _history) {
 				applyDraft();
 			} else {
 				controller()->showPeerHistory(history->peer);
@@ -1878,13 +1880,14 @@ void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
 		return;
 	}
 
-	auto draft = !_history
-		? nullptr
-		: _history->localEditDraft()
-		? _history->localEditDraft()
-		: _history->localDraft();
+	const auto editDraft = _history ? _history->localEditDraft({}) : nullptr;
+	const auto draft = editDraft
+		? editDraft
+		: _history
+		? _history->localDraft({})
+		: nullptr;
 	auto fieldAvailable = canWriteMessage();
-	if (!draft || (!_history->localEditDraft() && !fieldAvailable)) {
+	if (!draft || (!_history->localEditDraft({}) && !fieldAvailable)) {
 		auto fieldWillBeHiddenAfterEdit = (!fieldAvailable && _editMsgId != 0);
 		clearFieldText(0, fieldHistoryAction);
 		_field->setFocus();
@@ -1913,11 +1916,11 @@ void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
 	_previewState = draft->previewState;
 
 	_replyEditMsg = nullptr;
-	if (const auto editDraft = _history->localEditDraft()) {
+	if (const auto editDraft = _history->localEditDraft({})) {
 		setEditMsgId(editDraft->msgId);
 		_replyToId = 0;
 	} else {
-		_replyToId = readyToForward() ? 0 : _history->localDraft()->msgId;
+		_replyToId = readyToForward() ? 0 : _history->localDraft({})->msgId;
 		setEditMsgId(0);
 	}
 	updateCmdStartShown();
@@ -2036,7 +2039,7 @@ void HistoryWidget::showHistory(
 							info->inlineReturnTo = wasDialogsEntryState;
 						}
 						sendBotStartCommand();
-						_history->clearLocalDraft();
+						_history->clearLocalDraft({});
 						applyDraft();
 						_send->finishAnimating();
 					}
@@ -2356,10 +2359,10 @@ void HistoryWidget::unregisterDraftSources() {
 	}
 	session().local().unregisterDraftSource(
 		_history,
-		Data::DraftKey::Local());
+		Data::DraftKey::Local({}));
 	session().local().unregisterDraftSource(
 		_history,
-		Data::DraftKey::LocalEdit());
+		Data::DraftKey::LocalEdit({}));
 }
 
 void HistoryWidget::registerDraftSource() {
@@ -2380,7 +2383,9 @@ void HistoryWidget::registerDraftSource() {
 	};
 	session().local().registerDraftSource(
 		_history,
-		editMsgId ? Data::DraftKey::LocalEdit() : Data::DraftKey::Local(),
+		(editMsgId
+			? Data::DraftKey::LocalEdit({})
+			: Data::DraftKey::Local({})),
 		std::move(draftSource));
 }
 
@@ -3564,16 +3569,16 @@ void HistoryWidget::saveEditMsg() {
 				cancelEdit();
 			}
 		})();
-		if (const auto editDraft = history->localEditDraft()) {
+		if (const auto editDraft = history->localEditDraft({})) {
 			if (editDraft->saveRequestId == requestId) {
-				history->clearLocalEditDraft();
+				history->clearLocalEditDraft({});
 				history->session().local().writeDrafts(history);
 			}
 		}
 	};
 
 	const auto fail = [=](const QString &error, mtpRequestId requestId) {
-		if (const auto editDraft = history->localEditDraft()) {
+		if (const auto editDraft = history->localEditDraft({})) {
 			if (editDraft->saveRequestId == requestId) {
 				editDraft->saveRequestId = 0;
 			}
@@ -3648,6 +3653,7 @@ Api::SendAction HistoryWidget::prepareSendAction(
 		Api::SendOptions options) const {
 	auto result = Api::SendAction(_history, options);
 	result.replyTo = replyToId();
+	result.topicRootId = 0;
 	result.options.sendAs = _sendAs
 		? _history->session().sendAsPeers().resolveChosen(
 			_history->peer).get()
@@ -6650,12 +6656,13 @@ void HistoryWidget::replyToMessage(not_null<HistoryItem*> item) {
 	}
 
 	if (_editMsgId) {
-		if (auto localDraft = _history->localDraft()) {
+		if (const auto localDraft = _history->localDraft({})) {
 			localDraft->msgId = item->id;
 		} else {
 			_history->setLocalDraft(std::make_unique<Data::Draft>(
 				TextWithTags(),
 				item->id,
+				MsgId(), // topicRootId
 				MessageCursor(),
 				Data::PreviewState::Allowed));
 		}
@@ -6708,9 +6715,10 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
 			_history->setLocalDraft(std::make_unique<Data::Draft>(
 				_field,
 				_replyToId,
+				MsgId(), // topicRootId
 				_previewState));
 		} else {
-			_history->clearLocalDraft();
+			_history->clearLocalDraft({});
 		}
 	}
 
@@ -6732,6 +6740,7 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
 	_history->setLocalEditDraft(std::make_unique<Data::Draft>(
 		editData,
 		item->id,
+		MsgId(), // topicRootId
 		cursor,
 		previewState));
 	applyDraft();
@@ -6811,10 +6820,10 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) {
 		refreshTopBarActiveChat();
 		updateControlsGeometry();
 		update();
-	} else if (auto localDraft = (_history ? _history->localDraft() : nullptr)) {
+	} else if (const auto localDraft = (_history ? _history->localDraft({}) : nullptr)) {
 		if (localDraft->msgId) {
 			if (localDraft->textWithTags.text.isEmpty()) {
-				_history->clearLocalDraft();
+				_history->clearLocalDraft({});
 			} else {
 				localDraft->msgId = 0;
 			}
@@ -6856,7 +6865,7 @@ void HistoryWidget::cancelEdit() {
 
 	_replyEditMsg = nullptr;
 	setEditMsgId(0);
-	_history->clearLocalEditDraft();
+	_history->clearLocalEditDraft({});
 	applyDraft();
 
 	if (_saveEditMsgRequestId) {
diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
index 670fd2b25..bd1e22ded 100644
--- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
+++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
@@ -1169,16 +1169,18 @@ void ComposeControls::setFieldText(
 
 void ComposeControls::saveFieldToHistoryLocalDraft() {
 	const auto key = draftKeyCurrent();
-	if (!_history || key == Data::DraftKey::None()) {
+	if (!_history || !key) {
 		return;
 	}
 	const auto id = _header->getDraftMessageId();
 	if (_preview && (id || !_field->empty())) {
+		const auto key = draftKeyCurrent();
 		_history->setDraft(
-			draftKeyCurrent(),
+			key,
 			std::make_unique<Data::Draft>(
 				_field,
 				_header->getDraftMessageId(),
+				key.topicRootId(),
 				_preview->state()));
 	} else {
 		_history->clearDraft(draftKeyCurrent());
@@ -1680,15 +1682,14 @@ Data::DraftKey ComposeControls::draftKey(DraftType type) const {
 
 	switch (_currentDialogsEntryState.section) {
 	case Section::History:
-		return (type == DraftType::Edit) ? Key::LocalEdit() : Key::Local();
+	case Section::Replies:
+		return (type == DraftType::Edit)
+			? Key::LocalEdit(_currentDialogsEntryState.rootId)
+			: Key::Local(_currentDialogsEntryState.rootId);
 	case Section::Scheduled:
 		return (type == DraftType::Edit)
 			? Key::ScheduledEdit()
 			: Key::Scheduled();
-	case Section::Replies:
-		return (type == DraftType::Edit)
-			? Key::RepliesEdit(_currentDialogsEntryState.rootId)
-			: Key::Replies(_currentDialogsEntryState.rootId);
 	}
 	return Key::None();
 }
@@ -2380,11 +2381,13 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) {
 	const auto previewState = previewPage
 		? Data::PreviewState::Allowed
 		: Data::PreviewState::EmptyOnEdit;
+	const auto key = draftKey(DraftType::Edit);
 	_history->setDraft(
-		draftKey(DraftType::Edit),
+		key,
 		std::make_unique<Data::Draft>(
 			editData,
 			item->id,
+			key.topicRootId(),
 			cursor,
 			previewState));
 	applyDraft();
@@ -2424,6 +2427,7 @@ void ComposeControls::replyToMessage(FullMsgId id) {
 				std::make_unique<Data::Draft>(
 					TextWithTags(),
 					id.msg,
+					key.topicRootId(),
 					MessageCursor(),
 					Data::PreviewState::Allowed));
 		}
@@ -2438,7 +2442,6 @@ void ComposeControls::replyToMessage(FullMsgId id) {
 
 void ComposeControls::cancelReplyMessage() {
 	Expects(_history != nullptr);
-	Expects(draftKeyCurrent() != Data::DraftKey::None());
 
 	const auto wasReply = replyingToMessage();
 	_header->replyToMessage({});
diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp
index 104663634..1211d83d7 100644
--- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp
@@ -645,7 +645,7 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
 	if (forwarded && forwarded->imported) {
 		result.flags |= Flag::Imported;
 	}
-	// We don't want to pass and update it in Date for now.
+	// We don't want to pass and update it in Data for now.
 	//if (item->unread()) {
 	//	result.flags |= Flag::Unread;
 	//}
diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp
index 7ec29fb01..d6a307feb 100644
--- a/Telegram/SourceFiles/history/view/history_view_element.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_element.cpp
@@ -185,7 +185,7 @@ bool SimpleElementDelegate::elementHideReply(not_null<const Element*> view) {
 
 bool SimpleElementDelegate::elementShownUnread(
 		not_null<const Element*> view) {
-	return view->data()->unread();
+	return view->data()->unread(view->data()->history());
 }
 
 void SimpleElementDelegate::elementSendBotCommand(
diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
index 8e4ff214f..2ce5e65f1 100644
--- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp
@@ -553,7 +553,7 @@ bool PinnedWidget::listElementHideReply(not_null<const Element*> view) {
 }
 
 bool PinnedWidget::listElementShownUnread(not_null<const Element*> view) {
-	return view->data()->unread();
+	return view->data()->unread(view->data()->history());
 }
 
 bool PinnedWidget::listIsGoodForAroundPosition(
diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
index 46a087875..b70c16332 100644
--- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp
@@ -351,8 +351,8 @@ RepliesWidget::RepliesWidget(
 }
 
 RepliesWidget::~RepliesWidget() {
-	if (_topic && _topic->forum()->creating(_rootId)) {
-		_topic->forum()->discardCreatingId(_rootId);
+	if (_topic && _topic->creating()) {
+		_topic->discard();
 		_topic = nullptr;
 	}
 	base::take(_sendAction);
@@ -434,15 +434,20 @@ void RepliesWidget::setupRootView() {
 }
 
 void RepliesWidget::setupTopicViewer() {
-	_history->owner().itemIdChanged(
+	const auto owner = &_history->owner();
+	owner->itemIdChanged(
 	) | rpl::start_with_next([=](const Data::Session::IdChange &change) {
 		if (_rootId == change.oldId) {
 			_rootId = change.newId.msg;
+			_sendAction = owner->sendActionManager().repliesPainter(
+				_history,
+				_rootId);
 			_root = lookupRoot();
 			if (_topic && _topic->rootId() == change.oldId) {
 				setTopic(_topic->forum()->topicFor(change.newId.msg));
 			} else {
 				refreshReplies();
+				refreshTopBarActiveChat();
 			}
 			_inner->update();
 		}
@@ -1007,6 +1012,7 @@ Api::SendAction RepliesWidget::prepareSendAction(
 		Api::SendOptions options) const {
 	auto result = Api::SendAction(_history, options);
 	result.replyTo = replyToId();
+	result.topicRootId = _rootId;
 	result.options.sendAs = _composeControls->sendAsPeer();
 	return result;
 }
@@ -1440,7 +1446,7 @@ bool RepliesWidget::preventsClose(Fn<void()> &&continueCallback) const {
 		return true;
 	} else if (!_newTopicDiscarded
 		&& _topic
-		&& _topic->forum()->creating(_rootId)) {
+		&& _topic->creating()) {
 		const auto weak = Ui::MakeWeak(this);
 		auto sure = [=](Fn<void()> &&close) {
 			if (const auto strong = weak.data()) {
@@ -1943,14 +1949,7 @@ bool RepliesWidget::listElementHideReply(not_null<const Element*> view) {
 }
 
 bool RepliesWidget::listElementShownUnread(not_null<const Element*> view) {
-	if (!_root) {
-		return false;
-	}
-	const auto item = view->data();
-	const auto till = item->out()
-		? _replies->computeOutboxReadTillFull()
-		: _replies->computeInboxReadTillFull();
-	return (item->id > till);
+	return _replies->isServerSideUnread(view->data());
 }
 
 bool RepliesWidget::listIsGoodForAroundPosition(
diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.cpp b/Telegram/SourceFiles/history/view/history_view_send_action.cpp
index dfb941166..5380ccfb5 100644
--- a/Telegram/SourceFiles/history/view/history_view_send_action.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_send_action.cpp
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 
 #include "data/data_user.h"
 #include "data/data_send_action.h"
+#include "data/data_forum_topic.h"
 #include "data/data_session.h"
 #include "main/main_session.h"
 #include "history/history.h"
@@ -39,13 +40,20 @@ constexpr auto kStatusShowClientsideSpeaking = 6 * crl::time(1000);
 
 } // namespace
 
-SendActionPainter::SendActionPainter(not_null<History*> history)
+SendActionPainter::SendActionPainter(
+	not_null<History*> history,
+	MsgId rootId)
 : _history(history)
+, _rootId(rootId)
 , _weak(&_history->session())
 , _st(st::dialogsTextStyle)
 , _sendActionText(st::dialogsTextWidthMin) {
 }
 
+void SendActionPainter::setTopic(Data::ForumTopic *topic) {
+	_topic = topic;
+}
+
 bool SendActionPainter::updateNeedsAnimating(
 		not_null<UserData*> user,
 		const MTPSendMessageAction &action) {
@@ -382,7 +390,7 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
 			st::normalFont->height,
 			st::dialogsMiniPreviewTop + st::dialogsMiniPreview);
 		_history->peer->owner().sendActionManager().updateAnimation({
-			_history,
+			_topic ? ((Data::Thread*)_topic) : _history,
 			0,
 			_sendActionAnimation.width() + _animationLeft,
 			height,
diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.h b/Telegram/SourceFiles/history/view/history_view_send_action.h
index 03ae7cede..2a0e2e6b3 100644
--- a/Telegram/SourceFiles/history/view/history_view_send_action.h
+++ b/Telegram/SourceFiles/history/view/history_view_send_action.h
@@ -16,6 +16,10 @@ namespace Main {
 class Session;
 } // namespace Main
 
+namespace Data {
+class ForumTopic;
+} // namespace Data
+
 namespace Api {
 enum class SendProgressType;
 struct SendProgress;
@@ -25,7 +29,9 @@ namespace HistoryView {
 
 class SendActionPainter final {
 public:
-	explicit SendActionPainter(not_null<History*> history);
+	explicit SendActionPainter(not_null<History*> history, MsgId rootId = 0);
+
+	void setTopic(Data::ForumTopic *topic);
 
 	bool paint(
 		Painter &p,
@@ -53,8 +59,10 @@ public:
 
 private:
 	const not_null<History*> _history;
+	const MsgId _rootId = 0;
 	const base::weak_ptr<Main::Session> _weak;
 	const style::TextStyle &_st;
+	Data::ForumTopic *_topic = nullptr;
 	base::flat_map<not_null<UserData*>, crl::time> _typing;
 	base::flat_map<not_null<UserData*>, crl::time> _speaking;
 	base::flat_map<not_null<UserData*>, Api::SendProgress> _sendActions;
diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
index 001cb9ed2..a0ac7262a 100644
--- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
@@ -152,7 +152,7 @@ TopBarWidget::TopBarWidget(
 		using AnimationUpdate = Data::SendActionManager::AnimationUpdate;
 		session().data().sendActionManager().animationUpdated(
 		) | rpl::filter([=](const AnimationUpdate &update) {
-			return (update.history == _activeChat.key.history());
+			return (update.thread == _activeChat.key.thread());
 		}) | rpl::start_with_next([=] {
 			update();
 		}, lifetime());
@@ -722,6 +722,7 @@ void TopBarWidget::backClicked() {
 void TopBarWidget::setActiveChat(
 		ActiveChat activeChat,
 		SendActionPainter *sendAction) {
+	_sendAction = sendAction;
 	if (_activeChat.key == activeChat.key
 		&& _activeChat.section == activeChat.section) {
 		_activeChat = activeChat;
@@ -733,7 +734,6 @@ void TopBarWidget::setActiveChat(
 		!= activeChat.key.history());
 
 	_activeChat = activeChat;
-	_sendAction = sendAction;
 	_titlePeerText.clear();
 	_back->clearState();
 	update();
diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp
index d33098b7d..e6cf0add8 100644
--- a/Telegram/SourceFiles/info/info_controller.cpp
+++ b/Telegram/SourceFiles/info/info_controller.cpp
@@ -204,29 +204,35 @@ void Controller::setupMigrationViewer() {
 	) | rpl::filter([=] {
 		return peer->migrateTo() || (peer->migrateFrom() != _migrated);
 	}) | rpl::start_with_next([=] {
-		const auto window = parentController();
-		const auto section = _section;
-		auto params = Window::SectionShow(
-			Window::SectionShow::Way::Backward,
-			anim::type::instant,
-			anim::activation::background);
-		if (wrap() == Wrap::Side) {
-			params.thirdColumn = true;
-		}
-		InvokeQueued(_widget, [=] {
-			window->showSection(
-				std::make_shared<Memento>(peer, section),
-				params);
-		});
+		replaceWith(std::make_shared<Memento>(peer, _section));
 	}, lifetime());
 }
 
+void Controller::replaceWith(std::shared_ptr<Memento> memento) {
+	const auto window = parentController();
+	const auto section = _section;
+	auto params = Window::SectionShow(
+		Window::SectionShow::Way::Backward,
+		anim::type::instant,
+		anim::activation::background);
+	if (wrap() == Wrap::Side) {
+		params.thirdColumn = true;
+	}
+	InvokeQueued(_widget, [=, memento = std::move(memento)]() mutable {
+		window->showSection(std::move(memento), params);
+	});
+}
+
 void Controller::setupTopicViewer() {
 	session().data().itemIdChanged(
 	) | rpl::start_with_next([=](const Data::Session::IdChange &change) {
 		if (const auto topic = _key.topic()) {
-			if (topic->rootId() == change.oldId) {
-				_key = Key(topic->forum()->topicFor(change.newId.msg));
+			if (topic->rootId() == change.oldId
+				|| (topic->peer()->id == change.newId.peer
+					&& topic->rootId() == change.newId.msg)) {
+				const auto now = topic->forum()->topicFor(change.newId.msg);
+				_key = Key(now);
+				replaceWith(std::make_shared<Memento>(now, _section));
 			}
 		}
 	}, _lifetime);
diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h
index e1dae0794..cdbf53396 100644
--- a/Telegram/SourceFiles/info/info_controller.h
+++ b/Telegram/SourceFiles/info/info_controller.h
@@ -240,6 +240,8 @@ private:
 	void setupMigrationViewer();
 	void setupTopicViewer();
 
+	void replaceWith(std::shared_ptr<Memento> memento);
+
 	not_null<WrapWidget*> _widget;
 	Key _key;
 	PeerData *_migrated = nullptr;
diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
index 71071689c..acc5ace7b 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
@@ -577,6 +577,8 @@ Ui::MultiSlideTracker DetailsFiller::fillChannelButtons(
 }
 
 object_ptr<Ui::RpWidget> DetailsFiller::fill() {
+	Expects(!_topic || !_topic->creating());
+
 	add(object_ptr<Ui::BoxContentDivider>(_wrap));
 	add(CreateSkipWidget(_wrap));
 	add(setupInfo());
@@ -585,6 +587,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() {
 	}
 	setupMainButtons();
 	add(CreateSkipWidget(_wrap));
+
 	return std::move(_wrap);
 }
 
diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
index 73fbbdd3d..d9f22fe6f 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
@@ -83,6 +83,9 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent(
 	}, _cover->lifetime());
 	_cover->setOnlineCount(rpl::single(0));
 	if (_topic) {
+		if (_topic->creating()) {
+			return result;
+		}
 		result->add(SetupDetails(_controller, parent, _topic));
 	} else {
 		result->add(SetupDetails(_controller, parent, _peer));
diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp
index 8488924c7..abbcf0201 100644
--- a/Telegram/SourceFiles/mainwidget.cpp
+++ b/Telegram/SourceFiles/mainwidget.cpp
@@ -555,12 +555,14 @@ bool MainWidget::shareUrl(
 		QFIXED_MAX
 	};
 	const auto history = peer->owner().history(peer);
+	const auto topicRootId = 0;
 	history->setLocalDraft(std::make_unique<Data::Draft>(
 		textWithTags,
-		0,
+		0, // replyTo
+		topicRootId,
 		cursor,
 		Data::PreviewState::Allowed));
-	history->clearLocalEditDraft();
+	history->clearLocalEditDraft(topicRootId);
 	history->session().changes().historyUpdated(
 		history,
 		Data::HistoryUpdate::Flag::LocalDraftSet);
@@ -587,12 +589,14 @@ bool MainWidget::inlineSwitchChosen(
 		int(botAndQuery.size()),
 		QFIXED_MAX
 	};
+	const auto topicRootId = 0;
 	h->setLocalDraft(std::make_unique<Data::Draft>(
 		textWithTags,
-		0,
+		0, // replyTo
+		topicRootId,
 		cursor,
 		Data::PreviewState::Allowed));
-	h->clearLocalEditDraft();
+	h->clearLocalEditDraft(topicRootId);
 	h->session().changes().historyUpdated(
 		h,
 		Data::HistoryUpdate::Flag::LocalDraftSet);
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp
index 5ec8d0aba..ff27d2159 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp
@@ -1587,7 +1587,7 @@ void FormController::uploadEncryptedFile(
 	auto prepared = std::make_shared<FileLoadResult>(
 		TaskId(),
 		file.uploadData->fileId,
-		FileLoadTo(PeerId(), Api::SendOptions(), MsgId(), MsgId()),
+		FileLoadTo(PeerId(), Api::SendOptions(), MsgId(), MsgId(), MsgId()),
 		TextWithTags(),
 		std::shared_ptr<SendingAlbum>(nullptr));
 	prepared->type = SendMediaType::Secure;
diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h
index d45245033..c02b87c7c 100644
--- a/Telegram/SourceFiles/storage/localimageloader.h
+++ b/Telegram/SourceFiles/storage/localimageloader.h
@@ -200,15 +200,18 @@ struct FileLoadTo {
 		PeerId peer,
 		Api::SendOptions options,
 		MsgId replyTo,
+		MsgId topicRootId,
 		MsgId replaceMediaOf)
 	: peer(peer)
 	, options(options)
 	, replyTo(replyTo)
+	, topicRootId(topicRootId)
 	, replaceMediaOf(replaceMediaOf) {
 	}
 	PeerId peer;
 	Api::SendOptions options;
 	MsgId replyTo;
+	MsgId topicRootId;
 	MsgId replaceMediaOf;
 };
 
diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp
index 22d66ac1e..578dcc348 100644
--- a/Telegram/SourceFiles/storage/storage_account.cpp
+++ b/Telegram/SourceFiles/storage/storage_account.cpp
@@ -1025,17 +1025,20 @@ std::unique_ptr<MTP::Config> Account::readMtpConfig() {
 template <typename Callback>
 void EnumerateDrafts(
 		const Data::HistoryDrafts &map,
-		Data::Draft *cloudDraft,
 		bool supportMode,
 		const base::flat_map<Data::DraftKey, MessageDraftSource> &sources,
 		Callback &&callback) {
 	for (const auto &[key, draft] : map) {
-		if (key == Data::DraftKey::Cloud() || sources.contains(key)) {
-			continue;
-		} else if (key == Data::DraftKey::Local()
-			&& !supportMode
-			&& Data::draftsAreEqual(draft.get(), cloudDraft)) {
+		if (key.isCloud() || sources.contains(key)) {
 			continue;
+		} else if (key.isLocal()
+			&& (!supportMode || key.topicRootId())) {
+			const auto i = map.find(
+				Data::DraftKey::Cloud(key.topicRootId()));
+			const auto cloud = (i != end(map)) ? i->second.get() : nullptr;
+			if (Data::DraftsAreEqual(draft.get(), cloud)) {
+				continue;
+			}
 		}
 		callback(
 			key,
@@ -1085,10 +1088,6 @@ void Account::unregisterDraftSource(
 void Account::writeDrafts(not_null<History*> history) {
 	const auto peerId = history->peer->id;
 	const auto &map = history->draftsMap();
-	const auto cloudIt = map.find(Data::DraftKey::Cloud());
-	const auto cloudDraft = (cloudIt != end(map))
-		? cloudIt->second.get()
-		: nullptr;
 	const auto supportMode = _owner->session().supportMode();
 	const auto sourcesIt = _draftSources.find(history);
 	const auto &sources = (sourcesIt != _draftSources.end())
@@ -1097,7 +1096,6 @@ void Account::writeDrafts(not_null<History*> history) {
 	auto count = 0;
 	EnumerateDrafts(
 		map,
-		cloudDraft,
 		supportMode,
 		sources,
 		[&](auto&&...) { ++count; });
@@ -1133,7 +1131,6 @@ void Account::writeDrafts(not_null<History*> history) {
 	};
 	EnumerateDrafts(
 		map,
-		cloudDraft,
 		supportMode,
 		sources,
 		sizeCallback);
@@ -1159,7 +1156,6 @@ void Account::writeDrafts(not_null<History*> history) {
 	};
 	EnumerateDrafts(
 		map,
-		cloudDraft,
 		supportMode,
 		sources,
 		writeCallback);
@@ -1173,10 +1169,6 @@ void Account::writeDrafts(not_null<History*> history) {
 void Account::writeDraftCursors(not_null<History*> history) {
 	const auto peerId = history->peer->id;
 	const auto &map = history->draftsMap();
-	const auto cloudIt = map.find(Data::DraftKey::Cloud());
-	const auto cloudDraft = (cloudIt != end(map))
-		? cloudIt->second.get()
-		: nullptr;
 	const auto supportMode = _owner->session().supportMode();
 	const auto sourcesIt = _draftSources.find(history);
 	const auto &sources = (sourcesIt != _draftSources.end())
@@ -1185,7 +1177,6 @@ void Account::writeDraftCursors(not_null<History*> history) {
 	auto count = 0;
 	EnumerateDrafts(
 		map,
-		cloudDraft,
 		supportMode,
 		sources,
 		[&](auto&&...) { ++count; });
@@ -1223,7 +1214,6 @@ void Account::writeDraftCursors(not_null<History*> history) {
 	};
 	EnumerateDrafts(
 		map,
-		cloudDraft,
 		supportMode,
 		sources,
 		writeCallback);
@@ -1282,7 +1272,7 @@ void Account::readDraftCursors(PeerId peerId, Data::HistoryDrafts &map) {
 			? Data::DraftKey::FromSerialized(keyValue)
 			: keysOld
 			? Data::DraftKey::FromSerializedOld(keyValueOld)
-			: Data::DraftKey::Local();
+			: Data::DraftKey::Local(0);
 		qint32 position = 0, anchor = 0, scroll = QFIXED_MAX;
 		draft.stream >> position >> anchor >> scroll;
 		if (const auto i = map.find(key); i != end(map)) {
@@ -1309,13 +1299,14 @@ void Account::readDraftCursorsLegacy(
 		return;
 	}
 
-	if (const auto i = map.find(Data::DraftKey::Local()); i != end(map)) {
+	if (const auto i = map.find(Data::DraftKey::Local({})); i != end(map)) {
 		i->second->cursor = MessageCursor(
 			localPosition,
 			localAnchor,
 			localScroll);
 	}
-	if (const auto i = map.find(Data::DraftKey::LocalEdit()); i != end(map)) {
+	if (const auto i = map.find(Data::DraftKey::LocalEdit({}))
+		; i != end(map)) {
 		i->second->cursor = MessageCursor(
 			editPosition,
 			editAnchor,
@@ -1327,7 +1318,7 @@ void Account::readDraftsWithCursors(not_null<History*> history) {
 	const auto guard = gsl::finally([&] {
 		if (const auto migrated = history->migrateFrom()) {
 			readDraftsWithCursors(migrated);
-			migrated->clearLocalEditDraft();
+			migrated->clearLocalEditDraft({});
 			history->takeLocalDraft(migrated);
 		}
 	});
@@ -1396,10 +1387,11 @@ void Account::readDraftsWithCursors(not_null<History*> history) {
 		const auto key = keysOld
 			? Data::DraftKey::FromSerializedOld(keyValueOld)
 			: Data::DraftKey::FromSerialized(keyValue);
-		if (key && key != Data::DraftKey::Cloud()) {
+		if (key && !key.isCloud()) {
 			map.emplace(key, std::make_unique<Data::Draft>(
 				data,
 				messageId,
+				key.topicRootId(),
 				MessageCursor(),
 				previewState));
 		}
@@ -1457,24 +1449,31 @@ void Account::readDraftsWithCursorsLegacy(
 		editTagsSerialized,
 		editData.text.size());
 
+	const auto topicRootId = MsgId();
 	auto map = base::flat_map<Data::DraftKey, std::unique_ptr<Data::Draft>>();
 	if (!msgData.text.isEmpty() || msgReplyTo) {
-		map.emplace(Data::DraftKey::Local(), std::make_unique<Data::Draft>(
-			msgData,
-			msgReplyTo,
-			MessageCursor(),
-			(msgPreviewCancelled
-				? Data::PreviewState::Cancelled
-				: Data::PreviewState::Allowed)));
+		map.emplace(
+			Data::DraftKey::Local(topicRootId),
+			std::make_unique<Data::Draft>(
+				msgData,
+				msgReplyTo,
+				topicRootId,
+				MessageCursor(),
+				(msgPreviewCancelled
+					? Data::PreviewState::Cancelled
+					: Data::PreviewState::Allowed)));
 	}
 	if (editMsgId) {
-		map.emplace(Data::DraftKey::LocalEdit(), std::make_unique<Data::Draft>(
-			editData,
-			editMsgId,
-			MessageCursor(),
-			(editPreviewCancelled
-				? Data::PreviewState::Cancelled
-				: Data::PreviewState::Allowed)));
+		map.emplace(
+			Data::DraftKey::LocalEdit(topicRootId),
+			std::make_unique<Data::Draft>(
+				editData,
+				editMsgId,
+				topicRootId,
+				MessageCursor(),
+				(editPreviewCancelled
+					? Data::PreviewState::Cancelled
+					: Data::PreviewState::Allowed)));
 	}
 	readDraftCursors(peerId, map);
 	history->setDraftsMap(std::move(map));
diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp
index 3c2cead33..5c4a57160 100644
--- a/Telegram/SourceFiles/support/support_helper.cpp
+++ b/Telegram/SourceFiles/support/support_helper.cpp
@@ -47,6 +47,7 @@ namespace {
 constexpr auto kOccupyFor = TimeId(60);
 constexpr auto kReoccupyEach = 30 * crl::time(1000);
 constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4;
+constexpr auto kTopicRootId = MsgId(0);
 
 class EditInfoBox : public Ui::BoxContent {
 public:
@@ -157,7 +158,8 @@ Data::Draft OccupiedDraft(const QString &normalizedName) {
 			+ QString::number(OccupationTag())
 			+ ";n:"
 			+ normalizedName },
-		MsgId(0),
+		MsgId(0), // replyTo
+		kTopicRootId,
 		MessageCursor(),
 		Data::PreviewState::Allowed
 	};
@@ -176,7 +178,7 @@ uint32 ParseOccupationTag(History *history) {
 	if (!TrackHistoryOccupation(history)) {
 		return 0;
 	}
-	const auto draft = history->cloudDraft();
+	const auto draft = history->cloudDraft(kTopicRootId);
 	if (!draft) {
 		return 0;
 	}
@@ -202,7 +204,7 @@ QString ParseOccupationName(History *history) {
 	if (!TrackHistoryOccupation(history)) {
 		return QString();
 	}
-	const auto draft = history->cloudDraft();
+	const auto draft = history->cloudDraft(kTopicRootId);
 	if (!draft) {
 		return QString();
 	}
@@ -228,7 +230,7 @@ TimeId OccupiedBySomeoneTill(History *history) {
 	if (!TrackHistoryOccupation(history)) {
 		return 0;
 	}
-	const auto draft = history->cloudDraft();
+	const auto draft = history->cloudDraft(kTopicRootId);
 	if (!draft) {
 		return 0;
 	}
@@ -340,7 +342,7 @@ void Helper::updateOccupiedHistory(
 		not_null<Window::SessionController*> controller,
 		History *history) {
 	if (isOccupiedByMe(_occupiedHistory)) {
-		_occupiedHistory->clearCloudDraft();
+		_occupiedHistory->clearCloudDraft(kTopicRootId);
 		_session->api().saveDraftToCloudDelayed(_occupiedHistory);
 	}
 	_occupiedHistory = history;
@@ -364,7 +366,7 @@ void Helper::occupyInDraft() {
 		&& !isOccupiedBySomeone(_occupiedHistory)
 		&& !_supportName.isEmpty()) {
 		const auto draft = OccupiedDraft(_supportNameNormalized);
-		_occupiedHistory->createCloudDraft(&draft);
+		_occupiedHistory->createCloudDraft(kTopicRootId, &draft);
 		_session->api().saveDraftToCloudDelayed(_occupiedHistory);
 		_reoccupyTimer.callEach(kReoccupyEach);
 	}
@@ -373,7 +375,7 @@ void Helper::occupyInDraft() {
 void Helper::reoccupy() {
 	if (isOccupiedByMe(_occupiedHistory)) {
 		const auto draft = OccupiedDraft(_supportNameNormalized);
-		_occupiedHistory->createCloudDraft(&draft);
+		_occupiedHistory->createCloudDraft(kTopicRootId, &draft);
 		_session->api().saveDraftToCloudDelayed(_occupiedHistory);
 	}
 }
diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp
index 57f7616ec..07e0b211d 100644
--- a/Telegram/SourceFiles/window/notifications_manager.cpp
+++ b/Telegram/SourceFiles/window/notifications_manager.cpp
@@ -979,6 +979,10 @@ void Manager::notificationActivated(
 			const auto window = session->windows().front();
 			const auto history = session->data().history(
 				id.contextId.peerId);
+			const auto item = history->owner().message(
+				history->peer,
+				id.msgId);
+			const auto topic = item ? item->topic() : nullptr;
 			if (!reply.text.isEmpty()) {
 				// #TODO forum notifications
 				const auto replyToId = (id.msgId > 0
@@ -988,6 +992,7 @@ void Manager::notificationActivated(
 				auto draft = std::make_unique<Data::Draft>(
 					reply,
 					replyToId,
+					(topic ? topic->rootId() : 0),
 					MessageCursor{
 						int(reply.text.size()),
 						int(reply.text.size()),
@@ -1057,16 +1062,18 @@ void Manager::notificationReplied(
 		return;
 	}
 	const auto history = session->data().history(id.contextId.peerId);
+	const auto item = history->owner().message(history->peer, id.msgId);
+	const auto topic = item ? item->topic() : nullptr;
 
 	auto message = Api::MessageToSend(Api::SendAction(history));
 	message.textWithTags = reply;
 	message.action.replyTo = (id.msgId > 0 && !history->peer->isUser())
 		? id.msgId
 		: id.contextId.topicRootId;
+	message.action.topicRootId = topic ? topic->rootId() : 0;
 	message.action.clearDraft = false;
 	history->session().api().sendMessage(std::move(message));
 
-	const auto item = history->owner().message(history->peer, id.msgId);
 	if (item && item->isUnreadMention() && !item->isIncomingUnreadMedia()) {
 		history->session().api().markContentsRead(item);
 	}
diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp
index 1d3181c2a..6a2ee6fe7 100644
--- a/Telegram/SourceFiles/window/window_peer_menu.cpp
+++ b/Telegram/SourceFiles/window/window_peer_menu.cpp
@@ -844,14 +844,16 @@ void Filler::addCreatePoll() {
 		? SendMenu::Type::SilentOnly
 		: SendMenu::Type::Scheduled;
 	const auto flag = PollData::Flags();
+	const auto topicRootId = _request.rootId;
 	const auto replyToId = _request.currentReplyToId
 		? _request.currentReplyToId
-		: _request.rootId;
+		: topicRootId;
 	auto callback = [=] {
 		PeerMenuCreatePoll(
 			controller,
 			peer,
 			replyToId,
+			topicRootId,
 			flag,
 			flag,
 			source,
@@ -1168,6 +1170,7 @@ void PeerMenuCreatePoll(
 		not_null<Window::SessionController*> controller,
 		not_null<PeerData*> peer,
 		MsgId replyToId,
+		MsgId topicRootId,
 		PollData::Flags chosen,
 		PollData::Flags disabled,
 		Api::SendType sendType,
@@ -1194,8 +1197,9 @@ void PeerMenuCreatePoll(
 			result.options);
 		action.clearDraft = false;
 		action.replyTo = replyToId;
-		if (const auto localDraft = action.history->localDraft()) {
-			action.clearDraft = localDraft->textWithTags.text.isEmpty();
+		action.topicRootId = topicRootId;
+		if (const auto local = action.history->localDraft(topicRootId)) {
+			action.clearDraft = local->textWithTags.text.isEmpty();
 		}
 		const auto api = &peer->session().api();
 		api->polls().create(result.poll, action, crl::guard(weak, [=] {
diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h
index b314082a1..ed3ce1103 100644
--- a/Telegram/SourceFiles/window/window_peer_menu.h
+++ b/Telegram/SourceFiles/window/window_peer_menu.h
@@ -69,6 +69,7 @@ void PeerMenuCreatePoll(
 	not_null<Window::SessionController*> controller,
 	not_null<PeerData*> peer,
 	MsgId replyToId = 0,
+	MsgId topicRootId = 0,
 	PollData::Flags chosen = PollData::Flags(),
 	PollData::Flags disabled = PollData::Flags(),
 	Api::SendType sendType = Api::SendType::Normal,
diff --git a/Telegram/lib_ui b/Telegram/lib_ui
index 9ab11ccb3..d8b1f4671 160000
--- a/Telegram/lib_ui
+++ b/Telegram/lib_ui
@@ -1 +1 @@
-Subproject commit 9ab11ccb36b7d03a3a24ebcd18d2f13b03fc3682
+Subproject commit d8b1f46715e5fcaf781b76ecbc386cbe31492287