From 94bce6cc1d862400c2b89c4e570415dc17ee529f Mon Sep 17 00:00:00 2001
From: Emilia <emilia@jumpsca.re>
Date: Tue, 18 Mar 2025 20:56:49 +0200
Subject: [PATCH] Horrors beyond my comprehension

---
 Blacklight.sln.DotSettings.user               |  4 +
 Blacklight/App.axaml                          |  2 +
 Blacklight/Blacklight.csproj                  |  6 ++
 Blacklight/ViewModels/ClientViewModel.cs      |  1 +
 .../Dialogs/CreateChannelViewModel.cs         | 77 +++++++++++++++++
 Blacklight/ViewModels/DockFactory.cs          | 16 ++++
 .../Tools/ResourceExplorerViewModel.cs        | 72 ++++++++++++++--
 .../Windows/CreateChannelViewModel.cs         |  6 --
 .../Views/Dialogs/CreateChannelView.axaml     | 65 ++++++++++++++
 .../CreateChannelView.axaml.cs                |  4 +-
 Blacklight/Views/MainWindow.axaml             | 24 ++++--
 .../Views/Tools/ResourceExplorerView.axaml    | 18 +++-
 .../Views/Windows/CreateChannelView.axaml     | 23 -----
 Lightquark.NET/Client.cs                      |  2 +-
 Lightquark.NET/ClientMethods/Channel.cs       | 86 +++++++++++++++++--
 Lightquark.NET/ClientMethods/Gateway.cs       | 22 +++++
 Lightquark.NET/ClientMethods/Quark.cs         | 26 ++++--
 Lightquark.NET/Objects/Channel.cs             | 16 ++--
 Lightquark.NET/Objects/Quark.cs               | 12 ++-
 Lightquark.NET/Objects/QuarkListItem.cs       | 47 ++++++++--
 .../Converters/ChannelCreationConverter.cs    | 13 ---
 21 files changed, 445 insertions(+), 97 deletions(-)
 create mode 100644 Blacklight/ViewModels/Dialogs/CreateChannelViewModel.cs
 delete mode 100644 Blacklight/ViewModels/Windows/CreateChannelViewModel.cs
 create mode 100644 Blacklight/Views/Dialogs/CreateChannelView.axaml
 rename Blacklight/Views/{Windows => Dialogs}/CreateChannelView.axaml.cs (62%)
 delete mode 100644 Blacklight/Views/Windows/CreateChannelView.axaml
 delete mode 100644 Lightquark.NET/Util/Converters/ChannelCreationConverter.cs

diff --git a/Blacklight.sln.DotSettings.user b/Blacklight.sln.DotSettings.user
index 8bece1a..a4c636f 100644
--- a/Blacklight.sln.DotSettings.user
+++ b/Blacklight.sln.DotSettings.user
@@ -16,5 +16,9 @@
 	<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>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProportionalDock_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F91c655b1795150937d9264ac1431c463ed5e5e8ad0286ad17e16f16175d94_003FProportionalDock_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2854ce6d56c18d0d837d3a3ef9c4f2c7c77691fa3528c8394986ac7ce7719_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6354a7b35d7821629924d3676acd7e67a6f7f94343e0e66ec439aa2bd6ed5_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeDataTemplate_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe9a5203b1cebd5ba4f745e6a4b3dd651886fe711fe1ad32dec118143c4fbd0_003FTreeDataTemplate_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeDataTemplate_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe9a5203b1cebd5ba4f745e6a4b3dd651886fe711fe1ad32dec118143c4fbd0_003FTreeDataTemplate_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeView_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F49b572a64c2c02980ffe97ff1673549ed9bdfbf9738ec66def83611762e85c_003FTreeView_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	</wpf:ResourceDictionary>
\ No newline at end of file
diff --git a/Blacklight/App.axaml b/Blacklight/App.axaml
index 2cf90ee..d91b9e4 100644
--- a/Blacklight/App.axaml
+++ b/Blacklight/App.axaml
@@ -7,6 +7,7 @@
 	xmlns:core="clr-namespace:Dock.Model.Core;assembly=Dock.Model"
 	xmlns:documents="clr-namespace:Blacklight.ViewModels.Documents"
 	xmlns:util="clr-namespace:Blacklight.Util"
+	xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
 	RequestedThemeVariant="Default">
 	<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
 	<Application.Resources>
@@ -61,6 +62,7 @@
 	<Application.Styles>
 		<FluentTheme />
 		<DockFluentTheme />
+		<dialogHostAvalonia:DialogHostStyles />
 		<Style Selector="DockControl">
 			<Setter Property="(ControlRecyclingDataTemplate.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />
 		</Style>
