about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2024-01-08 13:55:15 +0100
committerRory& <root@rory.gay>2024-01-08 13:56:32 +0100
commitede3857084bc7c6e65b7d36cbf913b09596e2787 (patch)
treeb94694c307fb831ea5e63fabde0dbb5f56f02941
parentSmall changes (diff)
downloadMatrixUtils-ede3857084bc7c6e65b7d36cbf913b09596e2787.tar.xz
Internal changes to policy list viewer (extensibility), fix duplicating change handler for room list page (performance), use /state in room list page before sync
-rw-r--r--.editorconfig15
-rw-r--r--.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml2
m---------LibMatrix0
-rw-r--r--MatrixRoomUtils.Abstractions/FileStorageProvider.cs (renamed from MatrixRoomUtils.Desktop/FileStorageProvider.cs)4
-rw-r--r--MatrixRoomUtils.Abstractions/MatrixRoomUtils.Abstractions.csproj12
-rw-r--r--MatrixRoomUtils.Abstractions/RoomInfo.cs (renamed from MatrixRoomUtils.Web/Classes/RoomInfo.cs)15
-rw-r--r--MatrixRoomUtils.Desktop/App.axaml.cs12
-rw-r--r--MatrixRoomUtils.Desktop/Components/NavigationStack.axaml12
-rw-r--r--MatrixRoomUtils.Desktop/Components/NavigationStack.axaml.cs24
-rw-r--r--MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml21
-rw-r--r--MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml.cs15
-rw-r--r--MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml9
-rw-r--r--MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml.cs31
-rw-r--r--MatrixRoomUtils.Desktop/LoginWindow.axaml18
-rw-r--r--MatrixRoomUtils.Desktop/LoginWindow.axaml.cs1
-rw-r--r--MatrixRoomUtils.Desktop/MRUStorageWrapper.cs6
-rw-r--r--MatrixRoomUtils.Desktop/MainWindow.axaml.cs13
-rw-r--r--MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj29
-rw-r--r--MatrixRoomUtils.Desktop/Properties/launchSettings.json3
-rw-r--r--MatrixRoomUtils.Desktop/RoomInfo.cs40
-rw-r--r--MatrixRoomUtils.Desktop/SentryService.cs2
-rw-r--r--MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs6
-rw-r--r--MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj36
-rw-r--r--MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/Index.razor89
-rw-r--r--MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Index.razor102
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor336
-rw-r--r--MatrixRoomUtils.Web/Pages/User/DMManager.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor5
-rw-r--r--MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor6
-rw-r--r--MatrixRoomUtils.Web/Pages/User/Profile.razor78
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomList.razor29
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor13
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListItem.razor13
-rw-r--r--MatrixRoomUtils.Web/wwwroot/css/app.css10
-rw-r--r--MatrixRoomUtils.sln6
-rw-r--r--MatrixRoomUtils.sln.DotSettings.user9
m---------MxApiExtensions0
-rw-r--r--nuget.config12
41 files changed, 611 insertions, 428 deletions
diff --git a/.editorconfig b/.editorconfig
index e8dd2db..3bc1adf 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -371,7 +371,7 @@ dotnet_style_qualification_for_field = false:suggestion
 dotnet_style_qualification_for_method = false:suggestion
 dotnet_style_qualification_for_property = false:suggestion
 dotnet_style_require_accessibility_modifiers = for_non_interface_members:error
-file_header_template = # ReSharper properties
+file_header_template =  # ReSharper properties
 
 resharper_alignment_tab_fill_style = use_spaces
 resharper_align_first_arg_by_paren = false
@@ -606,8 +606,8 @@ resharper_line_break_before_requires_clause = do_not_change
 resharper_linkage_specification_braces = end_of_line
 resharper_linkage_specification_indentation = none
 resharper_local_function_body = expression_body
-resharper_macro_block_begin =
-resharper_macro_block_end =
+resharper_macro_block_begin = 
+resharper_macro_block_end = 
 resharper_max_array_initializer_elements_on_line = 10000
 resharper_max_attribute_length_for_same_line = 38
 resharper_max_enum_members_on_line = 3
@@ -622,7 +622,7 @@ resharper_new_line_before_catch = true
 resharper_new_line_before_else = true
 resharper_new_line_before_enumerators = true
 resharper_normalize_tag_names = false
-resharper_no_indent_inside_elements =
+resharper_no_indent_inside_elements = 
 resharper_no_indent_inside_if_element_longer_than = 2000000
 resharper_null_checking_pattern_style = not_null_pattern
 resharper_object_creation_when_type_evident = target_typed
@@ -675,7 +675,7 @@ resharper_requires_expression_braces = next_line
 resharper_resx_allow_far_alignment = false
 resharper_resx_attribute_indent = single_indent
 resharper_resx_insert_final_newline = false
-resharper_resx_linebreak_before_elements =
+resharper_resx_linebreak_before_elements = 
 resharper_resx_max_blank_lines_between_tags = 0
 resharper_resx_max_line_length = 2147483647
 resharper_resx_pi_attribute_style = do_not_touch
@@ -902,7 +902,7 @@ resharper_xmldoc_wrap_text = true
 resharper_xml_allow_far_alignment = false
 resharper_xml_attribute_indent = align_by_first_attribute
 resharper_xml_insert_final_newline = false
-resharper_xml_linebreak_before_elements =
+resharper_xml_linebreak_before_elements = 
 resharper_xml_max_blank_lines_between_tags = 2
 resharper_xml_max_line_length = 180
 resharper_xml_pi_attribute_style = do_not_touch
@@ -1792,3 +1792,6 @@ resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_
 resharper_xaml_x_key_attribute_disallowed_highlighting = error
 resharper_xunit_xunit_test_with_console_output_highlighting = warning
 resharper_zero_index_from_end_highlighting = warning
+
+# ReSharper properties
+resharper_csharp_int_align_comments = true
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml b/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml
index c468b91..f74ab1c 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml
+++ b/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml
@@ -5,6 +5,8 @@
       <map>
         <entry key="MatrixRoomUtils.Desktop/App.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" />
         <entry key="MatrixRoomUtils.Desktop/Components/NavigationStack.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" />
+        <entry key="MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" />
+        <entry key="MatrixRoomUtils.Desktop/Components/RoomList.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" />
         <entry key="MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" />
         <entry key="MatrixRoomUtils.Desktop/LoginWindow.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" />
         <entry key="MatrixRoomUtils.Desktop/MainWindow.axaml" value="MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj" />
diff --git a/LibMatrix b/LibMatrix
-Subproject 5affd9f061e75f6575a2fe6715f9e8757cfe87e
+Subproject 0f9f9e9201bbbed5981135d67e1265fd0f31aef
diff --git a/MatrixRoomUtils.Desktop/FileStorageProvider.cs b/MatrixRoomUtils.Abstractions/FileStorageProvider.cs
index 0429d1a..73d5604 100644
--- a/MatrixRoomUtils.Desktop/FileStorageProvider.cs
+++ b/MatrixRoomUtils.Abstractions/FileStorageProvider.cs
@@ -5,7 +5,7 @@ using LibMatrix.Extensions;
 using LibMatrix.Interfaces.Services;
 using Microsoft.Extensions.Logging;
 
