about summary refs log tree commit diff
path: root/MatrixRoomUtils.Web/Pages
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2023-11-09 07:38:33 +0100
committerTheArcaneBrony <myrainbowdash949@gmail.com>2023-11-09 07:38:33 +0100
commitc37bcb0e4a878d4f4c0e47988adb8624131c82cd (patch)
tree706f5300ce83b511d807decddd5b3521168b5fc7 /MatrixRoomUtils.Web/Pages
parentFix updates (diff)
downloadMatrixUtils-c37bcb0e4a878d4f4c0e47988adb8624131c82cd.tar.xz
event types
Diffstat (limited to 'MatrixRoomUtils.Web/Pages')
-rw-r--r--MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor7
-rw-r--r--MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor22
-rw-r--r--MatrixRoomUtils.Web/Pages/Index.razor22
-rw-r--r--MatrixRoomUtils.Web/Pages/LoginPage.razor46
-rw-r--r--MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor113
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Create.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Index.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor8
-rw-r--r--MatrixRoomUtils.Web/Pages/ServerInfo.razor235
-rw-r--r--MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor124
-rw-r--r--MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/User/DMManager.razor11
12 files changed, 459 insertions, 134 deletions
diff --git a/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
index 4b2dc4f..94c51b2 100644
--- a/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
+++ b/MatrixRoomUtils.Web/Pages/Dev/DevUtilities.razor
@@ -16,7 +16,7 @@ else {
         <summary>Room List</summary>
         @foreach (var room in Rooms) {
             <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@room.Replace('.', '~')">
-                <RoomListItem RoomId="@room"></RoomListItem>
+                <RoomListItem RoomInfo="@(new RoomInfo() { Room = hs.GetRoom(room) })" LoadData="true"></RoomListItem>
             </a>
         }
     </details>
@@ -37,10 +37,11 @@ else {
 
 @code {
     public List<string> Rooms { get; set; } = new();
+    public AuthenticatedHomeserverGeneric? hs { get; set; }
 
     protected override async Task OnInitializedAsync() {
         await base.OnInitializedAsync();
-        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        hs = await MRUStorage.GetCurrentSessionOrNavigate();
         if (hs == null) return;
         Rooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList();
         Console.WriteLine("Fetched joined rooms!");
@@ -76,4 +77,4 @@ else {
         StateHasChanged();
     }
 
-}
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor
index 59ce70f..c605e7a 100644
--- a/MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor
+++ b/MatrixRoomUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -1,20 +1,34 @@
 @page "/HSAdmin"
 @using LibMatrix.Homeservers
+@using ArcaneLibs.Extensions
 <h3>Homeserver Admininistration</h3>
 <hr/>
 
-<h4>Synapse tools</h4>
-<hr/>
-<a href="/HSAdmin/RoomQuery">Query rooms</a>
+@if (Homeserver is null) {
+    <p>Homeserver is null...</p>
+}
+else {
+    @if (Homeserver is AuthenticatedHomeserverSynapse) {
+        <h4>Synapse tools</h4>
+        <hr/>
+        <a href="/HSAdmin/RoomQuery">Query rooms</a>
+    }
+    else {
+        <p>Homeserver type @Homeserver.GetType().Name does not have any administration tools in MRU.</p>
+        <p>Server info:</p>
+        <pre>@ServerVersionResponse?.ToJson(ignoreNull: true)</pre>
+    }
+}
 
 @code {
     public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+    public ServerVersionResponse? ServerVersionResponse { get; set; }
 
     protected override async Task OnInitializedAsync() {
         Homeserver = await MRUStorage.GetCurrentSessionOrNavigate();
         if (Homeserver is null) return;
+        ServerVersionResponse = await Homeserver.GetServerVersionAsync();
         await base.OnInitializedAsync();
     }
 
-
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Index.razor b/MatrixRoomUtils.Web/Pages/Index.razor
index 74dd651..804fde3 100644
--- a/MatrixRoomUtils.Web/Pages/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Index.razor
@@ -3,6 +3,7 @@
 @using LibMatrix
 @using LibMatrix.Homeservers
 @using ArcaneLibs.Extensions
+@using MatrixRoomUtils.Web.Pages.Dev
 
 <PageTitle>Index</PageTitle>
 
@@ -28,13 +29,18 @@ Small collection of tools to do not-so-everyday things.
 
                     </p>
                     <span style="display: inline-block; width: 128px;">@__auth.UserInfo.RoomCount rooms</span>
-                    <span style="color: #888888">@__auth.ServerVersion.Server.Name @__auth.ServerVersion.Server.Version</span>
+                    <a style="color: #888888" href="@("/ServerInfo/"+__auth.Homeserver.ServerName+"/")">@__auth.ServerVersion.Server.Name @__auth.ServerVersion.Server.Version</a>
                     @if (_auth.Proxy != null) {
                         <span class="badge badge-info"> (proxied via @_auth.Proxy)</span>
                     }
                     else {
                         <p>Not proxied</p>
                     }
+                    @if (DEBUG) {
+                        <p>T=@__auth.Homeserver.GetType().FullName</p>
+                        <p>D=@__auth.Homeserver.WhoAmI.DeviceId</p>
+                        <p>U=@__auth.Homeserver.WhoAmI.UserId</p>
+                    }
                 </td>
                 <td>
                     <p>
@@ -51,10 +57,17 @@ Small collection of tools to do not-so-everyday things.
 
 @code
 {
+#if DEBUG
+    bool DEBUG = true;
+#else
+    bool DEBUG = false;
+#endif
+
     private class AuthInfo {
         public UserAuth UserAuth { get; set; }
         public UserInfo UserInfo { get; set; }
         public ServerVersionResponse ServerVersion { get; set; }
+        public AuthenticatedHomeserverGeneric Homeserver { get; set; }
     }
 
     // private Dictionary<UserAuth, UserInfo> _users = new();
@@ -69,7 +82,7 @@ Small collection of tools to do not-so-everyday things.
             UserInfo userInfo = new();
             AuthenticatedHomeserverGeneric hs;
             try {
-                hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken);
+                hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy);
             }
             catch (MatrixException e) {
                 if (e.ErrorCode == "M_UNKNOWN_TOKEN") {
@@ -88,7 +101,8 @@ Small collection of tools to do not-so-everyday things.
             _auth.Add(new() {
                 UserInfo = userInfo,
                 UserAuth = token,
-                ServerVersion = await hs.GetServerVersionAsync()
+                ServerVersion = await hs.GetServerVersionAsync(),
+                Homeserver = hs
             });
     // StateHasChanged();
         });
@@ -105,7 +119,7 @@ Small collection of tools to do not-so-everyday things.
     private async Task RemoveUser(UserAuth auth, bool logout = false) {
         try {
             if (logout) {
-                await (await hsProvider.GetAuthenticatedWithToken(auth.Homeserver, auth.AccessToken)).Logout();
+                await (await hsProvider.GetAuthenticatedWithToken(auth.Homeserver, auth.AccessToken, auth.Proxy)).Logout();
             }
         }
         catch (Exception e) {
diff --git a/MatrixRoomUtils.Web/Pages/LoginPage.razor b/MatrixRoomUtils.Web/Pages/LoginPage.razor
index 1b466c9..c926a93 100644
--- a/MatrixRoomUtils.Web/Pages/LoginPage.razor
+++ b/MatrixRoomUtils.Web/Pages/LoginPage.razor
@@ -7,15 +7,15 @@
 
 <span>
     <span>@@</span><!--
-    --><FancyTextBox @bind-Value="@newRecordInput.username"></FancyTextBox><!--
+    --><FancyTextBox @bind-Value="@newRecordInput.Username"></FancyTextBox><!--
     --><span>:</span><!--
-    --><FancyTextBox @bind-Value="@newRecordInput.homeserver"></FancyTextBox>
+    --><FancyTextBox @bind-Value="@newRecordInput.Homeserver"></FancyTextBox>
     via
-    <FancyTextBox @bind-Value="@newRecordInput.password" IsPassword="true"></FancyTextBox>
+    <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox>
 </span>
 <span style="display: block;">
     <label>Password:</label>
-    <FancyTextBox @bind-Value="@newRecordInput.password" IsPassword="true"></FancyTextBox>
+    <FancyTextBox @bind-Value="@newRecordInput.Password" IsPassword="true"></FancyTextBox>
 </span>
 <button @onclick="AddRecord">Add account to queue</button>
 <br/>
@@ -34,18 +34,18 @@
     </thead>
     @foreach (var record in records) {
         var r = record;
-        <tr style="background-color: @(LoggedInSessions.Any(x => x.UserId == $"@{r.username}:{r.homeserver}" && x.Proxy == r.proxy) ? "green" : "unset")">
+        <tr style="background-color: @(LoggedInSessions.Any(x => x.UserId == $"@{r.Username}:{r.Homeserver}" && x.Proxy == r.Proxy) ? "green" : "unset")">
             <td style="border-width: 1px;">
-                <FancyTextBox @bind-Value="@r.homeserver"></FancyTextBox>
+                <FancyTextBox @bind-Value="@r.Username"></FancyTextBox>
             </td>
             <td style="border-width: 1px;">
-                <FancyTextBox @bind-Value="@r.username"></FancyTextBox>
+                <FancyTextBox @bind-Value="@r.Homeserver"></FancyTextBox>
             </td>
             <td style="border-width: 1px;">
-                <FancyTextBox @bind-Value="@r.password" IsPassword="true"></FancyTextBox>
+                <FancyTextBox @bind-Value="@r.Password" IsPassword="true"></FancyTextBox>
             </td>
             <td style="border-width: 1px;">
-                <FancyTextBox @bind-Value="@r.proxy"></FancyTextBox>
+                <FancyTextBox @bind-Value="@r.Proxy"></FancyTextBox>
             </td>
             <td>
                 <a role="button" @onclick="() => records.Remove(r)">Remove</a>
@@ -59,21 +59,20 @@
 <LogView></LogView>
 
 @code {
-    readonly List<(string homeserver, string username, string password, string? proxy)> records = new();
-    (string homeserver, string username, string password, string? proxy) newRecordInput = ("", "", "", null);
+    readonly List<LoginStruct> records = new();
+    private LoginStruct newRecordInput = new();
 
     List<UserAuth>? LoggedInSessions { get; set; } = new();
 
     async Task Login() {
         var loginTasks = records.Select(async record => {
-            var (homeserver, username, password, proxy) = record;
-            if (LoggedInSessions.Any(x => x.UserId == $"@{username}:{homeserver}" && x.Proxy == proxy)) return;
+            if (LoggedInSessions.Any(x => x.UserId == $"@{record.Username}:{record.Homeserver}" && x.Proxy == record.Proxy)) return;
             try {
-                var result = new UserAuth(await hsProvider.Login(homeserver, username, password, proxy)) {
-                    Proxy = proxy
+                var result = new UserAuth(await hsProvider.Login(record.Homeserver, record.Username, record.Password, record.Proxy)) {
+                    Proxy = record.Proxy
                 };
                 if (result == null) {
-                    Console.WriteLine($"Failed to login to {homeserver} as {username}!");
+                    Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!");
                     return;
                 }
                 Console.WriteLine($"Obtained access token for {result.UserId}!");
@@ -82,7 +81,7 @@
                 LoggedInSessions = await MRUStorage.GetAllTokens();
             }
             catch (Exception e) {
-                Console.WriteLine($"Failed to login to {homeserver} as {username}!");
+                Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!");
                 Console.WriteLine(e);
             }
             StateHasChanged();
@@ -104,14 +103,21 @@
             if (parts.Length < 3)
                 continue;
             string? via = parts.Length > 3 ? parts[3] : null;
-            records.Add((parts[0], parts[1], parts[2], via));
+            records.Add(new() { Homeserver = parts[0], Username = parts[1], Password = parts[2], Proxy = via });
         }
     }
 
     private async Task AddRecord() {
         LoggedInSessions = await MRUStorage.GetAllTokens();
         records.Add(newRecordInput);
-        newRecordInput = ("", "", "", null);
+        newRecordInput = new();
     }
 
-}
+    private class LoginStruct {
+        public string? Homeserver { get; set; } = "";
+        public string? Username { get; set; } = "";
+        public string? Password { get; set; } = "";
+        public string? Proxy { get; set; }
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor b/MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor
new file mode 100644
index 0000000..02dfe44
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/ModerationUtilities/UserRoomHistory.razor
@@ -0,0 +1,113 @@
+@page "/UserRoomHistory/{UserId}"
+@using LibMatrix.Homeservers
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.RoomTypes
+@using ArcaneLibs.Extensions
+<h3>UserRoomHistory</h3>
+
+<span>Enter mxid: </span>
+<FancyTextBox @bind-Value="@UserId"></FancyTextBox>
+
+@if (string.IsNullOrWhiteSpace(UserId)) {
+    <p>UserId is null!</p>
+}
+else {
+    <p>Checked @checkedRooms.Count so far...</p>
+    @if (currentHs is not null) {
+        <p>Checking rooms from @currentHs.UserId's perspective</p>
+    }
+    else if (checkedRooms.Count > 1) {
+        <p>Done!</p>
+    }
+    @foreach (var (state, rooms) in matchingStates) {
+        <u>@state</u>
+        <br/>
+        @foreach (var roomInfo in rooms) {
+            <RoomListItem RoomInfo="roomInfo" LoadData="true"></RoomListItem>
+        }
+    }
+}
+
+@code {
+    private string? _userId;
+
+    [Parameter]
+    public string? UserId {
+        get => _userId;
+        set {
+            _userId = value;
+            FindMember(value);
+        }
+    }
+
+    private List<AuthenticatedHomeserverGeneric> hss = new();
+    private AuthenticatedHomeserverGeneric? currentHs { get; set; }
+
+    protected override async Task OnInitializedAsync() {
+        var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+        var sessions = await MRUStorage.GetAllTokens();
+        foreach (var userAuth in sessions) {
+            var session = await MRUStorage.GetSession(userAuth);
+            if (session is not null) {
+                hss.Add(session);
+                StateHasChanged();
+            }
+        }
+
+        StateHasChanged();
+        Console.WriteLine("Rerendered!");
+        await base.OnInitializedAsync();
+        if (!string.IsNullOrWhiteSpace(UserId)) FindMember(UserId);
+    }
+
+    public Dictionary<string, List<RoomInfo>> matchingStates = new();
+    public List<string> checkedRooms = new();
+    private SemaphoreSlim _semaphoreSlim = new(1, 1);
+
+    public async Task FindMember(string mxid) {
+        await _semaphoreSlim.WaitAsync();
+        if (mxid != UserId) {
+            _semaphoreSlim.Release();
+            return; //abort if changed
+        }
+        matchingStates.Clear();
+        foreach (var homeserver in hss) {
+            currentHs = homeserver;
+            var rooms = await homeserver.GetJoinedRooms();
+            rooms.RemoveAll(x => checkedRooms.Contains(x.RoomId));
+            checkedRooms.AddRange(rooms.Select(x => x.RoomId));
+            var tasks = rooms.Select(x => GetMembershipAsync(x, mxid)).ToAsyncEnumerable();
+            await foreach (var (room, state) in tasks) {
+                if (state is null) continue;
+                if (!matchingStates.ContainsKey(state.Membership))
+                    matchingStates.Add(state.Membership, new());
+                var roomInfo = new RoomInfo() {
+                    Room = room
+                };
+                matchingStates[state.Membership].Add(roomInfo);
+                roomInfo.StateEvents.Add(new() {
+                    Type = RoomNameEventContent.EventId,
+                    TypedContent = new RoomNameEventContent() {
+                        Name = await room.GetNameOrFallbackAsync(4)
+                    }
+                });
+                StateHasChanged();
+                if (mxid != UserId) {
+                    _semaphoreSlim.Release();
+                    return; //abort if changed
+                }
+            }
+            StateHasChanged();
+        }
+        currentHs = null;
+        StateHasChanged();
+        _semaphoreSlim.Release();
+    }
+
+    public async Task<(GenericRoom roomId, RoomMemberEventContent? content)> GetMembershipAsync(GenericRoom room, string mxid) {
+        return (room, await room.GetStateOrNullAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, mxid));
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Create.razor b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
index 5823757..08b21dd 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
@@ -88,7 +88,7 @@
         <tr>
             <td>Room icon:</td>
             <td>
-                <img src="@hsResolver.ResolveMediaUri(Homeserver.ServerName, roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/>
+                <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/>
                 <div style="display: inline-block; vertical-align: middle;">
                     <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox><br/>
                     <InputFile OnChange="RoomIconFilePicked"></InputFile>
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
index fd32cb3..60f4f62 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
@@ -156,6 +156,7 @@
                         Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue...";
                     }
                     RenderContents |= queue.Count == 0;
+                    if (queue.Count > 10) RenderContents = false;
                     await Task.Delay(RenderContents ? 25 : 25);
                 }
                 else {
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor b/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor
index 1f4a923..01bf555 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Timeline.razor
@@ -47,10 +47,14 @@
     private StateEventResponse GetProfileEventBefore(StateEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == "m.room.member" && e.StateKey == Event.Sender);
 
     private Type ComponentType(StateEvent Event) => Event.TypedContent switch {
-        RoomMessageEventContent => typeof(TimelineMessageItem),
+        RoomCanonicalAliasEventContent => typeof(TimelineCanonicalAliasItem),
+        RoomHistoryVisibilityEventContent => typeof(TimelineHistoryVisibilityItem),
+        RoomTopicEventContent => typeof(TimelineRoomTopicItem),
         RoomMemberEventContent => typeof(TimelineMemberItem),
+        RoomMessageEventContent => typeof(TimelineMessageItem),
         RoomCreateEventContent => typeof(TimelineRoomCreateItem),
+        RoomNameEventContent => typeof(TimelineRoomNameItem),
         _ => typeof(TimelineUnknownItem)
-        };
+    };
 
 }
diff --git a/MatrixRoomUtils.Web/Pages/ServerInfo.razor b/MatrixRoomUtils.Web/Pages/ServerInfo.razor
new file mode 100644
index 0000000..5b3f1c1
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/ServerInfo.razor
@@ -0,0 +1,235 @@
+@page "/ServerInfo/{Homeserver}"
+@using LibMatrix.Homeservers
+@using LibMatrix.Responses
+@using ArcaneLibs.Extensions
+<h3>ServerInfo</h3>
+<hr/>
+@if (ServerVersionResponse is not null) {
+    <p>Server version: @ServerVersionResponse.Server.Name @ServerVersionResponse.Server.Version</p>
+    <pre>@ServerVersionResponse?.ToJson(ignoreNull: true)</pre>
+    <br/>
+}
+@if (ClientVersionsResponse is not null) {
+    <p>Client versions:</p>
+    <details>
+        <summary>JSON data</summary>
+        <pre>@ClientVersionsResponse?.ToJson(ignoreNull: true)</pre>
+    </details>
+    <u>Spec versions</u>
+    <table>
+        <thead>
+            <td></td>
+            <td>Version</td>
+            <td>Release date</td>
+        </thead>
+        @foreach (var (version, info) in ClientVersions) {
+            <tr>
+                <td>@(ClientVersionsResponse.Versions.Contains(version) ? "\u2714" : "\u274c")</td>
+                <td><a href="@info.SpecUrl">@info.Name</a></td>
+                <td>@info.Released</td>
+            </tr>
+        }
+
+        @foreach (var version in ClientVersionsResponse.Versions) {
+            if (!ClientVersions.ContainsKey(version)) {
+                <tr>
+                    <td>@("\u2714")</td>
+                    <td><a href="https://spec.matrix.org/@version">Unknown version: @version</a></td>
+                    <td></td>
+                </tr>
+            }
+        }
+    </table>
+    <u>Unstable features</u>
+    <table>
+        <thead>
+            <td style="padding-right: 8px;">Supported</td>
+            <td style="padding-right: 8px;">Enabled</td>
+            <td style="padding-right: 8px;">Name</td>
+        </thead>
+        @* @foreach (var (version, info) in ClientVersions) { *@
+        @*     <tr> *@
+        @*          *@
+        @*             <td>@("\u2714")</td> *@
+        @*         <td>@(ClientVersionsResponse.Versions.Contains(version) ? "\u2714" : "\u274c")</td> *@
+        @*         <td>@info.Released</td> *@
+        @*     </tr> *@
+        @* } *@
+
+        @foreach (var version in ClientVersionsResponse.UnstableFeatures) {
+            if (!ClientVersions.ContainsKey(version.Key)) {
+                <tr>
+                    <td>@("\u2714")</td>
+                    <td>@(version.Value ? "\u2714" : "\u274c")</td>
+                    <td>@version.Key</td>
+                </tr>
+            }
+        }
+    </table>
+}
+
+
+@code {
+
+    [Parameter]
+    public string? Homeserver { get; set; }
+
+    public ServerVersionResponse? ServerVersionResponse { get; set; }
+    public ClientVersionsResponse? ClientVersionsResponse { get; set; }
+
+    protected override async Task OnParametersSetAsync() {
+        if (Homeserver is not null) {
+            var rhs = await hsProvider.GetRemoteHomeserver(Homeserver);
+            ServerVersionResponse = await rhs.GetServerVersionAsync();
+            ClientVersionsResponse = await rhs.GetClientVersionsAsync();
+        }
+        base.OnParametersSetAsync();
+    }
+
+    private class ClientVersionInfo {
+        public string Name { get; set; }
+        public string SpecUrl { get; set; }
+        public DateTime Released { get; set; }
+    }
+
+    private Dictionary<string, ClientVersionInfo> ClientVersions = new() {
+        {
+            "legacy",
+            new() {
+                Name = "Legacy: Last draft before  formal release of r0.0.0",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/legacy/"
+            }
+        },
+        {
+            "r0.0.0",
+            new() {
+                Name = "r0.0.0: Initial release: media repo, sync v2",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/r0.0.0/"
+            }
+        },
+        {
+            "r0.0.1",
+            new() {
+                Name = "r0.0.1: User-interactive authentication, groups, read receipts, presence",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/r0.0.1/"
+            }
+        },
+        {
+            "r0.1.0",
+            new() {
+                Name = "r0.1.0: Device management, account data, push rules, VoIP",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/r0.1.0/"
+            }
+        },
+        {
+            "r0.2.0",
+            new() {
+                Name = "r0.2.0: Clarifications",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/client_server/r0.2.0.html"
+            }
+        },
+        {
+            "r0.3.0",
+            new() {
+                Name = "r0.3.0: Device management",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/client_server/r0.3.0.html"
+            }
+        },
+        {
+            "r0.4.0",
+            new() {
+                Name = "r0.4.0: Room directory",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/r0.4.0/"
+            }
+        },
+        {
+            "r0.5.0",
+            new() {
+                Name = "r0.5.0: Push rules, VoIP, groups, read receipts, presence",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/r0.5.0/"
+            }
+        },
+        {
+            "r0.6.0",
+            new() {
+                Name = "r0.6.0: Unbinding 3PIDs, clean up bindings from register",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/r0.6.0/"
+            }
+        },
+        {
+            "r0.6.1",
+            new(){
+                Name = "r0.6.1: Moderation policies, better alias handling",
+                Released = DateTime.Parse("2014-07-01 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/legacy/r0.6.1/"
+            }
+        },
+        {
+            "v1.1",
+            new() {
+                Name = "v1.1: Key backup, knocking",
+                Released = DateTime.Parse("2021-11-09 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/v1.1/"
+            }
+        }, {
+            "v1.2",
+            new() {
+                Name = "v1.2: ",
+                Released = DateTime.Parse("2022-02-02 00:00:00 +0000"),
+                SpecUrl = "https://spec.matrix.org/v1.2/"
+            }
+        }, {
+            "v1.3",
+            new() {
+                Name = "v1.3: ",
+                Released = DateTime.Parse("2022-06-15 00:00:00 +0100"),
+                SpecUrl = "https://spec.matrix.org/v1.3/"
+            }
+        }, {
+            "v1.4",
+            new() {
+                Name = "v1.4: ",
+                Released = DateTime.Parse("2022-09-29 00:00:00 +0100"),
+                SpecUrl = "https://spec.matrix.org/v1.4/"
+            }
+        }, {
+            "v1.5",
+            new() {
+                Name = "v1.5: ",
+                Released = DateTime.Parse("2022-11-17 08:22:11 -0700"),
+                SpecUrl = "https://spec.matrix.org/v1.5/"
+            }
+        }, {
+            "v1.6",
+            new () {
+                Name = "v1.6: ",
+                Released = DateTime.Parse("2023-02-14 08:25:40 -0700"),
+                SpecUrl = "https://spec.matrix.org/v1.6"
+            }
+        }, {
+            "v1.7",
+            new () {
+                Name = "v1.7: ",
+                Released = DateTime.Parse("2023-05-25 09:47:21 -0600"),
+                SpecUrl = "https://spec.matrix.org/v1.7"
+            }
+        }, {
+            "v1.8",
+            new () {
+                Name = "v1.8: Room version 11",
+                Released = DateTime.Parse("2023-08-23 09:23:53 -0600"),
+                SpecUrl = "https://spec.matrix.org/v1.8"
+            }
+        }
+    };
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor b/MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor
index 0ab0bd2..dbf2f5f 100644
--- a/MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor
+++ b/MatrixRoomUtils.Web/Pages/Tools/KnownHomeserverList.razor
@@ -7,51 +7,43 @@
 <hr/>
 
 @if (!IsFinished) {
-    <p>Loading... Please wait...</p>
-    <progress value="@QueryProgress.ProcessedRooms" max="@QueryProgress.TotalRooms"></progress>
-    <p>@QueryProgress.ProcessedRooms / @QueryProgress.TotalRooms</p>
-    @foreach (var (room, state) in QueryProgress.ProcessedUsers.Where(x => !x.Value.IsFinished).OrderByDescending(x => x.Value.Total).ToList()) {
-        @if (state.Blocked) {
-            <p>🔒 @room.RoomId - @state.Processed / @state.Total, @state.Timing.Elapsed elapsed...</p>
-        }
-        else if (state.Slowmode) {
-            <p>🐢 @room.RoomId - @state.Processed / @state.Total, @state.Timing.Elapsed elapsed...</p>
-        }
-        else {
-            <p>@room.RoomId - @state.Processed / @state.Total, @state.Timing.Elapsed elapsed...</p>
-        }
-        <progress value="@state.Processed" max="@state.Total"></progress>
-    }
+    <p>
+        <b>Loading...</b>
+    </p>
 }
-else {
-    @foreach (var server in Homeservers.OrderByDescending(x => x.KnownUserCount).ThenBy(x => x.Server).ToList()) {
-        <p>@server.Server - @server.KnownUserCount</p>
-    }
+
+@foreach (var (homeserver, members) in counts.OrderByDescending(x => x.Value)) {
+    <p>@homeserver - @members</p>
 }
 <hr/>
 
 @code {
-    List<HomeserverInfo> Homeservers = new();
+    Dictionary<string, List<string>> homeservers { get; set; } = new();
+    Dictionary<string, int> counts { get; set; } = new();
+    // List<HomeserverInfo> Homeservers = new();
     bool IsFinished { get; set; }
-    HomeserverInfoQueryProgress QueryProgress { get; set; } = new();
-    AuthenticatedHomeserverGeneric hs { get; set; }
+    // HomeserverInfoQueryProgress QueryProgress { get; set; } = new();
+    AuthenticatedHomeserverGeneric? hs { get; set; }
+
     protected override async Task OnInitializedAsync() {
         hs = await MRUStorage.GetCurrentSessionOrNavigate();
         if (hs is null) return;
-        var sw = Stopwatch.StartNew();
-        Homeservers = await GetHomeservers(progressCallback: async progress => {
-            if (sw.ElapsedMilliseconds > 1000) {
-                Console.WriteLine("Progress updated...");
-                QueryProgress = progress;
-                StateHasChanged();
-                Console.WriteLine("Progress rendered!");
-                sw.Restart();
-                await Task.Delay(100);
-                return true;
+        var fetchTasks = (await hs.GetJoinedRooms()).Select(x=>x.GetMembersByHomeserverAsync()).ToAsyncEnumerable();
+        await foreach (var result in fetchTasks) {
+            foreach (var (resHomeserver, resMembers) in result) {
+                if (!homeservers.TryAdd(resHomeserver, resMembers)) {
+                    homeservers[resHomeserver].AddRange(resMembers);
+                }
+                counts[resHomeserver] = homeservers[resHomeserver].Count;
             }
-            Console.WriteLine($"Progress updated, but not rendering because only {sw.ElapsedMilliseconds}ms elapsed since last call...");
-            return false;
-        });
+            // StateHasChanged();
+            // await Task.Delay(250);
+        }
+
+        foreach (var resHomeserver in homeservers.Keys) {
+            homeservers[resHomeserver] = homeservers[resHomeserver].Distinct().ToList();
+            counts[resHomeserver] = homeservers[resHomeserver].Count;
+        }
 
         IsFinished = true;
         StateHasChanged();
@@ -59,64 +51,4 @@ else {
         await base.OnInitializedAsync();
     }
 
-    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 hs.GetJoinedRooms();
-        progress.TotalRooms = rooms.Count;
-
-        var semaphore = new SemaphoreSlim(4);
-        var tasks = rooms.Select(async room => {
-            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 = room.GetMembersAsync();
-            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])) continue;
-                homeServers.Add(new HomeserverInfo { Server = state.StateKey.Split(':')[1] });
-                Console.WriteLine($"Added new homeserver {state.StateKey.Split(':')[1]}");
-            }
-            semaphore.Release();
-            progress.ProcessedUsers[room].IsFinished = true;
-            progress.ProcessedRooms++;
-            if (progressCallback is not null)
-                await progressCallback.Invoke(progress);
-        });
-        // var results = tasks.ToAsyncEnumerable();
-        await Task.WhenAll(tasks);
-
-        Console.WriteLine("Calculating member counts...");
-        homeServers.ForEach(x => x.KnownUserCount = x.KnownUsers.Count);
-        Console.WriteLine(homeServers.First(x => x.Server == "rory.gay").ToJson());
-        Console.WriteLine("Recalculated!");
-        return homeServers;
-    }
-
-    class HomeserverInfo {
-        public string Server { get; set; }
-        public int? KnownUserCount { get; set; }
-        public List<string> KnownUsers { get; } = new();
-    }
-
-    class HomeserverInfoQueryProgress {
-        public int ProcessedRooms { get; set; }
-        public int TotalRooms { get; set; }
-        public Dictionary<GenericRoom, State> ProcessedUsers { get; } = new();
-        public List<HomeserverInfo> CurrentState { get; set; } = new();
-
-        public class State {
-            public int Processed { get; set; }
-            public int Total { get; set; }
-            public bool Blocked { get; set; }
-            public bool Slowmode { get; set; }
-            public float Progress => (float)Processed / Total;
-            public bool IsFinished { get; set; }
-            public Stopwatch Timing { get; } = Stopwatch.StartNew();
-        }
-    }
-
-}
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor b/MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor
index 59ec79e..20aa639 100644
--- a/MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor
+++ b/MatrixRoomUtils.Web/Pages/Tools/MediaLocator.razor
@@ -94,7 +94,7 @@
         lines.ToList().ForEach(async line => {
             await sem.WaitAsync();
             try {
-                homeservers.Add((await hsResolver.ResolveHomeserverFromWellKnown(line)).client);
+                homeservers.Add((await hsResolver.ResolveHomeserverFromWellKnown(line)).Client);
                 StateHasChanged();
             }
             catch (Exception e) {
diff --git a/MatrixRoomUtils.Web/Pages/User/DMManager.razor b/MatrixRoomUtils.Web/Pages/User/DMManager.razor
index 92e1bc2..f753f18 100644
--- a/MatrixRoomUtils.Web/Pages/User/DMManager.razor
+++ b/MatrixRoomUtils.Web/Pages/User/DMManager.razor
@@ -40,7 +40,14 @@
             var roomList = new List<RoomInfo>();
             DMRooms.Add(await Homeserver.GetProfileAsync(userId), roomList);
             foreach (var room in rooms) {
-                roomList.Add(new RoomInfo() { Room = Homeserver.GetRoom(room) });
+                var roomInfo = new RoomInfo() { Room = Homeserver.GetRoom(room) };
+                roomList.Add(roomInfo);
+                roomInfo.StateEvents.Add(new() {
+                    Type = RoomNameEventContent.EventId,
+                    TypedContent = new RoomNameEventContent() {
+                        Name = await Homeserver.GetRoom(room).GetNameOrFallbackAsync(4)
+                    }
+                });
             }
             StateHasChanged();
         }
@@ -51,6 +58,4 @@
         await base.OnInitializedAsync();
     }
 
-
-
 }
\ No newline at end of file