about summary refs log tree commit diff
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2023-07-12 00:18:56 +0200
committerTheArcaneBrony <myrainbowdash949@gmail.com>2023-07-12 00:18:56 +0200
commit0e43e947ca8be0e529f2505a66143cb8b1fcbb8e (patch)
tree6fc7af3f8b7387ff5e10401f521946316f1fe5f5
parentLocal changes (diff)
downloadMatrixUtils-0e43e947ca8be0e529f2505a66143cb8b1fcbb8e.tar.xz
Changes
-rw-r--r--MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs22
-rw-r--r--MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj1
-rw-r--r--MatrixRoomUtils.Web/Pages/About.razor32
-rw-r--r--MatrixRoomUtils.Web/Pages/Rooms/Create.razor450
-rw-r--r--MatrixRoomUtils.Web/Shared/ModalWindow.razor93
-rw-r--r--MatrixRoomUtils.Web/Shared/ModalWindow.razor.css70
-rw-r--r--MatrixRoomUtils.Web/wwwroot/index.html2
-rw-r--r--MatrixRoomUtils.sln.DotSettings.user3
8 files changed, 378 insertions, 295 deletions
diff --git a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs
index 334c05c..be78a97 100644
--- a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs
+++ b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs
@@ -1,6 +1,8 @@
+using System.Reflection;
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 using System.Text.RegularExpressions;
+using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Interfaces;
 using MatrixRoomUtils.Core.StateEventTypes;
 using MatrixRoomUtils.Core.StateEventTypes.Spec;
@@ -21,7 +23,7 @@ public class CreateRoomRequest {
     //we dont want to use this, we want more control
     // [JsonPropertyName("preset")]
     // public string Preset { get; set; } = null!;
-    
+
     [JsonPropertyName("initial_state")]
     public List<StateEvent> InitialState { get; set; } = null!;
 
@@ -39,7 +41,21 @@ public class CreateRoomRequest {
     /// </summary>
 
     public StateEvent this[string event_type, string event_key = ""] {
-        get => InitialState.First(x => x.Type == event_type && x.StateKey == event_key);
+        get {
+            var stateEvent = InitialState.FirstOrDefault(x => x.Type == event_type && x.StateKey == event_key);
+            if (stateEvent == null) {
+                InitialState.Add(stateEvent = new StateEvent {
+                    Type = event_type,
+                    StateKey = event_key,
+                    TypedContent = Activator.CreateInstance(
+                        StateEvent.KnownStateEventTypes.FirstOrDefault(x =>
+                            x.GetCustomAttributes<MatrixEventAttribute>()?
+                                .Any(y => y.EventName == event_type) ?? false) ?? typeof(object)
+                        )
+                });
+            }
+            return stateEvent;
+        }
         set {
             var stateEvent = InitialState.FirstOrDefault(x => x.Type == event_type && x.StateKey == event_key);
             if (stateEvent == null)
@@ -57,4 +73,4 @@ public class CreateRoomRequest {
 
         return errors;
     }
-}
\ No newline at end of file
+}
diff --git a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
index 4bb7aeb..5e72471 100644
--- a/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
+++ b/MatrixRoomUtils.Web/MatrixRoomUtils.Web.csproj
@@ -11,6 +11,7 @@
         <PackageReference Include="Blazored.SessionStorage" Version="2.3.0" />
         <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.7" />
         <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.7" PrivateAssets="all" />
+        <PackageReference Include="XtermBlazor" Version="1.9.0" />
     </ItemGroup>
 
     <ItemGroup>
diff --git a/MatrixRoomUtils.Web/Pages/About.razor b/MatrixRoomUtils.Web/Pages/About.razor
index cf43c4f..b8d9c4a 100644
--- a/MatrixRoomUtils.Web/Pages/About.razor
+++ b/MatrixRoomUtils.Web/Pages/About.razor
@@ -1,7 +1,9 @@
 @page "/About"
 @using System.Net
+@using System.Net.Sockets
 @inject NavigationManager NavigationManager
 @inject ILocalStorageService LocalStorage
+@using XtermBlazor
 
 <PageTitle>About</PageTitle>
 
@@ -20,6 +22,9 @@
     <p>This deployment also serves a copy of the compiled, hosting-ready binaries at <a href="MRU-SRC.tar.xz">/MRU-SRC.tar.xz</a>!</p>
 }
 
+<Xterm @ref="_terminal" Options="_options" OnFirstRender="@OnFirstRender"  style="max-width: fit-content; overflow-x: hidden;"/>
+
+
 
 @code {
     private bool showBinDownload { get; set; }
@@ -34,4 +39,29 @@
         await base.OnInitializedAsync();
     }
 
-}
\ No newline at end of file
+
+    private Xterm _terminal;
+
+    private TerminalOptions _options = new TerminalOptions
+    {
+        CursorBlink = true,
+        CursorStyle = CursorStyle.Block,
+        Theme =
+        {
+            Background = "#17615e",
+        },
+    };
+
+    private async Task OnFirstRender() {
+        var message = "Hello, World!\nThis is a terminal emulator!\n\nYou can type stuff here, and it will be sent to the server!\n\nThis is a test of the emergency broadcast system.\n\nThis is only a t";
+        _terminal.Options.RendererType = RendererType.Dom;
+        _terminal.Options.ScreenReaderMode = true;
+        TcpClient.
+        for (var i = 0; i < message.Length; i++) {
+            await _terminal.Write(message[i].ToString());
+
+            await Task.Delay(50);
+            _terminal.Options.Theme.Background = $"#{(i * 2):X6}";
+        }
+    }
+}
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Create.razor b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
index 4255424..3a98801 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
@@ -12,213 +12,228 @@
 <h3>Room Manager - Create Room</h3>
 
 @* <pre Contenteditable="true" @onkeypress="@JsonChanged" content="JsonString">@JsonString</pre> *@
