about summary refs log tree commit diff
diff options
context:
space:
mode:
m---------ArcaneLibs0
m---------LibMatrix0
-rw-r--r--MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj4
-rw-r--r--MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs2
-rw-r--r--MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs2
-rw-r--r--MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs4
-rw-r--r--MatrixRoomUtils.Web/Classes/RoomInfo.cs15
-rw-r--r--MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj4
-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
-rw-r--r--MatrixRoomUtils.Web/Shared/MxcImage.razor43
-rw-r--r--MatrixRoomUtils.Web/Shared/RoomListItem.razor83
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor19
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor27
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor27
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor8
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor32
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor6
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor27
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor37
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor2
-rw-r--r--MatrixRoomUtils.Web/_Imports.razor2
-rw-r--r--MatrixRoomUtils.sln.DotSettings.user6
m---------MxApiExtensions0
34 files changed, 756 insertions, 187 deletions
diff --git a/ArcaneLibs b/ArcaneLibs
-Subproject 8f3dce2beefb1117816c89d8f2a638e76306a9a
+Subproject f99ce19767f85d6d2f05919b91267cc140a792a
diff --git a/LibMatrix b/LibMatrix
-Subproject b75135d8cdb702423d693558ffaec3f025264b9
+Subproject aaa3204704b677b59b60dd2719080ead5a0c127
diff --git a/MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj b/MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj
index 70b4ffc..f21b154 100644
--- a/MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj
+++ b/MatrixRoomUtils.LibDMSpace/MatrixRoomUtils.LibDMSpace.csproj
@@ -2,9 +2,11 @@
 
     <PropertyGroup>
         <TargetFramework>net7.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <LinkIncremental>true</LinkIncremental>
         <LangVersion>preview</LangVersion>
+        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
     </PropertyGroup>
 
     <ItemGroup>
diff --git a/MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs b/MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
index b88f06a..de5cb26 100644
--- a/MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
+++ b/MatrixRoomUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
@@ -5,7 +5,7 @@ using LibMatrix.Interfaces;
 namespace MatrixRoomUtils.LibDMSpace.StateEvents; 
 
 [MatrixEvent(EventName = EventId)]
