diff --git a/.idea/.idea.Blacklight/.idea/avalonia.xml b/.idea/.idea.Blacklight/.idea/avalonia.xml
index 6dab72c4346600c9550bce9c1c32463e1940ebbb..9fd4a891368518cc19195f3795a5827cd9fde7a1 100644
--- a/.idea/.idea.Blacklight/.idea/avalonia.xml
+++ b/.idea/.idea.Blacklight/.idea/avalonia.xml
@@ -8,7 +8,7 @@
         <entry key="Blacklight/ErrorWindow.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
         <entry key="Blacklight/Views/App.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
         <entry key="Blacklight/Views/ClientView.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
-        <entry key="Blacklight/Views/Documents/DocumentView.axaml" value="Blacklight/Blacklight.csproj" />
+        <entry key="Blacklight/Views/Documents/DocumentView.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
         <entry key="Blacklight/Views/ErrorBoundary.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" />
         <entry key="Blacklight/Views/LoadingView.axaml" value="Blacklight/Blacklight.csproj" />
         <entry key="Blacklight/Views/LoadingViewModel.axaml" value="Blacklight/Blacklight.csproj" />
diff --git a/Blacklight.sln.DotSettings.user b/Blacklight.sln.DotSettings.user
index f1fd528f5d53e5cbb2a12ef757282ae170f664bb..89c0383e5dfa66cd5bfdeba7a59673c55b20b9bd 100644
--- a/Blacklight.sln.DotSettings.user
+++ b/Blacklight.sln.DotSettings.user
@@ -5,10 +5,12 @@
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABinding_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F72cdf2fe6a3b54998bac4691eedd12e9311b7efeae35e65311b545606e2f_003FBinding_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AContentControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa0b45743773945a3bb67ca6440ae9eadf9c00_003F50_003F9d315227_003FContentControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADockFluentTheme_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc13735b26c914624b4ee4a88a575ae253ae00_003Feb_003F23efc6b0_003FDockFluentTheme_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADocument_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F70d6cb8d5417c39b894d858f68789b12b9b5fc997e73e89dab1897b4c6e4bf_003FDocument_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe8b9e6b427dceb6f436414441b760d4dc52661df17f386ce4caa48aab354989_003FFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHtmlControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F58931fa041a846b53462c4c4c5f52620bf9de09eff582aa4b1ed83c83e4e10ea_003FHtmlControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAssetLoader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fda232ecc044f4c3d8e856366253516531d2800_003F_005F27262_003FIAssetLoader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F52dbc4b2fb874f79fb4fec0b07cf7f6ca6ac95616e1d164ef210a0645a1aa9_003FIFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e29f6bc726b4d68bc914cbabf2575377600_003F59_003Fa3291e5a_003FIMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIToolDock_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9c3ecc55c41dc7ed59e07448c63f6e65a746b8d44d381c2de59e8b39f659a968_003FIToolDock_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALowerCaseStringEnumConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e29f6bc726b4d68bc914cbabf2575377600_003F27_003F3006d2c3_003FLowerCaseStringEnumConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANetworkInformation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b1b048ea0e94b28ad7cbac8ef6bc4c26e00_003Fb4_003Fff405095_003FNetworkInformation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
diff --git a/Blacklight/App.axaml b/Blacklight/App.axaml
index 7253eff06d9441cb40e2cfc342d862a6d5b314d7..58766c3c758ccb1f919d13eab159594db250a1e3 100644
--- a/Blacklight/App.axaml
+++ b/Blacklight/App.axaml
@@ -8,11 +8,54 @@
 	xmlns:local="using:Blacklight"
 	xmlns:controls="clr-namespace:Dock.Model.Controls;assembly=Dock.Model"
 	xmlns:core="clr-namespace:Dock.Model.Core;assembly=Dock.Model"
+	xmlns:documents="clr-namespace:Blacklight.ViewModels.Documents"
+	xmlns:util="clr-namespace:Blacklight.Util"
 	RequestedThemeVariant="Default">
 	<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
 	<Application.Resources>
-		<idcr:ControlRecycling x:Key="ControlRecyclingKey" TryToUseIdAsKey="True" />
-		<Color x:Key="RegionColor">Transparent</Color>
+		<ResourceDictionary>
+			<idcr:ControlRecycling x:Key="ControlRecyclingKey" TryToUseIdAsKey="True" />
+			<util:MessageStatusEnumToColor x:Key="MessageStatusEnumToColor"/>
+			<Color x:Key="RegionColor">Transparent</Color>
+			<ResourceDictionary.ThemeDictionaries>
+				<ResourceDictionary x:Key="Light">
+					<SolidColorBrush x:Key="RightBeltColor">#E2e9e9e9</SolidColorBrush>
+					<SolidColorBrush x:Key="NavBarColor">#E2D4DCE2</SolidColorBrush>
+					<SolidColorBrush x:Key="NavBarBorderColor">#e1e0dd</SolidColorBrush>
+					<SolidColorBrush x:Key="ResourceExplorerColor">#E2e9e9e9</SolidColorBrush>
+					<SolidColorBrush x:Key="ResourceExplorerBorderColor">#e1e0dd</SolidColorBrush>
+					<SolidColorBrush x:Key="BottomBarColor">#e9e9e9</SolidColorBrush>
+					<SolidColorBrush x:Key="DocumentColor">#D6e9e9e9</SolidColorBrush>
+					<SolidColorBrush x:Key="TabStripColor">#E2e9e9e9</SolidColorBrush>
+					<SolidColorBrush x:Key="TabPointerOverColor">#1E00FF00</SolidColorBrush>
+					<SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush>
+					<SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush>
+					<SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush>
+					<SolidColorBrush x:Key="DockThemeForegroundBrush">#000000</SolidColorBrush>
+					<SolidColorBrush x:Key="DockApplicationAccentForegroundBrush">#000000</SolidColorBrush>
+					<SolidColorBrush x:Key="TabItemSelectedBorderColor">#4e4d4a</SolidColorBrush>
+					<SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush>
+					<SolidColorBrush x:Key="MessageContentBrush">#000000</SolidColorBrush>
+				</ResourceDictionary>
+				<ResourceDictionary x:Key="Dark">
+					<SolidColorBrush x:Key="RightBeltColor">#F22b2d30</SolidColorBrush>
+					<SolidColorBrush x:Key="NavBarColor">#F21e1f22</SolidColorBrush>
+					<SolidColorBrush x:Key="NavBarBorderColor">#3c3f41</SolidColorBrush>
+					<SolidColorBrush x:Key="ResourceExplorerColor">#F22b2d30</SolidColorBrush>
+					<SolidColorBrush x:Key="ResourceExplorerBorderColor">#1e1f22</SolidColorBrush>
+					<SolidColorBrush x:Key="BottomBarColor">#2a2c2f</SolidColorBrush>
+					<SolidColorBrush x:Key="DocumentColor">#E6262626</SolidColorBrush>
+					<SolidColorBrush x:Key="TabStripColor">#F22b2d30</SolidColorBrush>
+					<SolidColorBrush x:Key="TabPointerOverColor">#0A00FF00</SolidColorBrush>
+					<SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush>
+					<SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush>
+					<SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush>
+					<SolidColorBrush x:Key="TabItemSelectedBorderColor">#b1b2b5</SolidColorBrush>
+					<SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush>
+					<SolidColorBrush x:Key="MessageContentBrush">#FFFFFF</SolidColorBrush>
+				</ResourceDictionary>
+			</ResourceDictionary.ThemeDictionaries>
+		</ResourceDictionary>
 	</Application.Resources>
 	<Application.DataTemplates>
 		<local:ViewLocator />
