about summary refs log tree commit diff
path: root/MatrixRoomUtils.Web
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixRoomUtils.Web')
-rw-r--r--MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs4
-rw-r--r--MatrixRoomUtils.Web/Classes/RoomInfo.cs54
-rw-r--r--MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj2
-rw-r--r--MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor (renamed from MatrixRoomUtils.Web/Pages/DevOptions.razor)3
-rw-r--r--MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor (renamed from MatrixRoomUtils.Web/Pages/DebugTools.razor)3
-rw-r--r--MatrixRoomUtils.Web/Pages/Dev/ModalTest.razor (renamed from MatrixRoomUtils.Web/Pages/ModalTest.razor)2
-rw-r--r--MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/Index.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/InvalidSession.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/LoginPage.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Create.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Index.razor264
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor3
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Space.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/StateEditor.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/StateViewer.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor (renamed from MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor)2
-rw-r--r--MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor (renamed from MatrixRoomUtils.Web/Pages/MediaLocator.razor)1
-rw-r--r--MatrixRoomUtils.Web/Pages/Tools/SpaceDebug.razor (renamed from MatrixRoomUtils.Web/Pages/SpaceDebug.razor)1
-rw-r--r--MatrixRoomUtils.Web/Shared/NavMenu.razor49
-rw-r--r--MatrixRoomUtils.Web/Shared/PortableDevTools.razor26
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomList.razor89
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor5
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListItem.razor143
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListItem.razor.css56
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/UserListItem.razor30
32 files changed, 426 insertions, 330 deletions
diff --git a/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs b/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs
index 8ea85e9..0947bbe 100644
--- a/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs
+++ b/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs
@@ -1,6 +1,5 @@
 using LibMatrix;
 using LibMatrix.Homeservers;
-using LibMatrix.Responses;
 using LibMatrix.Services;
 using Microsoft.AspNetCore.Components;
 
@@ -22,6 +21,9 @@ public class MRUStorageWrapper {
     }
 
     public async Task<List<UserAuth>?> GetAllTokens() {
+        if (!await _storageService.DataStorageProvider.ObjectExistsAsync("mru.tokens")) {
+            
+        }
         return await _storageService.DataStorageProvider.LoadObjectAsync<List<UserAuth>>("mru.tokens") ??
                new List<UserAuth>();
     }
diff --git a/MatrixRoomUtils.Web/Classes/RoomInfo.cs b/MatrixRoomUtils.Web/Classes/RoomInfo.cs
index 0e21871..31943dc 100644
--- a/MatrixRoomUtils.Web/Classes/RoomInfo.cs
+++ b/MatrixRoomUtils.Web/Classes/RoomInfo.cs
@@ -1,13 +1,15 @@
+using System.Collections.ObjectModel;
+using ArcaneLibs;
 using LibMatrix;
+using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.Interfaces;
-using LibMatrix.Responses;
 using LibMatrix.RoomTypes;
 
 namespace MatrixRoomUtils.Web.Classes;
 