-<style> 
-     table.table-top-first-tr tr td:first-child { 
-         vertical-align: top; 
-     } 
+<style>
+     table.table-top-first-tr tr td:first-child {
+         vertical-align: top;
+     }
  </style>
 <table class="table-top-first-tr">
-    <tr>
-        <td style="padding-bottom: 16px;">Preset:</td>
-        <td style="padding-bottom: 16px;">
-            <InputSelect @bind-Value="@RoomPreset">
-                @foreach (var createRoomRequest in Presets) {
-                    <option value="@createRoomRequest.Key">@createRoomRequest.Key</option>
-                }
-            </InputSelect>
-        </td>
-    </tr>
-    <tr>
-        <td>Room name:</td>
-        <td>
-            <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox>
-        </td>
-    </tr>
-    <tr>
-        <td>Room alias (localpart):</td>
-        <td>
-            <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox>
-        </td>
-    </tr>
-    <tr>
-        <td>Room type:</td>
-        <td>
-            <InputSelect @bind-Value="@creationEvent._creationContentBaseType.Type">
-                <option value="">Room</option>
-                <option value="m.space">Space</option>
-            </InputSelect>
-            <FancyTextBox @bind-Value="@creationEvent._creationContentBaseType.Type"></FancyTextBox>
-        </td>
-    </tr>
-    <tr>
-        <td style="padding-top: 16px;">History visibility:</td>
-        <td style="padding-top: 16px;">
-            @{
-                var historyVisibility = creationEvent["m.room.history_visibility"].TypedContent as HistoryVisibilityEventData;
-            }
-            <InputSelect @bind-Value="@historyVisibility.HistoryVisibility">
-                <option value="invited">Invited</option>
-                <option value="joined">Joined</option>
-                <option value="shared">Shared</option>
-                <option value="world_readable">World readable</option>
-            </InputSelect>
-        </td>
-    </tr>
-    <tr>
-        <td>Guest access:</td>
+    <tr style="padding-bottom: 16px;">
+        <td>Preset:</td>
         <td>