@@ -55,12 +98,12 @@
 		<Style Selector="DocumentControl">
 
 			<Setter Property="HeaderTemplate">
-				<DataTemplate DataType="core:IDockable">
+				<DataTemplate DataType="documents:DocumentViewModel">
 					<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="6, 2">
 						<TextBlock Margin="0, 0, 2, 0" FontSize="16" Foreground="{DynamicResource ChannelHashtagColor}">
 							#
 						</TextBlock>
-						<TextBlock VerticalAlignment="Center" Text="{Binding Title}" />
+						<TextBlock VerticalAlignment="Center" FontStyle="{Binding FontStyle}" Text="{Binding Title}" />
 					</StackPanel>
 				</DataTemplate>
 			</Setter>
diff --git a/Blacklight/App.axaml.cs b/Blacklight/App.axaml.cs
index cb7d2e5471e976f8639956e696babf9462d6e2ab..1d156443e8516b539b626c7f77f998c1936fecb2 100644
--- a/Blacklight/App.axaml.cs
+++ b/Blacklight/App.axaml.cs
@@ -5,6 +5,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
 using Blacklight.Util;
 using Blacklight.ViewModels;
 using Blacklight.Views;
@@ -19,6 +20,7 @@ public partial class App : Application
 {
     
     public static IServiceProvider? Services { get; set; }
+    public static DockContext DockContext { get; set; }
     
     public override void Initialize()
     {
@@ -55,8 +57,11 @@ public partial class App : Application
             var client = serviceProvider.GetRequiredService<Client>();
             client.OnLogOut = () =>
             {
-                mainWindowVm.CurrentViewModel = loginViewModel;
-                loginViewModel.CurrentControl = new MainLoginPrompt();
+                Dispatcher.UIThread.Invoke(() =>
+                {
+                    mainWindowVm.CurrentViewModel = loginViewModel;
+                    loginViewModel.CurrentControl = new MainLoginPrompt();
+                });
                 var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
                 try
                 {
diff --git a/Blacklight/Util/MessageStatusEnumToColor.cs b/Blacklight/Util/MessageStatusEnumToColor.cs
new file mode 100644
index 0000000000000000000000000000000000000000..06b0e5f04c1d296fab4f231ad73f22966c927da4
--- /dev/null
+++ b/Blacklight/Util/MessageStatusEnumToColor.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace Blacklight.Util;
+
+public class MessageStatusEnumToColor : IValueConverter
+{
+    public static readonly MessageStatusEnumToColor Instance = new();
+    
+    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value != null)
+        {
+            // If it's not null, just return the value unchanged
+            return value;
+        }
+
+        // If null, attempt to retrieve the resource from Application.Current.Resources
+        var app = Application.Current;
+        if (app?.Resources.TryGetValue("MessageContentBrush", out var foundResource) == true)
+        {
+            return foundResource;
+        }
+        
+        // If the resource is not found, return AvaloniaProperty.UnsetValue or a fallback
+        return Colors.Orange;
+    }
+
+    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        // Usually not needed; throw or return original
+        return value;
+    }
+}
\ No newline at end of file
diff --git a/Blacklight/ViewModels/DockFactory.cs b/Blacklight/ViewModels/DockFactory.cs
index 14e43fe7f7f86548306e478bd6149e7c236c4ba2..3026c107a3ba8738d0eca985fabba254002c9c7f 100644
--- a/Blacklight/ViewModels/DockFactory.cs
+++ b/Blacklight/ViewModels/DockFactory.cs
@@ -1,6 +1,6 @@
 using System;
 using System.Collections.Generic;
-using Blacklight.Models.Documents;
+using System.Linq;
 using Blacklight.Models.Tools;
 using Blacklight.ViewModels.Docks;
 using Blacklight.ViewModels.Documents;
@@ -10,6 +10,7 @@ using Dock.Model.Controls;
 using Dock.Model.Core;
 using Dock.Model.Mvvm;
 using Dock.Model.Mvvm.Controls;
+using Lightquark.NET.Objects;
 
 namespace Blacklight.ViewModels;
 
@@ -22,12 +23,12 @@ public class DockFactory : Factory
 
     public override IRootDock CreateLayout()
     {
-        var document1 = new DocumentViewModel {Id = "Document1", Title = "Document 1", CanPin = false, CanFloat = false};
-        var document2 = new DocumentViewModel {Id = "Document2", Title = "Document 2", CanPin = false, CanFloat = false};
-        var document3 = new DocumentViewModel {Id = "Document3", Title = "Document 3", CanPin = false, CanFloat = false};
-        var document4 = new DocumentViewModel {Id = "Document4", Title = "Document 4", CanPin = false, CanFloat = false};
-        var document5 = new DocumentViewModel {Id = "Document5", Title = "Document 5", CanPin = false, CanFloat = false};
-        var document6 = new DocumentViewModel {Id = "Document6", Title = "Document 6", CanPin = false, CanFloat = false};
+        // var document1 = new DocumentViewModel {Id = "Document1", Title = "Document 1", CanPin = false, CanFloat = false};
+        // var document2 = new DocumentViewModel {Id = "Document2", Title = "Document 2", CanPin = false, CanFloat = false};
+        // var document3 = new DocumentViewModel {Id = "Document3", Title = "Document 3", CanPin = false, CanFloat = false};
+        // var document4 = new DocumentViewModel {Id = "Document4", Title = "Document 4", CanPin = false, CanFloat = false};
+        // var document5 = new DocumentViewModel {Id = "Document5", Title = "Document 5", CanPin = false, CanFloat = false};
+        // var document6 = new DocumentViewModel {Id = "Document6", Title = "Document 6", CanPin = false, CanFloat = false};
         
         var resourceExplorer = new ResourceExplorerViewModel
         {
@@ -65,8 +66,8 @@ public class DockFactory : Factory
         var documentDock = new CustomDocumentDock
         {
             IsCollapsable = false,
-            ActiveDockable = document1,
-            VisibleDockables = CreateList<IDockable>(document1, document2, document3, document4, document5, document6),
+            ActiveDockable = null,
+            VisibleDockables = CreateList<IDockable>(),
             CanCreateDocument = false
         };
         
@@ -93,6 +94,8 @@ public class DockFactory : Factory
         
         _documentDock = documentDock;
         _rootDock = rootDock;
+
+        App.DockContext = new DockContext(_documentDock, mainLayout);
         
         return rootDock;
     }
@@ -101,8 +104,6 @@ public class DockFactory : Factory
     {
         ContextLocator = new Dictionary<string, Func<object?>>
         {
-            ["Document1"] = () => new Document1(),
-            ["Document2"] = () => new Document1(),
             ["Resource Explorer"] = () => new ResourceExplorer(),
         };
 
@@ -119,4 +120,119 @@ public class DockFactory : Factory
 
         base.InitLayout(layout);
     }
-}
\ No newline at end of file
+}
+
+public class DockContext
+{
+    private IDock _mainLayoutDock;
+    private IDocumentDock DocumentDock => GetDocumentDocks(_mainLayoutDock).First().DocumentDock;
+
+    private DocumentViewModel? _ephemeralDocument;
+
+    public DockContext(IDocumentDock documentDock, IDock mainLayoutDock)
+    {
+        _mainLayoutDock = mainLayoutDock;
+    }
+
+    private List<DocumentDockWithParent> GetDocumentDocks(IDock dock)
+    {
+        List<DocumentDockWithParent> documentDocks = [];
+        if (dock.VisibleDockables == null) return documentDocks;
+        foreach (var dockable in dock.VisibleDockables)
+        {
+            switch (dockable)
+            {
+                case IDocumentDock docDock:
+                    documentDocks.Add(new DocumentDockWithParent
+                    {
+                        DocumentDock = docDock,
+                        ParentDock = dock
+                    });
+                    break;
+                case IProportionalDock propDock:
+                    documentDocks.AddRange(GetDocumentDocks(propDock));
+                    break;
+            }
+        }
+
+        return documentDocks;
+    }
+    
+    public void AddDocument(DocumentViewModel document, IDock? parent = null)
+    {
+        parent ??= DocumentDock;
+        if (parent.VisibleDockables == null) throw new Exception("wtf? parent.VisibleDockables is null");
+        parent.VisibleDockables.Add(document);
+        parent.ActiveDockable = document;
+    }
+
+    public void RemoveDocument(DocumentViewModel document)
+    {
+        var docks = GetDocumentDocks(_mainLayoutDock);
+        foreach (var dockWithParent in docks)
+        {
+            if (dockWithParent.DocumentDock.VisibleDockables == null) throw new Exception("wtf? _documentDock.VisibleDockables is null");
+            dockWithParent.DocumentDock.VisibleDockables.Remove(document);
+        }
+    }
+
+    public bool TryFocusChannel(Channel channel, bool makePermanent = false)
+    {
+        var docks = GetDocumentDocks(_mainLayoutDock);
+        foreach (var dockWithParent in docks)
+        {
+            if (dockWithParent.DocumentDock.VisibleDockables?.FirstOrDefault(d => (d as DocumentViewModel)?.Channel == channel) is not DocumentViewModel doc) continue;
+            dockWithParent.DocumentDock.ActiveDockable = doc;
+            dockWithParent.ParentDock.ActiveDockable = dockWithParent.DocumentDock;
+            if (makePermanent)
+            {
+                if (_ephemeralDocument?.Channel == channel)
+                {
+                    _ephemeralDocument = null;
+                }
+                doc.IsEphemeral = false;
+            }
+
+            return true;
+        }
+        return false;
+    }
+    
+    public void AddDocument(Channel channel, bool isEphemeral, bool shouldCloseEphemeral = true)
+    {
+        var document = new DocumentViewModel
+        {
+            Id = channel.Id.ToString(),
+            Channel = channel,
+            Title = channel.Name,
+            CanPin = false,
+            CanFloat = false
+        };
+        if (!isEphemeral)
+        {
+            if (_ephemeralDocument?.Channel == channel)
+            {
+                // I am not sure this bit is ever actually run, since TryFocusChannel does similar stuff
+                _ephemeralDocument.IsEphemeral = false;
+                DocumentDock.ActiveDockable = _ephemeralDocument;
+                _ephemeralDocument = null;
+            }
+            else
+            {
+                AddDocument(document);
+            }
+            return;
+        }
+
+        if (shouldCloseEphemeral && _ephemeralDocument != null) RemoveDocument(_ephemeralDocument);
+        document.IsEphemeral = true;
+        _ephemeralDocument = document;
+        AddDocument(_ephemeralDocument);
+    }
+    
+    private record DocumentDockWithParent
+    {
+        public required IDocumentDock DocumentDock;
+        public required IDock ParentDock;
+    }
+}
diff --git a/Blacklight/ViewModels/Documents/DocumentViewModel.cs b/Blacklight/ViewModels/Documents/DocumentViewModel.cs
index 778cd134ecbd1495443fad05fd2138424816a277..d43ac06eeb8fac54ab1311dc9a8503d639313bfe 100644
--- a/Blacklight/ViewModels/Documents/DocumentViewModel.cs
+++ b/Blacklight/ViewModels/Documents/DocumentViewModel.cs
@@ -1,7 +1,75 @@
-using Dock.Model.Mvvm.Controls;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+using Avalonia.Media;
+using CommunityToolkit.Mvvm.Input;
+using Dock.Model.Mvvm.Controls;
+using Lightquark.NET;
+using Lightquark.NET.Objects;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
 
 namespace Blacklight.ViewModels.Documents;
 
 public class DocumentViewModel : Document
 {
+    private bool _isEphemeral;
+    private string _messageInput = "";
+    public required Channel Channel { get; set; }
+
+    public ObservableCollection<Message> Messages
+    {
+        get
+        {
+            var client = App.Services!.GetRequiredService<Client>();
+            if (!client.Messages.TryGetValue(Channel.Id, out var messageCollection))
+            {
+                Log.Information("Channel {ChannelId} has no messages, creating collection", Channel.Id);
+                messageCollection = new ObservableCollection<Message>();
+                client.Messages[Channel.Id] = messageCollection;
+            }
+            return messageCollection;
+        }
+    }
+
+    public bool IsEphemeral
+    {
+        get => _isEphemeral;
+        set
+        {
+            if (SetProperty(ref _isEphemeral, value)) OnPropertyChanged(nameof(FontStyle));
+        }
+    }
+
+    public FontStyle FontStyle => IsEphemeral ? FontStyle.Italic : FontStyle.Normal;
+    public ICommand SendCommand => new RelayCommand(SendMessage);
+
+    public string MessageInput
+    {
+        get => _messageInput;
+        set => SetProperty(ref _messageInput, value);
+    }
+
+    public ICommand TestCommand => new RelayCommand(Test);
+
+    private void Test()
+    {
+        Log.Information("Channel {ChannelId} has {MessageCount} messages", Channel.Id, Messages.Count);
+    }
+
+    private async void SendMessage()
+    {
+        try
+        {
+            Log.Information("Sending message {MessageInput}", MessageInput);
+            var client = App.Services!.GetRequiredService<Client>();
+            client.SendMessage(MessageInput, Channel.Id);
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "Failed to send message");
+        }
+        MessageInput = "";
+    }
 }