-public class RoomInfo {
+public class RoomInfo : NotifyPropertyChanged {
     public GenericRoom Room { get; set; }
-    public List<StateEventResponse?> StateEvents { get; init; } = new();
+    public ObservableCollection<StateEventResponse?> StateEvents { get; } = new();
 
     public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") {
         var @event = StateEvents.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey);
@@ -28,4 +30,48 @@ public class RoomInfo {
         StateEvents.Add(@event);
         return @event;
     }
-}
+
+    public string? RoomIcon {
+        get => _roomIcon ?? "https://api.dicebear.com/6.x/identicon/svg?seed=" + Room.RoomId;
+        set => SetField(ref _roomIcon, value);
+    }
+
+    public string? RoomName {
+        get => _roomName ?? Room.RoomId;
+        set => SetField(ref _roomName, value);
+    }
+
+    public RoomCreateEventContent? CreationEventContent {
+        get => _creationEventContent;
+        set => SetField(ref _creationEventContent, value);
+    }
+
+    public string? RoomCreator {
+        get => _roomCreator;
+        set => SetField(ref _roomCreator, value);
+    }
+
+    // public string? GetRoomIcon() => (StateEvents.FirstOrDefault(x => x?.Type == RoomAvatarEventContent.EventId)?.TypedContent as RoomAvatarEventContent)?.Url ??
+    // "mxc://rory.gay/dgP0YPjJEWaBwzhnbyLLwGGv";
+
+    private string? _roomIcon;
+    private string? _roomName;
+    private RoomCreateEventContent? _creationEventContent;
+    private string? _roomCreator;
+
+    public RoomInfo() {
+        StateEvents.CollectionChanged += (_, args) => {
+            if (args.NewItems is { Count: > 0 })
+                foreach (StateEventResponse newState in args.NewItems) {
+                    if (newState.TypedContent is RoomNameEventContent roomNameContent)
+                        RoomName = roomNameContent.Name;
+                    else if (newState.TypedContent is RoomAvatarEventContent roomAvatarContent)
+                        RoomIcon = roomAvatarContent.Url;
+                    else if (newState.TypedContent is RoomCreateEventContent roomCreateContent) {
+                        CreationEventContent = roomCreateContent;
+                        RoomCreator = newState.Sender;
+                    }
+                }
+        };
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
index 03cc9ae..625a303 100644
--- a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
+++ b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
@@ -21,8 +21,10 @@
         <ProjectReference Condition="Exists('..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" />
         <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="ArcaneLibs" Version="*-preview*" />
         <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+
     </ItemGroup>
 
 
 
+
 </Project>
diff --git a/MatrixRoomUtils.Web/Pages/DevOptions.razor b/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor
index 8511a26..9b0f61c 100644
--- a/MatrixRoomUtils.Web/Pages/DevOptions.razor
+++ b/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor
@@ -1,5 +1,4 @@
-@page "/DevOptions"
-@using LibMatrix.Extensions
+@page "/Dev/Options"
 @using ArcaneLibs.Extensions
 @inject NavigationManager NavigationManager
 @inject ILocalStorageService LocalStorage
diff --git a/MatrixRoomUtils.Web/Pages/DebugTools.razor b/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
index 5d47277..f7e6aec 100644
--- a/MatrixRoomUtils.Web/Pages/DebugTools.razor
+++ b/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
@@ -1,7 +1,6 @@
-@page "/Debug"
+@page "/Dev/Utilities"
 @using System.Reflection
 @using ArcaneLibs.Extensions
-@using LibMatrix
 @using LibMatrix.Extensions
 @using LibMatrix.Homeservers
 @inject ILocalStorageService LocalStorage
diff --git a/MatrixRoomUtils.Web/Pages/ModalTest.razor b/MatrixRoomUtils.Web/Pages/Dev/ModalTest.razor
index 1d14005..4a0487f 100644
--- a/MatrixRoomUtils.Web/Pages/ModalTest.razor
+++ b/MatrixRoomUtils.Web/Pages/Dev/ModalTest.razor
@@ -1,4 +1,4 @@
-@page "/ModalTest"
+@page "/Dev/ModalTest"
 @inject IJSRuntime JsRuntime
 <h3>ModalTest</h3>
 
diff --git a/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor b/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
index a4f9d97..7e4058b 100644
--- a/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
+++ b/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
@@ -1,8 +1,6 @@
 @page "/HSAdmin/RoomQuery"
 @using LibMatrix.Responses.Admin
 @using LibMatrix.Filters
-@using LibMatrix.Extensions
-@using LibMatrix
 @using LibMatrix.Homeservers
 @using ArcaneLibs.Extensions
 
diff --git a/MatrixRoomUtils.Web/Pages/Index.razor b/MatrixRoomUtils.Web/Pages/Index.razor
index 834c373..00f3253 100644
--- a/MatrixRoomUtils.Web/Pages/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Index.razor
@@ -1,7 +1,6 @@
 @page "/"
 @using LibMatrix.Responses
 @using LibMatrix
-@using LibMatrix.Helpers
 @using LibMatrix.Homeservers
 @using ArcaneLibs.Extensions
 
diff --git a/MatrixRoomUtils.Web/Pages/InvalidSession.razor b/MatrixRoomUtils.Web/Pages/InvalidSession.razor
index b476c68..7d4769c 100644
--- a/MatrixRoomUtils.Web/Pages/InvalidSession.razor
+++ b/MatrixRoomUtils.Web/Pages/InvalidSession.razor
@@ -1,5 +1,4 @@
 @page "/InvalidSession"
-@using LibMatrix.Responses
 @using LibMatrix
 
 <PageTitle>Invalid session</PageTitle>
diff --git a/MatrixRoomUtils.Web/Pages/LoginPage.razor b/MatrixRoomUtils.Web/Pages/LoginPage.razor
index c7f5922..1b466c9 100644
--- a/MatrixRoomUtils.Web/Pages/LoginPage.razor
+++ b/MatrixRoomUtils.Web/Pages/LoginPage.razor
@@ -1,6 +1,5 @@
 @page "/Login"
 @using System.Text.Json
-@using LibMatrix.Responses
 @inject ILocalStorageService LocalStorage
 @inject IJSRuntime JsRuntime
 <h3>Login</h3>
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Create.razor b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
index 3225862..04dcdcc 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
@@ -4,8 +4,6 @@
 @using ArcaneLibs.Extensions
 @using LibMatrix
 @using LibMatrix.EventTypes.Spec.State
-@using LibMatrix.Extensions
-@using LibMatrix.Helpers
 @using LibMatrix.Homeservers
 @using LibMatrix.Responses
 @using MatrixRoomUtils.Web.Classes.RoomCreationTemplates
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
index 69a0ede..4d98402 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
@@ -1,25 +1,32 @@
 @page "/Rooms"
 @using LibMatrix.Filters
 @using LibMatrix.Helpers
-@using LibMatrix.Responses
 @using LibMatrix.EventTypes.Spec.State
 @using LibMatrix
+@using LibMatrix.Homeservers
+@using ArcaneLibs.Extensions
+@using LibMatrix.Extensions
+@using LibMatrix.Responses
+@using System.Collections.ObjectModel
+@inject ILogger<Index> logger
 <h3>Room list</h3>
 
 <p>@Status</p>
-@if (RenderContents) {
-    <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile"></RoomList>
-}
-
+<p>@Status2</p>
+@* @if (RenderContents) { *@
+<RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents"></RoomList>
+@* } *@
+@* else { *@
+@* <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" StillFetching="true"></RoomList> *@
+@* } *@
 
 @code {
-
-    public List<RoomInfo> KnownRooms { get; set; } = new();
-
-    private List<RoomInfo> Rooms { get; set; } = new();
+    private ObservableCollection<RoomInfo> Rooms { get; } = new();
     private ProfileResponseEventContent GlobalProfile { get; set; }
 
-    private SyncFilter filter = new() {
+    private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
+    private static SyncFilter filter = new() {
         AccountData = new SyncFilter.EventFilter {
             NotTypes = new List<string> { "*" },
             Limit = 1
@@ -43,7 +50,7 @@
                     "m.room.avatar",
                     "m.room.create",
                     "org.matrix.mjolnir.shortcode",
-                    "m.room.power_levels"
+                    "m.room.power_levels",
                 }
             },
             Timeline = new SyncFilter.RoomFilter.StateFilter {
@@ -53,112 +60,115 @@
         }
     };
 
+    private static SyncFilter profileUpdateFilter = new() {
+        AccountData = new SyncFilter.EventFilter {
+            NotTypes = new List<string> { "*" },
+            Limit = 1
+        },
+        Presence = new SyncFilter.EventFilter {
+            NotTypes = new List<string> { "*" },
+            Limit = 1
+        },
+        Room = new SyncFilter.RoomFilter {
+            AccountData = new SyncFilter.RoomFilter.StateFilter {
+                NotTypes = new List<string> { "*" },
+                Limit = 1
+            },
+            Ephemeral = new SyncFilter.RoomFilter.StateFilter {
+                NotTypes = new List<string> { "*" },
+                Limit = 1
+            },
+            State = new SyncFilter.RoomFilter.StateFilter {
+                Types = new List<string> {
+                    "m.room.member"
+                },
+                Senders = new()
+            },
+            Timeline = new SyncFilter.RoomFilter.StateFilter {
+                NotTypes = new List<string> { "*" },
+                Limit = 1
+            }
+        }
+    };
+
     protected override async Task OnInitializedAsync() {
-        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
-        if (hs is null) return;
-        GlobalProfile = await hs.GetProfileAsync(hs.WhoAmI.UserId);
+        Homeserver = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (Homeserver is null) return;
+        var rooms = await Homeserver.GetJoinedRooms();
+        foreach (var room in rooms) {
+            Rooms.Add(new(){Room = room});
+        }
+        
+        GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId);
 
-        Status = "Syncing...";
-        var syncHelper = new SyncHelper(hs) {
-            Timeout = 0,
+        var syncHelper = new SyncHelper(Homeserver, logger) {
+            Timeout = 10000,
             Filter = filter
         };
-        // SyncResponse? sync = null;
-        string? nextBatch = null;
-        var syncs = syncHelper.EnumerateSyncAsync();
-        await foreach (var sync in syncs) {
-            nextBatch = sync?.NextBatch ?? nextBatch;
-            if (sync is null) continue;
-            Console.WriteLine($"Got sync, next batch: {nextBatch}!");
-
-            if (sync.Rooms is null) continue;
-            if (sync.Rooms.Join is null) continue;
-            foreach (var (roomId, roomData) in sync.Rooms.Join) {
-                RoomInfo room;
-                if (Rooms.Any(x => x.Room.RoomId == roomId)) {
-                    room = Rooms.First(x => x.Room.RoomId == roomId);
-                }
-                else {
-                    room = new RoomInfo {
-                        Room = hs.GetRoom(roomId),
-                        StateEvents = new List<StateEventResponse?>()
-                    };
-                    Rooms.Add(room);
-                    KnownRooms.Add(room);
+        profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId);
+        var profileSyncHelper = new SyncHelper(Homeserver, logger) {
+            Timeout = 10000,
+            Filter = profileUpdateFilter
+        };
+        RunSyncLoop(syncHelper);
+        RunSyncLoop(profileSyncHelper);
+        RunQueueProcessor();
+        await base.OnInitializedAsync();
+    }
+
+    private async Task RunQueueProcessor() {
+        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!");
                 }
-                room.StateEvents.AddRange(roomData.State.Events);
-            }
-            Status = $"Got {Rooms.Count} rooms so far! Next batch: {nextBatch}";
-            StateHasChanged();
-            await Task.Delay(100);
-            if (!syncHelper.IsInitialSync) break;
-        }
-        // while (sync is null or { Rooms.Join.Count: >= 1}) {
-            // sync = await syncHelper.SyncAsync(since: nextBatch, filter: filter, timeout: 0);
-            
-        // }
-        Console.WriteLine("Sync done!");
-        Status = "Sync complete!";
-        foreach (var roomInfo in Rooms) {
-            if (!roomInfo.StateEvents.Any(x => x.Type == "m.room.name")) {
-                roomInfo.StateEvents.Add(new StateEventResponse {
-                    Type = "m.room.name",
-                    TypedContent = new RoomNameEventContent {
-                        Name = roomInfo.Room.RoomId
+                if (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");
                     }
-                });
-            }
-            if (!roomInfo.StateEvents.Any(x => x.Type == "m.room.avatar")) {
-                roomInfo.StateEvents.Add(new StateEventResponse {
-                    Type = "m.room.avatar",
-                    TypedContent = new RoomAvatarEventContent {
-
+                    else {
+                        Console.WriteLine($"QueueWorker: encountered new room {roomId}!");
+                        room = new RoomInfo() {
+                            Room = Homeserver.GetRoom(roomId)
+                        };
+                        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 })
+                        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;
+                    await Task.Delay(RenderContents ? 25 : 25);
+                }
+                else {
+                    Console.WriteLine("Failed to dequeue item");
+                }
             }
-            if (!roomInfo.StateEvents.Any(x => x.Type == "org.matrix.mjolnir.shortcode")) {
-                roomInfo.StateEvents.Add(new StateEventResponse {
-                    Type = "org.matrix.mjolnir.shortcode"
-                });
+            catch (Exception e) {
+                Console.WriteLine("QueueWorker exception: " + e);
             }
         }
-        Console.WriteLine("Set stub data!");
-        Status = "Set stub data!";
-        SemaphoreSlim semaphore = new(8, 8);
-        var memberTasks = Rooms.Select(async roomInfo => {
-            if (!roomInfo.StateEvents.Any(x => x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId)) {
-                await semaphore.WaitAsync();
-                roomInfo.StateEvents.Add(new StateEventResponse {
-                    Type = "m.room.member",
-                    StateKey = hs.WhoAmI.UserId,
-                    TypedContent = await roomInfo.Room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs.WhoAmI.UserId) ?? new RoomMemberEventContent {
-                        Membership = "unknown"
-                    }
-                });
-                semaphore.Release();
-            }
-        }).ToList();
-        await Task.WhenAll(memberTasks);
-        Console.WriteLine("Set all room member data!");
-        Status = "Set all room member data!";
-    // var res = await hs.SyncHelper.Sync(filter: filter);
-    // if (res is not null) {
-    //     foreach (var (roomId, roomData) in res.Rooms.Join) {
-    //         var room = new RoomInfo() {
-    //             Room = hs.GetRoom(roomId),
-    //             StateEvents = roomData.State.Events.Where(x => x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId).ToList()
-    //         };
-    //         Rooms.Add(room);
-    //     }
-    // }
-    // Rooms = (await hs.GetJoinedRooms()).Select(x => new RoomInfo() { Room = x }).ToList();
-
-        RenderContents = true;
-        Status = "";
-        await base.OnInitializedAsync();
     }
 
-    private bool RenderContents { get; set; }
+    private bool RenderContents { get; set; } = false;
 
     private string _status;
 
@@ -170,4 +180,46 @@
         }
     }
 
-}
+    private string _status2;
+
+    public string Status2 {
+        get => _status2;
+        set {
+            _status2 = value;
+            StateHasChanged();
+        }
+    }
+
+    private Queue<KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>> queue = new();
+
+    private async Task RunSyncLoop(SyncHelper syncHelper) {
+        Status = "Initial syncing...";
+        Console.WriteLine("starting sync");
+
+        var syncs = syncHelper.EnumerateSyncAsync();
+        await foreach (var sync in syncs) {
+            Console.WriteLine("trying sync");
+            if (sync is null) continue;
+
+            Status = $"Got sync with {sync.Rooms?.Join?.Count ?? 0} room updates, next batch: {sync.NextBatch}!";
+            if (sync?.Rooms?.Join != null)
+                foreach (var joinedRoom in sync.Rooms.Join)
+                    if ( /*joinedRoom.Value.AccountData?.Events?.Count > 0 ||*/ joinedRoom.Value.State?.Events?.Count > 0) {
+                        joinedRoom.Value.State.Events.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI?.UserId);
+                        // 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)
+                            joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender) ?? false);
+                        
+                        queue.Enqueue(joinedRoom);
+                    }
+
+            Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue... " +
+                     $"{sync?.Rooms?.Join?.Count ?? 0} new updates!";
+
+            Status2 = $"Next batch: {sync.NextBatch}";
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
index 15220da..3cc6a15 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
@@ -1,9 +1,6 @@
 @page "/Rooms/{RoomId}/Policies"
 @using LibMatrix
