diff --git a/LibMatrix b/LibMatrix
-Subproject 0f9f9e9201bbbed5981135d67e1265fd0f31aef
+Subproject 8dadf547033d71480fd7756809992c0f32549f5
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
index dbe0648..b89d59c 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor
@@ -1,192 +1,124 @@
@page "/Rooms/{RoomId}/Policies"
@using LibMatrix
-@using LibMatrix.Homeservers
@using ArcaneLibs.Extensions
@using LibMatrix.EventTypes.Spec.State
@using LibMatrix.EventTypes.Spec.State.Policy
@using System.Diagnostics
-@using System.Diagnostics.CodeAnalysis
-@using LibMatrix.Extensions
-@using LibMatrix.Responses
-<h3>Policy list editor - Editing @RoomId</h3>
-<hr/>
+@using LibMatrix.RoomTypes
+@using System.Collections.Frozen
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using LibMatrix.EventTypes
-<p>
- This policy list contains @GetPolicyCount(typeof(ServerPolicyRuleEventContent)) server bans,
- @GetPolicyCount(typeof(RoomPolicyRuleEventContent)) room bans and
- @GetPolicyCount(typeof(UserPolicyRuleEventContent)) user bans.
- @foreach (var (key, value) in PolicyEventsByType) {
- <p>@key.Name: @value.Count</p>
- }
-</p>
-<InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label>
+@using MatrixRoomUtils.Web.Shared.PolicyEditorComponents
-<h3>Server policies</h3>
+<h3>Policy list editor - Editing @RoomId</h3>
<hr/>
-@if (!GetPolicyEventsByType(typeof(ServerPolicyRuleEventContent)).Any()) {
- <p>No server policies</p>
-}
-else {
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">Server</th>
- <th>Reason</th>
- <th>Expires</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) {
- var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
- <tr>
- <td>
- <span>Entity: @policyData.Entity</span>
- <span><br/>State: @policyEvent.StateKey</span>
- </td>
- <td>@policyData.Reason</td>
- <td>
- @policyData.ExpiryDateTime
- </td>
- <td>
- <button class="btn" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Edit</button>
- @* <button class="btn btn-danger" $1$ @onclick="async () => await RemovePolicyAsync(policyEvent)" #1#>Remove</button> *@
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>Redacted or invalid events</summary>
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">State key</th>
- <th>Serialised Contents</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) {
- <tr>
- <td>@policyEvent.StateKey</td>
- <td>@policyEvent.RawContent.ToJson(false, true)</td>
- </tr>
- }
- </tbody>
- </table>
- </details>
+@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
+<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
+
+@if (Loading) {
+ <p>Loading...</p>
}
-<h3>Room policies</h3>
-<hr/>
-@if (!GetPolicyEventsByType(typeof(RoomPolicyRuleEventContent)).Any()) {
- <p>No room policies</p>
+else if (PolicyEventsByType is not { Count: > 0 }) {
+ <p>No policies yet</p>
}
else {
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">Room</th>
- <th>Reason</th>
- <th>Expires</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) {
- var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
- <tr>
- <td>Entity: @policyData.Entity<br/>State: @policyEvent.StateKey</td>
- <td>@policyData.Reason</td>
- <td>
- @policyData.ExpiryDateTime
- </td>
- <td>
- <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>Redacted or invalid events</summary>
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">State key</th>
- <th>Serialised Contents</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) {
+ @foreach (var (type, value) in PolicyEventsByType) {
+ <p>
+ @(GetValidPolicyEventsByType(type).Count) active,
+ @(GetInvalidPolicyEventsByType(type).Count) invalid
+ (@value.Count total)
+ @(GetPolicyTypeName(type).ToLower())
+ </p>
+ }
+
+ @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
+ <details>
+ <summary>
+ <span>
+ @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies")
+ </span>
+ <hr style="margin: revert;"/>
+ </summary>
+ <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ @{
+ var policies = GetValidPolicyEventsByType(type);
+ var invalidPolicies = GetInvalidPolicyEventsByType(type);
+ // enumerate all properties with friendly name
+ var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
+ .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
+ .ToFrozenSet();
+ var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+ }
+ <thead>
<tr>
- <td>@policyEvent.StateKey</td>
- <td>@policyEvent.RawContent!.ToJson(false, true)</td>
+ @foreach (var name in propNames) {
+ <th style="border-width: 1px">@name</th>
+ }
+ <th style="border-width: 1px">Actions</th>
</tr>
- }
- </tbody>
- </table>
- </details>
-}
-<h3>User policies</h3>
-<hr/>
-@if (!GetPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Any()) {
- <p>No user policies</p>
-}
-else {
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- @if (EnableAvatars) {
- <th></th>
- }
- <th style="max-width: 0.2vw; word-wrap: anywhere;">User</th>
- <th>Reason</th>
- <th>Expires</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) {
- var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
- <tr>
- @if (EnableAvatars) {
- <td>
- @if (Avatars.ContainsKey(policyData.Entity)) {
- <img class="avatar48" src="@Avatars[policyData.Entity]"/>
+ </thead>
+ <tbody style="border-width: 1px;">
+ @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) {
+ <tr>
+ @{
+ var typedContent = policy.TypedContent!;
+ var proxySafeProps = typedContent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenSet();
+ Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
}
- </td>
+ @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
+ <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
+ }
+ <td>
+ <div style="display: ruby;">
+ @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) {
+ <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton>
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
+ @if (policy.IsLegacyType) {
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
+ }
+ }
+ </div>
+ </td>
+ </tr>
}
- <td style="word-wrap: anywhere;">Entity: @string.Join("", policyData.Entity.Take(64))<br/>State: @string.Join("", policyEvent.StateKey.Take(64))</td>
- <td>@policyData.Reason</td>
- <td>
- @policyData.ExpiryDateTime
- </td>
- <td>
- <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>Redacted or invalid events</summary>
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th>State key</th>
- <th>Serialised Contents</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) {
- <tr>
- <td>@policyEvent.StateKey</td>
- <td>@policyEvent.RawContent.ToJson(false, true)</td>
- </tr>
- }
- </tbody>
- </table>
- </details>
+ </tbody>
+ </table>
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + GetPolicyTypeName(type).ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ <thead>
+ <tr>
+ <th style="border-width: 1px">State key</th>
+ <th style="border-width: 1px">Json contents</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in invalidPolicies) {
+ <tr>
+ <td>@policy.StateKey</td>
+ <td>
+ <pre>@policy.RawContent.ToJson(true, false)</pre>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+ </details>
+ }
+}
+
+@if (CurrentlyEditingEvent is not null) {
+ <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
}
@code {
@@ -197,47 +129,59 @@ else {
private const bool Debug = false;
#endif
+ private bool Loading { get; set; } = true;
//get room list
// - sync withroom list filter
// Type = support.feline.msc3784
//support.feline.policy.lists.msc.v1
[Parameter]
- public string? RoomId { get; set; }
+ public string RoomId { get; set; } = null!;
private bool _enableAvatars;
+ private StateEventResponse? _currentlyEditingEvent;
- static readonly Dictionary<string, string?> Avatars = new();
+ // static readonly Dictionary<string, string?> Avatars = new();
// static readonly Dictionary<string, RemoteHomeserver> Servers = new();
// private static List<StateEventResponse> PolicyEvents { get; set; } = new();
private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new();
- public bool EnableAvatars {
- get => _enableAvatars;
+ private StateEventResponse? CurrentlyEditingEvent {
+ get => _currentlyEditingEvent;
set {
- _enableAvatars = value;
- if (value) GetAllAvatars();
+ _currentlyEditingEvent = value;
+ StateHasChanged();
}
}
+ // public bool EnableAvatars {
+ // get => _enableAvatars;
+ // set {
+ // _enableAvatars = value;
+ // if (value) GetAllAvatars();
+ // }
+ // }
+
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; }
+ private GenericRoom Room { get; set; }
+ private RoomPowerLevelEventContent PowerLevels { get; set; }
+
protected override async Task OnInitializedAsync() {
var sw = Stopwatch.StartNew();
await base.OnInitializedAsync();
- var hs = await MRUStorage.GetCurrentSessionOrNavigate();
- if (hs is null) return;
- RoomId = RoomId.Replace('~', '.');
+ Homeserver = (await MRUStorage.GetCurrentSessionOrNavigate())!;
+ if (Homeserver is null) return;
+ Room = Homeserver.GetRoom(RoomId!);
+ PowerLevels = (await Room.GetPowerLevelsAsync())!;
await LoadStatesAsync();
Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!");
}
private async Task LoadStatesAsync() {
- var hs = await MRUStorage.GetCurrentSessionOrNavigate();
- if (hs is null) return;
-
- var room = hs.GetRoom(RoomId);
-
- var states = room.GetFullStateAsync();
+ Loading = true;
+ var states = Room.GetFullStateAsync();
+ PolicyEventsByType.Clear();
await foreach (var state in states) {
if (state is null) continue;
if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
@@ -245,46 +189,79 @@ else {
PolicyEventsByType[state.MappedType].Add(state);
}
+ Loading = false;
StateHasChanged();
}
- private async Task GetAllAvatars() {
- // if (!_enableAvatars) return;
- Console.WriteLine("Getting avatars...");
- var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
- Console.WriteLine($"Got {users.Count} users!");
- var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
- Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
- var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
- await foreach (var server in homeserverTasks) {
- if (server is null) continue;
- var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
- await Task.WhenAll(profileTasks);
- profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
- foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
- // if (profile is null) continue;
- if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
- var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
- Avatars.TryAdd(profile.Key, url);
- }
- else Avatars.TryAdd(profile.Key, null);
- }
- StateHasChanged();
- }
+ // private async Task GetAllAvatars() {
+ // // if (!_enableAvatars) return;
+ // Console.WriteLine("Getting avatars...");
+ // var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
+ // Console.WriteLine($"Got {users.Count} users!");
+ // var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
+ // Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
+ // var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
+ // await foreach (var server in homeserverTasks) {
+ // if (server is null) continue;
+ // var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
+ // await Task.WhenAll(profileTasks);
+ // profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
+ // foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
+ // // if (profile is null) continue;
+ // if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
+ // var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
+ // Avatars.TryAdd(profile.Key, url);
+ // }
+ // else Avatars.TryAdd(profile.Key, null);
+ // }
+ //
+ // StateHasChanged();
+ // }
+ // }
+ //
+ // private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
+ // try {
+ // return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
+ // }
+ // catch {
+ // return null;
+ // }
+ // }
+
+ private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+
+ private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+
+ private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+
+ private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
+ ?? type.GetCustomAttributes<MatrixEventAttribute>()
+ .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName;
+
+ private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
+
+ private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { });
+ PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
+ await LoadStatesAsync();
}
- private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
- try {
- return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
- }
- catch {
- return null;
- }
+ private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent);
+ await LoadStatesAsync();
}
- private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
- private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
- private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
- private int GetPolicyCount(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type].Count : 0;
+ private async Task UpgradePolicyAsync(StateEventResponse policyEvent) {
+ policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type;
+ await LoadStatesAsync();
+ }
+
+ private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+
+ // event types, unnamed
+ private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
+ .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixRoomUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
new file mode 100644
index 0000000..4fd151d
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -0,0 +1,92 @@
+@using LibMatrix.EventTypes.Spec.State.Policy
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using System.Collections.Frozen
+@using LibMatrix.EventTypes
+<ModalWindow Title="@((string.IsNullOrWhiteSpace(PolicyEvent.EventId) ? "Creating new " : "Editing ") + (PolicyEvent.MappedType.GetFriendlyNameOrNull()?.ToLower() ?? "event"))"
+ OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="300">
+ @{
+ var policyData = (PolicyEvent.TypedContent as PolicyRuleEventContent)!;
+ }
+ @if (string.IsNullOrWhiteSpace(PolicyEvent.EventId)) {
+ <span>Policy type:</span>
+ <select @bind="@PolicyEvent.Type">
+ <option>Select a value</option>
+ @foreach (var (type, mappedType) in PolicyTypes) {
+ <option value="@type">@mappedType.GetFriendlyName().ToLower()</option>
+ }
+ </select>
+ }
+
+
+ @{
+ // enumerate all properties with friendly name
+ var props = PolicyEvent.MappedType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
+ .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
+ .ToFrozenSet();
+ var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+ }
+ <table>
+ <thead style="border-bottom: solid #ffffff44 1px;">
+ <tr>
+ <th>Property</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var prop in props) {
+ <tr>
+ <td style="padding-right: 8px;">
+ <span>@prop.GetFriendlyName()</span>
+ @if (Nullable.GetUnderlyingType(prop.PropertyType) is not null) {
+ <span style="color: red;">*</span>
+ }
+ </td>
+ @{
+ var getter = prop.GetGetMethod();
+ var setter = prop.GetSetMethod();
+ }
+ @switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) {
+ case Type t when t == typeof(string):
+ <FancyTextBox Value="@(getter?.Invoke(policyData, null) as string)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(policyData, [e]); StateHasChanged(); })"></FancyTextBox>
+ break;
+ default:
+ <p style="color: red;">Unsupported type: @prop.PropertyType</p>
+ break;
+ }
+ </tr>
+ }
+ </tbody>
+ </table>
+ <br/>
+ <pre>
+ @PolicyEvent.ToJson(true, false)
+ </pre>
+ <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })">Cancel</LinkButton>
+ <LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })">Save</LinkButton>
+ @* <span>Target entity: </span> *@
+ @* <FancyTextBox @bind-Value="@policyData.Entity"></FancyTextBox><br/> *@
+ @* <span>Reason: </span> *@
+ @* <FancyTextBox @bind-Value="@policyData.Reason"></FancyTextBox> *@
+</ModalWindow>
+
+@code {
+
+ [Parameter]
+ public StateEventResponse? PolicyEvent { get; set; }
+
+ [Parameter]
+ public required Action OnClose { get; set; }
+
+ [Parameter]
+ public required Action<StateEventResponse> OnSave { get; set; }
+
+ private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+
+ private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
+ .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln
index a26bbaa..e5bf946 100644
--- a/MatrixRoomUtils.sln
+++ b/MatrixRoomUtils.sln
@@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "Lib
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.Abstractions", "MatrixRoomUtils.Abstractions\MatrixRoomUtils.Abstractions.csproj", "{FE20ED20-0D55-4D74-822B-E2AC7A54C487}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLIRainbow", "CLIRainbow\CLIRainbow.csproj", "{D237350C-C1BB-4E67-B3D5-066889E1FB8B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -132,6 +134,10 @@ Global
{FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
|