-namespace MatrixRoomUtils.Desktop;
+namespace MatrixRoomUtils.Abstractions;
 
 public class FileStorageProvider : IStorageProvider {
     private readonly ILogger<FileStorageProvider> _logger;
@@ -17,7 +17,7 @@ public class FileStorageProvider : IStorageProvider {
     /// </summary>
     /// <param name="targetPath"></param>
     public FileStorageProvider(string targetPath) {
-        new Logger<FileStorageProvider>(new LoggerFactory()).LogInformation("test");
+        // new Logger<FileStorageProvider>(new LoggerFactory()).LogInformation("test");
         Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}");
         TargetPath = targetPath;
         if (!Directory.Exists(targetPath)) {
diff --git a/MatrixRoomUtils.Abstractions/MatrixRoomUtils.Abstractions.csproj b/MatrixRoomUtils.Abstractions/MatrixRoomUtils.Abstractions.csproj
new file mode 100644
index 0000000..1665ff0
--- /dev/null
+++ b/MatrixRoomUtils.Abstractions/MatrixRoomUtils.Abstractions.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+    
+    <ItemGroup>
+        <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+    </ItemGroup>
+</Project>
diff --git a/MatrixRoomUtils.Web/Classes/RoomInfo.cs b/MatrixRoomUtils.Abstractions/RoomInfo.cs
index 9d0cd59..6db3447 100644
--- a/MatrixRoomUtils.Web/Classes/RoomInfo.cs
+++ b/MatrixRoomUtils.Abstractions/RoomInfo.cs
@@ -4,10 +4,9 @@ using ArcaneLibs;
 using LibMatrix;
 using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.EventTypes.Spec.State.RoomInfo;
-using LibMatrix.Interfaces;
 using LibMatrix.RoomTypes;
 
-namespace MatrixRoomUtils.Web.Classes;
+namespace MatrixRoomUtils.Abstractions;
 
 public class RoomInfo : NotifyPropertyChanged {
     public required GenericRoom Room { get; set; }
@@ -44,7 +43,9 @@ public class RoomInfo : NotifyPropertyChanged {
                 else
                     @event.RawContent = default!;
             }
-            else throw;
+            else {
+                throw;
+            }
         }
 
         StateEvents.Add(@event);
@@ -84,12 +85,12 @@ public class RoomInfo : NotifyPropertyChanged {
     public RoomInfo() {
         StateEvents.CollectionChanged += (_, args) => {
             if (args.NewItems is { Count: > 0 })
-                foreach (StateEventResponse newState in args.NewItems) {
-                    if (newState.GetType == typeof(RoomNameEventContent) && newState.TypedContent is RoomNameEventContent roomNameContent)
+                foreach (StateEventResponse newState in args.NewItems) { // TODO: switch statement benchmark?
+                    if (newState.Type == RoomNameEventContent.EventId && newState.TypedContent is RoomNameEventContent roomNameContent)
                         RoomName = roomNameContent.Name;
-                    else if (newState.GetType == typeof(RoomAvatarEventContent) && newState.TypedContent is RoomAvatarEventContent roomAvatarContent)
+                    else if (newState is { Type: RoomAvatarEventContent.EventId, TypedContent: RoomAvatarEventContent roomAvatarContent })
                         RoomIcon = roomAvatarContent.Url;
-                    else if (newState.GetType == typeof(RoomCreateEventContent) && newState.TypedContent is RoomCreateEventContent roomCreateContent) {
+                    else if (newState is { Type: RoomCreateEventContent.EventId, TypedContent: RoomCreateEventContent roomCreateContent }) {
                         CreationEventContent = roomCreateContent;
                         RoomCreator = newState.Sender;
                     }
diff --git a/MatrixRoomUtils.Desktop/App.axaml.cs b/MatrixRoomUtils.Desktop/App.axaml.cs
index 3963be6..aeb154c 100644
--- a/MatrixRoomUtils.Desktop/App.axaml.cs
+++ b/MatrixRoomUtils.Desktop/App.axaml.cs
@@ -1,7 +1,9 @@
 using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
+using Avalonia.Styling;
 using LibMatrix.Services;
+using MatrixRoomUtils.Abstractions;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 
@@ -10,10 +12,6 @@ namespace MatrixRoomUtils.Desktop;
 public partial class App : Application {
     public IHost host { get; set; }
 
-    public override void Initialize() {
-        AvaloniaXamlLoader.Load(this);
-    }
-
     public override void OnFrameworkInitializationCompleted() {
         host = Host.CreateDefaultBuilder().ConfigureServices((ctx, services) => {
             services.AddSingleton<MRUDesktopConfiguration>();
@@ -42,6 +40,10 @@ public partial class App : Application {
             var scope = scopeFac.CreateScope();
             desktop.MainWindow = scope.ServiceProvider.GetRequiredService<MainWindow>();
         }
+        
+        if(Environment.GetEnvironmentVariable("AVALONIA_THEME")?.Equals("dark", StringComparison.OrdinalIgnoreCase) ?? false)
+            RequestedThemeVariant = ThemeVariant.Dark;
+        
         base.OnFrameworkInitializationCompleted();
     }
-}
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml b/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml
index c773b8d..bc6b75d 100644
--- a/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml
+++ b/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml
@@ -4,9 +4,9 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="MatrixRoomUtils.Desktop.Components.NavigationStack">
-    <DockPanel x:Name="dock">
-        <StackPanel x:Name="navPanel"></StackPanel>
-        <UserControl x:Name="content"></UserControl>
-    </DockPanel>
-
-</UserControl>
+    <StackPanel x:Name="dock">
+        <Label>NagivationStack</Label>
+        <StackPanel x:Name="navPanel" Orientation="Horizontal"></StackPanel>
+        <ContentControl x:Name="content"></ContentControl>
+    </StackPanel>
+</UserControl>
\ No newline at end of file
diff --git a/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml.cs b/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml.cs
index d6343e2..92c617b 100644
--- a/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml.cs
+++ b/MatrixRoomUtils.Desktop/Components/NavigationStack.axaml.cs
@@ -1,4 +1,5 @@
 using Avalonia.Controls;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 
 namespace MatrixRoomUtils.Desktop.Components;
@@ -8,12 +9,24 @@ public partial class NavigationStack : UserControl {
         InitializeComponent();
     }
 
-    private void InitializeComponent() {
-        AvaloniaXamlLoader.Load(this);
+    // private void InitializeComponent() {
+        // AvaloniaXamlLoader.Load(this);
+        // buildView();
+    // }
+
+    protected override void OnLoaded(RoutedEventArgs e) {
+        base.OnLoaded(e);
         buildView();
     }
-
+    
     private void buildView() {
+        if (navPanel is null) {
+            Console.WriteLine("NavigationStack buildView called while navpanel is null!");
+            // await Task.Delay(100);
+            // if (navPanel is null)
+                // await buildView();
+            // else Console.WriteLine("navpanel is not null!");
+        }
         navPanel.Children.Clear();
         foreach (var item in _stack) {
             Button btn = new() {
@@ -25,7 +38,7 @@ public partial class NavigationStack : UserControl {
             };
             navPanel.Children.Add(btn);
         }
-        content = Current?.View ?? new UserControl();
+        content.Content = Current?.View ?? new UserControl();
     }
 
 
@@ -44,13 +57,16 @@ public partial class NavigationStack : UserControl {
             Name = name,
             View = view
         });
+        buildView();
     }
 
     public void Pop() {
         _stack.RemoveAt(_stack.Count - 1);
+        buildView();
     }
 
     public void PopTo(int index) {
         _stack.RemoveRange(index, _stack.Count - index);
+        buildView();
     }
 }
diff --git a/MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml b/MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml
new file mode 100644
index 0000000..0e43d99
--- /dev/null
+++ b/MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml
@@ -0,0 +1,21 @@
+<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:components="clr-namespace:MatrixRoomUtils.Desktop.Components.Pages"
+             xmlns:components1="clr-namespace:MatrixRoomUtils.Desktop.Components"
+             xmlns:abstractions="clr-namespace:MatrixRoomUtils.Abstractions;assembly=MatrixRoomUtils.Abstractions"
+
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="MatrixRoomUtils.Desktop.Components.Pages.RoomList"
+             x:DataType="components:RoomList"
+             DataContext="{Binding $self}"
+             >
+    <ListBox ItemsSource="{Binding Rooms}">
+        <ListBox.ItemTemplate>
+            <DataTemplate DataType="abstractions:RoomInfo">
+                <components1:RoomListEntry Room="{Binding Path=.}"/>
+            </DataTemplate>
+        </ListBox.ItemTemplate>
+    </ListBox>
+</UserControl>
diff --git a/MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml.cs b/MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml.cs
new file mode 100644
index 0000000..53c3063
--- /dev/null
+++ b/MatrixRoomUtils.Desktop/Components/Pages/RoomList.axaml.cs
@@ -0,0 +1,15 @@
+using System.Collections.ObjectModel;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using MatrixRoomUtils.Abstractions;
+
+namespace MatrixRoomUtils.Desktop.Components.Pages;
+
+public partial class RoomList : UserControl {
+    private ObservableCollection<RoomInfo> Rooms { get; set; } = new();
+
+    public RoomList() {
+        InitializeComponent();
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml b/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml
index 09fe52b..db22ccc 100644
--- a/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml
+++ b/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml
@@ -2,10 +2,15 @@
              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:components="clr-namespace:MatrixRoomUtils.Desktop.Components"
              mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="32"
-             x:Class="MatrixRoomUtils.Desktop.Components.RoomListEntry">
+             x:Class="MatrixRoomUtils.Desktop.Components.RoomListEntry"
+             
+             x:DataType="components:RoomListEntry"
+             DataContext="{Binding $self}"
+             >
     <StackPanel Orientation="Horizontal">
         <Image MaxWidth="64" x:Name="RoomIcon"></Image>
-        <Label x:Name="RoomName"></Label>
+        <Label x:Name="RoomName" Content="{Binding Room.RoomName}"></Label>
     </StackPanel>
 </UserControl>
diff --git a/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml.cs b/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml.cs
index 69458aa..73115a2 100644
--- a/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml.cs
+++ b/MatrixRoomUtils.Desktop/Components/RoomListEntry.axaml.cs
@@ -7,29 +7,28 @@ using LibMatrix.EventTypes.Spec.State.RoomInfo;
 using LibMatrix.Helpers;
 using LibMatrix.Interfaces.Services;
 using LibMatrix.Services;
+using MatrixRoomUtils.Abstractions;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
 
 namespace MatrixRoomUtils.Desktop.Components;
 
 public partial class RoomListEntry : UserControl {
-    private readonly IServiceScopeFactory _serviceScopeFactory;
-    private readonly RoomInfo _roomInfo;
+    public RoomInfo Room { get; set; }
 
-    public RoomListEntry(IServiceScopeFactory serviceScopeFactory, RoomInfo roomInfo) {
-        _serviceScopeFactory = serviceScopeFactory;
-        _roomInfo = roomInfo;
+    public RoomListEntry() {
         InitializeComponent();
     }
 
     protected override void OnLoaded(RoutedEventArgs e) {
         base.OnLoaded(e);
-        RoomName.Content = _roomInfo.Room.RoomId;
+        RoomName.Content = Room.Room.RoomId;
         Task.WhenAll(GetRoomName(), GetRoomIcon());
     }
 
     private async Task GetRoomName() {
         try {
-            var nameEvent = await _roomInfo.GetStateEvent("m.room.name");
+            var nameEvent = await Room.GetStateEvent("m.room.name");
             if (nameEvent?.TypedContent is RoomNameEventContent nameData)
                 RoomName.Content = nameData.Name;
         }
@@ -41,18 +40,22 @@ public partial class RoomListEntry : UserControl {
 
     private async Task GetRoomIcon() {
         try {
-            var avatarEvent = await _roomInfo.GetStateEvent("m.room.avatar");
+            using var hc = new HttpClient();
+            var avatarEvent = await Room.GetStateEvent("m.room.avatar");
             if (avatarEvent?.TypedContent is RoomAvatarEventContent avatarData) {
                 var mxcUrl = avatarData.Url;
-                await using var svc = _serviceScopeFactory.CreateAsyncScope();
-                var hs = await svc.ServiceProvider.GetService<MRUStorageWrapper>()?.GetCurrentSessionOrPrompt()!;
-                var hsResolver = svc.ServiceProvider.GetService<HomeserverResolverService>();
-                var storage = svc.ServiceProvider.GetService<TieredStorageService>()?.CacheStorageProvider;
-                var resolvedUrl = await hsResolver.ResolveMediaUri(hs.ServerName, mxcUrl);
+                var resolvedUrl = await Room.Room.GetResolvedRoomAvatarUrlAsync();
+                
+                // await using var svc = _serviceScopeFactory.CreateAsyncScope();
+                // var hs = await svc.ServiceProvider.GetService<MRUStorageWrapper>()?.GetCurrentSessionOrPrompt()!;
+                // var hsResolver = svc.ServiceProvider.GetService<HomeserverResolverService>();
+                // var storage = svc.ServiceProvider.GetService<TieredStorageService>()?.CacheStorageProvider;
+                // var resolvedUrl = await hsResolver.ResolveMediaUri(hs.ServerName, mxcUrl);
+                var storage = new FileStorageProvider("cache");
                 var storageKey = $"media/{mxcUrl.Replace("mxc://", "").Replace("/", ".")}";
                 try {
                     if (!await storage.ObjectExistsAsync(storageKey))
-                        await storage.SaveStreamAsync(storageKey, await hs.ClientHttpClient.GetStreamAsync(resolvedUrl));
+                        await storage.SaveStreamAsync(storageKey, await hc.GetStreamAsync(resolvedUrl));
 
                     RoomIcon.Source = new Bitmap(await storage.LoadStreamAsync(storageKey) ?? throw new NullReferenceException());
                 }
diff --git a/MatrixRoomUtils.Desktop/LoginWindow.axaml b/MatrixRoomUtils.Desktop/LoginWindow.axaml
index a4600d5..fc0ee6f 100644
--- a/MatrixRoomUtils.Desktop/LoginWindow.axaml
+++ b/MatrixRoomUtils.Desktop/LoginWindow.axaml
@@ -4,14 +4,22 @@
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:desktop="clr-namespace:MatrixRoomUtils.Desktop"
         mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
-        x:Class="MatrixRoomUtils.Desktop.LoginWindow"
         Title="LoginWindow"
+        x:Class="MatrixRoomUtils.Desktop.LoginWindow"
         x:DataType="desktop:LoginWindow"
         DataContext="{Binding $self}"
-        >
+        SizeToContent="WidthAndHeight" CanResize="False"
+        MinWidth="250">
     <StackPanel>
-        <TextBox Text="{Binding Username, Mode=TwoWay}" />
-        <MaskedTextBox PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" />
+        <Label>Log in</Label>
+        <StackPanel Orientation="Horizontal">
+            <Label Width="100">User ID</Label>
+            <TextBox MinWidth="250" Text="{Binding Username, Mode=TwoWay}" />
+        </StackPanel>
+        <StackPanel Orientation="Horizontal">
+            <Label Width="100">Password</Label>
+            <MaskedTextBox MinWidth="250" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" />
+        </StackPanel>
         <Button Click="Login">Login</Button>
     </StackPanel>
-</Window>
+</Window>
\ No newline at end of file
diff --git a/MatrixRoomUtils.Desktop/LoginWindow.axaml.cs b/MatrixRoomUtils.Desktop/LoginWindow.axaml.cs
index 1f31b05..183c46b 100644
--- a/MatrixRoomUtils.Desktop/LoginWindow.axaml.cs
+++ b/MatrixRoomUtils.Desktop/LoginWindow.axaml.cs
@@ -2,6 +2,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
 
 namespace MatrixRoomUtils.Desktop;
 
diff --git a/MatrixRoomUtils.Desktop/MRUStorageWrapper.cs b/MatrixRoomUtils.Desktop/MRUStorageWrapper.cs
index 8a44518..b69c50d 100644
--- a/MatrixRoomUtils.Desktop/MRUStorageWrapper.cs
+++ b/MatrixRoomUtils.Desktop/MRUStorageWrapper.cs
@@ -1,3 +1,4 @@
+using Avalonia;
 using LibMatrix;
 using LibMatrix.Homeservers;
 using LibMatrix.Responses;
@@ -74,8 +75,11 @@ public class MRUStorageWrapper(TieredStorageService storageService, HomeserverPr
         if (session is null) {
             // _navigationManager.NavigateTo("/Login");
             var wnd = new LoginWindow(this);
+            wnd.Position = MainWindow.Instance.Position + new PixelPoint(50, 50);
             await wnd.ShowDialog(MainWindow.Instance);
-            while (wnd.IsVisible) await Task.Delay(100);
+            while (wnd.IsVisible) {
+                await Task.Delay(100);
+            }
             session = await GetCurrentSession();
         }
 
diff --git a/MatrixRoomUtils.Desktop/MainWindow.axaml.cs b/MatrixRoomUtils.Desktop/MainWindow.axaml.cs
index 9db59c5..6ef573e 100644
--- a/MatrixRoomUtils.Desktop/MainWindow.axaml.cs
+++ b/MatrixRoomUtils.Desktop/MainWindow.axaml.cs
@@ -1,5 +1,7 @@
 using Avalonia.Controls;
 using Avalonia.Interactivity;
+using MatrixRoomUtils.Abstractions;
+using MatrixRoomUtils.Desktop.Components;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
@@ -26,7 +28,6 @@ public partial class MainWindow : Window {
         _logger.LogInformation("Cache location: {}", _configuration.CacheStoragePath);
         _logger.LogInformation("Data location: {}", _configuration.DataStoragePath);
 
-
         // for (int i = 0; i < 100; i++) {
         // roomList.Children.Add(new RoomListEntry());
         // }
@@ -39,12 +40,18 @@ public partial class MainWindow : Window {
         var rooms = await hs.GetJoinedRooms();
         foreach (var room in rooms) {
             // roomList.Children.Add(new RoomListEntry(_scopeFactory, new RoomInfo(room)));
+
+            windowContent.Push("home", new RoomListEntry() {
+                Room = new RoomInfo() {
+                    Room = room
+                }
+            });
+            base.OnLoaded(e);
         }
-        base.OnLoaded(e);
     }
 
     // public Command
     // protected void LoadedCommand() {
     // _logger.LogInformation("async command");
     // }
-}
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj b/MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj
index 6d9fc0e..94bf245 100644
--- a/MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj
+++ b/MatrixRoomUtils.Desktop/MatrixRoomUtils.Desktop.csproj
@@ -10,29 +10,27 @@
         <LangVersion>preview</LangVersion>
         <ImplicitUsings>enable</ImplicitUsings>
         <InvariantGlobalization>true</InvariantGlobalization>
-        <PublishTrimmed>true</PublishTrimmed>
-        <PublishReadyToRun>true</PublishReadyToRun>
-        <PublishSingleFile>true</PublishSingleFile>
-        <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>
-        <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>
-        <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>
+<!--        <PublishTrimmed>true</PublishTrimmed>-->
+<!--        <PublishReadyToRun>true</PublishReadyToRun>-->
+<!--        <PublishSingleFile>true</PublishSingleFile>-->
+<!--        <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>-->
+<!--        <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>-->
+<!--        <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>-->
     </PropertyGroup>
 
 
     <ItemGroup>
-        <PackageReference Include="Avalonia" Version="11.0.5" />
-        <PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
-        <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
-        <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" />
+        <PackageReference Include="Avalonia" Version="11.0.6" />
+        <PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
+        <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
+        <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
         <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
-        <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" />
+        <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.6" />
         <PackageReference Include="Sentry" Version="3.36.0" />
     </ItemGroup>
 
 
-    <ItemGroup>
-        <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
-    </ItemGroup>
+
 
     <ItemGroup>
         <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.5" />
@@ -46,4 +44,7 @@
             <CopyToOutputDirectory>Always</CopyToOutputDirectory>
         </Content>
     </ItemGroup>
+    <ItemGroup>
+      <ProjectReference Include="..\MatrixRoomUtils.Abstractions\MatrixRoomUtils.Abstractions.csproj" />
+    </ItemGroup>
 </Project>
diff --git a/MatrixRoomUtils.Desktop/Properties/launchSettings.json b/MatrixRoomUtils.Desktop/Properties/launchSettings.json
index 997e294..36405e8 100644
--- a/MatrixRoomUtils.Desktop/Properties/launchSettings.json
+++ b/MatrixRoomUtils.Desktop/Properties/launchSettings.json
@@ -12,7 +12,8 @@
       "commandName": "Project",
       "dotnetRunMessages": true,
       "environmentVariables": {
-        "DOTNET_ENVIRONMENT": "Development"
+        "DOTNET_ENVIRONMENT": "Development",
+        "AVALONIA_THEME": "Dark"
       }
     },
     "Local config": {
diff --git a/MatrixRoomUtils.Desktop/RoomInfo.cs b/MatrixRoomUtils.Desktop/RoomInfo.cs
deleted file mode 100644
index a562086..0000000
--- a/MatrixRoomUtils.Desktop/RoomInfo.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using LibMatrix;
-using LibMatrix.EventTypes;
-using LibMatrix.Interfaces;
-using LibMatrix.Responses;
-using LibMatrix.RoomTypes;
-
-namespace MatrixRoomUtils.Desktop;
-
-public class RoomInfo {
-    public RoomInfo() { }
-
-    public RoomInfo(GenericRoom room) {
-        Room = room;
-    }
-
-    public GenericRoom Room { get; set; }
-    public List<StateEventResponse?> StateEvents { get; init; } = new();
-
-    public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") {
-        var @event = StateEvents.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey);
-        if (@event is not null) return @event;
-        @event = new StateEventResponse {
-            RoomId = Room.RoomId,
-            Type = type,
-            StateKey = stateKey,
-            Sender = null, //TODO: implement
-            EventId = null
-        };
-        try {
-            @event.TypedContent = await Room.GetStateAsync<EventContent>(type, stateKey);
-        }
-        catch (MatrixException e) {
-            if (e is { ErrorCode: "M_NOT_FOUND" }) @event.TypedContent = default!;
-            else throw;
-        }
-
-        StateEvents.Add(@event);
-        return @event;
-    }
-}
diff --git a/MatrixRoomUtils.Desktop/SentryService.cs b/MatrixRoomUtils.Desktop/SentryService.cs
index 648946c..26212fa 100644
--- a/MatrixRoomUtils.Desktop/SentryService.cs
+++ b/MatrixRoomUtils.Desktop/SentryService.cs
@@ -6,7 +6,7 @@ namespace MatrixRoomUtils.Desktop;
 
 public class SentryService : IDisposable {
     private IDisposable? _sentrySdkDisposable;
-    public SentryService(IServiceScopeFactory scopeFactory, ILogger logger) {
+    public SentryService(IServiceScopeFactory scopeFactory, ILogger<SentryService> logger) {
         var config = scopeFactory.CreateScope().ServiceProvider.GetRequiredService<MRUDesktopConfiguration>();
         if (config.SentryDsn is null) {
             logger.LogWarning("Sentry DSN is not set, skipping Sentry initialisation");
diff --git a/MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs b/MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs
index cbe2303..1cf7064 100644
--- a/MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs
+++ b/MatrixRoomUtils.LibDMSpace/DMSpaceRoom.cs
@@ -24,7 +24,7 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI
     }
 
     public async Task<EventIdResponse> AddChildAsync(GenericRoom room) {
-        var members = room.GetMembersAsync(true);
+        var members = room.GetMembersEnumerableAsync(true);
         Dictionary<string, int> memberCountByHs = new();
         await foreach (var member in members) {
             var server = member.StateKey.Split(':')[1];
@@ -61,12 +61,12 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI
                     }
                 };
                 // Add all DM room members
-                var members = homeserver.GetRoom(roomid).GetMembersAsync();
+                var members = homeserver.GetRoom(roomid).GetMembersEnumerableAsync();
                 await foreach (var member in members)
                     if (member.StateKey != userId)
                         dri.RemoteUsers.Add(member.StateKey);
                 // Remove members of DM space
-                members = GetMembersAsync();
+                members = GetMembersEnumerableAsync();
                 await foreach (var member in members)
                     if (dri.RemoteUsers.Contains(member.StateKey))
                         dri.RemoteUsers.Remove(member.StateKey);
diff --git a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
index c6678ce..ed62751 100644
--- a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
+++ b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
@@ -23,6 +23,7 @@
         <ProjectReference Condition="Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" />
         <PackageReference Condition="!Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="ArcaneLibs.Blazor.Components" Version="*-preview*" />
         <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+        <ProjectReference Include="..\MatrixRoomUtils.Abstractions\MatrixRoomUtils.Abstractions.csproj" />
         <ProjectReference Include="..\MatrixRoomUtils.LibDMSpace\MatrixRoomUtils.LibDMSpace.csproj" />
     </ItemGroup>
 
@@ -35,39 +36,4 @@
       </Content>
     </ItemGroup>
 
-    <ItemGroup>
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-Bold.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-BoldItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-ExtraBold.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-ExtraBoldItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-ExtraLight.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-ExtraLightItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-Italic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-Light.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-LightItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-Medium.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-MediumItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-Regular.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-SemiBold.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-SemiBoldItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-Thin.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMono-ThinItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-Bold.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-BoldItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-ExtraBold.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-ExtraBoldItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-ExtraLight.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-ExtraLightItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-Italic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-Light.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-LightItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-Medium.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-MediumItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-Regular.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-SemiBold.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-SemiBoldItalic.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-Thin.ttf" />
-      <_ContentIncludedByDefault Remove="wwwroot\css\jetbrains-mono\ttf\JetBrainsMonoNL-ThinItalic.ttf" />
-    </ItemGroup>
-
 </Project>
diff --git a/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
index 94c51b2..27fe35e 100644
--- a/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
+++ b/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
@@ -3,6 +3,7 @@
 @using ArcaneLibs.Extensions
 @using LibMatrix.Extensions
 @using LibMatrix.Homeservers
+@using MatrixRoomUtils.Abstractions
 @inject ILocalStorageService LocalStorage
 @inject NavigationManager NavigationManager
 <h3>Debug Tools</h3>
diff --git a/MatrixRoomUtils.Web/Pages/Index.razor b/MatrixRoomUtils.Web/Pages/Index.razor
index 2d1d6c0..ebb0ebb 100644
--- a/MatrixRoomUtils.Web/Pages/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Index.razor
@@ -16,30 +16,29 @@ Small collection of tools to do not-so-everyday things.
 <hr/>
 <form>
     <table>
-        @foreach (var __auth in _auth.OrderByDescending(x => x.UserInfo.RoomCount)) {
-            var _auth = __auth.UserAuth;
+        @foreach (var session in _sessions.OrderByDescending(x => x.UserInfo.RoomCount)) {
+            var _auth = session.UserAuth;
             <tr class="user-entry">
                 <td>
-                    <img class="avatar" src="@__auth.UserInfo.AvatarUrl"/>
+                    <img class="avatar" src="@session.UserInfo.AvatarUrl"/>
                 </td>
                 <td class="user-info">
-                    @* <div class="user-info"> *@
                     <p>
                         <input type="radio" name="csa" checked="@(_currentSession.AccessToken == _auth.AccessToken)" @onclick="@(() => SwitchSession(_auth))" style="text-decoration-line: unset;"/>
-                        <b>@__auth.UserInfo.DisplayName</b> on <b>@_auth.Homeserver</b><br/>
+                        <b>@session.UserInfo.DisplayName</b> on <b>@_auth.Homeserver</b><br/>
                     </p>
-                    <span style="display: inline-block; width: 128px;">@__auth.UserInfo.RoomCount rooms</span>
-                    <a style="color: #888888" href="@("/ServerInfo/"+__auth.Homeserver.ServerName+"/")">@__auth.ServerVersion.Server.Name @__auth.ServerVersion.Server.Version</a>
+                    <span style="display: inline-block; width: 128px;">@session.UserInfo.RoomCount rooms</span>
+                    <a style="color: #888888" href="@("/ServerInfo/" + session.Homeserver.ServerName + "/")">@session.ServerVersion.Server.Name @session.ServerVersion.Server.Version</a>
                     @if (_auth.Proxy != null) {
                         <span class="badge badge-info"> (proxied via @_auth.Proxy)</span>
                     }
                     else {
                         <p>Not proxied</p>
                     }
-                    @if (DEBUG) {
-                        <p>T=@__auth.Homeserver.GetType().FullName</p>
-                        <p>D=@__auth.Homeserver.WhoAmI.DeviceId</p>
-                        <p>U=@__auth.Homeserver.WhoAmI.UserId</p>
+                    @if (_debug) {
+                        <p>T=@session.Homeserver.GetType().FullName</p>
+                        <p>D=@session.Homeserver.WhoAmI.DeviceId</p>
+                        <p>U=@session.Homeserver.WhoAmI.UserId</p>
                     }
                 </td>
                 <td>
@@ -49,18 +48,46 @@ Small collection of tools to do not-so-everyday things.
                         <LinkButton OnClick="@(() => RemoveUser(_auth, true))">Log out</LinkButton>
                     </p>
                 </td>
-                @* </div> *@
             </tr>
         }
     </table>
 </form>
 
+@if (_offlineSessions.Count > 0) {
+    <br/>
+    <br/>
+    <h5>Sessions on unreachable servers</h5>
+    <hr/>
+    <form>
+        <table>
+            @foreach (var session in _offlineSessions) {
+                <tr class="user-entry">
+                    <td>
+                        <p>
+                            @{
+                                string[] parts = session.UserId.Split(':');
+                            }
+                            <span>@parts[0][1..]</span> on <span>@parts[1]</span>
+                            @if (!string.IsNullOrWhiteSpace(session.Proxy)) {
+                                <span class="badge badge-info"> (proxied via @session.Proxy)</span>
+                            }
+                        </p>
+                    </td>
+                    <td>
+                        <LinkButton OnClick="@(() => RemoveUser(session))">Remove</LinkButton>
+                    </td>
+                </tr>
+            }
+        </table>
+    </form>
+}
+
 @code
 {
 #if DEBUG
-    bool DEBUG = true;
+    private const bool _debug = true;
 #else
-    bool DEBUG = false;
+    private const bool _debug = false;
 #endif
 
     private class AuthInfo {
@@ -71,35 +98,42 @@ Small collection of tools to do not-so-everyday things.
     }
 
     // private Dictionary<UserAuth, UserInfo> _users = new();
-    private List<AuthInfo> _auth = new();
+    private readonly List<AuthInfo> _sessions = [];
+    private readonly List<UserAuth> _offlineSessions = [];
+    private LoginResponse? _currentSession;
 
     protected override async Task OnInitializedAsync() {
+        Console.WriteLine("Index.OnInitializedAsync");
         _currentSession = await MRUStorage.GetCurrentToken();
-    // _users.Clear();
-        _auth.Clear();
+        _sessions.Clear();
+        _offlineSessions.Clear();
         var tokens = await MRUStorage.GetAllTokens();
         var profileTasks = tokens.Select(async token => {
             UserInfo userInfo = new();
             AuthenticatedHomeserverGeneric hs;
+            Console.WriteLine($"Getting hs for {token.ToJson()}");
             try {
                 hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy);
             }
             catch (MatrixException e) {
-                if (e.ErrorCode == "M_UNKNOWN_TOKEN") {
-                    NavigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken);
-                    return;
-                }
-                throw;
+                if (e.ErrorCode != "M_UNKNOWN_TOKEN") throw;
+                NavigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken);
+                return;
+
             }
             catch (HttpRequestException e) {
                 logger.LogError(e, $"Failed to instantiate AuthenticatedHomeserver for {token.ToJson()}, homeserver may be offline?", token.UserId);
+                _offlineSessions.Add(token);
                 return;
             }
+            
+            Console.WriteLine($"Got hs for {token.ToJson()}");
+
             var roomCountTask = hs.GetJoinedRooms();
             var profile = await hs.GetProfileAsync(hs.WhoAmI.UserId);
             userInfo.DisplayName = profile.DisplayName ?? hs.WhoAmI.UserId;
             Console.WriteLine(profile.ToJson());
-            _auth.Add(new() {
+            _sessions.Add(new() {
                 UserInfo = new() {
                     AvatarUrl = string.IsNullOrWhiteSpace(profile.AvatarUrl) ? "https://api.dicebear.com/6.x/identicon/svg?seed=" + hs.WhoAmI.UserId : hs.ResolveMediaUri(profile.AvatarUrl),
                     RoomCount = (await roomCountTask).Count,
@@ -110,7 +144,9 @@ Small collection of tools to do not-so-everyday things.
                 Homeserver = hs
             });
         });
+        Console.WriteLine("Waiting for profile tasks");
         await Task.WhenAll(profileTasks);
+        Console.WriteLine("Done waiting for profile tasks");
         await base.OnInitializedAsync();
     }
 
@@ -127,19 +163,20 @@ Small collection of tools to do not-so-everyday things.
             }
         }
         catch (Exception e) {
-            if (e is MatrixException {ErrorCode: "M_UNKNOWN_TOKEN" }) {
-    //todo: handle this
+            if (e is MatrixException { ErrorCode: "M_UNKNOWN_TOKEN" }) {
+                //todo: handle this
                 return;
             }
+
             Console.WriteLine(e);
         }
+
         await MRUStorage.RemoveToken(auth);
         if ((await MRUStorage.GetCurrentToken())?.AccessToken == auth.AccessToken)
             await MRUStorage.SetCurrentToken((await MRUStorage.GetAllTokens() ?? throw new InvalidOperationException()).FirstOrDefault());
         await OnInitializedAsync();
     }
 
-    private LoginResponse _currentSession;
 
     private async Task SwitchSession(UserAuth auth) {
         Console.WriteLine($"Switching to {auth.Homeserver} {auth.UserId} via {auth.Proxy}");
diff --git a/MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor b/MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor
index d33756b..506a8a1 100644
--- a/MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor
+++ b/MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor
@@ -4,6 +4,7 @@
 @using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.RoomTypes
 @using ArcaneLibs.Extensions
+@using MatrixRoomUtils.Abstractions
 <h3>UserRoomHistory</h3>
 
 <span>Enter mxid: </span>
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
index 2ac4bcb..6cabe82 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
@@ -1,25 +1,21 @@
 @page "/Rooms"
 @using LibMatrix.Filters
 @using LibMatrix.Helpers
-@using LibMatrix.EventTypes.Spec.State
-@using LibMatrix
-@using LibMatrix.Homeservers
-@using ArcaneLibs.Extensions
 @using LibMatrix.Extensions
 @using LibMatrix.Responses
 @using System.Collections.ObjectModel
 @using System.Diagnostics
+@using ArcaneLibs.Extensions
+@using MatrixRoomUtils.Abstractions
 @inject ILogger<Index> logger
 <h3>Room list</h3>
 
 <p>@Status</p>
 <p>@Status2</p>
-@* @if (RenderContents) { *@
+
+<LinkButton href="/Rooms/Create">Create new room</LinkButton>
+
 <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents"></RoomList>
-@* } *@
-@* else { *@
-@* <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" StillFetching="true"></RoomList> *@
-@* } *@
 
 @code {
     private ObservableCollection<RoomInfo> Rooms { get; } = new();
@@ -47,9 +43,9 @@
             },
             State = new SyncFilter.RoomFilter.StateFilter {
                 Types = new List<string> {
+                    "m.room.create",
                     "m.room.name",
                     "m.room.avatar",
-                    "m.room.create",
                     "org.matrix.mjolnir.shortcode",
                     "m.room.power_levels",
                 }
@@ -92,49 +88,72 @@
     //     }
     // };
 
+    private SyncHelper syncHelper;
+
+    // SyncHelper profileSyncHelper;
+
     protected override async Task OnInitializedAsync() {
         Homeserver = await MRUStorage.GetCurrentSessionOrNavigate();
         if (Homeserver is null) return;
         var rooms = await Homeserver.GetJoinedRooms();
-        foreach (var room in rooms) {
-            Rooms.Add(new(){Room = room});
+        // SemaphoreSlim _semaphore = new(160, 160);
+
+        var roomTasks = rooms.Select(async room => {
+            RoomInfo ri;
+            // await _semaphore.WaitAsync();
+            ri = new() { Room = room };
+            await Task.WhenAll((filter.Room?.State?.Types ?? []).Select(x => ri.GetStateEvent(x)));
+            return ri;
+        }).ToAsyncEnumerable();
+
+        await foreach (var room in roomTasks) {
+            Rooms.Add(room);
+            StateHasChanged();
+            // await Task.Delay(50);
+            // _semaphore.Release();
         }
-        
-        GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId);
 
-        var syncHelper = new SyncHelper(Homeserver, logger) {
-            Timeout = 10000,
+        if (rooms.Count >= 150) RenderContents = true;
+
+        GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId);
+        syncHelper = new SyncHelper(Homeserver, logger) {
+            Timeout = 30000,
             Filter = filter,
             MinimumDelay = TimeSpan.FromMilliseconds(5000)
         };
-        // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId);
-        // var profileSyncHelper = new SyncHelper(Homeserver, logger) {
+        //  profileSyncHelper = new SyncHelper(Homeserver, logger) {
         //     Timeout = 10000,
         //     Filter = profileUpdateFilter,
         //     MinimumDelay = TimeSpan.FromMilliseconds(5000)
-        // };
+        //  };
+        // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId);
+
         RunSyncLoop(syncHelper);
         // RunSyncLoop(profileSyncHelper);
         RunQueueProcessor();
+
         await base.OnInitializedAsync();
     }
-    
+
     private async Task RunQueueProcessor() {
         var renderTimeSw = Stopwatch.StartNew();
+        var isInitialSync = true;
         while (true) {
             try {
-                if (queue.Count == 0) {
-                    while (queue.Count == 0) {
-                        Console.WriteLine("Queue is empty, waiting...");
-                        await Task.Delay(2500);
-                    }
-                    Console.WriteLine("Queue no longer empty!");
+                while (queue.Count == 0) {
+                    Console.WriteLine("Queue is empty, waiting...");
+                    await Task.Delay(isInitialSync ? 100 : 2500);
                 }
-                while (queue.TryDequeue(out var queueEntry)) {
+
+                Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!");
+
+                int maxUpdates = 10;
+                isInitialSync = false;
+                while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) {
                     var (roomId, roomData) = queueEntry;
                     Console.WriteLine($"Dequeued room {roomId}");
                     RoomInfo room;
-                    
+
                     if (Rooms.Any(x => x.Room.RoomId == roomId)) {
                         room = Rooms.First(x => x.Room.RoomId == roomId);
                         Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events");
@@ -146,26 +165,23 @@
                         };
                         Rooms.Add(room);
                     }
-                    
+
                     if (room.StateEvents is null) {
                         Console.WriteLine($"QueueWorker: {roomId} does not have state events on record?");
                         throw new InvalidDataException("Somehow this is null???");
                     }
-                    if (roomData.State?.Events is {Count: >0 })
+
+                    if (roomData.State?.Events is { Count: > 0 })
                         room.StateEvents.MergeStateEventLists(roomData.State.Events);
                     else {
                         Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!");
                     }
-                    if (Random.Shared.Next(101) < 20 || true) {
-                        Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue...";
-                    }
-                    RenderContents |= queue.Count == 0;
-                    if (queue.Count > 10) RenderContents = false;
-                    await Task.Delay(RenderContents ? 25 : 6);
                 }
-                // else {
-                    // Console.WriteLine("Failed to dequeue item");
-                // }
+                Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}");
+                    Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue...";
+
+                RenderContents |= queue.Count == 0;
+                await Task.Delay(Rooms.Count);
             }
             catch (Exception e) {
                 Console.WriteLine("QueueWorker exception: " + e);
@@ -214,11 +230,15 @@
                         // We can't trust servers to give us what we ask for, and this ruins performance
                         // Thanks, Conduit.
                         joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false);
-                        if(filter.Room?.State?.NotSenders?.Any() ?? false)
+                        if (filter.Room?.State?.NotSenders?.Any() ?? false)
                             joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender) ?? false);
-                        
+
                         queue.Enqueue(joinedRoom);
                     }
+            if (sync.Rooms.Leave is {Count: > 0})
+                foreach (var leftRoom in sync.Rooms.Leave)
+                    if (Rooms.Any(x => x.Room.RoomId == leftRoom.Key))
+                        Rooms.Remove(Rooms.First(x => x.Room.RoomId == leftRoom.Key));
 
             Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue... " +
                      $"{sync?.Rooms?.Join?.Count ?? 0} new updates!";
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
index 846d1cb..dbe0648 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
@@ -4,184 +4,199 @@
 @using ArcaneLibs.Extensions
 @using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.EventTypes.Spec.State.Policy
+@using System.Diagnostics
+@using System.Diagnostics.CodeAnalysis
+@using LibMatrix.Extensions
+@using LibMatrix.Responses
 <h3>Policy list editor - Editing @RoomId</h3>
 <hr/>
 
 <p>
-    This policy list contains @PolicyEvents.Count(x => x.Type == "m.policy.rule.server") server bans,
-    @PolicyEvents.Count(x => x.Type == "m.policy.rule.room") room bans and
-    @PolicyEvents.Count(x => x.Type == "m.policy.rule.user") user bans.
+    This policy list contains @GetPolicyCount(typeof(ServerPolicyRuleEventContent)) server bans,
+    @GetPolicyCount(typeof(RoomPolicyRuleEventContent)) room bans and
+    @GetPolicyCount(typeof(UserPolicyRuleEventContent)) user bans.
+    @foreach (var (key, value) in PolicyEventsByType) {
+        <p>@key.Name: @value.Count</p>
+    }
 </p>
-<InputCheckbox @bind-Value="_enableAvatars" @oninput="GetAllAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label>
-
+<InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label>
 
-@if (!PolicyEvents.Any(x => x.Type == "m.policy.rule.server")) {
+<h3>Server policies</h3>
+<hr/>
+@if (!GetPolicyEventsByType(typeof(ServerPolicyRuleEventContent)).Any()) {
     <p>No server policies</p>
 }
 else {
-    <h3>Server policies</h3>
-    <hr/>
-    <table class="table table-striped table-hover" style="width: fit-Content;">
+    <table class="table table-striped table-hover" style="width: fit-content;">
         <thead>
-        <tr>
-            <th scope="col" style="max-width: 50vw;">Server</th>
-            <th scope="col">Reason</th>
-            <th scope="col">Expires</th>
-            <th scope="col">Actions</th>
-        </tr>
-        </thead>
-        <tbody>
-        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && (x.TypedContent as PolicyRuleEventContent).Entity is not null)) {
-            var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
             <tr>
-                <td>Entity: @policyData.Entity<br/>State: @policyEvent.StateKey</td>
-                <td>@policyData.Reason</td>
-                <td>
-                    @policyData.ExpiryDateTime
-                </td>
-                <td>
-                    <button class="btn" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Edit</button>
-                    @* <button class="btn btn-danger" $1$ @onclick="async () => await RemovePolicyAsync(policyEvent)" #1#>Remove</button> *@
-                </td>
+                <th style="max-width: 50vw;">Server</th>
+                <th>Reason</th>
+                <th>Expires</th>
+                <th>Actions</th>
             </tr>
-        }
+        </thead>
+        <tbody>
+            @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) {
+                var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
+                <tr>
+                    <td>
+                        <span>Entity: @policyData.Entity</span>
+                        <span><br/>State: @policyEvent.StateKey</span>
+                    </td>
+                    <td>@policyData.Reason</td>
+                    <td>
+                        @policyData.ExpiryDateTime
+                    </td>
+                    <td>
+                        <button class="btn" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Edit</button>
+                        @* <button class="btn btn-danger" $1$ @onclick="async () => await RemovePolicyAsync(policyEvent)" #1#>Remove</button> *@
+                    </td>
+                </tr>
+            }
         </tbody>
     </table>
     <details>
-        <summary>Redacted events</summary>
-        <table class="table table-striped table-hover" style="width: fit-Content;">
+        <summary>Redacted or invalid events</summary>
+        <table class="table table-striped table-hover" style="width: fit-content;">
             <thead>
-            <tr>
-                <th scope="col" style="max-width: 50vw;">State key</th>
-                <th scope="col">Serialised Contents</th>
-            </tr>
-            </thead>
-            <tbody>
-            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && (x.TypedContent as PolicyRuleEventContent).Entity == null)) {
-                var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
                 <tr>
-                    <td>@policyEvent.StateKey</td>
-                    <td>@policyEvent.RawContent.ToJson(false, true)</td>
+                    <th style="max-width: 50vw;">State key</th>
+                    <th>Serialised Contents</th>
                 </tr>
-            }
+            </thead>
+            <tbody>
+                @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) {
+                    <tr>
+                        <td>@policyEvent.StateKey</td>
+                        <td>@policyEvent.RawContent.ToJson(false, true)</td>
+                    </tr>
+                }
             </tbody>
         </table>
     </details>
 }
-@if (!PolicyEvents.Any(x => x.Type == "m.policy.rule.room")) {
+<h3>Room policies</h3>
+<hr/>
+@if (!GetPolicyEventsByType(typeof(RoomPolicyRuleEventContent)).Any()) {
     <p>No room policies</p>
 }
 else {
-    <h3>Room policies</h3>
-    <hr/>
-    <table class="table table-striped table-hover" style="width: fit-Content;">
+    <table class="table table-striped table-hover" style="width: fit-content;">
         <thead>
-        <tr>
-            <th scope="col" style="max-width: 50vw;">Room</th>
-            <th scope="col">Reason</th>
-            <th scope="col">Expires</th>
-            <th scope="col">Actions</th>
-        </tr>
-        </thead>
-        <tbody>
-        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && (x.TypedContent as PolicyRuleEventContent).Entity is not null)) {
-            var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
             <tr>
-                <td>Entity: @policyData.Entity<br/>State: @policyEvent.StateKey</td>
-                <td>@policyData.Reason</td>
-                <td>
-                    @policyData.ExpiryDateTime
-                </td>
-                <td>
-                    <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
-                </td>
+                <th style="max-width: 50vw;">Room</th>
+                <th>Reason</th>
+                <th>Expires</th>
+                <th>Actions</th>
             </tr>
-        }
+        </thead>
+        <tbody>
+            @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) {
+                var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
+                <tr>
+                    <td>Entity: @policyData.Entity<br/>State: @policyEvent.StateKey</td>
+                    <td>@policyData.Reason</td>
+                    <td>
+                        @policyData.ExpiryDateTime
+                    </td>
+                    <td>
+                        <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
+                    </td>
+                </tr>
+            }
         </tbody>
     </table>
     <details>
-        <summary>Redacted events</summary>
-        <table class="table table-striped table-hover" style="width: fit-Content;">
+        <summary>Redacted or invalid events</summary>
+        <table class="table table-striped table-hover" style="width: fit-content;">
             <thead>
-            <tr>
-                <th scope="col" style="max-width: 50vw;">State key</th>
-                <th scope="col">Serialised Contents</th>
-            </tr>
-            </thead>
-            <tbody>
-            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && (x.TypedContent as PolicyRuleEventContent).Entity == null)) {
                 <tr>
-                    <td>@policyEvent.StateKey</td>
-                    <td>@policyEvent.RawContent!.ToJson(false, true)</td>
+                    <th style="max-width: 50vw;">State key</th>
+                    <th>Serialised Contents</th>
                 </tr>
-            }
+            </thead>
+            <tbody>
+                @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) {
+                    <tr>
+                        <td>@policyEvent.StateKey</td>
+                        <td>@policyEvent.RawContent!.ToJson(false, true)</td>
+                    </tr>
+                }
             </tbody>
         </table>
     </details>
 }
-@if (!PolicyEvents.Any(x => x.Type == "m.policy.rule.user")) {
+<h3>User policies</h3>
+<hr/>
+@if (!GetPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Any()) {
     <p>No user policies</p>
 }
 else {
-    <h3>User policies</h3>
-    <hr/>
-    <table class="table table-striped table-hover" style="width: fit-Content;">
+    <table class="table table-striped table-hover" style="width: fit-content;">
         <thead>
-        <tr>
-            @if (_enableAvatars) {
-                <th scope="col"></th>
-            }
-            <th scope="col" style="max-width: 0.2vw; word-wrap: anywhere;">User</th>
-            <th scope="col">Reason</th>
-            <th scope="col">Expires</th>
-            <th scope="col">Actions</th>
-        </tr>
-        </thead>
-        <tbody>
-        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && (x.TypedContent as PolicyRuleEventContent).Entity is not null)) {
-            var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
             <tr>
-                @if (_enableAvatars) {
-                    <td scope="col">
-                        <img style="width: 48px; height: 48px; aspect-ratio: unset; border-radius: 50%;" src="@(avatars.ContainsKey(policyData.Entity) ? avatars[policyData.Entity] : "")"/>
-                    </td>
+                @if (EnableAvatars) {
+                    <th></th>
                 }
-                <td style="word-wrap: anywhere;">Entity: @string.Join("", policyData.Entity.Take(64))<br/>State: @string.Join("", policyEvent.StateKey.Take(64))</td>
-                <td>@policyData.Reason</td>
-                <td>
-                    @policyData.ExpiryDateTime
-                </td>
-                <td>
-                    <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
-                </td>
+                <th style="max-width: 0.2vw; word-wrap: anywhere;">User</th>
+                <th>Reason</th>
+                <th>Expires</th>
+                <th>Actions</th>
             </tr>
-        }
+        </thead>
+        <tbody>
+            @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) {
+                var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
+                <tr>
+                    @if (EnableAvatars) {
+                        <td>
+                            @if (Avatars.ContainsKey(policyData.Entity)) {
+                                <img class="avatar48" src="@Avatars[policyData.Entity]"/>
+                            }
+                        </td>
+                    }
+                    <td style="word-wrap: anywhere;">Entity: @string.Join("", policyData.Entity.Take(64))<br/>State: @string.Join("", policyEvent.StateKey.Take(64))</td>
+                    <td>@policyData.Reason</td>
+                    <td>
+                        @policyData.ExpiryDateTime
+                    </td>
+                    <td>
+                        <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
+                    </td>
+                </tr>
+            }
         </tbody>
     </table>
     <details>
-        <summary>Redacted events</summary>
-        <table class="table table-striped table-hover" style="width: fit-Content;">
+        <summary>Redacted or invalid events</summary>
+        <table class="table table-striped table-hover" style="width: fit-content;">
             <thead>
-            <tr>
-                <th scope="col">State key</th>
-                <th scope="col">Serialised Contents</th>
-            </tr>
-            </thead>
-            <tbody>
-            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && (x.TypedContent as PolicyRuleEventContent).Entity == null)) {
                 <tr>
-                    <td>@policyEvent.StateKey</td>
-                    <td>@policyEvent.RawContent.ToJson(false, true)</td>
+                    <th>State key</th>
+                    <th>Serialised Contents</th>
                 </tr>
-            }
+            </thead>
+            <tbody>
+                @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) {
+                    <tr>
+                        <td>@policyEvent.StateKey</td>
+                        <td>@policyEvent.RawContent.ToJson(false, true)</td>
+                    </tr>
+                }
             </tbody>
         </table>
     </details>
 }
 
-<LogView></LogView>
-
 @code {
+
+#if DEBUG
+    private const bool Debug = true;
+#else
+    private const bool Debug = false;
+#endif
+
     //get room list
     // - sync withroom list filter
     // Type = support.feline.msc3784
@@ -192,18 +207,28 @@ else {
 
     private bool _enableAvatars;
 
-    static readonly Dictionary<string, string?> avatars = new();
-    static readonly Dictionary<string, RemoteHomeserver> servers = new();
+    static readonly Dictionary<string, string?> Avatars = new();
+    // static readonly Dictionary<string, RemoteHomeserver> Servers = new();
 
-    public static List<StateEventResponse> PolicyEvents { get; set; } = new();
+    // private static List<StateEventResponse> PolicyEvents { get; set; } = new();
+    private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
+
+    public bool EnableAvatars {
+        get => _enableAvatars;
+        set {
+            _enableAvatars = value;
+            if (value) GetAllAvatars();
+        }
+    }
 
     protected override async Task OnInitializedAsync() {
+        var sw = Stopwatch.StartNew();
         await base.OnInitializedAsync();
         var hs = await MRUStorage.GetCurrentSessionOrNavigate();
         if (hs is null) return;
         RoomId = RoomId.Replace('~', '.');
         await LoadStatesAsync();
-        Console.WriteLine("Policy list editor initialized!");
+        Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
     }
 
     private async Task LoadStatesAsync() {
@@ -214,39 +239,52 @@ else {
 
         var states = room.GetFullStateAsync();
         await foreach (var state in states) {
-            if (!state.Type.StartsWith("m.policy.rule")) continue;
-            PolicyEvents.Add(state);
+            if (state is null) continue;
+            if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
+            if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new());
+            PolicyEventsByType[state.MappedType].Add(state);
         }
 
-
-        // var stateEventsQuery = await room.GetStateAsync("");
-        // var stateEvents = stateEventsQuery.Value.Deserialize<List<StateEventResponse>>();
-        // PolicyEvents = stateEvents.Where(x => x.Type.StartsWith("m.policy.rule"))
-            // .Select(x => JsonSerializer.Deserialize<StateEventResponse>(JsonSerializer.Serialize(x))).ToList();
         StateHasChanged();
     }
 
-    private async Task GetAvatar(string userId) {
-        try {
-            if (avatars.ContainsKey(userId)) return;
-            var hs = userId.Split(':')[1];
-            var server = servers.ContainsKey(hs) ? servers[hs] : new RemoteHomeserver(userId.Split(':')[1]);
-            if (!servers.ContainsKey(hs)) servers.Add(hs, server);
-            var profile = await server.GetProfileAsync(userId);
-            avatars.Add(userId, await hsResolver.ResolveMediaUri(server.BaseUrl, profile.AvatarUrl));
-            servers.Add(userId, server);
+    private async Task GetAllAvatars() {
+        // if (!_enableAvatars) return;
+        Console.WriteLine("Getting avatars...");
+        var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
+        Console.WriteLine($"Got {users.Count} users!");
+        var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
+        Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
+        var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
+        await foreach (var server in homeserverTasks) {
+            if (server is null) continue;
+            var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
+            await Task.WhenAll(profileTasks);
+            profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
+            foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
+                // if (profile is null) continue;
+                if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
+                    var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
+                    Avatars.TryAdd(profile.Key, url);
+                }
+                else Avatars.TryAdd(profile.Key, null);
+            }
             StateHasChanged();
         }
-        catch {
-    // ignored
-        }
     }
 
-    private async Task GetAllAvatars() {
-        foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && (x.TypedContent as PolicyRuleEventContent).Entity is not null)) {
-            await GetAvatar((policyEvent.TypedContent as PolicyRuleEventContent).Entity);
+    private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
+        try {
+            return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
+        }
+        catch {
+            return null;
         }
-        StateHasChanged();
     }
 
-}
+    private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+    private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+    private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+    private int GetPolicyCount(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type].Count : 0;
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/DMManager.razor b/MatrixRoomUtils.Web/Pages/User/DMManager.razor
index 1b28516..a327793 100644
--- a/MatrixRoomUtils.Web/Pages/User/DMManager.razor
+++ b/MatrixRoomUtils.Web/Pages/User/DMManager.razor
@@ -1,7 +1,7 @@
 @page "/User/DirectMessages"
-@using LibMatrix.Homeservers
 @using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.Responses
+@using MatrixRoomUtils.Abstractions
 <h3>Direct Messages</h3>
 <hr/>
 
diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor
index 553f46d..60c68ac 100644
--- a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor
+++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor
@@ -7,6 +7,7 @@
 @using MatrixRoomUtils.LibDMSpace.StateEvents
 @using ArcaneLibs.Extensions
 @using System.Text.Json.Serialization
+@using MatrixRoomUtils.Abstractions
 <b>
     <u>DM Space setup tool - stage 2: Fix DM room attribution</u>
 </b>
@@ -185,9 +186,9 @@ else {
         }
         catch { }
 
-        var membersEnum = room.GetMembersAsync();
+        var membersEnum = room.GetMembersEnumerableAsync(true);
         await foreach (var member in membersEnum)
-            if (member.TypedContent is RoomMemberEventContent memberEvent && !string.IsNullOrWhiteSpace(memberEvent.Membership) && memberEvent.Membership == "join")
+            if (member.TypedContent is RoomMemberEventContent memberEvent)
                 roomMembers[roomInfo].Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey });
 
         if (string.IsNullOrWhiteSpace(roomInfo.RoomName) || roomInfo.RoomName == room.RoomId) {
diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor
index 854b09c..42573e6 100644
--- a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor
+++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor
@@ -7,6 +7,8 @@
 @using MatrixRoomUtils.LibDMSpace.StateEvents
 @using ArcaneLibs.Extensions
 @using System.Text.Json.Serialization
+@using MatrixRoomUtils.Abstractions
+
 <b>
     <u>DM Space setup tool - stage 3: Preview space layout</u>
 </b>
@@ -154,9 +156,9 @@ else {
         }
         catch { }
 
-        var membersEnum = room.GetMembersAsync();
+        var membersEnum = room.GetMembersEnumerableAsync(true);
         await foreach (var member in membersEnum)
-            if (member.TypedContent is RoomMemberEventContent memberEvent && !string.IsNullOrWhiteSpace(memberEvent.Membership) && memberEvent.Membership == "join")
+            if (member.TypedContent is RoomMemberEventContent memberEvent)
                 roomMembers.Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey });
 
         if (string.IsNullOrWhiteSpace(roomInfo.RoomName) || roomInfo.RoomName == room.RoomId) {
diff --git a/MatrixRoomUtils.Web/Pages/User/Profile.razor b/MatrixRoomUtils.Web/Pages/User/Profile.razor
index ae3fb76..73d7c6e 100644
--- a/MatrixRoomUtils.Web/Pages/User/Profile.razor
+++ b/MatrixRoomUtils.Web/Pages/User/Profile.razor
@@ -9,40 +9,43 @@
 @if (NewProfile is not null) {
     <h4>Profile</h4>
     <hr/>
-
-    <img src="@Homeserver.ResolveMediaUri(NewProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/>
-    <div style="display: inline-block; vertical-align: middle;">
-        <span>Display name: </span><FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox><br/>
-        <span>Avatar URL: </span><FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox>
-        <InputFile OnChange="@AvatarChanged"></InputFile><br/>
-        <LinkButton OnClick="@(() => UpdateProfile())">Update profile</LinkButton>
-        <LinkButton OnClick="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton>
+    <div>
+        <img src="@Homeserver.ResolveMediaUri(NewProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/>
+        <div style="display: inline-block; vertical-align: middle;">
+            <span>Display name: </span><FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox><br/>
+            <span>Avatar URL: </span><FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox>
+            <InputFile OnChange="@AvatarChanged"></InputFile><br/>
+            <LinkButton OnClick="@(() => UpdateProfile())">Update profile</LinkButton>
+            <LinkButton OnClick="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton>
+        </div>
     </div>
     @if (!string.IsNullOrWhiteSpace(Status)) {
         <p>@Status</p>
     }
 
-    <details>
-        <summary style="font-size: 1.5rem;">Room profiles<hr></summary>
-
-        @foreach (var (roomId, roomProfile) in RoomProfiles.OrderBy(x=>RoomNames.TryGetValue(x.Key, out var _name) ? _name : x.Key)) {
-            <details class="details-compact">
-                <summary style="@(roomProfile.DisplayName == OldProfile.DisplayName && roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")">@(RoomNames.TryGetValue(roomId, out var name) ? name : roomId)</summary>
-                <img src="@Homeserver.ResolveMediaUri(roomProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/>
-                <div style="display: inline-block; vertical-align: middle;">
-                    <span>Display name: </span><FancyTextBox BackgroundColor="@(roomProfile.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" @bind-Value="@roomProfile.DisplayName"></FancyTextBox><br/>
-                    <span>Avatar URL: </span><FancyTextBox BackgroundColor="@(roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" @bind-Value="@roomProfile.AvatarUrl"></FancyTextBox>
-                    <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, roomId))"></InputFile><br/>
-                    <LinkButton OnClick="@(() => UpdateRoomProfile(roomId))">Update profile</LinkButton>
-                </div>
-                <br/>
-                @if (!string.IsNullOrWhiteSpace(Status)) {
-                    <p>@Status</p>
-                }
-            </details>
+    <br/>
+
+    @* <details> *@
+    <h4>Room profiles<hr></h4>
+
+    @foreach (var (roomId, roomProfile) in RoomProfiles.OrderBy(x => RoomNames.TryGetValue(x.Key, out var _name) ? _name : x.Key)) {
+        <details class="details-compact">
+            <summary style="@(roomProfile.DisplayName == OldProfile.DisplayName && roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")">@(RoomNames.TryGetValue(roomId, out var name) ? name : roomId)</summary>
+            <img src="@Homeserver.ResolveMediaUri(roomProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/>
+            <div style="display: inline-block; vertical-align: middle;">
+                <span>Display name: </span><FancyTextBox BackgroundColor="@(roomProfile.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" @bind-Value="@roomProfile.DisplayName"></FancyTextBox><br/>
+                <span>Avatar URL: </span><FancyTextBox BackgroundColor="@(roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" @bind-Value="@roomProfile.AvatarUrl"></FancyTextBox>
+                <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, roomId))"></InputFile><br/>
+                <LinkButton OnClick="@(() => UpdateRoomProfile(roomId))">Update profile</LinkButton>
+            </div>
             <br/>
-        }
-    </details>
+            @if (!string.IsNullOrWhiteSpace(Status)) {
+                <p>@Status</p>
+            }
+        </details>
+        <br/>
+    }
+    // </details>
 }
 
 @code {
@@ -54,7 +57,10 @@
 
     private string? Status {
         get => _status;
-        set { _status = value; StateHasChanged(); }
+        set {
+            _status = value;
+            StateHasChanged();
+        }
     }
 
     private Dictionary<string, RoomMemberEventContent> RoomProfiles { get; set; } = new();
@@ -65,14 +71,15 @@
         if (Homeserver is null) return;
         Status = "Loading global profile...";
         if (Homeserver.WhoAmI?.UserId is null) return;
-        NewProfile = (await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId)).DeepClone();
-        OldProfile = (await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId)).DeepClone();
+        NewProfile = (await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId)); //.DeepClone();
+        OldProfile = (await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId)); //.DeepClone();
         Status = "Loading room profiles...";
         var roomProfiles = Homeserver.GetRoomProfilesAsync();
         await foreach (var (roomId, roomProfile) in roomProfiles) {
             // Status = $"Got profile for {roomId}...";
-            RoomProfiles[roomId] = roomProfile.DeepClone();
+            RoomProfiles[roomId] = roomProfile; //.DeepClone();
         }