\ No newline at end of file
diff --git a/Blacklight/Views/Documents/DocumentView.axaml b/Blacklight/Views/Documents/DocumentView.axaml
index 20776a35eb0f68201e6ba4d646724222522e37e1..6f3a5b98fe87e5ab464eb89c890508513b07c9ec 100644
--- a/Blacklight/Views/Documents/DocumentView.axaml
+++ b/Blacklight/Views/Documents/DocumentView.axaml
@@ -4,14 +4,52 @@
               xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
               xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
               xmlns:vm="using:Blacklight.ViewModels.Documents"
+              xmlns:util="clr-namespace:Blacklight.Util"
+              xmlns:objects="clr-namespace:Lightquark.NET.Objects;assembly=Lightquark.NET"
+              xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections"
               mc:Ignorable="d"
               d:DesignWidth="300" d:DesignHeight="400"
               x:DataType="vm:DocumentViewModel" x:CompileBindings="True">
-    <Grid Focusable="True" Background="{DynamicResource DocumentColor}">
-        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
-            <TextBlock Text="{Binding Id}" Padding="2" />
-            <TextBlock Text="{Binding Title}" Padding="2" />
-            <TextBlock Text="{Binding Context}" Padding="2" />
-        </StackPanel>
-    </Grid>
+    <!-- <Grid Focusable="True" Background="{DynamicResource DocumentColor}"> -->
+        <Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="{DynamicResource DocumentColor}">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="*"/>
+                <RowDefinition Height="Auto"/>
+            </Grid.RowDefinitions>
+            <StackPanel Grid.Row="0">
+                <!-- <TextBlock Text="{Binding Id}" Padding="2" /> -->
+                <!-- <TextBlock Text="{Binding Title}" Padding="2" /> -->
+                <!-- <TextBlock Text="{Binding Context}" Padding="2" /> -->
+                <!-- <TextBlock Text="{Binding IsEphemeral}" Padding="2" /> -->
+                <!-- <Button Command="{Binding TestCommand}">Meow</Button> -->
+                <ItemsControl ItemsSource="{Binding Messages}" >
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate>
+                            <StackPanel Margin="8 3" Orientation="Vertical">
+                                <StackPanel Orientation="Horizontal">
+                                    <TextBlock VerticalAlignment="Center" FontWeight="Bold" Text="{Binding VisualAuthor.Username}" />
+                                    <Border Background="DeepPink" Padding="2" CornerRadius="3" Margin="5 0" IsVisible="{Binding VisualAuthor.IsBot}">
+                                        <TextBlock FontWeight="Bold" Text="{Binding VisualAuthor.BotTag}" />
+                                    </Border>
+                                </StackPanel>
+                                <TextBlock Foreground="{Binding Visuals.TextColor, Converter={StaticResource MessageStatusEnumToColor}}" Text="{Binding Content}" />
+                            </StackPanel>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                </ItemsControl>
+            </StackPanel>
+            <Grid Grid.Row="1" HorizontalAlignment="Stretch">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="*" />
+                    <ColumnDefinition Width="Auto" />
+                </Grid.ColumnDefinitions>
+                <TextBox MaxHeight="200" Grid.Column="0" AcceptsReturn="True" TextWrapping="Wrap" Text="{Binding MessageInput}">
+                    <TextBox.KeyBindings>
+                        <KeyBinding Gesture="Enter" Command="{Binding SendCommand}"/>
+                    </TextBox.KeyBindings>
+                </TextBox>
+                <Button Grid.Column="1" Command="{Binding SendCommand}">Send</Button>
+            </Grid>
+        </Grid>
+    <!-- </Grid> -->
 </UserControl>
