diff options
Diffstat (limited to 'MatrixUtils.Web/Shared')
26 files changed, 1341 insertions, 0 deletions
diff --git a/MatrixUtils.Web/Shared/EditablePre.razor b/MatrixUtils.Web/Shared/EditablePre.razor new file mode 100644 index 0000000..acb477c --- /dev/null +++ b/MatrixUtils.Web/Shared/EditablePre.razor @@ -0,0 +1,19 @@ +@inherits InputBase<string> +<pre id="@Id" class="@CssClass" @onkeyup="Callback" contenteditable="true">@CurrentValue</pre> + +@code { + + protected override bool TryParseValueFromString(string? value, out string result, out string? validationErrorMessage) { + result = value; + validationErrorMessage = null; + return true; + } + + public object Id { get; set; } + + private Task Callback() { + Console.WriteLine("beep"); + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/InlineUserItem.razor b/MatrixUtils.Web/Shared/InlineUserItem.razor new file mode 100644 index 0000000..6df2307 --- /dev/null +++ b/MatrixUtils.Web/Shared/InlineUserItem.razor @@ -0,0 +1,71 @@ +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Helpers +@using LibMatrix.Homeservers +@using LibMatrix.Responses +<div style="background-color: #ffffff11; border-radius: 0.5em; height: 1em; display: inline-block; vertical-align: middle;" alt="@UserId"> + <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "vertical-align: top;") width: 1em; height: 1em; border-radius: 50%;" src="@ProfileAvatar"/> + <span style="position: relative; top: -5px;">@ProfileName</span> + + <div style="display: inline-block;"> + @if (ChildContent is not null) { + @ChildContent + } + </div> + +</div> + +@code { + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public UserProfileResponse? User { get; set; } + + [Parameter] + public RoomMemberEventContent? MemberEvent { get; set; } + + [Parameter] + public string? UserId { get; set; } + + [Parameter] + public string? ProfileAvatar { get; set; } = null; + + [Parameter] + public string? ProfileName { get; set; } = null; + + [Parameter] + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + private static SemaphoreSlim _semaphoreSlim = new(128); + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); + if(Homeserver is null) return; + + await _semaphoreSlim.WaitAsync(); + + if (User == null && UserId == null && MemberEvent != null) + throw new ArgumentNullException(nameof(UserId)); + + if (MemberEvent != null) { + User = new UserProfileResponse { + AvatarUrl = MemberEvent.AvatarUrl, + DisplayName = MemberEvent.DisplayName + }; + } + + if (User is null && UserId is not null) { + User ??= await Homeserver.GetProfileAsync(UserId); + } + + + ProfileAvatar ??= Homeserver.ResolveMediaUri(User.AvatarUrl); + ProfileName ??= User.DisplayName; + + _semaphoreSlim.Release(); + } + +} diff --git a/MatrixUtils.Web/Shared/LogView.razor b/MatrixUtils.Web/Shared/LogView.razor new file mode 100644 index 0000000..d541b82 --- /dev/null +++ b/MatrixUtils.Web/Shared/LogView.razor @@ -0,0 +1,41 @@ +@* @using System.Text *@ +@* @if (LocalStorageWrapper.Settings.DeveloperSettings.EnableLogViewers) { *@ +@* <u>Logs</u> *@ +@* <br/> *@ +@* <pre> *@ +@* @_stringBuilder *@ +@* </pre> *@ +@* } *@ +@* *@ +@* @code { *@ +@* StringBuilder _stringBuilder = new(); *@ +@* *@ +@* protected override async Task OnInitializedAsync() { *@ +@* if (!LocalStorageWrapper.Settings.DeveloperSettings.EnableConsoleLogging) { *@ +@* Console.WriteLine("Console logging disabled!"); *@ +@* var _sw = new StringWriter(); *@ +@* Console.SetOut(_sw); *@ +@* Console.SetError(_sw); *@ +@* return; *@ +@* } *@ +@* if (!LocalStorageWrapper.Settings.DeveloperSettings.EnableLogViewers) return; *@ +@* //intecept stdout with textwriter to get logs *@ +@* var sw = new StringWriter(_stringBuilder); *@ +@* Console.SetOut(sw); *@ +@* Console.SetError(sw); *@ +@* //keep updated *@ +@* var length = 0; *@ +@* Task.Run(async () => { *@ +@* while (true) { *@ +@* await Task.Delay(100); *@ +@* if (_stringBuilder.Length != length) { *@ +@* StateHasChanged(); *@ +@* length = _stringBuilder.Length; *@ +@* } *@ +@* } *@ +@* // ReSharper disable once FunctionNeverReturns - This is intentional behavior *@ +@* }); *@ +@* await base.OnInitializedAsync(); *@ +@* } *@ +@* *@ +@* } *@ \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MainLayout.razor b/MatrixUtils.Web/Shared/MainLayout.razor new file mode 100644 index 0000000..92194b2 --- /dev/null +++ b/MatrixUtils.Web/Shared/MainLayout.razor @@ -0,0 +1,19 @@ +@inherits LayoutComponentBase + +<div class="page"> + <div class="sidebar"> + <NavMenu/> + </div> + + <main> + <div class="top-row px-4"> + <PortableDevTools></PortableDevTools> + <a href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/" target="_blank">Git</a> + <a href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support" target="_blank">Matrix</a> + </div> + + <article class="Content px-4"> + @Body + </article> + </main> +</div> \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MainLayout.razor.css b/MatrixUtils.Web/Shared/MainLayout.razor.css new file mode 100644 index 0000000..01a5066 --- /dev/null +++ b/MatrixUtils.Web/Shared/MainLayout.razor.css @@ -0,0 +1,81 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + +.top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; +} + +.top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; +} + +.top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/MatrixUtils.Web/Shared/MxcImage.razor b/MatrixUtils.Web/Shared/MxcImage.razor new file mode 100644 index 0000000..fb8c248 --- /dev/null +++ b/MatrixUtils.Web/Shared/MxcImage.razor @@ -0,0 +1,53 @@ +<img class="@Class" src="@ResolvedUri" style="@Style"/> +@code { + private string _mxcUri; + private string _style; + private string _resolvedUri; + + [Parameter] + public string MxcUri { + get => _mxcUri ?? ""; + set { + Console.WriteLine($"New MXC uri: {value}"); + _mxcUri = value; + UriHasChanged(value); + } + } + + //mxcuri binding + + + [Parameter] + public string Style { + get => _style; + set { + _style = value; + StateHasChanged(); + } + } + [Parameter] + public RemoteHomeserver? Homeserver { get; set; } + + private string ResolvedUri { + get => _resolvedUri; + set { + _resolvedUri = value; + StateHasChanged(); + } + } + + private async Task UriHasChanged(string value) { + var uri = value[5..].Split('/'); + Console.WriteLine($"UriHasChanged: {value} {uri[0]}"); + if (Homeserver is null) { + Console.WriteLine($"Homeserver is null, creating new remotehomeserver for {uri[0]}"); + Homeserver = await hsProvider.GetRemoteHomeserver(uri[0]); + } + ResolvedUri = Homeserver.ResolveMediaUri(value); + Console.WriteLine($"ResolvedUri: {ResolvedUri}"); + } + + [Parameter] + public string Class { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/NavMenu.razor b/MatrixUtils.Web/Shared/NavMenu.razor new file mode 100644 index 0000000..43e2237 --- /dev/null +++ b/MatrixUtils.Web/Shared/NavMenu.razor @@ -0,0 +1,96 @@ +<div class="top-row ps-3 navbar navbar-dark"> + <div class="container-fluid"> + <a class="navbar-brand" href="">Rory&::MatrixUtils</a> + <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> + <span class="navbar-toggler-icon"></span> + </button> + </div> +</div> + +<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu"> + <nav class="flex-column"> + <div class="nav-item px-3"> + <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> + <span class="oi oi-home" aria-hidden="true"></span> Home + </NavLink> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="About"> + <span class="oi oi-info" aria-hidden="true"></span> About RMU + </NavLink> + </div> + + <!-- Main tools --> + + <div class="nav-item px-3"> + <h5 style="margin-left: 1em;">Main tools</h5> + <hr style="margin-bottom: 0em;"/> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="Rooms"> + <span class="oi oi-plus" aria-hidden="true"></span> Room list + </NavLink> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="User/Profile"> + <span class="oi oi-plus" aria-hidden="true"></span> Manage profile + </NavLink> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="User/DirectMessages"> + <span class="oi oi-plus" aria-hidden="true"></span> Manage DMs + </NavLink> + </div> + + <!-- Extra tools --> + + <div class="nav-item px-3"> + <h5 style="margin-left: 1em;">Extra tools</h5> + <hr style="margin-bottom: 0em;"/> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="HSAdmin"> + <span class="oi oi-plus" aria-hidden="true"></span> Homeserver admin + </NavLink> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="Tools"> + <span class="oi oi-plus" aria-hidden="true"></span> Other tools + </NavLink> + </div> + + <!-- RMU --> + + <div class="nav-item px-3"> + <h5 style="margin-left: 1em;">RMU</h5> + <hr style="margin-bottom: 0em;"/> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="Dev/Options"> + <span class="oi oi-plus" aria-hidden="true"></span> Developer options + </NavLink> + </div> + + <div class="nav-item px-3"> + <NavLink class="nav-link" href="Dev/Utilities"> + <span class="oi oi-plus" aria-hidden="true"></span> Developer utilities + </NavLink> + </div> + </nav> +</div> + +@code { + private bool collapseNavMenu = true; + + private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() => collapseNavMenu = !collapseNavMenu; + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/NavMenu.razor.css b/MatrixUtils.Web/Shared/NavMenu.razor.css new file mode 100644 index 0000000..447f2df --- /dev/null +++ b/MatrixUtils.Web/Shared/NavMenu.razor.css @@ -0,0 +1,68 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0, 0, 0, 0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + +.nav-item:first-of-type { + padding-top: 1rem; +} + +.nav-item:last-of-type { + padding-bottom: 1rem; +} + +.nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; +} + +.nav-item ::deep a.active { + background-color: rgba(255, 255, 255, 0.25); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255, 255, 255, 0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } + + .nav-scrollable { + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor new file mode 100644 index 0000000..4fd151d --- /dev/null +++ b/MatrixUtils.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/MatrixUtils.Web/Shared/RoomList.razor b/MatrixUtils.Web/Shared/RoomList.razor new file mode 100644 index 0000000..ed443dd --- /dev/null +++ b/MatrixUtils.Web/Shared/RoomList.razor @@ -0,0 +1,97 @@ +@using MatrixUtils.Web.Shared.RoomListComponents; +@using LibMatrix +@using LibMatrix.Extensions +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using System.Collections.ObjectModel +@using LibMatrix.Responses +@using MatrixUtils.Abstractions +@using _Imports = MatrixUtils.Web._Imports +@if (!StillFetching) { + <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> + } +} +else { + @foreach (var category in RoomsWithTypes.OrderBy(x => x.Value.Count)) { + <RoomListCategory Category="@category" GlobalProfile="@GlobalProfile"></RoomListCategory> + } +} + +@code { + + [Parameter] + public ObservableCollection<RoomInfo> Rooms { get; set; } + + [Parameter] + public UserProfileResponse? GlobalProfile { get; set; } + + [Parameter] + public bool StillFetching { get; set; } = true; + + [Parameter] + public EventCallback<bool> StillFetchingChanged { get; set; } + + private Dictionary<string, List<RoomInfo>> RoomsWithTypes => Rooms is null ? new() : Rooms.GroupBy(x => GetRoomTypeName(x.CreationEventContent?.Type)).ToDictionary(x => x.Key, x => x.ToList()); + + private bool hooked; + protected override async Task OnParametersSetAsync() { + var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + if (!hooked) { + Rooms.CollectionChanged += (_, args) => { + foreach (RoomInfo item in args.NewItems) { + item.PropertyChanged += (_, args2) => { + // Console.WriteLine(args2); + + if (args2.PropertyName == nameof(item.CreationEventContent)) + StateHasChanged(); + }; + } + }; + hooked = true; + } + + // GlobalProfile ??= await hs.GetProfileAsync(hs.WhoAmI.UserId); + + await base.OnParametersSetAsync(); + } + + private string GetRoomTypeName(string? roomType) => roomType switch { + null => "Room", + "m.space" => "Space", + "msc3588.stories.stories-room" => "Story room", + "support.feline.policy.lists.msc.v1" => "MSC3784 Policy list (v1)", + _ => roomType + }; + + // private static SemaphoreSlim _semaphoreSlim = new(8, 8); + + // private async Task ProcessRoom(RoomInfo room) { + // await _semaphoreSlim.WaitAsync(); + // string roomType; + // try { + // var createEvent = (await room.GetStateEvent("m.room.create")).TypedContent as RoomCreateEventContent; + // roomType = GetRoomTypeName(createEvent.Type); + // + // if (roomType == "Room") { + // var mjolnirData = await room.GetStateEvent("org.matrix.mjolnir.shortcode"); + // if (mjolnirData?.RawContent?.ToJson(ignoreNull: true) is not null and not "{}") + // roomType = "Legacy policy room"; + // } + // } + // catch (MatrixException e) { + // roomType = $"Error: {e.ErrorCode}"; + // } + // + // // if (!RoomsWithTypes.ContainsKey(roomType)) { + // // RoomsWithTypes.Add(roomType, new List<RoomInfo>()); + // // } + // // RoomsWithTypes[roomType].Add(room); + // + // StateHasChanged(); + // _semaphoreSlim.Release(); + // } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/RoomList.razor.css b/MatrixUtils.Web/Shared/RoomList.razor.css new file mode 100644 index 0000000..a159305 --- /dev/null +++ b/MatrixUtils.Web/Shared/RoomList.razor.css @@ -0,0 +1,8 @@ +.room-list-item { + background-color: #ffffff11; + border-radius: 0.5em; + display: block; + margin-top: 4px; + padding: 4px; + width: fit-content; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor new file mode 100644 index 0000000..3d0070f --- /dev/null +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor @@ -0,0 +1,63 @@ +@using MatrixUtils.Web.Classes.Constants +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Homeservers +@using LibMatrix.Responses +@using MatrixUtils.Abstractions +<details> + <summary>@RoomType (@Rooms.Count)</summary> + @foreach (var room in Rooms) { + <div class="room-list-item"> + <RoomListItem RoomInfo="@room" ShowOwnProfile="@(RoomType == "Room")"></RoomListItem> + @* @if (RoomVersionDangerLevel(room) != 0 && *@ + @* (room.StateEvents.FirstOrDefault(x=>x.Type == "m.room.power_levels")?.TypedContent is RoomPowerLevelEventContent powerLevels && powerLevels.UserHasPermission(Homeserver.UserId, "m.room.tombstone"))) { *@ + @* <MatrixUtils.Web.Shared.SimpleComponents.LinkButton Color="@(RoomVersionDangerLevel(room) == 2 ? "#ff0000" : "#ff8800")" href="@($"/Rooms/Create?Import={room.Room.RoomId}")">Upgrade room</MatrixUtils.Web.Shared.SimpleComponents.LinkButton> *@ + @* } *@ + <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Timeline")">View timeline</LinkButton> + <LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/View")">View state</LinkButton> + <LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/Edit")">Edit state</LinkButton> + + @if (room.CreationEventContent?.Type == "m.space") { + <RoomListSpace Space="@room"></RoomListSpace> + } + else if (room.CreationEventContent?.Type == "support.feline.policy.lists.msc.v1" || RoomType == "org.matrix.mjolnir.policy") { + <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")">Manage policies</LinkButton> + } + </div> + } +</details> +<br/> + +@code { + + [Parameter] + public KeyValuePair<string, List<RoomInfo>> Category { get; set; } + + [Parameter] + public UserProfileResponse? GlobalProfile { get; set; } + + [CascadingParameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + + private string RoomType => Category.Key; + private List<RoomInfo> Rooms => Category.Value; + + private int RoomVersionDangerLevel(RoomInfo room) { + var creationEvent = room.StateEvents.FirstOrDefault(x => x?.Type == "m.room.create"); + if (creationEvent is null) return 0; + return creationEvent.TypedContent is not RoomCreateEventContent roomVersionContent ? 0 + : RoomConstants.DangerousRoomVersions.Contains(roomVersionContent.RoomVersion) ? 2 + : roomVersionContent.RoomVersion != RoomConstants.RecommendedRoomVersion ? 1 : 0; + } + + public static string GetRoomTypeName(string roomType) { + return roomType switch { + null => "Room", + "m.space" => "Space", + "org.matrix.mjolnir.policy" => "Policy room", + + _ => roomType + }; + } + +} diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListPolicyRoom.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListPolicyRoom.razor new file mode 100644 index 0000000..7afdc8c --- /dev/null +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListPolicyRoom.razor @@ -0,0 +1,13 @@ +@using LibMatrix.RoomTypes +<LinkButton href="@($"/Rooms/{Room.RoomId}/Policies")">Manage policies</LinkButton> + +@code { + + [Parameter] + public GenericRoom Room { get; set; } + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + } + +} diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor new file mode 100644 index 0000000..895d642 --- /dev/null +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor @@ -0,0 +1,60 @@ +@using System.Collections.ObjectModel +@using MatrixUtils.Abstractions +<MatrixUtils.Web.Shared.SimpleComponents.LinkButton href="@($"/Rooms/{Space.Room.RoomId}/Space")">Manage space</MatrixUtils.Web.Shared.SimpleComponents.LinkButton> + +<br/> +<details @ontoggle="SpaceChildrenOpened"> + <summary>@Children.Count children</summary> + @if (_shouldRenderChildren) { + <p>Breadcrumb: @Breadcrumbs</p> + <div style="margin-left: 8px;"> + <RoomList Rooms="Children"></RoomList> + </div> + } +</details> + +@code { + + [Parameter] + public RoomInfo Space { get; set; } + + [Parameter, CascadingParameter] + public List<RoomInfo> KnownRooms { get; set; } = new(); + + [Parameter, CascadingParameter] + public string? Breadcrumbs { + get => _breadcrumbs + Space.Room.RoomId; + set => _breadcrumbs = value; + } + + private ObservableCollection<RoomInfo> Children { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + if (Breadcrumbs == null) throw new ArgumentNullException(nameof(Breadcrumbs)); + await Task.Delay(Random.Shared.Next(1000, 10000)); + var rooms = Space.Room.AsSpace.GetChildrenAsync(); + await foreach (var room in rooms) { + if (Breadcrumbs.Contains(room.RoomId)) continue; + var roomInfo = KnownRooms.FirstOrDefault(x => x.Room.RoomId == room.RoomId); + if (roomInfo is null) { + roomInfo = new RoomInfo() { + Room = room + }; + KnownRooms.Add(roomInfo); + } + Children.Add(roomInfo); + } + await base.OnInitializedAsync(); + } + + private bool _shouldRenderChildren = false; + private string? _breadcrumbs; + + private Task SpaceChildrenOpened() { + if (_shouldRenderChildren) return Task.CompletedTask; + _shouldRenderChildren = true; + Console.WriteLine($"[RoomList] Rendering children of {Space.Room.RoomId}"); + return Task.CompletedTask; + } + +} diff --git a/MatrixUtils.Web/Shared/RoomListItem.razor b/MatrixUtils.Web/Shared/RoomListItem.razor new file mode 100644 index 0000000..1046dd1 --- /dev/null +++ b/MatrixUtils.Web/Shared/RoomListItem.razor @@ -0,0 +1,196 @@ +@using System.Text.Json +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Helpers +@using LibMatrix.Homeservers +@using LibMatrix.Responses +@using LibMatrix.RoomTypes +@using MatrixUtils.Abstractions +@using MatrixUtils.Web.Classes.Constants +@if (RoomInfo is not null) { + <div class="roomListItem @(HasDangerousRoomVersion ? "dangerousRoomVersion" : HasOldRoomVersion ? "oldRoomVersion" : "")" id="@RoomInfo.Room.RoomId"> + @if (OwnMemberState != null) { + <MxcImage Class="@("avatar32" + (OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? " highlightChange" : "") + (ChildContent is not null ? " vcenter" : ""))" + MxcUri="@(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl)"/> + <span class="centerVertical border75 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "")"> + @(OwnMemberState?.DisplayName ?? GlobalProfile?.DisplayName ?? "Loading...") + </span> + <span class="centerVertical noLeftPadding">-></span> + } + <MxcImage Class="avatar32" MxcUri="@RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/> + <div class="inlineBlock"> + <span class="centerVertical">@RoomInfo.RoomName</span> + @if (ChildContent is not null) { + @ChildContent + } + </div> + + </div> +} +else { + <p>Warning: RoomInfo is null!</p> +} + +@code { + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public RoomInfo? RoomInfo { + get => _roomInfo; + set { + _roomInfo = value; + OnParametersSetAsync(); + } + } + + [Parameter] + public bool ShowOwnProfile { get; set; } = false; + + [Parameter] + public RoomMemberEventContent? OwnMemberState { get; set; } + + [CascadingParameter] + public UserProfileResponse? GlobalProfile { get; set; } + + [Parameter] + public bool LoadData { + get => _loadData; + set { + _loadData = value; + OnParametersSetAsync(); + } + } + + private bool HasOldRoomVersion { get; set; } = false; + private bool HasDangerousRoomVersion { get; set; } = false; + + private static SemaphoreSlim _semaphoreSlim = new(8); + private RoomInfo? _roomInfo; + private bool _loadData = false; + private static AuthenticatedHomeserverGeneric? hs { get; set; } + + private bool _hooked; + protected override async Task OnParametersSetAsync() { + if (RoomInfo != null) { + if (!_hooked) { + _hooked = true; + RoomInfo.PropertyChanged += (_, a) => { + Console.WriteLine(a.PropertyName); + StateHasChanged(); + }; + } + + if (LoadData) { + try { + await RoomInfo.GetStateEvent("m.room.create"); + if (ShowOwnProfile) + OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.WhoAmI.UserId)).TypedContent as RoomMemberEventContent; + + await RoomInfo.GetStateEvent("m.room.name"); + await RoomInfo.GetStateEvent("m.room.avatar"); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_FORBIDDEN") { + LoadData = false; + RoomInfo.StateEvents.Add(new() { + Type = "m.room.create", + TypedContent = new RoomCreateEventContent() { RoomVersion = "0" }, + RoomId = null, Sender = null, EventId = null //TODO: implement + }); + RoomInfo.StateEvents.Add(new() { + Type = "m.room.name", + TypedContent = new RoomNameEventContent() { + Name = "M_FORBIDDEN: Are you a member of this room? " + RoomInfo.Room.RoomId + }, + RoomId = null, Sender = null, EventId = null //TODO: implement + }); + } + } + } + } + + await base.OnParametersSetAsync(); + } + + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + + await _semaphoreSlim.WaitAsync(); + + hs ??= await RMUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + + try { + await CheckRoomVersion(); + // await GetRoomInfo(); + // await LoadOwnProfile(); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_FORBIDDEN" }) { + throw; + } + // RoomName = "Error: " + e.Message; + // RoomIcon = "/blobfox_outage.gif"; + } + catch (Exception e) { + Console.WriteLine($"Failed to load room info for {RoomInfo.Room.RoomId}: {e.Message}"); + } + _semaphoreSlim.Release(); + } + + private async Task LoadOwnProfile() { + if (!ShowOwnProfile) return; + try { + // OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.UserId)).TypedContent as RoomMemberEventContent; + GlobalProfile ??= await hs.GetProfileAsync(hs.UserId); + } + catch (MatrixException e) { + if (e is { ErrorCode: "M_FORBIDDEN" }) { + Console.WriteLine($"Failed to get profile for {hs.UserId}: {e.Message}"); + ShowOwnProfile = false; + } + else { + throw; + } + } + } + + private async Task CheckRoomVersion() { + while (RoomInfo?.CreationEventContent is null) { + Console.WriteLine($"Room creation event content for {RoomInfo.Room.RoomId} is null..."); + await Task.Delay(Random.Shared.Next(1000, 2500)); + } + var ce = RoomInfo.CreationEventContent; + if (int.TryParse(ce.RoomVersion, out var rv)) { + if (rv < 10) + HasOldRoomVersion = true; + } + else // treat unstable room versions as dangerous + HasDangerousRoomVersion = true; + + if (RoomConstants.DangerousRoomVersions.Contains(ce.RoomVersion)) { + HasDangerousRoomVersion = true; + // RoomName = "Dangerous room: " + RoomName; + } + } + + // private async Task GetRoomInfo() { + // try { + // RoomName ??= ((await RoomInfo.GetStateEvent("m.room.name"))?.TypedContent as RoomNameEventContent)?.Name ?? RoomId; + // + // var state = (await RoomInfo.GetStateEvent("m.room.avatar")).TypedContent as RoomAvatarEventContent; + // if (state?.Url is { } url) { + // RoomIcon = await hsResolver.ResolveMediaUri(hs.ServerName, url); + // // Console.WriteLine($"Got avatar for room {RoomId}: {roomIcon} ({url})"); + // } + // } + // catch (MatrixException e) { + // if (e is not { ErrorCode: "M_FORBIDDEN" }) { + // throw; + // } + // } + // } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/RoomListItem.razor.css b/MatrixUtils.Web/Shared/RoomListItem.razor.css new file mode 100644 index 0000000..13de656 --- /dev/null +++ b/MatrixUtils.Web/Shared/RoomListItem.razor.css @@ -0,0 +1,48 @@ +.roomListItem { + background-color: #ffffff11; + border-radius: 25px; + margin: 8px; + width: fit-Content; +} + +.roomListItem.dangerousRoomVersion { + border: red 4px solid; +} + +.roomListItem.oldRoomVersion { + border: #FF0 1px solid; +} + +.avatar32 { + width: 32px; + height: 32px; + border-radius: 50%; +} + +.avatar32.vcenter { + vertical-align: baseline; +} + +.highlightChange { + background-color: red; + border-color: red; + border-width: 3px; + border-style: dashed; +} + +.inlineBlock { + display: inline-block; +} + +.centerVertical { + vertical-align: middle; + padding-right: 8px; +} + +.noLeftPadding { + padding-left: 0px; +} + +.border75 { + border-radius: 75px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor new file mode 100644 index 0000000..8d608e3 --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor @@ -0,0 +1,33 @@ +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Homeservers +@using LibMatrix.Responses +<h3>BaseTimelineItem</h3> + +@code { + + [Parameter] + public StateEventResponse Event { get; set; } + + [Parameter] + public List<StateEventResponse> Events { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + public List<StateEventResponse> EventsBefore => Events.TakeWhile(e => e.EventId != Event.EventId).ToList(); + + public List<StateEventResponse> MatchingEventsBefore => EventsBefore.Where(x => x.Type == Event.Type && x.StateKey == Event.StateKey).ToList(); + + public StateEventResponse? PreviousState => MatchingEventsBefore.LastOrDefault(); + + public RoomMemberEventContent? CurrentSenderMemberEventContent => EventsBefore.LastOrDefault(x => x.Type == "m.room.member" && x.StateKey == Event.Sender)? + .TypedContent as RoomMemberEventContent; + + public UserProfileResponse CurrentSenderProfile => new() { DisplayName = CurrentSenderMemberEventContent?.DisplayName, AvatarUrl = CurrentSenderMemberEventContent?.AvatarUrl }; + + public bool HasPreviousMessage => EventsBefore.Last() is { Type: "m.room.message" } response && response.Sender == Event.Sender; + + + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor new file mode 100644 index 0000000..1213432 --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor @@ -0,0 +1,27 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@inherits BaseTimelineItem + +@if (currentEventContent is not null) { + @if (previousEventContent is null) { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room alias to "@currentEventContent.Alias"</i> + } + else { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room name from "@previousEventContent.Alias" to "@currentEventContent.Alias"</i> + } +} +else { + <details> + <summary>Unknown event @Event.Type (@Event.StateKey)</summary> + <pre> + @Event.ToJson() + </pre> + </details> +} + +@code { + private RoomCanonicalAliasEventContent? previousEventContent => PreviousState?.TypedContent as RoomCanonicalAliasEventContent; + + private RoomCanonicalAliasEventContent? currentEventContent => Event.TypedContent as RoomCanonicalAliasEventContent; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor new file mode 100644 index 0000000..172a38c --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor @@ -0,0 +1,27 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@inherits BaseTimelineItem + +@if (currentEventContent is not null) { + @if (previousEventContent is null) { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the history visibility to "@currentEventContent.HistoryVisibility"</i> + } + else { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the history visibility from "@previousEventContent.HistoryVisibility" to "@currentEventContent.HistoryVisibility"</i> + } +} +else { + <details> + <summary>Unknown event @Event.Type (@Event.StateKey)</summary> + <pre> + @Event.ToJson() + </pre> + </details> +} + +@code { + private RoomHistoryVisibilityEventContent? previousEventContent => PreviousState?.TypedContent as RoomHistoryVisibilityEventContent; + + private RoomHistoryVisibilityEventContent? currentEventContent => Event.TypedContent as RoomHistoryVisibilityEventContent; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor new file mode 100644 index 0000000..3b18b95 --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor @@ -0,0 +1,53 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@inherits BaseTimelineItem + +@if (roomMemberData is not null) { + @switch (roomMemberData.Membership) { + case "ban": + <i>@Event.StateKey was banned</i> + break; + case "invite": + <i>@Event.StateKey was invited</i> + break; + case "join" when Event.ReplacesState is not null: + <i>@Event.StateKey changed their display name to @(roomMemberData.DisplayName ?? Event.Sender)</i> + break; + case "join": + @if (prevRoomMemberData is null) { + <i><InlineUserItem User="@(new UserProfileResponse() { DisplayName = roomMemberData.DisplayName, AvatarUrl = roomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> joined</i> + } + else { + <i><InlineUserItem User="@(new UserProfileResponse() { DisplayName = prevRoomMemberData.DisplayName, AvatarUrl = prevRoomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed their profile to <InlineUserItem User="@(new UserProfileResponse() { DisplayName = roomMemberData.DisplayName, AvatarUrl = roomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem></i> + } + break; + case "leave": + <i>@Event.StateKey left</i> + break; + case "knock": + <i>@Event.StateKey knocked</i> + break; + default: + <i>@Event.StateKey has an unknown state:</i> + <pre> + @Event.ToJson() + </pre> + break; + } +} +else { + <details> + <summary>Unknown membership event for @Event.StateKey</summary> + <pre> + @Event.ToJson() + </pre> + </details> +} + +@code { + + private RoomMemberEventContent? roomMemberData => Event.TypedContent as RoomMemberEventContent; + private RoomMemberEventContent? prevRoomMemberData => PreviousState?.TypedContent as RoomMemberEventContent; + +} diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor new file mode 100644 index 0000000..81956b0 --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor @@ -0,0 +1,34 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec +@inherits BaseTimelineItem + +<span> + @if (!HasPreviousMessage) { + <span><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem>:</span><br/> + } + @switch (currentEventContent.MessageType) { + case "m.text": { + @foreach (var line in currentEventContent.Body.Split('\n')) { + <span>@line</span><br/> + } + break; + } + case "m.image": { + <i>@currentEventContent.Body</i><br/> + <img src="@Homeserver.ResolveMediaUri(currentEventContent.Url)"> + break; + } + default: { + <pre> + @Event.RawContent?.ToJson(indent: false) + </pre> + break; + } + } +</span> + +@code { + private RoomMessageEventContent? previousEventContent => PreviousState?.TypedContent as RoomMessageEventContent; + + private RoomMessageEventContent? currentEventContent => Event.TypedContent as RoomMessageEventContent; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor new file mode 100644 index 0000000..f3e6c7e --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor @@ -0,0 +1,18 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@inherits BaseTimelineItem + +<i> + @Event.Sender created the room with room version @CreationEventContent.RoomVersion + @(CreationEventContent.Federate ?? true ? "and" : "without") federating with other servers.<br/> + This room is of type @(CreationEventContent.Type ?? "Untyped room (usually a chat room)") +</i> +<pre> + @Event.RawContent?.ToJson(indent: false) +</pre> + +@code { + + private RoomCreateEventContent CreationEventContent => Event.TypedContent as RoomCreateEventContent; + +} diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor new file mode 100644 index 0000000..eeec3de --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor @@ -0,0 +1,27 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@inherits BaseTimelineItem + +@if (currentEventContent is not null) { + @if (previousEventContent is null) { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room name to "@currentEventContent.Name"</i> + } + else { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room name from "@previousEventContent.Name" to "@currentEventContent.Name"</i> + } +} +else { + <details> + <summary>Unknown event @Event.Type (@Event.StateKey)</summary> + <pre> + @Event.ToJson() + </pre> + </details> +} + +@code { + private RoomNameEventContent? previousEventContent => PreviousState?.TypedContent as RoomNameEventContent; + + private RoomNameEventContent? currentEventContent => Event.TypedContent as RoomNameEventContent; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor new file mode 100644 index 0000000..7ef17a8 --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor @@ -0,0 +1,37 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@inherits BaseTimelineItem + +@if (currentEventContent is not null) { + @if (previousEventContent is null) { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room topic to</i><br/> + <pre> + @currentEventContent.Topic + </pre> + } + else { + <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room topic from</i><br/> + <pre> + @previousEventContent.Topic + </pre><br/> + <i>to</i><br/> + <pre> + @currentEventContent.Topic + </pre> + } +} +else { + <details> + <summary>Unknown event @Event.Type (@Event.StateKey)</summary> + <pre> + @Event.ToJson() + </pre> + </details> +} + +@code { + private RoomTopicEventContent? previousEventContent => PreviousState?.TypedContent as RoomTopicEventContent; + + private RoomTopicEventContent? currentEventContent => Event.TypedContent as RoomTopicEventContent; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor new file mode 100644 index 0000000..4f05b30 --- /dev/null +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor @@ -0,0 +1,16 @@ +@using ArcaneLibs.Extensions +@inherits BaseTimelineItem + +<div> + <details style="display: inline;"> + <summary> + <i style="color: red;">Unknown event type: <pre style="display: inline;">@Event.Type</pre></i> + </summary> + <pre>@Event.ToJson(ignoreNull: true)</pre> + </details> +</div> + +@code { + + +} diff --git a/MatrixUtils.Web/Shared/UserListItem.razor b/MatrixUtils.Web/Shared/UserListItem.razor new file mode 100644 index 0000000..525296e --- /dev/null +++ b/MatrixUtils.Web/Shared/UserListItem.razor @@ -0,0 +1,44 @@ +@using LibMatrix.Helpers +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Homeservers +@using LibMatrix.Responses +<div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content;"> + <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@(string.IsNullOrWhiteSpace(User?.AvatarUrl) ? "https://api.dicebear.com/6.x/identicon/svg?seed=" + UserId : User.AvatarUrl)"/> + <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@User?.DisplayName</span> + + <div style="display: inline-block;"> + @if (ChildContent is not null) { + @ChildContent + } + </div> + +</div> + +@code { + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public UserProfileResponse? User { get; set; } + + [Parameter] + public string UserId { get; set; } + + private AuthenticatedHomeserverGeneric _homeserver = null!; + + protected override async Task OnInitializedAsync() { + _homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + if (_homeserver is null) return; + + if (User == null) { + if (UserId == null) { + throw new ArgumentNullException(nameof(UserId)); + } + User = await _homeserver.GetProfileAsync(UserId); + } + + await base.OnInitializedAsync(); + } + +} \ No newline at end of file |