+
         StateHasChanged();
         Status = "Room profiles loaded, loading room names...";
 
@@ -80,6 +87,7 @@
             var name = await x.GetNameOrFallbackAsync();
             return new KeyValuePair<string, string?>(x.RoomId, name);
         }).ToAsyncEnumerable();
+        
         await foreach (var (roomId, roomName) in roomNameTasks) {
             // Status = $"Got room name for {roomId}: {roomName}";
             RoomNames[roomId] = roomName;
@@ -106,13 +114,14 @@
         StateHasChanged();
         await OnInitializedAsync();
     }
+
     private async Task RoomAvatarChanged(InputFileChangeEventArgs arg, string roomId) {
         var res = await Homeserver.UploadFile(arg.File.Name, arg.File.OpenReadStream(Int64.MaxValue), arg.File.ContentType);
         Console.WriteLine(res);
         RoomProfiles[roomId].AvatarUrl = res;
         StateHasChanged();
     }
-    
+
     private async Task UpdateRoomProfile(string roomId) {
         Status = "Busy processing room profile update, please do not leave this page...";
         StateHasChanged();
@@ -122,5 +131,4 @@
         StateHasChanged();
     }
 
-}
-
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomList.razor b/MatrixRoomUtils.Web/Shared/RoomList.razor
index f78c7f7..31f0430 100644
--- a/MatrixRoomUtils.Web/Shared/RoomList.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomList.razor
@@ -5,6 +5,7 @@
 @using LibMatrix.EventTypes.Spec.State
 @using System.Collections.ObjectModel
 @using LibMatrix.Responses