-@using LibMatrix.Extensions
-@using LibMatrix.Helpers
 @using LibMatrix.Homeservers
-@using LibMatrix.Responses
 @using ArcaneLibs.Extensions
 @using LibMatrix.EventTypes.Spec.State
 <h3>Policy list editor - Editing @RoomId</h3>
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Space.razor b/MatrixRoomUtils.Web/Pages/Rooms/Space.razor
index d0236e2..ce94d01 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Space.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Space.razor
@@ -1,6 +1,4 @@
 @page "/Rooms/{RoomId}/Space"
-@using LibMatrix.Extensions
-@using LibMatrix.Responses
 @using LibMatrix.RoomTypes
 @using ArcaneLibs.Extensions
 @using LibMatrix
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixRoomUtils.Web/Pages/Rooms/StateEditor.razor
index e47ba11..0e9d4f6 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/StateEditor.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/StateEditor.razor
@@ -1,6 +1,4 @@
 @page "/Rooms/{RoomId}/State/Edit"
-@using LibMatrix.Extensions
-@using LibMatrix.Responses
 @using ArcaneLibs.Extensions
 @using LibMatrix
 @inject ILocalStorageService LocalStorage
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/StateViewer.razor b/MatrixRoomUtils.Web/Pages/Rooms/StateViewer.razor
index e9c5da1..2d0e0b0 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/StateViewer.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/StateViewer.razor
@@ -1,6 +1,4 @@
 @page "/Rooms/{RoomId}/State/View"