diff --git a/Blacklight/Blacklight.csproj b/Blacklight/Blacklight.csproj
index a0cadd8..d7c3af5 100644
--- a/Blacklight/Blacklight.csproj
+++ b/Blacklight/Blacklight.csproj
@@ -22,6 +22,7 @@
         <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
         <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.2.0.9" />
         <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+        <PackageReference Include="DialogHost.Avalonia" Version="0.9.2" />
         <PackageReference Include="Dock.Avalonia" Version="11.2.0" />
         <PackageReference Include="Dock.Model.Mvvm" Version="11.2.0.1" />
         <PackageReference Include="Markdig" Version="0.40.0" />
@@ -45,4 +46,9 @@
     <ItemGroup>
       <ProjectReference Include="..\Lightquark.NET\Lightquark.NET.csproj" />
     </ItemGroup>
+
+
+    <ItemGroup>
+      <UpToDateCheckInput Remove="Views\Windows\CreateChannelView.axaml" />
+    </ItemGroup>
 </Project>
diff --git a/Blacklight/ViewModels/ClientViewModel.cs b/Blacklight/ViewModels/ClientViewModel.cs
index d852c10..b1d4d22 100644
--- a/Blacklight/ViewModels/ClientViewModel.cs
+++ b/Blacklight/ViewModels/ClientViewModel.cs
@@ -62,6 +62,7 @@ public class ClientViewModel : ViewModelBase
     public void TestCommand()
     {
         Log.Information("Current items in QuarkList: {ItemCount}", Client.QuarkList.Count);
+        _ = Client.UpdateResourceTree();
     }
     
     public void LogoutNow()
diff --git a/Blacklight/ViewModels/Dialogs/CreateChannelViewModel.cs b/Blacklight/ViewModels/Dialogs/CreateChannelViewModel.cs
new file mode 100644
index 0000000..b181e04
--- /dev/null
+++ b/Blacklight/ViewModels/Dialogs/CreateChannelViewModel.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.Input;
+using DialogHostAvalonia;
+using Lightquark.NET;
+using Lightquark.NET.Objects;
+using Lightquark.NET.Objects.Reply;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
+
+namespace Blacklight.ViewModels.Dialogs;
+
+public class CreateChannelViewModel : ViewModelBase
+{
+    private Quark _meow;
+    private string _errorMessage = "";
+    private string? _channelName;
+    private string? _channelDescription;
+    public ICommand CreateChannelCommand => new RelayCommand(CreateChannel);
+
+    private async void CreateChannel()
+    {
+        try
+        {
+            if (ChannelName == null)
+            {
+                ErrorMessage = "Name is required";
+                return;
+            }
+            if (ChannelName.Length is < 1 or > 64)
+            {
+                ErrorMessage = "Name should be between 1 and 64 characters";
+                return;
+            }
+
+            var client = App.Services!.GetRequiredService<Client>();
+            var res = await client.CreateChannel(Quark.Id, ChannelName, ChannelDescription);
+            if (res.Request.Success)
+            {
+                DialogHost.GetDialogSession("DialogHost")?.Close(false);
+                return;
+            }
+
+            ErrorMessage = res.Response.Message;
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "Failed channel creation");
+            ErrorMessage = "Failed to create channel.";
+        }
+    }
+
+    public Quark Quark
+    {
+        get => _meow;
+        set => SetProperty(ref _meow, value);
+    }
+
+    public string ErrorMessage
+    {
+        get => _errorMessage;
+        set => SetProperty(ref _errorMessage, value);
+    }
+
+    public string? ChannelName
+    {
+        get => _channelName;
+        set => SetProperty(ref _channelName, value);
+    }
+
+    public string? ChannelDescription
+    {
+        get => _channelDescription;
+        set => SetProperty(ref _channelDescription, value);
+    }
+}
+
diff --git a/Blacklight/ViewModels/DockFactory.cs b/Blacklight/ViewModels/DockFactory.cs
index 414f312..ec3a311 100644
--- a/Blacklight/ViewModels/DockFactory.cs
+++ b/Blacklight/ViewModels/DockFactory.cs
@@ -11,6 +11,7 @@ using Dock.Model.Core;
 using Dock.Model.Mvvm;
 using Dock.Model.Mvvm.Controls;
 using Lightquark.NET.Objects;
+using MongoDB.Bson;
 
 namespace Blacklight.ViewModels;
 
@@ -166,6 +167,21 @@ public class DockContext
         parent.ActiveDockable = document;
     }
 