-public class DMRoomInfo : EventContent {
+public class DMRoomInfo : TimelineEventContent {
     public const string EventId = "gay.rory.dm_room_info";
     [JsonPropertyName("remote_users")]
     public List<string> RemoteUsers { get; set; }
diff --git a/MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs b/MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
index 7824324..80eeef9 100644
--- a/MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
+++ b/MatrixRoomUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
@@ -5,7 +5,7 @@ using LibMatrix.Interfaces;
 namespace MatrixRoomUtils.LibDMSpace.StateEvents; 
 
 [MatrixEvent(EventName = EventId)]
-public class DMSpaceInfo : EventContent {
+public class DMSpaceInfo : TimelineEventContent {
     public const string EventId = "gay.rory.dm_space_info";
 
     [JsonPropertyName("is_layered")]
diff --git a/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs b/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs
index b6836c8..2c3b9ce 100644
--- a/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs
+++ b/MatrixRoomUtils.Web/Classes/MRUStorageWrapper.cs
@@ -43,11 +43,11 @@ public class MRUStorageWrapper(TieredStorageService storageService, HomeserverPr
             return null;
         }
 
-        return await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken);
+        return await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy);
     }
 
     public async Task<AuthenticatedHomeserverGeneric?> GetSession(UserAuth userAuth) {
-        return await homeserverProviderService.GetAuthenticatedWithToken(userAuth.Homeserver, userAuth.AccessToken);
+        return await homeserverProviderService.GetAuthenticatedWithToken(userAuth.Homeserver, userAuth.AccessToken, userAuth.Proxy);
     }
 
     public async Task<AuthenticatedHomeserverGeneric?> GetCurrentSessionOrNavigate() {
diff --git a/MatrixRoomUtils.Web/Classes/RoomInfo.cs b/MatrixRoomUtils.Web/Classes/RoomInfo.cs
index a2fa6f5..37973a0 100644
--- a/MatrixRoomUtils.Web/Classes/RoomInfo.cs
+++ b/MatrixRoomUtils.Web/Classes/RoomInfo.cs
@@ -25,7 +25,18 @@ public class RoomInfo : NotifyPropertyChanged {
             @event.RawContent = await Room.GetStateAsync<JsonObject>(type, stateKey);
         }
         catch (MatrixException e) {
-            if (e is { ErrorCode: "M_NOT_FOUND" }) @event.RawContent = default!;
+            if (e is { ErrorCode: "M_NOT_FOUND" }) {
+                if (type == "m.room.name")
+                    @event = new() {
+                        Type = type,
+                        StateKey = stateKey,
+                        TypedContent = new RoomNameEventContent() {
+                            Name = await Room.GetNameOrFallbackAsync()
+                        }
+                    };
+                else
+                    @event.RawContent = default!;
+            }
             else throw;
         }
 
@@ -60,7 +71,7 @@ public class RoomInfo : NotifyPropertyChanged {
     private string? _roomName;
     private RoomCreateEventContent? _creationEventContent;
     private string? _roomCreator;
-    
+
     public string? DefaultRoomName { get; set; }
 
     public RoomInfo() {
diff --git a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
index 543f8db..5d5568f 100644
--- a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
+++ b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
@@ -4,9 +4,11 @@
         <TargetFramework>net7.0</TargetFramework>
         <Nullable>enable</Nullable>
         <ImplicitUsings>enable</ImplicitUsings>
-        <UseBlazorWebAssembly>true</UseBlazorWebAssembly>
         <LinkIncremental>true</LinkIncremental>
         <LangVersion>preview</LangVersion>
+        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+
+        <UseBlazorWebAssembly>true</UseBlazorWebAssembly>
     </PropertyGroup>
 
     <ItemGroup>
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
diff --git a/MatrixRoomUtils.Web/Shared/MxcImage.razor b/MatrixRoomUtils.Web/Shared/MxcImage.razor
new file mode 100644
index 0000000..f54c1f8
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/MxcImage.razor
@@ -0,0 +1,43 @@
+<img class="@Class" src="@ResolvedUri" style="@Style"/>
+@code {
+    private string _mxcUri;
+    private string _style;
+    private string _resolvedUri;
+
+    [Parameter]
+    public string MxcUri {
+        get => _mxcUri ?? "";
+        set {
+            _mxcUri = value;
+            UriHasChanged(value);
+        }
+    }
+    
+    [Parameter]
+    public string Style {
+        get => _style;
+        set {
+            _style = value;
+            StateHasChanged();
+        }
+    }
+    [Parameter]
+    public RemoteHomeserver? Homeserver { get; set; }
+
+    private string ResolvedUri {
+        get => _resolvedUri;
+        set {
+            _resolvedUri = value;
+            StateHasChanged();
+        }
+    }
+
+    private async Task UriHasChanged(string value) {
+        var uri = value[5..].Split('/');
+        ResolvedUri = (Homeserver ?? await hsProvider.GetRemoteHomeserver(uri[0])).ResolveMediaUri(value);
+    }
+
+    [Parameter]
+    public string Class { get; set; }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
index a24ccad..970526d 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListItem.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
@@ -9,14 +9,14 @@
 @if (RoomInfo is not null) {
     <div class="roomListItem @(HasDangerousRoomVersion ? "dangerousRoomVersion" : HasOldRoomVersion ? "oldRoomVersion" : "")" id="@RoomInfo.Room.RoomId">
         @if (OwnMemberState != null) {
-            <img class="avatar32 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "") @(ChildContent is not null ? "vcenter" : "")"
-                 src="@(hs.ResolveMediaUri(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl) ?? "/icon-192.png")"/>
+            <MxcImage Class="@("avatar32" + (OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? " highlightChange" : "") + (ChildContent is not null ? " vcenter" : ""))"
+                      MxcUri="@(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl)"/>
             <span class="centerVertical border75 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "")">
                 @(OwnMemberState?.DisplayName ?? GlobalProfile?.DisplayName ?? "Loading...")
             </span>
             <span class="centerVertical noLeftPadding">-></span>
         }
-        <img class="avatar32" src="@hs?.ResolveMediaUri(RoomInfo.RoomIcon)" style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/>
+        <MxcImage Class="avatar32" MxcUri="RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/>
         <div class="inlineBlock">
             <span class="centerVertical">@RoomInfo.RoomName</span>
             @if (ChildContent is not null) {
@@ -36,7 +36,13 @@ else {
     public RenderFragment? ChildContent { get; set; }
 
     [Parameter]
-    public RoomInfo? RoomInfo { get; set; }
+    public RoomInfo? RoomInfo {
+        get => _roomInfo;
+        set {
+            _roomInfo = value;
+            OnParametersSetAsync();
+        }
+    }
 
     [Parameter]
     public bool ShowOwnProfile { get; set; } = false;
@@ -48,42 +54,52 @@ else {
     public UserProfileResponse? GlobalProfile { get; set; }
 
     [Parameter]
-    public bool LoadData { get; set; } = false;
+    public bool LoadData {
+        get => _loadData;
+        set {
+            _loadData = value;
+            OnParametersSetAsync();
+        }
+    }
 
     private bool HasOldRoomVersion { get; set; } = false;
     private bool HasDangerousRoomVersion { get; set; } = false;
 
     private static SemaphoreSlim _semaphoreSlim = new(8);
+    private RoomInfo? _roomInfo;
+    private bool _loadData = false;
     private static AuthenticatedHomeserverGeneric? hs { get; set; }
 
     protected override async Task OnParametersSetAsync() {
-        RoomInfo.PropertyChanged += (_, a) => {
-            Console.WriteLine(a.PropertyName);
-            StateHasChanged();
-        };
-
-        if (LoadData) {
-            try {
-                await RoomInfo.GetStateEvent("m.room.create");
-                if (ShowOwnProfile)
-                    OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.WhoAmI.UserId)).TypedContent as RoomMemberEventContent;
-
-                await RoomInfo.GetStateEvent("m.room.name");
-                await RoomInfo.GetStateEvent("m.room.avatar");
-            }
-            catch (MatrixException e) {
-                if (e.ErrorCode == "M_FORBIDDEN") {
-                    LoadData = false;
-                    RoomInfo.StateEvents.Add(new() {
-                        Type = "m.room.create",
-                        TypedContent = new RoomCreateEventContent() { RoomVersion = "0" }
-                    });
-                    RoomInfo.StateEvents.Add(new() {
-                        Type = "m.room.name",
-                        TypedContent = new RoomNameEventContent() {
-                            Name = "M_FORBIDDEN: Are you a member of this room? " + RoomInfo.Room.RoomId
-                        }
-                    });
+        if (RoomInfo != null) {
+            RoomInfo.PropertyChanged += (_, a) => {
+                Console.WriteLine(a.PropertyName);
+                StateHasChanged();
+            };
+
+            if (LoadData) {
+                try {
+                    await RoomInfo.GetStateEvent("m.room.create");
+                    if (ShowOwnProfile)
+                        OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.WhoAmI.UserId)).TypedContent as RoomMemberEventContent;
+
+                    await RoomInfo.GetStateEvent("m.room.name");
+                    await RoomInfo.GetStateEvent("m.room.avatar");
+                }
+                catch (MatrixException e) {
+                    if (e.ErrorCode == "M_FORBIDDEN") {
+                        LoadData = false;
+                        RoomInfo.StateEvents.Add(new() {
+                            Type = "m.room.create",
+                            TypedContent = new RoomCreateEventContent() { RoomVersion = "0" }
+                        });
+                        RoomInfo.StateEvents.Add(new() {
+                            Type = "m.room.name",
+                            TypedContent = new RoomNameEventContent() {
+                                Name = "M_FORBIDDEN: Are you a member of this room? " + RoomInfo.Room.RoomId
+                            }
+                        });
+                    }
                 }
             }
         }
@@ -170,5 +186,4 @@ else {
     //     }
     // }
 
-}
-
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
index 9efeaab..8d608e3 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
@@ -1,5 +1,7 @@
 @using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.Homeservers
+@using LibMatrix.Responses
 <h3>BaseTimelineItem</h3>
 
 @code {
@@ -13,4 +15,19 @@
     [Parameter]
     public AuthenticatedHomeserverGeneric Homeserver { get; set; }
 
-}
+    public List<StateEventResponse> EventsBefore => Events.TakeWhile(e => e.EventId != Event.EventId).ToList();
+
+    public List<StateEventResponse> MatchingEventsBefore => EventsBefore.Where(x => x.Type == Event.Type && x.StateKey == Event.StateKey).ToList();
+
+    public StateEventResponse? PreviousState => MatchingEventsBefore.LastOrDefault();
+
+    public RoomMemberEventContent? CurrentSenderMemberEventContent => EventsBefore.LastOrDefault(x => x.Type == "m.room.member" && x.StateKey == Event.Sender)?
+        .TypedContent as RoomMemberEventContent;
+
+    public UserProfileResponse CurrentSenderProfile => new() { DisplayName = CurrentSenderMemberEventContent?.DisplayName, AvatarUrl = CurrentSenderMemberEventContent?.AvatarUrl };
+
+    public bool HasPreviousMessage => EventsBefore.Last() is { Type: "m.room.message" } response && response.Sender == Event.Sender;
+
+
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
new file mode 100644
index 0000000..1213432
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
@@ -0,0 +1,27 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+    @if (previousEventContent is null) {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room alias to "@currentEventContent.Alias"</i>
+    }
+    else {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room name from "@previousEventContent.Alias" to "@currentEventContent.Alias"</i>
+    }
+}
+else {
+    <details>
+        <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+        <pre>
+            @Event.ToJson()
+        </pre>
+    </details>
+}
+
+@code {
+    private RoomCanonicalAliasEventContent? previousEventContent => PreviousState?.TypedContent as RoomCanonicalAliasEventContent;
+
+    private RoomCanonicalAliasEventContent? currentEventContent => Event.TypedContent as RoomCanonicalAliasEventContent;
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
new file mode 100644
index 0000000..172a38c
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
@@ -0,0 +1,27 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+    @if (previousEventContent is null) {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the history visibility to "@currentEventContent.HistoryVisibility"</i>
+    }
+    else {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the history visibility from "@previousEventContent.HistoryVisibility" to "@currentEventContent.HistoryVisibility"</i>
+    }
+}
+else {
+    <details>
+        <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+        <pre>
+            @Event.ToJson()
+        </pre>
+    </details>
+}
+
+@code {
+    private RoomHistoryVisibilityEventContent? previousEventContent => PreviousState?.TypedContent as RoomHistoryVisibilityEventContent;
+
+    private RoomHistoryVisibilityEventContent? currentEventContent => Event.TypedContent as RoomHistoryVisibilityEventContent;
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
index ed4dceb..3b18b95 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
@@ -15,7 +15,12 @@
             <i>@Event.StateKey changed their display name to @(roomMemberData.DisplayName ?? Event.Sender)</i>
             break;
         case "join":
-            <i><InlineUserItem User="@(new UserProfileResponse())" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> joined</i>
+            @if (prevRoomMemberData is null) {
+                <i><InlineUserItem User="@(new UserProfileResponse() { DisplayName = roomMemberData.DisplayName, AvatarUrl = roomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> joined</i>
+            }
+            else {
+                <i><InlineUserItem User="@(new UserProfileResponse() { DisplayName = prevRoomMemberData.DisplayName, AvatarUrl = prevRoomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed their profile to <InlineUserItem User="@(new UserProfileResponse() { DisplayName = roomMemberData.DisplayName, AvatarUrl = roomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem></i>
+            }
             break;
         case "leave":
             <i>@Event.StateKey left</i>
@@ -43,5 +48,6 @@ else {
 @code {
 
     private RoomMemberEventContent? roomMemberData => Event.TypedContent as RoomMemberEventContent;
+    private RoomMemberEventContent? prevRoomMemberData => PreviousState?.TypedContent as RoomMemberEventContent;
 
 }
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
index 8073406..81956b0 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
@@ -1,10 +1,34 @@
 @using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec
 @inherits BaseTimelineItem
 
-<pre>
-    @Event.RawContent?.ToJson(indent: false)
-</pre>
+<span>
+    @if (!HasPreviousMessage) {
+        <span><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem>:</span><br/>
+    }
+    @switch (currentEventContent.MessageType) {
+        case "m.text": {
+            @foreach (var line in currentEventContent.Body.Split('\n')) {
+                <span>@line</span><br/>
+            }
+            break;
+        }
+        case "m.image": {
+            <i>@currentEventContent.Body</i><br/>
+            <img src="@Homeserver.ResolveMediaUri(currentEventContent.Url)">
+            break;
+        }
+        default: {
+            <pre>
+               @Event.RawContent?.ToJson(indent: false)
+            </pre>
+            break;
+        }
+    }
+</span>
 
 @code {
+    private RoomMessageEventContent? previousEventContent => PreviousState?.TypedContent as RoomMessageEventContent;
 
-}
+    private RoomMessageEventContent? currentEventContent => Event.TypedContent as RoomMessageEventContent;
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
index 2d05151..f3e6c7e 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
@@ -2,11 +2,11 @@
 @using LibMatrix.EventTypes.Spec.State
 @inherits BaseTimelineItem
 
-<p>
+<i>
     @Event.Sender created the room with room version @CreationEventContent.RoomVersion
-    @(CreationEventContent.Federate ?? false ? "and" : "without") federating with other servers.<br/>
+    @(CreationEventContent.Federate ?? true ? "and" : "without") federating with other servers.<br/>
     This room is of type @(CreationEventContent.Type ?? "Untyped room (usually a chat room)")
-</p>
+</i>
 <pre>
     @Event.RawContent?.ToJson(indent: false)
 </pre>
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
new file mode 100644
index 0000000..eeec3de
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
@@ -0,0 +1,27 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+    @if (previousEventContent is null) {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room name to "@currentEventContent.Name"</i>
+    }
+    else {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room name from "@previousEventContent.Name" to "@currentEventContent.Name"</i>
+    }
+}
+else {
+    <details>
+        <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+        <pre>
+            @Event.ToJson()
+        </pre>
+    </details>
+}
+
+@code {
+    private RoomNameEventContent? previousEventContent => PreviousState?.TypedContent as RoomNameEventContent;
+
+    private RoomNameEventContent? currentEventContent => Event.TypedContent as RoomNameEventContent;
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
new file mode 100644
index 0000000..7ef17a8
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
@@ -0,0 +1,37 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+    @if (previousEventContent is null) {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room topic to</i><br/>
+        <pre>
+            @currentEventContent.Topic
+        </pre>
+    }
+    else {
+        <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room topic from</i><br/>
+        <pre>
+            @previousEventContent.Topic
+        </pre><br/>
+        <i>to</i><br/>
+        <pre>
+            @currentEventContent.Topic
+        </pre>
+    }
+}
+else {
+    <details>
+        <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+        <pre>
+            @Event.ToJson()
+        </pre>
+    </details>
+}
+
+@code {
+    private RoomTopicEventContent? previousEventContent => PreviousState?.TypedContent as RoomTopicEventContent;
+
+    private RoomTopicEventContent? currentEventContent => Event.TypedContent as RoomTopicEventContent;
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
index 1ab530d..4f05b30 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
@@ -6,7 +6,7 @@
         <summary>
             <i style="color: red;">Unknown event type: <pre style="display: inline;">@Event.Type</pre></i>
         </summary>
-        <pre>@Event.ToJson()</pre>
+        <pre>@Event.ToJson(ignoreNull: true)</pre>
     </details>
 </div>
 
diff --git a/MatrixRoomUtils.Web/_Imports.razor b/MatrixRoomUtils.Web/_Imports.razor
index 91b69fb..0d07b3a 100644
--- a/MatrixRoomUtils.Web/_Imports.razor
+++ b/MatrixRoomUtils.Web/_Imports.razor
@@ -11,8 +11,8 @@
 @using MatrixRoomUtils.Web
 @using MatrixRoomUtils.Web.Classes
 @using MatrixRoomUtils.Web.Shared
-
 @using ArcaneLibs.Blazor.Components
+@using LibMatrix.Homeservers
 
 @inject NavigationManager NavigationManager
 @inject MRUStorageWrapper MRUStorage
diff --git a/MatrixRoomUtils.sln.DotSettings.user b/MatrixRoomUtils.sln.DotSettings.user
index b15cbb5..7597038 100644
--- a/MatrixRoomUtils.sln.DotSettings.user
+++ b/MatrixRoomUtils.sln.DotSettings.user
@@ -44,6 +44,12 @@
 	<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=c33adfe1_002D4af3_002D4c1e_002D9689_002De5e34a9f9113/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &amp;lt;LibMatrix&amp;gt; #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
   &lt;Project Location="/home/root@Rory/git/Matrix/MatrixRoomUtils" Presentation="&amp;lt;LibMatrix&amp;gt;" /&gt;
 &lt;/SessionState&gt;</s:String>
+	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=2cb12623_002D4918_002D4176_002D9b4a_002D88d846ccd3ed_0023LibMatrix_002EExampleBot/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=48dbb05f_002Db007_002D4b24_002D89b3_002D3cc177c79007_0023MediaModeratorPoC/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=95052ee6_002D7513_002D46fb_002D91bd_002Dee82026b42f1_0023PluralContactBotPoC/@EntryIndexedValue">True</s:Boolean>
+	
+	
+	
 	
 	
 	
diff --git a/MxApiExtensions b/MxApiExtensions
-Subproject cb719c315e225ba97e32b2dca7d9b184755234c
+Subproject 2e8aa30daa4a33fa33622bccb344dfc24483e32