diff options
author | Rory& <root@rory.gay> | 2024-01-24 02:31:56 +0100 |
---|---|---|
committer | Rory& <root@rory.gay> | 2024-01-24 17:05:25 +0100 |
commit | 03313562d21d5db9bf6a14ebbeab80e06c883d3a (patch) | |
tree | e000546a2ee8e6a886a7ed9fd01ad674178fb7cb /MatrixUtils.Web/Pages/Rooms | |
parent | Make RMU installable (diff) | |
download | MatrixUtils-03313562d21d5db9bf6a14ebbeab80e06c883d3a.tar.xz |
MRU->RMU, fixes, cleanup
Diffstat (limited to 'MatrixUtils.Web/Pages/Rooms')
-rw-r--r-- | MatrixUtils.Web/Pages/Rooms/Create.razor | 338 | ||||
-rw-r--r-- | MatrixUtils.Web/Pages/Rooms/Index.razor | 250 | ||||
-rw-r--r-- | MatrixUtils.Web/Pages/Rooms/PolicyList.razor | 267 | ||||
-rw-r--r-- | MatrixUtils.Web/Pages/Rooms/Space.razor | 100 | ||||
-rw-r--r-- | MatrixUtils.Web/Pages/Rooms/StateEditor.razor | 144 | ||||
-rw-r--r-- | MatrixUtils.Web/Pages/Rooms/StateViewer.razor | 127 | ||||
-rw-r--r-- | MatrixUtils.Web/Pages/Rooms/Timeline.razor | 60 |
7 files changed, 1286 insertions, 0 deletions
diff --git a/MatrixUtils.Web/Pages/Rooms/Create.razor b/MatrixUtils.Web/Pages/Rooms/Create.razor new file mode 100644 index 0000000..35b2ffb --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Create.razor @@ -0,0 +1,338 @@ +@page "/Rooms/Create" +@using System.Text.Json +@using System.Reflection +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers +@using LibMatrix.Responses +@using MatrixUtils.Web.Classes.RoomCreationTemplates +@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@ + +<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 style="padding-bottom: 16px;"> + <td>Preset:</td> + <td> + @if (Presets is null) { + <p style="color: red;">Presets is null!</p> + } + else { + <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> + @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>Room icon:</td> + <td> + <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/> + <div style="display: inline-block; vertical-align: middle;"> + <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox><br/> + <InputFile OnChange="RoomIconFilePicked"></InputFile> + </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> + @if (serverAcl?.Allow is null) { + <p>No allow rules exist!</p> + <button @onclick="@(() => { serverAcl.Allow = new List<string> { "*" }; })">Create sane defaults</button> + } + else { + <details> + <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).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 List<string>(); })">Create sane defaults</button> + } + else { + <details> + <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).Deny.Count) deny rules</summary> + @* <StringListEditor @bind-Items="@serverAcl.Allow"></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/> +<ModalWindow Title="Creation JSON"> + <pre> + @creationEvent.ToJson(ignoreNull: true) + </pre> +</ModalWindow> +<ModalWindow Title="Creation JSON (with null values)"> + <pre> + @creationEvent.ToJson() + </pre> +</ModalWindow> + +@if (_matrixException is not null) { + <ModalWindow Title="@("Matrix exception: " + _matrixException.ErrorCode)"> + <pre> + @_matrixException.Message + </pre> + </ModalWindow> +} + +@code { + + private string RoomPreset { + get => Presets.ContainsValue(creationEvent) ? Presets.First(x => x.Value == creationEvent).Key : "Not a preset"; + set { + creationEvent = Presets[value]; + JsonChanged(); + StateHasChanged(); + } + } + + private CreateRoomRequest? creationEvent { get; set; } + + private Dictionary<string, CreateRoomRequest>? Presets { get; set; } = new(); + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + private MatrixException? _matrixException { get; set; } + + private RoomHistoryVisibilityEventContent? historyVisibility => creationEvent?["m.room.history_visibility"].TypedContent as RoomHistoryVisibilityEventContent; + private RoomGuestAccessEventContent? guestAccessEvent => creationEvent?["m.room.guest_access"].TypedContent as RoomGuestAccessEventContent; + private RoomServerACLEventContent? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as RoomServerACLEventContent; + private RoomAvatarEventContent? roomAvatarEvent => creationEvent?["m.room.avatar"].TypedContent as RoomAvatarEventContent; + + protected override async Task OnInitializedAsync() { + Homeserver = await RMUStorage.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 RoomAvatarEventContent).Url = res; + StateHasChanged(); + } + + private async Task CreateRoom() { + Console.WriteLine("Create room"); + Console.WriteLine(creationEvent.ToJson()); + creationEvent.CreationContent.Add("rory.gay.created_using", "Rory&::MatrixUtils (https://rmu.rory.gay)"); + try { + var id = await Homeserver.CreateRoom(creationEvent); + } + catch (MatrixException e) { + _matrixException = e; + } + } + + 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 RoomMemberEventContent { + 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/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor new file mode 100644 index 0000000..0ec9487 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor @@ -0,0 +1,250 @@ +@page "/Rooms" +@using LibMatrix.Filters +@using LibMatrix.Helpers +@using LibMatrix.Extensions +@using LibMatrix.Responses +@using System.Collections.ObjectModel +@using System.Diagnostics +@using ArcaneLibs.Extensions +@using MatrixUtils.Abstractions +@inject ILogger<Index> logger +<h3>Room list</h3> + +<p>@Status</p> +<p>@Status2</p> + +<LinkButton href="/Rooms/Create">Create new room</LinkButton> + +<RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents"></RoomList> + +@code { + private ObservableCollection<RoomInfo> Rooms { get; } = new(); + private UserProfileResponse GlobalProfile { get; set; } + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + private static SyncFilter filter = new() { + AccountData = new SyncFilter.EventFilter { + NotTypes = new List<string> { "*" }, + Limit = 1 + }, + Presence = new SyncFilter.EventFilter { + NotTypes = new List<string> { "*" }, + Limit = 1 + }, + Room = new SyncFilter.RoomFilter { + AccountData = new SyncFilter.RoomFilter.StateFilter { + NotTypes = new List<string> { "*" }, + Limit = 1 + }, + Ephemeral = new SyncFilter.RoomFilter.StateFilter { + NotTypes = new List<string> { "*" }, + Limit = 1 + }, + State = new SyncFilter.RoomFilter.StateFilter { + Types = new List<string> { + "m.room.create", + "m.room.name", + "m.room.avatar", + "org.matrix.mjolnir.shortcode", + "m.room.power_levels", + } + }, + Timeline = new SyncFilter.RoomFilter.StateFilter { + NotTypes = new List<string> { "*" }, + Limit = 1 + } + } + }; + + // private static SyncFilter profileUpdateFilter = new() { + // AccountData = new SyncFilter.EventFilter { + // NotTypes = new List<string> { "*" }, + // Limit = 1 + // }, + // Presence = new SyncFilter.EventFilter { + // NotTypes = new List<string> { "*" }, + // Limit = 1 + // }, + // Room = new SyncFilter.RoomFilter { + // AccountData = new SyncFilter.RoomFilter.StateFilter { + // NotTypes = new List<string> { "*" }, + // Limit = 1 + // }, + // Ephemeral = new SyncFilter.RoomFilter.StateFilter { + // NotTypes = new List<string> { "*" }, + // Limit = 1 + // }, + // State = new SyncFilter.RoomFilter.StateFilter { + // Types = new List<string> { + // "m.room.member" + // }, + // Senders = new() + // }, + // Timeline = new SyncFilter.RoomFilter.StateFilter { + // NotTypes = new List<string> { "*" }, + // Limit = 1 + // } + // } + // }; + + private SyncHelper syncHelper; + + // SyncHelper profileSyncHelper; + + protected override async Task OnInitializedAsync() { + Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + if (Homeserver is null) return; + var rooms = await Homeserver.GetJoinedRooms(); + // SemaphoreSlim _semaphore = new(160, 160); + + var roomTasks = rooms.Select(async room => { + RoomInfo ri; + // await _semaphore.WaitAsync(); + ri = new() { Room = room }; + await Task.WhenAll((filter.Room?.State?.Types ?? []).Select(x => ri.GetStateEvent(x))); + return ri; + }).ToAsyncEnumerable(); + + await foreach (var room in roomTasks) { + Rooms.Add(room); + StateHasChanged(); + // await Task.Delay(50); + // _semaphore.Release(); + } + + if (rooms.Count >= 150) RenderContents = true; + + GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId); + syncHelper = new SyncHelper(Homeserver, logger) { + Timeout = 30000, + Filter = filter, + MinimumDelay = TimeSpan.FromMilliseconds(5000) + }; + // profileSyncHelper = new SyncHelper(Homeserver, logger) { + // Timeout = 10000, + // Filter = profileUpdateFilter, + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + // }; + // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId); + + RunSyncLoop(syncHelper); + // RunSyncLoop(profileSyncHelper); + RunQueueProcessor(); + + await base.OnInitializedAsync(); + } + + private async Task RunQueueProcessor() { + var renderTimeSw = Stopwatch.StartNew(); + var isInitialSync = true; + while (true) { + try { + while (queue.Count == 0) { + Console.WriteLine("Queue is empty, waiting..."); + await Task.Delay(isInitialSync ? 100 : 2500); + } + + Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!"); + + int maxUpdates = 10; + isInitialSync = false; + while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { + var (roomId, roomData) = queueEntry; + Console.WriteLine($"Dequeued room {roomId}"); + RoomInfo room; + + if (Rooms.Any(x => x.Room.RoomId == roomId)) { + room = Rooms.First(x => x.Room.RoomId == roomId); + Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events"); + } + else { + Console.WriteLine($"QueueWorker: encountered new room {roomId}!"); + room = new RoomInfo() { + Room = Homeserver.GetRoom(roomId) + }; + Rooms.Add(room); + } + + if (room.StateEvents is null) { + Console.WriteLine($"QueueWorker: {roomId} does not have state events on record?"); + throw new InvalidDataException("Somehow this is null???"); + } + + if (roomData.State?.Events is { Count: > 0 }) + room.StateEvents.MergeStateEventLists(roomData.State.Events); + else { + Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!"); + } + } + Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}"); + Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue..."; + + RenderContents |= queue.Count == 0; + await Task.Delay(Rooms.Count); + } + catch (Exception e) { + Console.WriteLine("QueueWorker exception: " + e); + } + } + } + + private bool RenderContents { get; set; } = false; + + private string _status; + + public string Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + private string _status2; + + public string Status2 { + get => _status2; + set { + _status2 = value; + StateHasChanged(); + } + } + + private Queue<KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>> queue = new(); + + private async Task RunSyncLoop(SyncHelper syncHelper) { + Status = "Initial syncing..."; + Console.WriteLine("starting sync"); + + var syncs = syncHelper.EnumerateSyncAsync(); + await foreach (var sync in syncs) { + Console.WriteLine("trying sync"); + if (sync is null) continue; + + Status = $"Got sync with {sync.Rooms?.Join?.Count ?? 0} room updates, next batch: {sync.NextBatch}!"; + if (sync?.Rooms?.Join != null) + foreach (var joinedRoom in sync.Rooms.Join) + if ( /*joinedRoom.Value.AccountData?.Events?.Count > 0 ||*/ joinedRoom.Value.State?.Events?.Count > 0) { + joinedRoom.Value.State.Events.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI?.UserId); + // We can't trust servers to give us what we ask for, and this ruins performance + // Thanks, Conduit. + joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false); + if (filter.Room?.State?.NotSenders?.Any() ?? false) + joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender) ?? false); + + queue.Enqueue(joinedRoom); + } + if (sync.Rooms.Leave is {Count: > 0}) + foreach (var leftRoom in sync.Rooms.Leave) + if (Rooms.Any(x => x.Room.RoomId == leftRoom.Key)) + Rooms.Remove(Rooms.First(x => x.Room.RoomId == leftRoom.Key)); + + Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue... " + + $"{sync?.Rooms?.Join?.Count ?? 0} new updates!"; + + Status2 = $"Next batch: {sync.NextBatch}"; + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor new file mode 100644 index 0000000..bfc0375 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor @@ -0,0 +1,267 @@ +@page "/Rooms/{RoomId}/Policies" +@using LibMatrix +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Diagnostics +@using LibMatrix.RoomTypes +@using System.Collections.Frozen +@using System.Reflection +@using ArcaneLibs.Attributes +@using LibMatrix.EventTypes + +@using MatrixUtils.Web.Shared.PolicyEditorComponents + +<h3>Policy list editor - Editing @RoomId</h3> +<hr/> +@* <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> +} +else if (PolicyEventsByType is not { Count: > 0 }) { + <p>No policies yet</p> +} +else { + @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> + @foreach (var name in propNames) { + <th style="border-width: 1px">@name</th> + } + <th style="border-width: 1px">Actions</th> + </tr> + </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()}"); + } + @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> + } + </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 { + +#if DEBUG + private const bool Debug = true; +#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; } = null!; + + private bool _enableAvatars; + private StateEventResponse? _currentlyEditingEvent; + + // 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(); + + private StateEventResponse? CurrentlyEditingEvent { + get => _currentlyEditingEvent; + set { + _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(); + Homeserver = (await RMUStorage.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() { + 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; + if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new()); + 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<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 UpdatePolicyAsync(StateEventResponse policyEvent) { + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent); + await LoadStatesAsync(); + } + + 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/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor new file mode 100644 index 0000000..2dd84a1 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Space.razor @@ -0,0 +1,100 @@ +@page "/Rooms/{RoomId}/Space" +@using LibMatrix.RoomTypes +@using ArcaneLibs.Extensions +@using LibMatrix +<h3>Room manager - Viewing Space</h3> + +<button onclick="@JoinAllRooms">Join all rooms</button> +@foreach (var room in Rooms) { + <RoomListItem Room="room" ShowOwnProfile="true"></RoomListItem> +} + + +<br/> +<details style="background: #0002;"> + <summary style="background: #fff1;">State list</summary> + @foreach (var stateEvent in States.OrderBy(x => x.StateKey).ThenBy(x => x.Type)) { + <p>@stateEvent.StateKey/@stateEvent.Type:</p> + <pre>@stateEvent.RawContent.ToJson()</pre> + } +</details> + +@code { + + [Parameter] + public string RoomId { get; set; } = "invalid!!!!!!"; + + private GenericRoom? Room { get; set; } + + private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>(); + private List<GenericRoom> Rooms { get; } = new(); + private List<string> ServersInSpace { get; } = new(); + + protected override async Task OnInitializedAsync() { + var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + + Room = hs.GetRoom(RoomId.Replace('~', '.')); + + var state = Room.GetFullStateAsync(); + await foreach (var stateEvent in state) { + switch (stateEvent.Type) { + case "m.space.child": { + var roomId = stateEvent.StateKey; + var room = hs.GetRoom(roomId); + if (room is not null) { + Rooms.Add(room); + } + break; + } + case "m.room.member": { + var serverName = stateEvent.StateKey.Split(':').Last(); + if (!ServersInSpace.Contains(serverName)) { + ServersInSpace.Add(serverName); + } + break; + } + } + } + await base.OnInitializedAsync(); + + // var state = await Room.GetStateAsync(""); + // if (state is not null) { + // // Console.WriteLine(state.Value.ToJson()); + // States = state.Value.Deserialize<StateEventResponse[]>()!; + // + // foreach (var stateEvent in States) { + // if (stateEvent.Type == "m.space.child") { + // // if (stateEvent.Content.ToJson().Length < 5) return; + // var roomId = stateEvent.StateKey; + // var room = hs.GetRoom(roomId); + // if (room is not null) { + // Rooms.Add(room); + // } + // } + // else if (stateEvent.Type == "m.room.member") { + // var serverName = stateEvent.StateKey.Split(':').Last(); + // if (!ServersInSpace.Contains(serverName)) { + // ServersInSpace.Add(serverName); + // } + // } + // } + + // if(state.Value.TryGetProperty("Type", out var Type)) + // { + // } + // else + // { + // //this is fine, apprently... + // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!"); + // } + + // await base.OnInitializedAsync(); + } + + private async Task JoinAllRooms() { + List<Task<RoomIdResponse>> tasks = Rooms.Select(room => room.JoinAsync(ServersInSpace.ToArray())).ToList(); + await Task.WhenAll(tasks); + } + +} diff --git a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor new file mode 100644 index 0000000..fc3a310 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor @@ -0,0 +1,144 @@ +@page "/Rooms/{RoomId}/State/Edit" +@using ArcaneLibs.Extensions +@using LibMatrix +@inject ILocalStorageService LocalStorage +@inject NavigationManager NavigationManager +<h3>Room state editor - Editing @RoomId</h3> +<hr/> + +<p>@status</p> + +<input type="checkbox" id="showAll" @bind="ShowMembershipEvents"/> Show member events +<br/> +<InputSelect @bind-Value="shownStateKey"> + <option value="">-- State key --</option> + @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey != "").Select(x => x.StateKey).Distinct().OrderBy(x => x)) { + <option value="@stateEvent">@stateEvent</option> + Console.WriteLine(stateEvent); + } +</InputSelect> +<br/> +<InputSelect @bind-Value="shownType"> + <option value="">-- Type --</option> + @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey != shownStateKey).Select(x => x.Type).Distinct().OrderBy(x => x)) { + <option value="@stateEvent">@stateEvent</option> + } +</InputSelect> +<br/> + +<textarea @bind="shownEventJson" style="width: 100%; height: fit-Content;"></textarea> + +<LogView></LogView> + +@code { + //get room list + // - sync withroom list filter + // Type = support.feline.msc3784 + //support.feline.policy.lists.msc.v1 + + [Parameter] + public string? RoomId { get; set; } + + public List<StateEventResponse> FilteredEvents { get; set; } = new(); + public List<StateEventResponse> Events { get; set; } = new(); + public string status = ""; + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + RoomId = RoomId.Replace('~', '.'); + await LoadStatesAsync(); + Console.WriteLine("Policy list editor initialized!"); + } + + private DateTime _lastUpdate = DateTime.Now; + + private async Task LoadStatesAsync() { + var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + + var StateLoaded = 0; + var response = (hs.GetRoom(RoomId)).GetFullStateAsync(); + await foreach (var _ev in response) { + // var e = new StateEventResponse { + // Type = _ev.Type, + // StateKey = _ev.StateKey, + // OriginServerTs = _ev.OriginServerTs, + // Content = _ev.Content + // }; + Events.Add(_ev); + if (string.IsNullOrEmpty(_ev.StateKey)) { + FilteredEvents.Add(_ev); + } + StateLoaded++; + + if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue; + _lastUpdate = DateTime.Now; + status = $"Loaded {StateLoaded} state events"; + StateHasChanged(); + await Task.Delay(0); + } + + StateHasChanged(); + } + + private async Task RebuildFilteredData() { + status = "Rebuilding filtered data..."; + StateHasChanged(); + await Task.Delay(1); + var _FilteredEvents = Events; + if (!ShowMembershipEvents) + _FilteredEvents = _FilteredEvents.Where(x => x.Type != "m.room.member").ToList(); + + status = "Done, rerendering!"; + StateHasChanged(); + await Task.Delay(1); + FilteredEvents = _FilteredEvents; + + if (_shownType is not null) + shownEventJson = _FilteredEvents.First(x => x.Type == _shownType).RawContent.ToJson(indent: true, ignoreNull: true); + + StateHasChanged(); + } + + public struct PreRenderedStateEvent { + public string content { get; set; } + public long origin_server_ts { get; set; } + public string state_key { get; set; } + public string type { get; set; } + // public string Sender { get; set; } + // public string EventId { get; set; } + // public string UserId { get; set; } + // public string ReplacesState { get; set; } + } + + public bool ShowMembershipEvents { + get => _showMembershipEvents; + set { + _showMembershipEvents = value; + RebuildFilteredData(); + } + } + + private bool _showMembershipEvents; + private string _shownStateKey; + private string _shownType; + + private string shownStateKey { + get => _shownStateKey; + set { + _shownStateKey = value; + RebuildFilteredData(); + } + } + + private string shownType { + get => _shownType; + set { + _shownType = value; + RebuildFilteredData(); + } + } + + private string shownEventJson { get; set; } +} diff --git a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor new file mode 100644 index 0000000..fabc33c --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor @@ -0,0 +1,127 @@ +@page "/Rooms/{RoomId}/State/View" +@using ArcaneLibs.Extensions +@using LibMatrix +@inject ILocalStorageService LocalStorage +@inject NavigationManager NavigationManager +<h3>Room state viewer - Viewing @RoomId</h3> +<hr/> + +<p>@status</p> + +<input type="checkbox" id="showAll" @bind="ShowMembershipEvents"/> Show member events + +<table class="table table-striped table-hover" style="width: fit-Content;"> + <thead> + <tr> + <th scope="col">Type</th> + <th scope="col">Content</th> + </tr> + </thead> + <tbody> + @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) { + <tr> + <td>@stateEvent.Type</td> + <td style="max-width: fit-Content;"> + <pre>@stateEvent.RawContent.ToJson()</pre> + </td> + </tr> + } + </tbody> +</table> + +@foreach (var group in FilteredEvents.GroupBy(x => x.StateKey).OrderBy(x => x.Key).Where(x => x.Key != "")) { + <details> + <summary>@group.Key</summary> + <table class="table table-striped table-hover" style="width: fit-Content;"> + <thead> + <tr> + <th scope="col">Type</th> + <th scope="col">Content</th> + </tr> + </thead> + <tbody> + @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) { + <tr> + <td>@stateEvent.Type</td> + <td style="max-width: fit-Content;"> + <pre>@stateEvent.RawContent.ToJson()</pre> + </td> + </tr> + } + </tbody> + </table> + </details> +} + +<LogView></LogView> + +@code { + //get room list + // - sync withroom list filter + // Type = support.feline.msc3784 + //support.feline.policy.lists.msc.v1 + + [Parameter] + public string? RoomId { get; set; } + + public List<StateEventResponse> FilteredEvents { get; set; } = new(); + public List<StateEventResponse> Events { get; set; } = new(); + public string status = ""; + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + await LoadStatesAsync(); + Console.WriteLine("Policy list editor initialized!"); + } + + private DateTime _lastUpdate = DateTime.Now; + + private async Task LoadStatesAsync() { + var StateLoaded = 0; + var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + var response = (hs.GetRoom(RoomId)).GetFullStateAsync(); + await foreach (var _ev in response) { + Events.Add(_ev); + if (string.IsNullOrEmpty(_ev.StateKey)) { + FilteredEvents.Add(_ev); + } + StateLoaded++; + + if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue; + _lastUpdate = DateTime.Now; + status = $"Loaded {StateLoaded} state events"; + StateHasChanged(); + await Task.Delay(0); + } + + StateHasChanged(); + } + + private async Task RebuildFilteredData() { + status = "Rebuilding filtered data..."; + StateHasChanged(); + await Task.Delay(1); + var _FilteredEvents = Events; + if (!ShowMembershipEvents) + _FilteredEvents = _FilteredEvents.Where(x => x.Type != "m.room.member").ToList(); + + status = "Done, rerendering!"; + StateHasChanged(); + await Task.Delay(1); + FilteredEvents = _FilteredEvents; + StateHasChanged(); + } + + public bool ShowMembershipEvents { + get => _showMembershipEvents; + set { + _showMembershipEvents = value; + RebuildFilteredData(); + } + } + + private bool _showMembershipEvents; +} diff --git a/MatrixUtils.Web/Pages/Rooms/Timeline.razor b/MatrixUtils.Web/Pages/Rooms/Timeline.razor new file mode 100644 index 0000000..8d0f731 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor @@ -0,0 +1,60 @@ +@page "/Rooms/{RoomId}/Timeline" +@using MatrixUtils.Web.Shared.TimelineComponents +@using LibMatrix +@using LibMatrix.EventTypes.Spec +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Homeservers +<h3>RoomManagerTimeline</h3> +<hr/> +<p>Loaded @Events.Count events...</p> + +@foreach (var evt in Events) { + <div type="@evt.Type" key="@evt.StateKey" itemid="@evt.EventId"> + <DynamicComponent Type="@ComponentType(evt)" + Parameters="@(new Dictionary<string, object> { { "Event", evt }, { "Events", Events }, { "Homeserver", Homeserver!} })"> + </DynamicComponent> + </div> +} + +@code { + + [Parameter] + public string RoomId { get; set; } + + private List<MessagesResponse> Messages { get; } = new(); + private List<StateEventResponse> Events { get; } = new(); + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + protected override async Task OnInitializedAsync() { + Console.WriteLine("RoomId: " + RoomId); + Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + if (Homeserver is null) return; + var room = Homeserver.GetRoom(RoomId); + MessagesResponse? msgs = null; + do { + msgs = await room.GetMessagesAsync(limit: 1000, from: msgs?.End, dir: "b"); + Messages.Add(msgs); + Console.WriteLine($"Got {msgs.Chunk.Count} messages"); + msgs.Chunk.Reverse(); + Events.InsertRange(0, msgs.Chunk); + } while (msgs.End is not null); + + + await base.OnInitializedAsync(); + } + + private StateEventResponse GetProfileEventBefore(StateEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == "m.room.member" && e.StateKey == Event.Sender); + + private Type ComponentType(StateEvent Event) => Event.TypedContent switch { + RoomCanonicalAliasEventContent => typeof(TimelineCanonicalAliasItem), + RoomHistoryVisibilityEventContent => typeof(TimelineHistoryVisibilityItem), + RoomTopicEventContent => typeof(TimelineRoomTopicItem), + RoomMemberEventContent => typeof(TimelineMemberItem), + RoomMessageEventContent => typeof(TimelineMessageItem), + RoomCreateEventContent => typeof(TimelineRoomCreateItem), + RoomNameEventContent => typeof(TimelineRoomNameItem), + _ => typeof(TimelineUnknownItem) + }; + +} |