+    // TODO
+    // public void RemoveDocument(ObjectId channelId)
+    // {
+    //     var docks = GetDocumentDocks(_mainLayoutDock);
+    //     foreach (var dockWithParent in docks)
+    //     {
+    //         if (dockWithParent.DocumentDock.VisibleDockables == null) throw new Exception("wtf? _documentDock.VisibleDockables is null");
+    //         var toRemove = dockWithParent.DocumentDock.VisibleDockables.OfType<DocumentViewModel>().Where(d => d.Channel.Id == channelId);
+    //         foreach (var removable in toRemove)
+    //         {
+    //             dockWithParent.DocumentDock.VisibleDockables.Remove(removable);
+    //         }
+    //     }
+    // }
+    
     public void RemoveDocument(DocumentViewModel document)
     {
         var docks = GetDocumentDocks(_mainLayoutDock);
diff --git a/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs b/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs
index 59890ce..1199844 100644
--- a/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs
+++ b/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs
@@ -1,5 +1,13 @@
 using System;
+using System.Diagnostics;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Blacklight.ViewModels.Dialogs;
+using DialogHostAvalonia;
 using Dock.Model.Mvvm.Controls;
+using Lightquark.NET;
+using Microsoft.Extensions.DependencyInjection;
 using MongoDB.Bson;
 using Serilog;
 
@@ -15,14 +23,18 @@ public class ResourceExplorerViewModel : Tool
             return;
         }
         Log.Information("Cats go meow {QuarkId}", quarkId);
-        
+        var client = App.Services!.GetRequiredService<Client>();
+        DialogHost.Show(new CreateChannelViewModel
+        {
+            Quark = client.Quarks.First(q => q.Id == quarkId)
+        });
     }
 
     
     // ReSharper disable once UnusedMember.Global
     public bool CanCreateChannel(object obj)
     {
-        return obj is ObjectId;
+        return obj is ObjectId; // TODO: Check permission
     } 
     
     public void LeaveQuark(object obj)
@@ -32,7 +44,7 @@ public class ResourceExplorerViewModel : Tool
     // ReSharper disable once UnusedMember.Global
     public bool CanLeaveQuark(object obj)
     {
-        return true;
+        return false;
     } 
     public void EditQuark(object obj)
     {
@@ -41,15 +53,63 @@ public class ResourceExplorerViewModel : Tool
     // ReSharper disable once UnusedMember.Global
     public bool CanEditQuark(object obj)
     {
-        return true;
+        return false;
     } 
     public void DeleteQuark(object obj)
     {
         throw new NotImplementedException();
     }
     // ReSharper disable once UnusedMember.Global
-    public bool CanDeletQuark(object obj)
+    public bool CanDeleteQuark(object obj)
+    {
+        return false;
+    } 
+    public void EditChannel(object obj)
+    {
+        throw new NotImplementedException();
+    }
+    // ReSharper disable once UnusedMember.Global
+    public bool CanEditChannel(object obj)
+    {
+        return false;
+    } 
+    public async void DeleteChannel(object obj)
+    {
+        try
+        {
+            if (obj is not ObjectId channelId)
+            {
+                Log.Information("obj is {Obj}", obj.GetType().FullName);
+                return;
+            }
+            var client = App.Services!.GetRequiredService<Client>();
+            var res = await client.DeleteChannel(channelId);
+            if (res.Request.Success) return;
+            
+            // App.DockContext.RemoveDocument(channelId); TODO
+            
+            await DialogHost.Show(new TextBlock
+            {
+                Text = $"Failed to delete channel: {res.Response.Message}",
+                Foreground = Brushes.Red,
+                FontWeight = FontWeight.Bold
+            });
+        }
+        catch (Exception ex)
+        {
+            Log.Error(ex, "Failed to delete channel");
+            
+            await DialogHost.Show(new TextBlock
+            {
+                Text = "Failed to delete channel",
+                Foreground = Brushes.Red,
+                FontWeight = FontWeight.Bold
+            });
+        }
+    }
+    // ReSharper disable once UnusedMember.Global
+    public bool CanDeleteChannel(object obj)
     {
-        return true;
+        return obj is ObjectId; // TODO: Check permission
     } 
 }
