about summary refs log tree commit diff
path: root/MatrixRoomUtils.Web/Pages
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixRoomUtils.Web/Pages')
-rw-r--r--MatrixRoomUtils.Web/Pages/DataExportPage.razor67
-rw-r--r--MatrixRoomUtils.Web/Pages/DebugTools.razor13
-rw-r--r--MatrixRoomUtils.Web/Pages/DevOptions.razor62
-rw-r--r--MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor4
-rw-r--r--MatrixRoomUtils.Web/Pages/Index.razor67
-rw-r--r--MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor112
-rw-r--r--MatrixRoomUtils.Web/Pages/LoginPage.razor52
-rw-r--r--MatrixRoomUtils.Web/Pages/MediaLocator.razor5
-rw-r--r--MatrixRoomUtils.Web/Pages/ModalTest.razor14
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor95
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor39
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor21
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor678
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor71
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerTimeline.razor7
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor42
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor9
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor64
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Index.razor13
19 files changed, 686 insertions, 749 deletions
diff --git a/MatrixRoomUtils.Web/Pages/DataExportPage.razor b/MatrixRoomUtils.Web/Pages/DataExportPage.razor
deleted file mode 100644
index 732cd74..0000000
--- a/MatrixRoomUtils.Web/Pages/DataExportPage.razor
+++ /dev/null
@@ -1,67 +0,0 @@
-@page "/Export"
-@using System.Text.Json
-@inject NavigationManager NavigationManager
-@inject ILocalStorageService LocalStorage
-
-<PageTitle>Export</PageTitle>
-
-<h3>Data export</h3>
-
-<br/><br/>
-<h5>Signed in accounts - <a href="/Login">Add new account</a> or <a href="/ImportUsers">Import from TSV</a></h5>
-<hr/>
-@if (_isLoaded) {
-    @foreach (var (token, user) in RuntimeCache.LoginSessions) {
-    @* <IndexUserItem User="@user"/> *@
-        <pre>
-@user.LoginResponse.UserId[1..].Split(":")[0]\auth\access_token=@token
-@user.LoginResponse.UserId[1..].Split(":")[0]\auth\device_id=@user.LoginResponse.DeviceId
-@user.LoginResponse.UserId[1..].Split(":")[0]\auth\home_server=@(RuntimeCache.HomeserverResolutionCache.ContainsKey(user.LoginResponse.HomeServer) ? RuntimeCache.HomeserverResolutionCache[user.LoginResponse.HomeServer].Result : "loading...")
-@user.LoginResponse.UserId[1..].Split(":")[0]\auth\user_id=@@@user.LoginResponse.UserId
-@user.LoginResponse.UserId[1..].Split(":")[0]\user\automatically_share_keys_with_trusted_users=true
-@user.LoginResponse.UserId[1..].Split(":")[0]\user\muted_tags=global
-@user.LoginResponse.UserId[1..].Split(":")[0]\user\online_key_backup=true
-@user.LoginResponse.UserId[1..].Split(":")[0]\user\only_share_keys_with_verified_users=false
-    </pre>
-    }
-}
-else {
-    <p>Loading...</p>
-    <p>@resolvedHomeservers/@totalHomeservers homeservers resolved...</p>
-}
-
-@code {
-    private bool _isLoaded;
-    private int resolvedHomeservers;
-    private int totalHomeservers;
-
-    protected override async Task OnInitializedAsync() {
-        await base.OnInitializedAsync();
-        if (!RuntimeCache.WasLoaded) {
-            await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-        }
-        var homeservers = RuntimeCache.LoginSessions.Values.Select(x => x.LoginResponse.HomeServer).Distinct();
-        totalHomeservers = homeservers.Count();
-        StateHasChanged();
-        foreach (var hs in homeservers) {
-            if (RuntimeCache.HomeserverResolutionCache.ContainsKey(hs)) {
-                resolvedHomeservers++;
-                continue;
-            }
-            var resolvedHomeserver = (await new RemoteHomeServer(hs).Configure()).FullHomeServerDomain;
-
-            RuntimeCache.HomeserverResolutionCache.Add(hs, new HomeServerResolutionResult { Result = resolvedHomeserver, ResolutionTime = DateTime.Now });
-            await LocalStorageWrapper.SaveCacheToLocalStorage(LocalStorage);
-
-            Console.WriteLine("Saved to local storage:");
-            Console.WriteLine(JsonSerializer.Serialize(RuntimeCache.HomeserverResolutionCache, new JsonSerializerOptions {
-                WriteIndented = true
-            }));
-            resolvedHomeservers++;
-            StateHasChanged();
-        }
-        StateHasChanged();
-        _isLoaded = true;
-    }
-
-}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/DebugTools.razor b/MatrixRoomUtils.Web/Pages/DebugTools.razor
index 4e4cec8..5116754 100644
--- a/MatrixRoomUtils.Web/Pages/DebugTools.razor
+++ b/MatrixRoomUtils.Web/Pages/DebugTools.razor
@@ -37,13 +37,10 @@ else {
     public List<string> Rooms { get; set; } = new();
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         await base.OnInitializedAsync();
-        if (RuntimeCache.CurrentHomeServer == null) {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
-        Rooms = (await RuntimeCache.CurrentHomeServer.GetJoinedRooms()).Select(x => x.RoomId).ToList();
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs == null) return;
+        Rooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList();
         Console.WriteLine("Fetched joined rooms!");
     }
 