\ No newline at end of file
diff --git a/Blacklight/Views/MainWindow.axaml b/Blacklight/Views/MainWindow.axaml
index d297dbf3cafd23f650f77c5ce77335128188e353..f835523f6d0e5f40263231284711536d5353b088 100644
--- a/Blacklight/Views/MainWindow.axaml
+++ b/Blacklight/Views/MainWindow.axaml
@@ -6,6 +6,7 @@
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:login="clr-namespace:Blacklight.Views.Login"
+        xmlns:util="clr-namespace:Blacklight.Util"
         mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
         x:Class="Blacklight.Views.MainWindow"
         x:DataType="vm:MainWindowViewModel"
@@ -23,44 +24,6 @@
 	</Window.Background>
 
 	<Window.Resources>
-		<ResourceDictionary>
-			<ResourceDictionary.ThemeDictionaries>
-				<ResourceDictionary x:Key="Light">
-					<SolidColorBrush x:Key="RightBeltColor">#E2e9e9e9</SolidColorBrush>
-					<SolidColorBrush x:Key="NavBarColor">#E2D4DCE2</SolidColorBrush>
-					<SolidColorBrush x:Key="NavBarBorderColor">#e1e0dd</SolidColorBrush>
-					<SolidColorBrush x:Key="ResourceExplorerColor">#E2e9e9e9</SolidColorBrush>
-					<SolidColorBrush x:Key="ResourceExplorerBorderColor">#e1e0dd</SolidColorBrush>
-					<SolidColorBrush x:Key="BottomBarColor">#e9e9e9</SolidColorBrush>
-					<SolidColorBrush x:Key="DocumentColor">#D6e9e9e9</SolidColorBrush>
-					<SolidColorBrush x:Key="TabStripColor">#E2e9e9e9</SolidColorBrush>
-					<SolidColorBrush x:Key="TabPointerOverColor">#1E00FF00</SolidColorBrush>
-					<SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush>
-					<SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush>
-					<SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush>
-					<SolidColorBrush x:Key="DockThemeForegroundBrush">#000000</SolidColorBrush>
-					<SolidColorBrush x:Key="DockApplicationAccentForegroundBrush">#000000</SolidColorBrush>
-					<SolidColorBrush x:Key="TabItemSelectedBorderColor">#4e4d4a</SolidColorBrush>
-					<SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush>
-				</ResourceDictionary>
-				<ResourceDictionary x:Key="Dark">
-					<SolidColorBrush x:Key="RightBeltColor">#F22b2d30</SolidColorBrush>
-					<SolidColorBrush x:Key="NavBarColor">#F21e1f22</SolidColorBrush>
-					<SolidColorBrush x:Key="NavBarBorderColor">#3c3f41</SolidColorBrush>
-					<SolidColorBrush x:Key="ResourceExplorerColor">#F22b2d30</SolidColorBrush>
-					<SolidColorBrush x:Key="ResourceExplorerBorderColor">#1e1f22</SolidColorBrush>
-					<SolidColorBrush x:Key="BottomBarColor">#2a2c2f</SolidColorBrush>
-					<SolidColorBrush x:Key="DocumentColor">#E6262626</SolidColorBrush>
-					<SolidColorBrush x:Key="TabStripColor">#F22b2d30</SolidColorBrush>
-					<SolidColorBrush x:Key="TabPointerOverColor">#0A00FF00</SolidColorBrush>
-					<SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush>
-					<SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush>
-					<SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush>
-					<SolidColorBrush x:Key="TabItemSelectedBorderColor">#b1b2b5</SolidColorBrush>
-					<SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush>
-				</ResourceDictionary>
-			</ResourceDictionary.ThemeDictionaries>
-		</ResourceDictionary>
 	</Window.Resources>
 
 	<Window.DataTemplates>
diff --git a/Blacklight/Views/Tools/ResourceExplorerView.axaml b/Blacklight/Views/Tools/ResourceExplorerView.axaml
index 12532ac0cc605fb7077e77b376f055370b4f6f35..87ca236692cd657052f618737cc74b6f789f5413 100644
--- a/Blacklight/Views/Tools/ResourceExplorerView.axaml
+++ b/Blacklight/Views/Tools/ResourceExplorerView.axaml
@@ -9,7 +9,7 @@
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="Blacklight.Views.Tools.ResourceExplorerView">
 	<Grid ColumnDefinitions="*" RowDefinitions="*" Background="{DynamicResource ResourceExplorerColor}">