-            @{
-                var guestAccessEvent = creationEvent["m.room.guest_access"].TypedContent as GuestAccessEventData;
+            @if (Presets is null) {
+                <p style="color: red;">Presets is null!</p>
             }
-            <ToggleSlider @bind-Value="guestAccessEvent.IsGuestAccessEnabled">
-                @(guestAccessEvent.IsGuestAccessEnabled ? "Guests can join" : "Guests cannot join") (@guestAccessEvent.GuestAccess)
-            </ToggleSlider>
-            <InputSelect @bind-Value="@guestAccessEvent.GuestAccess">
-                <option value="can_join">Can join</option>
-                <option value="forbidden">Forbidden</option>
-            </InputSelect>
-        </td>
-    </tr>
-
-    <tr>
-        <td>Room icon:</td>
-        <td>
-            @{
-                var roomAvatarEvent = creationEvent["m.room.avatar"].TypedContent as RoomAvatarEventData;
+            else {
+                <InputSelect @bind-Value="@RoomPreset">
+                    @foreach (var createRoomRequest in Presets) {
+                        <option value="@createRoomRequest.Key">@createRoomRequest.Key</option>
+                    }
+                </InputSelect>
             }
-            <img src="@MediaResolver.ResolveMediaUri(HomeServer.HomeServerDomain, 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>
-            </div>
-
-        </td>
-    </tr>
-    <tr>
-        <td>Permissions:</td>
-        <details>
-            <summary>@creationEvent.PowerLevelContentOverride.Users.Count members</summary>
-            @foreach (var user in creationEvent.PowerLevelContentOverride.Events.Keys) {
-                var _event = user;
-                <tr>
-                    <td>
-                        <FancyTextBox Formatter="@GetPermissionFriendlyName"
-                                      Value="@_event"
-                                      ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }">
-                        </FancyTextBox>:
-                    </td>
-                    <td>
-                        <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/>
-                    </td>
-                </tr>
-            }
-            @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) {
-                var _user = user;
-                <tr>
-                    <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td>
-                    <td>
-                        <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/>
-                    </td>
-                </tr>
-            }
-        </details>
-    </tr>
-    <tr>
-        <td>Server ACLs:</td>
-        <td>
-            @{
-                var serverAcl = creationEvent["m.room.server_acls"].TypedContent as ServerACLEventData;
-            }
-            <details>
-                <summary>@((creationEvent["m.room.server_acls"].TypedContent as ServerACLEventData).Allow.Count) allow rules</summary>
-                <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor>
-            </details>
-            <details>
-                <summary>@(creationEvent["m.room.server_acls"].TypedContent as ServerACLEventData).Deny.Count deny rules</summary>
-                <StringListEditor @bind-Items="@serverAcl.Deny"></StringListEditor>
-            </details>
         </td>
     </tr>
