about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
m---------LibMatrix0
-rw-r--r--MatrixUtils.Abstractions/RoomInfo.cs1
-rw-r--r--MatrixUtils.Web/MatrixUtils.Web.csproj4
-rw-r--r--MatrixUtils.Web/Pages/Client/ClientComponents/ClientRoomList.razor15
-rw-r--r--MatrixUtils.Web/Pages/Client/ClientComponents/ClientStatusList.razor35
-rw-r--r--MatrixUtils.Web/Pages/Client/ClientComponents/ClientSyncWrapper.cs41
-rw-r--r--MatrixUtils.Web/Pages/Client/ClientComponents/MatrixClient.razor31
-rw-r--r--MatrixUtils.Web/Pages/Client/Index.razor72
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor10
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor26
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css12
-rw-r--r--MatrixUtils.Web/Pages/Tools/UserTrace.razor143
-rw-r--r--MatrixUtils.Web/Shared/UpdateAvailableDetector.razor38
-rw-r--r--MatrixUtils.Web/Shared/UpdateAvailableDetector.razor.css15
15 files changed, 323 insertions, 121 deletions
diff --git a/.gitignore b/.gitignore
index 1821c7e..984ec54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ MatrixRoomUtils.Bot/bot_data/
 appsettings.Local*.json
 nixpkgs/
 *.DotSettings.user
+*.patch
 
 test.tsv
 test-proxy.tsv