@@ -53,7 +50,9 @@ else {
 
     private async Task SendGetRequest() {
         var field = typeof(IHomeServer).GetRuntimeFields().First(x => x.ToString().Contains("<_httpClient>k__BackingField"));
-        var httpClient = field.GetValue(RuntimeCache.CurrentHomeServer) as HttpClient;
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs == null) return;
+        var httpClient = field.GetValue(hs) as MatrixHttpClient;
         try {
             var res = await httpClient.GetAsync(get_request_url);
             if (res.IsSuccessStatusCode) {
diff --git a/MatrixRoomUtils.Web/Pages/DevOptions.razor b/MatrixRoomUtils.Web/Pages/DevOptions.razor
index cdb5693..70dac31 100644
--- a/MatrixRoomUtils.Web/Pages/DevOptions.razor
+++ b/MatrixRoomUtils.Web/Pages/DevOptions.razor
@@ -7,69 +7,23 @@
 <h3>Rory&::MatrixUtils - Developer options</h3>
 <hr/>
 
-<InputCheckbox @bind-Value="@LocalStorageWrapper.Settings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox><label> Enable log views</label><br/>
-<InputCheckbox @bind-Value="@LocalStorageWrapper.Settings.DeveloperSettings.EnableConsoleLogging" @oninput="@LogStuff"></InputCheckbox><label> Enable console logging</label><br/>
-<InputCheckbox @bind-Value="@LocalStorageWrapper.Settings.DeveloperSettings.EnablePortableDevtools" @oninput="@LogStuff"></InputCheckbox><label> Enable portable devtools</label><br/>
-<button @onclick="@DropCaches">Drop caches</button>
-<button @onclick="@RandomiseCacheTimers">Randomise cache timers</button>
+<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox><label> Enable log views</label><br/>
+<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnableConsoleLogging" @oninput="@LogStuff"></InputCheckbox><label> Enable console logging</label><br/>
+<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnablePortableDevtools" @oninput="@LogStuff"></InputCheckbox><label> Enable portable devtools</label><br/>
 <br/>
 
-<details open>
-    <summary>View caches</summary>
-    <p>Generic cache:</p>
-    <ul>
-        @foreach (var item in RuntimeCache.GenericResponseCache) {
-            <li>
-                @item.Key: @item.Value.Cache.Count entries<br/>
-                @if (item.Value.Cache.Count > 0) {
-                    <p>Earliest expiry: @(item.Value.Cache.Min(x => x.Value.ExpiryTime)) (@string.Format("{0:g}", item.Value.Cache.Min(x => x.Value.ExpiryTime).Value.Subtract(DateTime.Now)) from now)</p>
-                    @* <p>Average expiry: @(item.Value.Cache.Average(x => x.Value.ExpiryTime.Value))(@item.Value.Cache.Average(x => x.Value.ExpiryTime).Value.Subtract(DateTime.Now) from now)</p> *@
-                    <p>Last expiry: @(item.Value.Cache.Max(x => x.Value.ExpiryTime)) (@string.Format("{0:g}", item.Value.Cache.Max(x => x.Value.ExpiryTime).Value.Subtract(DateTime.Now)) from now)</p>
-                }
-            </li>
-        }
-    </ul>
-</details>
-
 @code {
 
+    MRUStorageWrapper.Settings settings { get; set; } = new();
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+        settings = await TieredStorage.DataStorageProvider.LoadObjectAsync<MRUStorageWrapper.Settings>("mru.settings");
         await base.OnInitializedAsync();
-        Task.Run(async () => {
-            while (true) {
-                await Task.Delay(1000);
-                StateHasChanged();
-            }
-        });
     }
 
-    protected async Task LogStuff() {
+    private async Task LogStuff() {
         await Task.Delay(100);
-        Console.WriteLine($"Settings: {LocalStorageWrapper.Settings.ToJson()}");
-
-        await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
-    }
-
-    protected async Task DropCaches() {
-        foreach (var (key, value) in RuntimeCache.GenericResponseCache) {
-            value.Cache.Clear();
-        }
-
-    //RuntimeCache.GenericResponseCache.Clear();
-        RuntimeCache.HomeserverResolutionCache.Clear();
-        await LocalStorageWrapper.SaveCacheToLocalStorage(LocalStorage);
-    }
-
-    protected async Task RandomiseCacheTimers() {
-        foreach (var keyValuePair in RuntimeCache.GenericResponseCache) {
-            Console.WriteLine($"Randomising cache timer for {keyValuePair.Key}");
-            foreach (var cacheItem in keyValuePair.Value.Cache) {
-                cacheItem.Value.ExpiryTime = DateTime.Now.AddSeconds(Random.Shared.Next(15, 120));
-            }
-
-            await LocalStorageWrapper.SaveCacheToLocalStorage(LocalStorage);
-        }
+        Console.WriteLine($"Settings: {settings.ToJson()}");
+        await TieredStorage.DataStorageProvider.SaveObjectAsync("mru.settings", settings);
     }
 
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor b/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
index 858fad9..e3ebc72 100644
--- a/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
+++ b/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
@@ -165,7 +165,9 @@
 
     private async Task Search() {
         Results.Clear();
-        var searchRooms = RuntimeCache.CurrentHomeServer.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator();
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+        var searchRooms = hs.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator();
         while (await searchRooms.MoveNextAsync()) {
             var room = searchRooms.Current;
             Console.WriteLine("Hit: " + room.ToJson(false));
diff --git a/MatrixRoomUtils.Web/Pages/Index.razor b/MatrixRoomUtils.Web/Pages/Index.razor
index 33cca61..16a6cee 100644
--- a/MatrixRoomUtils.Web/Pages/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Index.razor
@@ -1,7 +1,7 @@
 @page "/"
-@using MatrixRoomUtils.Web.Shared.IndexComponents
-@inject NavigationManager NavigationManager
-@inject ILocalStorageService LocalStorage
+@using MatrixRoomUtils.Core.Helpers
+@using MatrixRoomUtils.Core.Responses
+@using MatrixRoomUtils.Web.Shared.SimpleComponents
 
 <PageTitle>Index</PageTitle>
 
@@ -12,18 +12,67 @@ Small collection of tools to do not-so-everyday things.
 <h5>Signed in accounts - <a href="/Login">Add new account</a></h5>
 <hr/>
 <form>
-    @foreach (var (token, user) in RuntimeCache.LoginSessions) {
-        <IndexUserItem User="@user"/>
+    @foreach (var (auth, user) in _users.OrderByDescending(x=>x.Value.RoomCount)) {
+        var _auth = auth;
+        var _user = user;
+        <div style="margin-bottom: 1em;">
+            <img style="border-radius: 50%; height: 3em; width: 3em;" src="@_user.AvatarUrl"/>
+            <p style="margin-left: 1em; margin-top: -0.5em; display: inline-block;">
+                <input type="radio" name="csa" checked="@(_currentSession.AccessToken == _auth.AccessToken)" @onclick="@(()=>SwitchSession(_auth))" style="text-decoration-line: unset;"/>
+                <b>@_user.DisplayName</b> on <b>@_auth.Homeserver</b>
+                <a role="button" @onclick="@(() => RemoveUser(_auth))">Remove</a>
+                
+            </p>
+            <p style="margin-top: -1.5em; margin-left: 4em;">Member of @_user.RoomCount rooms</p>
+        
+        </div>
     }
 </form>
 
 @code
 {
+    private Dictionary<LoginResponse, UserInfo> _users = new();
+
     protected override async Task OnInitializedAsync() {
-        if (!RuntimeCache.WasLoaded) {
-            Console.WriteLine("[INDEX] !!! LOCALSTORAGE WAS NOT LOADED !!!");
-            await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-        }
+        _currentSession = await MRUStorage.GetCurrentToken();
+        _users.Clear();
+        var tokens = await MRUStorage.GetAllTokens();
+        var profileTasks = tokens.Select(async token => {
+            UserInfo userInfo = new();
+            var hs = await HomeserverProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken);
+            var roomCountTask = hs.GetJoinedRooms();
+            var profile = await hs.GetProfile(hs.WhoAmI.UserId);
+            userInfo.DisplayName = profile.DisplayName ?? hs.WhoAmI.UserId;
+            userInfo.AvatarUrl = MediaResolver.ResolveMediaUri(hs.FullHomeServerDomain,
+                profile.AvatarUrl
+                ?? "https://api.dicebear.com/6.x/identicon/svg?seed=" + hs.WhoAmI.UserId
+                );
+            userInfo.RoomCount = (await roomCountTask).Count;
+            _users.Add(token, userInfo);
+    // StateHasChanged();
+        });
+        await Task.WhenAll(profileTasks);
         await base.OnInitializedAsync();
     }
+
+    private class UserInfo {
+        internal string AvatarUrl { get; set; }
+        internal string DisplayName { get; set; }
+        internal int RoomCount { get; set; } = 0;
+    }
+
+    private async Task RemoveUser(LoginResponse auth) {
+        await MRUStorage.RemoveToken(auth);
+        if ((await MRUStorage.GetCurrentToken()).AccessToken == auth.AccessToken)
+            MRUStorage.SetCurrentToken((await MRUStorage.GetAllTokens()).FirstOrDefault());
+        await OnInitializedAsync();
+    }
+
+    private LoginResponse _currentSession;
+
+    private async Task SwitchSession(LoginResponse auth) {
+        Console.WriteLine($"Switching to {auth.Homeserver} {auth.AccessToken} {auth.UserId}");
+        await MRUStorage.SetCurrentToken(auth);
+        await OnInitializedAsync();
+    }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor b/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
index 80dbfd1..5ccecab 100644
--- a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
+++ b/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
@@ -2,6 +2,7 @@
 @using System.Text.Json
 @using System.Diagnostics
 @using MatrixRoomUtils.Core.Responses
+@using MatrixRoomUtils.Core.StateEventTypes
 <h3>Known Homeserver List</h3>
 <hr/>
 
@@ -33,10 +34,10 @@ else {
     List<HomeServerInfo> HomeServers = new();
     bool IsFinished { get; set; }
     HomeServerInfoQueryProgress QueryProgress { get; set; } = new();
-
+    AuthenticatedHomeServer hs { get; set; }
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-
+        hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
         var sw = Stopwatch.StartNew();
         HomeServers = await GetHomeservers(progressCallback: async progress => {
             if (sw.ElapsedMilliseconds > 1000) {
@@ -61,7 +62,8 @@ else {
     private async Task<List<HomeServerInfo>> GetHomeservers(int memberLimit = 1000, Func<HomeServerInfoQueryProgress, Task<bool>>? progressCallback = null) {
         HomeServerInfoQueryProgress progress = new();
         List<HomeServerInfo> homeServers = new();
-        var rooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms();
+        
+        var rooms = await hs.GetJoinedRooms();
         progress.TotalRooms = rooms.Count;
 
         var semaphore = new SemaphoreSlim(4);
@@ -70,53 +72,69 @@ else {
             await semaphore.WaitAsync();
             progress.ProcessedUsers.Add(room, new HomeServerInfoQueryProgress.State());
             Console.WriteLine($"Fetching states for room ({rooms.IndexOf(room)}/{rooms.Count}) ({room.RoomId})");
-            var states = (await room.GetStateAsync("")).Value.Deserialize<List<StateEventResponse>>();
-            states.RemoveAll(x => x.Type != "m.room.member" || x.Content.GetProperty("membership").GetString() != "join");
-            Console.WriteLine($"Room {room.RoomId} has {states.Count} members");
-            if (states.Count > memberLimit) {
-                Console.WriteLine("Skipping!");
-                semaphore.Release();
-                progress.ProcessedUsers.Remove(room);
-                progress.TotalRooms--;
-                return;
-            }
-            progress.ProcessedUsers[room].Total = states.Count;
-            var updateInterval = progress.ProcessedUsers[room].Total >= 1000 ? 1000 : 100;
-            while (progress.ProcessedUsers.Any(x => x.Value.Total == 0) && progress.ProcessedUsers[room].Total >= 1000) {
-                progress.ProcessedUsers[room].Blocked = true;
-                await Task.Delay(1000);
-    // if(progressCallback != null)
-    //     await progressCallback.Invoke(progress);
-            }
-            progress.ProcessedUsers[room].Blocked = false;
-            var processedStates = 0;
-            foreach (var state in states) {
-                await semLock.WaitAsync();
-                semLock.Release();
-                if (progress.ProcessedUsers.Count(x => x.Value.Total == 0) > 5 && progress.ProcessedUsers[room].Total >= 200) {
-                    progress.ProcessedUsers[room].Slowmode = true;
-                    await Task.Delay(progress.ProcessedUsers[room].Total >= 500 ? 1000 : 100);
-                }
-                else {
-                    progress.ProcessedUsers[room].Slowmode = false;
-                }
+            var states = room.GetFullStateAsync();
+            await foreach (var state in states) {
+                if (state.Type is not "m.room.member") continue;
+                progress.ProcessedUsers[room].Total++;
                 if (!homeServers.Any(x => x.Server == state.StateKey.Split(':')[1])) {
                     homeServers.Add(new HomeServerInfo { Server = state.StateKey.Split(':')[1] });
-                }
-                var hs = homeServers.First(x => x.Server == state.StateKey.Split(':')[1]);
-                if (!hs.KnownUsers.Contains(state.StateKey.Split(':')[0]))
-                    hs.KnownUsers.Add(state.StateKey.Split(':')[0]);
-                if (++progress.ProcessedUsers[room].Processed % updateInterval == 0 && progressCallback != null) {
-                    await semLock.WaitAsync();
-                    var _ = await progressCallback.Invoke(progress);
-                    semLock.Release();
+                    Console.WriteLine($"Added new homeserver {state.StateKey.Split(':')[1]}");
                 }
             }
-            Console.WriteLine("Collected states!");
-            progress.ProcessedRooms++;
-            progress.ProcessedUsers[room].IsFinished = true;
-            progressCallback?.Invoke(progress);
             semaphore.Release();
+            progress.ProcessedUsers[room].IsFinished = true;
+            progress.ProcessedRooms++;
+            if (progressCallback is not null)
+                await progressCallback.Invoke(progress);
+
+
+
+    //         states.RemoveAll(x => x.Type != "m.room.member" || (x.TypedContent as RoomMemberEventData).Membership != "join");
+    //         Console.WriteLine($"Room {room.RoomId} has {states.Count} members");
+    //         if (states.Count > memberLimit) {
+    //             Console.WriteLine("Skipping!");
+    //             semaphore.Release();
+    //             progress.ProcessedUsers.Remove(room);
+    //             progress.TotalRooms--;
+    //             return;
+    //         }
+    //         progress.ProcessedUsers[room].Total = states.Count;
+    //         var updateInterval = progress.ProcessedUsers[room].Total >= 1000 ? 1000 : 100;
+    //         while (progress.ProcessedUsers.Any(x => x.Value.Total == 0) && progress.ProcessedUsers[room].Total >= 1000) {
+    //             progress.ProcessedUsers[room].Blocked = true;
+    //             await Task.Delay(1000);
+    // // if(progressCallback is not null)
+    // //     await progressCallback.Invoke(progress);
+    //         }
+    //         progress.ProcessedUsers[room].Blocked = false;
+    //         var processedStates = 0;
+    //         foreach (var state in states) {
+    //             await semLock.WaitAsync();
+    //             semLock.Release();
+    //             if (progress.ProcessedUsers.Count(x => x.Value.Total == 0) > 5 && progress.ProcessedUsers[room].Total >= 200) {
+    //                 progress.ProcessedUsers[room].Slowmode = true;
+    //                 await Task.Delay(progress.ProcessedUsers[room].Total >= 500 ? 1000 : 100);
+    //             }
+    //             else {
+    //                 progress.ProcessedUsers[room].Slowmode = false;
+    //             }
+    //             if (!homeServers.Any(x => x.Server == state.StateKey.Split(':')[1])) {
+    //                 homeServers.Add(new HomeServerInfo { Server = state.StateKey.Split(':')[1] });
+    //             }
+    //             var hs = homeServers.First(x => x.Server == state.StateKey.Split(':')[1]);
+    //             if (!hs.KnownUsers.Contains(state.StateKey.Split(':')[0]))
+    //                 hs.KnownUsers.Add(state.StateKey.Split(':')[0]);
+    //             if (++progress.ProcessedUsers[room].Processed % updateInterval == 0 && progressCallback is not null) {
+    //                 await semLock.WaitAsync();
+    //                 var _ = await progressCallback.Invoke(progress);
+    //                 semLock.Release();
+    //             }
+    //         }
+            // Console.WriteLine("Collected states!");
+            // progress.ProcessedRooms++;
+            // progress.ProcessedUsers[room].IsFinished = true;
+            // progressCallback?.Invoke(progress);
+            // semaphore.Release();
         });
         await Task.WhenAll(tasks);
 
@@ -136,7 +154,7 @@ else {
     class HomeServerInfoQueryProgress {
         public int ProcessedRooms { get; set; }
         public int TotalRooms { get; set; }
-        public Dictionary<Room, State> ProcessedUsers { get; } = new();
+        public Dictionary<GenericRoom, State> ProcessedUsers { get; } = new();
         public List<HomeServerInfo> CurrentState { get; set; } = new();
 
         public class State {
diff --git a/MatrixRoomUtils.Web/Pages/LoginPage.razor b/MatrixRoomUtils.Web/Pages/LoginPage.razor
index 9df7fa6..e19dd04 100644
--- a/MatrixRoomUtils.Web/Pages/LoginPage.razor
+++ b/MatrixRoomUtils.Web/Pages/LoginPage.razor
@@ -1,6 +1,6 @@
 @page "/Login"
-@using MatrixRoomUtils.Core.Authentication
 @using System.Text.Json
+@using MatrixRoomUtils.Core.Responses
 @using MatrixRoomUtils.Web.Shared.SimpleComponents
 @inject ILocalStorageService LocalStorage
 @inject IJSRuntime JsRuntime
@@ -22,20 +22,25 @@
 
 <InputFile OnChange="@FileChanged" accept=".tsv"></InputFile>
 <br/>
-<button @onclick="Login">Login</button>
 <br/><br/>
 <h4>Parsed records</h4>
 <hr/>
 <table border="1">
+    <thead>
+        <td>Username</td>
+        <td>Homeserver</td>
+    </thead>
     @foreach (var (homeserver, username, password) in records) {
-        <tr style="background-color: @(RuntimeCache.LoginSessions.Any(x => x.Value.LoginResponse.UserId == $"@{username}:{homeserver}") ? "green" : "unset")">
+        var record = (homeserver, username, password);
+        <tr style="background-color: @(LoggedInSessions.Any(x => x.UserId == $"@{username}:{homeserver}") ? "green" : "unset")">
             <td style="border-width: 1px;">@username</td>
             <td style="border-width: 1px;">@homeserver</td>
-            <td style="border-width: 1px;">@password.Length chars</td>
+            <td><a role="button" @onclick="() => records.Remove(record)">Remove</a></td>
         </tr>
     }
 </table>
 <br/>
+<button @onclick="Login">Login</button>
 <br/>
 <LogView></LogView>
 
@@ -43,26 +48,34 @@
     readonly List<(string homeserver, string username, string password)> records = new();
     (string homeserver, string username, string password) newRecordInput = ("", "", "");
 
+    List<LoginResponse> LoggedInSessions { get; set; } = new();
+    
     async Task Login() {
-        foreach (var (homeserver, username, password) in records) {
-            if (RuntimeCache.LoginSessions.Any(x => x.Value.LoginResponse.UserId == $"@{username}:{homeserver}")) continue;
-            var result = await MatrixAuth.Login(homeserver, username, password);
-            Console.WriteLine($"Obtained access token for {result.UserId}!");
-
-            var userinfo = new UserInfo {
-                LoginResponse = result
-            };
-            userinfo.Profile = await RuntimeCache.CurrentHomeServer.GetProfile(result.UserId);
-            RuntimeCache.LastUsedToken = result.AccessToken;
+        var loginTasks = records.Select(async record => {
+            var (homeserver, username, password) = record;
+            if (LoggedInSessions.Any(x => x.UserId == $"@{username}:{homeserver}")) return;
+            try {
+                var result = await HomeserverProvider.Login(homeserver, username, password);
+                if (result == null) {
+                    Console.WriteLine($"Failed to login to {homeserver} as {username}!");
+                    return;
+                }
+                Console.WriteLine($"Obtained access token for {result.UserId}!");
 
-            RuntimeCache.LoginSessions.Add(result.AccessToken, userinfo);
+                await MRUStorage.AddToken(result);
+                LoggedInSessions = await MRUStorage.GetAllTokens();
+            }
+            catch (Exception e) {
+                Console.WriteLine($"Failed to login to {homeserver} as {username}!");
+                Console.WriteLine(e);
+            }
             StateHasChanged();
-        }
-
-        await LocalStorageWrapper.SaveToLocalStorage(LocalStorage);
+        });
+        await Task.WhenAll(loginTasks);
     }
 
     private async Task FileChanged(InputFileChangeEventArgs obj) {
+        LoggedInSessions = await MRUStorage.GetAllTokens();
         Console.WriteLine(JsonSerializer.Serialize(obj, new JsonSerializerOptions {
             WriteIndented = true
         }));
@@ -78,7 +91,8 @@
         }
     }
 
-    private void AddRecord() {
+    private async Task AddRecord() {
+        LoggedInSessions = await MRUStorage.GetAllTokens();
         records.Add(newRecordInput);
         newRecordInput = ("", "", "");
     }
diff --git a/MatrixRoomUtils.Web/Pages/MediaLocator.razor b/MatrixRoomUtils.Web/Pages/MediaLocator.razor
index 38d1514..6221041 100644
--- a/MatrixRoomUtils.Web/Pages/MediaLocator.razor
+++ b/MatrixRoomUtils.Web/Pages/MediaLocator.razor
@@ -82,7 +82,6 @@
     }
 
     async Task addMoreHomeservers() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         var res = await Http.GetAsync("/homeservers.txt");
         var content = await res.Content.ReadAsStringAsync();
         homeservers.Clear();
@@ -93,10 +92,8 @@
         lines.ToList().ForEach(async line => {
             await sem.WaitAsync();
             try {
-                homeservers.Add(await rhs.ResolveHomeserverFromWellKnown(line));
+                homeservers.Add(await HomeserverResolver.ResolveHomeserverFromWellKnown(line));
                 StateHasChanged();
-                if (Random.Shared.Next(0, 101) == 50)
-                    await LocalStorageWrapper.SaveCacheToLocalStorage(LocalStorage);
             }
             catch (Exception e) {
                 Console.WriteLine(e);
diff --git a/MatrixRoomUtils.Web/Pages/ModalTest.razor b/MatrixRoomUtils.Web/Pages/ModalTest.razor
index d031dc2..f32c672 100644
--- a/MatrixRoomUtils.Web/Pages/ModalTest.razor
+++ b/MatrixRoomUtils.Web/Pages/ModalTest.razor
@@ -31,20 +31,6 @@
         double _y = 0;
         double multiplier = 1;
 
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-        //var rooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms();
-        
-        //rooms.ForEach(async room => {
-        //    var name = await room.GetNameAsync();
-        //    _windowInfos.Add(_windowInfos.Count, new WindowInfo() { X = _x, Y = _y, Title = name});
-        //    _x += 20;
-        //    _y += 20;
-        //    var dimension = await JsRuntime.InvokeAsync<WindowDimension>("getWindowDimensions");
-        //    if (_x > dimension.Width - 100) _x %= dimension.Width - 100;
-        //    if (_y > dimension.Height - 50) _y %= dimension.Height - 50;
-        //    StateHasChanged();
-        //});
-
         for (int i = 0; i < 200; i++) {
             var i1 = i;
             _windowInfos.Add(_windowInfos.Count, new WindowInfo() {
diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
index 0840ebf..8e2609f 100644
--- a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
+++ b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
@@ -1,9 +1,8 @@
 @page "/PolicyListEditor/{RoomId}"
 @using MatrixRoomUtils.Core.StateEventTypes
 @using System.Text.Json
+@using MatrixRoomUtils.Core.Helpers
 @using MatrixRoomUtils.Core.Responses
-@inject ILocalStorageService LocalStorage
-@inject NavigationManager NavigationManager
 <h3>Policy list editor - Editing @RoomId</h3>
 <hr/>
 
@@ -31,12 +30,13 @@ else {
         </tr>
         </thead>
         <tbody>
-        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && x.Content.Entity != null)) {
+        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && (x.TypedContent as PolicyRuleStateEventData).Entity is not null)) {
+            var policyData = policyEvent.TypedContent as PolicyRuleStateEventData;
             <tr>
-                <td>Entity: @policyEvent.Content.Entity<br/>State: @policyEvent.StateKey</td>
-                <td>@policyEvent.Content.Reason</td>
+                <td>Entity: @policyData.Entity<br/>State: @policyEvent.StateKey</td>
+                <td>@policyData.Reason</td>
                 <td>
-                    @policyEvent.Content.ExpiryDateTime
+                    @policyData.ExpiryDateTime
                 </td>
                 <td>
                     <button class="btn" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Edit</button>
@@ -47,7 +47,7 @@ else {
         </tbody>
     </table>
     <details>
-        <summary>Invalid events</summary>
+        <summary>Redacted events</summary>
         <table class="table table-striped table-hover" style="width: fit-Content;">
             <thead>
             <tr>
@@ -56,10 +56,11 @@ else {
             </tr>
             </thead>
             <tbody>
-            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && x.Content.Entity == null)) {
+            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.server" && (x.TypedContent as PolicyRuleStateEventData).Entity == null)) {
+                var policyData = policyEvent.TypedContent as PolicyRuleStateEventData;
                 <tr>
                     <td>@policyEvent.StateKey</td>
-                    <td>@policyEvent.Content.ToJson(false, true)</td>
+                    <td>@policyEvent.RawContent.ToJson(false, true)</td>
                 </tr>
             }
             </tbody>
@@ -82,12 +83,13 @@ else {
         </tr>
         </thead>
         <tbody>
-        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && x.Content.Entity != null)) {
+        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && (x.TypedContent as PolicyRuleStateEventData).Entity is not null)) {
+            var policyData = policyEvent.TypedContent as PolicyRuleStateEventData;
             <tr>
-                <td>Entity: @policyEvent.Content.Entity<br/>State: @policyEvent.StateKey</td>
-                <td>@policyEvent.Content.Reason</td>
+                <td>Entity: @policyData.Entity<br/>State: @policyEvent.StateKey</td>
+                <td>@policyData.Reason</td>
                 <td>
-                    @policyEvent.Content.ExpiryDateTime
+                    @policyData.ExpiryDateTime
                 </td>
                 <td>
                     <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
@@ -97,7 +99,7 @@ else {
         </tbody>
     </table>
     <details>
-        <summary>Invalid events</summary>
+        <summary>Redacted events</summary>
         <table class="table table-striped table-hover" style="width: fit-Content;">
             <thead>
             <tr>
@@ -106,10 +108,10 @@ else {
             </tr>
             </thead>
             <tbody>
-            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && x.Content.Entity == null)) {
+            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.room" && (x.TypedContent as PolicyRuleStateEventData).Entity == null)) {
                 <tr>
                     <td>@policyEvent.StateKey</td>
-                    <td>@policyEvent.Content.ToJson(false, true)</td>
+                    <td>@policyEvent.RawContent.ToJson(false, true)</td>
                 </tr>
             }
             </tbody>
@@ -135,17 +137,18 @@ else {
         </tr>
         </thead>
         <tbody>
-        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && x.Content.Entity != null)) {
+        @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && (x.TypedContent as PolicyRuleStateEventData).Entity is not null)) {
+            var policyData = policyEvent.TypedContent as PolicyRuleStateEventData;
             <tr>
                 @if (_enableAvatars) {
                     <td scope="col">
-                        <img style="width: 48px; height: 48px; aspect-ratio: unset; border-radius: 50%;" src="@(avatars.ContainsKey(policyEvent.Content.Entity) ? avatars[policyEvent.Content.Entity] : "")"/>
+                        <img style="width: 48px; height: 48px; aspect-ratio: unset; border-radius: 50%;" src="@(avatars.ContainsKey(policyData.Entity) ? avatars[policyData.Entity] : "")"/>
                     </td>
                 }
-                <td style="word-wrap: anywhere;">Entity: @string.Join("", policyEvent.Content.Entity.Take(64))<br/>State: @string.Join("", policyEvent.StateKey.Take(64))</td>
-                <td>@policyEvent.Content.Reason</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>
-                    @policyEvent.Content.ExpiryDateTime
+                    @policyData.ExpiryDateTime
                 </td>
                 <td>
                     <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
@@ -155,7 +158,7 @@ else {
         </tbody>
     </table>
     <details>
-        <summary>Invalid events</summary>
+        <summary>Redacted events</summary>
         <table class="table table-striped table-hover" style="width: fit-Content;">
             <thead>
             <tr>
@@ -164,10 +167,10 @@ else {
             </tr>
             </thead>
             <tbody>
-            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && x.Content.Entity == null)) {
+            @foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && (x.TypedContent as PolicyRuleStateEventData).Entity == null)) {
                 <tr>
                     <td>@policyEvent.StateKey</td>
-                    <td>@policyEvent.Content.ToJson(false, true)</td>
+                    <td>@policyEvent.RawContent.ToJson(false, true)</td>
                 </tr>
             }
             </tbody>
@@ -191,36 +194,32 @@ else {
     static readonly Dictionary<string, string?> avatars = new();
     static readonly Dictionary<string, RemoteHomeServer> servers = new();
 
-    public static List<StateEventResponse<PolicyRuleStateEventData>> PolicyEvents { get; set; } = new();
+    public static List<StateEventResponse> PolicyEvents { get; set; } = new();
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         await base.OnInitializedAsync();
-    // if(RuntimeCache.AccessToken == null || RuntimeCache.CurrentHomeserver == null)
-        if (RuntimeCache.CurrentHomeServer == null) {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
         RoomId = RoomId.Replace('~', '.');
         await LoadStatesAsync();
         Console.WriteLine("Policy list editor initialized!");
     }
 
     private async Task LoadStatesAsync() {
-    // using var client = new HttpClient();
-    // client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", LocalStorageWrapper.AccessToken);
-    // var response = await client.GetAsync($"{LocalStorageWrapper.CurrentHomeserver}/_matrix/client/v3/rooms/{RoomId}/state");
-    // var Content = await response.Content.ReadAsStringAsync();
-    // Console.WriteLine(JsonSerializer.Deserialize<object>(Content).ToJson());
-    // var stateEvents = JsonSerializer.Deserialize<List<StateEventResponse>>(Content);
-        var room = await RuntimeCache.CurrentHomeServer.GetRoom(RoomId);
-        var stateEventsQuery = await room.GetStateAsync("");
-        if (stateEventsQuery == null) {
-            Console.WriteLine("state events query is null!!!");
+        var hs = await MRUStorage.GetCurrentSession();
+        var room = await hs.GetRoom(RoomId);
+
+        var states = room.GetFullStateAsync();
+        await foreach (var state in states) {
+            if (!state.Type.StartsWith("m.policy.rule")) continue;
+            PolicyEvents.Add(state);
         }
-        var stateEvents = stateEventsQuery.Value.Deserialize<List<StateEventResponse>>();
-        PolicyEvents = stateEvents.Where(x => x.Type.StartsWith("m.policy.rule"))
-            .Select(x => JsonSerializer.Deserialize<StateEventResponse<PolicyRuleStateEventData>>(JsonSerializer.Serialize(x))).ToList();
+        
+        
+        // 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();
     }
 
@@ -228,10 +227,10 @@ else {
         try {
             if (avatars.ContainsKey(userId)) return;
             var hs = userId.Split(':')[1];
-            var server = servers.ContainsKey(hs) ? servers[hs] : await new RemoteHomeServer(userId.Split(':')[1]).Configure();
+            var server = servers.ContainsKey(hs) ? servers[hs] : new RemoteHomeServer(userId.Split(':')[1]);
             if (!servers.ContainsKey(hs)) servers.Add(hs, server);
             var profile = await server.GetProfile(userId);
-            avatars.Add(userId, server.ResolveMediaUri(profile.AvatarUrl));
+            avatars.Add(userId, MediaResolver.ResolveMediaUri(server.FullHomeServerDomain, profile.AvatarUrl));
             servers.Add(userId, server);
             StateHasChanged();
         }
@@ -241,8 +240,8 @@ else {
     }
 
     private async Task GetAllAvatars() {
-        foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && x.Content.Entity != null)) {
-            await GetAvatar(policyEvent.Content.Entity);
+        foreach (var policyEvent in PolicyEvents.Where(x => x.Type == "m.policy.rule.user" && (x.TypedContent as PolicyRuleStateEventData).Entity is not null)) {
+            await GetAvatar((policyEvent.TypedContent as PolicyRuleStateEventData).Entity);
         }
         StateHasChanged();
     }
diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
index 8f711b5..4db2b5a 100644
--- a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
+++ b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
@@ -1,4 +1,7 @@
 @page "/PolicyListEditor"
+@using System.Text.Json.Serialization
+@using MatrixRoomUtils.Core.Interfaces
+@using MatrixRoomUtils.Core.StateEventTypes
 @inject ILocalStorageService LocalStorage
 @inject NavigationManager NavigationManager
 <h3>Policy list editor - Room list</h3>
@@ -39,28 +42,26 @@ else {
     private int totalRoomCount { get; set; }
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         await base.OnInitializedAsync();
-        if (RuntimeCache.CurrentHomeServer == null) {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
         await EnumeratePolicyRooms();
         Console.WriteLine("Policy list editor initialized!");
     }
 
     private async Task EnumeratePolicyRooms() {
-        var xxxrooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms();
-        totalRoomCount = xxxrooms.Count;
+        var hs = await MRUStorage.GetCurrentSession();
+        var rooms = await hs.GetJoinedRooms();
+        totalRoomCount = rooms.Count;
         StateHasChanged();
 
-        var xxxsemaphore = new SemaphoreSlim(1000);
-        var xxxtasks = new List<Task<PolicyRoomInfo?>>();
-        foreach (var room in xxxrooms) {
-            xxxtasks.Add(GetPolicyRoomInfo(room.RoomId, xxxsemaphore));
+        var semaphore = new SemaphoreSlim(8);
+        var tasks = new List<Task<PolicyRoomInfo?>>();
+        foreach (var room in rooms) {
+            tasks.Add(GetPolicyRoomInfo(room.RoomId, semaphore));
         }
-        var xxxresults = await Task.WhenAll(xxxtasks);
-        PolicyRoomList.AddRange(xxxresults.Where(x => x != null).Select(x => x.Value));
+        var results = await Task.WhenAll(tasks);
+        PolicyRoomList.AddRange(results.Where(x => x is not null).Select(x => x.Value));
 
         Console.WriteLine($"Detected policy lists: {PolicyRoomList.ToJson()}");
     }
@@ -68,15 +69,15 @@ else {
     private async Task<PolicyRoomInfo?> GetPolicyRoomInfo(string room, SemaphoreSlim semaphore) {
         try {
             await semaphore.WaitAsync();
+            var hs = await MRUStorage.GetCurrentSession();
             PolicyRoomInfo roomInfo = new() {
                 RoomId = room
             };
-            var r = await RuntimeCache.CurrentHomeServer.GetRoom(room);
-            var shortcodeState = await r.GetStateAsync("org.matrix.mjolnir.shortcode");
-            if (!shortcodeState.HasValue) return null;
-            roomInfo.Shortcode = shortcodeState.Value.TryGetProperty("shortcode", out var shortcode) ? shortcode.GetString() : null;
+            var r = await hs.GetRoom(room);
+            var shortcodeState = await r.GetStateAsync<MjolnirShortcodeEventData>("org.matrix.mjolnir.shortcode");
+            roomInfo.Shortcode = shortcodeState.Shortcode;
 
-            if (roomInfo.Shortcode != null) {
+            if (roomInfo.Shortcode is not null) {
                 roomInfo.Name = await r.GetNameAsync();
                 return roomInfo;
             }
@@ -90,6 +91,8 @@ else {
         }
     }
 
+    
+
     public struct PolicyRoomInfo {
         public
             string RoomId { get; set; }
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor
index 9b0bb88..087adf8 100644
--- a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManager.razor
@@ -34,25 +34,22 @@ else {
 <LogView></LogView>
 
 @code {
-    public List<Room> Rooms { get; set; } = new();
-    public List<Room> Spaces { get; set; } = new();
+    public List<GenericRoom> Rooms { get; set; } = new();
+    public List<GenericRoom> Spaces { get; set; } = new();
 
     protected override async Task OnInitializedAsync() {
         Console.WriteLine("Initializing room manager");
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         Console.WriteLine("Loaded from local storage");
         await base.OnInitializedAsync();
         Console.WriteLine("Initialized base");
-        if (RuntimeCache.CurrentHomeServer == null) {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
         Console.WriteLine("Fetching joined rooms");
-        var _rooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms();
+        var _rooms = await hs.GetJoinedRooms();
         StateHasChanged();
         Console.WriteLine($"Got {_rooms.Count} rooms");
         var semaphore = new SemaphoreSlim(10);
-        var tasks = new List<Task<Room?>>();
+        var tasks = new List<Task<GenericRoom?>>();
         foreach (var room in _rooms) {
             tasks.Add(CheckIfSpace(room, semaphore));
         }
@@ -61,14 +58,14 @@ else {
         Console.WriteLine("Fetched joined rooms!");
     }
 
-    private async Task<Room?> CheckIfSpace(Room room, SemaphoreSlim semaphore) {
+    private async Task<GenericRoom?> CheckIfSpace(GenericRoom room, SemaphoreSlim semaphore) {
         await semaphore.WaitAsync();
     // Console.WriteLine($"Checking if {room.RoomId} is a space");
         try {
             var state = await room.GetStateAsync<CreateEvent>("m.room.create");
-            if (state != null) {
+            if (state is not null) {
     //Console.WriteLine(state.Value.ToJson());
-                if (state.Type != null) {
+                if (state.Type is not null) {
                     if (state.Type == "m.space") {
                         Console.WriteLine($"Room {room.RoomId} is a space!");
                         Spaces.Add(room);
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
index 80d852a..8368aa5 100644
--- a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
@@ -1,339 +1,339 @@
-@page "/RoomManagerCreateRoom"
-@using MatrixRoomUtils.Core.Responses
-@using System.Text.Json
-@using System.Reflection
-@using MatrixRoomUtils.Core.StateEventTypes
-@using MatrixRoomUtils.Web.Classes.RoomCreationTemplates
-@* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not *@
-@using MatrixRoomUtils.Web.Shared.SimpleComponents
-
-<h3>Room Manager - Create Room</h3>
-
-@* <pre Contenteditable="true" @onkeypress="@JsonChanged" ="JsonString">@JsonString</pre> *@
-<style>
-    table.table-top-first-tr tr td:first-child {
-        vertical-align: top;
-    }
-</style>
-<table class="table-top-first-tr">
-    <tr>
-        <td style="padding-bottom: 16px;">Preset:</td>
-        <td style="padding-bottom: 16px;">
-            <InputSelect @bind-Value="@RoomPreset">
-                @foreach (var createRoomRequest in Presets) {
-                    <option value="@createRoomRequest.Key">@createRoomRequest.Key</option>
-                }
-            </InputSelect>
-        </td>
-    </tr>
-    @if (creationEvent != null) {
-        <tr>
-            <td>Room name:</td>
-            <td>
-                <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox>
-            </td>
-        </tr>
-        <tr>
-            <td>Room alias (localpart):</td>
-            <td>
-                <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox>
-            </td>
-        </tr>
-        <tr>
-            <td>Room type:</td>
-            <td>
-                <InputSelect @bind-Value="@creationEvent._creationContentBaseType.Type">
-                    <option value="">Room</option>
-                    <option value="m.space">Space</option>
-                </InputSelect>
-                <FancyTextBox @bind-Value="@creationEvent._creationContentBaseType.Type"></FancyTextBox>
-            </td>
-        </tr>
-        <tr>
-            <td style="padding-top: 16px;">History visibility:</td>
-            <td style="padding-top: 16px;">
-                @* <InputSelect @bind-Value="@creationEvent.HistoryVisibility"> *@
-                @*     <option value="invited">Invited</option> *@
-                @*     <option value="joined">Joined</option> *@
-                @*     <option value="shared">Shared</option> *@
-                @*     <option value="world_readable">World readable</option> *@
-                @* </InputSelect> *@
-            </td>
-        </tr>
-        <tr>
-            <td>Guest access:</td>
-            <td>
-                <ToggleSlider Value="guestAccessEvent.IsGuestAccessEnabled" ValueChanged="@(v => { guestAccessEvent.IsGuestAccessEnabled = v; creationEvent["m.room.guest_access"].Content = guestAccessEvent; })">@(guestAccessEvent.IsGuestAccessEnabled ? "Guests can join" : "Guests cannot join") (@guestAccessEvent.GuestAccess)</ToggleSlider>
-                @* <InputSelect @bind-Value="@creationEvent.GuestAccess"> *@
-                @* <option value="can_join">Can join</option> *@
-                @* <option value="forbidden">Forbidden</option> *@
-                @* </InputSelect> *@
-            </td>
-        </tr>
-
-        <tr>
-            <td>Room icon:</td>
-            <td>
-                <img src="@RuntimeCache.CurrentHomeServer?.ResolveMediaUri(creationEvent.RoomIcon ?? "")" style="width: 128px; height: 128px; border-radius: 50%;"/>
-                <div style="    display: inline-block;
-                            vertical-align: middle;">
-                    <FancyTextBox @bind-Value="@creationEvent.RoomIcon"></FancyTextBox><br/>
-                    <InputFile OnChange="RoomIconFilePicked"></InputFile>
-                </div>
-
-            </td>
-        </tr>
-        <tr>
-            <td>Permissions:</td>
-            <details>
-                <summary>@creationEvent.PowerLevelContentOverride.Users.Count members</summary>
-                @foreach (var user in creationEvent.PowerLevelContentOverride.Events.Keys) {
-                    var _event = user;
-                    <tr>
-                        <td><FancyTextBox Formatter="@GetPermissionFriendlyName" Value="@_event" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }"></FancyTextBox>:</td>
-                        <td>
-                            <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/>
-                        </td>
-                    </tr>
-                }
-                @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) {
-                    var _user = user;
-                    <tr>
-                        <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td>
-                        <td>
-                            <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/>
-                        </td>
-                    </tr>
-                }
-            </details>
-        </tr>
-        <tr>
-            <td>Server ACLs:</td>
-            <td>
-                <details>
-                    <summary>@(creationEvent.ServerACLs.Allow.Count) allow rules</summary>
-                    <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLAllowRules"></StringListEditor>
-                </details>
-                <details>
-                    <summary>@creationEvent.ServerACLs.Deny.Count deny rules</summary>
-                    <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLDenyRules"></StringListEditor>
-                </details>
-            </td>
-        </tr>
-
-        <tr>
-            <td>Invited members:</td>
-            <td>
-                <details>
-                    <summary>@creationEvent.InitialState.Count(x => x.Type == "m.room.member") members</summary>
-                    <button @onclick="() => { RuntimeCache.LoginSessions.Select(x => x.Value.LoginResponse.UserId).ToList().ForEach(InviteMember); }">Invite all logged in accounts</button>
-                    @foreach (var member in creationEvent.InitialState.Where(x => x.Type == "m.room.member" && x.StateKey != RuntimeCache.CurrentHomeServer.UserId)) {
-                        <UserListItem UserId="@member.StateKey"></UserListItem>
-                    }
-                </details>
-            </td>
-        </tr>
-
-        @* Initial states, should remain at bottom? *@
-
-        <tr>
-            <td style="vertical-align: top;">Initial states:</td>
-            <td>
-                <details>
-
-                    @code{
-
-                        private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" };
-
-                    }
-
-                    <summary>@creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary>
-                    <table>
-                        @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) {
-                            <tr>
-                                <td style="vertical-align: top;">
-                                    @(initialState.Type):
-                                    @if (!string.IsNullOrEmpty(initialState.StateKey)) {
-                                        <br/>
-                                        <span>(@initialState.StateKey)</span>
-                                    }
-                                </td>
-
-                                <td>
-                                    <pre>@JsonSerializer.Serialize(initialState.Content, new JsonSerializerOptions { WriteIndented = true })</pre>
-                                </td>
-                            </tr>
-                        }
-                    </table>
-                </details>
-                <details>
-                    <summary>@creationEvent.InitialState.Count initial states</summary>
-                    <table>
-                        @foreach (var initialState in creationEvent.InitialState) {
-                            var _state = initialState;
-                            <tr>
-                                <td style="vertical-align: top;">
-                                    <span>@(_state.Type):</span><br/>
-                                    <button @onclick="() => { creationEvent.InitialState.Remove(_state); StateHasChanged(); }">Remove</button>
-                                </td>
-
-                                <td>
-                                    <pre>@JsonSerializer.Serialize(_state.Content, new JsonSerializerOptions { WriteIndented = true })</pre>
-                                </td>
-                            </tr>
-                        }
-                    </table>
-                </details>
-            </td>
-        </tr>
-    }
-</table>
-<button @onclick="CreateRoom">Create room</button>
-<br/>
-<details>
-    <summary>Creation JSON</summary>
-    <pre>
-        @creationEvent.ToJson(ignoreNull: true)
-    </pre>
-</details>
-<details open>
-    <summary>Creation JSON (with null values)</summary>
-    <pre>
-    @creationEvent.ToJson()
-    </pre>
-</details>
-
-
-@code {
-
-    private string RoomPreset {
-        get {
-            if (Presets.ContainsValue(creationEvent)) {
-                return Presets.First(x => x.Value == creationEvent).Key;
-            }
-            return "Not a preset";
-        }
-        set {
-            creationEvent = Presets[value];
-            JsonChanged();
-            OverwriteWrappedPropertiesFromEvent();
-            creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
-            creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
-            guestAccessEvent = creationEvent["m.room.guest_access"].As<GuestAccessData>().Content;
-
-            Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}");
-            Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}");
-            creationEvent["m.room.guest_access"].As<GuestAccessData>().Content.IsGuestAccessEnabled = true;
-            Console.WriteLine("-- Created new guest access content --");
-            Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}");
-            Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}");
-            Console.WriteLine($"Creation event casted back: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}");
-            StateHasChanged();
-        }
-    }
-
-    private Dictionary<string, string> creationEventValidationErrors { get; set; } = new();
-
-    private CreateRoomRequest creationEvent { get; set; }
-    GuestAccessData guestAccessEvent { get; set; }
-
-    private Dictionary<string, CreateRoomRequest> Presets { get; set; } = new();
-
-    protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-
-    //creationEvent = Presets["Default room"] = 
-        foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) {
-            Console.WriteLine($"Found room creation template in class: {x.FullName}");
-            var instance = (IRoomCreationTemplate)Activator.CreateInstance(x);
-            Presets[instance.Name] = instance.CreateRoomRequest;
-        }
-        Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
-
-        if (!Presets.ContainsKey("Default")) {
-            Console.WriteLine($"No default room found in {Presets.Count} presets: {string.Join(", ", Presets.Keys)}");
-        }
-        else RoomPreset = "Default";
-
-        await base.OnInitializedAsync();
-    }
-
-    private void JsonChanged() => Console.WriteLine(creationEvent.ToJson());
-
-    //wrappers
-    private List<string> ServerACLAllowRules { get; set; } = new();
-    private List<string> ServerACLDenyRules { get; set; } = new();
-
-    private void OverwriteWrappedPropertiesFromEvent() {
-        Console.WriteLine("Overwriting wrapped properties from event");
-        ServerACLAllowRules = creationEvent.ServerACLs.Allow;
-        ServerACLDenyRules = creationEvent.ServerACLs.Deny;
-    }
-
-    private async Task OverwriteWrappedProperties() {
-        Console.WriteLine("Overwriting wrapped properties");
-        Console.WriteLine($"Allow: {ServerACLAllowRules.Count}: {string.Join(", ", ServerACLAllowRules)}");
-        Console.WriteLine($"Deny: {ServerACLDenyRules.Count}: {string.Join(", ", ServerACLDenyRules)}");
-        creationEvent.ServerACLs = new ServerACLData {
-            Allow = ServerACLAllowRules,
-            Deny = ServerACLDenyRules,
-            AllowIpLiterals = creationEvent.ServerACLs.AllowIpLiterals
-        };
-
-        StateHasChanged();
-    }
-
-    private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) {
-        var res = await RuntimeCache.CurrentHomeServer.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType);
-        Console.WriteLine(res);
-        creationEvent.RoomIcon = res;
-        StateHasChanged();
-    }
-
-    private async Task CreateRoom() {
-        Console.WriteLine("Create room");
-        Console.WriteLine(creationEvent.ToJson());
-        creationEvent.CreationContent.Add("rory.gay.created_using", "Rory&::MatrixRoomUtils (https://mru.rory.gay)");
-    //creationEvent.CreationContent.Add();
-        var id = await RuntimeCache.CurrentHomeServer.CreateRoom(creationEvent);
-    // NavigationManager.NavigateTo($"/RoomManager/{id.RoomId.Replace('.','~')}");
-    }
-
-    private void InviteMember(string mxid) {
-        if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && RuntimeCache.CurrentHomeServer.UserId != mxid)
-            creationEvent.InitialState.Add(new StateEvent {
-                Type = "m.room.member",
-                StateKey = mxid,
-                Content = new {
-                    membership = "invite",
-                    reason = "Automatically invited at room creation time."
-                }
-            });
-    }
-
-    private string GetStateFriendlyName(string key) => key switch {
-        "m.room.history_visibility" => "History visibility",
-        "m.room.guest_access" => "Guest access",
-        "m.room.join_rules" => "Join rules",
-        "m.room.server_acl" => "Server ACL",
-        "m.room.avatar" => "Avatar",
-        _ => key
-        };
-
-    private string GetPermissionFriendlyName(string key) => key switch {
-        "m.reaction" => "Send reaction",
-        "m.room.avatar" => "Change room icon",
-        "m.room.canonical_alias" => "Change room alias",
-        "m.room.encryption" => "Enable encryption",
-        "m.room.history_visibility" => "Change history visibility",
-        "m.room.name" => "Change room name",
-        "m.room.power_levels" => "Change power levels",
-        "m.room.tombstone" => "Upgrade room",
-        "m.room.topic" => "Change room topic",
-        "m.room.pinned_events" => "Pin events",
-        "m.room.server_acl" => "Change server ACLs",
-        _ => key
-        };
-
-    }
-
+@* @page "/RoomManagerCreateRoom" *@
+@* @using MatrixRoomUtils.Core.Responses *@
+@* @using System.Text.Json *@
+@* @using System.Reflection *@
+@* @using MatrixRoomUtils.Core.Helpers *@
+@* @using MatrixRoomUtils.Core.StateEventTypes *@
+@* @using MatrixRoomUtils.Web.Classes.RoomCreationTemplates *@
+@* $1$ ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not #1# *@
+@* @using MatrixRoomUtils.Web.Shared.SimpleComponents *@
+@* *@
+@* <h3>Room Manager - Create Room</h3> *@
+@* *@
+@* $1$ <pre Contenteditable="true" @onkeypress="@JsonChanged" ="JsonString">@JsonString</pre> #1# *@
+@* <style> *@
+@*     table.table-top-first-tr tr td:first-child { *@
+@*         vertical-align: top; *@
+@*     } *@
+@* </style> *@
+@* <table class="table-top-first-tr"> *@
+@*     <tr> *@
+@*         <td style="padding-bottom: 16px;">Preset:</td> *@
+@*         <td style="padding-bottom: 16px;"> *@
+@*             <InputSelect @bind-Value="@RoomPreset"> *@
+@*                 @foreach (var createRoomRequest in Presets) { *@
+@*                     <option value="@createRoomRequest.Key">@createRoomRequest.Key</option> *@
+@*                 } *@
+@*             </InputSelect> *@
+@*         </td> *@
+@*     </tr> *@
+@*     @if (creationEvent is not null) { *@
+@*         <tr> *@
+@*             <td>Room name:</td> *@
+@*             <td> *@
+@*                 <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox> *@
+@*             </td> *@
+@*         </tr> *@
+@*         <tr> *@
+@*             <td>Room alias (localpart):</td> *@
+@*             <td> *@
+@*                 <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox> *@
+@*             </td> *@
+@*         </tr> *@
+@*         <tr> *@
+@*             <td>Room type:</td> *@
+@*             <td> *@
+@*                 <InputSelect @bind-Value="@creationEvent._creationContentBaseType.Type"> *@
+@*                     <option value="">Room</option> *@
+@*                     <option value="m.space">Space</option> *@
+@*                 </InputSelect> *@
+@*                 <FancyTextBox @bind-Value="@creationEvent._creationContentBaseType.Type"></FancyTextBox> *@
+@*             </td> *@
+@*         </tr> *@
+@*         <tr> *@
+@*             <td style="padding-top: 16px;">History visibility:</td> *@
+@*             <td style="padding-top: 16px;"> *@
+@*                 $1$ <InputSelect @bind-Value="@creationEvent.HistoryVisibility"> #1# *@
+@*                 $1$     <option value="invited">Invited</option> #1# *@
+@*                 $1$     <option value="joined">Joined</option> #1# *@
+@*                 $1$     <option value="shared">Shared</option> #1# *@
+@*                 $1$     <option value="world_readable">World readable</option> #1# *@
+@*                 $1$ </InputSelect> #1# *@
+@*             </td> *@
+@*         </tr> *@
+@*         <tr> *@
+@*             <td>Guest access:</td> *@
+@*             <td> *@
+@*                 <ToggleSlider Value="guestAccessEvent.IsGuestAccessEnabled" ValueChanged="@(v => { guestAccessEvent.IsGuestAccessEnabled = v; creationEvent["m.room.guest_access"].Content = guestAccessEvent; })">@(guestAccessEvent.IsGuestAccessEnabled ? "Guests can join" : "Guests cannot join") (@guestAccessEvent.GuestAccess)</ToggleSlider> *@
+@*                 $1$ <InputSelect @bind-Value="@creationEvent.GuestAccess"> #1# *@
+@*                 $1$ <option value="can_join">Can join</option> #1# *@
+@*                 $1$ <option value="forbidden">Forbidden</option> #1# *@
+@*                 $1$ </InputSelect> #1# *@
+@*             </td> *@
+@*         </tr> *@
+@* *@
+@*         <tr> *@
+@*             <td>Room icon:</td> *@
+@*             <td> *@
+@*                 <img src="@MediaResolver.ResolveMediaUri(creationEvent.RoomIcon ?? "")" style="width: 128px; height: 128px; border-radius: 50%;"/> *@
+@*                 <div style="    display: inline-block; *@
+@*                             vertical-align: middle;"> *@
+@*                     <FancyTextBox @bind-Value="@creationEvent.RoomIcon"></FancyTextBox><br/> *@
+@*                     <InputFile OnChange="RoomIconFilePicked"></InputFile> *@
+@*                 </div> *@
+@* *@
+@*             </td> *@
+@*         </tr> *@
+@*         <tr> *@
+@*             <td>Permissions:</td> *@
+@*             <details> *@
+@*                 <summary>@creationEvent.PowerLevelContentOverride.Users.Count members</summary> *@
+@*                 @foreach (var user in creationEvent.PowerLevelContentOverride.Events.Keys) { *@
+@*                     var _event = user; *@
+@*                     <tr> *@
+@*                         <td><FancyTextBox Formatter="@GetPermissionFriendlyName" Value="@_event" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }"></FancyTextBox>:</td> *@
+@*                         <td> *@
+@*                             <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/> *@
+@*                         </td> *@
+@*                     </tr> *@
+@*                 } *@
+@*                 @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) { *@
+@*                     var _user = user; *@
+@*                     <tr> *@
+@*                         <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td> *@
+@*                         <td> *@
+@*                             <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/> *@
+@*                         </td> *@
+@*                     </tr> *@
+@*                 } *@
+@*             </details> *@
+@*         </tr> *@
+@*         <tr> *@
+@*             <td>Server ACLs:</td> *@
+@*             <td> *@
+@*                 <details> *@
+@*                     <summary>@(creationEvent["server"].ServerACLs.Allow.Count) allow rules</summary> *@
+@*                     <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLAllowRules"></StringListEditor> *@
+@*                 </details> *@
+@*                 <details> *@
+@*                     <summary>@creationEvent.ServerACLs.Deny.Count deny rules</summary> *@
+@*                     <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLDenyRules"></StringListEditor> *@
+@*                 </details> *@
+@*             </td> *@
+@*         </tr> *@
+@* *@
+@*         <tr> *@
+@*             <td>Invited members:</td> *@
+@*             <td> *@
+@*                 <details> *@
+@*                     <summary>@creationEvent.InitialState.Count(x => x.Type == "m.room.member") members</summary> *@
+@*                     <button @onclick="() => { RuntimeCache.LoginSessions.Select(x => x.Value.LoginResponse.UserId).ToList().ForEach(InviteMember); }">Invite all logged in accounts</button> *@
+@*                     @foreach (var member in creationEvent.InitialState.Where(x => x.Type == "m.room.member" && x.StateKey != RuntimeCache.CurrentHomeServer.UserId)) { *@
+@*                         <UserListItem UserId="@member.StateKey"></UserListItem> *@
+@*                     } *@
+@*                 </details> *@
+@*             </td> *@
+@*         </tr> *@
+@* *@
+@*         $1$ Initial states, should remain at bottom? #1# *@
+@* *@
+@*         <tr> *@
+@*             <td style="vertical-align: top;">Initial states:</td> *@
+@*             <td> *@
+@*                 <details> *@
+@* *@
+@*                     @code{ *@
+@* *@
+@*                         private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" }; *@
+@* *@
+@*                     } *@
+@* *@
+@*                     <summary>@creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary> *@
+@*                     <table> *@
+@*                         @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) { *@
+@*                             <tr> *@
+@*                                 <td style="vertical-align: top;"> *@
+@*                                     @(initialState.Type): *@
+@*                                     @if (!string.IsNullOrEmpty(initialState.StateKey)) { *@
+@*                                         <br/> *@
+@*                                         <span>(@initialState.StateKey)</span> *@
+@*                                     } *@
+@*                                 </td> *@
+@* *@
+@*                                 <td> *@
+@*                                     <pre>@JsonSerializer.Serialize(initialState.Content, new JsonSerializerOptions { WriteIndented = true })</pre> *@
+@*                                 </td> *@
+@*                             </tr> *@
+@*                         } *@
+@*                     </table> *@
+@*                 </details> *@
+@*                 <details> *@
+@*                     <summary>@creationEvent.InitialState.Count initial states</summary> *@
+@*                     <table> *@
+@*                         @foreach (var initialState in creationEvent.InitialState) { *@
+@*                             var _state = initialState; *@
+@*                             <tr> *@
+@*                                 <td style="vertical-align: top;"> *@
+@*                                     <span>@(_state.Type):</span><br/> *@
+@*                                     <button @onclick="() => { creationEvent.InitialState.Remove(_state); StateHasChanged(); }">Remove</button> *@
+@*                                 </td> *@
+@* *@
+@*                                 <td> *@
+@*                                     <pre>@JsonSerializer.Serialize(_state.Content, new JsonSerializerOptions { WriteIndented = true })</pre> *@
+@*                                 </td> *@
+@*                             </tr> *@
+@*                         } *@
+@*                     </table> *@
+@*                 </details> *@
+@*             </td> *@
+@*         </tr> *@
+@*     } *@
+@* </table> *@
+@* <button @onclick="CreateRoom">Create room</button> *@
+@* <br/> *@
+@* <details> *@
+@*     <summary>Creation JSON</summary> *@
+@*     <pre> *@
+@*         @creationEvent.ToJson(ignoreNull: true) *@
+@*     </pre> *@
+@* </details> *@
+@* <details open> *@
+@*     <summary>Creation JSON (with null values)</summary> *@
+@*     <pre> *@
+@*     @creationEvent.ToJson() *@
+@*     </pre> *@
+@* </details> *@
+@* *@
+@* *@
+@* @code { *@
+@* *@
+@*     private string RoomPreset { *@
+@*         get { *@
+@*             if (Presets.ContainsValue(creationEvent)) { *@
+@*                 return Presets.First(x => x.Value == creationEvent).Key; *@
+@*             } *@
+@*             return "Not a preset"; *@
+@*         } *@
+@*         set { *@
+@*             creationEvent = Presets[value]; *@
+@*             JsonChanged(); *@
+@*             OverwriteWrappedPropertiesFromEvent(); *@
+@*             creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); *@
+@*             creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); *@
+@*             guestAccessEvent = creationEvent["m.room.guest_access"].As<GuestAccessData>().Content; *@
+@* *@
+@*             Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}"); *@
+@*             Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}"); *@
+@*             creationEvent["m.room.guest_access"].As<GuestAccessData>().Content.IsGuestAccessEnabled = true; *@
+@*             Console.WriteLine("-- Created new guest access content --"); *@
+@*             Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}"); *@
+@*             Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}"); *@
+@*             Console.WriteLine($"Creation event casted back: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}"); *@
+@*             StateHasChanged(); *@
+@*         } *@
+@*     } *@
+@* *@
+@*     private Dictionary<string, string> creationEventValidationErrors { get; set; } = new(); *@
+@* *@
+@*     private CreateRoomRequest creationEvent { get; set; } *@
+@*     GuestAccessData guestAccessEvent { get; set; } *@
+@* *@
+@*     private Dictionary<string, CreateRoomRequest> Presets { get; set; } = new(); *@
+@* *@
+@*     protected override async Task OnInitializedAsync() { *@
+@* *@
+@*     //creationEvent = Presets["Default room"] =  *@
+@*         foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) { *@
+@*             Console.WriteLine($"Found room creation template in class: {x.FullName}"); *@
+@*             var instance = (IRoomCreationTemplate)Activator.CreateInstance(x); *@
+@*             Presets[instance.Name] = instance.CreateRoomRequest; *@
+@*         } *@
+@*         Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); *@
+@* *@
+@*         if (!Presets.ContainsKey("Default")) { *@
+@*             Console.WriteLine($"No default room found in {Presets.Count} presets: {string.Join(", ", Presets.Keys)}"); *@
+@*         } *@
+@*         else RoomPreset = "Default"; *@
+@* *@
+@*         await base.OnInitializedAsync(); *@
+@*     } *@
+@* *@
+@*     private void JsonChanged() => Console.WriteLine(creationEvent.ToJson()); *@
+@* *@
+@*     //wrappers *@
+@*     private List<string> ServerACLAllowRules { get; set; } = new(); *@
+@*     private List<string> ServerACLDenyRules { get; set; } = new(); *@
+@* *@
+@*     private void OverwriteWrappedPropertiesFromEvent() { *@
+@*         Console.WriteLine("Overwriting wrapped properties from event"); *@
+@*         ServerACLAllowRules = creationEvent.ServerACLs.Allow; *@
+@*         ServerACLDenyRules = creationEvent.ServerACLs.Deny; *@
+@*     } *@
+@* *@
+@*     private async Task OverwriteWrappedProperties() { *@
+@*         Console.WriteLine("Overwriting wrapped properties"); *@
+@*         Console.WriteLine($"Allow: {ServerACLAllowRules.Count}: {string.Join(", ", ServerACLAllowRules)}"); *@
+@*         Console.WriteLine($"Deny: {ServerACLDenyRules.Count}: {string.Join(", ", ServerACLDenyRules)}"); *@
+@*         creationEvent.ServerACLs = new ServerACLData { *@
+@*             Allow = ServerACLAllowRules, *@
+@*             Deny = ServerACLDenyRules, *@
+@*             AllowIpLiterals = creationEvent.ServerACLs.AllowIpLiterals *@
+@*         }; *@
+@* *@
+@*         StateHasChanged(); *@
+@*     } *@
+@* *@
+@*     private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) { *@
+@*         var res = await RuntimeCache.CurrentHomeServer.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); *@
+@*         Console.WriteLine(res); *@
+@*         creationEvent.RoomIcon = res; *@
+@*         StateHasChanged(); *@
+@*     } *@
+@* *@
+@*     private async Task CreateRoom() { *@
+@*         Console.WriteLine("Create room"); *@
+@*         Console.WriteLine(creationEvent.ToJson()); *@
+@*         creationEvent.CreationContent.Add("rory.gay.created_using", "Rory&::MatrixRoomUtils (https://mru.rory.gay)"); *@
+@*     //creationEvent.CreationContent.Add(); *@
+@*         var id = await RuntimeCache.CurrentHomeServer.CreateRoom(creationEvent); *@
+@*     // NavigationManager.NavigateTo($"/RoomManager/{id.RoomId.Replace('.','~')}"); *@
+@*     } *@
+@* *@
+@*     private void InviteMember(string mxid) { *@
+@*         if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && RuntimeCache.CurrentHomeServer.UserId != mxid) *@
+@*             creationEvent.InitialState.Add(new StateEvent { *@
+@*                 Type = "m.room.member", *@
+@*                 StateKey = mxid, *@
+@*                 Content = new { *@
+@*                     membership = "invite", *@
+@*                     reason = "Automatically invited at room creation time." *@
+@*                 } *@
+@*             }); *@
+@*     } *@
+@* *@
+@*     private string GetStateFriendlyName(string key) => key switch { *@
+@*         "m.room.history_visibility" => "History visibility", *@
+@*         "m.room.guest_access" => "Guest access", *@
+@*         "m.room.join_rules" => "Join rules", *@
+@*         "m.room.server_acl" => "Server ACL", *@
+@*         "m.room.avatar" => "Avatar", *@
+@*         _ => key *@
+@*         }; *@
+@* *@
+@*     private string GetPermissionFriendlyName(string key) => key switch { *@
+@*         "m.reaction" => "Send reaction", *@
+@*         "m.room.avatar" => "Change room icon", *@
+@*         "m.room.canonical_alias" => "Change room alias", *@
+@*         "m.room.encryption" => "Enable encryption", *@
+@*         "m.room.history_visibility" => "Change history visibility", *@
+@*         "m.room.name" => "Change room name", *@
+@*         "m.room.power_levels" => "Change power levels", *@
+@*         "m.room.tombstone" => "Upgrade room", *@
+@*         "m.room.topic" => "Change room topic", *@
+@*         "m.room.pinned_events" => "Pin events", *@
+@*         "m.room.server_acl" => "Change server ACLs", *@
+@*         _ => key *@
+@*         }; *@
+@* *@
+@*     } *@
+@* *@
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
index a9c71c4..afa39b9 100644
--- a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
@@ -14,7 +14,7 @@
     <summary style="background: #fff1;">State list</summary>
     @foreach (var stateEvent in States.OrderBy(x => x.StateKey).ThenBy(x => x.Type)) {
         <p>@stateEvent.StateKey/@stateEvent.Type:</p>
-        <pre>@stateEvent.Content.ToJson()</pre>
+        <pre>@stateEvent.RawContent.ToJson()</pre>
     }
 </details>
 
@@ -23,36 +23,57 @@
     [Parameter]
     public string RoomId { get; set; } = "invalid!!!!!!";
 
-    private Room? Room { get; set; }
+    private GenericRoom? Room { get; set; }
 
-    private StateEventResponse<object>[] States { get; set; } = Array.Empty<StateEventResponse<object>>();
-    private List<Room> Rooms { get; } = new();
+    private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>();
+    private List<GenericRoom> Rooms { get; } = new();
     private List<string> ServersInSpace { get; } = new();
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-        Room = await RuntimeCache.CurrentHomeServer.GetRoom(RoomId.Replace('~', '.'));
-        var state = await Room.GetStateAsync("");
-        if (state != null) {
-    // Console.WriteLine(state.Value.ToJson());
-            States = state.Value.Deserialize<StateEventResponse<object>[]>()!;
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
 
-            foreach (var stateEvent in States) {
-                if (stateEvent.Type == "m.space.child") {
-    // if (stateEvent.Content.ToJson().Length < 5) return;
-                    var roomId = stateEvent.StateKey;
-                    var room = await RuntimeCache.CurrentHomeServer.GetRoom(roomId);
-                    if (room != null) {
-                        Rooms.Add(room);
-                    }
+        Room = await hs.GetRoom(RoomId.Replace('~', '.'));
+
+        var state = Room.GetFullStateAsync();
+        await foreach (var stateEvent in state) {
+            if (stateEvent.Type == "m.space.child") {
+                var roomId = stateEvent.StateKey;
+                var room = await hs.GetRoom(roomId);
+                if (room is not null) {
+                    Rooms.Add(room);
                 }
-                else if (stateEvent.Type == "m.room.member") {
-                    var serverName = stateEvent.StateKey.Split(':').Last();
-                    if (!ServersInSpace.Contains(serverName)) {
-                        ServersInSpace.Add(serverName);
-                    }
+            }
+            else if (stateEvent.Type == "m.room.member") {
+                var serverName = stateEvent.StateKey.Split(':').Last();
+                if (!ServersInSpace.Contains(serverName)) {
+                    ServersInSpace.Add(serverName);
                 }
             }
+        }
+        await base.OnInitializedAsync();
+
+    //     var state = await Room.GetStateAsync("");
+    //     if (state is not null) {
+    // // Console.WriteLine(state.Value.ToJson());
+    //         States = state.Value.Deserialize<StateEventResponse[]>()!;
+    //
+    //         foreach (var stateEvent in States) {
+    //             if (stateEvent.Type == "m.space.child") {
+    // // if (stateEvent.Content.ToJson().Length < 5) return;
+    //                 var roomId = stateEvent.StateKey;
+    //                 var room = await hs.GetRoom(roomId);
+    //                 if (room is not null) {
+    //                     Rooms.Add(room);
+    //                 }
+    //             }
+    //             else if (stateEvent.Type == "m.room.member") {
+    //                 var serverName = stateEvent.StateKey.Split(':').Last();
+    //                 if (!ServersInSpace.Contains(serverName)) {
+    //                     ServersInSpace.Add(serverName);
+    //                 }
+    //             }
+    //         }
 
     // if(state.Value.TryGetProperty("Type", out var Type))
     // {
@@ -62,8 +83,8 @@
     //     //this is fine, apprently...
     //     //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!");
     // }
-        }
-        await base.OnInitializedAsync();
+
+    // await base.OnInitializedAsync();
     }
 
     private async Task JoinAllRooms() {
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerTimeline.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerTimeline.razor
index 9513a8a..e32b5cb 100644
--- a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerTimeline.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerTimeline.razor
@@ -20,10 +20,11 @@
     private List<StateEventResponse> Events { get; } = new();
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         RoomId = RoomId.Replace('~', '.');
         Console.WriteLine("RoomId: " + RoomId);
-        var room = await RuntimeCache.CurrentHomeServer.GetRoom(RoomId);
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+        var room = await hs.GetRoom(RoomId);
         MessagesResponse? msgs = null;
         do {
             msgs = await room.GetMessagesAsync(limit: 250, from: msgs?.End, dir: "b");
@@ -32,7 +33,7 @@
             msgs.Chunk.Reverse();
             Events.InsertRange(0, msgs.Chunk);
             StateHasChanged();
-        } while (msgs.End != null);
+        } while (msgs.End is not null);
 
         await base.OnInitializedAsync();
     }
diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor
index 296514c..b2d28f6 100644
--- a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor
@@ -45,12 +45,9 @@
     public string status = "";
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         await base.OnInitializedAsync();
-        if (RuntimeCache.CurrentHomeServer != null) {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
         RoomId = RoomId.Replace('~', '.');
         await LoadStatesAsync();
         Console.WriteLine("Policy list editor initialized!");
@@ -59,25 +56,20 @@
     private DateTime _lastUpdate = DateTime.Now;
 
     private async Task LoadStatesAsync() {
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+
         var StateLoaded = 0;
-        using var client = new HttpClient();
-    //TODO: can this be improved?
-        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", RuntimeCache.CurrentHomeServer.AccessToken);
-        var response = await client.GetAsync($"{RuntimeCache.CurrentHomeServer.FullHomeServerDomain}/_matrix/client/v3/rooms/{RoomId}/state");
-    // var response = await client.GetAsync($"http://localhost:5117/matrix-hq-state.json");
-    //var _events = await response.Content.ReadFromJsonAsync<Queue<StateEventStruct>>();
-        var _data = await response.Content.ReadAsStreamAsync();
-        var __events = JsonSerializer.DeserializeAsyncEnumerable<StateEventResponse>(_data);
-        await foreach (var _ev in __events) {
-            var e = new StateEventResponse {
-                Type = _ev.Type,
-                StateKey = _ev.StateKey,
-                OriginServerTs = _ev.OriginServerTs,
-                Content = _ev.Content
-            };
-            Events.Add(e);
-            if (string.IsNullOrEmpty(e.StateKey)) {
-                FilteredEvents.Add(e);
+        var response = (await hs.GetRoom(RoomId)).GetFullStateAsync();
+        await foreach (var _ev in response) {
+            // var e = new StateEventResponse {
+            //     Type = _ev.Type,
+            //     StateKey = _ev.StateKey,
+            //     OriginServerTs = _ev.OriginServerTs,
+            //     Content = _ev.Content
+            // };
+            Events.Add(_ev);
+            if (string.IsNullOrEmpty(_ev.StateKey)) {
+                FilteredEvents.Add(_ev);
             }
             StateLoaded++;
             if ((DateTime.Now - _lastUpdate).TotalMilliseconds > 100) {
@@ -104,8 +96,8 @@
         await Task.Delay(1);
         FilteredEvents = _FilteredEvents;
 
-        if (_shownType != null)
-            shownEventJson = _FilteredEvents.Where(x => x.Type == _shownType).First().Content.ToJson(indent: true, ignoreNull: true);
+        if (_shownType is not null)
+            shownEventJson = _FilteredEvents.Where(x => x.Type == _shownType).First().RawContent.ToJson(indent: true, ignoreNull: true);
 
         StateHasChanged();
     }
diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor
index 82b5d75..55c44d9 100644
--- a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateRoomList.razor
@@ -22,13 +22,10 @@ else {
     public List<string> Rooms { get; set; } = new();
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         await base.OnInitializedAsync();
-        if (RuntimeCache.CurrentHomeServer == null) {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
-        Rooms = (await RuntimeCache.CurrentHomeServer.GetJoinedRooms()).Select(x => x.RoomId).ToList();
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+        Rooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList();
         Console.WriteLine("Fetched joined rooms!");
     }
 
diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor
index ff1d9ac..a0072ab 100644
--- a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor
@@ -1,6 +1,7 @@
 @page "/RoomStateViewer/{RoomId}"
 @using System.Net.Http.Headers
 @using System.Text.Json
+@using MatrixRoomUtils.Core.Responses
 @inject ILocalStorageService LocalStorage
 @inject NavigationManager NavigationManager
 <h3>Room state viewer - Viewing @RoomId</h3>
@@ -18,18 +19,18 @@
     </tr>
     </thead>
     <tbody>
-    @foreach (var stateEvent in FilteredEvents.Where(x => x.state_key == "").OrderBy(x => x.origin_server_ts)) {
+    @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) {
         <tr>
-            <td>@stateEvent.type</td>
+            <td>@stateEvent.Type</td>
             <td style="max-width: fit-Content;">
-                <pre>@stateEvent.content</pre>
+                <pre>@stateEvent.RawContent.ToJson()</pre>
             </td>
         </tr>
     }
     </tbody>
 </table>
 
-@foreach (var group in FilteredEvents.GroupBy(x => x.state_key).OrderBy(x => x.Key).Where(x => x.Key != "")) {
+@foreach (var group in FilteredEvents.GroupBy(x => x.StateKey).OrderBy(x => x.Key).Where(x => x.Key != "")) {
     <details>
         <summary>@group.Key</summary>
         <table class="table table-striped table-hover" style="width: fit-Content;">
@@ -40,11 +41,11 @@
             </tr>
             </thead>
             <tbody>
-            @foreach (var stateEvent in group.OrderBy(x => x.origin_server_ts)) {
+            @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) {
                 <tr>
-                    <td>@stateEvent.type</td>
+                    <td>@stateEvent.Type</td>
                     <td style="max-width: fit-Content;">
-                        <pre>@stateEvent.content</pre>
+                        <pre>@stateEvent.RawContent.ToJson()</pre>
                     </td>
                 </tr>
             }
@@ -64,17 +65,14 @@
     [Parameter]
     public string? RoomId { get; set; }
 
-    public List<PreRenderedStateEvent> FilteredEvents { get; set; } = new();
-    public List<PreRenderedStateEvent> Events { get; set; } = new();
+    public List<StateEventResponse> FilteredEvents { get; set; } = new();
+    public List<StateEventResponse> Events { get; set; } = new();
     public string status = "";
 
     protected override async Task OnInitializedAsync() {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
         await base.OnInitializedAsync();
-        if (RuntimeCache.CurrentHomeServer == null) {
-            NavigationManager.NavigateTo("/Login");
-            return;
-        }
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
         RoomId = RoomId.Replace('~', '.');
         await LoadStatesAsync();
         Console.WriteLine("Policy list editor initialized!");
@@ -84,24 +82,13 @@
 
     private async Task LoadStatesAsync() {
         var StateLoaded = 0;
-    //TODO: can we improve this?
-        using var client = new HttpClient();
-        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", RuntimeCache.CurrentHomeServer.AccessToken);
-        var response = await client.GetAsync($"{RuntimeCache.CurrentHomeServer.FullHomeServerDomain}/_matrix/client/v3/rooms/{RoomId}/state");
-    // var response = await client.GetAsync($"http://localhost:5117/matrix-hq-state.json");
-    //var _events = await response.Content.ReadFromJsonAsync<Queue<StateEventStruct>>();
-        var _data = await response.Content.ReadAsStreamAsync();
-        var __events = JsonSerializer.DeserializeAsyncEnumerable<StateEventStruct>(_data);
-        await foreach (var _ev in __events) {
-            var e = new PreRenderedStateEvent {
-                type = _ev.type,
-                state_key = _ev.state_key,
-                origin_server_ts = _ev.origin_server_ts,
-                content = _ev.content.ToJson(true, true)
-            };
-            Events.Add(e);
-            if (string.IsNullOrEmpty(e.state_key)) {
-                FilteredEvents.Add(e);
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+        var response = (await hs.GetRoom(RoomId)).GetFullStateAsync();
+        await foreach (var _ev in response) {
+            Events.Add(_ev);
+            if (string.IsNullOrEmpty(_ev.StateKey)) {
+                FilteredEvents.Add(_ev);
             }
             StateLoaded++;
             if ((DateTime.Now - _lastUpdate).TotalMilliseconds > 100) {
@@ -121,7 +108,7 @@
         await Task.Delay(1);
         var _FilteredEvents = Events;
         if (!ShowMembershipEvents)
-            _FilteredEvents = _FilteredEvents.Where(x => x.type != "m.room.member").ToList();
+            _FilteredEvents = _FilteredEvents.Where(x => x.Type != "m.room.member").ToList();
 
         status = "Done, rerendering!";
         StateHasChanged();
@@ -130,17 +117,6 @@
         StateHasChanged();
     }
 
-    public struct PreRenderedStateEvent {
-        public string content { get; set; }
-        public long origin_server_ts { get; set; }
-        public string state_key { get; set; }
-        public string type { get; set; }
-    // public string Sender { get; set; }
-    // public string EventId { get; set; }
-    // public string UserId { get; set; }
-    // public string ReplacesState { get; set; }
-    }
-
     public bool ShowMembershipEvents {
         get => _showMembershipEvents;
         set {
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
index 17551c9..20ddd0d 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
@@ -1,20 +1,19 @@
 @page "/Rooms"
 <h3>Room list</h3>
 
-@if (Rooms != null) {
+@if (Rooms is not null) {
     <RoomList Rooms="Rooms"></RoomList>
 }
 
 
 @code {
 
-    private List<Room> Rooms { get; set; }
+    private List<GenericRoom> Rooms { get; set; }
     
-    protected override async Task OnInitializedAsync()
-    {
-        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
-
-        Rooms = await RuntimeCache.CurrentHomeServer.GetJoinedRooms();
+    protected override async Task OnInitializedAsync() {
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+        Rooms = await hs.GetJoinedRooms();
 
         await base.OnInitializedAsync();
     }