+    @if (creationEvent is not null) {
+        <tr>
+            <td>Room name:</td>
+            <td>
+                @if (creationEvent.Name is null) {
+                    <p style="color: red;">creationEvent.Name is null!</p>
+                }
+                else {
+                    <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox>
+                    <p>(#<FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox>:@HomeServer.WhoAmI.UserId.Split(':').Last())</p>
+                }
+            </td>
+        </tr>
+        <tr>
+            <td>Room type:</td>
+            <td>
+                @if (creationEvent._creationContentBaseType is null) {
+                    <p style="color: red;">creationEvent._creationContentBaseType is null!</p>
+                }
+                else {
+                    <InputSelect @bind-Value="@creationEvent._creationContentBaseType.Type">
+                        <option value="">Room</option>
+                        <option value="m.space">Space</option>
+                    </InputSelect>
+                    <FancyTextBox @bind-Value="@creationEvent._creationContentBaseType.Type"></FancyTextBox>
+                }
+            </td>
+        </tr>
+        <tr>
+            <td style="padding-top: 16px;">History visibility:</td>
+            <td style="padding-top: 16px;">
+                <InputSelect @bind-Value="@historyVisibility.HistoryVisibility">
+                    <option value="invited">Invited</option>
+                    <option value="joined">Joined</option>
+                    <option value="shared">Shared</option>
+                    <option value="world_readable">World readable</option>
+                </InputSelect>
+            </td>
+        </tr>
+        <tr>
+            <td>Guest access:</td>
+            <td>
+                <ToggleSlider @bind-Value="guestAccessEvent.IsGuestAccessEnabled">
+                    @(guestAccessEvent.IsGuestAccessEnabled ? "Guests can join" : "Guests cannot join") (@guestAccessEvent.GuestAccess)
+                </ToggleSlider>
+                <InputSelect @bind-Value="@guestAccessEvent.GuestAccess">
+                    <option value="can_join">Can join</option>
+                    <option value="forbidden">Forbidden</option>
+                </InputSelect>
+            </td>
+        </tr>
 
-    <tr>
-        <td>Invited members:</td>
-        <td>
+        <tr>
+            <td>Room icon:</td>
+            <td>
+                <img src="@MediaResolver.ResolveMediaUri(HomeServer.HomeServerDomain, 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>
+                </div>
+            </td>
+        </tr>
+        <tr>
+            <td>Permissions:</td>
             <details>
-                <summary>@creationEvent.InitialState.Count(x => x.Type == "m.room.member") members</summary>
-                @* <button @onclick="() => { RuntimeCache.LoginSessions.Select(x => x.Value.LoginResponse.UserId).ToList().ForEach(InviteMember); }">Invite all logged in accounts</button> *@
-                @foreach (var member in creationEvent.InitialState.Where(x => x.Type == "m.room.member" && x.StateKey != HomeServer.UserId)) {
-                    <UserListItem UserId="@member.StateKey"></UserListItem>
+                <summary>@creationEvent.PowerLevelContentOverride.Users.Count members</summary>
+                @foreach (var user in creationEvent.PowerLevelContentOverride.Events.Keys) {
+                    var _event = user;
+                    <tr>
+                        <td>
+                            <FancyTextBox Formatter="@GetPermissionFriendlyName"
+                                          Value="@_event"
+                                          ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }">
+                            </FancyTextBox>:
+                        </td>
+                        <td>
+                            <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/>
+                        </td>
+                    </tr>
+                }
+                @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) {
+                    var _user = user;
+                    <tr>
+                        <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td>
+                        <td>
+                            <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/>
+                        </td>
+                    </tr>
                 }
             </details>
-        </td>
-    </tr>
-    @* Initial states, should remain at bottom *@
-    <tr>
-        <td style="vertical-align: top;">Initial states:</td>
-        <td>
-            <details>
-
-                @code
-                {
-                    private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" };
+        </tr>
+        <tr>
+            <td>Server ACLs:</td>
+            <td>
+                @if (serverAcl?.Allow is null) {
+                    <p>No allow rules exist!</p>
+                    <button @onclick="@(() => { serverAcl.Allow = new() { "*" }; })">Create sane defaults</button>
                 }
+                else {
+                    <details>
+                        <summary>@((creationEvent["m.room.server_acls"].TypedContent as ServerACLEventData).Allow.Count) allow rules</summary>
+                        @* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@
+                    </details>
+                }
+                @if (serverAcl?.Deny is null) {
+                    <p>No deny rules exist!</p>
+                    <button @onclick="@(() => { serverAcl.Allow = new(); })">Create sane defaults</button>
+                }
+                else {
+                    <details>
+                        <summary>@((creationEvent["m.room.server_acls"].TypedContent as ServerACLEventData).Deny.Count) deny rules</summary>
+                        @* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@
+                    </details>
+                }
+            </td>
+        </tr>
 
-                <summary> @creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary>
-                <table>
-                    @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) {
-                        <tr>
-                            <td style="vertical-align: top;">
-                                @(initialState.Type):
-                                @if (!string.IsNullOrEmpty(initialState.StateKey)) {
-                                    <br/>
-                                    <span>(@initialState.StateKey)</span>
-                                }
-
-                            </td>
-                            <td>
-                                <pre>@JsonSerializer.Serialize(initialState.RawContent, new JsonSerializerOptions { WriteIndented = true })</pre>
-                            </td>
-                        </tr>
+        <tr>
+            <td>Invited members:</td>
+            <td>
+                <details>
+                    <summary>@creationEvent.InitialState.Count(x => x.Type == "m.room.member") members</summary>
+                    @* <button @onclick="() => { RuntimeCache.LoginSessions.Select(x => x.Value.LoginResponse.UserId).ToList().ForEach(InviteMember); }">Invite all logged in accounts</button> *@
+                    @foreach (var member in creationEvent.InitialState.Where(x => x.Type == "m.room.member" && x.StateKey != HomeServer.UserId)) {
+                        <UserListItem UserId="@member.StateKey"></UserListItem>
                     }
-                </table>
-            </details>
-            <details>
-                <summary> @creationEvent.InitialState.Count initial states</summary>
-                <table>
-                    @foreach (var initialState in creationEvent.InitialState) {
-                        var _state = initialState;
-                        <tr>
-                            <td style="vertical-align: top;">
-                                <span>@(_state.Type):</span><br/>
-                                <button @onclick="() => { creationEvent.InitialState.Remove(_state); StateHasChanged(); }">Remove</button>
-                            </td>
+                </details>
+            </td>
+        </tr>
+        @* Initial states, should remain at bottom *@
+        <tr>
+            <td style="vertical-align: top;">Initial states:</td>
+            <td>
+                <details>
 
-                            <td>
-                                <pre>@JsonSerializer.Serialize(_state.RawContent, new JsonSerializerOptions { WriteIndented = true })</pre>
-                            </td>
-                        </tr>
+                    @code
+                    {
+                        private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" };
                     }
-                </table>
-            </details>
-        </td>
-    </tr>
+
+                    <summary> @creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary>
+                    <table>
+                        @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) {
+                            <tr>
+                                <td style="vertical-align: top;">
+                                    @(initialState.Type):
+                                    @if (!string.IsNullOrEmpty(initialState.StateKey)) {
+                                        <br/>
+                                        <span>(@initialState.StateKey)</span>
+                                    }
+
+                                </td>
+                                <td>
+                                    <pre>@JsonSerializer.Serialize(initialState.RawContent, new JsonSerializerOptions { WriteIndented = true })</pre>
+                                </td>
+                            </tr>
+                        }
+                    </table>
+                </details>
+                <details>
+                    <summary> @creationEvent.InitialState.Count initial states</summary>
+                    <table>
+                        @foreach (var initialState in creationEvent.InitialState) {
+                            var _state = initialState;
+                            <tr>
+                                <td style="vertical-align: top;">
+                                    <span>@(_state.Type):</span><br/>
+                                    <button @onclick="() => { creationEvent.InitialState.Remove(_state); StateHasChanged(); }">Remove</button>
+                                </td>
+
+                                <td>
+                                    <pre>@JsonSerializer.Serialize(_state.RawContent, new JsonSerializerOptions { WriteIndented = true })</pre>
+                                </td>
+                            </tr>
+                        }
+                    </table>
+                </details>
+            </td>
+        </tr>
     }
 </table>
 <button @onclick="CreateRoom">Create room</button>
 <br/>
-<details>
-    <summary>Creation JSON</summary>
+<ModalWindow Title="Creation JSON">
     <pre>
-         @creationEvent.ToJson(ignoreNull: true) 
-     </pre>
-</details>
-<details open>
-    <summary>Creation JSON (with null values)</summary>
+        @creationEvent.ToJson(ignoreNull: true)
+    </pre>
+</ModalWindow>
+<ModalWindow Title="Creation JSON (with null values)">
     <pre>
-     @creationEvent.ToJson()
-     </pre>
-</details>
+        @creationEvent.ToJson()
+    </pre>
+</ModalWindow>
 
+@if (_matrixException is not null) {
+    <ModalWindow Title="@("Matrix exception: " + _matrixException.ErrorCode)">
+        <pre>
+            @_matrixException.Message
+        </pre>
+    </ModalWindow>
+}
 
 @code {
 
@@ -232,22 +247,22 @@
         set {
             creationEvent = Presets[value];
             JsonChanged();
-
-            creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
-            creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
-            guestAccessEvent = creationEvent["m.room.guest_access"].TypedContent as GuestAccessEventData;
             StateHasChanged();
         }
     }
 
-    private Dictionary<string, string> creationEventValidationErrors { get; set; } = new();
+    private CreateRoomRequest? creationEvent { get; set; }
 
-    private CreateRoomRequest creationEvent { get; set; }
-    GuestAccessEventData guestAccessEvent { get; set; }
-
-    private Dictionary<string, CreateRoomRequest> Presets { get; set; } = new();
+    private Dictionary<string, CreateRoomRequest>? Presets { get; set; } = new();
     private AuthenticatedHomeServer? HomeServer { get; set; }
 
+    private MatrixException? _matrixException { get; set; }
+
+    private HistoryVisibilityEventData? historyVisibility => creationEvent?["m.room.history_visibility"].TypedContent as HistoryVisibilityEventData;
+    private GuestAccessEventData? guestAccessEvent => creationEvent?["m.room.guest_access"].TypedContent as GuestAccessEventData;
+    private ServerACLEventData? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as ServerACLEventData;
+    private RoomAvatarEventData? roomAvatarEvent => creationEvent?["m.room.avatar"].TypedContent as RoomAvatarEventData;
+
     protected override async Task OnInitializedAsync() {
         HomeServer = await MRUStorage.GetCurrentSessionOrNavigate();
         if (HomeServer is null) return;
@@ -280,7 +295,12 @@
         Console.WriteLine("Create room");
         Console.WriteLine(creationEvent.ToJson());
         creationEvent.CreationContent.Add("rory.gay.created_using", "Rory&::MatrixRoomUtils (https://mru.rory.gay)");
-        var id = await HomeServer.CreateRoom(creationEvent);
+        try {
+            var id = await HomeServer.CreateRoom(creationEvent);
+        }
+        catch (MatrixException e) {
+            _matrixException = e;
+        }
     }
 
     private void InviteMember(string mxid) {
@@ -295,28 +315,28 @@
             });
     }
 