diff --git a/LibMatrix b/LibMatrix
-Subproject 37b97d65c0a5262539a5de560e911048166b8bb
+Subproject 896ee7f099f817e8cc9aba96a9db00fcce67163
diff --git a/MatrixUtils.Abstractions/RoomInfo.cs b/MatrixUtils.Abstractions/RoomInfo.cs
index 5c258a4..aff0e25 100644
--- a/MatrixUtils.Abstractions/RoomInfo.cs
+++ b/MatrixUtils.Abstractions/RoomInfo.cs
@@ -27,6 +27,7 @@ public class RoomInfo : NotifyPropertyChanged {
     
     public readonly GenericRoom Room;
     public ObservableCollection<StateEventResponse?> StateEvents { get; private set; } = new();
+    public ObservableCollection<StateEventResponse?> Timeline { get; private set; } = new();
 
     private static ConcurrentBag<AuthenticatedHomeserverGeneric> homeserversWithoutEventFormatSupport = new();
     private static SvgIdenticonGenerator identiconGenerator = new();
diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj
index 53c056a..c2732de 100644
--- a/MatrixUtils.Web/MatrixUtils.Web.csproj
+++ b/MatrixUtils.Web/MatrixUtils.Web.csproj
@@ -46,5 +46,9 @@
     <ItemGroup>
         <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
     </ItemGroup>
+
+    <ItemGroup>
+      <Folder Include="Pages\Tools\Moderation\Draupnir\" />
+    </ItemGroup>
     
 </Project>
diff --git a/MatrixUtils.Web/Pages/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Client/ClientComponents/ClientRoomList.razor
new file mode 100644
index 0000000..845f30d
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Client/ClientComponents/ClientRoomList.razor
@@ -0,0 +1,15 @@
+@using ClientContext = MatrixUtils.Web.Pages.Client.Index.ClientContext
+@* user header and room list *@
+@foreach (var room in Data.SyncWrapper.Rooms) {
+    <LinkButton OnClick="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")">
+        @room.RoomName
+    </LinkButton>
+    <br/>
+}
+
+@code {
+
+    [Parameter]
+    public ClientContext Data { get; set; } = null!;
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Client/ClientComponents/ClientStatusList.razor b/MatrixUtils.Web/Pages/Client/ClientComponents/ClientStatusList.razor
new file mode 100644
index 0000000..1100c98
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Client/ClientComponents/ClientStatusList.razor
@@ -0,0 +1,35 @@
+@using ClientContext = MatrixUtils.Web.Pages.Client.Index.ClientContext;
+@using System.Collections.ObjectModel
+
+@foreach (var ctx in Data) {
+    <pre>
+        @ctx.Homeserver.UserId - @ctx.SyncWrapper.Status
+    </pre>
+}
+
+@code {
+
+    [Parameter]
+    public ObservableCollection<ClientContext> Data { get; set; } = null!;
+
+    protected override void OnInitialized() {
+        Data.CollectionChanged += (_, e) => {
+            foreach (var item in e.NewItems?.Cast<ClientContext>() ?? []) {
+                item.SyncWrapper.PropertyChanged += (_, pe) => {
+                    if (pe.PropertyName == nameof(item.SyncWrapper.Status))
+                        StateHasChanged();
+                };
+            }
+
+            StateHasChanged();
+        };
+
+        Data.ToList().ForEach(ctx => {
+            ctx.SyncWrapper.PropertyChanged += (_, pe) => {
+                if (pe.PropertyName == nameof(ctx.SyncWrapper.Status))
+                    StateHasChanged();
+            };
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Client/ClientComponents/ClientSyncWrapper.cs b/MatrixUtils.Web/Pages/Client/ClientComponents/ClientSyncWrapper.cs
new file mode 100644
index 0000000..16051b8
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Client/ClientComponents/ClientSyncWrapper.cs
@@ -0,0 +1,41 @@
+using System.Collections.ObjectModel;
+using ArcaneLibs;
+using LibMatrix;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using MatrixUtils.Abstractions;
+
+namespace MatrixUtils.Web.Pages.Client.ClientComponents;
+
+public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : NotifyPropertyChanged {
+    private SyncHelper _syncHelper = new SyncHelper(homeserver) {
+        MinimumDelay = TimeSpan.FromMilliseconds(2000),
+        IsInitialSync = false
+    };
+    private string _status = "Loading...";
+
+    public ObservableCollection<StateEvent> AccountData { get; set; } = new();
+    public ObservableCollection<RoomInfo> Rooms { get; set; } = new();
+
+    public string Status {
+        get => _status;
+        set => SetField(ref _status, value);
+    }
+
+    public async Task Start() {
+        Task.Yield();
+        var resp = _syncHelper.EnumerateSyncAsync();
+        Status = $"[{DateTime.Now:s}] Syncing...";
+        await foreach (var response in resp) {
+            Task.Yield();
+            Status = $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates";
+            await HandleSyncResponse(response);
+            await Task.Yield();
+        }
+    }
+
+    private async Task HandleSyncResponse(SyncResponse resp) {
+        
+    }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Client/ClientComponents/MatrixClient.razor b/MatrixUtils.Web/Pages/Client/ClientComponents/MatrixClient.razor
new file mode 100644
index 0000000..b4a81f7
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Client/ClientComponents/MatrixClient.razor
@@ -0,0 +1,31 @@
+@using Index = MatrixUtils.Web.Pages.Client.Index
+@using MatrixUtils.Web.Pages.Client.ClientComponents
+
+<div class="container-fluid">
+    <div class="row">
+        <div class="col-3">
+            <ClientRoomList Data="@Data"/>
+        </div>
+        <div class="col-6">
+            @if (Data.SelectedRoom != null) {
+                <Index.RoomHeader Data="@Data"/>
+                <Index.RoomTimeline Data="@Data"/>
+            }
+            else {
+                <p>No room selected</p>
+            }
+        </div>
+        @if (Data.SelectedRoom != null) {
+            <div class="col-3">
+                <Index.UserList Data="@Data"/>
+            </div>
+        }
+    </div>
+</div>
+
+@code {
+
+    [Parameter]
+    public Index.ClientContext Data { get; set; } = null!;
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Client/Index.razor b/MatrixUtils.Web/Pages/Client/Index.razor
new file mode 100644
index 0000000..2a9a327
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Client/Index.razor
@@ -0,0 +1,72 @@
+@page "/Client"
+@using LibMatrix
+@using MatrixUtils.Abstractions
+@using MatrixUtils.Web.Pages.Client.ClientComponents
+@using System.Collections.ObjectModel
+
+<h3>Client</h3>
+
+
+@foreach (var client in Clients) {
+    <LinkButton Color="@(SelectedClient == client ? "#ff00ff" : "")" OnClick="@(async () => SelectedClient = client)">
+        @client.Homeserver.WhoAmI.UserId
+    </LinkButton>
+}
+<ClientStatusList Data="@Clients"></ClientStatusList>
+
+
+@* @foreach (var client in Clients) { *@
+@*     <div class="card"> *@
+@*         <span>@client.Homeserver.UserId - @client.SyncWrapper.Status</span> *@
+@*     </div> *@
+@* } *@
+
+@if (SelectedClient != null) {
+    <div class="card">
+        <MatrixClient Data="@SelectedClient"/>
+    </div>
+}
+
+@code {
+
+    private static readonly ObservableCollection<ClientContext> Clients = [];
+    private static ClientContext _selectedClient;
+
+    private ClientContext SelectedClient {
+        get => _selectedClient;
+        set {
+            _selectedClient = value;
+            StateHasChanged();
+        }
+    }
+
+    protected override async Task OnInitializedAsync() {
+        var tokens = await RMUStorage.GetAllTokens();
+        var tasks = tokens.Select(async token => {
+            try {
+                var cc = new ClientContext() {
+                    Homeserver = await RMUStorage.GetSession(token)
+                };
+                cc.SyncWrapper = new ClientSyncWrapper(cc.Homeserver);
+
+#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+                cc.SyncWrapper.Start();
+#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+
+                Clients.Add(cc);
+                StateHasChanged();
+            }
+            catch { }
+        }).ToList();
+        await Task.WhenAll(tasks);
+    }
+
+    public class ClientContext {
+        public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+        public ClientSyncWrapper SyncWrapper { get; set; }
+
+        public RoomInfo? SelectedRoom { get; set; }
+    }
+
+}
+
diff --git a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor
index afd58af..11df261 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor
@@ -82,18 +82,19 @@
 
 @foreach (var res in Results) {
     <div style="background-color: #ffffff11; border-radius: 0.5em; display: block; margin-top: 4px; padding: 4px;">
-        <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem>
+        @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@
         <p>
             @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) {
-                <span>@res.CanonicalAlias (@res.RoomId)</span>
+                <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span>
                 <br/>
             }
             else {
-                <span>@res.RoomId</span>
+                <span>@res.RoomId (@res.Name)</span>
                 <br/>
             }
             @if (!string.IsNullOrWhiteSpace(res.Creator)) {
-                <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span>
+                @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@
+                <span>Created by @res.Creator</span>
                 <br/>
             }
         </p>
@@ -178,6 +179,7 @@
             }
         }
 
+        StateHasChanged();
     }
 
     private readonly Dictionary<string, string> validOrderBy = new() {
diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor
index 7a3b27b..6483f01 100644
--- a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor
+++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor
@@ -1,15 +1,16 @@
 @using MatrixUtils.Abstractions
 <div class="spaceListItem" style="@(SelectedSpace == Space ? "background-color: #FFFFFF33;" : "")" onclick="@SelectSpace">
-    @if (IsSpaceOpened()) {
-        <span onclick="@ToggleSpace">▼ </span>
-    }
-    else {
-        <span onclick="@ToggleSpace">▶ </span>
-    }
-
-    <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage>
-    <span class="spaceNameEllipsis">@Space.RoomName</span>
+    <div class="spaceListItemContainer">
+        @if (IsSpaceOpened()) {
+            <span onclick="@ToggleSpace">▼ </span>
+        }
+        else {
+            <span onclick="@ToggleSpace">▶ </span>
+        }
 
+        <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage>
+        <span class="spaceNameEllipsis">@Space.RoomName</span>
+    </div>
     @if (IsSpaceOpened()) {
         <span>meow</span>
     }
@@ -19,10 +20,10 @@
 
     [Parameter]
     public RoomInfo Space { get; set; }
-    
+
     [Parameter]
     public RoomInfo SelectedSpace { get; set; }
-    
+
     [Parameter]
     public EventCallback<RoomInfo> SelectedSpaceChanged { get; set; }
 
@@ -52,5 +53,4 @@
         return OpenedSpaces.Contains(Space);
     }
 
-}
-
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css
index a88975b..d6e413f 100644
--- a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css
+++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css
@@ -11,7 +11,17 @@
 .spaceListItem {
     display: block;
     width: 100%;
-    height: 50px;
+    height: 3em;
+}
+
+.spaceListItemContainer {
+    display: flex;
+    align-items: center;
+    vertical-align: center;
+    justify-content: space-between;
+    padding: 0 16px;
+    width: 100%;
+    height: 100%;
 }
 
 .spaceListItem > img {
diff --git a/MatrixUtils.Web/Pages/Tools/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/UserTrace.razor
index 4ad9874..95fe02b 100644
--- a/MatrixUtils.Web/Pages/Tools/UserTrace.razor
+++ b/MatrixUtils.Web/Pages/Tools/UserTrace.razor
@@ -5,6 +5,7 @@
 @using LibMatrix
 @using System.Collections.Frozen
 @using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Filters
 @using MatrixUtils.Abstractions
 <h3>User Trace</h3>
 <hr/>
@@ -17,7 +18,7 @@
 <details>
     <summary>Rooms to be searched (@rooms.Count)</summary>
     @foreach (var room in rooms) {
-        <span>@room.Room.RoomId</span>
+        <span>@room.RoomId</span>
         <br/>
     }
 </details>
@@ -48,8 +49,11 @@
 }
 
 @code {
+
     private ObservableCollection<string> log { get; set; } = new();
-    List<RoomInfo> rooms { get; set; } = new();
+
+    // List<RoomInfo> rooms { get; set; } = new();
+    List<GenericRoom> rooms { get; set; } = [];
     Dictionary<string, List<Match>> matches = new();
 
     private string UserIdString {
@@ -63,46 +67,58 @@
         log.CollectionChanged += (sender, args) => StateHasChanged();
         var hs = await RMUStorage.GetCurrentSessionOrNavigate();
         if (hs is null) return;
-        var sessions = await RMUStorage.GetAllTokens();
-        var baseRooms = new List<GenericRoom>();
-        foreach (var userAuth in sessions) {
-            var session = await RMUStorage.GetSession(userAuth);
-            if (session is not null) {
-                baseRooms.AddRange(await session.GetJoinedRooms());
-                var sessionRooms = (await session.GetJoinedRooms()).Where(x => !rooms.Any(y => y.Room.RoomId == x.RoomId)).ToList();
-                StateHasChanged();
-                log.Add($"Got {sessionRooms.Count} rooms for {userAuth.UserId}");
+        // var sessions = await RMUStorage.GetAllTokens();
+        // var baseRooms = new List<GenericRoom>();
+        // foreach (var userAuth in sessions) {
+        //     var session = await RMUStorage.GetSession(userAuth);
+        //     if (session is not null) {
+        //         baseRooms.AddRange(await session.GetJoinedRooms());
+        //         var sessionRooms = (await session.GetJoinedRooms()).Where(x => !rooms.Any(y => y.Room.RoomId == x.RoomId)).ToList();
+        //         StateHasChanged();
+        //         log.Add($"Got {sessionRooms.Count} rooms for {userAuth.UserId}");
+        //     }
+        // }
+        //
+        // log.Add("Done fetching rooms!");
+        //
+        // baseRooms = baseRooms.DistinctBy(x => x.RoomId).ToList();
+        //
+        // // rooms.CollectionChanged += (sender, args) => StateHasChanged();
+        // var tasks = baseRooms.Select(async newRoom => {
+        //     bool success = false;
+        //     while (!success)
+        //         try {
+        //             var state = await newRoom.GetFullStateAsListAsync();
+        //             var newRoomInfo = new RoomInfo(newRoom, state);
+        //             rooms.Add(newRoomInfo);
+        //             log.Add($"Got {newRoomInfo.StateEvents.Count} events for {newRoomInfo.RoomName}");
+        //             success = true;
+        //         }
+        //         catch (MatrixException e) {
+        //             log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
+        //             throw;
+        //         }
+        //         catch (HttpRequestException e) {
+        //             log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
+        //         }
+        // });
+        // await Task.WhenAll(tasks);
+        //
+        // log.Add($"Done fetching members!");
+        //
+        // UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x));
+
+        foreach (var session in await RMUStorage.GetAllTokens()) {
+            var _hs = await RMUStorage.GetSession(session);
+            if (_hs is not null) {
+                rooms.AddRange(await _hs.GetJoinedRooms());
+                log.Add($"Got {rooms.Count} rooms after adding {_hs.UserId}");
             }
         }
 
-        log.Add("Done fetching rooms!");
-
-        baseRooms = baseRooms.DistinctBy(x => x.RoomId).ToList();
-
-        // rooms.CollectionChanged += (sender, args) => StateHasChanged();
-        var tasks = baseRooms.Select(async newRoom => {
-            bool success = false;
-            while (!success)
-                try {
-                    var state = await newRoom.GetFullStateAsListAsync();
-                    var newRoomInfo = new RoomInfo(newRoom, state);
-                    rooms.Add(newRoomInfo);
-                    log.Add($"Got {newRoomInfo.StateEvents.Count} events for {newRoomInfo.RoomName}");
-                    success = true;
-                }
-                catch (MatrixException e) {
-                    log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
-                    throw;
-                }
-                catch (HttpRequestException e) {
-                    log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
-                }
-        });
-        await Task.WhenAll(tasks);
-
-        log.Add($"Done fetching members!");
-
-        UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x));
+        //get distinct rooms evenly distributed per session, accounting for count per session
+        rooms = rooms.OrderBy(x => rooms.Count(y => y.Homeserver == x.Homeserver)).DistinctBy(x => x.RoomId).ToList();
+        log.Add($"Got {rooms.Count} rooms");
 
         StateHasChanged();
         Console.WriteLine("Rerendered!");
@@ -113,18 +129,25 @@
         foreach (var userId in UserIDs) {
             matches.Add(userId, new List<Match>());
 
-            foreach (var room in rooms) {
-                var state = room.StateEvents.Where(x => x!.Type == RoomMemberEventContent.EventId).ToList();
-                if (state!.Any(x => x.StateKey == userId)) {
-                    matches[userId].Add(new() {
-                        Event = state.First(x => x.StateKey == userId),
-                        Room = room.Room,
-                        RoomName = room.RoomName ?? "No name"
-                    });
-                }
+            // foreach (var room in rooms) {
+            //     var state = room.StateEvents.Where(x => x!.Type == RoomMemberEventContent.EventId).ToList();
+            //     if (state!.Any(x => x.StateKey == userId)) {
+            //         matches[userId].Add(new() {
+            //             Event = state.First(x => x.StateKey == userId),
+            //             Room = room.Room,
+            //             RoomName = room.RoomName ?? "No name"
+            //         });
+            //     }
+            // }
+
+            log.Add($"Searching for {userId}...");
+            await foreach (var match in GetMatches(userId)) {
+                matches[userId].Add(match);
             }
         }
 
+        log.Add("Done!");
+
         StateHasChanged();
 
         return "";
@@ -135,8 +158,8 @@
     private async Task DoImportFromRoomId() {
         try {
             if (ImportFromRoomId is null) return;
-            var room = rooms.FirstOrDefault(x => x.Room.RoomId == ImportFromRoomId);
-            UserIdString = string.Join("\n", (await room.Room.GetMembersListAsync()).Select(x => x.StateKey));
+            var room = rooms.FirstOrDefault(x => x.RoomId == ImportFromRoomId);
+            UserIdString = string.Join("\n", (await room.GetMembersListAsync()).Select(x => x.StateKey));
         }
         catch (Exception e) {
             Console.WriteLine(e);
@@ -152,4 +175,24 @@
         public string RoomName { get; set; }
     }
 
+    private async IAsyncEnumerable<Match> GetMatches(string userId) {
+        var results = rooms.Select(async room => {
+            var state = await room.GetStateEventOrNullAsync(room.RoomId, userId);
+            if (state is not null) {
+                return new Match {
+                    Room = room,
+                    Event = state,
+                    RoomName = await room.GetNameOrFallbackAsync()
+                };
+            }
+
+            return null;
+        }).ToAsyncEnumerable();
+        await foreach (var result in results) {
+            if (result is not null) {
+                yield return result;
+            }
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor b/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor
deleted file mode 100644
index 5197a6f..0000000
--- a/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor
+++ /dev/null
@@ -1,38 +0,0 @@
-@* Source: https://whuysentruit.medium.com/blazor-wasm-pwa-adding-a-new-update-available-notification-d9f65c4ad13 *@
-@inject IJSRuntime _jsRuntime
-
-@if (_newVersionAvailable)
-{
-    <button type="button" class="btn btn-warning shadow floating-update-button" onclick="window.location.reload()">
-        A new version of the application is available. Click here to reload.
-    </button>
-}
-
-@code {
-
-    private bool _newVersionAvailable = false;
-
-    protected override async Task OnInitializedAsync()
-    {
-        await RegisterForUpdateAvailableNotification();
-    }
-
-    private async Task RegisterForUpdateAvailableNotification()
-    {
-        await _jsRuntime.InvokeAsync<object>(
-            identifier: "registerForUpdateAvailableNotification",
-            DotNetObjectReference.Create(this),
-            nameof(OnUpdateAvailable));
-    }
-
-    [JSInvokable(nameof(OnUpdateAvailable))]
-    public Task OnUpdateAvailable()
-    {
-        _newVersionAvailable = true;
-
-        StateHasChanged();
-
-        return Task.CompletedTask;
-    }
-
-}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor.css b/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor.css
deleted file mode 100644
index 32bff09..0000000
--- a/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.floating-update-button {
-    position: fixed;
-
-    right: 2rem;
-    bottom: 2rem;
-
-    padding: 1rem 1.5rem;
-
-    animation: fadein 2s ease-out;
-}
-
-@keyframes fadein {
-    from { right: -100%; }
-    to { right: 2rem; }
-}
\ No newline at end of file