-@using LibMatrix.Extensions
-@using LibMatrix.Responses
 @using ArcaneLibs.Extensions
 @using LibMatrix
 @inject ILocalStorageService LocalStorage
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor b/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor
index 68125cb..e22be4a 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor
@@ -4,7 +4,6 @@
 @using LibMatrix.EventTypes.Spec
 @using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.Homeservers
-@using LibMatrix.Responses
 <h3>RoomManagerTimeline</h3>
 <hr/>
 <p>Loaded @Events.Count events...</p>
diff --git a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor b/MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor
index 4cd2032..939838e 100644
--- a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
+++ b/MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor
@@ -1,8 +1,6 @@
 @page "/KnownHomeserverList"
 @using System.Diagnostics
 @using ArcaneLibs.Extensions
-@using LibMatrix
-@using LibMatrix.Extensions
 @using LibMatrix.Homeservers
 @using LibMatrix.RoomTypes
 <h3>Known Homeserver List</h3>
diff --git a/MatrixRoomUtils.Web/Pages/MediaLocator.razor b/MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor
index e1686b9..a376efa 100644
--- a/MatrixRoomUtils.Web/Pages/MediaLocator.razor
+++ b/MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor
@@ -1,5 +1,4 @@
 @page "/MediaLocator"
-@using LibMatrix
 @using LibMatrix.Homeservers
 @inject HttpClient Http
 <h3>Media locator</h3>
diff --git a/MatrixRoomUtils.Web/Pages/SpaceDebug.razor b/MatrixRoomUtils.Web/Pages/Tools/SpaceDebug.razor
index b07ba84..5b4815d 100644
--- a/MatrixRoomUtils.Web/Pages/SpaceDebug.razor
+++ b/MatrixRoomUtils.Web/Pages/Tools/SpaceDebug.razor
@@ -1,5 +1,4 @@
 @page "/SpaceDebug"
-@using LibMatrix.RoomTypes
 @using LibMatrix.Filters
 @using LibMatrix.Helpers
 <h3>SpaceDebug</h3>
diff --git a/MatrixRoomUtils.Web/Shared/NavMenu.razor b/MatrixRoomUtils.Web/Shared/NavMenu.razor
index ad671c5..68b491d 100644
--- a/MatrixRoomUtils.Web/Shared/NavMenu.razor
+++ b/MatrixRoomUtils.Web/Shared/NavMenu.razor
@@ -14,61 +14,74 @@
                 <span class="oi oi-home" aria-hidden="true"></span> Home
             </NavLink>
         </div>
+        
         <div class="nav-item px-3">
             <NavLink class="nav-link" href="About">
                 <span class="oi oi-info" aria-hidden="true"></span> About MRU
             </NavLink>
         </div>
+
+        <!-- Main tools -->
+        
         <div class="nav-item px-3">
             <h5 style="margin-left: 1em;">Main tools</h5>
             <hr style="margin-bottom: 0em;"/>
         </div>
-       
+
         <div class="nav-item px-3">
             <NavLink class="nav-link" href="Rooms">
                 <span class="oi oi-plus" aria-hidden="true"></span> Room list
             </NavLink>
         </div>