-    private string GetStateFriendlyName(string key) => key switch { 
-        "m.room.history_visibility" => "History visibility", 
-        "m.room.guest_access" => "Guest access", 
-        "m.room.join_rules" => "Join rules", 
-        "m.room.server_acl" => "Server ACL", 
-        "m.room.avatar" => "Avatar", 
-        _ => key 
+    private string GetStateFriendlyName(string key) => key switch {
+        "m.room.history_visibility" => "History visibility",
+        "m.room.guest_access" => "Guest access",
+        "m.room.join_rules" => "Join rules",
+        "m.room.server_acl" => "Server ACL",
+        "m.room.avatar" => "Avatar",
+        _ => key
         };
 
-    private string GetPermissionFriendlyName(string key) => key switch { 
-        "m.reaction" => "Send reaction", 
-        "m.room.avatar" => "Change room icon", 
-        "m.room.canonical_alias" => "Change room alias", 
-        "m.room.encryption" => "Enable encryption", 
-        "m.room.history_visibility" => "Change history visibility", 
-        "m.room.name" => "Change room name", 
-        "m.room.power_levels" => "Change power levels", 
-        "m.room.tombstone" => "Upgrade room", 
-        "m.room.topic" => "Change room topic", 
-        "m.room.pinned_events" => "Pin events", 
-        "m.room.server_acl" => "Change server ACLs", 
-        _ => key 
+    private string GetPermissionFriendlyName(string key) => key switch {
+        "m.reaction" => "Send reaction",
+        "m.room.avatar" => "Change room icon",
+        "m.room.canonical_alias" => "Change room alias",
+        "m.room.encryption" => "Enable encryption",
+        "m.room.history_visibility" => "Change history visibility",
+        "m.room.name" => "Change room name",
+        "m.room.power_levels" => "Change power levels",
+        "m.room.tombstone" => "Upgrade room",
+        "m.room.topic" => "Change room topic",
+        "m.room.pinned_events" => "Pin events",
+        "m.room.server_acl" => "Change server ACLs",
+        _ => key
         };
 
-}
\ No newline at end of file
+}
diff --git a/MatrixRoomUtils.Web/Shared/ModalWindow.razor b/MatrixRoomUtils.Web/Shared/ModalWindow.razor
index 75c2933..216f1f3 100644
--- a/MatrixRoomUtils.Web/Shared/ModalWindow.razor
+++ b/MatrixRoomUtils.Web/Shared/ModalWindow.razor
@@ -1,95 +1,37 @@
 <div class="r-modal" style="top: @(_y)px; left: @(_x)px;">
     <div class="titlebar" @onmousedown="MouseDown" @onmouseup="MouseUp" @onmousemove="MouseMove" @onmouseleave="MouseMove">
         <b class="title">@Title</b>
