diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index fe0c43529..25579daa0 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D75" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="Application" representedClassName=".Application" syncable="YES">
         <attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="name" attributeType="String"/>
@@ -115,6 +115,7 @@
         <attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
         <attribute name="id" attributeType="String"/>
         <attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="url" attributeType="String"/>
         <attribute name="username" attributeType="String"/>
         <relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mentions" inverseEntity="Status"/>
@@ -236,16 +237,17 @@
         <element name="Emoji" positionX="0" positionY="0" width="128" height="149"/>
         <element name="History" positionX="0" positionY="0" width="128" height="119"/>
         <element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
-        <element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
-        <element name="MastodonUser" positionX="0" positionY="0" width="128" height="629"/>
-        <element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
-        <element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
-        <element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
-        <element name="PrivateNote" positionX="72" positionY="153" width="128" height="89"/>
-        <element name="Setting" positionX="72" positionY="162" width="128" height="134"/>
+        <element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
+        <element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
+        <element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
+        <element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
+        <element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
+        <element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
+        <element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
         <element name="Status" positionX="0" positionY="0" width="128" height="569"/>
+        <element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
+        <element name="Setting" positionX="72" positionY="162" width="128" height="134"/>
         <element name="Subscription" positionX="81" positionY="171" width="128" height="149"/>
         <element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="149"/>
-        <element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
     </elements>
 </model>
\ No newline at end of file
diff --git a/CoreDataStack/Entity/Mention.swift b/CoreDataStack/Entity/Mention.swift
index 9559ea5d5..864ca4948 100644
--- a/CoreDataStack/Entity/Mention.swift
+++ b/CoreDataStack/Entity/Mention.swift
@@ -10,6 +10,9 @@ import Foundation
 
 public final class Mention: NSManagedObject {
     public typealias ID = UUID
+    
+    @NSManaged public private(set) var index: NSNumber
+    
     @NSManaged public private(set) var identifier: ID
     @NSManaged public private(set) var id: String
     @NSManaged public private(set) var createAt: Date
@@ -32,9 +35,11 @@ public extension Mention {
     @discardableResult
     static func insert(
         into context: NSManagedObjectContext,
-        property: Property
+        property: Property,
+        index: Int
     ) -> Mention {
         let mention: Mention = context.insertObject()
+        mention.index = NSNumber(value: index)
         mention.id = property.id
         mention.username = property.username
         mention.acct = property.acct
diff --git a/Localization/app.json b/Localization/app.json
index facda97cf..4b6cb82af 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -94,7 +94,8 @@
             "timeline": {
                 "loader": {
                     "load_missing_posts": "Load missing posts",
-                    "loading_missing_posts": "Loading missing posts..."
+                    "loading_missing_posts": "Loading missing posts...",
+                    "show_more_replies": "Show more replies"
                 },
                 "header": {
                     "no_status_found": "No Status Found",
@@ -203,7 +204,7 @@
         },
         "confirm_email": {
             "title": "One last thing.",
-            "subtitle": "We just sent an email to %@,\ntap the link to confirm your account.",
+            "subtitle": "We just sent an email to %s,\ntap the link to confirm your account.",
             "button": {
                 "open_email_app": "Open Email App",
                 "dont_receive_email": "I never got an email"
@@ -244,6 +245,7 @@
             },
             "content_input_placeholder": "Type or paste what's on your mind",
             "compose_action": "Publish",
+            "replying_to_user": "replying to %s",
             "attachment": {
                 "photo": "photo",
                 "video": "video",
@@ -258,7 +260,8 @@
                 "six_hours": "6 Hours",
                 "one_day": "1 Day",
                 "three_days": "3 Days",
-                "seven_days": "7 Days"
+                "seven_days": "7 Days",
+                "option_number": "Option %ld"
             },
             "content_warning": {
                 "placeholder": "Write an accurate warning here..."
@@ -327,6 +330,18 @@
         "favorite": {
             "title": "Your Favorites"
         },
+        "thread": {
+            "back_title": "Post",
+            "title": "Post from %s",
+            "reblog": {
+                "single": "%s reblog",
+                "multiple": "%s reblogs"
+            },
+            "favorite": {
+                "single": "%s favorite",
+                "multiple": "%s favorites"
+            }
+        },
         "settings": {
             "title": "Settings",
             "section": {
@@ -363,4 +378,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index f4127506c..7850ed97a 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -155,6 +155,8 @@
 		DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
 		DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
 		DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
+		DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
+		DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
 		DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
 		DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
 		DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
@@ -229,7 +231,7 @@
 		DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
 		DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
 		DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
-		DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */; };
+		DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
 		DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
 		DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
 		DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
@@ -274,6 +276,15 @@
 		DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
 		DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */; };
 		DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */; };
+		DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */; };
+		DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */; };
+		DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */; };
+		DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */; };
+		DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; };
+		DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */; };
+		DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; };
+		DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */; };
+		DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; };
 		DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; };
 		DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
 		DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
@@ -317,6 +328,7 @@
 		DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
 		DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */; };
 		DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */; };
+		DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
 		DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
 		DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
 		DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
@@ -546,6 +558,8 @@
 		DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
 		DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
 		DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
+		DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
+		DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
 		DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
 		DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
 		DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
@@ -626,7 +640,7 @@
 		DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
 		DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
 		DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
-		DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
+		DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = "<group>"; };
 		DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
 		DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
 		DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
@@ -673,6 +687,15 @@
 		DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
 		DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerEmptyStateView.swift; sourceTree = "<group>"; };
 		DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionTableViewCell.swift; sourceTree = "<group>"; };
+		DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewController.swift; sourceTree = "<group>"; };
+		DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = "<group>"; };
+		DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedThreadViewModel.swift; sourceTree = "<group>"; };
+		DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = "<group>"; };
+		DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = "<group>"; };
+		DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Thread.swift"; sourceTree = "<group>"; };
+		DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = "<group>"; };
+		DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+StatusProvider.swift"; sourceTree = "<group>"; };
+		DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = "<group>"; };
 		DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = "<group>"; };
 		DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
 		DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
@@ -715,6 +738,7 @@
 		DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = "<group>"; };
 		DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardView.swift; sourceTree = "<group>"; };
 		DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardMeterView.swift; sourceTree = "<group>"; };
+		DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
 		DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
 		DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
 		DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
@@ -887,6 +911,7 @@
 				DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
 				DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */,
 				0F1E2D0A2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift */,
+				DBB9759B262462E1004620BD /* ThreadMetaView.swift */,
 			);
 			path = Content;
 			sourceTree = "<group>";
@@ -1092,9 +1117,11 @@
 			children = (
 				2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */,
 				2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
+				DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */,
 				2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
 				2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
 				DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */,
+				DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */,
 				DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */,
 			);
 			path = TableviewCell;
@@ -1361,6 +1388,7 @@
 				DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
 				DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
 				DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
+				DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */,
 				DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
 				DB9A488326034BD7008B817C /* APIService+Status.swift */,
 				DB9A488F26035963008B817C /* APIService+Media.swift */,
@@ -1483,7 +1511,7 @@
 			children = (
 				DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */,
 				DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
-				DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
+				DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */,
 				DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
 				DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
 				DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
@@ -1611,6 +1639,7 @@
 				DB9D6BFD25E4F57B0051B173 /* Notification */,
 				DB9D6C0825E4F5A60051B173 /* Profile */,
 				DB789A1025F9F29B0071ACA0 /* Compose */,
+				DB938EEB2623F52600E5B6C1 /* Thread */,
 			);
 			path = Scene;
 			sourceTree = "<group>";
@@ -1651,6 +1680,20 @@
 			path = Extension;
 			sourceTree = "<group>";
 		};