\ No newline at end of file
diff --git a/Blacklight/ViewModels/Windows/CreateChannelViewModel.cs b/Blacklight/ViewModels/Windows/CreateChannelViewModel.cs
deleted file mode 100644
index 436893a..0000000
--- a/Blacklight/ViewModels/Windows/CreateChannelViewModel.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Blacklight.ViewModels.Windows;
-
-public class CreateChannelViewModel : ViewModelBase
-{
-    
-}
\ No newline at end of file
diff --git a/Blacklight/Views/Dialogs/CreateChannelView.axaml b/Blacklight/Views/Dialogs/CreateChannelView.axaml
new file mode 100644
index 0000000..d886551
--- /dev/null
+++ b/Blacklight/Views/Dialogs/CreateChannelView.axaml
@@ -0,0 +1,65 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:dialogs="clr-namespace:Blacklight.ViewModels.Dialogs"
+             xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="Blacklight.Views.Dialogs.CreateChannelView"
+             x:DataType="dialogs:CreateChannelViewModel">
+	<Grid>
+		<Grid.RowDefinitions>
+			*,*,*,*,*
+		</Grid.RowDefinitions>
+		<Grid.ColumnDefinitions>
+			Auto,*,Auto
+		</Grid.ColumnDefinitions>
+		
+		<TextBlock Grid.Row="0" 
+		           Grid.Column="0" 
+		           Grid.ColumnSpan="3" 
+		           FontWeight="Bold" HorizontalAlignment="Center" Text="{Binding Quark.Name, StringFormat=Create channel in {0}}" />
+		
+		<Grid Margin="0,4" Grid.Row="1" 
+		      Grid.Column="0" 
+		      Grid.ColumnSpan="3">
+			<Grid.ColumnDefinitions>
+				Auto,*,Auto
+			</Grid.ColumnDefinitions>
+			<TextBlock VerticalAlignment="Center" Grid.Column="0" Margin="0, 0, 10, 0">Name:</TextBlock>
+			<TextBox MinWidth="196" MaxLength="64" Grid.Column="2" Watermark="Name" Text="{Binding ChannelName}"/>
+		</Grid>
+		
+		<Grid Margin="0,4" Grid.Row="2" 
+		      Grid.Column="0" 
+		      Grid.ColumnSpan="3">
+			<Grid.ColumnDefinitions>
+				Auto,*,Auto
+			</Grid.ColumnDefinitions>
+			<TextBlock VerticalAlignment="Center" Grid.Column="0" Margin="0, 0, 10, 0">Description:</TextBlock>
+			<TextBox MinWidth="196" MaxLength="512" Grid.Column="2" Watermark="Description (optional)" Text="{Binding ChannelDescription}"/>
+		</Grid>
+		
+		<TextBlock MaxWidth="256"
+		           Grid.Row="3"
+		           Grid.Column="0"
+		           Foreground="Red"
+		           FontWeight="Bold"
+		           TextWrapping="Wrap"
+		           HorizontalAlignment="Center"
+		           Grid.ColumnSpan="3"
+		           Text="{Binding ErrorMessage}"/>
+		
+		<Grid Margin="0,4" Grid.Row="4" 
+		      Grid.Column="0" 
+		      Grid.ColumnSpan="3"
+		      HorizontalAlignment="Stretch">
+			<Grid.ColumnDefinitions>
+				Auto,*,Auto
+			</Grid.ColumnDefinitions>
+			<Button Grid.Column="0" Command="{Binding CreateChannelCommand}">Create</Button>
+			<Button Grid.Column="2"
+			        Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHostAvalonia:DialogHost}, Path=CloseDialogCommand}">Cancel</Button>
+		</Grid>
+	</Grid>
+</UserControl>
diff --git a/Blacklight/Views/Windows/CreateChannelView.axaml.cs b/Blacklight/Views/Dialogs/CreateChannelView.axaml.cs
similarity index 62%
rename from Blacklight/Views/Windows/CreateChannelView.axaml.cs
rename to Blacklight/Views/Dialogs/CreateChannelView.axaml.cs
index f24ee56..d8fe34c 100644
--- a/Blacklight/Views/Windows/CreateChannelView.axaml.cs
+++ b/Blacklight/Views/Dialogs/CreateChannelView.axaml.cs
@@ -2,9 +2,9 @@
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 
-namespace Blacklight.Views.Windows;
+namespace Blacklight.Views.Dialogs;
 