+@using MatrixRoomUtils.Abstractions
 @using _Imports = MatrixRoomUtils.Web._Imports
 @if (!StillFetching) {
     <p>Fetching room details... @RoomsWithTypes.Sum(x => x.Value.Count) out of @Rooms.Count done!</p>
@@ -34,18 +35,23 @@ else {
 
     private Dictionary<string, List<RoomInfo>> RoomsWithTypes => Rooms is null ? new() : Rooms.GroupBy(x => GetRoomTypeName(x.CreationEventContent?.Type)).ToDictionary(x => x.Key, x => x.ToList());
 
+    private bool hooked;
     protected override async Task OnParametersSetAsync() {
         var hs = await MRUStorage.GetCurrentSessionOrNavigate();
         if (hs is null) return;
-        Rooms.CollectionChanged += (_, args) => {
-            foreach (RoomInfo item in args.NewItems) {
-                item.PropertyChanged += (_, args2) => {
-                    Console.WriteLine(args2);
-                    if(args2.PropertyName == nameof(item.CreationEventContent))
-                        StateHasChanged();
-                };
-            }
-        };
+        if (!hooked) {
+            Rooms.CollectionChanged += (_, args) => {
+                foreach (RoomInfo item in args.NewItems) {
+                    item.PropertyChanged += (_, args2) => {
+                        // Console.WriteLine(args2);
+                        
+                        if (args2.PropertyName == nameof(item.CreationEventContent))
+                            StateHasChanged();
+                    };
+                }
+            };
+            hooked = true;
+        }
 
         // GlobalProfile ??= await hs.GetProfileAsync(hs.WhoAmI.UserId);
 
@@ -53,10 +59,10 @@ else {
     }
 
     private string GetRoomTypeName(string? roomType) => roomType switch {
+        null => "Room",
         "m.space" => "Space",
         "msc3588.stories.stories-room" => "Story room",
         "support.feline.policy.lists.msc.v1" => "MSC3784 Policy list (v1)",
-        null => "Room",
         _ => roomType
         };
 
@@ -88,5 +94,4 @@ else {
     //     _semaphoreSlim.Release();
     // }
 
-}
-
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
index 55ffc1e..4db25e1 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
@@ -3,6 +3,7 @@
 @using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.Homeservers
 @using LibMatrix.Responses
+@using MatrixRoomUtils.Abstractions
 <details>
     <summary>@RoomType (@Rooms.Count)</summary>
     @foreach (var room in Rooms) {
@@ -42,17 +43,19 @@
     private List<RoomInfo> Rooms => Category.Value;
 
     private int RoomVersionDangerLevel(RoomInfo room) {
-        var roomVersion = room.StateEvents.FirstOrDefault(x => x.Type == "m.room.create");
-        if (roomVersion is null) return 0;
-        return roomVersion.TypedContent is not RoomCreateEventContent roomVersionContent ? 0
+        var creationEvent = room.StateEvents.FirstOrDefault(x => x?.Type == "m.room.create");
+        if (creationEvent is null) return 0;
+        return creationEvent.TypedContent is not RoomCreateEventContent roomVersionContent ? 0
             : RoomConstants.DangerousRoomVersions.Contains(roomVersionContent.RoomVersion) ? 2
                 : roomVersionContent.RoomVersion != RoomConstants.RecommendedRoomVersion ? 1 : 0;
     }
     
     public static string GetRoomTypeName(string roomType) {
         return roomType switch {
-            "Room" => "Rooms",
-            "org.matrix.mjolnir.policy" => "Policies",
+            null => "Room",
+            "m.space" => "Space",
+            "org.matrix.mjolnir.policy" => "Policy room",
+            
             _ => roomType
         };
     }
diff --git a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
index e08f98d..a6c006b 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
@@ -1,4 +1,5 @@
 @using System.Collections.ObjectModel
+@using MatrixRoomUtils.Abstractions
 <MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton href="@($"/Rooms/{Space.Room.RoomId}/Space")">Manage space</MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton>
 
 <br/>
diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
index 3aa28e6..07f0756 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListItem.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
@@ -5,6 +5,7 @@
 @using LibMatrix.Homeservers
 @using LibMatrix.Responses
 @using LibMatrix.RoomTypes
+@using MatrixRoomUtils.Abstractions
 @using MatrixRoomUtils.Web.Classes.Constants
 @if (RoomInfo is not null) {
     <div class="roomListItem @(HasDangerousRoomVersion ? "dangerousRoomVersion" : HasOldRoomVersion ? "oldRoomVersion" : "")" id="@RoomInfo.Room.RoomId">
@@ -70,12 +71,16 @@ else {
     private bool _loadData = false;
     private static AuthenticatedHomeserverGeneric? hs { get; set; }
 
+    private bool _hooked;
     protected override async Task OnParametersSetAsync() {
         if (RoomInfo != null) {
-            RoomInfo.PropertyChanged += (_, a) => {
-                Console.WriteLine(a.PropertyName);
-                StateHasChanged();
-            };
+            if (!_hooked) {
+                _hooked = true;
+                RoomInfo.PropertyChanged += (_, a) => {
+                    Console.WriteLine(a.PropertyName);
+                    StateHasChanged();
+                };
+            }
 
             if (LoadData) {
                 try {
diff --git a/MatrixRoomUtils.Web/wwwroot/css/app.css b/MatrixRoomUtils.Web/wwwroot/css/app.css
index 3baddbb..3fac9ca 100644
--- a/MatrixRoomUtils.Web/wwwroot/css/app.css
+++ b/MatrixRoomUtils.Web/wwwroot/css/app.css
@@ -1,8 +1,16 @@
 @import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
 @import url('jetbrains-mono/jetbrains-mono.css');
 
+.avatar48 {
+    width: 48px;
+    height: 48px;
+    aspect-ratio: unset;
+    border-radius: 50%;
+}
+
 .details-compact > summary {
-    line-height: 0px;
+    line-height: 0;
+    white-space: nowrap;
 }
 
 .details-compact[open] > summary {
diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln
index 58379b3..a26bbaa 100644
--- a/MatrixRoomUtils.sln
+++ b/MatrixRoomUtils.sln
@@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.LibDMSpace"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{1CAA2B6D-0365-4C8B-96EE-26026514FEE2}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.Abstractions", "MatrixRoomUtils.Abstractions\MatrixRoomUtils.Abstractions.csproj", "{FE20ED20-0D55-4D74-822B-E2AC7A54C487}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -126,6 +128,10 @@ Global
 		{1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
diff --git a/MatrixRoomUtils.sln.DotSettings.user b/MatrixRoomUtils.sln.DotSettings.user
index 6daed5b..7142e45 100644
--- a/MatrixRoomUtils.sln.DotSettings.user
+++ b/MatrixRoomUtils.sln.DotSettings.user
@@ -1,2 +1,9 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
-	<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">DoNotShowAndRun</s:String></wpf:ResourceDictionary>
\ No newline at end of file
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/SourceGeneratedFilesToSkip/=MatrixRoomUtils_002EDesktop_005CAvalonia_002EGenerators_002FAvalonia_002EGenerators_002ENameGenerator_002EAvaloniaNameSourceGenerator_002FMatrixRoomUtils_002EDesktop_002EComponents_002ENavigationStack_002Eg_002Ecs/@EntryIndexedValue">ExplicitlyExcluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/SourceGeneratedFilesToSkip/=MatrixRoomUtils_002EDesktop_005CAvalonia_002EGenerators_002FAvalonia_002EGenerators_002ENameGenerator_002EAvaloniaNameSourceGenerator_002FMatrixRoomUtils_002EDesktop_002EComponents_002ERoomListEntry_002Eg_002Ecs/@EntryIndexedValue">ExplicitlyExcluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">DoNotShowAndRun</s:String>
+	<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=eac3956f_002D7f23_002D4a95_002Dbcf9_002Dc3bb0c1010f5/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
+  &lt;Solution /&gt;
+&lt;/SessionState&gt;</s:String>
+	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=27c08a4f_002D5af0_002D4c2c_002Dafcb_002D050e3388c116_0023MatrixRoomUtils_002EDesktop/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=f997f26f_002D2ec1_002D4d18_002Db3dd_002Dc46fb2ad65c0_0023MatrixRoomUtils_002EWeb_002EServer/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
diff --git a/MxApiExtensions b/MxApiExtensions
-Subproject 0b662d36de30c4bdd3d9be97d08ace8d4d7be58
+Subproject 3c0c7b2e56a24bda06b8c567e5608546898c99d
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..d9402a4
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+  <packageSources>
+    <!-- optional -->
+    <clear />
+    <!-- optional -->
+    <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
+    <!-- nightly feed -->
+    <add key="avalonia-nightly" value="https://nuget-feed-nightly.avaloniaui.net/v3/index.json" protocolVersion="3" />
+  </packageSources>
+</configuration>