+		DB938EEB2623F52600E5B6C1 /* Thread */ = {
+			isa = PBXGroup;
+			children = (
+				DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */,
+				DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */,
+				DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */,
+				DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */,
+				DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */,
+				DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */,
+				DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */,
+			);
+			path = Thread;
+			sourceTree = "<group>";
+		};
 		DB98335F25C93B0400AD9700 /* Recovered References */ = {
 			isa = PBXGroup;
 			children = (
@@ -1757,6 +1800,7 @@
 			children = (
 				DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */,
 				DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
+				DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */,
 			);
 			path = Control;
 			sourceTree = "<group>";
@@ -2257,6 +2301,7 @@
 				2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
 				2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
 				DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
+				DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
 				DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
 				2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
 				0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */,
@@ -2265,6 +2310,7 @@
 				2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
 				DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
 				2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
+				DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
 				5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
 				DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
 				DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
@@ -2278,6 +2324,7 @@
 				DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
 				2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
 				DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
+				DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
 				0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
 				DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */,
 				DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
@@ -2301,13 +2348,14 @@
 				2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
 				5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */,
 				DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
+				DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
 				2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
 				DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
 				DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
 				0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
 				2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
 				DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
-				DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
+				DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
 				2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
 				DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
 				DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
@@ -2324,6 +2372,7 @@
 				DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */,
 				DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
 				DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
+				DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
 				2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
 				5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
 				DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
@@ -2394,8 +2443,10 @@
 				DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
 				DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
 				DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
+				DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
 				DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
 				2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */,
+				DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
 				DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
 				0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
 				DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
@@ -2413,11 +2464,13 @@
 				DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
 				DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
 				2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */,
+				DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */,
 				DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */,
 				2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
 				DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,
 				2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
 				DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
+				DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
 				2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
 				DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
 				DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
@@ -2466,6 +2519,7 @@
 				0F202227261411BB000C64BF /* HashtagTimelineViewController+StatusProvider.swift in Sources */,
 				2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
 				2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
+				DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
 				2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
 				DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
 				DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
@@ -2487,7 +2541,9 @@
 				0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
 				DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
 				DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
+				DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
 				2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
+				DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */,
 				DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
 				0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
 				5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 6ec23cf5d..fd1ce69a1 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
 		<key>CoreDataStack.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>10</integer>
+			<integer>20</integer>
 		</dict>
 		<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
 		<dict>
diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift
index 28064ac38..c2608fe83 100644
--- a/Mastodon/Coordinator/SceneCoordinator.swift
+++ b/Mastodon/Coordinator/SceneCoordinator.swift
@@ -51,6 +51,9 @@ extension SceneCoordinator {
         // compose
         case compose(viewModel: ComposeViewModel)
         
+        // thread
+        case thread(viewModel: ThreadViewModel)
+        
         // Hashtag Timeline
         case hashtagTimeline(viewModel: HashtagTimelineViewModel)
       
@@ -227,6 +230,10 @@ private extension SceneCoordinator {
             let _viewController = ComposeViewController()
             _viewController.viewModel = viewModel
             viewController = _viewController
+        case .thread(let viewModel):
+            let _viewController = ThreadViewController()
+            _viewController.viewModel = viewModel
+            viewController = _viewController
         case .hashtagTimeline(let viewModel):
             let _viewController = HashtagTimelineViewController()
             _viewController.viewModel = viewModel
diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift
index 9f82f6ca5..da3455201 100644
--- a/Mastodon/Diffiable/Item/Item.swift
+++ b/Mastodon/Diffiable/Item/Item.swift
@@ -14,6 +14,12 @@ import MastodonSDK
 enum Item {
     // timeline
     case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
+    
+    // thread
+    case root(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
+    case reply(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
+    case leaf(statusObjectID: NSManagedObjectID, attribute: StatusAttribute)
+    case leafBottomLoader(statusObjectID: NSManagedObjectID)
 
     // normal list
     case status(objectID: NSManagedObjectID, attribute: StatusAttribute)
@@ -21,6 +27,7 @@ enum Item {
     // loader
     case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
     case publicMiddleLoader(statusID: String)
+    case topLoader
     case bottomLoader
     
     case emptyStateHeader(attribute: EmptyStateHeaderAttribute)
@@ -35,13 +42,16 @@ extension Item {
     class StatusAttribute: StatusContentWarningAttribute {
         var isStatusTextSensitive: Bool?
         var isStatusSensitive: Bool?
+        var isSeparatorLineHidden: Bool
 
         init(
             isStatusTextSensitive: Bool? = nil,
-            isStatusSensitive: Bool? = nil
+            isStatusSensitive: Bool? = nil,
+            isSeparatorLineHidden: Bool = false
         ) {
             self.isStatusTextSensitive = isStatusTextSensitive
             self.isStatusSensitive = isStatusSensitive
+            self.isSeparatorLineHidden = isSeparatorLineHidden
         }
         
         // delay attribute init
@@ -59,6 +69,23 @@ extension Item {
         }
     }
     
+//    class LeafAttribute {
+//        let identifier = UUID()
+//        let statusID: Status.ID
+//        var level: Int = 0
+//        var hasReply: Bool = true
+//
+//        init(
+//            statusID: Status.ID,
+//            level: Int,
+//            hasReply: Bool = true
+//        ) {
+//            self.statusID = statusID
+//            self.level = level
+//            self.hasReply = hasReply
+//        }
+//    }
+    
     class EmptyStateHeaderAttribute: Hashable {
         let id = UUID()
         let reason: Reason
@@ -99,12 +126,22 @@ extension Item: Equatable {
         switch (lhs, rhs) {
         case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
             return objectIDLeft == objectIDRight
+        case (.root(let objectIDLeft, _), .root(let objectIDRight, _)):
+            return objectIDLeft == objectIDRight
+        case (.reply(let objectIDLeft, _), .reply(let objectIDRight, _)):
+            return objectIDLeft == objectIDRight
+        case (.leaf(let objectIDLeft, _), .leaf(let objectIDRight, _)):
+            return objectIDLeft == objectIDRight
+        case (.leafBottomLoader(let objectIDLeft), .leafBottomLoader(let objectIDRight)):
+            return objectIDLeft == objectIDRight
         case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
             return objectIDLeft == objectIDRight
         case (.homeMiddleLoader(let upperLeft), .homeMiddleLoader(let upperRight)):
             return upperLeft == upperRight
         case (.publicMiddleLoader(let upperLeft), .publicMiddleLoader(let upperRight)):
             return upperLeft == upperRight
+        case (.topLoader, .topLoader):
+            return true
         case (.bottomLoader, .bottomLoader):
             return true
         case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)):
@@ -120,6 +157,14 @@ extension Item: Hashable {
         switch self {
         case .homeTimelineIndex(let objectID, _):
             hasher.combine(objectID)
+        case .root(let objectID, _):
+            hasher.combine(objectID)
+        case .reply(let objectID, _):
+            hasher.combine(objectID)
+        case .leaf(let objectID, _):
+            hasher.combine(objectID)
+        case .leafBottomLoader(let objectID):
+            hasher.combine(objectID)
         case .status(let objectID, _):
             hasher.combine(objectID)
         case .homeMiddleLoader(upperTimelineIndexAnchorObjectID: let upper):
@@ -128,6 +173,8 @@ extension Item: Hashable {
         case .publicMiddleLoader(let upper):
             hasher.combine(String(describing: Item.publicMiddleLoader.self))
             hasher.combine(upper)
+        case .topLoader:
+            hasher.combine(String(describing: Item.topLoader.self))
         case .bottomLoader:
             hasher.combine(String(describing: Item.bottomLoader.self))
         case .emptyStateHeader(let attribute):
diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift
index 56aa32798..0e7c574b4 100644
--- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift
+++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift
@@ -34,6 +34,7 @@ extension ComposeStatusSection {
         dependency: NeedsDependency,
         managedObjectContext: NSManagedObjectContext,
         composeKind: ComposeKind,
+        repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
         customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
         textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
         composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
@@ -50,8 +51,29 @@ extension ComposeStatusSection {
             weak composeStatusPollExpiresOptionCollectionViewCellDelegate
         ] collectionView, indexPath, item -> UICollectionViewCell? in
             switch item {
-            case .replyTo(let repliedToStatusObjectID):
+            case .replyTo(let replyToStatusObjectID):
                 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell
+                managedObjectContext.perform {
+                    guard let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
+                        return
+                    }
+                    let status = replyTo.reblog ?? replyTo
+                    
+                    // set avatar
+                    cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
+                    // set name username
+                    cell.statusView.nameLabel.text = {
+                        let author = status.author
+                        return author.displayName.isEmpty ? author.username : author.displayName
+                    }()
+                    cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
+                    // set text
+                    cell.statusView.activeTextLabel.configure(content: status.content)
+                    // set date
+                    cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow
+                    
+                    cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag)
+                }
                 return cell
             case .input(let replyToStatusObjectID, let attribute):
                 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
@@ -63,16 +85,22 @@ extension ComposeStatusSection {
                         return
                     }
                     cell.statusView.headerContainerStackView.isHidden = false
-                    cell.statusView.headerInfoLabel.text = "[TODO] \(replyTo.author.displayName)"
+                    cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
+                    cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback)
                 }
-                ComposeStatusSection.configure(cell: cell, attribute: attribute)
+                ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
                 cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
                 cell.composeContent
                     .removeDuplicates()
                     .receive(on: DispatchQueue.main)
                     .sink { text in
                         // self size input cell
+                        // needs restore content offset to resolve issue #83
+                        let oldContentOffset = collectionView.contentOffset
                         collectionView.collectionViewLayout.invalidateLayout()
+                        collectionView.layoutIfNeeded()
+                        collectionView.contentOffset = oldContentOffset
+                        
                         // bind input data
                         attribute.composeContent.value = text
                     }
@@ -167,6 +195,7 @@ extension ComposeStatusSection {
             case .pollOption(let attribute):
                 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell
                 cell.pollOptionView.optionTextField.text = attribute.option.value
+                cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1)
                 cell.pollOption
                     .receive(on: DispatchQueue.main)
                     .assign(to: \.value, on: attribute.option)
@@ -196,7 +225,7 @@ extension ComposeStatusSection {
 
 extension ComposeStatusSection {
     
-    static func configure(
+    static func configureStatusContent(
         cell: ComposeStatusContentCollectionViewCell,
         attribute: ComposeStatusItem.ComposeStatusAttribute
     ) {
diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift
index fe720e0f0..36d4853a8 100644
--- a/Mastodon/Diffiable/Section/StatusSection.swift
+++ b/Mastodon/Diffiable/Section/StatusSection.swift
@@ -22,9 +22,16 @@ extension StatusSection {
         managedObjectContext: NSManagedObjectContext,
         timestampUpdatePublisher: AnyPublisher<Date, Never>,
         statusTableViewCellDelegate: StatusTableViewCellDelegate,
-        timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
+        timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?,
+        threadReplyLoaderTableViewCellDelegate: ThreadReplyLoaderTableViewCellDelegate?
     ) -> UITableViewDiffableDataSource<StatusSection, Item> {
-        UITableViewDiffableDataSource(tableView: tableView) { [weak statusTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in
+        UITableViewDiffableDataSource(tableView: tableView) { [
+            weak dependency,
+            weak statusTableViewCellDelegate,
+            weak timelineMiddleLoaderTableViewCellDelegate,
+            weak threadReplyLoaderTableViewCellDelegate
+        ] tableView, indexPath, item -> UITableViewCell? in
+            guard let dependency = dependency else { return UITableViewCell() }
             guard let statusTableViewCellDelegate = statusTableViewCellDelegate else { return UITableViewCell() }
 
             switch item {
@@ -46,7 +53,10 @@ extension StatusSection {
                 }
                 cell.delegate = statusTableViewCellDelegate
                 return cell
-            case .status(let objectID, let attribute):
+            case .status(let objectID, let attribute),
+                 .root(let objectID, let attribute),
+                 .reply(let objectID, let attribute),
+                 .leaf(let objectID, let attribute):
                 let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
                 let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
                 let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
@@ -62,8 +72,30 @@ extension StatusSection {
                         requestUserID: requestUserID,
                         statusItemAttribute: attribute
                     )
+                    
+                    switch item {
+                    case .root:
+                        StatusSection.configureThreadMeta(cell: cell, status: status)
+                        ManagedObjectObserver.observe(object: status.reblog ?? status)
+                            .receive(on: DispatchQueue.main)
+                            .sink { _ in
+                                // do nothing
+                            } receiveValue: { change in
+                                guard case .update(let object) = change.changeType,
+                                      let status = object as? Status else { return }
+                                StatusSection.configureThreadMeta(cell: cell, status: status)
+                            }
+                            .store(in: &cell.disposeBag)
+                    default:
+                        break
+                    }
                 }
                 cell.delegate = statusTableViewCellDelegate
+                
+                return cell
+            case .leafBottomLoader:
+                let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self), for: indexPath) as! ThreadReplyLoaderTableViewCell
+                cell.delegate = threadReplyLoaderTableViewCellDelegate
                 return cell
             case .publicMiddleLoader(let upperTimelineStatusID):
                 let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
@@ -75,6 +107,10 @@ extension StatusSection {
                 cell.delegate = timelineMiddleLoaderTableViewCellDelegate
                 timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: nil, timelineIndexobjectID: upperTimelineIndexObjectID)
                 return cell
+            case .topLoader:
+                let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
+                cell.startAnimating()
+                return cell
             case .bottomLoader:
                 let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
                 cell.startAnimating()
@@ -288,6 +324,9 @@ extension StatusSection {
         // toolbar
         StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
         
+        // separator line
+        cell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden
+        
         // set date
         let createdAt = (status.reblog ?? status).createdAt
         cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
@@ -312,6 +351,41 @@ extension StatusSection {
             }
             .store(in: &cell.disposeBag)
     }
+    
+    static func configureThreadMeta(
+        cell: StatusTableViewCell,
+        status: Status
+    ) {
+        cell.selectionStyle = .none
+        cell.threadMetaView.dateLabel.text = {
+            let formatter = DateFormatter()
+            formatter.dateStyle = .medium
+            formatter.timeStyle = .short
+            return formatter.string(from: status.createdAt)
+        }()
+        let reblogCountTitle: String = {
+            let count = status.reblogsCount.intValue
+            if count > 1 {
+                return L10n.Scene.Thread.Reblog.multiple(String(count))
+            } else {
+                return L10n.Scene.Thread.Reblog.single(String(count))
+            }
+        }()
+        cell.threadMetaView.reblogButton.setTitle(reblogCountTitle, for: .normal)
+        
+        let favoriteCountTitle: String = {
+            let count = status.favouritesCount.intValue
+            if count > 1 {
+                return L10n.Scene.Thread.Favorite.multiple(String(count))
+            } else {
+                return L10n.Scene.Thread.Favorite.single(String(count))
+            }
+        }()
+        cell.threadMetaView.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
+        
+        cell.threadMetaView.isHidden = false
+    }
+    
 
     static func configureHeader(
         cell: StatusTableViewCell,
@@ -319,16 +393,19 @@ extension StatusSection {
     ) {
         if status.reblog != nil {
             cell.statusView.headerContainerStackView.isHidden = false
-            cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
+            cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
             cell.statusView.headerInfoLabel.text = {
                 let author = status.author
                 let name = author.displayName.isEmpty ? author.username : author.displayName
                 return L10n.Common.Controls.Status.userReblogged(name)
             }()
-        } else if let replyTo = status.replyTo {
+        } else if status.inReplyToID != nil {
             cell.statusView.headerContainerStackView.isHidden = false
             cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
             cell.statusView.headerInfoLabel.text = {
+                guard let replyTo = status.replyTo else {
+                    return L10n.Common.Controls.Status.userRepliedTo("-")
+                }
                 let author = replyTo.author
                 let name = author.displayName.isEmpty ? author.username : author.displayName
                 return L10n.Common.Controls.Status.userRepliedTo(name)
diff --git a/Mastodon/Extension/CGImage.swift b/Mastodon/Extension/CGImage.swift
index 252f1289a..cced4abee 100644
--- a/Mastodon/Extension/CGImage.swift
+++ b/Mastodon/Extension/CGImage.swift
@@ -26,7 +26,7 @@ extension CGImage {
               let pointer = CFDataGetBytePtr(data) else { return nil }
         
         let length = CFDataGetLength(data)
-        guard length > 0 else { return nil}
+        guard length > 0 else { return nil }
         
         var luma: CGFloat = 0.0
         for i in stride(from: 0, to: length, by: 4) {
diff --git a/Mastodon/Extension/UIBarButtonItem.swift b/Mastodon/Extension/UIBarButtonItem.swift
index 8a0630f03..cf1f84e97 100644
--- a/Mastodon/Extension/UIBarButtonItem.swift
+++ b/Mastodon/Extension/UIBarButtonItem.swift
@@ -17,4 +17,3 @@ extension UIBarButtonItem {
     }
     
 }
-
diff --git a/Mastodon/Extension/UIImage.swift b/Mastodon/Extension/UIImage.swift
index 072a3d4d4..35766c0bc 100644
--- a/Mastodon/Extension/UIImage.swift
+++ b/Mastodon/Extension/UIImage.swift
@@ -59,7 +59,7 @@ extension UIImage {
     }
 }
 
-public extension UIImage {
+extension UIImage {
     func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? {
         let maxRadius = min(size.width, size.height) / 2
         let cornerRadius: CGFloat = {
@@ -75,3 +75,18 @@ public extension UIImage {
         }
     }
 }
+
+extension UIImage {
+    static func adaptiveUserInterfaceStyleImage(lightImage: UIImage, darkImage: UIImage) -> UIImage {
+        let imageAsset = UIImageAsset()
+        imageAsset.register(lightImage, with: UITraitCollection(traitsFrom: [
+            UITraitCollection(displayScale: 1.0),
+            UITraitCollection(userInterfaceStyle: .light)
+        ]))
+        imageAsset.register(darkImage, with: UITraitCollection(traitsFrom: [
+            UITraitCollection(displayScale: 1.0),
+            UITraitCollection(userInterfaceStyle: .dark)
+        ]))
+        return imageAsset.image(with: UITraitCollection.current)
+    }
+}
diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift
index 5fd25edc4..cd655e077 100644
--- a/Mastodon/Generated/Assets.swift
+++ b/Mastodon/Generated/Assets.swift
@@ -44,7 +44,6 @@ internal enum Asset {
       internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor")
       internal static let navigationBar = ColorAsset(name: "Colors/Background/navigationBar")
       internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
-      internal static let searchResult = ColorAsset(name: "Colors/Background/searchResult")
       internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
       internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
       internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
@@ -81,7 +80,6 @@ internal enum Asset {
       internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
       internal static let valid = ColorAsset(name: "Colors/TextField/valid")
     }
-    internal static let battleshipGrey = ColorAsset(name: "Colors/battleshipGrey")
     internal static let brandBlue = ColorAsset(name: "Colors/brand.blue")
     internal static let danger = ColorAsset(name: "Colors/danger")
     internal static let disabled = ColorAsset(name: "Colors/disabled")
@@ -92,31 +90,35 @@ internal enum Asset {
   internal enum Connectivity {
     internal static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split")
   }
-  internal enum Profile {
-    internal enum Banner {
-      internal static let bioEditBackgroundGray = ColorAsset(name: "Profile/Banner/bio.edit.background.gray")
-      internal static let nameEditBackgroundGray = ColorAsset(name: "Profile/Banner/name.edit.background.gray")
-      internal static let usernameGray = ColorAsset(name: "Profile/Banner/username.gray")
-    }
+  internal enum Human {
+    internal static let faceSmilingAdaptive = ImageAsset(name: "Human/face.smiling.adaptive")
   }
-  internal enum Settings {
-    internal static let appearanceAutomatic = ImageAsset(name: "Settings/appearance.automatic")
-    internal static let appearanceDark = ImageAsset(name: "Settings/appearance.dark")
-    internal static let appearanceLight = ImageAsset(name: "Settings/appearance.light")
-  }
-  internal enum Welcome {
-    internal enum Illustration {
-      internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan")
-      internal static let cloudBase = ImageAsset(name: "Welcome/illustration/cloud.base")
-      internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Welcome/illustration/elephant.on.airplane.with.contrail")
-      internal static let elephantThreeOnGrass = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass")
-      internal static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass.with.tree.three")
-      internal static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass.with.tree.two")
+  internal enum Scene {
+    internal enum Compose {
+      internal static let background = ColorAsset(name: "Scene/Compose/background")
+      internal static let toolbarBackground = ColorAsset(name: "Scene/Compose/toolbar.background")
+    }
+    internal enum Profile {
+      internal enum Banner {
+        internal static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray")
+        internal static let nameEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/name.edit.background.gray")
+        internal static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray")
+      }
+    }
+    internal enum Welcome {
+      internal enum Illustration {
+        internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan")
+        internal static let cloudBase = ImageAsset(name: "Scene/Welcome/illustration/cloud.base")
+        internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Scene/Welcome/illustration/elephant.on.airplane.with.contrail")
+        internal static let elephantThreeOnGrass = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass")
+        internal static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three")
+        internal static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two")
+      }
+      internal static let mastodonLogoBlack = ImageAsset(name: "Scene/Welcome/mastodon.logo.black")
+      internal static let mastodonLogoBlackLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.black.large")
+      internal static let mastodonLogo = ImageAsset(name: "Scene/Welcome/mastodon.logo")
+      internal static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large")
     }
-    internal static let mastodonLogoBlack = ImageAsset(name: "Welcome/mastodon.logo.black")
-    internal static let mastodonLogoBlackLarge = ImageAsset(name: "Welcome/mastodon.logo.black.large")
-    internal static let mastodonLogo = ImageAsset(name: "Welcome/mastodon.logo")
-    internal static let mastodonLogoLarge = ImageAsset(name: "Welcome/mastodon.logo.large")
   }
 }
 // swiftlint:enable identifier_name line_length nesting type_body_length type_name
diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift
index 4b2175e86..6eed41a29 100644
--- a/Mastodon/Generated/Strings.swift
+++ b/Mastodon/Generated/Strings.swift
@@ -35,14 +35,6 @@ internal enum L10n {
         /// Server Error
         internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
       }
-      internal enum SignOut {
-        /// Sign Out
-        internal static let confirm = L10n.tr("Localizable", "Common.Alerts.SignOut.Confirm")
-        /// Are you sure you want to sign out?
-        internal static let message = L10n.tr("Localizable", "Common.Alerts.SignOut.Message")
-        /// Sign out
-        internal static let title = L10n.tr("Localizable", "Common.Alerts.SignOut.Title")
-      }
       internal enum SignUpFailure {
         /// Sign Up Failure
         internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title")
@@ -211,6 +203,8 @@ internal enum L10n {
           internal static let loadingMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadingMissingPosts")
           /// Load missing posts
           internal static let loadMissingPosts = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.LoadMissingPosts")
+          /// Show more replies
+          internal static let showMoreReplies = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.ShowMoreReplies")
         }
       }
     }
@@ -230,6 +224,10 @@ internal enum L10n {
       internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction")
       /// Type or paste what's on your mind
       internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder")
+      /// replying to %@
+      internal static func replyingToUser(_ p1: Any) -> String {
+        return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1))
+      }
       internal enum Attachment {
         /// This %@ is broken and can't be\nuploaded to Mastodon.
         internal static func attachmentBroken(_ p1: Any) -> String {
@@ -265,6 +263,10 @@ internal enum L10n {
         internal static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay")
         /// 1 Hour
         internal static let oneHour = L10n.tr("Localizable", "Scene.Compose.Poll.OneHour")
+        /// Option %ld
+        internal static func optionNumber(_ p1: Int) -> String {
+          return L10n.tr("Localizable", "Scene.Compose.Poll.OptionNumber", p1)
+        }
         /// 7 Days
         internal static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays")
         /// 6 Hours
@@ -589,59 +591,31 @@ internal enum L10n {
         internal static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm")
       }
     }
-    internal enum Settings {
-      /// Settings
-      internal static let title = L10n.tr("Localizable", "Scene.Settings.Title")
-      internal enum Section {
-        internal enum Appearance {
-          /// Automatic
-          internal static let automatic = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Automatic")
-          /// Always Dark
-          internal static let dark = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Dark")
-          /// Always Light
-          internal static let light = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Light")
-          /// Appearance
-          internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title")
+    internal enum Thread {
+      /// Post
+      internal static let backTitle = L10n.tr("Localizable", "Scene.Thread.BackTitle")
+      /// Post from %@
+      internal static func title(_ p1: Any) -> String {
+        return L10n.tr("Localizable", "Scene.Thread.Title", String(describing: p1))
+      }
+      internal enum Favorite {
+        /// %@ favorites
+        internal static func multiple(_ p1: Any) -> String {
+          return L10n.tr("Localizable", "Scene.Thread.Favorite.Multiple", String(describing: p1))
         }
-        internal enum Boringzone {
-          /// Privacy Policy
-          internal static let privacy = L10n.tr("Localizable", "Scene.Settings.Section.Boringzone.Privacy")
-          /// Terms of Service
-          internal static let terms = L10n.tr("Localizable", "Scene.Settings.Section.Boringzone.Terms")
-          /// The Boring zone
-          internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Boringzone.Title")
+        /// %@ favorite
+        internal static func single(_ p1: Any) -> String {
+          return L10n.tr("Localizable", "Scene.Thread.Favorite.Single", String(describing: p1))
         }
-        internal enum Notifications {
-          /// Reblogs my post
-          internal static let boosts = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Boosts")
-          /// Favorites my post
-          internal static let favorites = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Favorites")
-          /// Follows me
-          internal static let follows = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Follows")
-          /// Mentions me
-          internal static let mentions = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Mentions")
-          /// Notifications
-          internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Title")
-          internal enum Trigger {
-            /// anyone
-            internal static let anyone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Anyone")
-            /// anyone I follow
-            internal static let follow = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follow")
-            /// a follower
-            internal static let follower = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Follower")
-            /// no one
-            internal static let noone = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Noone")
-            /// Notify me when
-            internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Title")
-          }
+      }
+      internal enum Reblog {
+        /// %@ reblogs
+        internal static func multiple(_ p1: Any) -> String {
+          return L10n.tr("Localizable", "Scene.Thread.Reblog.Multiple", String(describing: p1))
         }
-        internal enum Spicyzone {
-          /// Clear Media Cache
-          internal static let clear = L10n.tr("Localizable", "Scene.Settings.Section.Spicyzone.Clear")
-          /// Sign Out
-          internal static let signout = L10n.tr("Localizable", "Scene.Settings.Section.Spicyzone.Signout")
-          /// The spicy zone
-          internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Spicyzone.Title")
+        /// %@ reblog
+        internal static func single(_ p1: Any) -> String {
+          return L10n.tr("Localizable", "Scene.Thread.Reblog.Single", String(describing: p1))
         }
       }
     }
diff --git a/Mastodon/Helper/MastodonField.swift b/Mastodon/Helper/MastodonField.swift
index e828602e4..5f652b32c 100644
--- a/Mastodon/Helper/MastodonField.swift
+++ b/Mastodon/Helper/MastodonField.swift
@@ -11,7 +11,7 @@ import ActiveLabel
 enum MastodonField {
     
     static func parse(field string: String) -> ParseResult {
-        let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.]+)?)")
+        let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?)")
         let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
         let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
         
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift
index f8c99c13f..25322e216 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift
@@ -33,6 +33,10 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
 // MARK: - ActionToolbarContainerDelegate
 extension StatusTableViewCellDelegate where Self: StatusProvider {
     
+    func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, replyButtonDidPressed sender: UIButton) {
+        StatusProviderFacade.responseToStatusReplyAction(provider: self, cell: cell)
+    }
+    
     func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) {
         StatusProviderFacade.responseToStatusReblogAction(provider: self, cell: cell)
     }
@@ -46,9 +50,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
         guard let item = item(for: cell, indexPath: nil) else { return }
             
         switch item {
-        case .homeTimelineIndex(_, let attribute):
-            attribute.isStatusTextSensitive = false
-        case .status(_, let attribute):
+        case .homeTimelineIndex(_, let attribute),
+            .status(_, let attribute),
+            .root(_, let attribute),
+            .reply(_, let attribute),
+            .leaf(_, let attribute):
             attribute.isStatusTextSensitive = false
         default:
             return
@@ -81,9 +87,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
         guard let item = item(for: cell, indexPath: nil) else { return }
         
         switch item {
-        case .homeTimelineIndex(_, let attribute):
-            attribute.isStatusSensitive = false
-        case .status(_, let attribute):
+        case .homeTimelineIndex(_, let attribute),
+            .status(_, let attribute),
+            .root(_, let attribute),
+            .reply(_, let attribute),
+            .leaf(_, let attribute):
             attribute.isStatusSensitive = false
         default:
             return
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
index 32915baf9..cd6cbf589 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
@@ -12,9 +12,6 @@ import os.log
 import UIKit
 
 extension StatusTableViewCellDelegate where Self: StatusProvider {
-    // TODO:
-    // func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
-    // }
     
     func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
         // update poll when status appear
@@ -102,6 +99,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
             }
             .store(in: &disposeBag)
     }
+    
+    func handleTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        StatusProviderFacade.coordinateToStatusThreadScene(for: .primary, provider: self, indexPath: indexPath)
+    }
+    
 }
 
 extension StatusTableViewCellDelegate where Self: StatusProvider {}
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider.swift b/Mastodon/Protocol/StatusProvider/StatusProvider.swift
index e16343ee6..8e27a2207 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider.swift
@@ -13,7 +13,7 @@ import CoreDataStack
 protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
     // async
     func status() -> Future<Status?, Never>
-    func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never>
+    func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never>
     func status(for cell: UICollectionViewCell) -> Future<Status?, Never>
     
     // sync
diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
index abdc27902..0e26614c5 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
@@ -60,6 +60,54 @@ extension StatusProviderFacade {
             }
             .store(in: &provider.disposeBag)
     }
+    
+}
+
+extension StatusProviderFacade {
+    
+    static func coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, indexPath: IndexPath) {
+        _coordinateToStatusThreadScene(
+            for: target,
+            provider: provider,
+            status: provider.status(for: nil, indexPath: indexPath)
+        )
+    }
+    
+    static func coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
+        _coordinateToStatusThreadScene(
+            for: target,
+            provider: provider,
+            status: provider.status(for: cell, indexPath: nil)
+        )
+    }
+    
+    private static func _coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, status: Future<Status?, Never>) {
+        status
+            .sink { [weak provider] status in
+                guard let provider = provider else { return }
+                let _status: Status? = {
+                    switch target {
+                    case .primary:      return status?.reblog ?? status         // original status
+                    case .secondary:    return status                           // reblog or status
+                    }
+                }()
+                guard let status = _status else { return }
+                
+                let threadViewModel = CachedThreadViewModel(context: provider.context, status: status)
+                DispatchQueue.main.async {
+                    if provider.navigationController == nil {
+                        let from = provider.presentingViewController ?? provider
+                        provider.dismiss(animated: true) {
+                            provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
+                        }
+                    } else {
+                        provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: provider, transition: .show)
+                    }
+                }
+            }
+            .store(in: &provider.disposeBag)
+    }
+    
 }
 
 extension StatusProviderFacade {
@@ -229,7 +277,6 @@ extension StatusProviderFacade {
 }
 
 extension StatusProviderFacade {
- 
     
     static func responseToStatusReblogAction(provider: StatusProvider) {
         _responseToStatusReblogAction(
@@ -337,10 +384,41 @@ extension StatusProviderFacade {
 
 }
 
+extension StatusProviderFacade {
+    
+    static func responseToStatusReplyAction(provider: StatusProvider) {
+        _responseToStatusReplyAction(
+            provider: provider,
+            status: provider.status()
+        )
+    }
+    
+    static func responseToStatusReplyAction(provider: StatusProvider, cell: UITableViewCell) {
+        _responseToStatusReplyAction(
+            provider: provider,
+            status: provider.status(for: cell, indexPath: nil)
+        )
+    }
+    
+    private static func _responseToStatusReplyAction(provider: StatusProvider, status: Future<Status?, Never>) {
+        status
+            .sink { [weak provider] status in
+                guard let provider = provider else { return }
+                guard let status = status?.reblog ?? status else { return }
+                
+                let composeViewModel = ComposeViewModel(context: provider.context, composeKind: .reply(repliedToStatusObjectID: status.objectID))
+                provider.coordinator.present(scene: .compose(viewModel: composeViewModel), from: provider, transition: .modal(animated: true, completion: nil))
+            }
+            .store(in: &provider.context.disposeBag)
+        
+    }
+    
+}
+
 extension StatusProviderFacade {
     enum Target {
-        case primary        // original
-        case secondary      // attachment reblog or reply
+        case primary        // original status
+        case secondary      // wrapper status or reply (when needs. e.g tap header of status view)
     }
 }
  
diff --git a/Mastodon/Protocol/StatusTableViewControllerAspect.swift b/Mastodon/Protocol/StatusTableViewControllerAspect.swift
index ecd8291ff..f96998ea6 100644
--- a/Mastodon/Protocol/StatusTableViewControllerAspect.swift
+++ b/Mastodon/Protocol/StatusTableViewControllerAspect.swift
@@ -9,10 +9,12 @@ import UIKit
 import AVKit
 
 //   Check List                     Last Updated
-// - FavoriteViewController:            2021/4/8
+// - HomeViewController:                2021/4/13
+// - FavoriteViewController:            2021/4/14
 // - HashtagTimelineViewController:     2021/4/8
-// - UserTimelineViewController:        2021/4/8
-// * StatusTableViewControllerAspect:   2021/4/7
+// - UserTimelineViewController:        2021/4/13
+// - ThreadViewController:              2021/4/13
+// * StatusTableViewControllerAspect:   2021/4/12
 
 // (Fake) Aspect protocol to group common protocol extension implementations
 // Needs update related view controller when aspect interface changes
@@ -69,7 +71,7 @@ extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegat
     }
 }
 
-// [B4] StatusTableViewControllerAspect.aspectTableView(_:didEndDisplaying:forRowAt:)
+// [B4] aspectTableView(_:didEndDisplaying:forRowAt:)
 extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegate & StatusProvider {
     /// [Media] hook to notify video service
     func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
@@ -93,6 +95,14 @@ extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegat
     }
 }
 
+// [B5] aspectTableView(_:didSelectRowAt:)
+extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegate & StatusProvider {
+    /// [UI] hook to coordinator to thread
+    func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        handleTableView(tableView, didSelectRowAt: indexPath)
+    }
+}
+
 // MARK: - UITableViewDataSourcePrefetching [C]
 
 // [C1] aspectTableView(:prefetchRowsAt)
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json
index 6bce2b697..bd6f07f25 100644
--- a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json
@@ -23,9 +23,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0x00",
-          "green" : "0x00",
-          "red" : "0x00"
+          "blue" : "0x2E",
+          "green" : "0x2C",
+          "red" : "0x2C"
         }
       },
       "idiom" : "universal"
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json
index 55f84c267..23d03492f 100644
--- a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json
@@ -5,9 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0xFE",
-          "green" : "0xFF",
-          "red" : "0xFE"
+          "blue" : "254",
+          "green" : "255",
+          "red" : "254"
         }
       },
       "idiom" : "universal"
@@ -23,9 +23,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0x2E",
-          "green" : "0x2C",
-          "red" : "0x2C"
+          "blue" : "0x00",
+          "green" : "0x00",
+          "red" : "0x00"
         }
       },
       "idiom" : "universal"
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json
index d8f32572f..9fa2b261b 100644
--- a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json
@@ -5,9 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0xFF",
+          "blue" : "0xFE",
           "green" : "0xFF",
-          "red" : "0xFF"
+          "red" : "0xFE"
         }
       },
       "idiom" : "universal"
@@ -23,9 +23,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0x2B",
-          "green" : "0x23",
-          "red" : "0x1F"
+          "blue" : "0x3C",
+          "green" : "0x3A",
+          "red" : "0x3A"
         }
       },
       "idiom" : "universal"
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json
index d47050048..5da572b1d 100644
--- a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json
@@ -23,9 +23,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0x2B",
-          "green" : "0x23",
-          "red" : "0x1F"
+          "blue" : "0x3C",
+          "green" : "0x3A",
+          "red" : "0x3A"
         }
       },
       "idiom" : "universal"
diff --git a/Mastodon/Resources/Assets.xcassets/Profile/Banner/Contents.json b/Mastodon/Resources/Assets.xcassets/Human/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Profile/Banner/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Human/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/Contents.json
new file mode 100644
index 000000000..df869a35c
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+  "images" : [
+    {
+      "filename" : "emojiIconLight.pdf",
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "filename" : "emojiIconDark.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}
diff --git a/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconDark.pdf b/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconDark.pdf
new file mode 100644
index 000000000..77c6c2d32
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconDark.pdf
@@ -0,0 +1,97 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
+0.225600 0.613812 0.894400 scn
+48.000000 0.000000 m
+74.509674 0.000000 96.000000 21.490326 96.000000 48.000000 c
+96.000000 74.509666 74.509674 96.000000 48.000000 96.000000 c
+21.490332 96.000000 0.000000 74.509666 0.000000 48.000000 c
+0.000000 21.490326 21.490332 0.000000 48.000000 0.000000 c
+h
+48.000023 39.999962 m
+38.338688 39.999962 31.928020 41.125294 24.000021 42.666630 c
+22.189354 43.015961 18.666687 42.666630 18.666687 37.333294 c
+18.666687 26.666626 30.920021 13.333298 48.000023 13.333298 c
+65.077354 13.333298 77.333359 26.666626 77.333359 37.333294 c
+77.333359 42.666630 73.810692 43.018627 72.000023 42.666630 c
+64.072021 41.125294 57.661354 39.999962 48.000023 39.999962 c
+h
+38.666645 59.999981 m
+38.666645 54.845322 35.681877 50.666649 31.999979 50.666649 c
+28.318081 50.666649 25.333313 54.845322 25.333313 59.999981 c
+25.333313 65.154640 28.318081 69.333313 31.999979 69.333313 c
+35.681877 69.333313 38.666645 65.154640 38.666645 59.999981 c
+h
+63.999977 50.666649 m
+67.681877 50.666649 70.666641 54.845322 70.666641 59.999981 c
+70.666641 65.154640 67.681877 69.333313 63.999977 69.333313 c
+60.318081 69.333313 57.333313 65.154640 57.333313 59.999981 c
+57.333313 54.845322 60.318081 50.666649 63.999977 50.666649 c
+h
+48.000000 34.666645 m
+32.000000 34.666645 24.000000 37.333313 24.000000 37.333313 c
+24.000000 37.333313 29.333334 26.666649 48.000000 26.666649 c
+66.666672 26.666649 72.000000 37.333313 72.000000 37.333313 c
+72.000000 37.333313 64.000000 34.666645 48.000000 34.666645 c
+h
+f*
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  1603
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 96.000000 96.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Type /Catalog
+     /Pages 5 0 R
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000001693 00000 n
+0000001716 00000 n
+0000001889 00000 n
+0000001963 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+2022
+%%EOF
\ No newline at end of file
diff --git a/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconLight.pdf b/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconLight.pdf
new file mode 100644
index 000000000..61f471d6d
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Human/face.smiling.adaptive.imageset/emojiIconLight.pdf
@@ -0,0 +1,103 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
+0.168627 0.564706 0.850980 scn
+90.000000 48.000000 m
+90.000000 24.804031 71.195969 6.000000 48.000000 6.000000 c
+24.804039 6.000000 6.000000 24.804031 6.000000 48.000000 c
+6.000000 71.195961 24.804041 90.000000 48.000000 90.000000 c
+71.195969 90.000000 90.000000 71.195961 90.000000 48.000000 c
+h
+48.000000 0.000000 m
+74.509674 0.000000 96.000000 21.490326 96.000000 48.000000 c
+96.000000 74.509666 74.509674 96.000000 48.000000 96.000000 c
+21.490332 96.000000 0.000000 74.509666 0.000000 48.000000 c
+0.000000 21.490326 21.490332 0.000000 48.000000 0.000000 c
+h
+38.666645 59.999981 m
+38.666645 54.845322 35.681877 50.666649 31.999979 50.666649 c
+28.318081 50.666649 25.333313 54.845322 25.333313 59.999981 c
+25.333313 65.154640 28.318081 69.333313 31.999979 69.333313 c
+35.681877 69.333313 38.666645 65.154640 38.666645 59.999981 c
+h
+63.999977 50.666649 m
+67.681877 50.666649 70.666641 54.845322 70.666641 59.999981 c
+70.666641 65.154640 67.681877 69.333313 63.999977 69.333313 c
+60.318081 69.333313 57.333313 65.154640 57.333313 59.999981 c
+57.333313 54.845322 60.318081 50.666649 63.999977 50.666649 c
+h
+48.000023 39.999962 m
+38.338688 39.999962 31.928020 41.125294 24.000021 42.666630 c
+22.189354 43.015961 18.666687 42.666630 18.666687 37.333294 c
+18.666687 26.666626 30.920021 13.333298 48.000023 13.333298 c
+65.077354 13.333298 77.333359 26.666626 77.333359 37.333294 c
+77.333359 42.666630 73.810684 43.018627 72.000023 42.666630 c
+64.072021 41.125294 57.661354 39.999962 48.000023 39.999962 c
+h
+24.000000 37.333313 m
+24.000000 37.333313 32.000000 34.666645 48.000000 34.666645 c
+64.000000 34.666645 72.000000 37.333313 72.000000 37.333313 c
+72.000000 37.333313 66.666672 26.666649 48.000000 26.666649 c
+29.333334 26.666649 24.000000 37.333313 24.000000 37.333313 c
+h
+f*
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  1869
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 96.000000 96.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Type /Catalog
+     /Pages 5 0 R
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000001959 00000 n
+0000001982 00000 n
+0000002155 00000 n
+0000002229 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+2288
+%%EOF
\ No newline at end of file
diff --git a/Mastodon/Resources/Assets.xcassets/Profile/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Compose/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Profile/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Compose/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Compose/background.colorset/Contents.json
similarity index 76%
rename from Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Compose/background.colorset/Contents.json
index 3338422aa..82edd034b 100644
--- a/Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Scene/Compose/background.colorset/Contents.json
@@ -5,9 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0xFE",
-          "green" : "0xFF",
-          "red" : "0xFE"
+          "blue" : "1.000",
+          "green" : "1.000",
+          "red" : "1.000"
         }
       },
       "idiom" : "universal"
@@ -23,9 +23,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0x00",
-          "green" : "0x00",
-          "red" : "0x00"
+          "blue" : "30",
+          "green" : "28",
+          "red" : "28"
         }
       },
       "idiom" : "universal"
diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Compose/toolbar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Compose/toolbar.background.colorset/Contents.json
new file mode 100644
index 000000000..4ef70f635
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Scene/Compose/toolbar.background.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "222",
+          "green" : "216",
+          "red" : "214"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "43",
+          "green" : "43",
+          "red" : "43"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Profile/Banner/bio.edit.background.gray.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/bio.edit.background.gray.colorset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Profile/Banner/bio.edit.background.gray.colorset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/bio.edit.background.gray.colorset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Profile/Banner/name.edit.background.gray.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/name.edit.background.gray.colorset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Profile/Banner/name.edit.background.gray.colorset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/name.edit.background.gray.colorset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Profile/Banner/username.gray.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/username.gray.colorset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Profile/Banner/username.gray.colorset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Profile/Banner/username.gray.colorset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Profile/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Profile/Contents.json
new file mode 100644
index 000000000..6e965652d
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Scene/Profile/Contents.json
@@ -0,0 +1,9 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "provides-namespace" : true
+  }
+}
diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/Contents.json
new file mode 100644
index 000000000..6e965652d
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/Contents.json
@@ -0,0 +1,9 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "provides-namespace" : true
+  }
+}
diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/Contents.json
new file mode 100644
index 000000000..6e965652d
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/Contents.json
@@ -0,0 +1,9 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "provides-namespace" : true
+  }
+}
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/background.cyan.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/background.cyan.colorset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/background.cyan.colorset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/background.cyan.colorset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/untitled10007Group61.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/untitled10007Group61.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/untitled10007Group61.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/untitled10007Group61.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/untitled10007Group61@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/untitled10007Group61@2x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/untitled10007Group61@2x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/untitled10007Group61@2x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/untitled10007Group61@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/untitled10007Group61@3x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/cloud.base.imageset/untitled10007Group61@3x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.imageset/untitled10007Group61@3x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@2x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@2x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@2x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@3x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@3x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.on.airplane.with.contrail.imageset/untitled10006Group21@3x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@2x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@2x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@2x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@3x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@3x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.imageset/untitled10003Group11@3x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@2x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@2x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@2x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@3x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@3x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three.imageset/untitled10005Group101@3x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@2x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@2x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@2x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@3x.png
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@3x.png
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two.imageset/untitled10004Group111@3x.png
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.imageset/mastodon.logo.black.pdf
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.large.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.large.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.black.large.imageset/mastodon.logo.black.large.pdf
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.imageset/logotypeFull1.pdf b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logotypeFull1.pdf
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.imageset/logotypeFull1.pdf
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.imageset/logotypeFull1.pdf
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.large.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.large.imageset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/Contents.json
diff --git a/Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.large.imageset/logotypeFull1.large.pdf b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logotypeFull1.large.pdf
similarity index 100%
rename from Mastodon/Resources/Assets.xcassets/Welcome/mastodon.logo.large.imageset/logotypeFull1.large.pdf
rename to Mastodon/Resources/Assets.xcassets/Scene/Welcome/mastodon.logo.large.imageset/logotypeFull1.large.pdf
diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings
index d584be424..b6fd18128 100644
--- a/Mastodon/Resources/en.lproj/Localizable.strings
+++ b/Mastodon/Resources/en.lproj/Localizable.strings
@@ -71,6 +71,7 @@ Your account looks like this to them.";
 "Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended.";
 "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
 "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
+"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
 "Common.Countable.Photo.Multiple" = "photos";
 "Common.Countable.Photo.Single" = "photo";
 "Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
@@ -88,10 +89,12 @@ uploaded to Mastodon.";
 "Scene.Compose.Poll.DurationTime" = "Duration: %@";
 "Scene.Compose.Poll.OneDay" = "1 Day";
 "Scene.Compose.Poll.OneHour" = "1 Hour";
+"Scene.Compose.Poll.OptionNumber" = "Option %ld";
 "Scene.Compose.Poll.SevenDays" = "7 Days";
 "Scene.Compose.Poll.SixHours" = "6 Hours";
 "Scene.Compose.Poll.ThirtyMinutes" = "30 minutes";
 "Scene.Compose.Poll.ThreeDays" = "3 Days";
+"Scene.Compose.ReplyingToUser" = "replying to %@";
 "Scene.Compose.Title.NewPost" = "New Post";
 "Scene.Compose.Title.NewReply" = "New Reply";
 "Scene.Compose.Visibility.Direct" = "Only people I mention";
@@ -210,5 +213,11 @@ any server.";
 "Scene.Settings.Section.Spicyzone.Signout" = "Sign Out";
 "Scene.Settings.Section.Spicyzone.Title" = "The spicy zone";
 "Scene.Settings.Title" = "Settings";
+"Scene.Thread.BackTitle" = "Post";
+"Scene.Thread.Favorite.Multiple" = "%@ favorites";
+"Scene.Thread.Favorite.Single" = "%@ favorite";
+"Scene.Thread.Reblog.Multiple" = "%@ reblogs";
+"Scene.Thread.Reblog.Single" = "%@ reblog";
+"Scene.Thread.Title" = "Post from %@";
 "Scene.Welcome.Slogan" = "Social networking
-back in your hands.";
\ No newline at end of file
+back in your hands.";
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift
index 0163a54cd..95e9b4f1a 100644
--- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift
+++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift
@@ -6,9 +6,24 @@
 //
 
 import UIKit
+import Combine
 
 final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCell {
     
+    var disposeBag = Set<AnyCancellable>()
+    
+    let statusView = StatusView()
+    
+    let framePublisher = PassthroughSubject<CGRect, Never>()
+
+    override func prepareForReuse() {
+        super.prepareForReuse()
+        
+        statusView.isStatusTextSensitive = false
+        statusView.cleanUpContentWarning()
+        disposeBag.removeAll()
+    }
+    
     override init(frame: CGRect) {
         super.init(frame: frame)
         _init()
@@ -19,12 +34,29 @@ final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCel
         _init()
     }
     
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        framePublisher.send(bounds)
+    }
+    
 }
 
 extension ComposeRepliedToStatusContentCollectionViewCell {
     
     private func _init() {
+        backgroundColor = .clear
+        statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.systemBackground.color
+
+        statusView.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(statusView)
+        NSLayoutConstraint.activate([
+            statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
+            statusView.leadingAnchor.constraint(equalTo:  contentView.readableContentGuide.leadingAnchor),
+            contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor),
+            contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
+        ])
         
+        statusView.actionToolbarContainer.isHidden = true
     }
     
 }
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentTableViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift
similarity index 98%
rename from Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentTableViewCell.swift
rename to Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift
index bc087c990..141a944fd 100644
--- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentTableViewCell.swift
+++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift
@@ -1,5 +1,5 @@
 //
-//  ComposeStatusAttachmentTableViewCell.swift
+//  ComposeStatusAttachmentCollectionViewCell.swift
 //  Mastodon
 //
 //  Created by MainasuK Cirno on 2021-3-17.
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentCollectionViewCell.swift
index f1fe6b541..2b71e55f3 100644
--- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentCollectionViewCell.swift
+++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentCollectionViewCell.swift
@@ -91,7 +91,7 @@ extension ComposeStatusContentCollectionViewCell {
         
         statusContentWarningEditorView.containerView.isHidden = true
     }
-    
+
 }
 
 // MARK: - TextEditorViewChangeObserver
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift
index 2c321f51f..39a12f954 100644
--- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift
+++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift
@@ -29,7 +29,7 @@ final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionVi
     
     override var isHighlighted: Bool {
         didSet {
-            pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.secondarySystemBackground.color : Asset.Colors.Background.systemBackground.color
+            pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.tertiarySystemBackground.color : Asset.Colors.Background.secondarySystemBackground.color
             pollOptionView.plusCircleImageView.tintColor = isHighlighted ? Asset.Colors.Button.normal.color.withAlphaComponent(0.5) : Asset.Colors.Button.normal.color
         }
     }