-public partial class CreateChannelView : Window
+public partial class CreateChannelView : UserControl
 {
     public CreateChannelView()
     {
diff --git a/Blacklight/Views/MainWindow.axaml b/Blacklight/Views/MainWindow.axaml
index 6b12f56..894fb54 100644
--- a/Blacklight/Views/MainWindow.axaml
+++ b/Blacklight/Views/MainWindow.axaml
@@ -6,7 +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"
+        xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
         mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="700"
         x:Class="Blacklight.Views.MainWindow"
         x:DataType="vm:MainWindowViewModel"
@@ -34,18 +34,24 @@
 			<views:ClientView />
 		</DataTemplate>
 	</Window.DataTemplates>
-
-	<Panel>
+	
+	<dialogHostAvalonia:DialogHost 
+		CloseOnClickAway="True"
+		BlurBackground="True"
+		BlurBackgroundRadius="5"
+		Identifier="DialogHost">
 		<Panel>
 			<Panel>
-				<TransitioningContentControl Content="{Binding CurrentViewModel}">
-					<TransitioningContentControl.PageTransition>
-						<CrossFade Duration="0.500" />
-					</TransitioningContentControl.PageTransition>
-				</TransitioningContentControl>
+				<Panel>
+					<TransitioningContentControl Content="{Binding CurrentViewModel}">
+						<TransitioningContentControl.PageTransition>
+							<CrossFade Duration="0.500" />
+						</TransitioningContentControl.PageTransition>
+					</TransitioningContentControl>
+				</Panel>
 			</Panel>
 		</Panel>
-	</Panel>
+	</dialogHostAvalonia:DialogHost>
 
 
 </Window>
\ No newline at end of file
diff --git a/Blacklight/Views/Tools/ResourceExplorerView.axaml b/Blacklight/Views/Tools/ResourceExplorerView.axaml
index 45bd4d8..d35dfa4 100644
--- a/Blacklight/Views/Tools/ResourceExplorerView.axaml
+++ b/Blacklight/Views/Tools/ResourceExplorerView.axaml
@@ -11,7 +11,12 @@
              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}" SelectionChanged="ItemSelected">
-	    <TreeView.DataTemplates>
+			<TreeView.Styles>
+				<Style Selector="TreeViewItem" x:DataType="objects:QuarkListItem">
+					<Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
+				</Style>
+			</TreeView.Styles>
+			<TreeView.DataTemplates>
 		    <util:ResourceExplorerTemplateSelector>
 			    <TreeDataTemplate x:Key="QuarkTemplate" x:DataType="objects:QuarkListItem" ItemsSource="{Binding Children}">
 				    <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
@@ -43,6 +48,17 @@
 				    <StackPanel Orientation="Horizontal">
 					    <TextBlock FontWeight="Bold" Foreground="{DynamicResource ChannelHashtagColor}" Margin="0,0,2,0">#</TextBlock>
 					    <TextBlock Text="{Binding Channel.Name, FallbackValue='No Channel Information'}"></TextBlock>
+					    					    
+					    <StackPanel.ContextMenu>
+						    <ContextMenu>
+							    <MenuItem Header="Edit channel"
+							              Command="{Binding $parent[TreeView].((tools:ResourceExplorerViewModel)DataContext).EditChannel}"
+							              CommandParameter="{Binding Channel.Id}"/>
+							    <MenuItem Header="Delete channel"
+							              Command="{Binding $parent[TreeView].((tools:ResourceExplorerViewModel)DataContext).DeleteChannel}"
+							              CommandParameter="{Binding Channel.Id}"/>
+						    </ContextMenu>
+					    </StackPanel.ContextMenu>
 				    </StackPanel>
 			    </TreeDataTemplate>
 			    <TreeDataTemplate x:Key="FolderTemplate" x:DataType="objects:QuarkListItem" ItemsSource="{Binding Children}">
diff --git a/Blacklight/Views/Windows/CreateChannelView.axaml b/Blacklight/Views/Windows/CreateChannelView.axaml
deleted file mode 100644
index 91d5363..0000000
--- a/Blacklight/Views/Windows/CreateChannelView.axaml
+++ /dev/null
@@ -1,23 +0,0 @@
-<Window xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
-        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
-        x:Class="Blacklight.Views.Windows.CreateChannelView"
-        WindowStartupLocation="CenterScreen"
-        Title="CreateChannelView">
-	<Panel>
-		<ExperimentalAcrylicBorder IsHitTestVisible="False">
-			<ExperimentalAcrylicBorder.Material>
-				<ExperimentalAcrylicMaterial
-					BackgroundSource="Digger"
-					TintColor="Black"
-					TintOpacity="1"
-					MaterialOpacity="0.65" />
-			</ExperimentalAcrylicBorder.Material>
-		</ExperimentalAcrylicBorder>
-		<TextBlock>
-			Welcome to Avalonia!
-		</TextBlock>
-	</Panel>
-</Window>
\ No newline at end of file
diff --git a/Lightquark.NET/Client.cs b/Lightquark.NET/Client.cs
index 8699091..01bb254 100644
--- a/Lightquark.NET/Client.cs
+++ b/Lightquark.NET/Client.cs
@@ -73,7 +73,7 @@ public partial class Client : ObservableObject
         {
             Formatting = Formatting.Indented,
             NullValueHandling = NullValueHandling.Ignore,
-            Converters = new List<JsonConverter> { new AttachmentCreationConverter(), new StatusCreationConverter(), new ChannelCreationConverter() },
+            Converters = new List<JsonConverter> { new AttachmentCreationConverter(), new StatusCreationConverter() },
             ContractResolver = new CamelCasePropertyNamesContractResolver()
         };
         var mapperConfig = new MapperConfiguration(cfg =>
diff --git a/Lightquark.NET/ClientMethods/Channel.cs b/Lightquark.NET/ClientMethods/Channel.cs
index 25ab170..9268f2c 100644
--- a/Lightquark.NET/ClientMethods/Channel.cs
+++ b/Lightquark.NET/ClientMethods/Channel.cs
@@ -1,21 +1,89 @@
-using Lightquark.NET.Objects;
+using Avalonia.Threading;
+using Lightquark.NET.Objects;
+using Lightquark.NET.Objects.Reply;
+using MongoDB.Bson;
+using Serilog;
 
 namespace Lightquark.NET;
 
 public partial class Client
 {
-    private void AddOrUpdateChannel(Channel channel)
+    private async void AddOrUpdateChannel(Channel channel)
     {
-        var existingChannel = Channels.FirstOrDefault(q => q.Id == channel.Id);
-        if (existingChannel != null)
+        try
         {
-            // Update
-            var index = Channels.IndexOf(existingChannel);
-            Channels[index] = _mapper.Map(channel, existingChannel);
+            var existingChannel = Channels.FirstOrDefault(q => q.Id == channel.Id);
+            if (existingChannel != null)
+            {
+                // Update
+                var index = Channels.IndexOf(existingChannel);
+                channel = _mapper.Map(channel, existingChannel);
+                Channels[index] = channel;
+            }
+            else
+            {
+                Channels.Add(channel);
+            }
+
+            var parentQuark = Quarks.FirstOrDefault(q => q.Id == channel.QuarkId);
+            if (parentQuark != null)
+            {
+                Log.Information("Channel {ChannelId} ({ChannelName}) has parent quark found in cache: {QuarkId} ({QuarkName})", 
+                    channel.Id, 
+                    channel.Name, 
+                    parentQuark.Id, 
+                    parentQuark.Name);
+                var c = parentQuark.Channels.ToList();
+                c.Add(channel);
+                parentQuark.Channels = c.ToArray();
+            }
+            else
+            {
+                Log.Warning("Channel {ChannelId} ({ChannelName}) has no parent quark in cache: {QuarkId}", channel.Id, channel.Name, channel.QuarkId);
+            }
         }
-        else
+        catch (Exception ex)
         {
-            Channels.Add(channel);
+            Log.Error(ex, "Failed to AddOrUpdateChannel");
         }
     }
+
+    private void RemoveChannel(ObjectId channelId)
+    {
+        var existingChannel = Channels.FirstOrDefault(c => c.Id == channelId);
+        if (existingChannel == null) return;
+        Channels.Remove(existingChannel);
+        var parentQuark = Quarks.FirstOrDefault(q => q.Channels.Contains(existingChannel));
+        if (parentQuark == null) return;
+        var channels = parentQuark.Channels.ToList();
+        parentQuark.Channels = channels.Where(c => c.Id != channelId).ToArray();
+    }
+
+    public async Task<Reply<ChannelCreateReplyResponse>> CreateChannel(ObjectId quarkId, string name, string? description)
+    {
+        var res = await CallRpc<ChannelCreateReplyResponse>("POST", $"{Version}/channel", new
+        {
+            quark = quarkId.ToString(),
+            name,
+            description
+        });
+
+        return res;
+    }
+
+    public async Task<Reply<ChannelDeleteReplyResponse>> DeleteChannel(ObjectId channelId)
+    {
+        var res = await CallRpc<ChannelDeleteReplyResponse>("DELETE", $"{Version}/channel/{channelId}", null);
+        return res;
+    }
+}
+
+public record ChannelCreateReplyResponse : BaseReplyResponse
+{
+    
+}
+
+public record ChannelDeleteReplyResponse : BaseReplyResponse
+{
+    
 }
\ No newline at end of file
diff --git a/Lightquark.NET/ClientMethods/Gateway.cs b/Lightquark.NET/ClientMethods/Gateway.cs
index a3e95de..8125502 100644
--- a/Lightquark.NET/ClientMethods/Gateway.cs
+++ b/Lightquark.NET/ClientMethods/Gateway.cs
@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using Avalonia.Threading;
 using Lightquark.NET.Objects;
 using Lightquark.NET.Objects.Reply;
 using Lightquark.NET.Util;
@@ -120,6 +121,19 @@ public partial class Client
                         var selectionMessage = ParseGateway<SelectionGatewayMessage>(msg.Text!);
                         GatekeeperSelection(selectionMessage);
                         break;
+                    case "channelCreate":
+                    case "channelUpdate":
+                        var channelCreateMessage = ParseGateway<ChannelCreateGatewayMessage>(msg.Text!);
+                        channelCreateMessage.Channel.QuarkId = channelCreateMessage.Quark.Id;
+                        channelCreateMessage.Channel.Quark = null;
+                        AddOrUpdateChannel(channelCreateMessage.Channel);
+                        _ = Dispatcher.UIThread.Invoke(UpdateResourceTree);
+                        break;
+                    case "channelDelete":
+                        var channelDeleteMessage = ParseGateway<ChannelCreateGatewayMessage>(msg.Text!);
+                        RemoveChannel(channelDeleteMessage.Channel.Id);
+                        _ = Dispatcher.UIThread.Invoke(UpdateResourceTree);
+                        break;
                 }
             }
             catch (Exception ex)