-        @* <div class="nav-item px-3"> *@
-        @*     <h5 style="margin-left: 1em;">Plural tools</h5> *@
-        @*     <hr style="margin-bottom: 0em;"/> *@
-        @* </div> *@
+
+        <div class="nav-item px-3">
+            <NavLink class="nav-link" href="User/Manage">
+                <span class="oi oi-plus" aria-hidden="true"></span> Manage user
+            </NavLink>
+        </div>
+
+        <!-- Extra tools -->
+        
         <div class="nav-item px-3">
             <h5 style="margin-left: 1em;">Extra tools</h5>
             <hr style="margin-bottom: 0em;"/>
         </div>
-        @* <div class="nav-item px-3"> *@
-        @*     <NavLink class="nav-link" href="KnownHomeserverList"> *@
-        @*         <span class="oi oi-plus" aria-hidden="true"></span> Known homeserver list *@
-        @*     </NavLink> *@
-        @* </div> *@
-        @* <div class="nav-item px-3"> *@
-        @*     <NavLink class="nav-link" href="MediaLocator"> *@
-        @*         <span class="oi oi-plus" aria-hidden="true"></span> Media locator *@
-        @*     </NavLink> *@
-        @* </div> *@
 
         <div class="nav-item px-3">
             <NavLink class="nav-link" href="HSAdmin">
-                <span class="oi oi-plus" aria-hidden="true"></span> HS Admin
+                <span class="oi oi-plus" aria-hidden="true"></span> Synapse administration
             </NavLink>
         </div>
-        
+
         <div class="nav-item px-3">
             <NavLink class="nav-link" href="SpaceDebug">
                 <span class="oi oi-plus" aria-hidden="true"></span> Space relationships
             </NavLink>
         </div>
+        <div class="nav-item px-3">
+            <NavLink class="nav-link" href="KnownHomeservers">
+                <span class="oi oi-plus" aria-hidden="true"></span> Known homeservers
+            </NavLink>
+        </div>
 
+        <!-- MRU -->
+        
         <div class="nav-item px-3">
             <h5 style="margin-left: 1em;">MRU</h5>
             <hr style="margin-bottom: 0em;"/>
         </div>
+        
         <div class="nav-item px-3">
-            <NavLink class="nav-link" href="DevOptions">
+            <NavLink class="nav-link" href="Dev/Options">
                 <span class="oi oi-plus" aria-hidden="true"></span> Developer options
             </NavLink>
         </div>
+        
+        <div class="nav-item px-3">
+            <NavLink class="nav-link" href="Dev/Utilities">
+                <span class="oi oi-plus" aria-hidden="true"></span> Developer utilities
+            </NavLink>
+        </div>
     </nav>
 </div>
 
diff --git a/MatrixRoomUtils.Web/Shared/PortableDevTools.razor b/MatrixRoomUtils.Web/Shared/PortableDevTools.razor
deleted file mode 100644
index 8ca10a0..0000000
--- a/MatrixRoomUtils.Web/Shared/PortableDevTools.razor
+++ /dev/null
@@ -1,26 +0,0 @@
-@* @if (Enabled) { *@
-@*     <a href="/DevOptions">Portable devtools (enabled)</a> *@
-@*     <div id="PortableDevTools" style="position: fixed; bottom: 0; right: 0; min-width: 200px; min-height: 100px; background: #0002;" draggable> *@
-@*         $1$ <p>Cache size: @RuntimeCache.GenericResponseCache.Sum(x => x.Value.Cache.Count)</p> #1# *@
-@*     </div> *@
-@* } *@
-@* else { *@
-@*     <a href="/DevOptions">Portable devtools (disabled)</a> *@
-@* } *@
-@* *@
-@* @code { *@
-@*     private bool Enabled { get; set; } = LocalStorageWrapper.Settings.DeveloperSettings.EnablePortableDevtools; *@
-@* *@
-@*     protected override async Task OnInitializedAsync() => *@
-@*     // if(!RuntimeCache.WasLoaded) *@
-@*     // await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage); *@
-@*     // StateHasChanged(); *@
-@*         Task.Run(async () => { *@
-@*             while (true) { *@
-@*                 await Task.Delay(100); *@
-@*                 Enabled = LocalStorageWrapper.Settings.DeveloperSettings.EnablePortableDevtools; *@
-@*                 StateHasChanged(); *@
-@*             } *@
-@*         }); *@
-@* *@
-@* } *@
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomList.razor b/MatrixRoomUtils.Web/Shared/RoomList.razor
index 91ebb0b..705f68c 100644
--- a/MatrixRoomUtils.Web/Shared/RoomList.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomList.razor
@@ -3,8 +3,10 @@
 @using LibMatrix.Extensions
 @using ArcaneLibs.Extensions
 @using LibMatrix.EventTypes.Spec.State