@@ -82,7 +82,7 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
         pollOptionView.optionTextField.isHidden = true
         pollOptionView.plusCircleImageView.isHidden = false
         
-        pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemBackground.color
+        pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
         setupBorderColor()
         
         pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift
index 3e82cd51a..b463f13ac 100644
--- a/Mastodon/Scene/Compose/ComposeViewController.swift
+++ b/Mastodon/Scene/Compose/ComposeViewController.swift
@@ -31,7 +31,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
         button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
         button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
         button.setTitleColor(.white, for: .normal)
-        button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 16, bottom: 3, right: 16)
+        button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16)     // set 28pt height
         button.adjustsImageWhenHighlighted = false
         return button
     }()
@@ -49,7 +49,8 @@ final class ComposeViewController: UIViewController, NeedsDependency {
         collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
         collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
         collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
-        collectionView.backgroundColor = Asset.Colors.Background.systemBackground.color
+        collectionView.backgroundColor = Asset.Scene.Compose.background.color
+        collectionView.alwaysBounceVertical = true
         return collectionView
     }()
     
@@ -66,20 +67,9 @@ final class ComposeViewController: UIViewController, NeedsDependency {
         return view
     }()
     
-    let composeToolbarView: ComposeToolbarView = {
-        let composeToolbarView = ComposeToolbarView()
-        let text = UITextView()
-        let inputView = UIInputView(frame: .init(x: 0, y: 0, width: 40, height: 40), inputViewStyle: .keyboard)
-        text.inputAccessoryView = inputView
-        composeToolbarView.backgroundColor = inputView.backgroundColor
-        return composeToolbarView
-    }()
+    let composeToolbarView = ComposeToolbarView()
     var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