@@ -244,6 +258,14 @@ public record MessageCreateGatewayMessage : BaseGatewayMessage
     public required ObjectId ChannelId { get; init; }
 }
 
+public record ChannelCreateGatewayMessage : BaseGatewayMessage
+{
+    [JsonProperty("channel")]
+    public required Channel Channel { get; init; }
+    [JsonProperty("quark")]
+    public required Quark Quark { get; init; }
+}
+
 public record GatewayBusEvent
 {
     public required BaseGatewayMessage BaseMessage { get; init; }
diff --git a/Lightquark.NET/ClientMethods/Quark.cs b/Lightquark.NET/ClientMethods/Quark.cs
index c834c38..5722be5 100644
--- a/Lightquark.NET/ClientMethods/Quark.cs
+++ b/Lightquark.NET/ClientMethods/Quark.cs
@@ -1,5 +1,7 @@
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using Avalonia.Media.Imaging;
+using Avalonia.Threading;
 using Lightquark.NET.Assets;
 using Lightquark.NET.Objects;
 using Lightquark.NET.Objects.Reply;
@@ -104,7 +106,7 @@ public partial class Client
         }
         
         await SaveQuarkList();
-        await UpdateResourceTree();
+        _ = Dispatcher.UIThread.Invoke(UpdateResourceTree);
     }
     
     private Func<QuarkListItem, bool> IdFinder(ObjectId id)