-        <button class="close" @onclick="OnCloseClicked">X</button>
-    </div>
-    <div class="content">
-        @ChildContent
+        <button class="btnclose" @onclick="OnCloseClicked">X</button>
+        <button class="btncollapse" @onclick="@(() => Collapsed = !Collapsed)">_</button>
     </div>
+        <div class="content" style="@(Collapsed ? "height: 0px;" : "")">
+            @ChildContent
+        </div>
 </div>
 
-<style>
-    .r-modal {
-        position: absolute;
-        width: fit-content;
-        height: fit-content;
-        z-index: 1000;
-    }
-    .r-modal:hover {
-        z-index: 1001;
-    }
-    
-    .r-modal > .titlebar {
-        position: absolute;
-        display: block;
-        top: 0;
-        left: 0;
-        width: 100%;
-        height: 25px;
-        background-color: #000;
-        user-select: none;
-    }
-    
-    .r-modal > .titlebar > .title {
-        position: relative;
-        top: 0;
-        left: 0;
-        width: fit-content;
-        height: 100%;
-        line-height: 25px;
-        padding-left: 10px;
-        color: #fff;
-    }
-    
-    .r-modal > .titlebar > .close {
-        position: absolute;
-        top: 0;
-        right: 0;
-        width: 25px;
-        height: 100%;
-        line-height: 25px;
-        text-align: center;
-        color: #fff;
-        background-color: #111;
-        cursor: pointer;
-    }
-    
-    .r-modal > .content {
-        position: relative;
-        top: 25px;
-        left: 0;
-        width: fit-content;
-        height: fit-content;
-        min-width: 150px;
-        min-height: 5px;
-        max-width: 75vw;
-        max-height: 75vh;
-        overflow: auto;
-        background-color: #fff;
-    }
-</style>
-
 @code {
-    
+
     [Parameter]
     public RenderFragment? ChildContent { get; set; }
-    
+
     [Parameter]
     public string Title { get; set; } = "Untitled window";
-    
+
     [Parameter]
     public double X { get; set; } = 60;
-    
+
     [Parameter]
     public double Y { get; set; } = 60;
-    
+
     [Parameter]
     public Action OnCloseClicked { get; set; }
-    
+
+    [Parameter]
+    public bool Collapsed { get; set; } = false;
 
     private double _x = 60;
     private double _y = 60;
-    
+
     protected override void OnInitialized() {
         _x = X;
         _y = Y;
@@ -97,16 +39,17 @@
 
     private void WindowDrag(DragEventArgs obj) {
         Console.WriteLine("Drag: " + obj.ToJson());
-        
+
         _x += obj.MovementX;
         _y += obj.MovementY;
-        
+
         StateHasChanged();
     }
 
     private bool isDragging = false;
     private double dragX = 0;
     private double dragY = 0;
+
     private void MouseDown(MouseEventArgs obj) {
         isDragging = true;
         dragX = obj.ClientX;
@@ -127,4 +70,4 @@
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/MatrixRoomUtils.Web/Shared/ModalWindow.razor.css b/MatrixRoomUtils.Web/Shared/ModalWindow.razor.css
new file mode 100644
index 0000000..b25ab0e
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/ModalWindow.razor.css
@@ -0,0 +1,70 @@
+.r-modal {
+    position: absolute;
+    width: fit-content;
+    height: fit-content;
+    z-index: 1000;
+}
+.r-modal:hover {
+    z-index: 1001;
+}
+
+.r-modal > .titlebar {
+    position: absolute;
+    display: block;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 25px;
+    background-color: #000;
+    user-select: none;
+}
+
+.r-modal > .titlebar > .title {
+    position: relative;
+    top: 0;
+    left: 0;
+    width: fit-content;
+    height: 100%;
+    line-height: 25px;
+    padding-left: 10px;
+    color: #fff;
+}
+
+.r-modal > .titlebar > .btnclose {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 25px;
+    height: 100%;
+    line-height: 25px;
+    text-align: center;
+    color: #fff;
+    background-color: #111;
+    cursor: pointer;
+}
+.r-modal > .titlebar > .btncollapse {
+    position: absolute;
+    top: 0;
+    right: 25px;
+    width: 25px;
+    height: 100%;
+    line-height: 25px;
+    text-align: center;
+    color: #fff;
+    background-color: #111;
+    cursor: pointer;
+}
+
+.r-modal > .content {
+    position: relative;
+    top: 25px;
+    left: 0;
+    width: fit-content;
+    height: fit-content;
+    min-width: 150px;
+    min-height: 5px;
+    max-width: 75vw;
+    max-height: 75vh;
+    overflow: auto;
+    background-color: #111;
+}
diff --git a/MatrixRoomUtils.Web/wwwroot/index.html b/MatrixRoomUtils.Web/wwwroot/index.html
index 9c9e7d0..0598c4d 100644
--- a/MatrixRoomUtils.Web/wwwroot/index.html
+++ b/MatrixRoomUtils.Web/wwwroot/index.html
@@ -10,6 +10,8 @@
     <link href="css/app.css" rel="stylesheet"/>
     <link href="favicon.png" rel="icon" type="image/png"/>
     <link href="MatrixRoomUtils.Web.styles.css" rel="stylesheet"/>
+    <link href="_content/XtermBlazor/XtermBlazor.css" rel="stylesheet" />
+    <script src="_content/XtermBlazor/XtermBlazor.min.js"></script>
 </head>
 
 <body>
diff --git a/MatrixRoomUtils.sln.DotSettings.user b/MatrixRoomUtils.sln.DotSettings.user
index 33f4f70..785af7c 100644
--- a/MatrixRoomUtils.sln.DotSettings.user
+++ b/MatrixRoomUtils.sln.DotSettings.user
@@ -7,13 +7,14 @@
 	
 	<s:Boolean x:Key="/Default/Environment/Hierarchy/Build/SolutionBuilderNext/ShouldRestoreNugetPackages/@EntryValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=244e90fe_002Dee26_002D4f78_002D86eb_002D27529ae48905_0023MatrixRoomUtils/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=f997f26f_002D2ec1_002D4d18_002Db3dd_002Dc46fb2ad65c0_0023MatrixRoomUtils_002EWeb_002EServer/@EntryIndexedValue">True</s:Boolean>
+	
 	
 	
 	
 	
 	
 	
-	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=f997f26f_002D2ec1_002D4d18_002Db3dd_002Dc46fb2ad65c0_0023MatrixRoomUtils_002EWeb_002EServer/@EntryIndexedValue">True</s:Boolean>