-    let composeToolbarBackgroundView: UIView = {
-        let backgroundView = UIView()
-        backgroundView.backgroundColor = .secondarySystemBackground
-        return backgroundView
-    }()
+    let composeToolbarBackgroundView = UIView()
     
     private(set) lazy var imagePicker: PHPickerViewController = {
         var configuration = PHPickerConfiguration()
@@ -135,7 +125,7 @@ extension ComposeViewController {
                 self.title = title
             }
             .store(in: &disposeBag)
-        view.backgroundColor = Asset.Colors.Background.systemBackground.color
+        view.backgroundColor = Asset.Scene.Compose.background.color
         navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
         navigationItem.rightBarButtonItem = publishBarButtonItem
         publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
@@ -202,14 +192,27 @@ extension ComposeViewController {
         )
         .sink(receiveValue: { [weak self] isShow, state, endFrame, isCustomEmojiComposing in
             guard let self = self else { return }
+                        
+            let extraMargin: CGFloat = {
+                if self.view.safeAreaInsets.bottom == .zero {
+                    // needs extra margin for zero inset device to workaround UIKit issue
+                    return self.composeToolbarView.frame.height
+                } else {
+                    // default some magic 16 extra margin
+                    return 16
+                }
+            }()
+            
+            // update keyboard background color
 
             guard isShow, state == .dock else {
-                self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom
-                self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom
+                self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
+                self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
                 UIView.animate(withDuration: 0.3) {
                     self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
                     self.view.layoutIfNeeded()
                 }
+                self.updateKeyboardBackground(isKeyboardDisplay: isShow)
                 return
             }
             // isShow AND dock state
@@ -218,22 +221,23 @@ extension ComposeViewController {
             let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
             let padding = contentFrame.maxY - endFrame.minY
             guard padding > 0 else {
-                self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom
-                self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom
+                self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
+                self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
                 UIView.animate(withDuration: 0.3) {
                     self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
                     self.view.layoutIfNeeded()
                 }
+                self.updateKeyboardBackground(isKeyboardDisplay: false)
                 return
             }
 
-            // add 16pt margin
-            self.collectionView.contentInset.bottom = padding + 16
-            self.collectionView.verticalScrollIndicatorInsets.bottom = padding + 16
+            self.collectionView.contentInset.bottom = padding + extraMargin
+            self.collectionView.verticalScrollIndicatorInsets.bottom = padding + extraMargin
             UIView.animate(withDuration: 0.3) {
                 self.composeToolbarViewBottomLayoutConstraint.constant = padding
                 self.view.layoutIfNeeded()
             }
+            self.updateKeyboardBackground(isKeyboardDisplay: isShow)
         })
         .store(in: &disposeBag)
 
@@ -266,13 +270,17 @@ extension ComposeViewController {
             .store(in: &disposeBag)
         
         // bind visibility toolbar UI
-        viewModel.selectedStatusVisibility
-            .receive(on: DispatchQueue.main)
-            .sink { [weak self] type in
-                guard let self = self else { return }
-                self.composeToolbarView.visibilityButton.setImage(type.image, for: .normal)
-            }
-            .store(in: &disposeBag)
+        Publishers.CombineLatest(
+            viewModel.selectedStatusVisibility,
+            viewModel.traitCollectionDidChangePublisher
+        )
+        .receive(on: DispatchQueue.main)
+        .sink { [weak self] type, _ in
+            guard let self = self else { return }
+            let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
+            self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
+        }
+        .store(in: &disposeBag)
         
         viewModel.characterCount
             .receive(on: DispatchQueue.main)
@@ -324,6 +332,24 @@ extension ComposeViewController {
                 }
             })
             .store(in: &disposeBag)
+        
+        // setup snap behavior
+        Publishers.CombineLatest(
+            viewModel.repliedToCellFrame.removeDuplicates().eraseToAnyPublisher(),
+            viewModel.collectionViewState.eraseToAnyPublisher()
+        )
+        .receive(on: DispatchQueue.main)
+        .sink { [weak self] repliedToCellFrame, collectionViewState in
+            guard let self = self else { return }
+            guard repliedToCellFrame != .zero else { return }
+            switch collectionViewState {
+            case .fold:
+                self.collectionView.contentInset.top = -repliedToCellFrame.height
+            case .expand:
+                self.collectionView.contentInset.top = 0
+            }
+        }
+        .store(in: &disposeBag)
     }
     
     override func viewWillAppear(_ animated: Bool) {
@@ -336,6 +362,12 @@ extension ComposeViewController {
         }
     }
     
+    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+        
+        viewModel.traitCollectionDidChangePublisher.send()
+    }
+    
 }
 
 extension ComposeViewController {
@@ -463,6 +495,20 @@ extension ComposeViewController {
         imagePicker.delegate = self
         return imagePicker
     }
+    
+    private func updateKeyboardBackground(isKeyboardDisplay: Bool) {
+        guard isKeyboardDisplay else {
+            composeToolbarBackgroundView.backgroundColor = Asset.Scene.Compose.toolbarBackground.color
+            return
+        }
+        composeToolbarBackgroundView.backgroundColor = UIColor(dynamicProvider: { traitCollection -> UIColor in
+            // avoid elevated color
+            switch traitCollection.userInterfaceStyle {
+            case .light:        return .white
+            default:            return .black
+            }
+        })
+    }
 
 }
 
@@ -538,7 +584,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
             os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
 
             let stringRange = NSRange(location: 0, length: string.length)
-            let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.]+)?|#([^\\s.]+))")
+            let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))")
             // accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
             // precondition :\B with following space 
             let emojiMatches = string.matches(pattern: "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))")
@@ -727,6 +773,32 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
     
 }
 
+// MARK: - UIScrollViewDelegate
+extension ComposeViewController {
+    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
+        guard scrollView === collectionView else { return }
+
+        let repliedToCellFrame = viewModel.repliedToCellFrame.value
+        guard repliedToCellFrame != .zero else { return }
+        let throttle = viewModel.repliedToCellFrame.value.height - scrollView.adjustedContentInset.top
+        // print("\(throttle) - \(scrollView.contentOffset.y)")
+
+        switch viewModel.collectionViewState.value {
+        case .fold:
+            if scrollView.contentOffset.y < throttle {
+                viewModel.collectionViewState.value = .expand
+            }
+            os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function)
+
+        case .expand:
+            if scrollView.contentOffset.y > -44 {
+                viewModel.collectionViewState.value = .fold
+                os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function)
+            }
+        }
+    }
+}
+
 // MARK: - UITableViewDelegate
 extension ComposeViewController: UICollectionViewDelegate {
     
@@ -763,6 +835,10 @@ extension ComposeViewController: UICollectionViewDelegate {
 
 // MARK: - UIAdaptivePresentationControllerDelegate
 extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
+    
+    func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
+        return .fullScreen
+    }
 
     func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
         return viewModel.shouldDismiss.value
diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift
index 4d5a39be1..6581e1fb1 100644
--- a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift
+++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift
@@ -27,6 +27,7 @@ extension ComposeViewModel {
             dependency: dependency,
             managedObjectContext: context.managedObjectContext,
             composeKind: composeKind,
+            repliedToCellFrameSubscriber: repliedToCellFrame,
             customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
             textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
             composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
diff --git a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
index c3e903812..fd3f5bce0 100644
--- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
+++ b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
@@ -8,6 +8,7 @@
 import os.log
 import Foundation
 import Combine
+import CoreDataStack
 import GameplayKit
 import MastodonSDK
 
@@ -64,6 +65,15 @@ extension ComposeViewModel.PublishState {
                 guard viewModel.isPollComposing.value else { return nil }
                 return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
             }()
+            let inReplyToID: Mastodon.Entity.Status.ID? = {
+                guard case let .reply(repliedToStatusObjectID) = viewModel.composeKind else { return nil }
+                var id: Mastodon.Entity.Status.ID?
+                viewModel.context.managedObjectContext.performAndWait {
+                    guard let replyTo = viewModel.context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return }
+                    id = replyTo.id
+                }
+                return id
+            }()
             let sensitive: Bool = viewModel.isContentWarningComposing.value
             let spoilerText: String? = {
                 let text = viewModel.composeStatusAttribute.contentWarningContent.value.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -105,6 +115,7 @@ extension ComposeViewModel.PublishState {
                         mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
                         pollOptions: pollOptions,
                         pollExpiresIn: pollExpiresIn,
+                        inReplyToID: inReplyToID,
                         sensitive: sensitive,
                         spoilerText: spoilerText,
                         visibility: visibility
diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift
index f52c38a17..ef744d0b3 100644
--- a/Mastodon/Scene/Compose/ComposeViewModel.swift
+++ b/Mastodon/Scene/Compose/ComposeViewModel.swift
@@ -26,9 +26,11 @@ final class ComposeViewModel {
     let isPollComposing = CurrentValueSubject<Bool, Never>(false)
     let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
     let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
-    let selectedStatusVisibility = CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>(.public)
+    let selectedStatusVisibility: CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>
     let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
     let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
+    let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void())      // use CurrentValueSubject to make intial event emit
+    let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
     
     // output
     var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
@@ -55,6 +57,7 @@ final class ComposeViewModel {
     let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
     let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
     let characterCount = CurrentValueSubject<Int, Never>(0)
+    let collectionViewState = CurrentValueSubject<CollectionViewState, Never>(.fold)
     
     // for hashtag: #<hashag>' '
     // for mention: @<mention>' '
@@ -83,10 +86,40 @@ final class ComposeViewModel {
         case .post, .hashtag, .mention:       self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newPost)
         case .reply:                          self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
         }
+        self.selectedStatusVisibility = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value?.user.locked == true ? .private : .public)
         self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
         self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
         // end init
-        if case let .hashtag(text) = composeKind {
+        if case let .reply(repliedToStatusObjectID) = composeKind {
+            context.managedObjectContext.performAndWait {
+                guard let status = context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return }
+                let composeAuthor: MastodonUser? = {
+                    guard let objectID = self.activeAuthentication.value?.user.objectID else { return nil }
+                    guard let author = context.managedObjectContext.object(with: objectID) as? MastodonUser else { return nil }
+                    return author
+                }()
+                
+                var mentionAccts: [String] = []
+                if composeAuthor?.id != status.author.id {
+                    mentionAccts.append("@" + status.author.acct)
+                }
+                let mentions = (status.mentions ?? Set())
+                    .sorted(by: { $0.index.intValue < $1.index.intValue })
+                    .filter { $0.id != composeAuthor?.id }
+                for mention in mentions {
+                    mentionAccts.append("@" + mention.acct)
+                }
+                for acct in mentionAccts {
+                    UITextChecker.learnWord(acct)
+                }
+                
+                let initialComposeContent = mentionAccts.joined(separator: " ")
+                let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
+                self.preInsertedContent = preInsertedContent
+                self.composeStatusAttribute.composeContent.value = preInsertedContent
+            }
+            
+        } else if case let .hashtag(text) = composeKind {
             let initialComposeContent = "#" + text
             UITextChecker.learnWord(initialComposeContent)
             let preInsertedContent = initialComposeContent + " "
@@ -346,6 +379,13 @@ final class ComposeViewModel {
     
 }
 
+extension ComposeViewModel {
+    enum CollectionViewState {
+        case fold       // snap to input
+        case expand     // snap to reply
+    }
+}
+
 extension ComposeViewModel {
     func createNewPollOptionIfPossible() {
         guard pollOptionAttributes.value.count < 4 else { return }
diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift
index efe408265..99288a5e0 100644
--- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift
+++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift
@@ -41,7 +41,10 @@ final class ComposeToolbarView: UIView {
     let emojiButton: UIButton = {
         let button = HighlightDimmableButton()
         ComposeToolbarView.configureToolbarButtonAppearance(button: button)
-        button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
+        let image = Asset.Human.faceSmilingAdaptive.image
+            .af.imageScaled(to: CGSize(width: 20, height: 20))
+            .withRenderingMode(.alwaysTemplate)
+        button.setImage(image, for: .normal)
         return button
     }()
     
@@ -80,8 +83,12 @@ final class ComposeToolbarView: UIView {
 }
 
 extension ComposeToolbarView {
+    
     private func _init() {
-        backgroundColor = .secondarySystemBackground
+        // magic keyboard color (iOS 14):
+        // light with white background: RGB 214 216 222
+        // dark with black background: RGB 43 43 43
+        backgroundColor = Asset.Scene.Compose.toolbarBackground.color
         
         let stackView = UIStackView()
         stackView.axis = .horizontal
@@ -125,9 +132,18 @@ extension ComposeToolbarView {
         pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
         emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
         contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside)
-        visibilityButton.menu = createVisibilityContextMenu()
+        visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle)
         visibilityButton.showsMenuAsPrimaryAction = true
+        
+        updateToolbarButtonUserInterfaceStyle()
     }
+    
+    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+        
+        updateToolbarButtonUserInterfaceStyle()
+    }
+    
 }
 
 extension ComposeToolbarView {
@@ -152,12 +168,16 @@ extension ComposeToolbarView {
             }
         }
         
-        var image: UIImage {
+        func image(interfaceStyle: UIUserInterfaceStyle) -> UIImage {
             switch self {
-            case .public: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
-            case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
-            case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
-            case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
+            case .public:
+                switch interfaceStyle {
+                case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
+                default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
+                }
+            case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
+            case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
+            case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))!
             }
         }
         
@@ -182,6 +202,23 @@ extension ComposeToolbarView {
         button.layer.cornerCurve = .continuous
     }
     
+    private func updateToolbarButtonUserInterfaceStyle() {
+        switch traitCollection.userInterfaceStyle {
+        case .light:
+            mediaButton.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
+            contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
+
+        case .dark:
+            mediaButton.setImage(UIImage(systemName: "photo.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
+            contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
+
+        default:
+            assertionFailure()
+        }
+        
+        visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle)
+    }
+    
     private func createMediaContextMenu() -> UIMenu {
         var children: [UIMenuElement] = []
         let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
@@ -208,9 +245,9 @@ extension ComposeToolbarView {
         return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
     }
     
-    private func createVisibilityContextMenu() -> UIMenu {
+    private func createVisibilityContextMenu(interfaceStyle: UIUserInterfaceStyle) -> UIMenu {
         let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in
-            UIAction(title: type.title, image: type.image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
+            UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
                 guard let self = self else { return }
                 os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue)
                 self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type)
diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+StatusProvider.swift
index 23068b7bc..191ad374d 100644
--- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+StatusProvider.swift
+++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+StatusProvider.swift
@@ -18,14 +18,14 @@ extension HashtagTimelineViewController: StatusProvider {
         return Future { promise in promise(.success(nil)) }
     }
     
-    func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
+    func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
         return Future { promise in
             guard let diffableDataSource = self.viewModel.diffableDataSource else {
                 assertionFailure()
                 promise(.success(nil))
                 return
             }
-            guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
+            guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
                   let item = diffableDataSource.itemIdentifier(for: indexPath) else {
                 promise(.success(nil))
                 return
diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift
index c9bf87410..ea1a03aa9 100644
--- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift
+++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift
@@ -57,7 +57,7 @@ extension HashtagTimelineViewController {
         titleView.update(title: viewModel.hashtag, subtitle: nil)
         navigationItem.titleView = titleView
         
-        view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+        view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
         
         navigationItem.rightBarButtonItem = composeBarButtonItem
         
@@ -218,6 +218,10 @@ extension HashtagTimelineViewController: UITableViewDelegate {
     func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
         aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
     }
+    
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        aspectTableView(tableView, didSelectRowAt: indexPath)
+    }
 }
 
 // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift
index 26f32a33c..ed7b3a844 100644
--- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift
@@ -28,7 +28,8 @@ extension HashtagTimelineViewModel {
             managedObjectContext: context.managedObjectContext,
             timestampUpdatePublisher: timestampUpdatePublisher,
             statusTableViewCellDelegate: statusTableViewCellDelegate,
-            timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
+            timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
+            threadReplyLoaderTableViewCellDelegate: nil
         )
     }
 }
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
index cc051f33e..401e4fc14 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
@@ -33,8 +33,9 @@ extension HomeTimelineViewController {
                     guard let self = self else { return }
                     self.showProfileAction(action)
                 },
-                UIAction(title: "Settings", image: UIImage(systemName: "escape"), attributes: []) { [weak self] action in
-                    self?.coordinator.present(scene: .settings, from: self, transition: .modal(animated: true, completion: nil))
+                UIAction(title: "Show Thread", image: UIImage(systemName: "bubble.left.and.bubble.right"), attributes: []) { [weak self] action in
+                    guard let self = self else { return }
+                    self.showThreadAction(action)
                 },
                 UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
                     guard let self = self else { return }
@@ -307,5 +308,20 @@ extension HomeTimelineViewController {
         coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
     }
     
+    @objc private func showThreadAction(_ sender: UIAction) {
+        let alertController = UIAlertController(title: "Enter Status ID", message: nil, preferredStyle: .alert)
+        alertController.addTextField()
+        let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in
+            guard let self = self else { return }
+            guard let textField = alertController?.textFields?.first else { return }
+            let threadViewModel = RemoteThreadViewModel(context: self.context, statusID: textField.text ?? "")
+            self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show)
+        }
+        alertController.addAction(showAction)
+        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
+        alertController.addAction(cancelAction)
+        coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
+    }
+    
 }
 #endif
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift
index 9e1915301..aea931a62 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift
@@ -18,14 +18,14 @@ extension HomeTimelineViewController: StatusProvider {
         return Future { promise in promise(.success(nil)) }
     }
     
-    func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
+    func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
         return Future { promise in
             guard let diffableDataSource = self.viewModel.diffableDataSource else {
                 assertionFailure()
                 promise(.success(nil))
                 return
             }
-            guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
+            guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
                   let item = diffableDataSource.itemIdentifier(for: indexPath) else {
                 promise(.success(nil))
                 return
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
index 1f3dea81a..53909b2df 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
@@ -47,7 +47,6 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency {
         tableView.rowHeight = UITableView.automaticDimension
         tableView.separatorStyle = .none
         tableView.backgroundColor = .clear
-        
         return tableView
     }()
     
@@ -71,7 +70,7 @@ extension HomeTimelineViewController {
         super.viewDidLoad()
         
         title = L10n.Scene.HomeTimeline.title
-        view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+        view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
         navigationItem.leftBarButtonItem = settingBarButtonItem
         navigationItem.titleView = titleView
         titleView.delegate = self
@@ -179,6 +178,8 @@ extension HomeTimelineViewController {
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         
+        aspectViewWillAppear(animated)
+        
         // needs trigger manually after onboarding dismiss
         setNeedsStatusBarAppearanceUpdate()
     }
@@ -198,8 +199,8 @@ extension HomeTimelineViewController {
     
     override func viewDidDisappear(_ animated: Bool) {
         super.viewDidDisappear(animated)
-        context.videoPlaybackService.viewDidDisappear(from: self)
-        context.audioPlaybackService.viewDidDisappear(from: self)
+        
+        aspectViewDidDisappear(animated)
     }
 
     override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@@ -262,11 +263,19 @@ extension HomeTimelineViewController {
 
 }
 
+// MARK: - StatusTableViewControllerAspect
+extension HomeTimelineViewController: StatusTableViewControllerAspect { }
+
+extension HomeTimelineViewController: TableViewCellHeightCacheableContainer {
+    var cellFrameCache: NSCache<NSNumber, NSValue> { return viewModel.cellFrameCache }
+}
+
 // MARK: - UIScrollViewDelegate
 extension HomeTimelineViewController {
     func scrollViewDidScroll(_ scrollView: UIScrollView) {
-        handleScrollViewDidScroll(scrollView)
-        self.viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
+        
+        aspectScrollViewDidScroll(scrollView)
+        viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
     }
 }
 
@@ -281,32 +290,26 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
 extension HomeTimelineViewController: UITableViewDelegate {
     
     func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
-        return 200
-    // TODO:
-    //     guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
-    //     guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
-    //
-    //     guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
-    //         return 200
-    //     }
-    //     // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription)
-    //
-    //     return ceil(frame.height)
+        aspectTableView(tableView, estimatedHeightForRowAt: indexPath)
     }
     
     func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
-        handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
+        aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath)
     }
     
     func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
-        handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
+        aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
+    }
+    
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        aspectTableView(tableView, didSelectRowAt: indexPath)
     }
 }
 
 // MARK: - UITableViewDataSourcePrefetching
 extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
     func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
-        handleTableView(tableView, prefetchRowsAt: indexPaths)
+        aspectTableView(tableView, prefetchRowsAt: indexPaths)
     }
 }
 
