diff --git a/MatrixRoomUtils.Web/Classes/Constants/RoomConstants.cs b/MatrixRoomUtils.Web/Classes/Constants/RoomConstants.cs
new file mode 100644
index 0000000..da6bf0d
--- /dev/null
+++ b/MatrixRoomUtils.Web/Classes/Constants/RoomConstants.cs
@@ -0,0 +1,7 @@
+namespace MatrixRoomUtils.Web.Classes.Constants;
+
+public class RoomConstants {
+
+ public static readonly string[] DangerousRoomVersions = { "1", "8" };
+ public const string RecommendedRoomVersion = "10";
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Classes/RoomInfo.cs b/MatrixRoomUtils.Web/Classes/RoomInfo.cs
index 711bf55..5ecc431 100644
--- a/MatrixRoomUtils.Web/Classes/RoomInfo.cs
+++ b/MatrixRoomUtils.Web/Classes/RoomInfo.cs
@@ -6,7 +6,7 @@ namespace MatrixRoomUtils.Web.Classes;
public class RoomInfo {
public GenericRoom Room { get; set; }
- public List<StateEventResponse?> StateEvents { get; } = new();
+ public List<StateEventResponse?> StateEvents { get; init; } = new();
public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") {
var @event = StateEvents.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey);
diff --git a/MatrixRoomUtils.Web/Classes/SessionStorageProviderService.cs b/MatrixRoomUtils.Web/Classes/SessionStorageProviderService.cs
index 58466c7..e497ee3 100644
--- a/MatrixRoomUtils.Web/Classes/SessionStorageProviderService.cs
+++ b/MatrixRoomUtils.Web/Classes/SessionStorageProviderService.cs
@@ -4,9 +4,23 @@ using MatrixRoomUtils.Core.Interfaces.Services;
namespace MatrixRoomUtils.Web.Classes;
public class SessionStorageProviderService : IStorageProvider {
- private readonly ISessionStorageService _sessionStorage;
+ private readonly ISessionStorageService _sessionStorageService;
public SessionStorageProviderService(ISessionStorageService sessionStorage) {
- _sessionStorage = sessionStorage;
+ _sessionStorageService = sessionStorage;
}
+
+ async Task IStorageProvider.SaveAllChildrenAsync<T>(string key, T value) => throw new NotImplementedException();
+
+ async Task<T?> IStorageProvider.LoadAllChildrenAsync<T>(string key) where T : default => throw new NotImplementedException();
+
+ async Task IStorageProvider.SaveObjectAsync<T>(string key, T value) => await _sessionStorageService.SetItemAsync(key, value);
+
+ async Task<T?> IStorageProvider.LoadObjectAsync<T>(string key) where T : default => await _sessionStorageService.GetItemAsync<T>(key);
+
+ async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await _sessionStorageService.ContainKeyAsync(key);
+
+ async Task<List<string>> IStorageProvider.GetAllKeysAsync() => (await _sessionStorageService.KeysAsync()).ToList();
+
+ async Task IStorageProvider.DeleteObjectAsync(string key) => await _sessionStorageService.RemoveItemAsync(key);
}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
deleted file mode 100644
index 8368aa5..0000000
--- a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
+++ /dev/null
@@ -1,339 +0,0 @@
-@* @page "/RoomManagerCreateRoom" *@
-@* @using MatrixRoomUtils.Core.Responses *@
-@* @using System.Text.Json *@
-@* @using System.Reflection *@
-@* @using MatrixRoomUtils.Core.Helpers *@
-@* @using MatrixRoomUtils.Core.StateEventTypes *@
-@* @using MatrixRoomUtils.Web.Classes.RoomCreationTemplates *@
-@* $1$ ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not #1# *@
-@* @using MatrixRoomUtils.Web.Shared.SimpleComponents *@
-@* *@
-@* <h3>Room Manager - Create Room</h3> *@
-@* *@
-@* $1$ <pre Contenteditable="true" @onkeypress="@JsonChanged" ="JsonString">@JsonString</pre> #1# *@
-@* <style> *@
-@* table.table-top-first-tr tr td:first-child { *@
-@* vertical-align: top; *@
-@* } *@
-@* </style> *@
-@* <table class="table-top-first-tr"> *@
-@* <tr> *@
-@* <td style="padding-bottom: 16px;">Preset:</td> *@
-@* <td style="padding-bottom: 16px;"> *@
-@* <InputSelect @bind-Value="@RoomPreset"> *@
-@* @foreach (var createRoomRequest in Presets) { *@
-@* <option value="@createRoomRequest.Key">@createRoomRequest.Key</option> *@
-@* } *@
-@* </InputSelect> *@
-@* </td> *@
-@* </tr> *@
-@* @if (creationEvent is not null) { *@
-@* <tr> *@
-@* <td>Room name:</td> *@
-@* <td> *@
-@* <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox> *@
-@* </td> *@
-@* </tr> *@
-@* <tr> *@
-@* <td>Room alias (localpart):</td> *@
-@* <td> *@
-@* <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox> *@
-@* </td> *@
-@* </tr> *@
-@* <tr> *@
-@* <td>Room type:</td> *@
-@* <td> *@
-@* <InputSelect @bind-Value="@creationEvent._creationContentBaseType.Type"> *@
-@* <option value="">Room</option> *@
-@* <option value="m.space">Space</option> *@
-@* </InputSelect> *@
-@* <FancyTextBox @bind-Value="@creationEvent._creationContentBaseType.Type"></FancyTextBox> *@
-@* </td> *@
-@* </tr> *@
-@* <tr> *@
-@* <td style="padding-top: 16px;">History visibility:</td> *@
-@* <td style="padding-top: 16px;"> *@
-@* $1$ <InputSelect @bind-Value="@creationEvent.HistoryVisibility"> #1# *@
-@* $1$ <option value="invited">Invited</option> #1# *@
-@* $1$ <option value="joined">Joined</option> #1# *@
-@* $1$ <option value="shared">Shared</option> #1# *@
-@* $1$ <option value="world_readable">World readable</option> #1# *@
-@* $1$ </InputSelect> #1# *@
-@* </td> *@
-@* </tr> *@
-@* <tr> *@
-@* <td>Guest access:</td> *@
-@* <td> *@
-@* <ToggleSlider Value="guestAccessEvent.IsGuestAccessEnabled" ValueChanged="@(v => { guestAccessEvent.IsGuestAccessEnabled = v; creationEvent["m.room.guest_access"].Content = guestAccessEvent; })">@(guestAccessEvent.IsGuestAccessEnabled ? "Guests can join" : "Guests cannot join") (@guestAccessEvent.GuestAccess)</ToggleSlider> *@
-@* $1$ <InputSelect @bind-Value="@creationEvent.GuestAccess"> #1# *@
-@* $1$ <option value="can_join">Can join</option> #1# *@
-@* $1$ <option value="forbidden">Forbidden</option> #1# *@
-@* $1$ </InputSelect> #1# *@
-@* </td> *@
-@* </tr> *@
-@* *@
-@* <tr> *@
-@* <td>Room icon:</td> *@
-@* <td> *@
-@* <img src="@MediaResolver.ResolveMediaUri(creationEvent.RoomIcon ?? "")" style="width: 128px; height: 128px; border-radius: 50%;"/> *@
-@* <div style=" display: inline-block; *@
-@* vertical-align: middle;"> *@
-@* <FancyTextBox @bind-Value="@creationEvent.RoomIcon"></FancyTextBox><br/> *@
-@* <InputFile OnChange="RoomIconFilePicked"></InputFile> *@
-@* </div> *@
-@* *@
-@* </td> *@
-@* </tr> *@
-@* <tr> *@
-@* <td>Permissions:</td> *@
-@* <details> *@
-@* <summary>@creationEvent.PowerLevelContentOverride.Users.Count members</summary> *@
-@* @foreach (var user in creationEvent.PowerLevelContentOverride.Events.Keys) { *@
-@* var _event = user; *@
-@* <tr> *@
-@* <td><FancyTextBox Formatter="@GetPermissionFriendlyName" Value="@_event" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }"></FancyTextBox>:</td> *@
-@* <td> *@
-@* <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/> *@
-@* </td> *@
-@* </tr> *@
-@* } *@
-@* @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) { *@
-@* var _user = user; *@
-@* <tr> *@
-@* <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td> *@
-@* <td> *@
-@* <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/> *@
-@* </td> *@
-@* </tr> *@
-@* } *@
-@* </details> *@
-@* </tr> *@
-@* <tr> *@
-@* <td>Server ACLs:</td> *@
-@* <td> *@
-@* <details> *@
-@* <summary>@(creationEvent["server"].ServerACLs.Allow.Count) allow rules</summary> *@
-@* <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLAllowRules"></StringListEditor> *@
-@* </details> *@
-@* <details> *@
-@* <summary>@creationEvent.ServerACLs.Deny.Count deny rules</summary> *@
-@* <StringListEditor ItemsChanged="OverwriteWrappedProperties" Items="@ServerACLDenyRules"></StringListEditor> *@
-@* </details> *@
-@* </td> *@
-@* </tr> *@
-@* *@
-@* <tr> *@
-@* <td>Invited members:</td> *@
-@* <td> *@
-@* <details> *@
-@* <summary>@creationEvent.InitialState.Count(x => x.Type == "m.room.member") members</summary> *@
-@* <button @onclick="() => { RuntimeCache.LoginSessions.Select(x => x.Value.LoginResponse.UserId).ToList().ForEach(InviteMember); }">Invite all logged in accounts</button> *@
-@* @foreach (var member in creationEvent.InitialState.Where(x => x.Type == "m.room.member" && x.StateKey != RuntimeCache.CurrentHomeServer.UserId)) { *@
-@* <UserListItem UserId="@member.StateKey"></UserListItem> *@
-@* } *@
-@* </details> *@
-@* </td> *@
-@* </tr> *@
-@* *@
-@* $1$ Initial states, should remain at bottom? #1# *@
-@* *@
-@* <tr> *@
-@* <td style="vertical-align: top;">Initial states:</td> *@
-@* <td> *@
-@* <details> *@
-@* *@
-@* @code{ *@
-@* *@
-@* private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" }; *@
-@* *@
-@* } *@
-@* *@
-@* <summary>@creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary> *@
-@* <table> *@
-@* @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) { *@
-@* <tr> *@
-@* <td style="vertical-align: top;"> *@
-@* @(initialState.Type): *@
-@* @if (!string.IsNullOrEmpty(initialState.StateKey)) { *@
-@* <br/> *@
-@* <span>(@initialState.StateKey)</span> *@
-@* } *@
-@* </td> *@
-@* *@
-@* <td> *@
-@* <pre>@JsonSerializer.Serialize(initialState.Content, new JsonSerializerOptions { WriteIndented = true })</pre> *@
-@* </td> *@
-@* </tr> *@
-@* } *@
-@* </table> *@
-@* </details> *@
-@* <details> *@
-@* <summary>@creationEvent.InitialState.Count initial states</summary> *@
-@* <table> *@
-@* @foreach (var initialState in creationEvent.InitialState) { *@
-@* var _state = initialState; *@
-@* <tr> *@
-@* <td style="vertical-align: top;"> *@
-@* <span>@(_state.Type):</span><br/> *@
-@* <button @onclick="() => { creationEvent.InitialState.Remove(_state); StateHasChanged(); }">Remove</button> *@
-@* </td> *@
-@* *@
-@* <td> *@
-@* <pre>@JsonSerializer.Serialize(_state.Content, new JsonSerializerOptions { WriteIndented = true })</pre> *@
-@* </td> *@
-@* </tr> *@
-@* } *@
-@* </table> *@
-@* </details> *@
-@* </td> *@
-@* </tr> *@
-@* } *@
-@* </table> *@
-@* <button @onclick="CreateRoom">Create room</button> *@
-@* <br/> *@
-@* <details> *@
-@* <summary>Creation JSON</summary> *@
-@* <pre> *@
-@* @creationEvent.ToJson(ignoreNull: true) *@
-@* </pre> *@
-@* </details> *@
-@* <details open> *@
-@* <summary>Creation JSON (with null values)</summary> *@
-@* <pre> *@
-@* @creationEvent.ToJson() *@
-@* </pre> *@
-@* </details> *@
-@* *@
-@* *@
-@* @code { *@
-@* *@
-@* private string RoomPreset { *@
-@* get { *@
-@* if (Presets.ContainsValue(creationEvent)) { *@
-@* return Presets.First(x => x.Value == creationEvent).Key; *@
-@* } *@
-@* return "Not a preset"; *@
-@* } *@
-@* set { *@
-@* creationEvent = Presets[value]; *@
-@* JsonChanged(); *@
-@* OverwriteWrappedPropertiesFromEvent(); *@
-@* creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); *@
-@* creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); *@
-@* guestAccessEvent = creationEvent["m.room.guest_access"].As<GuestAccessData>().Content; *@
-@* *@
-@* Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}"); *@
-@* Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}"); *@
-@* creationEvent["m.room.guest_access"].As<GuestAccessData>().Content.IsGuestAccessEnabled = true; *@
-@* Console.WriteLine("-- Created new guest access content --"); *@
-@* Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}"); *@
-@* Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}"); *@
-@* Console.WriteLine($"Creation event casted back: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}"); *@
-@* StateHasChanged(); *@
-@* } *@
-@* } *@
-@* *@
-@* private Dictionary<string, string> creationEventValidationErrors { get; set; } = new(); *@
-@* *@
-@* private CreateRoomRequest creationEvent { get; set; } *@
-@* GuestAccessData guestAccessEvent { get; set; } *@
-@* *@
-@* private Dictionary<string, CreateRoomRequest> Presets { get; set; } = new(); *@
-@* *@
-@* protected override async Task OnInitializedAsync() { *@
-@* *@
-@* //creationEvent = Presets["Default room"] = *@
-@* foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) { *@
-@* Console.WriteLine($"Found room creation template in class: {x.FullName}"); *@
-@* var instance = (IRoomCreationTemplate)Activator.CreateInstance(x); *@
-@* Presets[instance.Name] = instance.CreateRoomRequest; *@
-@* } *@
-@* Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); *@
-@* *@
-@* if (!Presets.ContainsKey("Default")) { *@
-@* Console.WriteLine($"No default room found in {Presets.Count} presets: {string.Join(", ", Presets.Keys)}"); *@
-@* } *@
-@* else RoomPreset = "Default"; *@
-@* *@
-@* await base.OnInitializedAsync(); *@
-@* } *@
-@* *@
-@* private void JsonChanged() => Console.WriteLine(creationEvent.ToJson()); *@
-@* *@
-@* //wrappers *@
-@* private List<string> ServerACLAllowRules { get; set; } = new(); *@
-@* private List<string> ServerACLDenyRules { get; set; } = new(); *@
-@* *@
-@* private void OverwriteWrappedPropertiesFromEvent() { *@
-@* Console.WriteLine("Overwriting wrapped properties from event"); *@
-@* ServerACLAllowRules = creationEvent.ServerACLs.Allow; *@
-@* ServerACLDenyRules = creationEvent.ServerACLs.Deny; *@
-@* } *@
-@* *@
-@* private async Task OverwriteWrappedProperties() { *@
-@* Console.WriteLine("Overwriting wrapped properties"); *@
-@* Console.WriteLine($"Allow: {ServerACLAllowRules.Count}: {string.Join(", ", ServerACLAllowRules)}"); *@
-@* Console.WriteLine($"Deny: {ServerACLDenyRules.Count}: {string.Join(", ", ServerACLDenyRules)}"); *@
-@* creationEvent.ServerACLs = new ServerACLData { *@
-@* Allow = ServerACLAllowRules, *@
-@* Deny = ServerACLDenyRules, *@
-@* AllowIpLiterals = creationEvent.ServerACLs.AllowIpLiterals *@
-@* }; *@
-@* *@
-@* StateHasChanged(); *@
-@* } *@
-@* *@
-@* private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) { *@
-@* var res = await RuntimeCache.CurrentHomeServer.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); *@
-@* Console.WriteLine(res); *@
-@* creationEvent.RoomIcon = res; *@
-@* StateHasChanged(); *@
-@* } *@
-@* *@
-@* private async Task CreateRoom() { *@
-@* Console.WriteLine("Create room"); *@
-@* Console.WriteLine(creationEvent.ToJson()); *@
-@* creationEvent.CreationContent.Add("rory.gay.created_using", "Rory&::MatrixRoomUtils (https://mru.rory.gay)"); *@
-@* //creationEvent.CreationContent.Add(); *@
-@* var id = await RuntimeCache.CurrentHomeServer.CreateRoom(creationEvent); *@
-@* // NavigationManager.NavigateTo($"/RoomManager/{id.RoomId.Replace('.','~')}"); *@
-@* } *@
-@* *@
-@* private void InviteMember(string mxid) { *@
-@* if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && RuntimeCache.CurrentHomeServer.UserId != mxid) *@
-@* creationEvent.InitialState.Add(new StateEvent { *@
-@* Type = "m.room.member", *@
-@* StateKey = mxid, *@
-@* Content = new { *@
-@* membership = "invite", *@
-@* reason = "Automatically invited at room creation time." *@
-@* } *@
-@* }); *@
-@* } *@
-@* *@
-@* private string GetStateFriendlyName(string key) => key switch { *@
-@* "m.room.history_visibility" => "History visibility", *@
-@* "m.room.guest_access" => "Guest access", *@
-@* "m.room.join_rules" => "Join rules", *@
-@* "m.room.server_acl" => "Server ACL", *@
-@* "m.room.avatar" => "Avatar", *@
-@* _ => key *@
-@* }; *@
-@* *@
-@* private string GetPermissionFriendlyName(string key) => key switch { *@
-@* "m.reaction" => "Send reaction", *@
-@* "m.room.avatar" => "Change room icon", *@
-@* "m.room.canonical_alias" => "Change room alias", *@
-@* "m.room.encryption" => "Enable encryption", *@
-@* "m.room.history_visibility" => "Change history visibility", *@
-@* "m.room.name" => "Change room name", *@
-@* "m.room.power_levels" => "Change power levels", *@
-@* "m.room.tombstone" => "Upgrade room", *@
-@* "m.room.topic" => "Change room topic", *@
-@* "m.room.pinned_events" => "Pin events", *@
-@* "m.room.server_acl" => "Change server ACLs", *@
-@* _ => key *@
-@* }; *@
-@* *@
-@* } *@
-@* *@
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Create.razor b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
new file mode 100644
index 0000000..4255424
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Create.razor
@@ -0,0 +1,322 @@
+@page "/Rooms/Create"
+@using MatrixRoomUtils.Core.Responses
+@using System.Text.Json
+@using System.Reflection
+@using MatrixRoomUtils.Core.Helpers
+@using MatrixRoomUtils.Core.StateEventTypes
+@using MatrixRoomUtils.Core.StateEventTypes.Spec
+@using MatrixRoomUtils.Web.Classes.RoomCreationTemplates
+@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@
+@using MatrixRoomUtils.Web.Shared.SimpleComponents
+
+<h3>Room Manager - Create Room</h3>
+
+@* <pre Contenteditable="true" @onkeypress="@JsonChanged" content="JsonString">@JsonString</pre> *@
+<style>
+ table.table-top-first-tr tr td:first-child {
+ vertical-align: top;
+ }
+ </style>
+<table class="table-top-first-tr">
+ <tr>
+ <td style="padding-bottom: 16px;">Preset:</td>
+ <td style="padding-bottom: 16px;">
+ <InputSelect @bind-Value="@RoomPreset">
+ @foreach (var createRoomRequest in Presets) {
+ <option value="@createRoomRequest.Key">@createRoomRequest.Key</option>
+ }
+ </InputSelect>
+ </td>
+ </tr>
+ <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>
+ <td>
+ @{
+ var guestAccessEvent = creationEvent["m.room.guest_access"].TypedContent as GuestAccessEventData;
+ }
+ <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;
+ }
+ <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>
+
+ <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>
+ }
+ </details>
+ </td>
+ </tr>
+ @* Initial states, should remain at bottom *@
+ <tr>
+ <td style="vertical-align: top;">Initial states:</td>
+ <td>
+ <details>
+
+ @code
+ {
+ private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" };
+ }
+
+ <summary> @creationEvent.InitialState.Count(x => !ImplementedStates.Contains(x.Type)) custom states</summary>
+ <table>
+ @foreach (var initialState in creationEvent.InitialState.Where(x => !ImplementedStates.Contains(x.Type))) {
+ <tr>
+ <td style="vertical-align: top;">
+ @(initialState.Type):
+ @if (!string.IsNullOrEmpty(initialState.StateKey)) {
+ <br/>
+ <span>(@initialState.StateKey)</span>
+ }
+
+ </td>
+ <td>
+ <pre>@JsonSerializer.Serialize(initialState.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>
+ <pre>
+ @creationEvent.ToJson(ignoreNull: true)
+ </pre>
+</details>
+<details open>
+ <summary>Creation JSON (with null values)</summary>
+ <pre>
+ @creationEvent.ToJson()
+ </pre>
+</details>
+
+
+@code {
+
+ private string RoomPreset {
+ get {
+ if (Presets.ContainsValue(creationEvent)) {
+ return Presets.First(x => x.Value == creationEvent).Key;
+ }
+ return "Not a preset";
+ }
+ set {
+ creationEvent = Presets[value];
+ JsonChanged();
+
+ 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; }
+ GuestAccessEventData guestAccessEvent { get; set; }
+
+ private Dictionary<string, CreateRoomRequest> Presets { get; set; } = new();
+ private AuthenticatedHomeServer? HomeServer { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ HomeServer = await MRUStorage.GetCurrentSessionOrNavigate();
+ if (HomeServer is null) return;
+
+ foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) {
+ Console.WriteLine($"Found room creation template in class: {x.FullName}");
+ var instance = (IRoomCreationTemplate)Activator.CreateInstance(x);
+ Presets[instance.Name] = instance.CreateRoomRequest;
+ }
+ Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
+
+ if (!Presets.ContainsKey("Default")) {
+ Console.WriteLine($"No default room found in {Presets.Count} presets: {string.Join(", ", Presets.Keys)}");
+ }
+ else RoomPreset = "Default";
+
+ await base.OnInitializedAsync();
+ }
+
+ private void JsonChanged() => Console.WriteLine(creationEvent.ToJson());
+
+ private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) {
+ var res = await HomeServer.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType);
+ Console.WriteLine(res);
+ (creationEvent["m.room.avatar"].TypedContent as RoomAvatarEventData).Url = res;
+ StateHasChanged();
+ }
+
+ private async Task CreateRoom() {
+ Console.WriteLine("Create room");
+ Console.WriteLine(creationEvent.ToJson());
+ creationEvent.CreationContent.Add("rory.gay.created_using", "Rory&::MatrixRoomUtils (https://mru.rory.gay)");
+ var id = await HomeServer.CreateRoom(creationEvent);
+ }
+
+ private void InviteMember(string mxid) {
+ if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && HomeServer.UserId != mxid)
+ creationEvent.InitialState.Add(new StateEvent {
+ Type = "m.room.member",
+ StateKey = mxid,
+ TypedContent = new RoomMemberEventData() {
+ Membership = "invite",
+ Reason = "Automatically invited at room creation time."
+ }
+ });
+ }
+
+ private string GetStateFriendlyName(string key) => key switch {
+ "m.room.history_visibility" => "History visibility",
+ "m.room.guest_access" => "Guest access",
+ "m.room.join_rules" => "Join rules",
+ "m.room.server_acl" => "Server ACL",
+ "m.room.avatar" => "Avatar",
+ _ => key
+ };
+
+ private string GetPermissionFriendlyName(string key) => key switch {
+ "m.reaction" => "Send reaction",
+ "m.room.avatar" => "Change room icon",
+ "m.room.canonical_alias" => "Change room alias",
+ "m.room.encryption" => "Enable encryption",
+ "m.room.history_visibility" => "Change history visibility",
+ "m.room.name" => "Change room name",
+ "m.room.power_levels" => "Change power levels",
+ "m.room.tombstone" => "Upgrade room",
+ "m.room.topic" => "Change room topic",
+ "m.room.pinned_events" => "Pin events",
+ "m.room.server_acl" => "Change server ACLs",
+ _ => key
+ };
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
index d88d5b2..a70ed9d 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
@@ -1,25 +1,149 @@
@page "/Rooms"
@using MatrixRoomUtils.Core.StateEventTypes
@using MatrixRoomUtils.Core.StateEventTypes.Spec
+@using MatrixRoomUtils.Core.Filters
+@using MatrixRoomUtils.Core.Helpers
+@using MatrixRoomUtils.Core.Responses
<h3>Room list</h3>
-
-@if (Rooms is not null) {
+<p>@Status</p>
+@if (RenderContents) {
<RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile"></RoomList>
}
@code {
- private List<RoomInfo> Rooms { get; set; }
+ private List<RoomInfo> Rooms { get; set; } = new();
private ProfileResponseEventData GlobalProfile { get; set; }
protected override async Task OnInitializedAsync() {
var hs = await MRUStorage.GetCurrentSessionOrNavigate();
if (hs is null) return;
GlobalProfile = await hs.GetProfile(hs.WhoAmI.UserId);
- Rooms = (await hs.GetJoinedRooms()).Select(x => new RoomInfo() { Room = x }).ToList();
+ var filter = new SyncFilter() {
+ AccountData = new() {
+ NotTypes = new() { "*" }
+ },
+ Presence = new() {
+ NotTypes = new() { "*" }
+ },
+ Room = new RoomFilter() {
+ AccountData = new() {
+ NotTypes = new() { "*" }
+ },
+ Ephemeral = new() {
+ NotTypes = new() { "*" }
+ },
+ State = new RoomFilter.StateFilter() {
+ Types = new List<string>() {
+ "m.room.name",
+ "m.room.avatar",
+ "m.room.create",
+ "org.matrix.mjolnir.shortcode",
+ }
+ },
+ Timeline = new() {
+ NotTypes = new() { "*" },
+ Limit = 1
+ }
+ }
+ };
+ Status = "Syncing...";
+ SyncResult? sync = null;
+ string? nextBatch = null;
+ while (sync is null or { Rooms.Join.Count: > 10}) {
+ sync = await hs.SyncHelper.Sync(since: nextBatch, filter: filter);
+ nextBatch = sync?.NextBatch ?? nextBatch;
+ if (sync is null) continue;
+ Console.WriteLine($"Got sync, next batch: {nextBatch}!");
+
+ if (sync.Rooms is null) continue;
+ if (sync.Rooms.Join is null) continue;
+ foreach (var (roomId, roomData) in sync.Rooms.Join) {
+ RoomInfo room;
+ if (Rooms.Any(x => x.Room.RoomId == roomId)) {
+ room = Rooms.First(x => x.Room.RoomId == roomId);
+ }
+ else {
+ room = new RoomInfo() {
+ Room = await hs.GetRoom(roomId),
+ StateEvents = new()
+ };
+ Rooms.Add(room);
+ }
+ room.StateEvents.AddRange(roomData.State.Events);
+ }
+ Status = $"Got {Rooms.Count} rooms so far!";
+ StateHasChanged();
+ }
+ Console.WriteLine("Sync done!");
+ Status = "Sync complete!";
+ foreach (var roomInfo in Rooms) {
+ if (!roomInfo.StateEvents.Any(x => x.Type == "m.room.name")) {
+ roomInfo.StateEvents.Add(new StateEventResponse() {
+ Type = "m.room.name",
+ TypedContent = new RoomNameEventData() {
+ Name = roomInfo.Room.RoomId
+ }
+ });
+ }
+ if (!roomInfo.StateEvents.Any(x => x.Type == "m.room.avatar")) {
+ roomInfo.StateEvents.Add(new StateEventResponse() {
+ Type = "m.room.avatar",
+ TypedContent = new RoomAvatarEventData() {
+
+ }
+ });
+ }
+ if (!roomInfo.StateEvents.Any(x => x.Type == "org.matrix.mjolnir.shortcode")) {
+ roomInfo.StateEvents.Add(new StateEventResponse() {
+ Type = "org.matrix.mjolnir.shortcode"
+ });
+ }
+ }
+ Console.WriteLine("Set stub data!");
+ Status = "Set stub data!";
+ var memberTasks = Rooms.Select(async roomInfo => {
+ if (!roomInfo.StateEvents.Any(x => x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId)) {
+ roomInfo.StateEvents.Add(new StateEventResponse() {
+ Type = "m.room.member",
+ StateKey = hs.WhoAmI.UserId,
+ TypedContent = await roomInfo.Room.GetStateAsync<RoomMemberEventData>("m.room.member", hs.WhoAmI.UserId) ?? new RoomMemberEventData() {
+ Membership = "unknown"
+ }
+ });
+ }
+ }).ToList();
+ await Task.WhenAll(memberTasks);
+ Console.WriteLine("Set all room member data!");
+ Status = "Set all room member data!";
+ // var res = await hs.SyncHelper.Sync(filter: filter);
+ // if (res is not null) {
+ // foreach (var (roomId, roomData) in res.Rooms.Join) {
+ // var room = new RoomInfo() {
+ // Room = await hs.GetRoom(roomId),
+ // StateEvents = roomData.State.Events.Where(x => x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId).ToList()
+ // };
+ // Rooms.Add(room);
+ // }
+ // }
+ // Rooms = (await hs.GetJoinedRooms()).Select(x => new RoomInfo() { Room = x }).ToList();
+ RenderContents = true;
+ Status = "";
await base.OnInitializedAsync();
}
+ private bool RenderContents { get; set; } = false;
+
+ private string _status;
+
+ public string Status {
+ get => _status;
+ set {
+ _status = value;
+ StateHasChanged();
+ }
+ }
+
}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomList.razor b/MatrixRoomUtils.Web/Shared/RoomList.razor
index db2d059..fadec1c 100644
--- a/MatrixRoomUtils.Web/Shared/RoomList.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomList.razor
@@ -2,9 +2,8 @@
@using MatrixRoomUtils.Core.StateEventTypes
@using MatrixRoomUtils.Core.StateEventTypes.Common
@using MatrixRoomUtils.Core.StateEventTypes.Spec
-<p>@Rooms.Count rooms total, @RoomsWithTypes.Sum(x=>x.Value.Count) fetched so far...</p>
@if(Rooms.Count != RoomsWithTypes.Sum(x=>x.Value.Count)) {
- <p>Fetching more rooms...</p>
+ <p>Fetching room details... @RoomsWithTypes.Sum(x=>x.Value.Count) out of @Rooms.Count done!</p>
@foreach (var category in RoomsWithTypes.OrderBy(x => x.Value.Count)) {
<p>@category.Key (@category.Value.Count)</p>
}
@@ -28,7 +27,7 @@ else {
GlobalProfile ??= await (await MRUStorage.GetCurrentSession())!.GetProfile((await MRUStorage.GetCurrentSession())!.WhoAmI.UserId);
if (RoomsWithTypes.Any()) return;
- var tasks = Rooms.Select(AddRoom);
+ var tasks = Rooms.Select(ProcessRoom);
await Task.WhenAll(tasks);
await base.OnInitializedAsync();
@@ -44,7 +43,7 @@ else {
private static SemaphoreSlim _semaphoreSlim = new(8, 8);
- private async Task AddRoom(RoomInfo room) {
+ private async Task ProcessRoom(RoomInfo room) {
await _semaphoreSlim.WaitAsync();
string roomType;
try {
@@ -56,11 +55,6 @@ else {
if(mjolnirData?.RawContent?.ToJson(ignoreNull: true) is not null and not "{}")
roomType = "Legacy policy room";
}
- //prefetch some stuff
- await Task.WhenAll(
- room.GetStateEvent("m.room.name"),
- room.GetStateEvent("m.room.name")
- );
}
catch (MatrixException e) {
roomType = $"Error: {e.ErrorCode}";
@@ -71,9 +65,7 @@ else {
}
RoomsWithTypes[roomType].Add(room);
- // if (RoomsWithTypes[roomType].Count % 10 == 0)
StateHasChanged();
- // await Task.Delay(100);
_semaphoreSlim.Release();
}
diff --git a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
index 4be3c1f..709f2d7 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
@@ -1,12 +1,13 @@
@using MatrixRoomUtils.Core.StateEventTypes
@using MatrixRoomUtils.Core.StateEventTypes.Spec
+@using MatrixRoomUtils.Web.Classes.Constants
<details>
<summary>@roomType (@rooms.Count)</summary>
@foreach (var room in rooms) {
<div class="room-list-item">
<RoomListItem RoomInfo="@room" ShowOwnProfile="@(roomType == "Room")"></RoomListItem>
- @if (room.StateEvents.Any(x => x.Type == "m.room.create")) {
-
+ @if (RoomVersionDangerLevel(room) != 0) {
+ <MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton Color="@(RoomVersionDangerLevel(room) == 2 ? "#ff0000" : "#ff8800")" href="@($"/Rooms/Create?Import={room.Room.RoomId}")">Upgrade room</MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton>
}
<MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton href="@($"/Rooms/{room.Room.RoomId}/Timeline")">View timeline</MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton>
<MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/View")">View state</MatrixRoomUtils.Web.Shared.SimpleComponents.LinkButton>
@@ -30,5 +31,15 @@
private string roomType => Category.Key;
private List<RoomInfo> rooms => Category.Value;
-
+
+ private int RoomVersionDangerLevel(RoomInfo room) {
+ var roomVersion = room.StateEvents.FirstOrDefault(x=>x.Type == "m.room.create");
+ if (roomVersion is null) return 0;
+ var roomVersionContent = roomVersion.TypedContent as RoomCreateEventData;
+ if (roomVersionContent is null) return 0;
+ if (RoomConstants.DangerousRoomVersions.Contains(roomVersionContent.RoomVersion)) return 2;
+ if (roomVersionContent.RoomVersion != RoomConstants.RecommendedRoomVersion) return 1;
+ return 0;
+ }
+
}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/RoomListItem.razor b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
index d35c9ab..b89fb18 100644
--- a/MatrixRoomUtils.Web/Shared/RoomListItem.razor
+++ b/MatrixRoomUtils.Web/Shared/RoomListItem.razor
@@ -3,6 +3,7 @@
@using MatrixRoomUtils.Core.Helpers
@using MatrixRoomUtils.Core.StateEventTypes
@using MatrixRoomUtils.Core.StateEventTypes.Spec
+@using MatrixRoomUtils.Web.Classes.Constants
<div class="roomListItem" id="@RoomId" style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content; @(hasDangerousRoomVersion ? "border: red 4px solid;" : hasOldRoomVersion ? "border: #FF0 1px solid;" : "")">
@if (OwnMemberState != null) {
<img class="imageUnloaded @(string.IsNullOrWhiteSpace(OwnMemberState?.AvatarUrl ?? GlobalProfile?.AvatarUrl) ? "" : "imageLoaded")"
@@ -56,7 +57,6 @@
private static SemaphoreSlim _semaphoreSlim = new(8);
private static AuthenticatedHomeServer? hs { get; set; }
- private static readonly string[] DangerousRoomVersions = { "1", "8" };
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
@@ -113,7 +113,7 @@
else // treat unstable room versions as dangerous
hasDangerousRoomVersion = true;
- if (DangerousRoomVersions.Contains(ce.RoomVersion)) {
+ if (RoomConstants.DangerousRoomVersions.Contains(ce.RoomVersion)) {
hasDangerousRoomVersion = true;
roomName = "Dangerous room: " + roomName;
}
diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/LinkButton.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/LinkButton.razor
index 8c9e73b..09b5c32 100644
--- a/MatrixRoomUtils.Web/Shared/SimpleComponents/LinkButton.razor
+++ b/MatrixRoomUtils.Web/Shared/SimpleComponents/LinkButton.razor
@@ -1,4 +1,4 @@
-<a href="@href" class="btn btn-primary">
+<a href="@href" class="btn btn-primary" style="background-color: @(Color ?? "#1b6ec2");">
@ChildContent
</a>
@@ -9,5 +9,8 @@
[Parameter]
public RenderFragment ChildContent { get; set; }
-
+
+ [Parameter]
+ public string? Color { get; set; }
+
}
\ No newline at end of file
|