-@if(Rooms.Count != RoomsWithTypes.Sum(x=>x.Value.Count)) {
-    <p>Fetching room details... @RoomsWithTypes.Sum(x=>x.Value.Count) out of @Rooms.Count done!</p>
+@using System.Collections.ObjectModel
+@using _Imports = MatrixRoomUtils.Web._Imports
+@if (!StillFetching) {
+    <p>Fetching room details... @RoomsWithTypes.Sum(x => x.Value.Count) out of @Rooms.Count done!</p>
     @foreach (var category in RoomsWithTypes.OrderBy(x => x.Value.Count)) {
         <p>@category.Key (@category.Value.Count)</p>
     }
@@ -18,23 +20,35 @@ else {
 @code {
 
     [Parameter]
-    public List<RoomInfo> Rooms { get; set; }
+    public ObservableCollection<RoomInfo> Rooms { get; set; }
+
     [Parameter]
     public ProfileResponseEventContent? GlobalProfile { get; set; }
 
-    Dictionary<string, List<RoomInfo>> RoomsWithTypes = new();
+    [Parameter]
+    public bool StillFetching { get; set; } = true;
+
+    [Parameter]
+    public EventCallback<bool> StillFetchingChanged { get; set; }
 
-    protected override async Task OnInitializedAsync() {
+    private Dictionary<string, List<RoomInfo>> RoomsWithTypes => Rooms is null ? new() : Rooms.GroupBy(x => GetRoomTypeName(x.CreationEventContent?.Type)).ToDictionary(x => x.Key, x => x.ToList());
+
+    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();
+                };
+            }
+        };
 
-        GlobalProfile ??= await hs.GetProfileAsync(hs.WhoAmI.UserId);
-        if (RoomsWithTypes.Any()) return;
-
-        var tasks = Rooms.Select(ProcessRoom);
-        await Task.WhenAll(tasks);
+        // GlobalProfile ??= await hs.GetProfileAsync(hs.WhoAmI.UserId);
 
-        await base.OnInitializedAsync();
+        await base.OnParametersSetAsync();
     }
 
     private string GetRoomTypeName(string? roomType) => roomType switch {
@@ -45,32 +59,33 @@ else {
         _ => roomType
         };
 
+    // private static SemaphoreSlim _semaphoreSlim = new(8, 8);
 
-    private static SemaphoreSlim _semaphoreSlim = new(8, 8);
-    private async Task ProcessRoom(RoomInfo room) {
-        await _semaphoreSlim.WaitAsync();
-        string roomType;
-        try {
-            var createEvent = (await room.GetStateEvent("m.room.create")).TypedContent as RoomCreateEventContent;
-            roomType = GetRoomTypeName(createEvent.Type);
-
-            if (roomType == "Room") {
-                var mjolnirData = await room.GetStateEvent("org.matrix.mjolnir.shortcode");
-                if(mjolnirData?.RawContent?.ToJson(ignoreNull: true) is not null and not "{}")
-                    roomType = "Legacy policy room";
-            }
-        }
-        catch (MatrixException e) {
-            roomType = $"Error: {e.ErrorCode}";
-        }
-
-        if (!RoomsWithTypes.ContainsKey(roomType)) {
-            RoomsWithTypes.Add(roomType, new List<RoomInfo>());
-        }
-        RoomsWithTypes[roomType].Add(room);
-
-            StateHasChanged();
-        _semaphoreSlim.Release();
-    }
+    // private async Task ProcessRoom(RoomInfo room) {
+    //     await _semaphoreSlim.WaitAsync();
+    //     string roomType;
+    //     try {
+    //         var createEvent = (await room.GetStateEvent("m.room.create")).TypedContent as RoomCreateEventContent;
+    //         roomType = GetRoomTypeName(createEvent.Type);
+    //
+    //         if (roomType == "Room") {
+    //             var mjolnirData = await room.GetStateEvent("org.matrix.mjolnir.shortcode");
+    //             if (mjolnirData?.RawContent?.ToJson(ignoreNull: true) is not null and not "{}")
+    //                 roomType = "Legacy policy room";
+    //         }
+    //     }
+    //     catch (MatrixException e) {
+    //         roomType = $"Error: {e.ErrorCode}";
+    //     }
+    //
+    //     // if (!RoomsWithTypes.ContainsKey(roomType)) {
+    //         // RoomsWithTypes.Add(roomType, new List<RoomInfo>());
+    //     // }
+    //     // RoomsWithTypes[roomType].Add(room);
+    //
+    //     StateHasChanged();
+    //     _semaphoreSlim.Release();
+    // }
 
 }
+
diff --git a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
index 1b54577..e08f98d 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
@@ -1,3 +1,4 @@
+@using System.Collections.ObjectModel
 <MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton href="@($"/Rooms/{Space.Room.RoomId}/Space")">Manage space</MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton>
 
 <br/>
@@ -25,7 +26,7 @@
         set => _breadcrumbs = value;
     }
 