-		<TreeView Grid.Row="0" ItemsSource="{Binding $parent[views:ClientView].((viewModels:ClientViewModel)DataContext).Client.ResourceTree}">
+		<TreeView Grid.Row="0" ItemsSource="{Binding $parent[views:ClientView].((viewModels:ClientViewModel)DataContext).Client.ResourceTree}" SelectionChanged="ItemSelected">
 	    <TreeView.DataTemplates>
 		    <util:ResourceExplorerTemplateSelector>
 			    <TreeDataTemplate x:Key="QuarkTemplate" x:DataType="objects:QuarkListItem" ItemsSource="{Binding Children}">
diff --git a/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs b/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs
index 0e17f96a38f11c05604a2ab721755ae8256f181c..2cbaf966f508feeb002cba5856b11f26334f4917 100644
--- a/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs
+++ b/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs
@@ -1,18 +1,68 @@
-using Avalonia;
+using System;
+using System.Timers;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Markup.Xaml;
 using Blacklight.Util;
 using Dock.Model.Controls;
 using Dock.Model.Core;
+using Lightquark.NET.Objects;
+using Serilog;
 
 namespace Blacklight.Views.Tools;
 
 public partial class ResourceExplorerView : UserControl
 {
-    
+    private QuarkListItem? _selectedItem;
+    private Timer? _doubleTimer;
     public ResourceExplorerView()
     {
         InitializeComponent();
     }
+
+    private void ItemSelected(object? sender, SelectionChangedEventArgs e)
+    {
+        if (sender is TreeView treeView && treeView?.SelectedItem is QuarkListItem quarkListItem)
+        {
+            switch (quarkListItem.Type)
+            {
+                case QuarkListItemType.Channel:
+                    if (_selectedItem == quarkListItem)
+                    {
+                        var focus = App.DockContext.TryFocusChannel(quarkListItem.Channel!, true);
+                        if (focus) return;
+                        Log.Information("zamn thats a double click");
+                        _doubleTimer?.Dispose();
+                        _selectedItem = null;
+                        App.DockContext.AddDocument(quarkListItem.Channel!, false);
+                        // TODO: Open persistent tab
+                    }
+                    else
+                    {
+                        _selectedItem = quarkListItem;
+                        var focus = App.DockContext.TryFocusChannel(quarkListItem.Channel!);
+                        if (focus) return;
+                        _doubleTimer?.Dispose();
+                        _doubleTimer = new Timer(TimeSpan.FromSeconds(5));
+                        _doubleTimer.Elapsed += (_, _) =>
+                        {
+                            Log.Information("Welp they took too long to click im tired of waiting");
+                            _selectedItem = null;
+                            _doubleTimer?.Dispose();
+                        };
+                        _doubleTimer.Start();
+
+                        // TODO: Open or update ephemeral tab
+                        App.DockContext.AddDocument(quarkListItem.Channel!, true);
+                    }
+                    break;
+                // Currently we only care about channels
+                case QuarkListItemType.Quark:
+                case QuarkListItemType.Folder:
+                default:
+                    return;
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/Lightquark.NET/Assets/StaticFallback.cs b/Lightquark.NET/Assets/StaticFallback.cs
new file mode 100644
index 0000000000000000000000000000000000000000..36bc7d68c24066e3df49c89bf06eb6d883c20017
--- /dev/null
+++ b/Lightquark.NET/Assets/StaticFallback.cs
@@ -0,0 +1,9 @@
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+
+namespace Lightquark.NET.Assets;
+
+internal class StaticFallback
+{
+    public static Bitmap QuarkIcon = new Bitmap(AssetLoader.Open(new Uri("avares://Lightquark.NET/Assets/missing.png")));
+}
\ No newline at end of file
diff --git a/Lightquark.NET/Assets/missing.png b/Lightquark.NET/Assets/missing.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2e3d4158502266dfab8b3f8fc89d901e4509b86
Binary files /dev/null and b/Lightquark.NET/Assets/missing.png differ
diff --git a/Lightquark.NET/Client.cs b/Lightquark.NET/Client.cs
index df35aecae41ff78f3554cf6c91e27777bc165683..5bbf9697d4b9955f4eacc13fc7aa45200eb3dfd0 100644
--- a/Lightquark.NET/Client.cs
+++ b/Lightquark.NET/Client.cs
@@ -26,7 +26,7 @@ public partial class Client : ObservableObject
     public ObservableCollection<QuarkListItem> ResourceTree { get; } = [];
     public ObservableCollection<Quark> Quarks { get; } = [];
     public ObservableCollection<Channel> Channels { get; } = [];
-    public ObservableCollection<Message> Messages { get; } = [];
+    public Dictionary<ObjectId, ObservableCollection<Message>> Messages = new();
     public ObservableCollection<User> Users { get; } = [];
 
     public User? CurrentUser
diff --git a/Lightquark.NET/ClientMethods/Call.cs b/Lightquark.NET/ClientMethods/Call.cs
new file mode 100644
index 0000000000000000000000000000000000000000..27b3725c1b4686b5f108e4e4cf198696c988f6c4
--- /dev/null
+++ b/Lightquark.NET/ClientMethods/Call.cs
@@ -0,0 +1,67 @@
+using System.Net.Http.Headers;
+using System.Reflection;
+using Lightquark.NET.Objects.Reply;
+using Newtonsoft.Json;
+using Serilog;
+
+namespace Lightquark.NET;
+
+public partial class Client 
+{
+    public async Task<Reply<T>> Call<T>(string method, string route, object? body) where T : BaseReplyResponse
+    {
+        Log.Information("Starting HTTP to {Method} {Route}", method, route);
+        try
+        {
+            var finalUri = new Uri(new Uri(NetworkInformation!.BaseUrl!), route);
+            Log.Information("Using app server {BaseUrl}", NetworkInformation.BaseUrl);
+
+            HttpContent finalBody;
+            if (body is MultipartFormDataContent formDataBody)
+            {
+                finalBody = formDataBody;
+            }
+            else
+            {
+                finalBody = new StringContent(JsonConvert.SerializeObject(body));
+                finalBody.Headers.ContentType = new MediaTypeHeaderValue("application/json");
+            }
+
+            var request = new HttpRequestMessage();
+            request.Method = method switch
+            {
+                "GET" => HttpMethod.Get,
+                "POST" => HttpMethod.Post,
+                "PATCH" => HttpMethod.Patch,
+                "PUT" => HttpMethod.Put,
+                "HEAD" => HttpMethod.Head,
+                "DELETE" => HttpMethod.Delete,
+                _ => throw new ArgumentOutOfRangeException(nameof(method), method, null)
+            };
+            request.RequestUri = finalUri;
+            request.Content = finalBody;
+            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
+            request.Headers.Add("lq-agent", $"Blacklight/{Assembly.GetEntryAssembly()?.GetName().Version?.ToString()}");
+            
+            var res = await _httpClient.SendAsync(request);
+
+            Log.Information("Received response for {Path} with code {StatusCode}", route, res.StatusCode);
+            
+            var rawResponse = await res.Content.ReadAsStringAsync();
+            
+            Log.Debug(rawResponse);
+            
+            var response = JsonConvert.DeserializeObject<Reply<T>>(rawResponse);
+
+            if (response == null) throw new Exception($"Failed to parse API response: {rawResponse}");
+
+            return response;
+
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "Failed HTTP call to API");
+            throw;
+        }
+    } 
+}
\ No newline at end of file
diff --git a/Lightquark.NET/ClientMethods/Gateway.cs b/Lightquark.NET/ClientMethods/Gateway.cs
index 8de712c0b6b516a30d08269c6413cdbccb30f7fe..ae5c87b8a1a168809c95f8dff5f1bf1d67e088c6 100644
--- a/Lightquark.NET/ClientMethods/Gateway.cs
+++ b/Lightquark.NET/ClientMethods/Gateway.cs
@@ -2,6 +2,8 @@
 using Lightquark.NET.Objects;
 using Lightquark.NET.Objects.Reply;
 using Lightquark.NET.Util;
+using Lightquark.NET.Util.Converters;
+using MongoDB.Bson;
 using Newtonsoft.Json;
 using Serilog;
 using Websocket.Client;
@@ -101,9 +103,13 @@ public partial class Client
                             CurrentUserFromObject(userUpdateMessage.User);
                         }
                         break;
+                    case "messageCreate":
+                        var messageCreateMessage = ParseGateway<MessageCreateGatewayMessage>(msg.Text!);
+                        MessageFromObject(messageCreateMessage.Message, messageCreateMessage.ChannelId);
+                        break;
                     case "gatekeeperMeasure":
                         var measureMessage = ParseGateway<MeasureGatewayMessage>(msg.Text!);
-                        // GatekeeperMeasure(measureMessage);
+                        GatekeeperMeasure(measureMessage);
                         break;
                     case "gatekeeperSelection":
                         var selectionMessage = ParseGateway<SelectionGatewayMessage>(msg.Text!);
@@ -124,7 +130,7 @@ public partial class Client
     {
         SendGatewayMessage(new {@event = "heartbeat"});
     }
-
+    
     public async Task<Reply<T>> CallRpc<T>(string method, string route, object? body) where T : BaseReplyResponse
     {
         Log.Information("Starting RPC to {Method} {Route}", method, route);
@@ -224,6 +230,15 @@ public record UserUpdateGatewayMessage : BaseGatewayMessage
     public required User User { get; init; }
 }
 
+public record MessageCreateGatewayMessage : BaseGatewayMessage
+{
+    [JsonProperty("message")]
+    public required Message Message { get; init; }
+    [JsonProperty("channelId")]
+    [JsonConverter(typeof(ObjectIdConverter))]
+    public required ObjectId ChannelId { get; init; }
+}
+
 public record GatewayBusEvent
 {
     public required BaseGatewayMessage BaseMessage { get; init; }
diff --git a/Lightquark.NET/ClientMethods/Message.cs b/Lightquark.NET/ClientMethods/Message.cs
new file mode 100644
index 0000000000000000000000000000000000000000..72a532a74d7f225109942d07b75fecf3fead6a38
--- /dev/null
+++ b/Lightquark.NET/ClientMethods/Message.cs
@@ -0,0 +1,70 @@
+using System.Collections.ObjectModel;
+using Lightquark.NET.Objects;
+using Lightquark.NET.Objects.Reply;
+using MongoDB.Bson;
+using Newtonsoft.Json;
+using Serilog;
+
+namespace Lightquark.NET;
+
+public partial class Client
+{
+    public async void SendMessage(string message, ObjectId channelId)
+    {
+        try
+        {
+            var body = new MultipartFormDataContent();
+            body.Add(new StringContent(JsonConvert.SerializeObject(new
+            {
+                content = message
+            })), "payload");
+            var resTask = Call<MessageSendResponse>("POST", $"/{Version}/channel/{channelId}/messages", body);
+            var fakeId = ObjectId.GenerateNewId();
+            var fakeMsg = new Message
+            {
+                Id = fakeId,
+                Author = CurrentUser,
+                Content = message,
+                Status = MessageStatus.Sending
+            };
+            Messages[channelId].Add(fakeMsg);
+            var res = await resTask;
+            if (res.Request.Success)
+            {
+                Messages[channelId].Remove(fakeMsg);
+            }
+            else
+            {
+                fakeMsg.Status = MessageStatus.Fail;
+                fakeMsg.Content += $"\nFailed to send: {res.Response.Message}";
+            }
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "Failed to send message");
+        }
+    }
+
+    private void MessageFromObject(Message message, ObjectId? channelId = null)
+    {
+        channelId ??= message.ChannelId;
+        message.ChannelId = channelId.Value;
+        if (!Messages.TryGetValue(channelId.Value, out var messageCollection))
+        {
+            Log.Information("Channel {ChannelId} has no prior messages, creating collection", channelId);
+            messageCollection = [];
+            Messages[channelId.Value] = messageCollection;
+        }
+        // If no message with same id in collection add message
+        if (messageCollection.All(m => m.Id != message.Id))
+        {
+            Log.Information("New message {MessageId} in channel {ChannelId}", message.Id, channelId.Value);
+            messageCollection.Add(message);
+        }
+    }
+}
+
+public record MessageSendResponse : BaseReplyResponse
+{
+    
+}
\ No newline at end of file
diff --git a/Lightquark.NET/ClientMethods/Quark.cs b/Lightquark.NET/ClientMethods/Quark.cs
index db44a8dd489ce237785b872699bad47c568ebdd6..c834c384eacb10d68f84997aa720d127876570d6 100644
--- a/Lightquark.NET/ClientMethods/Quark.cs
+++ b/Lightquark.NET/ClientMethods/Quark.cs
@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using Avalonia.Media.Imaging;
+using Lightquark.NET.Assets;
 using Lightquark.NET.Objects;
 using Lightquark.NET.Objects.Reply;
 using MongoDB.Bson;
@@ -251,6 +252,7 @@ public partial class Client
         catch (Exception ex)
         {
             Log.Error(ex, "Failed to get quark icon for {@Quark}", quark);
+            quark.Icon = StaticFallback.QuarkIcon;
         }
         return quark;
     }
diff --git a/Lightquark.NET/ClientMethods/User.cs b/Lightquark.NET/ClientMethods/User.cs
index a9c0ef9de8f5eee030eb9aeba41492ef24ea1d80..14ce46715dd9ccc49ab2dbaacfcd61331294de0b 100644
--- a/Lightquark.NET/ClientMethods/User.cs
+++ b/Lightquark.NET/ClientMethods/User.cs
@@ -18,7 +18,7 @@ public partial class Client
     {
         try
         {
-            var avatarRes = await _httpClient.GetAsync(user!.AvatarUriGetter);
+            var avatarRes = await _httpClient.GetAsync(user!.AvatarUri);
             user.Avatar = new Bitmap(await avatarRes.Content.ReadAsStreamAsync());
         }
         catch (Exception ex)
diff --git a/Lightquark.NET/Lightquark.NET.csproj b/Lightquark.NET/Lightquark.NET.csproj
index 3b3b9bf9fa613ad7233670cd4cf13ac2cea5660e..c2faf49800f4cb4ad7bc27254e6c2e540ff1cc0c 100644
--- a/Lightquark.NET/Lightquark.NET.csproj
+++ b/Lightquark.NET/Lightquark.NET.csproj
@@ -17,4 +17,8 @@
       <PackageReference Include="Websocket.Client" Version="5.1.2" />
     </ItemGroup>
 
+    <ItemGroup>
+      <AvaloniaResource Include="Assets\missing.png" />
+    </ItemGroup>
+
 </Project>
diff --git a/Lightquark.NET/Objects/Channel.cs b/Lightquark.NET/Objects/Channel.cs
index 220f8b12f8106c4d360f78cade6023d21e46d877..9385ec0653a198172c333489819e4c65e012ec3c 100644
--- a/Lightquark.NET/Objects/Channel.cs
+++ b/Lightquark.NET/Objects/Channel.cs
@@ -1,4 +1,4 @@
-using Avalonia.Media.Imaging;
+using CommunityToolkit.Mvvm.ComponentModel;
 using Lightquark.NET.Util.Converters;
 using Lightquark.Types.Mongo;
 using MongoDB.Bson;
@@ -6,29 +6,63 @@ using Newtonsoft.Json;
 
 namespace Lightquark.NET.Objects;
 
-public class Channel : IChannel
+public class Channel : ObservableObject, IChannel
 {
+    private ObjectId _id;
+    private string _name;
+    private string _description;
+    private int? _index;
+    private ObjectId _quarkId;
+    private Quark[]? _virtualQuarks;
+
     [JsonProperty("_id")]
     [JsonConverter(typeof(ObjectIdConverter))]
-    public ObjectId Id { get; set; }
+    public ObjectId Id
+    {
+        get => _id;
+        set => SetProperty(ref _id, value);
+    }
 
     [JsonProperty("name")]
-    public string Name { get; set; }
-    
+    public string Name
+    {
+        get => _name;
+        set => SetProperty(ref _name, value);
+    }
+
     [JsonProperty("description")]
-    public string Description { get; set; }
-    
+    public string Description
+    {
+        get => _description;
+        set => SetProperty(ref _description, value);
+    }
+
     [JsonProperty("index")]
-    public int? Index { get; set; }
-    
+    public int? Index
+    {
+        get => _index;
+        set => SetProperty(ref _index, value);
+    }
+
     [JsonIgnore]
-    public ObjectId QuarkId { get; set; }
+    public ObjectId QuarkId
+    {
+        get => _quarkId;
+        set => SetProperty(ref _quarkId, value);
+    }
 
     // Virtuals
-    
+
     [JsonIgnore]
-    public Quark[]? VirtualQuarks { get; set; }
-    
+    public Quark[]? VirtualQuarks
+    {
+        get => _virtualQuarks;
+        set
+        {
+            if (SetProperty(ref _virtualQuarks, value)) OnPropertyChanged(nameof(Quark));
+        }
+    }
+
     [JsonProperty("quark")]
     public IQuark? Quark => VirtualQuarks?.First();
 }
\ No newline at end of file
diff --git a/Lightquark.NET/Objects/Message.cs b/Lightquark.NET/Objects/Message.cs
index 8a5a5adbe6da49af776d1bd86b35b06b62f63aa6..11f14f5fabf6ac685ff511d607400afa1d3451c2 100644
--- a/Lightquark.NET/Objects/Message.cs
+++ b/Lightquark.NET/Objects/Message.cs
@@ -1,4 +1,8 @@
-using Lightquark.NET.Util.Converters;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Lightquark.NET.Util.Converters;
 
 namespace Lightquark.NET.Objects;
 
@@ -7,33 +11,61 @@ using MongoDB.Bson;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 
-public class Message() : IMessage
+public class Message() : ObservableObject
 {
     [JsonProperty("_id")]
     [JsonConverter(typeof(ObjectIdConverter))]
-    public ObjectId Id { get; set; }
-    
+    public ObjectId Id
+    {
+        get => _id;
+        set => SetProperty(ref _id, value);
+    }
+
     [JsonIgnore]
     [JsonConverter(typeof(ObjectIdConverter))]
-    public ObjectId AuthorId { get; set; }
-    
+    public ObjectId AuthorId
+    {
+        get => _authorId;
+        set => SetProperty(ref _authorId, value);
+    }
+
     [JsonProperty("content")]
-    public string? Content { get; set; }
-    
+    public string? Content
+    {
+        get => _content;
+        set => SetProperty(ref _content, value);
+    }
+
     [JsonIgnore]
     [JsonConverter(typeof(ObjectIdConverter))]
-    public ObjectId ChannelId { get; set; }
+    public ObjectId ChannelId
+    {
+        get => _channelId;
+        set => SetProperty(ref _channelId, value);
+    }
 
 
     [JsonProperty("ua")]
-    public string UserAgent { get; set; } = "Unknown";
-    
+    public string UserAgent
+    {
+        get => _userAgent;
+        set => SetProperty(ref _userAgent, value);
+    }
+
     [JsonProperty("timestamp")]
-    public long Timestamp { get; set; }
-    
+    public long Timestamp
+    {
+        get => _timestamp;
+        set => SetProperty(ref _timestamp, value);
+    }
+
     [JsonProperty("edited")]
-    public bool Edited { get; set; }
-    
+    public bool Edited
+    {
+        get => _edited;
+        set => SetProperty(ref _edited, value);
+    }
+
     [JsonProperty("attachments")] 
     public IAttachment[] Attachments
     {
@@ -50,74 +82,161 @@ public class Message() : IMessage
     }
 
     [JsonIgnore]
-    public string[]? OldAttachments { get; set; }
-    
-    
+    public string[]? OldAttachments
+    {
+        get => _oldAttachments;
+        set
+        {
+            if (SetProperty(ref _oldAttachments, value)) OnPropertyChanged(nameof(Attachments));
+        }
+    }
+
+
     [JsonIgnore]
-    public Attachment[]? NewAttachments { get; set; }
+    public Attachment[]? NewAttachments
+    {
+        get => _newAttachments;
+        set
+        {
+            if (SetProperty(ref _newAttachments, value)) OnPropertyChanged(nameof(Attachments));
+        }
+    }
 
     [JsonProperty("specialAttributes")]
-    public JArray SpecialAttributes { get; set; } = [];
-    
+    public JArray SpecialAttributes
+    {
+        get => _specialAttributes;
+        set => SetProperty(ref _specialAttributes, value);
+    }
+
     // Virtuals and stuff
-    
+    // TODO: merge these into one this is really stupid
+
     [JsonIgnore]
-    public User[]? VirtualAuthors { get; set; }
+    public User[]? VirtualAuthors
+    {
+        get => _virtualAuthors;
+        set
+        {
+            if (SetProperty(ref _virtualAuthors, value)) OnPropertyChanged(nameof(Author));
+        }
+    }
 
     [JsonProperty("author")] 
-    public IUser? Author
+    public User? Author
     {
         get
         {
             var author = VirtualAuthors?.FirstOrDefault();
             return author?.Safe;
         }
+        set => VirtualAuthors = [value!];
+    }
+
+    public User? VisualAuthor
+    {
+        get
+        {
+            var botMessage = _specialAttributes.FirstOrDefault(a => a["type"].ToString() == "botMessage");
+            if (botMessage != null)
+            {
+                var avatarUriString = botMessage["avatarUri"]?.ToString();
+                return new User()
+                {
+                    Username = botMessage["username"]?.ToString() ?? Author!.Username,
+                    AvatarUri = avatarUriString != null ? new Uri(avatarUriString) : Author!.AvatarUri,
+                    BotTag = Author!.Username,
+                    IsBot = true
+                };
+            }
+
+            if (Author!.IsBot) Author.BotTag = "BOT";
+            return Author;
+        }
     }
 
+    private ObjectId _id;
+    private ObjectId _authorId;
+    private string? _content;
+    private ObjectId _channelId;
+    private string _userAgent = "Unknown";
+    private long _timestamp;
+    private bool _edited;
+    private string[]? _oldAttachments;
+    private Attachment[]? _newAttachments;
+    private JArray _specialAttributes = [];
+    private User[]? _virtualAuthors;
+    private MessageStatus _status = MessageStatus.Complete;
 
     [JsonIgnore]
-    public static readonly BsonDocument[] LookupPipeline =
-    [
-        new BsonDocument("$lookup", new BsonDocument
+    public MessageStatus Status
+    {
+        get => _status;
+        set
         {
-            { "from", "users" },
-            { "localField", "authorId" },
-            { "foreignField", "_id" },
-            { "as", "VirtualAuthors" }
-        })
-    ];
-
-    public Message(IMessage message) : this()
-    {
-        Id = message.Id;
-        AuthorId = message.AuthorId;
-        Content = message.Content;
-        ChannelId = message.ChannelId;
-        UserAgent = message.UserAgent;
-        Timestamp = message.Timestamp;
-        Edited = message.Edited;
-        NewAttachments = message.Attachments.Select(a => new Attachment(a)).ToArray();
-        SpecialAttributes = message.SpecialAttributes;
-        VirtualAuthors = [(User)(message.Author ?? throw new Exception("Failed to convert Author"))];
+            if (SetProperty(ref _status, value)) OnPropertyChanged(nameof(Visuals));   
+        }
     }
+
+    public MessageVisuals Visuals => new()
+    {
+        TextColor = Status switch
+        {
+            MessageStatus.Sending => new SolidColorBrush(Colors.Gray),
+            MessageStatus.Fail => new SolidColorBrush(Colors.Red),
+            MessageStatus.Complete =>  null,
+            _ => throw new ArgumentOutOfRangeException()
+        }
+    };
 }
 
-public class Attachment() : IAttachment
+public record MessageVisuals
 {
+    public IBrush? TextColor { get; set; }
+}
+
+public class Attachment() : ObservableObject, IAttachment
+{
+    private Uri _url = null!;
+    private long _size;
+    private string _mimeType = "application/octet-stream";
+    private string _filename = "Unknown";
+    private ObjectId _fileId;
+
     [JsonProperty("url")]
-    public Uri Url { get; set; } = null!;
+    public Uri Url
+    {
+        get => _url;
+        set => SetProperty(ref _url, value);
+    }
 
     [JsonProperty("size")]
-    public long Size { get; set; }
+    public long Size
+    {
+        get => _size;
+        set => SetProperty(ref _size, value);
+    }
 
     [JsonProperty("type")]
-    public string MimeType { get; set; } = "application/octet-stream";
+    public string MimeType
+    {
+        get => _mimeType;
+        set => SetProperty(ref _mimeType, value);
+    }
 
     [JsonProperty("filename")]
-    public string Filename { get; set; } = "Unknown";
-    
+    public string Filename
+    {
+        get => _filename;
+        set => SetProperty(ref _filename, value);
+    }
+
     [JsonIgnore]
-    public ObjectId FileId { get; set; }
+    public ObjectId FileId
+    {
+        get => _fileId;
+        set => SetProperty(ref _fileId, value);
+    }
 
     public Attachment(IAttachment attachment) : this()
     {
@@ -127,4 +246,11 @@ public class Attachment() : IAttachment
         Filename = attachment.Filename;
         FileId = attachment.FileId;
     }
+}
+
+public enum MessageStatus
+{
+    Sending,
+    Fail,
+    Complete
 }
\ No newline at end of file
diff --git a/Lightquark.NET/Objects/Reply/BaseReply.cs b/Lightquark.NET/Objects/Reply/BaseReply.cs
index e3835c7c5a7cc0243f568da33c22e3ffb686cd2a..d8edd9bc1ccaa0a0e326cd01fb22be8450cbf979 100644
--- a/Lightquark.NET/Objects/Reply/BaseReply.cs
+++ b/Lightquark.NET/Objects/Reply/BaseReply.cs
@@ -4,9 +4,7 @@ namespace Lightquark.NET.Objects.Reply;
 
 public record Reply<T> where T : BaseReplyResponse
 {
-    [JsonProperty("request")] 
     public required ReplyRequest Request;
-    [JsonProperty("response")] 
     public required T Response;
 }
 
@@ -14,14 +12,11 @@ public record ReplyRequest
 {
     [JsonProperty("status_code")]
     public int StatusCode;
-    [JsonProperty("success")]
     public bool Success;
-    [JsonProperty("cat")]
     public string? Cat;
 }
 
 public record BaseReplyResponse
 {
-    [JsonProperty("message")] 
     public required string Message;
 }
\ No newline at end of file
diff --git a/Lightquark.NET/Objects/User.cs b/Lightquark.NET/Objects/User.cs
index 968d7bc34e6c31d78ddd12420cab8335d3279d72..23e403a33b758e48be428def9725a31dbee95373 100644
--- a/Lightquark.NET/Objects/User.cs
+++ b/Lightquark.NET/Objects/User.cs
@@ -8,18 +8,18 @@ using Newtonsoft.Json;
 
 namespace Lightquark.NET.Objects;
 
-public class User : ObservableObject, IUser 
+public class User : ObservableObject 
 {
     private Bitmap _avatar;
     private IStatus? _status;
     private string? _pronouns;
     private ObjectId _id;
-    private string _email;
+    private string? _email;
     private string _username;
     private bool _admin;
     private bool _isBot;
     private bool _secretThirdThing;
-    private Uri _avatarUriGetter;
+    private Uri _avatarUri;
 
     [JsonProperty("_id")]
     [JsonConverter(typeof(ObjectIdConverter))]
@@ -29,19 +29,13 @@ public class User : ObservableObject, IUser
         set => SetProperty(ref _id, value);
     }
 
-    [JsonIgnore]
-    public required byte[] PasswordHash { get; init; }
-
     [JsonProperty("email")]
-    public required string Email
+    public string? Email
     {
         get => _email;
         [MemberNotNull(nameof(_email))] set => SetProperty(ref _email, value);
     }
-
-    [JsonIgnore]
-    public required byte[] Salt { get; init; }
-
+    
     [JsonProperty("username")]
     public required string Username
     {
@@ -63,6 +57,8 @@ public class User : ObservableObject, IUser
         set => SetProperty(ref _isBot, value);
     }
 
+    public string? BotTag { get; set; } = "";
+    
     [JsonProperty("secretThirdThing")]
     public bool SecretThirdThing
     {
@@ -92,16 +88,12 @@ public class User : ObservableObject, IUser
 
     // Calculated field
     [JsonProperty("avatarUri")]
-    public Uri AvatarUriGetter
+    public Uri AvatarUri
     {
-        get => _avatarUriGetter;
-        set => SetProperty(ref _avatarUriGetter, value);
+        get => _avatarUri;
+        set => SetProperty(ref _avatarUri, value);
     }
 
-    [JsonIgnore]
-    [JsonConverter(typeof(ObjectIdConverter))]
-    public ObjectId? AvatarFileId { get; set; }
-    
-    [JsonIgnore] public IUser Safe => this;
+    [JsonIgnore] public User Safe => this;
 
 }
\ No newline at end of file