@@ -149,8 +151,13 @@ public partial class Client
         });
     }
 
-    private async Task UpdateResourceTree()
+    private int _g;
+    private readonly SemaphoreSlim _g2 = new(1); 
+    public async Task UpdateResourceTree()
     {
+        Log.Information("[{G}] UpdateResourceTree called", _g++);
+        await _g2.WaitAsync();
+        Log.Information("[{G}] UpdateResourceTree obtained semaphore", _g);
         var quarkList = QuarkList.ToList();
         foreach (var quarkListItem in quarkList.ToList())
         {
@@ -174,6 +181,7 @@ public partial class Client
                 var populatedQuark = await PopulateQuarkListQuark(quarkListItem);
                 if (populatedQuark != null)
                 {
+                    populatedQuark.IsExpanded = quarkListItem.IsExpanded;
                     quarkList[index] = populatedQuark;
                 }
             }
@@ -185,19 +193,22 @@ public partial class Client
         {
             ResourceTree.Add(quarkListItem);
         }
+
+        _g2.Release();
+        Log.Information("[{G}] UpdateResourceTree ends, {A} items", _g, ResourceTree.Count);
     }
 
     private async Task<QuarkListItem?> PopulateQuarkListQuark(QuarkListItem quarkListItem)
     {
         if (quarkListItem.Type != QuarkListItemType.Quark)
             throw new Exception("Quark list parse failure? Non-quark passed to PopulateQuarkListQuark");
-        Log.Information("Quark {@Quark}", quarkListItem);
+        // Log.Information("Quark {@Quark}", quarkListItem);
         if (quarkListItem.QuarkId == null || quarkListItem.QuarkId == ObjectId.Empty)
             throw new Exception("Quark list parse failure? Empty or null quark id");
         var quark = Quarks.FirstOrDefault(q => q.Id == quarkListItem.QuarkId);
         if (quark == null)
         {
-            Log.Information("Missing Quark in cache, getting it from API");
+            // Log.Information("Missing Quark in cache, getting it from API");
             var rpcQuark = await CallRpc<QuarkReplyResponse>("GET", $"/{Version}/quark/{quarkListItem.QuarkId}", null);
             if (!rpcQuark.Request.Success) return quarkListItem;
             AddOrUpdateQuark(rpcQuark.Response.Quark!);
@@ -206,11 +217,12 @@ public partial class Client
 
         quarkListItem.Quark = quark;
         quarkListItem.Children = [];
-        Log.Information("Quark has {Count} channels", quark.Channels?.Length);
+        // Log.Information("Quark has {Count} channels", quark.Channels?.Length);
         foreach (var channel in quark.Channels ?? [])
         {
-            Log.Information("Channel {Ch}", channel);
-            AddOrUpdateChannel((Channel)channel);
+            // Log.Information("Channel {Ch}", channel);
+            channel.QuarkId = quark.Id;
+            AddOrUpdateChannel(channel);
             var cacheChannel = Channels.FirstOrDefault(c => c.Id == channel.Id);
             quarkListItem.Children.Add(new QuarkListItem
             {
diff --git a/Lightquark.NET/Objects/Channel.cs b/Lightquark.NET/Objects/Channel.cs
index 9385ec0..663a1cf 100644
--- a/Lightquark.NET/Objects/Channel.cs
+++ b/Lightquark.NET/Objects/Channel.cs
@@ -6,7 +6,7 @@ using Newtonsoft.Json;
 
 namespace Lightquark.NET.Objects;
 
-public class Channel : ObservableObject, IChannel
+public class Channel : ObservableObject
 {
     private ObjectId _id;
     private string _name;
@@ -14,6 +14,7 @@ public class Channel : ObservableObject, IChannel
     private int? _index;
     private ObjectId _quarkId;
     private Quark[]? _virtualQuarks;
+    private Quark? _quark;
 
     [JsonProperty("_id")]
     [JsonConverter(typeof(ObjectIdConverter))]
@@ -51,18 +52,15 @@ public class Channel : ObservableObject, IChannel
         set => SetProperty(ref _quarkId, value);
     }
 
-    // Virtuals
 
-    [JsonIgnore]
-    public Quark[]? VirtualQuarks
+    [JsonProperty("quark")]
+    public Quark? Quark
     {
-        get => _virtualQuarks;
+        get => _quark;
         set
         {
-            if (SetProperty(ref _virtualQuarks, value)) OnPropertyChanged(nameof(Quark));
+            SetProperty(ref _quark, value);
+            if (value != null) QuarkId = value.Id;
         }
     }
-
-    [JsonProperty("quark")]
-    public IQuark? Quark => VirtualQuarks?.First();
 }
\ No newline at end of file
diff --git a/Lightquark.NET/Objects/Quark.cs b/Lightquark.NET/Objects/Quark.cs
index 451574c..1e6b137 100644
--- a/Lightquark.NET/Objects/Quark.cs
+++ b/Lightquark.NET/Objects/Quark.cs
@@ -7,7 +7,7 @@ using Newtonsoft.Json;
 
 namespace Lightquark.NET.Objects;
 
-public class Quark : ObservableObject, IQuark
+public class Quark : ObservableObject
 {
     [JsonIgnore]
     private Bitmap? _icon;
@@ -15,6 +15,8 @@ public class Quark : ObservableObject, IQuark
     [JsonIgnore]
     public string? FetchedIcon = null;
 
+    private Channel[] _channels;
+
     [JsonIgnore]
     public Bitmap? Icon
     {
@@ -41,8 +43,12 @@ public class Quark : ObservableObject, IQuark
     public required ObjectId[] Owners { get; set; }
 
     [JsonProperty("channels")]
-    public IChannel[] Channels { get; set; }
-    
+    public Channel[] Channels
+    {
+        get => _channels;
+        set => _channels = value.DistinctBy(c => c.Id).ToArray();
+    }
+
     [JsonProperty("inviteEnabled")]
     public bool InviteEnabled { get; set; } = true;
 
diff --git a/Lightquark.NET/Objects/QuarkListItem.cs b/Lightquark.NET/Objects/QuarkListItem.cs
index 5608dc4..46c691b 100644
--- a/Lightquark.NET/Objects/QuarkListItem.cs
+++ b/Lightquark.NET/Objects/QuarkListItem.cs
@@ -1,13 +1,21 @@
 using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
 using Lightquark.NET.Util.Converters;
 using Lightquark.Types.Mongo;
 using MongoDB.Bson;
 using Newtonsoft.Json;
+using Serilog;
 
 namespace Lightquark.NET.Objects;
 
-public class QuarkListItem
+public class QuarkListItem : ObservableObject
 {
+    private bool _isExpanded;
+    private string? _folderName;
+    private ObservableCollection<QuarkListItem>? _children;
+    private Channel? _channel;
+    private Quark? _quark;
+
     [JsonProperty("type")]
     [JsonConverter(typeof(LowerCaseStringEnumConverter))]
     public required QuarkListItemType Type { get; init; }
@@ -15,18 +23,41 @@ public class QuarkListItem
     [JsonProperty("quarkId")]
     [JsonConverter(typeof(ObjectIdConverter))]
     public ObjectId? QuarkId { get; init; }
-    
+
     [JsonIgnore]
-    public Quark? Quark { get; set; }
-    
+    public Quark? Quark
+    {
+        get => _quark;
+        set => SetProperty(ref _quark, value);
+    }
+
     [JsonIgnore]
-    public Channel? Channel { get; set; }
-    
+    public Channel? Channel
+    {
+        get => _channel;
+        set => SetProperty(ref _channel, value);
+    }
+
     [JsonProperty("children")]
-    public ObservableCollection<QuarkListItem>? Children { get; set; }
+    public ObservableCollection<QuarkListItem>? Children
+    {
+        get => _children;
+        set => SetProperty(ref _children, value);
+    }
 
     [JsonProperty("name")]
-    public string? FolderName { get; set; }
+    public string? FolderName
+    {
+        get => _folderName;
+        set => SetProperty(ref _folderName, value);
+    }
+
+    [JsonIgnore]
+    public bool IsExpanded
+    {
+        get => _isExpanded;
+        set => SetProperty(ref _isExpanded, value);
+    }
 }
 
 public enum QuarkListItemType
diff --git a/Lightquark.NET/Util/Converters/ChannelCreationConverter.cs b/Lightquark.NET/Util/Converters/ChannelCreationConverter.cs
deleted file mode 100644
index 67c8999..0000000
--- a/Lightquark.NET/Util/Converters/ChannelCreationConverter.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Lightquark.NET.Objects;
-using Lightquark.Types.Mongo;
-using Newtonsoft.Json.Converters;
-
-namespace Lightquark.NET.Util.Converters;
-
-public class ChannelCreationConverter : CustomCreationConverter<IChannel>
-{
-    public override IChannel Create(Type objectType)
-    {
-        return new Channel();
-    }
-}
\ No newline at end of file
-- 
GitLab