-    private List<RoomInfo> Children { get; set; } = new();
+    private ObservableCollection<RoomInfo> Children { get; set; } = new();
 
     protected override async Task OnInitializedAsync() {
         if (Breadcrumbs == null) throw new ArgumentNullException(nameof(Breadcrumbs));
@@ -35,7 +36,7 @@
             if (Breadcrumbs.Contains(room.RoomId)) continue;
             var roomInfo = KnownRooms.FirstOrDefault(x => x.Room.RoomId == room.RoomId);
             if (roomInfo is null) {
-                roomInfo = new RoomInfo {
+                roomInfo = new RoomInfo() {
                     Room = room
                 };
                 KnownRooms.Add(roomInfo);
diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
index 0e1d70d..79c7f4e 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListItem.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
@@ -5,42 +5,39 @@
 @using LibMatrix.Homeservers
 @using LibMatrix.RoomTypes
 @using MatrixRoomUtils.Web.Classes.Constants
-<div class="roomListItem" id="@RoomId" style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content; @(hasDangerousRoomVersion ? "border: red 4px solid;" : hasOldRoomVersion ? "border: #FF0 1px solid;" : "")">
-    @if (OwnMemberState != null) {
-        <img class="imageUnloaded @(string.IsNullOrWhiteSpace(OwnMemberState?.AvatarUrl ?? GlobalProfile?.AvatarUrl) ? "" : "imageLoaded")"
-             style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%; @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "border-color: red; border-width: 3px; border-style: dashed;" : "")"
-             src="@hsResolver.ResolveMediaUri(hs.ServerName, OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl ?? "/icon-192.png").Result"/>
-        <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px; @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "background-color: red;" : "")">
-            @(OwnMemberState?.DisplayName ?? GlobalProfile?.DisplayName ?? "Loading...")
-        </span>
-        <span style="vertical-align: middle; padding-right: 8px; padding-left: 0px;">-></span>
-    }
-    <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height:  32px; border-radius: 50%;"
-         src="@roomIcon"/>
-    <div style="display: inline-block;">
-        <span style="vertical-align: middle; padding-right: 8px;">@roomName</span>
-        @if (ChildContent is not null) {
-            @ChildContent
+@if (RoomInfo is not null) {
+    <div class="roomListItem @(HasDangerousRoomVersion ? "dangerousRoomVersion" : HasOldRoomVersion ? "oldRoomVersion" : "")" id="@RoomInfo.Room.RoomId">
+        @if (OwnMemberState != null) {
+            <img class="avatar32 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "") " @*@(ChildContent is not null ? "vcenter" : "")*@
+                 src="@(hs.ResolveMediaUri(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl) ?? "/icon-192.png")"/>
+            <span class="centerVertical border75 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "")">
+                @(OwnMemberState?.DisplayName ?? GlobalProfile?.DisplayName ?? "Loading...")
+            </span>
+            <span class="centerVertical noLeftPadding">-></span>
         }
-    </div>
+        <img class="avatar32" src="@hs?.ResolveMediaUri(RoomInfo.RoomIcon)"/> @* style="@(ChildContent is not null ? "vertical-align: baseline;" : "")"*@
+        <div class="inlineBlock">
+            <span class="centerVertical">@RoomInfo.RoomName</span>
+            @* @if (ChildContent is not null) { *@
+            @* @ChildContent *@
+            @* } *@
+        </div>
 
-</div>
+    </div>
+}
+else {
+    <p>Warning: RoomInfo is null!</p>
+}
 
 @code {
 
-    [Parameter]
-    public RenderFragment? ChildContent { get; set; }
-
-    [Parameter]
-    public GenericRoom? Room { get; set; }
+    // [Parameter]
+    // public RenderFragment? ChildContent { get; set; }
 
     [Parameter]
     public RoomInfo? RoomInfo { get; set; }
 
     [Parameter]
-    public string? RoomId { get; set; }
-
-    [Parameter]
     public bool ShowOwnProfile { get; set; } = false;
 
     [Parameter]
@@ -49,16 +46,20 @@
     [CascadingParameter]
     public ProfileResponseEventContent? GlobalProfile { get; set; }
 
-    private string? roomName { get; set; }
-
-    private string? roomIcon { get; set; } = "/icon-192.png";
-
-    private bool hasOldRoomVersion { get; set; } = false;
-    private bool hasDangerousRoomVersion { get; set; } = false;
+    private bool HasOldRoomVersion { get; set; } = false;
+    private bool HasDangerousRoomVersion { get; set; } = false;
 
     private static SemaphoreSlim _semaphoreSlim = new(8);
     private static AuthenticatedHomeserverGeneric? hs { get; set; }
 
+    protected override async Task OnParametersSetAsync() {
+        RoomInfo.PropertyChanged += (_, a) => {
+            Console.WriteLine(a.PropertyName);
+            StateHasChanged();
+        };
+        await base.OnParametersSetAsync();
+    }
+
     protected override async Task OnInitializedAsync() {
         await base.OnInitializedAsync();
 
@@ -67,34 +68,20 @@
         hs ??= await MRUStorage.GetCurrentSessionOrNavigate();
         if (hs is null) return;
 
-        if (Room is null && RoomId is null && RoomInfo is null) {
-            throw new ArgumentNullException(nameof(RoomId));
-        }
-
-        // sweep from roominfo to id
-        if (RoomInfo is not null) Room = RoomInfo.Room;
-        if(Room is not null) RoomId = Room.RoomId;
-
-        //sweep from id to roominfo
-        if(RoomId is not null) Room ??= hs.GetRoom(RoomId);
-        if(Room is not null) RoomInfo ??= new RoomInfo {
-            Room = Room
-        };
-
         try {
-            await CheckRoomVersion();
-            await GetRoomInfo();
-            await LoadOwnProfile();
+    await CheckRoomVersion();
+    // await GetRoomInfo();
+    // await LoadOwnProfile();
         }
         catch (MatrixException e) {
             if (e is not { ErrorCode: "M_FORBIDDEN" }) {
                 throw;
             }
-            roomName = "Error: " + e.Message;
-            roomIcon = "/blobfox_outage.gif";
+    // RoomName = "Error: " + e.Message;
+    // RoomIcon = "/blobfox_outage.gif";
         }
         catch (Exception e) {
-            Console.WriteLine($"Failed to load room info for {RoomId}: {e.Message}");
+            Console.WriteLine($"Failed to load room info for {RoomInfo.Room.RoomId}: {e.Message}");
         }
         _semaphoreSlim.Release();
     }
@@ -102,7 +89,7 @@
     private async Task LoadOwnProfile() {
         if (!ShowOwnProfile) return;
         try {
-            OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.UserId)).TypedContent as RoomMemberEventContent;
+    // OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.UserId)).TypedContent as RoomMemberEventContent;
             GlobalProfile ??= await hs.GetProfileAsync(hs.UserId);
         }
         catch (MatrixException e) {
@@ -117,35 +104,39 @@
     }
 
     private async Task CheckRoomVersion() {
-        var ce = (await RoomInfo.GetStateEvent("m.room.create")).TypedContent as RoomCreateEventContent;
+        while (RoomInfo?.CreationEventContent is null) {
+            Console.WriteLine($"Room creation event content for {RoomInfo.Room.RoomId} is null...");
+            await Task.Delay(Random.Shared.Next(1000, 2500));
+        }
+        var ce = RoomInfo.CreationEventContent;
         if (int.TryParse(ce.RoomVersion, out var rv)) {
             if (rv < 10)
-                hasOldRoomVersion = true;
+                HasOldRoomVersion = true;
         }
         else // treat unstable room versions as dangerous
-            hasDangerousRoomVersion = true;
+            HasDangerousRoomVersion = true;
 
         if (RoomConstants.DangerousRoomVersions.Contains(ce.RoomVersion)) {
-            hasDangerousRoomVersion = true;
-            roomName = "Dangerous room: " + roomName;
-        }
-    }
-
-    private async Task GetRoomInfo() {
-        try {
-            roomName ??= ((await RoomInfo.GetStateEvent("m.room.name"))?.TypedContent as RoomNameEventContent)?.Name ?? RoomId;
-
-            var state = (await RoomInfo.GetStateEvent("m.room.avatar")).TypedContent as RoomAvatarEventContent;
-            if (state?.Url is { } url) {
-                roomIcon = await hsResolver.ResolveMediaUri(hs.ServerName, url);
-                // Console.WriteLine($"Got avatar for room {RoomId}: {roomIcon} ({url})");
-            }
-        }
-        catch (MatrixException e) {
-            if (e is not { ErrorCode: "M_FORBIDDEN" }) {
-                throw;
-            }
+            HasDangerousRoomVersion = true;
+    // RoomName = "Dangerous room: " + RoomName;
         }
     }
 
-}
+    // private async Task GetRoomInfo() {
+    //     try {
+    //         RoomName ??= ((await RoomInfo.GetStateEvent("m.room.name"))?.TypedContent as RoomNameEventContent)?.Name ?? RoomId;
+    //
+    //         var state = (await RoomInfo.GetStateEvent("m.room.avatar")).TypedContent as RoomAvatarEventContent;
+    //         if (state?.Url is { } url) {
+    //             RoomIcon = await hsResolver.ResolveMediaUri(hs.ServerName, url);
+    // // Console.WriteLine($"Got avatar for room {RoomId}: {roomIcon} ({url})");
+    //         }
+    //     }
+    //     catch (MatrixException e) {
+    //         if (e is not { ErrorCode: "M_FORBIDDEN" }) {
+    //             throw;
+    //         }
+    //     }
+    // }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor.css b/MatrixRoomUtils.Web/Shared/RoomListItem.razor.css
index da22d38..13de656 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListItem.razor.css
+++ b/MatrixRoomUtils.Web/Shared/RoomListItem.razor.css
@@ -1,10 +1,48 @@
-/*.imageUnloaded {*/
-/*    scale: 3;*/
-/*    opacity: 0.5;*/
-/*    transition: scale 0.5s ease-in-out;*/
-/*}*/
-
-.imageLoaded {
-    opacity: 1;
-    scale: 1;
+.roomListItem {
+    background-color: #ffffff11;
+    border-radius: 25px;
+    margin: 8px;
+    width: fit-Content;
+}
+
+.roomListItem.dangerousRoomVersion {
+    border: red 4px solid;
+}
+
+.roomListItem.oldRoomVersion {
+    border: #FF0 1px solid;
+}
+
+.avatar32 {
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+}
+
+.avatar32.vcenter {
+    vertical-align: baseline;
+}
+
+.highlightChange {
+    background-color: red;
+    border-color: red;
+    border-width: 3px;
+    border-style: dashed;
+}
+
+.inlineBlock {
+    display: inline-block;
+}
+
+.centerVertical {
+    vertical-align: middle;
+    padding-right: 8px;
+}
+
+.noLeftPadding {
+    padding-left: 0px;
+}
+
+.border75 {
+    border-radius: 75px;
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
index e4ee873..9efeaab 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
@@ -1,4 +1,3 @@
-@using LibMatrix.Responses
 @using LibMatrix
 @using LibMatrix.Homeservers
 <h3>BaseTimelineItem</h3>
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
index b58afba..a454103 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
@@ -1,4 +1,3 @@
-@using LibMatrix.Extensions
 @using ArcaneLibs.Extensions
 @using LibMatrix.EventTypes.Spec.State
 @inherits BaseTimelineItem
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
index 5dd87e0..8073406 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
@@ -1,4 +1,3 @@
-@using LibMatrix.Extensions
 @using ArcaneLibs.Extensions
 @inherits BaseTimelineItem
 
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
index ff77726..2d05151 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
@@ -1,4 +1,3 @@
-@using LibMatrix.Extensions
 @using ArcaneLibs.Extensions
 @using LibMatrix.EventTypes.Spec.State
 @inherits BaseTimelineItem
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
index 69845d9..1ab530d 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
@@ -1,4 +1,3 @@
-@using LibMatrix.Extensions
 @using ArcaneLibs.Extensions
 @inherits BaseTimelineItem
 
diff --git a/MatrixRoomUtils.Web/Shared/UserListItem.razor b/MatrixRoomUtils.Web/Shared/UserListItem.razor
index 9010820..96e8e64 100644
--- a/MatrixRoomUtils.Web/Shared/UserListItem.razor
+++ b/MatrixRoomUtils.Web/Shared/UserListItem.razor
@@ -1,8 +1,9 @@
 @using LibMatrix.Helpers
 @using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Homeservers
 <div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content;">
-    <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height:  32px; border-radius: 50%;" src="@profileAvatar"/>
-    <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@profileName</span>
+    <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height:  32px; border-radius: 50%;" src="@(string.IsNullOrWhiteSpace(User?.AvatarUrl) ? "https://api.dicebear.com/6.x/identicon/svg?seed=" + UserId : User.AvatarUrl)"/>
+    <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@User?.DisplayName</span>
 
     <div style="display: inline-block;">
         @if (ChildContent is not null) {
@@ -18,36 +19,25 @@
     public RenderFragment? ChildContent { get; set; }
 
     [Parameter]
-    public ProfileResponseEventContent User { get; set; }
+    public ProfileResponseEventContent? User { get; set; }
 
     [Parameter]
     public string UserId { get; set; }
 
-    private string? profileAvatar { get; set; } = "/icon-192.png";
-    private string? profileName { get; set; } = "Loading...";
-
-    private static SemaphoreSlim _semaphoreSlim = new(8);
+    private AuthenticatedHomeserverGeneric _homeserver = null!;
 
     protected override async Task OnInitializedAsync() {
-        await base.OnInitializedAsync();
-
-        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
-        if (hs is null) return;
-
-        await _semaphoreSlim.WaitAsync();
+        _homeserver = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (_homeserver is null) return;
 
         if (User == null) {
             if (UserId == null) {
                 throw new ArgumentNullException(nameof(UserId));
             }
-            User = await hs.GetProfileAsync(UserId);
+            User = await _homeserver.GetProfileAsync(UserId);
         }
 
-    // UserId = User.;
-        profileAvatar = await hsResolver.ResolveMediaUri(hs.ServerName, User.AvatarUrl);
-        profileName = User.DisplayName;
-
-        _semaphoreSlim.Release();
+        await base.OnInitializedAsync();
     }
 
-}
+}
\ No newline at end of file