@@ -317,7 +320,6 @@ extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControl
     }
 }
 
-
 // MARK: - TimelineMiddleLoaderTableViewCellDelegate
 extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
     func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID: NSManagedObjectID?) {
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift
index 5f16a18eb..6f5e66c0e 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift
@@ -29,7 +29,8 @@ extension HomeTimelineViewModel {
             managedObjectContext: fetchedResultsController.managedObjectContext,
             timestampUpdatePublisher: timestampUpdatePublisher,
             statusTableViewCellDelegate: statusTableViewCellDelegate,
-            timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
+            timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
+            threadReplyLoaderTableViewCellDelegate: nil
         )
         
 //        var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
@@ -88,6 +89,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
 
             for (i, timelineIndex) in timelineIndexes.enumerated() {
                 let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute()
+                attribute.isSeparatorLineHidden = false
                 
                 // append new item into snapshot
                 newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
@@ -96,6 +98,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
                 switch (isLast, timelineIndex.hasMore) {
                 case (false, true):
                     newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID))
+                    attribute.isSeparatorLineHidden = true
                 case (true, true):
                     shouldAddBottomLoader = true
                 default:
diff --git a/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift b/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift
index 9512ea78b..f5d8c41c8 100644
--- a/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift
+++ b/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift
@@ -16,14 +16,14 @@ final class WelcomeIllustrationView: UIView {
     let leftHillImageView = UIImageView()
     let centerHillImageView = UIImageView()
     
-    private let cloudBaseImage = Asset.Welcome.Illustration.cloudBase.image
-    private let elephantThreeOnGrassWithTreeTwoImage = Asset.Welcome.Illustration.elephantThreeOnGrassWithTreeTwo.image
-    private let elephantThreeOnGrassWithTreeThreeImage = Asset.Welcome.Illustration.elephantThreeOnGrassWithTreeThree.image
-    private let elephantThreeOnGrassImage = Asset.Welcome.Illustration.elephantThreeOnGrass.image
+    private let cloudBaseImage = Asset.Scene.Welcome.Illustration.cloudBase.image
+    private let elephantThreeOnGrassWithTreeTwoImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeTwo.image
+    private let elephantThreeOnGrassWithTreeThreeImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeThree.image
+    private let elephantThreeOnGrassImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrass.image
     
     // layout outside
     let elephantOnAirplaneWithContrailImageView: UIImageView = {
-        let imageView = UIImageView(image: Asset.Welcome.Illustration.elephantOnAirplaneWithContrail.image)
+        let imageView = UIImageView(image: Asset.Scene.Welcome.Illustration.elephantOnAirplaneWithContrail.image)
         imageView.contentMode = .scaleAspectFill
         return imageView
     }()
@@ -43,7 +43,7 @@ final class WelcomeIllustrationView: UIView {
 extension WelcomeIllustrationView {
     
     private func _init() {
-        backgroundColor = Asset.Welcome.Illustration.backgroundCyan.color
+        backgroundColor = Asset.Scene.Welcome.Illustration.backgroundCyan.color
         
         let topPaddingView = UIView()
         
diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
index c647d04ca..de89cd457 100644
--- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
+++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
@@ -17,7 +17,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
     var welcomeIllustrationViewBottomAnchorLayoutConstraint: NSLayoutConstraint?
     
     private(set) lazy var logoImageView: UIImageView = {
-        let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Welcome.mastodonLogo.image : Asset.Welcome.mastodonLogoBlackLarge.image
+        let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Scene.Welcome.mastodonLogo.image : Asset.Scene.Welcome.mastodonLogoBlackLarge.image
         let imageView = UIImageView(image: image)
         imageView.translatesAutoresizingMaskIntoConstraints = false
         return imageView
diff --git a/Mastodon/Scene/Profile/CachedProfileViewModel.swift b/Mastodon/Scene/Profile/CachedProfileViewModel.swift
index 0e5823d09..083724be1 100644
--- a/Mastodon/Scene/Profile/CachedProfileViewModel.swift
+++ b/Mastodon/Scene/Profile/CachedProfileViewModel.swift
@@ -10,8 +10,8 @@ import CoreDataStack
 
 final class CachedProfileViewModel: ProfileViewModel {
     
-    convenience init(context: AppContext, mastodonUser: MastodonUser) {
-        self.init(context: context, optionalMastodonUser: mastodonUser)
+    init(context: AppContext, mastodonUser: MastodonUser) {
+        super.init(context: context, optionalMastodonUser: mastodonUser)
     }
     
 }
diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+StatusProvider.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+StatusProvider.swift
index 2dadc8545..68adc1e3e 100644
--- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+StatusProvider.swift
+++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+StatusProvider.swift
@@ -18,14 +18,14 @@ extension FavoriteViewController: StatusProvider {
         return Future { promise in promise(.success(nil)) }
     }
     
-    func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
+    func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
         return Future { promise in
             guard let diffableDataSource = self.viewModel.diffableDataSource else {
                 assertionFailure()
                 promise(.success(nil))
                 return
             }
-            guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
+            guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
                   let item = diffableDataSource.itemIdentifier(for: indexPath) else {
                 promise(.success(nil))
                 return
diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift
index a175ae348..1e10a6322 100644
--- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift
+++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift
@@ -45,7 +45,7 @@ extension FavoriteViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         
-        view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+        view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
         navigationItem.titleView = titleView
         titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil)
         
@@ -114,6 +114,10 @@ extension FavoriteViewController: UITableViewDelegate {
         aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
     }
     
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        aspectTableView(tableView, didSelectRowAt: indexPath)
+    }
+    
 }
 
 // MARK: - UITableViewDataSourcePrefetching
diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift
index e64df2c99..85928e852 100644
--- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift
+++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift
@@ -25,7 +25,8 @@ extension FavoriteViewModel {
             managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext,
             timestampUpdatePublisher: timestampUpdatePublisher,
             statusTableViewCellDelegate: statusTableViewCellDelegate,
-            timelineMiddleLoaderTableViewCellDelegate: nil
+            timelineMiddleLoaderTableViewCellDelegate: nil,
+            threadReplyLoaderTableViewCellDelegate: nil
         )
         
         // set empty section to make update animation top-to-bottom style
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
index 2fba55e66..09d99c51e 100644
--- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
+++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
@@ -100,7 +100,7 @@ final class ProfileHeaderView: UIView {
         label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
         label.adjustsFontSizeToFitWidth = true
         label.minimumScaleFactor = 0.5
-        label.textColor = Asset.Profile.Banner.usernameGray.color
+        label.textColor = Asset.Scene.Profile.Banner.usernameGray.color
         label.text = "@alice"
         label.applyShadow(color: UIColor.black.withAlphaComponent(0.2), alpha: 0.5, x: 0, y: 2, blur: 2, spread: 0)
         return label
@@ -131,7 +131,7 @@ final class ProfileHeaderView: UIView {
         textEditorView.scrollView.isScrollEnabled = false
         textEditorView.isScrollEnabled = false
         textEditorView.font = .preferredFont(forTextStyle: .body)
-        textEditorView.backgroundColor = Asset.Profile.Banner.bioEditBackgroundGray.color
+        textEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
         textEditorView.layer.masksToBounds = true
         textEditorView.layer.cornerCurve = .continuous
         textEditorView.layer.cornerRadius = 10
@@ -356,9 +356,9 @@ extension ProfileHeaderView {
             bioTextEditorView.backgroundColor = .clear
             animator.addAnimations {
                 self.bannerImageViewOverlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
-                self.nameTextFieldBackgroundView.backgroundColor = Asset.Profile.Banner.nameEditBackgroundGray.color
+                self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
                 self.editAvatarBackgroundView.alpha = 1
-                self.bioTextEditorView.backgroundColor = Asset.Profile.Banner.bioEditBackgroundGray.color
+                self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
             }
         }
         
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift b/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift
index d4b57ffe4..c74560386 100644
--- a/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift
+++ b/Mastodon/Scene/Profile/Header/View/ProfileRelationshipActionButton.swift
@@ -29,6 +29,8 @@ final class ProfileRelationshipActionButton: RoundedEdgesButton {
 
 extension ProfileRelationshipActionButton {
     private func _init() {
+        titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
+        
         actvityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
         addSubview(actvityIndicatorView)
         NSLayoutConstraint.activate([
diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift
index 671f7c155..8fc915a0a 100644
--- a/Mastodon/Scene/Profile/ProfileViewController.swift
+++ b/Mastodon/Scene/Profile/ProfileViewController.swift
@@ -625,6 +625,11 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
     func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController postTimelineViewController: ScrollViewContainer, atIndex index: Int) {
         os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
         
+        // update segemented control
+        if index < profileHeaderViewController.pageSegmentedControl.numberOfSegments {
+            profileHeaderViewController.pageSegmentedControl.selectedSegmentIndex = index
+        }
+        
         // save content offset
         overlayScrollView.contentOffset.y = contentOffsets[index] ?? containerScrollView.contentOffset.y
         
diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift
index c480e6fc9..153f50998 100644
--- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift
+++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift
@@ -12,8 +12,8 @@ import MastodonSDK
 
 final class RemoteProfileViewModel: ProfileViewModel {
     
-    convenience init(context: AppContext, userID: Mastodon.Entity.Account.ID) {
-        self.init(context: context, optionalMastodonUser: nil)
+    init(context: AppContext, userID: Mastodon.Entity.Account.ID) {
+        super.init(context: context, optionalMastodonUser: nil)
         
         guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
             return
@@ -47,8 +47,6 @@ final class RemoteProfileViewModel: ProfileViewModel {
             self.mastodonUser.value = mastodonUser
         }
         .store(in: &disposeBag)
-        
     }
-
     
 }
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift
index 1ea164406..4fc857812 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift
@@ -18,14 +18,14 @@ extension UserTimelineViewController: StatusProvider {
         return Future { promise in promise(.success(nil)) }
     }
     
-    func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
+    func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
         return Future { promise in
             guard let diffableDataSource = self.viewModel.diffableDataSource else {
                 assertionFailure()
                 promise(.success(nil))
                 return
             }
-            guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
+            guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
                   let item = diffableDataSource.itemIdentifier(for: indexPath) else {
                 promise(.success(nil))
                 return
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
index e8e71ccf4..2ec350b07 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
@@ -45,7 +45,7 @@ extension UserTimelineViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         
-        view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+        view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
         
         tableView.translatesAutoresizingMaskIntoConstraints = false
         view.addSubview(tableView)
@@ -124,6 +124,10 @@ extension UserTimelineViewController: UITableViewDelegate {
         aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
     }
     
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        aspectTableView(tableView, didSelectRowAt: indexPath)
+    }
+    
 }
 
 // MARK: - UITableViewDataSourcePrefetching
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
index 1a09e1b35..8e6f1314f 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
@@ -25,7 +25,8 @@ extension UserTimelineViewModel {
             managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext,
             timestampUpdatePublisher: timestampUpdatePublisher,
             statusTableViewCellDelegate: statusTableViewCellDelegate,
-            timelineMiddleLoaderTableViewCellDelegate: nil
+            timelineMiddleLoaderTableViewCellDelegate: nil,
+            threadReplyLoaderTableViewCellDelegate: nil
         )
         
         // set empty section to make update animation top-to-bottom style
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift
index a92b8f37e..04fc526a0 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift
@@ -19,14 +19,14 @@ extension PublicTimelineViewController: StatusProvider {
         return Future { promise in promise(.success(nil)) }
     }
     
-    func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Status?, Never> {
+    func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
         return Future { promise in
             guard let diffableDataSource = self.viewModel.diffableDataSource else {
                 assertionFailure()
                 promise(.success(nil))
                 return
             }
-            guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
+            guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
                   let item = diffableDataSource.itemIdentifier(for: indexPath) else {
                 promise(.success(nil))
                 return
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
index ce0e8b19d..3ca407caa 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
@@ -28,7 +28,8 @@ extension PublicTimelineViewModel {
             managedObjectContext: fetchedResultsController.managedObjectContext,
             timestampUpdatePublisher: timestampUpdatePublisher,
             statusTableViewCellDelegate: statusTableViewCellDelegate,
-            timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
+            timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
+            threadReplyLoaderTableViewCellDelegate: nil
         )
         items.value = []
         stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift
index 5dcc47e02..704e425a6 100644
--- a/Mastodon/Scene/Search/SearchViewController.swift
+++ b/Mastodon/Scene/Search/SearchViewController.swift
@@ -82,7 +82,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
     // searching
     let searchingTableView: UITableView = {
         let tableView = UITableView()
-        tableView.backgroundColor = Asset.Colors.Background.searchResult.color
+        tableView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
         tableView.rowHeight = UITableView.automaticDimension
         tableView.separatorStyle = .singleLine
         tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
diff --git a/Mastodon/Scene/Share/View/Content/PollOptionView.swift b/Mastodon/Scene/Share/View/Content/PollOptionView.swift
index eafeb55cf..7125b691c 100644
--- a/Mastodon/Scene/Share/View/Content/PollOptionView.swift
+++ b/Mastodon/Scene/Share/View/Content/PollOptionView.swift
@@ -27,7 +27,7 @@ final class PollOptionView: UIView {
     
     let checkmarkBackgroundView: UIView = {
         let view = UIView()
-        view.backgroundColor = .systemBackground
+        view.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color
         return view
     }()
     
@@ -81,6 +81,7 @@ final class PollOptionView: UIView {
 
 extension PollOptionView {
     private func _init() {
+        // default color in the timeline
         roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
         
         roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift
index fc0fda099..9365179ef 100644
--- a/Mastodon/Scene/Share/View/Content/StatusView.swift
+++ b/Mastodon/Scene/Share/View/Content/StatusView.swift
@@ -29,7 +29,7 @@ final class StatusView: UIView {
     static let avatarToLabelSpacing: CGFloat = 5
     static let contentWarningBlurRadius: CGFloat = 12
     
-    static let boostIconImage: UIImage = {
+    static let reblogIconImage: UIImage = {
         let font = UIFont.systemFont(ofSize: 13, weight: .medium)
         let configuration = UIImage.SymbolConfiguration(font: font)
         let image = UIImage(systemName: "arrow.2.squarepath", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color)
@@ -61,7 +61,7 @@ final class StatusView: UIView {
     
     let headerIconLabel: UILabel = {
         let label = UILabel()
-        label.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
+        label.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
         return label
     }()
     
@@ -181,7 +181,7 @@ final class StatusView: UIView {
     // do not use visual effect view due to we blur text only without background
     let contentWarningBlurContentImageView: UIImageView = {
         let imageView = UIImageView()
-        imageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
+        imageView.backgroundColor = Asset.Colors.Background.systemBackground.color
         imageView.layer.masksToBounds = false
         return imageView
     }()
diff --git a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift
new file mode 100644
index 000000000..16d1b04a6
--- /dev/null
+++ b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift
@@ -0,0 +1,89 @@
+//
+//  ThreadMetaView.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import UIKit
+
+final class ThreadMetaView: UIView {
+    
+    let dateLabel: UILabel = {
+        let label = UILabel()
+        label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
+        label.text = "Date"
+        return label
+    }()
+    
+    let reblogButton: UIButton = {
+        let button = UIButton()
+        button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
+        button.setTitle("0 reblog", for: .normal)
+        button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal)
+        button.setTitleColor(Asset.Colors.Button.normal.color.withAlphaComponent(0.5), for: .highlighted)
+        return button
+    }()
+    
+    let favoriteButton: UIButton = {
+        let button = UIButton()
+        button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
+        button.setTitle("0 favorite", for: .normal)
+        button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal)
+        button.setTitleColor(Asset.Colors.Button.normal.color.withAlphaComponent(0.5), for: .highlighted)
+        return button
+    }()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        _init()
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        _init()
+    }
+    
+}
+
+extension ThreadMetaView {
+    private func _init() {
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.spacing = 20
+        
+        stackView.translatesAutoresizingMaskIntoConstraints = false
+        addSubview(stackView)
+        NSLayoutConstraint.activate([
+            stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
+            stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
+            stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
+            bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 12),
+        ])
+        
+        stackView.addArrangedSubview(dateLabel)
+        stackView.addArrangedSubview(reblogButton)
+        stackView.addArrangedSubview(favoriteButton)
+        
+        dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal)
+        favoriteButton.setContentHuggingPriority(.required - 1, for: .horizontal)
+    }
+}
+
+#if canImport(SwiftUI) && DEBUG
+import SwiftUI
+
+struct ThreadMetaView_Previews: PreviewProvider {
+    
+    static var previews: some View {
+        UIViewPreview(width: 375) {
+            ThreadMetaView()
+        }
+        .previewLayout(.fixed(width: 375, height: 100))
+    }
+    
+}
+
+#endif
+
diff --git a/Mastodon/Scene/Share/View/Control/AdaptiveUserInterfaceStyleBarButtonItem.swift b/Mastodon/Scene/Share/View/Control/AdaptiveUserInterfaceStyleBarButtonItem.swift
new file mode 100644
index 000000000..e801d1756
--- /dev/null
+++ b/Mastodon/Scene/Share/View/Control/AdaptiveUserInterfaceStyleBarButtonItem.swift
@@ -0,0 +1,76 @@
+//
+//  AdaptiveUserInterfaceStyleBarButtonItem.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-13.
+//
+
+import UIKit
+
+final class AdaptiveUserInterfaceStyleBarButtonItem: UIBarButtonItem {
+    
+    let button = AdaptiveCustomButton()
+    
+    init(lightImage: UIImage, darkImage: UIImage) {
+        super.init()
+        button.setImage(light: lightImage, dark: darkImage)
+        self.customView = button
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+    }
+    
+}
+
+extension AdaptiveUserInterfaceStyleBarButtonItem {
+    class AdaptiveCustomButton: UIButton {
+        
+        var lightImage: UIImage?
+        var darkImage: UIImage?
+        
+        override init(frame: CGRect) {
+            super.init(frame: frame)
+            _init()
+        }
+        
+        required init?(coder: NSCoder) {
+            super.init(coder: coder)
+            _init()
+        }
+        
+        private func _init() {
+            adjustsImageWhenHighlighted = false
+        }
+        
+        override var isHighlighted: Bool {
+            didSet {
+                alpha = isHighlighted ? 0.6 : 1
+            }
+        }
+        
+        override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+            super.traitCollectionDidChange(previousTraitCollection)
+            resetImage()
+        }
+        
+        func setImage(light: UIImage, dark: UIImage) {
+            lightImage = light
+            darkImage = dark
+            resetImage()
+        }
+        
+        private func resetImage() {
+            switch traitCollection.userInterfaceStyle {
+            case .light:
+                setImage(lightImage, for: .normal)
+            case .dark,
+                 .unspecified:
+                setImage(darkImage, for: .normal)
+            @unknown default:
+                assertionFailure()
+            }
+        }
+        
+    }
+}
diff --git a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift b/Mastodon/Scene/Share/View/Decoration/SawToothView.swift
index ef1c89cc0..8f41abbb3 100644
--- a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift
+++ b/Mastodon/Scene/Share/View/Decoration/SawToothView.swift
@@ -22,7 +22,7 @@ final class SawToothView: UIView {
     }
 
     func _init() {
-        backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+        backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
     }
 
     override func draw(_ rect: CGRect) {
@@ -37,7 +37,7 @@ final class SawToothView: UIView {
         }
         bezierPath.addLine(to: CGPoint(x: 0, y: bottomY))
         bezierPath.close()
-        Asset.Colors.Background.secondaryGroupedSystemBackground.color.setFill()
+        Asset.Colors.Background.systemBackground.color.setFill()
         bezierPath.fill()
         bezierPath.lineWidth = 0
         bezierPath.stroke()
diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
index b600924a6..afa044b67 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
@@ -32,6 +32,7 @@ protocol StatusTableViewCellDelegate: class {
     func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
     func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
     
+    func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, replyButtonDidPressed sender: UIButton)
     func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton)
     func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
     
@@ -56,14 +57,25 @@ final class StatusTableViewCell: UITableViewCell {
     var observations = Set<NSKeyValueObservation>()
     
     let statusView = StatusView()
-        
+    let threadMetaStackView = UIStackView()
+    let threadMetaView = ThreadMetaView()
+    let separatorLine = UIView.separatorLine
+    
+    var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint!
+    var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint!
+    
+    var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
+    var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint!
+
     override func prepareForReuse() {
         super.prepareForReuse()
+        selectionStyle = .default
         statusView.isStatusTextSensitive = false
         statusView.cleanUpContentWarning()
         statusView.pollTableView.dataSource = nil
         statusView.playerContainerView.reset()
         statusView.playerContainerView.isHidden = true
+        threadMetaView.isHidden = true
         disposeBag.removeAll()
         observations.removeAll()
     }
@@ -90,9 +102,8 @@ final class StatusTableViewCell: UITableViewCell {
 extension StatusTableViewCell {
     
     private func _init() {
-        selectionStyle = .none
-        backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
-        statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
+        backgroundColor = Asset.Colors.Background.systemBackground.color
+        statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.systemBackground.color
         
         statusView.translatesAutoresizingMaskIntoConstraints = false
         contentView.addSubview(statusView)
@@ -102,24 +113,74 @@ extension StatusTableViewCell {
             contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor),
         ])
         
-        let bottomPaddingView = UIView()
-        bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
-        contentView.addSubview(bottomPaddingView)
+        threadMetaStackView.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(threadMetaStackView)
         NSLayoutConstraint.activate([
-            bottomPaddingView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
-            bottomPaddingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
-            bottomPaddingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
-            bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
-            bottomPaddingView.heightAnchor.constraint(equalToConstant: StatusTableViewCell.bottomPaddingHeight).priority(.defaultHigh),
+            threadMetaStackView.topAnchor.constraint(equalTo: statusView.bottomAnchor),
+            threadMetaStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
+            threadMetaStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
+            threadMetaStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
         ])
-        bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
-                
+        threadMetaStackView.addArrangedSubview(threadMetaView)
+        
+        separatorLine.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(separatorLine)
+        separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
+        separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
+        separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor)
+        separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor)
+        NSLayoutConstraint.activate([
+            separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
+            separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
+        ])
+        resetSeparatorLineLayout()
+
         statusView.delegate = self
         statusView.pollTableView.delegate = self
         statusView.statusMosaicImageViewContainer.delegate = self
         statusView.actionToolbarContainer.delegate = self
+        
+        // default hidden
+        threadMetaView.isHidden = true
     }
     
+    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+        
+        resetSeparatorLineLayout()
+    }
+    
+}
+
+extension StatusTableViewCell {
+    private func resetSeparatorLineLayout() {
+        separatorLineToEdgeLeadingLayoutConstraint.isActive = false
+        separatorLineToEdgeTrailingLayoutConstraint.isActive = false
+        separatorLineToMarginLeadingLayoutConstraint.isActive = false
+        separatorLineToMarginTrailingLayoutConstraint.isActive = false
+        
+        if traitCollection.userInterfaceIdiom == .phone {
+            // to edge
+            NSLayoutConstraint.activate([
+                separatorLineToEdgeLeadingLayoutConstraint,
+                separatorLineToEdgeTrailingLayoutConstraint,
+            ])
+        } else {
+            if traitCollection.horizontalSizeClass == .compact {
+                // to edge
+                NSLayoutConstraint.activate([
+                    separatorLineToEdgeLeadingLayoutConstraint,
+                    separatorLineToEdgeTrailingLayoutConstraint,
+                ])
+            } else {
+                // to margin
+                NSLayoutConstraint.activate([
+                    separatorLineToMarginLeadingLayoutConstraint,
+                    separatorLineToMarginTrailingLayoutConstraint,
+                ])
+            }
+        }
+    }
 }
 
 // MARK: - UITableViewDelegate
@@ -242,19 +303,21 @@ extension StatusTableViewCell: MosaicImageViewContainerDelegate {
 
 // MARK: - ActionToolbarContainerDelegate
 extension StatusTableViewCell: ActionToolbarContainerDelegate {
+    
     func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
-        
+        delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, replyButtonDidPressed: sender)
     }
+    
     func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) {
         delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, reblogButtonDidPressed: sender)
     }
+    
     func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) {
         delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender)
     }
-    func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, bookmarkButtonDidPressed sender: UIButton) {
-        
-    }
+    
     func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) {
         
     }
+    
 }
diff --git a/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift
new file mode 100644
index 000000000..10ad0c5c8
--- /dev/null
+++ b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift
@@ -0,0 +1,124 @@
+//
+//  ThreadReplyLoaderTableViewCell.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-13.
+//
+
+import os.log
+import UIKit
+import Combine
+
+protocol ThreadReplyLoaderTableViewCellDelegate: class {
+    func threadReplyLoaderTableViewCell(_ cell: ThreadReplyLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton)
+}
+
+final class ThreadReplyLoaderTableViewCell: UITableViewCell {
+    
+    static let cellHeight: CGFloat = 44
+    
+    weak var delegate: ThreadReplyLoaderTableViewCellDelegate?
+    
+    let loadMoreButton: UIButton = {
+        let button = HighlightDimmableButton()
+        button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont
+        button.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
+        button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal)
+        button.setTitle(L10n.Common.Controls.Timeline.Loader.showMoreReplies, for: .normal)
+        return button
+    }()
+    
+    let separatorLine = UIView.separatorLine
+    
+    var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint!
+    var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint!
+    
+    var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
+    var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint!
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        _init()
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        _init()
+    }
+    
+    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+        
+        resetSeparatorLineLayout()
+    }
+
+}
+
+extension ThreadReplyLoaderTableViewCell {
+
+    func _init() {
+        selectionStyle = .none
+        backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+
+        loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(loadMoreButton)
+        NSLayoutConstraint.activate([
+            loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor),
+            loadMoreButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+            contentView.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor),
+            contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor),
+            loadMoreButton.heightAnchor.constraint(equalToConstant: ThreadReplyLoaderTableViewCell.cellHeight).priority(.required - 1),
+        ])
+        
+        separatorLine.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(separatorLine)
+        separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
+        separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
+        separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor)
+        separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor)
+        NSLayoutConstraint.activate([
+            separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
+            separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
+        ])
+        resetSeparatorLineLayout()
+        
+        loadMoreButton.addTarget(self, action: #selector(ThreadReplyLoaderTableViewCell.loadMoreButtonDidPressed(_:)), for: .touchUpInside)
+    }
+    
+    private func resetSeparatorLineLayout() {
+        separatorLineToEdgeLeadingLayoutConstraint.isActive = false
+        separatorLineToEdgeTrailingLayoutConstraint.isActive = false
+        separatorLineToMarginLeadingLayoutConstraint.isActive = false
+        separatorLineToMarginTrailingLayoutConstraint.isActive = false
+        
+        if traitCollection.userInterfaceIdiom == .phone {
+            // to edge
+            NSLayoutConstraint.activate([
+                separatorLineToEdgeLeadingLayoutConstraint,
+                separatorLineToEdgeTrailingLayoutConstraint,
+            ])
+        } else {
+            if traitCollection.horizontalSizeClass == .compact {
+                // to edge
+                NSLayoutConstraint.activate([
+                    separatorLineToEdgeLeadingLayoutConstraint,
+                    separatorLineToEdgeTrailingLayoutConstraint,
+                ])
+            } else {
+                // to margin
+                NSLayoutConstraint.activate([
+                    separatorLineToMarginLeadingLayoutConstraint,
+                    separatorLineToMarginTrailingLayoutConstraint,
+                ])
+            }
+        }
+    }
+    
+}
+
+extension ThreadReplyLoaderTableViewCell {
+    @objc private func loadMoreButtonDidPressed(_ sender: UIButton) {
+        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+        delegate?.threadReplyLoaderTableViewCell(self, loadMoreButtonDidPressed: sender)
+    }
+}
diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift
index 38bf7ef78..da7420e43 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift
@@ -10,9 +10,9 @@ import Combine
 
 class TimelineLoaderTableViewCell: UITableViewCell {
     
-    static let buttonHeight: CGFloat = 62
-    static let cellHeight: CGFloat = TimelineLoaderTableViewCell.buttonHeight + 17
-    static let extraTopPadding: CGFloat = 10
+    static let buttonHeight: CGFloat = 44
+    static let buttonMargin: CGFloat = 12
+    static let cellHeight: CGFloat = buttonHeight + 2 * buttonMargin
     static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium))
     
     var disposeBag = Set<AnyCancellable>()
@@ -22,7 +22,7 @@ class TimelineLoaderTableViewCell: UITableViewCell {
     let loadMoreButton: UIButton = {
         let button = HighlightDimmableButton()
         button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont
-        button.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
+        button.backgroundColor = Asset.Colors.Background.systemBackground.color
         button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal)
         button.setTitle(L10n.Common.Controls.Timeline.Loader.loadMissingPosts, for: .normal)
         button.setTitle("", for: .disabled)
@@ -73,15 +73,15 @@ class TimelineLoaderTableViewCell: UITableViewCell {
     
     func _init() {
         selectionStyle = .none
-        backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+        backgroundColor = .clear
         
         loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
         contentView.addSubview(loadMoreButton)
         NSLayoutConstraint.activate([
-            loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 7),
+            loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TimelineLoaderTableViewCell.buttonMargin),
             loadMoreButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
             contentView.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor),
-            contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: 14),
+            contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: TimelineLoaderTableViewCell.buttonMargin),
             loadMoreButton.heightAnchor.constraint(equalToConstant: TimelineLoaderTableViewCell.buttonHeight).priority(.required - 1),
         ])
         
diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift
index 75c06a339..7438f5bfd 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift
@@ -18,11 +18,8 @@ protocol TimelineMiddleLoaderTableViewCellDelegate: class {
 final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell {
     weak var delegate: TimelineMiddleLoaderTableViewCellDelegate?
     
-    let sawToothView: SawToothView = {
-        let sawToothView = SawToothView()
-        sawToothView.translatesAutoresizingMaskIntoConstraints = false
-        return sawToothView
-    }()
+    let topSawToothView = SawToothView()
+    let bottomSawToothView = SawToothView()
     
     override func _init() {
         super._init()
@@ -34,12 +31,23 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell {
         loadMoreButton.setInsets(forContentPadding: .zero, imageTitlePadding: 4)
         loadMoreButton.addTarget(self, action: #selector(TimelineMiddleLoaderTableViewCell.loadMoreButtonDidPressed(_:)), for: .touchUpInside)
         
-        contentView.addSubview(sawToothView)
+        topSawToothView.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(topSawToothView)
         NSLayoutConstraint.activate([
-            sawToothView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
-            sawToothView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
-            sawToothView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
-            sawToothView.heightAnchor.constraint(equalToConstant: 3),
+            topSawToothView.topAnchor.constraint(equalTo: contentView.topAnchor),
+            topSawToothView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+            topSawToothView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+            topSawToothView.heightAnchor.constraint(equalToConstant: 3),
+        ])
+        topSawToothView.transform = CGAffineTransform(scaleX: 1, y: -1) // upside down
+        
+        bottomSawToothView.translatesAutoresizingMaskIntoConstraints = false
+        contentView.addSubview(bottomSawToothView)
+        NSLayoutConstraint.activate([
+            bottomSawToothView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+            bottomSawToothView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+            bottomSawToothView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
+            bottomSawToothView.heightAnchor.constraint(equalToConstant: 3),
         ])
     }
 }
diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift
new file mode 100644
index 000000000..4accee1de
--- /dev/null
+++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift
@@ -0,0 +1,36 @@
+//
+//  TimelineTopLoaderTableViewCell.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import UIKit
+import Combine
+
+final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell {
+    override func _init() {
+        super._init()
+        
+        activityIndicatorView.isHidden = false
+        
+        startAnimating()
+    }
+}
+
+#if canImport(SwiftUI) && DEBUG
+import SwiftUI
+
+struct TimelineTopLoaderTableViewCell_Previews: PreviewProvider {
+    
+    static var previews: some View {
+        UIViewPreview(width: 375) {
+            TimelineTopLoaderTableViewCell()
+        }
+        .previewLayout(.fixed(width: 375, height: 100))
+    }
+    
+}
+
+#endif
+
diff --git a/Mastodon/Scene/Thread/CachedThreadViewModel.swift b/Mastodon/Scene/Thread/CachedThreadViewModel.swift
new file mode 100644
index 000000000..d4866b0bd
--- /dev/null
+++ b/Mastodon/Scene/Thread/CachedThreadViewModel.swift
@@ -0,0 +1,15 @@
+//
+//  CachedThreadViewModel.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import Foundation
+import CoreDataStack
+
+final class CachedThreadViewModel: ThreadViewModel {
+    init(context: AppContext, status: Status) {
+        super.init(context: context, optionalStatus: status)
+    }
+}
diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift
new file mode 100644
index 000000000..e79c355cf
--- /dev/null
+++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift
@@ -0,0 +1,50 @@
+//
+//  RemoteThreadViewModel.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import os.log
+import UIKit
+import CoreDataStack
+import MastodonSDK
+
+final class RemoteThreadViewModel: ThreadViewModel {
+        
+    init(context: AppContext, statusID: Mastodon.Entity.Status.ID) {
+        super.init(context: context, optionalStatus: nil)
+        
+        guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
+            return
+        }
+        let domain = activeMastodonAuthenticationBox.domain
+        context.apiService.status(
+            domain: domain,
+            statusID: statusID,
+            authorizationBox: activeMastodonAuthenticationBox
+        )
+        .retry(3)
+        .sink { completion in
+            switch completion {
+            case .failure(let error):
+                // TODO: handle error
+                os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote status %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, statusID, error.localizedDescription)
+            case .finished:
+                os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote status %s fetched", ((#file as NSString).lastPathComponent), #line, #function, statusID)
+            }
+        } receiveValue: { [weak self] response in
+            guard let self = self else { return }
+            let managedObjectContext = context.managedObjectContext
+            let request = Status.sortedFetchRequest
+            request.fetchLimit = 1
+            request.predicate = Status.predicate(domain: domain, id: response.value.id)
+            guard let status = managedObjectContext.safeFetch(request).first else {
+                assertionFailure()
+                return
+            }
+            self.rootItem.value = .root(statusObjectID: status.objectID, attribute: Item.StatusAttribute())
+        }
+        .store(in: &disposeBag)
+    }
+}
diff --git a/Mastodon/Scene/Thread/ThreadViewController+StatusProvider.swift b/Mastodon/Scene/Thread/ThreadViewController+StatusProvider.swift
new file mode 100644
index 000000000..05cc6e4b2
--- /dev/null
+++ b/Mastodon/Scene/Thread/ThreadViewController+StatusProvider.swift
@@ -0,0 +1,88 @@
+//
+//  ThreadViewController+StatusProvider.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+
+// MARK: - StatusProvider
+extension ThreadViewController: StatusProvider {
+    
+    func status() -> Future<Status?, Never> {
+        return Future { promise in promise(.success(nil)) }
+    }
+    
+    func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
+        return Future { promise in
+            guard let diffableDataSource = self.viewModel.diffableDataSource else {
+                assertionFailure()
+                promise(.success(nil))
+                return
+            }
+            guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
+                  let item = diffableDataSource.itemIdentifier(for: indexPath) else {
+                promise(.success(nil))
+                return
+            }
+            
+            switch item {
+            case .root(let statusObjectID, _),
+                 .reply(let statusObjectID, _),
+                 .leaf(let statusObjectID, _):
+                let managedObjectContext = self.viewModel.context.managedObjectContext
+                managedObjectContext.perform {
+                    let status = managedObjectContext.object(with: statusObjectID) as? Status
+                    promise(.success(status))
+                }
+            default:
+                promise(.success(nil))
+            }
+        }
+    }
+    
+    func status(for cell: UICollectionViewCell) -> Future<Status?, Never> {
+        return Future { promise in promise(.success(nil)) }
+    }
+    
+    var managedObjectContext: NSManagedObjectContext {
+        return viewModel.context.managedObjectContext
+    }
+    
+    var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
+        return viewModel.diffableDataSource
+    }
+    
+    func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
+        guard let diffableDataSource = self.viewModel.diffableDataSource else {
+            assertionFailure()
+            return nil
+        }
+        
+        guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
+              let item = diffableDataSource.itemIdentifier(for: indexPath) else {
+            return nil
+        }
+        
+        return item
+    }
+    
+    func items(indexPaths: [IndexPath]) -> [Item] {
+        guard let diffableDataSource = self.viewModel.diffableDataSource else {
+            assertionFailure()
+            return []
+        }
+        
+        var items: [Item] = []
+        for indexPath in indexPaths {
+            guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue }
+            items.append(item)
+        }
+        return items
+    }
+    
+}
diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift
new file mode 100644
index 000000000..bd15b930e
--- /dev/null
+++ b/Mastodon/Scene/Thread/ThreadViewController.swift
@@ -0,0 +1,211 @@
+//
+//  ThreadViewController.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import AVKit
+
+final class ThreadViewController: UIViewController, NeedsDependency {
+        
+    weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
+    weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
+    
+    var disposeBag = Set<AnyCancellable>()
+    var viewModel: ThreadViewModel!
+    
+    let titleView = DoubleTitleLabelNavigationBarTitleView()
+    
+    let replyBarButtonItem = AdaptiveUserInterfaceStyleBarButtonItem(
+        lightImage: UIImage(systemName: "arrowshape.turn.up.left")!,
+        darkImage: UIImage(systemName: "arrowshape.turn.up.left.fill")!
+    )
+    
+    let tableView: UITableView = {
+        let tableView = ControlContainableTableView()
+        tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
+        tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
+        tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
+        tableView.register(ThreadReplyLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self))
+        tableView.rowHeight = UITableView.automaticDimension
+        tableView.separatorStyle = .none
+        tableView.backgroundColor = .clear
+        
+        return tableView
+    }()
+    
+    deinit {
+        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
+    }
+    
+}
+
+extension ThreadViewController {
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
+        navigationItem.title = L10n.Scene.Thread.backTitle
+        navigationItem.titleView = titleView
+        navigationItem.rightBarButtonItem = replyBarButtonItem
+        replyBarButtonItem.button.addTarget(self, action: #selector(ThreadViewController.replyBarButtonItemPressed(_:)), for: .touchUpInside)
+        
+        viewModel.tableView = tableView
+        viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
+        tableView.delegate = self
+        tableView.prefetchDataSource = self
+        viewModel.setupDiffableDataSource(
+            for: tableView,
+            dependency: self,
+            statusTableViewCellDelegate: self,
+            threadReplyLoaderTableViewCellDelegate: self
+        )
+        
+        tableView.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(tableView)
+        NSLayoutConstraint.activate([
+            tableView.topAnchor.constraint(equalTo: view.topAnchor),
+            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+        ])
+        
+        viewModel.navigationBarTitle
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] title in
+                guard let self = self else { return }
+                self.titleView.update(title: title ?? L10n.Scene.Thread.backTitle, subtitle: nil)
+            }
+            .store(in: &disposeBag)
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        
+        aspectViewWillAppear(animated)
+    }
+    
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        
+        aspectViewDidDisappear(animated)
+    }
+    
+}
+
+extension ThreadViewController {
+    @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
+        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+        guard let rootItem = viewModel.rootItem.value,
+              case let .root(statusObjectID, _) = rootItem else { return }
+        let composeViewModel = ComposeViewModel(context: context, composeKind: .reply(repliedToStatusObjectID: statusObjectID))
+        coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
+    }
+}
+
+// MARK: - StatusTableViewControllerAspect
+extension ThreadViewController: StatusTableViewControllerAspect { }
+
+// MARK: - TableViewCellHeightCacheableContainer
+extension ThreadViewController: TableViewCellHeightCacheableContainer {
+    var cellFrameCache: NSCache<NSNumber, NSValue> { viewModel.cellFrameCache }
+}
+
+// MARK: - UITableViewDelegate
+extension ThreadViewController: UITableViewDelegate {
+
+    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
+        aspectTableView(tableView, estimatedHeightForRowAt: indexPath)
+    }
+    
+    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
+        aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath)
+    }
+    
+    func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
+        aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
+    }
+    
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        aspectTableView(tableView, didSelectRowAt: indexPath)
+    }
+    
+    func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
+        guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
+        guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
+        
+        // disable root selection
+        switch item {
+        case .root:
+            return nil
+        default:
+            return indexPath
+        }
+    }
+    
+}
+
+// MARK: - UITableViewDataSourcePrefetching
+extension ThreadViewController: UITableViewDataSourcePrefetching {
+    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
+        aspectTableView(tableView, prefetchRowsAt: indexPaths)
+    }
+}
+
+// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
+extension ThreadViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
+    func navigationBar() -> UINavigationBar? {
+        return navigationController?.navigationBar
+    }
+}
+
+// MARK: - AVPlayerViewControllerDelegate
+extension ThreadViewController: AVPlayerViewControllerDelegate {
+    
+    func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+        aspectPlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
+    }
+    
+    func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+        aspectPlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
+    }
+    
+}
+
+// MARK: - statusTableViewCellDelegate
+extension ThreadViewController: StatusTableViewCellDelegate {
+    weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
+    func parent() -> UIViewController { return self }
+}
+
+// MARK: - ThreadReplyLoaderTableViewCellDelegate
+extension ThreadViewController: ThreadReplyLoaderTableViewCellDelegate {
+    func threadReplyLoaderTableViewCell(_ cell: ThreadReplyLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) {
+        guard let diffableDataSource = viewModel.diffableDataSource else { return }
+        guard let indexPath = tableView.indexPath(for: cell) else { return }
+        guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
+        guard case let .leafBottomLoader(statusObjectID) = item else { return }
+        
+        let nodes = viewModel.descendantNodes.value
+        nodes.forEach { node in
+            expandReply(node: node, statusObjectID: statusObjectID)
+        }
+        viewModel.descendantNodes.value = nodes
+    }
+    
+    private func expandReply(node: ThreadViewModel.LeafNode, statusObjectID: NSManagedObjectID) {
+        if node.objectID == statusObjectID {
+            node.isChildrenExpanded = true
+        } else {
+            for child in node.children {
+                expandReply(node: child, statusObjectID: statusObjectID)
+            }
+        }
+    }
+}
diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift
new file mode 100644
index 000000000..323a7a545
--- /dev/null
+++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift
@@ -0,0 +1,186 @@
+//
+//  ThreadViewModel+Diffable.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import UIKit
+import Combine
+import CoreData
+
+extension ThreadViewModel {
+    
+    func setupDiffableDataSource(
+        for tableView: UITableView,
+        dependency: NeedsDependency,
+        statusTableViewCellDelegate: StatusTableViewCellDelegate,
+        threadReplyLoaderTableViewCellDelegate: ThreadReplyLoaderTableViewCellDelegate
+    ) {
+        let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
+            .autoconnect()
+            .share()
+            .eraseToAnyPublisher()
+        
+        diffableDataSource = StatusSection.tableViewDiffableDataSource(
+            for: tableView,
+            dependency: dependency,
+            managedObjectContext: context.managedObjectContext,
+            timestampUpdatePublisher: timestampUpdatePublisher,
+            statusTableViewCellDelegate: statusTableViewCellDelegate,
+            timelineMiddleLoaderTableViewCellDelegate: nil,
+            threadReplyLoaderTableViewCellDelegate: threadReplyLoaderTableViewCellDelegate
+        )
+        
+        var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
+        snapshot.appendSections([.main])
+        if let rootNode = self.rootNode.value, rootNode.replyToID != nil {
+            snapshot.appendItems([.topLoader], toSection: .main)
+        }
+        
+        diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
+        
+        Publishers.CombineLatest3(
+            rootItem,
+            ancestorItems,
+            descendantItems
+        )
+        .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)       // some magic to avoid jitter
+        .receive(on: DispatchQueue.main)
+        .sink { [weak self] rootItem, ancestorItems, descendantItems in
+            guard let self = self else { return }
+            guard let tableView = self.tableView,
+                  let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar()
+            else { return }
+            
+            guard let diffableDataSource = self.diffableDataSource else { return }
+            let oldSnapshot = diffableDataSource.snapshot()
+            
+            var newSnapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
+            newSnapshot.appendSections([.main])
+            
+            let currentState = self.loadThreadStateMachine.currentState
+            
+            // reply to
+            if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) {
+                newSnapshot.appendItems([.topLoader], toSection: .main)
+            }
+            newSnapshot.appendItems(ancestorItems, toSection: .main)
+            
+            // root
+            if let rootItem = rootItem {
+                switch rootItem {
+                case .root:
+                    newSnapshot.appendItems([rootItem], toSection: .main)
+                default:
+                    break
+                }
+            }
+            
+            // leaf
+            if !(currentState is LoadThreadState.NoMore) {
+                newSnapshot.appendItems([.bottomLoader], toSection: .main)
+            }
+            newSnapshot.appendItems(descendantItems, toSection: .main)
+            
+            // difference for first visiable item exclude .topLoader
+            guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
+                diffableDataSource.apply(newSnapshot)
+                return
+            }
+
+            // addtional margin for .topLoader
+            let oldTopMargin: CGFloat = {
+                let marginHeight = TimelineTopLoaderTableViewCell.cellHeight
+                if oldSnapshot.itemIdentifiers.contains(.topLoader) {
+                    return marginHeight
+                }
+                if !ancestorItems.isEmpty {
+                    return marginHeight
+                }
+                
+                return .zero
+            }()
+            
+            let oldRootCell: UITableViewCell? = {
+                guard let rootItem = rootItem else { return nil }
+                guard let index = oldSnapshot.indexOfItem(rootItem) else { return nil }
+                guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) else { return nil }
+                return cell
+            }()
+            // save height before cell reuse
+            let oldRootCellHeight = oldRootCell?.frame.height
+            
+            diffableDataSource.apply(newSnapshot, animatingDifferences: false) {
+                guard let _ = rootItem else {
+                    return
+                }
+                if let oldRootCellHeight = oldRootCellHeight {
+                    // set bottom inset. Make root item pin to top (with margin).
+                    let bottomSpacing = tableView.safeAreaLayoutGuide.layoutFrame.height - oldRootCellHeight - oldTopMargin
+                    tableView.contentInset.bottom = max(0, bottomSpacing)
+                }
+
+                // set scroll position
+                tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false)
+                let contentOffsetY: CGFloat = {
+                    var offset: CGFloat = tableView.contentOffset.y - difference.offset
+                    if tableView.contentInset.bottom != 0.0 && descendantItems.isEmpty {
+                        // needs restore top margin if bottom inset adjusted AND no descendantItems
+                        offset += oldTopMargin
+                    }
+                    return offset
+                }()
+                tableView.setContentOffset(CGPoint(x: 0, y: contentOffsetY), animated: false)
+            }
+        }
+        .store(in: &disposeBag)
+    }
+    
+}
+
+extension ThreadViewModel {
+    private struct Difference<T> {
+        let item: T
+        let sourceIndexPath: IndexPath
+        let targetIndexPath: IndexPath
+        let offset: CGFloat
+    }
+    
+    private func calculateReloadSnapshotDifference(
+        navigationBar: UINavigationBar,
+        tableView: UITableView,
+        oldSnapshot: NSDiffableDataSourceSnapshot<StatusSection, Item>,
+        newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, Item>
+    ) -> Difference<Item>? {
+        guard oldSnapshot.numberOfItems != 0 else { return nil }
+        guard let visibleIndexPaths = tableView.indexPathsForVisibleRows?.sorted() else { return nil }
+    
+        // find index of the first visible item exclude .topLoader
+        var _index: Int?
+        let items = oldSnapshot.itemIdentifiers(inSection: .main)
+        for (i, item) in items.enumerated() {
+            if case .topLoader = item { continue }
+            guard visibleIndexPaths.contains(where: { $0.row == i }) else { continue }
+            
+            _index = i
+            break
+        }
+        
+        guard let index = _index else  { return nil }
+        let sourceIndexPath = IndexPath(row: index, section: 0)
+        guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil }
+        
+        let item = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row]
+        guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: item) else { return nil }
+        let targetIndexPath = IndexPath(row: itemIndex, section: 0)
+        
+        let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar)
+        return Difference(
+            item: item,
+            sourceIndexPath: sourceIndexPath,
+            targetIndexPath: targetIndexPath,
+            offset: offset
+        )
+    }
+}
diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift
new file mode 100644
index 000000000..5327edc5c
--- /dev/null
+++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift
@@ -0,0 +1,127 @@
+//
+//  ThreadViewModel+LoadThreadState.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import os.log
+import Foundation
+import Combine
+import GameplayKit
+import CoreDataStack
+import MastodonSDK
+
+extension ThreadViewModel {
+    class LoadThreadState: GKState {
+        weak var viewModel: ThreadViewModel?
+                
+        init(viewModel: ThreadViewModel) {
+            self.viewModel = viewModel
+        }
+        
+        override func didEnter(from previousState: GKState?) {
+            os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
+        }
+    }
+}
+
+extension ThreadViewModel.LoadThreadState {
+    class Initial: ThreadViewModel.LoadThreadState {
+        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+            switch stateClass {
+            case is Loading.Type:   return true
+            default:                return false
+            }
+        }
+    }
+    
+    class Loading: ThreadViewModel.LoadThreadState {
+        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+            switch stateClass {
+            case is Fail.Type:      return true
+            case is NoMore.Type:      return true
+            default:                return false
+            }
+        }
+        
+        override func didEnter(from previousState: GKState?) {
+            super.didEnter(from: previousState)
+            
+            guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
+            guard let mastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
+                stateMachine.enter(Fail.self)
+                return
+            }
+            
+            guard let rootNode = viewModel.rootNode.value else {
+                stateMachine.enter(Fail.self)
+                return
+            }
+            
+            // trigger data source update
+            viewModel.rootItem.value = viewModel.rootItem.value
+            
+            let domain = rootNode.domain
+            let statusID = rootNode.statusID
+            let replyToID = rootNode.replyToID
+            
+            viewModel.context.apiService.statusContext(
+                domain: domain,
+                statusID: statusID,
+                mastodonAuthenticationBox: mastodonAuthenticationBox
+            )
+            .sink { completion in
+                switch completion {
+                case .failure(let error):
+                    os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch status context for %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, statusID, error.localizedDescription)
+                    stateMachine.enter(Fail.self)
+                case .finished:
+                    break
+                }
+            } receiveValue: { response in
+                stateMachine.enter(NoMore.self)
+
+                viewModel.ancestorNodes.value = ThreadViewModel.ReplyNode.replyToThread(
+                    for: replyToID,
+                    from: response.value.ancestors,
+                    domain: domain,
+                    managedObjectContext: viewModel.context.managedObjectContext
+                )
+                viewModel.descendantNodes.value = ThreadViewModel.LeafNode.tree(
+                    for: rootNode.statusID,
+                    from: response.value.descendants,
+                    domain: domain,
+                    managedObjectContext: viewModel.context.managedObjectContext
+                )
+            }
+            .store(in: &viewModel.disposeBag)
+        }
+
+    }
+    
+    class Fail: ThreadViewModel.LoadThreadState {
+        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+            switch stateClass {
+            case is Loading.Type:   return true
+            default:                return false
+            }
+        }
+        
+        override func didEnter(from previousState: GKState?) {
+            super.didEnter(from: previousState)
+            
+            guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
+            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                stateMachine.enter(Loading.self)
+            }
+        }
+    }
+    
+    class NoMore: ThreadViewModel.LoadThreadState {
+        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+            return false
+        }
+    }
+    
+}
diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift
new file mode 100644
index 000000000..50df678c6
--- /dev/null
+++ b/Mastodon/Scene/Thread/ThreadViewModel.swift
@@ -0,0 +1,279 @@
+//
+//  ThreadViewModel.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+import GameplayKit
+import MastodonSDK
+
+class ThreadViewModel {
+    
+    var disposeBag = Set<AnyCancellable>()
+    
+    // input
+    let context: AppContext
+    let rootNode: CurrentValueSubject<RootNode?, Never>
+    let rootItem: CurrentValueSubject<Item?, Never>
+    let cellFrameCache = NSCache<NSNumber, NSValue>()
+
+    weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
+    weak var tableView: UITableView?
+    
+    // output
+    var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
+    private(set) lazy var loadThreadStateMachine: GKStateMachine = {
+        let stateMachine = GKStateMachine(states: [
+            LoadThreadState.Initial(viewModel: self),
+            LoadThreadState.Loading(viewModel: self),
+            LoadThreadState.Fail(viewModel: self),
+            LoadThreadState.NoMore(viewModel: self),
+            
+        ])
+        stateMachine.enter(LoadThreadState.Initial.self)
+        return stateMachine
+    }()
+    let ancestorNodes = CurrentValueSubject<[ReplyNode], Never>([])
+    let ancestorItems = CurrentValueSubject<[Item], Never>([])
+    let descendantNodes = CurrentValueSubject<[LeafNode], Never>([])
+    let descendantItems = CurrentValueSubject<[Item], Never>([])
+    let navigationBarTitle: CurrentValueSubject<String?, Never>
+    
+    init(context: AppContext, optionalStatus: Status?) {
+        self.context = context
+        self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) })
+        self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
+        self.navigationBarTitle = CurrentValueSubject(
+            optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }
+        )
+        
+        rootNode
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] rootNode in
+                guard let self = self else { return }
+                guard rootNode != nil else { return }
+                self.loadThreadStateMachine.enter(LoadThreadState.Loading.self)
+            }
+            .store(in: &disposeBag)
+        
+        if optionalStatus == nil {
+            rootItem
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] rootItem in
+                    guard let self = self else { return }
+                    guard case let .root(objectID, _) = rootItem else { return }
+                    self.context.managedObjectContext.perform {
+                        guard let status = self.context.managedObjectContext.object(with: objectID) as? Status else {
+                            return
+                        }
+                        self.rootNode.value = RootNode(domain: status.domain, statusID: status.id, replyToID: status.inReplyToID)
+                        self.navigationBarTitle.value = L10n.Scene.Thread.title(status.author.displayNameWithFallback)
+                    }
+                }
+                .store(in: &disposeBag)
+        }
+        
+        // descendantNodes
+        
+        ancestorNodes
+            .receive(on: DispatchQueue.main)
+            .compactMap { [weak self] nodes -> [Item]? in
+                guard let self = self else { return nil }
+                guard !nodes.isEmpty else { return [] }
+                
+                guard let diffableDataSource = self.diffableDataSource else { return nil }
+                let oldSnapshot = diffableDataSource.snapshot()
+                var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
+                for item in oldSnapshot.itemIdentifiers {
+                    switch item {
+                    case .reply(let objectID, let attribute):
+                        oldSnapshotAttributeDict[objectID] = attribute
+                    default:
+                        break
+                    }
+                }
+                
+                var items: [Item] = []
+                for node in nodes {
+                    let attribute = oldSnapshotAttributeDict[node.statusObjectID] ?? Item.StatusAttribute()
+                    items.append(Item.reply(statusObjectID: node.statusObjectID, attribute: attribute))
+                }
+                
+                return items.reversed()
+            }
+            .assign(to: \.value, on: ancestorItems)
+            .store(in: &disposeBag)
+        
+        descendantNodes
+            .receive(on: DispatchQueue.main)
+            .compactMap { [weak self] nodes -> [Item]? in
+                guard let self = self else { return nil }
+                guard !nodes.isEmpty else { return [] }
+                
+                guard let diffableDataSource = self.diffableDataSource else { return nil }
+                let oldSnapshot = diffableDataSource.snapshot()
+                var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
+                for item in oldSnapshot.itemIdentifiers {
+                    switch item {
+                    case .leaf(let objectID, let attribute):
+                        oldSnapshotAttributeDict[objectID] = attribute
+                    default:
+                        break
+                    }
+                }
+                
+                var items: [Item] = []
+                
+                func buildThread(node: LeafNode) {
+                    let attribute = oldSnapshotAttributeDict[node.objectID] ?? Item.StatusAttribute()
+                    items.append(Item.leaf(statusObjectID: node.objectID, attribute: attribute))
+                    // only expand the first child
+                    if let firstChild = node.children.first {
+                        if !node.isChildrenExpanded {
+                            items.append(Item.leafBottomLoader(statusObjectID: node.objectID))
+                        } else {
+                            buildThread(node: firstChild)
+                        }
+                    }
+                }
+                
+                for node in nodes {
+                    buildThread(node: node)
+                }
+                return items
+            }
+            .assign(to: \.value, on: descendantItems)
+            .store(in: &disposeBag)
+    }
+    
+    deinit {
+        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+    }
+
+}
+
+extension ThreadViewModel {
+    
+    struct RootNode {
+        let domain: String
+        let statusID: Mastodon.Entity.Status.ID
+        let replyToID: Mastodon.Entity.Status.ID?
+    }
+    
+    class ReplyNode {
+        let statusID: Mastodon.Entity.Status.ID
+        let statusObjectID: NSManagedObjectID
+        
+        init(statusID: Mastodon.Entity.Status.ID, statusObjectID: NSManagedObjectID) {
+            self.statusID = statusID
+            self.statusObjectID = statusObjectID
+        }
+        
+        static func replyToThread(
+            for replyToID: Mastodon.Entity.Status.ID?,
+            from statuses: [Mastodon.Entity.Status],
+            domain: String,
+            managedObjectContext: NSManagedObjectContext
+        ) -> [ReplyNode] {
+            guard let replyToID = replyToID else {
+                return []
+            }
+            
+            var nodes: [ReplyNode] = []
+            managedObjectContext.performAndWait {
+                let request = Status.sortedFetchRequest
+                request.predicate = Status.predicate(domain: domain, ids: statuses.map { $0.id })
+                request.fetchLimit = statuses.count
+                let objects = managedObjectContext.safeFetch(request)
+                
+                var objectDict: [Mastodon.Entity.Status.ID: Status] = [:]
+                for object in objects {
+                    objectDict[object.id] = object
+                }
+                var nextID: Mastodon.Entity.Status.ID? = replyToID
+                while let _nextID = nextID {
+                    guard let object = objectDict[_nextID] else { break }
+                    nodes.append(ThreadViewModel.ReplyNode(statusID: _nextID, statusObjectID: object.objectID))
+                    nextID = object.inReplyToID
+                }
+            }
+            return nodes.reversed()
+        }
+    }
+    
+    class LeafNode {
+        let statusID: Mastodon.Entity.Status.ID
+        let objectID: NSManagedObjectID
+        let repliesCount: Int
+        let children: [LeafNode]
+        
+        var isChildrenExpanded: Bool = false    // default collapsed
+        
+        init(
+            statusID: Mastodon.Entity.Status.ID,
+            objectID: NSManagedObjectID,
+            repliesCount: Int,
+            children: [ThreadViewModel.LeafNode]
+        ) {
+            self.statusID = statusID
+            self.objectID = objectID
+            self.repliesCount = repliesCount
+            self.children = children
+        }
+        
+        static func tree(
+            for statusID: Mastodon.Entity.Status.ID,
+            from statuses: [Mastodon.Entity.Status],
+            domain: String,
+            managedObjectContext: NSManagedObjectContext
+        ) -> [LeafNode] {
+            // make an cache collection
+            var objectDict: [Mastodon.Entity.Status.ID: Status] = [:]
+            
+            managedObjectContext.performAndWait {
+                let request = Status.sortedFetchRequest
+                request.predicate = Status.predicate(domain: domain, ids: statuses.map { $0.id })
+                request.fetchLimit = statuses.count
+                let objects = managedObjectContext.safeFetch(request)
+                
+                for object in objects {
+                    objectDict[object.id] = object
+                }
+            }
+            
+            var tree: [LeafNode] = []
+            let firstTierStatuses = statuses.filter { $0.inReplyToID == statusID }
+            for status in firstTierStatuses {
+                guard let node = node(of: status.id, objectDict: objectDict) else { continue }
+                tree.append(node)
+            }
+
+            return tree
+        }
+        
+        static func node(
+            of statusID: Mastodon.Entity.Status.ID,
+            objectDict: [Mastodon.Entity.Status.ID: Status]
+        ) -> LeafNode? {
+            guard let object = objectDict[statusID] else { return nil }
+            let replies = (object.replyFrom ?? Set()).sorted(
+                by: { $0.createdAt > $1.createdAt } // order by date
+            )
+            let children = replies.compactMap { node(of: $0.id, objectDict: objectDict) }
+            return LeafNode(
+                statusID: statusID,
+                objectID: object.objectID,
+                repliesCount: object.repliesCount?.intValue ?? 0,
+                children: children
+            )
+        }
+    }
+    
+}
+
diff --git a/Mastodon/Service/APIService/APIService+Thread.swift b/Mastodon/Service/APIService/APIService+Thread.swift
new file mode 100644
index 000000000..2633518ca
--- /dev/null
+++ b/Mastodon/Service/APIService/APIService+Thread.swift
@@ -0,0 +1,57 @@
+//
+//  APIService+Thread.swift
+//  Mastodon
+//
+//  Created by MainasuK Cirno on 2021-4-12.
+//
+
+import os.log
+import Foundation
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+extension APIService {
+    
+    func statusContext(
+        domain: String,
+        statusID: Mastodon.Entity.Status.ID,
+        mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
+    ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Context>, Error> {
+        let authorization = mastodonAuthenticationBox.userAuthorization
+        guard domain == mastodonAuthenticationBox.domain else {
+            return Fail(error: APIError.implicit(.badRequest)).eraseToAnyPublisher()
+        }
+        
+        return Mastodon.API.Statuses.statusContext(
+            session: session,
+            domain: domain,
+            statusID: statusID,
+            authorization: authorization
+        )
+        .flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Context>, Error> in
+            return APIService.Persist.persistStatus(
+                managedObjectContext: self.backgroundManagedObjectContext,
+                domain: domain,
+                query: nil,
+                response: response.map { $0.ancestors + $0.descendants },
+                persistType: .lookUp,
+                requestMastodonUserID: nil,
+                log: OSLog.api
+            )
+            .setFailureType(to: Error.self)
+            .tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Context> in
+                switch result {
+                case .success:
+                    return response
+                case .failure(let error):
+                    throw error
+                }
+            }
+            .eraseToAnyPublisher()
+        }
+        .eraseToAnyPublisher()
+    }
+    
+}
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift
index a05574b6b..328fa2305 100644
--- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift
@@ -86,8 +86,8 @@ extension APIService.CoreData {
                 let object = Poll.insert(into: managedObjectContext, property: Poll.Property(id: poll.id, expiresAt: poll.expiresAt, expired: poll.expired, multiple: poll.multiple, votesCount: poll.votesCount, votersCount: poll.votersCount, networkDate: networkDate), votedBy: votedBy, options: options)
                 return object
             }
-            let metions = entity.mentions?.compactMap { mention -> Mention in
-                Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url))
+            let metions = entity.mentions?.enumerated().compactMap { index, mention -> Mention in
+                Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url), index: index)
             }
             let emojis = entity.emojis?.compactMap { emoji -> Emoji in
                 Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift
index 9bc699b71..f5bb4ea3d 100644
--- a/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift
+++ b/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift
@@ -221,6 +221,13 @@ extension APIService.Persist {
                 break
             }
             
+            // reply relationship link
+            for (_, status) in statusCache.dictionary {
+                guard let replyToID = status.inReplyToID, status.replyTo == nil else { continue }
+                guard let replyTo = statusCache.dictionary[replyToID] else { continue }
+                status.update(replyTo: replyTo)
+            }
+            
             // print working record tree map
             #if DEBUG
             DispatchQueue.global(qos: .utility).async {
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift
index da54c9344..bb5a4abfc 100644
--- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift
@@ -98,6 +98,7 @@ extension Mastodon.API.Statuses {
         public let mediaIDs: [String]?
         public let pollOptions: [String]?
         public let pollExpiresIn: Int?
+        public let inReplyToID: Mastodon.Entity.Status.ID?
         public let sensitive: Bool?
         public let spoilerText: String?
         public let visibility: Mastodon.Entity.Status.Visibility?
@@ -107,6 +108,7 @@ extension Mastodon.API.Statuses {
             mediaIDs: [String]?,
             pollOptions: [String]?,
             pollExpiresIn: Int?,
+            inReplyToID: Mastodon.Entity.Status.ID?,
             sensitive: Bool?,
             spoilerText: String?,
             visibility: Mastodon.Entity.Status.Visibility?
@@ -115,10 +117,10 @@ extension Mastodon.API.Statuses {
             self.mediaIDs = mediaIDs
             self.pollOptions = pollOptions
             self.pollExpiresIn = pollExpiresIn
+            self.inReplyToID = inReplyToID
             self.sensitive = sensitive
             self.spoilerText = spoilerText
             self.visibility = visibility
-            
         }
         
         var contentType: String? {
@@ -136,6 +138,7 @@ extension Mastodon.API.Statuses {
                 data.append(Data.multipart(key: "poll[options][]", value: pollOption))
             }
             pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) }
+            inReplyToID.flatMap { data.append(Data.multipart(key: "in_reply_to_id", value: $0)) }
             sensitive.flatMap { data.append(Data.multipart(key: "sensitive", value: $0)) }
             spoilerText.flatMap { data.append(Data.multipart(key: "spoiler_text", value: $0)) }
             visibility.flatMap { data.append(Data.multipart(key: "visibility", value: $0.rawValue)) }
@@ -146,3 +149,46 @@ extension Mastodon.API.Statuses {
     }
     
 }
+
+extension Mastodon.API.Statuses {
+
+    static func statusContextEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL {
+        return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("statuses/\(statusID)/context")
+    }
+    
+    /// Parent and child statuses
+    ///
+    /// View statuses above and below this status in the thread.
+    ///
+    /// - Since: 0.0.0
+    /// - Version: 3.3.0
+    /// # Last Update
+    ///   2021/4/12
+    /// # Reference
+    ///   [Document](https://docs.joinmastodon.org/methods/statuses/)
+    /// - Parameters:
+    ///   - session: `URLSession`
+    ///   - domain: Mastodon instance domain. e.g. "example.com"
+    ///   - statusID: id of status
+    ///   - authorization: User token. Optional for public statuses
+    /// - Returns: `AnyPublisher` contains `Context` nested in the response
+    public static func statusContext(
+        session: URLSession,
+        domain: String,
+        statusID: Mastodon.Entity.Status.ID,
+        authorization: Mastodon.API.OAuth.Authorization?
+    ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Context>, Error>  {
+        let request = Mastodon.API.get(
+            url: statusContextEndpointURL(domain: domain, statusID: statusID),
+            query: nil,
+            authorization: authorization
+        )
+        return session.dataTaskPublisher(for: request)
+            .tryMap { data, response in
+                let value = try Mastodon.API.decode(type: Mastodon.Entity.Context.self, from: data, response: response)
+                return Mastodon.Response.Content(value: value, response: response)
+            }
+            .eraseToAnyPublisher()
+    }
+    
+}