diff options
Diffstat (limited to 'MatrixUtils.Web')
29 files changed, 1239 insertions, 295 deletions
diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj index 515b235..dfb4713 100644 --- a/MatrixUtils.Web/MatrixUtils.Web.csproj +++ b/MatrixUtils.Web/MatrixUtils.Web.csproj @@ -11,6 +11,7 @@ <UseBlazorWebAssembly>true</UseBlazorWebAssembly> <BlazorEnableCompression>false</BlazorEnableCompression> <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> + <BlazorCacheBootResources>false</BlazorCacheBootResources> <!-- <RunAOTCompilation>true</RunAOTCompilation>--> </PropertyGroup> diff --git a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor index 611d4c1..87416a2 100644 --- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor +++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor @@ -12,9 +12,9 @@ else { <details> <summary>Room List</summary> - @foreach (var room in Rooms) { - <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@room.Replace('.', '~')"> - <RoomListItem RoomInfo="@(new RoomInfo() { Room = hs.GetRoom(room) })" LoadData="true"></RoomListItem> + @foreach (var roomId in Rooms) { + <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@roomId.Replace('.', '~')"> + <RoomListItem RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem> </a> } </details> diff --git a/MatrixUtils.Web/Pages/HSEInit.razor b/MatrixUtils.Web/Pages/HSEInit.razor index 3020ff7..b2fc0db 100644 --- a/MatrixUtils.Web/Pages/HSEInit.razor +++ b/MatrixUtils.Web/Pages/HSEInit.razor @@ -6,7 +6,7 @@ @code { protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - var tasks = Enumerable.Range(0, 5000).Select(i => Login()).ToList(); + var tasks = Enumerable.Range(0, 50).Select(i => Login()).ToList(); await Task.WhenAll(tasks); Console.WriteLine("All logins complete!"); var userAuths = tasks.Select(t => t.Result).Where(t => t != null).ToList(); diff --git a/MatrixUtils.Web/Pages/Index.razor b/MatrixUtils.Web/Pages/Index.razor index 0c0c87a..f216488 100644 --- a/MatrixUtils.Web/Pages/Index.razor +++ b/MatrixUtils.Web/Pages/Index.razor @@ -3,6 +3,8 @@ @using LibMatrix.Responses @using LibMatrix @using ArcaneLibs.Extensions +@using ArcaneLibs +@using System.Diagnostics <PageTitle>Index</PageTitle> @@ -12,7 +14,10 @@ Small collection of tools to do not-so-everyday things. <br/><br/> <h5>@totalSessions signed in sessions - <a href="/Login">Add new account</a></h5> @if (scannedSessions != totalSessions) { - <progress max="@totalSessions" value="@scannedSessions"></progress> + <span> + <span>@scannedSessions/@totalSessions</span> + <progress max="@totalSessions" value="@scannedSessions"></progress> + </span> } <hr/> <form> @@ -103,6 +108,7 @@ Small collection of tools to do not-so-everyday things. private readonly List<UserAuth> _offlineSessions = []; private LoginResponse? _currentSession; int scannedSessions = 0, totalSessions = 1; + private SvgIdenticonGenerator _identiconGenerator = new(); protected override async Task OnInitializedAsync() { Console.WriteLine("Index.OnInitializedAsync"); @@ -124,6 +130,7 @@ Small collection of tools to do not-so-everyday things. List<string> offlineServers = []; var sema = new SemaphoreSlim(64, 64); + var updateSw = Stopwatch.StartNew(); var tasks = tokens.Select(async token => { await sema.WaitAsync(); scannedSessions++; @@ -141,7 +148,7 @@ Small collection of tools to do not-so-everyday things. var serverVersionTask = hs.FederationClient?.GetServerVersionAsync(); _sessions.Add(new() { UserInfo = new() { - AvatarUrl = "/blobfox_outage.gif", + AvatarUrl = string.IsNullOrWhiteSpace((await profileTask).AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(hs.WhoAmI.UserId) : hs.ResolveMediaUri((await profileTask).AvatarUrl), RoomCount = (await joinedRoomsTask).Count, DisplayName = (await profileTask).DisplayName ?? hs.WhoAmI.UserId }, @@ -149,6 +156,10 @@ Small collection of tools to do not-so-everyday things. ServerVersion = await (serverVersionTask ?? Task.FromResult<ServerVersionResponse?>(null)!), Homeserver = hs }); + if (updateSw.ElapsedMilliseconds > 250) { + updateSw.Restart(); + StateHasChanged(); + } } catch (MatrixException e) { if (e is { ErrorCode: "M_UNKNOWN_TOKEN" }) _offlineSessions.Add(token); @@ -166,50 +177,9 @@ Small collection of tools to do not-so-everyday things. } sema.Release(); - - StateHasChanged(); }).ToList(); await Task.WhenAll(tasks); - - // var profileTasks = tokens.Select(async token => { - // UserInfo userInfo = new(); - // AuthenticatedHomeserverGeneric hs; - // Console.WriteLine($"Getting hs for {token.ToJson()}"); - // try { - // hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy); - // } - // catch (MatrixException e) { - // if (e.ErrorCode != "M_UNKNOWN_TOKEN") throw; - // _offlineSessions.Add(token); - // return; - // NavigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken); - // } - // catch (Exception e) { - // logger.LogError(e, $"Failed to instantiate AuthenticatedHomeserver for {token.ToJson()}, homeserver may be offline?", token.UserId); - // _offlineSessions.Add(token); - // return; - // } - // - // Console.WriteLine($"Got hs for {token.ToJson()}"); - // - // var roomCountTask = hs.GetJoinedRooms(); - // var profile = await hs.GetProfileAsync(hs.WhoAmI.UserId); - // userInfo.DisplayName = profile.DisplayName ?? hs.WhoAmI.UserId; - // Console.WriteLine(profile.ToJson()); - // _sessions.Add(new() { - // UserInfo = new() { - // AvatarUrl = string.IsNullOrWhiteSpace(profile.AvatarUrl) ? "/blobfox_outage.gif" : hs.ResolveMediaUri(profile.AvatarUrl), - // RoomCount = (await roomCountTask).Count, - // DisplayName = profile.DisplayName ?? hs.WhoAmI.UserId - // }, - // UserAuth = token, - // ServerVersion = await (hs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)), - // Homeserver = hs - // }); - // }).ToList(); - // Console.WriteLine($"Waiting for {profileTasks.Count} profile tasks"); - // await Task.WhenAll(profileTasks); - // Console.WriteLine("Done waiting for profile tasks"); + await base.OnInitializedAsync(); } diff --git a/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor index 3cb9e40..f9cbfa2 100644 --- a/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor +++ b/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor @@ -59,7 +59,7 @@ if (hs is null) return; data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms"); StateHasChanged(); - foreach (var room in await hs.GetJoinedRooms()) { + var tasks = (await hs.GetJoinedRooms()).Select(async room => { var plTask = room.GetPowerLevelsAsync(); var roomNameTask = room.GetNameOrFallbackAsync(); var EditorRoomInfo = new EditorRoomInfo { @@ -71,7 +71,11 @@ Rooms.Add(EditorRoomInfo); StateHasChanged(); - } + return Task.CompletedTask; + }).ToList(); + await Task.WhenAll(tasks); + await Task.Delay(500); + StateHasChanged(); } private class DraupnirProtectedRoomsData { @@ -87,7 +91,7 @@ } private async Task Apply() { - Console.WriteLine(string.Join('\n', Rooms.Where(x=>x.IsProtected).Select(x=>x.Room.RoomId))); + Console.WriteLine(string.Join('\n', Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId))); data.Rooms = Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId).ToList(); await hs.SetAccountDataAsync("org.matrix.mjolnir.protected_rooms", data); } diff --git a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor index 775361a..9218c8c 100644 --- a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor +++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor @@ -82,9 +82,7 @@ else { if (state is null) continue; if (!matchingStates.ContainsKey(state.Membership)) matchingStates.Add(state.Membership, new()); - var roomInfo = new RoomInfo() { - Room = room - }; + var roomInfo = new RoomInfo(room); matchingStates[state.Membership].Add(roomInfo); roomInfo.StateEvents.Add(new() { Type = RoomNameEventContent.EventId, diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor index 1813908..d7a3569 100644 --- a/MatrixUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor @@ -69,14 +69,14 @@ protected override async Task OnInitializedAsync() { Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); if (Homeserver is null) return; - var rooms = await Homeserver.GetJoinedRooms(); + // var rooms = await Homeserver.GetJoinedRooms(); // SemaphoreSlim _semaphore = new(160, 160); GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId); var filter = await Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetBasicRoomInfo); var filterData = await Homeserver.GetFilterAsync(filter); - Rooms = new ObservableCollection<RoomInfo>(rooms.Select(x => new RoomInfo() { Room = x })); + // Rooms = new ObservableCollection<RoomInfo>(rooms.Select(room => new RoomInfo(room))); // foreach (var stateType in filterData.Room?.State?.Types ?? []) { // var tasks = Rooms.Select(async room => { // try { @@ -126,7 +126,7 @@ Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!"); - int maxUpdates = 50; + int maxUpdates = 50000; isInitialSync = false; while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { var (roomId, roomData) = queueEntry; @@ -139,9 +139,7 @@ } else { Console.WriteLine($"QueueWorker: encountered new room {roomId}!"); - room = new RoomInfo() { - Room = Homeserver.GetRoom(roomId) - }; + room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events); Rooms.Add(room); } @@ -156,14 +154,14 @@ Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!"); } - await Task.Delay(100); + // await Task.Delay(100); } 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); + // RenderContents |= queue.Count == 0; + // await Task.Delay(Rooms.Count); } catch (Exception e) { Console.WriteLine("QueueWorker exception: " + e); diff --git a/MatrixUtils.Web/Pages/Rooms/Index2.razor b/MatrixUtils.Web/Pages/Rooms/Index2.razor new file mode 100644 index 0000000..ae31126 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2.razor @@ -0,0 +1,85 @@ +@page "/Rooms2" +@using LibMatrix.Responses +@using System.Collections.ObjectModel +@using System.ComponentModel +@using MatrixUtils.Abstractions +@using MatrixUtils.Web.Pages.Rooms.Index2Components +@inject ILogger<Index> logger +<h3>Room list</h3> + +<RoomsIndex2SyncContainer Data="@Data"></RoomsIndex2SyncContainer> +@if (Data.Homeserver is null || Data.GlobalProfile is null) { + <p>Creating homeserver instance and fetching global profile...</p> + return; +} + +<div> + <LinkButton Color="@(SelectedTab == Tab.Main ? null : "#0b0e62")" OnClick="() => Task.FromResult(SelectedTab = Tab.Main)">Main</LinkButton> + <LinkButton Color="@(SelectedTab == Tab.DMs ? null : "#0b0e62")" OnClick="() => Task.FromResult(SelectedTab = Tab.DMs)">DMs</LinkButton> + <LinkButton Color="@(SelectedTab == Tab.ByRoomType ? null : "#0b0e62")" OnClick="() => Task.FromResult(SelectedTab = Tab.ByRoomType)">By room type</LinkButton> +</div> +<br/> +<CascadingValue Value="@Data"> + @switch (SelectedTab) { + case Tab.Main: + <h3>Main tab</h3> + <RoomsIndex2MainTab></RoomsIndex2MainTab> + break; + case Tab.DMs: + <h3>DMs tab</h3> + break; + case Tab.ByRoomType: + <h3>By room type tab</h3> + break; + default: + throw new InvalidEnumArgumentException(); + } +</CascadingValue> +<br/> + +@* <LinkButton href="/Rooms/Create">Create new room</LinkButton> *@ + + +@code { + + private Tab SelectedTab { + get => _selectedTab; + set { + _selectedTab = value; + StateHasChanged(); + } + } + + public RoomListViewData Data { get; set; } = new RoomListViewData(); + + protected override async Task OnInitializedAsync() { + Data.Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + if (Data.Homeserver is null) return; + var rooms = await Data.Homeserver.GetJoinedRooms(); + Data.GlobalProfile = await Data.Homeserver.GetProfileAsync(Data.Homeserver.WhoAmI.UserId); + + foreach (var room in rooms) { + Data.Rooms.Add(new RoomInfo(room)); + } + StateHasChanged(); + + await base.OnInitializedAsync(); + } + + private Tab _selectedTab = Tab.Main; + + private enum Tab { + Main, + DMs, + ByRoomType + } + + public class RoomListViewData { + public ObservableCollection<RoomInfo> Rooms { get; } = []; + + public UserProfileResponse? GlobalProfile { get; set; } + + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor new file mode 100644 index 0000000..4216824 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor @@ -0,0 +1,27 @@ +@using MatrixUtils.Abstractions +<div class="spaceListItem" onclick="@ToggleSpace"> + <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage> + <span class="spaceNameEllipsis">@Space.RoomName</span> +</div> + +@code { + + [Parameter] + public RoomInfo Space { get; set; } + + [Parameter] + public List<RoomInfo> OpenedSpaces { get; set; } + + protected override Task OnInitializedAsync() { + Space.PropertyChanged += (sender, args) => { StateHasChanged(); }; + return base.OnInitializedAsync(); + } + + public void ToggleSpace() { + if (OpenedSpaces.Contains(Space)) { + OpenedSpaces.Remove(Space); + } else { + OpenedSpaces.Add(Space); + } + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css new file mode 100644 index 0000000..c174567 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css @@ -0,0 +1,15 @@ +.spaceNameEllipsis { + padding-left: 8px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + width: calc(100% - 38px); +} + +.spaceListItem { + display: block; + width: 100%; + height: 50px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor new file mode 100644 index 0000000..f4cf849 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor @@ -0,0 +1,53 @@ +@using MatrixUtils.Abstractions +@using System.Security.Cryptography +@using ArcaneLibs.Extensions +<h3>RoomsIndex2MainTab</h3> + +<div> + <div class="row"> + <div class="col-3" style="background-color: #ffffff66;"> + <LinkButton>Uncategorised rooms</LinkButton> + @foreach (var space in Data.Rooms.Where(x => x.RoomType == "m.space")) { + <div style="@("width: 100%; height: 50px; background-color: #" + RandomNumberGenerator.GetBytes(3).Append((byte)0x11).ToArray().AsHexString().Replace(" ",""))"> + <p>@space.RoomName</p> + </div> + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>omae wa mou shindeiru</p> + </div> + </div> +</div> + +@code { + + [CascadingParameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + Data.Rooms.CollectionChanged += (sender, args) => { + DebouncedStateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var newItem in args.NewItems) { + (newItem as RoomInfo).PropertyChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + }; + await base.OnInitializedAsync(); + } + + //debounce StateHasChanged, we dont want to reredner on every key stroke + + private CancellationTokenSource _debounceCts = new CancellationTokenSource(); + + private async Task DebouncedStateHasChanged() { + _debounceCts.Cancel(); + _debounceCts = new CancellationTokenSource(); + try { + await Task.Delay(100, _debounceCts.Token); + Console.WriteLine("DebouncedStateHasChanged - Calling StateHasChanged!"); + StateHasChanged(); + } + catch (TaskCanceledException) { } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor new file mode 100644 index 0000000..2b7c5ac --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor @@ -0,0 +1,198 @@ +@using MatrixUtils.Abstractions +@using System.Security.Cryptography +@using ArcaneLibs.Extensions +@using System.ComponentModel +@using System.Diagnostics +@using LibMatrix.EventTypes.Spec.State +@using MatrixUtils.Web.Pages.Rooms.Index2Components.MainTabComponents +@using Microsoft.AspNetCore.Components.Rendering +<h3>RoomsIndex2MainTab</h3> + +@* <div> *@ +@* <div class="row"> *@ +@* <div class="col-3" style="background-color: #ffffff66;"> *@ +@* <LinkButton>Uncategorised rooms</LinkButton> *@ +@* @foreach (var space in GetTopLevelSpaces()) { *@ +@* <a style="@("display:block; width: 100%; height: 50px; background-color: #" + RandomNumberGenerator.GetBytes(3).Append((byte)0x11).ToArray().AsHexString().Replace(" ", ""))"> *@ +@* <div style="vertical-align: middle;"> *@ +@* <div style="overflow:hidden; text-overflow: ellipsis; white-space: nowrap; ">@space.RoomName</div> *@ +@* </div> *@ +@* </a> *@ +@* } *@ +@* </div> *@ +@* <div class="col-9" style="background-color: #ff00ff66;"> *@ +@* <p>Placeholder for rooms list...</p> *@ +@* </div> *@ +@* </div> *@ +@* </div> *@ + +<div> + <div class="row"> + <div class="col-3" style="background-color: #ffffff22;"> + <LinkButton>Uncategorised rooms</LinkButton> + @foreach (var space in GetTopLevelSpaces()) { + @RecursingSpaceChildren(space) + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>Placeholder for rooms list...</p> + </div> + </div> +</div> + + +@code { + + [CascadingParameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + Data.Rooms.CollectionChanged += (sender, args) => { + DebouncedStateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var newItem in args.NewItems) { + (newItem as RoomInfo).PropertyChanged += OnRoomListChanged; + (newItem as RoomInfo).StateEvents.CollectionChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + }; + foreach (var newItem in Data.Rooms) { + newItem.PropertyChanged += OnRoomListChanged; + newItem.StateEvents.CollectionChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + + await base.OnInitializedAsync(); + StateHasChanged(); + } + + private void OnRoomListChanged(object? sender, PropertyChangedEventArgs e) { + if (e.PropertyName == "RoomName" || e.PropertyName == "RoomType") + DebouncedStateHasChanged(); + } + + private CancellationTokenSource _debounceCts = new CancellationTokenSource(); + + private async Task DebouncedStateHasChanged() { + _debounceCts.Cancel(); + _debounceCts = new CancellationTokenSource(); + try { + Console.WriteLine("DebouncedStateHasChanged - Waiting 50ms..."); + await Task.Delay(50, _debounceCts.Token); + Console.WriteLine("DebouncedStateHasChanged - Calling StateHasChanged!"); + StateHasChanged(); + } + catch (TaskCanceledException) { } + } + + private List<RoomInfo> GetTopLevelSpaces() { + var spaces = Data.Rooms.Where(x => x.RoomType == "m.space").OrderBy(x => x.RoomName).ToList(); + var allSpaceChildEvents = spaces.SelectMany(x => x.StateEvents.Where(y => + y.Type == SpaceChildEventContent.EventId && + y.RawContent!.Count > 0 + )).ToList(); + + Console.WriteLine($"Child count: {allSpaceChildEvents.Count}"); + + spaces.RemoveAll(x => allSpaceChildEvents.Any(y => y.StateKey == x.Room.RoomId)); + + if (allSpaceChildEvents.Count == 0) { + Console.WriteLine("No space children found, returning nothing..."); + return []; + } + + return spaces.ToList(); + } + + private List<RoomInfo> GetSpaceChildren(RoomInfo space) { + var childEvents = space.StateEvents.Where(x => + x.Type == SpaceChildEventContent.EventId && + x.RawContent!.Count > 0 + ).ToList(); + var children = childEvents.Select(x => Data.Rooms.FirstOrDefault(y => y.Room.RoomId == x.StateKey)).Where(x => x is not null).ToList(); + return children; + } + + private List<RoomInfo> GetSpaceChildSpaces(RoomInfo space) { + var children = GetSpaceChildren(space); + var childSpaces = children.Where(x => x.RoomType == "m.space").ToList(); + return childSpaces; + } + + private RoomInfo? SelectedSpace { get; set; } + private List<RoomInfo> OpenedSpaces { get; set; } = new List<RoomInfo>(); + + private RenderFragment RecursingSpaceChildren(RoomInfo space, List<RoomInfo>? parents = null, int depth = 0) { + parents ??= []; + var totalSw = Stopwatch.StartNew(); + var children = GetSpaceChildSpaces(space); + + var randomColor = RandomNumberGenerator.GetBytes(3).Append((byte)0x33).ToArray().AsHexString().Replace(" ", ""); + var isExpanded = OpenedSpaces.Contains(space); + + // Console.WriteLine($"RecursingSpaceChildren::FetchData - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + + // var renderSw = Stopwatch.StartNew(); + var rf = new RenderFragment(builder => { + builder.OpenElement(0, "div"); + //space list entry render fragment + // builder.AddContent(1, SpaceListEntry(space)); + builder.OpenComponent<MainTabSpaceItem>(1); + builder.AddAttribute(2, "Space", space); + builder.AddAttribute(2, "OpenedSpaces", OpenedSpaces); + builder.CloseComponent(); + builder.CloseElement(); + //space children render fragment + if (isExpanded) { + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "style", "padding-left: 10px;"); + foreach (var child in children) { + builder.AddContent(4, RecursingSpaceChildren(child, parents.Append(space).ToList(), depth + 1)); + } + + builder.CloseElement(); + } + }); + + // Console.WriteLine($"RecursingSpaceChildren::Render - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {renderSw.Elapsed}"); + if (totalSw.ElapsedMilliseconds > 20) + Console.WriteLine($"RecursingSpaceChildren::Total - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + // Console.WriteLine($"RecursingSpaceChildren::Total - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + return rf; + } + + // private RenderFragment SpaceListEntry(RoomInfo space) { + // return builder => { + // { + // builder.OpenElement(0, "div"); + // builder.AddAttribute(1, "style", "display: block; width: 100%; height: 50px;"); + // builder.AddAttribute(2, "onclick", EventCallback.Factory.Create(this, () => { + // if (OpenedSpaces.Contains(space)) { + // OpenedSpaces.Remove(space); + // } + // else { + // OpenedSpaces.Add(space); + // } + // + // StateHasChanged(); + // })); + // { + // builder.OpenComponent<MxcImage>(5); + // builder.AddAttribute(6, "Homeserver", Data.Homeserver); + // builder.AddAttribute(7, "MxcUri", space.RoomIcon); + // builder.AddAttribute(8, "Circular", true); + // builder.AddAttribute(9, "Width", 32); + // builder.AddAttribute(10, "Height", 32); + // builder.CloseComponent(); + // } + // { + // // room name, ellipsized + // builder.OpenElement(11, "span"); + // builder.AddAttribute(12, "class", "spaceNameEllipsis"); + // builder.AddContent(13, space.RoomName); + // builder.CloseElement(); + // } + // builder.CloseElement(); + // } + // }; + // } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor new file mode 100644 index 0000000..bbc63eb --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor @@ -0,0 +1,202 @@ +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using MatrixUtils.Abstractions +@using System.Diagnostics +@using System.Diagnostics.CodeAnalysis +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Extensions +@using LibMatrix.Utilities +@using System.Collections.ObjectModel +@using ArcaneLibs +@inject ILogger<RoomsIndex2SyncContainer> logger +<pre>RoomsIndex2SyncContainer</pre> +@foreach (var (name, value) in _statusList) { + <pre>[@name] @value.Status</pre> +} + +@code { + + [Parameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + private SyncHelper syncHelper; + + private Queue<KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>> queue = new(); + + private ObservableCollection<(string name, ObservableStatus value)> _statusList = new(); + + protected override async Task OnInitializedAsync() { + _statusList.CollectionChanged += (sender, args) => { + StateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var item in args.NewItems) { + if (item is not (string name, ObservableStatus value)) continue; + value.PropertyChanged += (sender, args) => { + if(value.Show) StateHasChanged(); + }; + } + }; + + while (Data.Homeserver is null) { + await Task.Delay(100); + } + + await SetUpSync(); + } + + private async Task SetUpSync() { + var status = await GetOrAddStatus("Main"); + var syncHelpers = new Dictionary<string, SyncHelper>() { + ["Main"] = new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetBasicRoomInfo), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + } + }; + status.Status = "Initial sync... Checking server filter capability..."; + var syncRes = await syncHelpers["Main"].SyncAsync(); + if (!syncRes.Rooms?.Join?.Any(x => x.Value.State?.Events?.Any(y => y.Type == SpaceChildEventContent.EventId) ?? false) ?? true) { + status.Status = "Initial sync indicates that server supports filters, starting helpers!"; + syncHelpers.Add("SpaceRelations", new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetSpaceRelations), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + }); + + syncHelpers.Add("Profile", new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetOwnMemberEvents), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + }); + } + else status.Status = "Initial sync indicates that server does not support filters, continuing without extra filters!"; + + await HandleSyncResponse(syncRes); + + // profileSyncHelper = new SyncHelper(Homeserver, logger) { + // Timeout = 10000, + // Filter = profileUpdateFilter, + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + // }; + // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId); + RunQueueProcessor(); + foreach (var helper in syncHelpers) { + Console.WriteLine($"Starting sync loop for {helper.Key}"); + RunSyncLoop(helper.Value, helper.Key); + } + } + + private async Task RunQueueProcessor() { + var status = await GetOrAddStatus("QueueProcessor"); + var statusd = await GetOrAddStatus("QueueProcessor/D", show: false); + while (true) { + await Task.Delay(1000); + try { + var renderTimeSw = Stopwatch.StartNew(); + while (queue.Count == 0) { + var delay = 1000; + Console.WriteLine("Queue is empty, waiting..."); + // Status2 = $"Queue is empty, waiting for {delay}ms..."; + await Task.Delay(delay); + } + + status.Status = $"Queue no longer empty after {renderTimeSw.Elapsed}!"; + renderTimeSw.Restart(); + + int maxUpdates = 5000; + while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { + var (roomId, roomData) = queueEntry; + statusd.Status = $"Dequeued room {roomId}"; + RoomInfo room; + + if (Data.Rooms.Any(x => x.Room.RoomId == roomId)) { + room = Data.Rooms.First(x => x.Room.RoomId == roomId); + statusd.Status = $"{roomId} already known with {room.StateEvents?.Count ?? 0} state events"; + } + else { + statusd.Status = $"Eencountered new room {roomId}!"; + room = new RoomInfo(Data.Homeserver!.GetRoom(roomId), roomData.State?.Events); + Data.Rooms.Add(room); + } + + if (roomData.State?.Events is { Count: > 0 }) + room.StateEvents!.MergeStateEventLists(roomData.State.Events); + else { + statusd.Status = $"Could not merge state for {room.Room.RoomId} as new data contains no state events!"; + } + + // await Task.Delay(10); + } + + status.Status = $"Got {Data.Rooms.Count} rooms so far! {queue.Count} entries left in processing queue... Parsed last response in {renderTimeSw.Elapsed}"; + + // RenderContents |= queue.Count == 0; + // await Task.Delay(Data.Rooms.Count); + } + catch (Exception e) { + Console.WriteLine("QueueWorker exception: " + e); + } + } + } + + private async Task RunSyncLoop(SyncHelper syncHelper, string name = "Unknown") { + var status = await GetOrAddStatus($"SYNC/{name}"); + status.Status = $"Initial syncing..."; + + var syncs = syncHelper.EnumerateSyncAsync(); + await foreach (var sync in syncs) { + var sw = Stopwatch.StartNew(); + status.Status = $"[{DateTime.Now}] Got {Data.Rooms.Count} rooms so far! {sync.Rooms?.Join?.Count ?? 0} new updates!"; + + await HandleSyncResponse(sync); + status.Status += $"\nProcessed sync in {sw.ElapsedMilliseconds}ms, queue length: {queue.Count}"; + } + } + + private async Task HandleSyncResponse(SyncResponse? sync) { + if (sync?.Rooms?.Join is { Count: > 0 }) + foreach (var joinedRoom in sync.Rooms.Join) + queue.Enqueue(joinedRoom); + + if (sync.Rooms.Leave is { Count: > 0 }) + foreach (var leftRoom in sync.Rooms.Leave) + if (Data.Rooms.Any(x => x.Room.RoomId == leftRoom.Key)) + Data.Rooms.Remove(Data.Rooms.First(x => x.Room.RoomId == leftRoom.Key)); + } + + private SemaphoreSlim _syncLock = new(1, 1); + + private async Task<ObservableStatus> GetOrAddStatus(string name, bool show = true, bool log = true) { + await _syncLock.WaitAsync(); + try { + if (_statusList.Any(x => x.name == name)) + return _statusList.First(x => x.name == name).value; + var status = new ObservableStatus() { + Name = name, + Log = log, + Show = show + }; + _statusList.Add((name, status)); + return status; + } + finally { + _syncLock.Release(); + } + } + + private class ObservableStatus : NotifyPropertyChanged { + private string _status = "Initialising..."; + public string Name { get; set; } = "Unknown"; + public bool Show { get; set; } = true; + public bool Log { get; set; } = true; + + public string Status { + get => _status; + set { + if(SetField(ref _status, value) && Log) + Console.WriteLine($"[{Name}]: {value}"); + } + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor new file mode 100644 index 0000000..c94d0b0 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor @@ -0,0 +1,158 @@ +@page "/Tools/PolicyListActivity" +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Diagnostics +@using LibMatrix.RoomTypes +@using LibMatrix.EventTypes.Common + + +@if (RoomData.Count == 0) +{ + <p>Loading...</p> +} +else + foreach (var room in RoomData) + { + <h3>@room.Key</h3> + @foreach (var year in room.Value.OrderBy(x => x.Key)) + { + <h5>@year.Key</h5> + <ActivityGraph Data="@year.Value" GlobalMax="MaxValue" + RLabel="removed" GLabel="new" BLabel="updated policies"> + </ActivityGraph> + } + } + + +@code { + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + public List<GenericRoom> FilteredRooms = new(); + + public Dictionary<DateOnly, ActivityGraph.RGB> TestData { get; set; } = new(); + + public ActivityGraph.RGB MaxValue { get; set; } = new() + { + R = 255, G = 255, B = 255 + }; + + public Dictionary<string, Dictionary<int, Dictionary<DateOnly, ActivityGraph.RGB>>> RoomData { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + var sw = Stopwatch.StartNew(); + await base.OnInitializedAsync(); + Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!; + if (Homeserver is null) return; + + //random test data + for (DateOnly i = new DateOnly(2020, 1, 1); i < new DateOnly(2020, 12, 30); i = i.AddDays(Random.Shared.Next(5))) + { + TestData[i] = new() + { + R = (int)(Random.Shared.NextSingle() * 255), + G = (int)(Random.Shared.NextSingle() * 255), + B = (int)(Random.Shared.NextSingle() * 255) + }; + } + + StateHasChanged(); + // return; + + var rooms = await Homeserver.GetJoinedRooms(); + // foreach (var room in rooms) + // { + // var type = await room.GetRoomType(); + // if (type == "support.feline.policy.lists.msc.v1") + // { + // Console.WriteLine($"{room.RoomId} is policy list by type"); + // FilteredRooms.Add(room); + // } + // else if(await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) + // { + // Console.WriteLine($"{room.RoomId} is policy list by shortcode"); + // FilteredRooms.Add(room); + // } + // } + var roomFilterTasks = rooms.Select(async room => + { + var type = await room.GetRoomType(); + if (type == "support.feline.policy.lists.msc.v1") + { + Console.WriteLine($"{room.RoomId} is policy list by type"); + return room; + } + else if (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) + { + Console.WriteLine($"{room.RoomId} is policy list by shortcode"); + return room; + } + + return null; + }).ToList(); + var filteredRooms = await Task.WhenAll(roomFilterTasks); + FilteredRooms.AddRange(filteredRooms.Where(x => x is not null).Cast<GenericRoom>()); + Console.WriteLine($"Filtered {FilteredRooms.Count} rooms in {sw.ElapsedMilliseconds}ms"); + + var roomTasks = FilteredRooms.Select(FetchRoomHistory).ToList(); + await Task.WhenAll(roomTasks); + + Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}"); + Console.WriteLine($"Filtered {FilteredRooms.Count} rooms in {sw.ElapsedMilliseconds}ms"); + } + + public async Task FetchRoomHistory(GenericRoom room) + { + var roomName = await room.GetNameOrFallbackAsync(); + if (string.IsNullOrWhiteSpace(roomName)) roomName = room.RoomId; + if (!RoomData.ContainsKey(roomName)) + { + RoomData[roomName] = new(); + } + + //use timeline + var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000); + await foreach (var response in timeline) + { + Console.WriteLine($"Got {response.State.Count} state, {response.Chunk.Count} timeline"); + if (response.State.Count != 0) throw new Exception("Why the hell did we receive state events?"); + foreach (var message in response.Chunk) + { + if (!message.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + //OriginServerTs to datetime + var dt = DateTimeOffset.FromUnixTimeMilliseconds((long)message.OriginServerTs!.Value).DateTime; + var date = new DateOnly(dt.Year, dt.Month, dt.Day); + if (!RoomData[roomName].ContainsKey(date.Year)) + { + RoomData[roomName][date.Year] = new(); + } + + if (!RoomData[roomName][date.Year].ContainsKey(date)) + { + // Console.WriteLine($"Adding {date} to {roomName}"); + RoomData[roomName][date.Year][date] = new(); + } + + var rgb = RoomData[roomName][date.Year][date]; + if (message.RawContent?.Count == 0) rgb.R++; + else if (string.IsNullOrWhiteSpace(message.Unsigned?.ReplacesState)) rgb.G++; + else rgb.B++; + RoomData[roomName][date.Year][date] = rgb; + } + + var max = RoomData.SelectMany(x => x.Value.Values).Aggregate(new ActivityGraph.RGB(), (current, next) => new() + { + R = Math.Max(current.R, next.Average(x => x.Value.R)), + G = Math.Max(current.G, next.Average(x => x.Value.G)), + B = Math.Max(current.B, next.Average(x => x.Value.B)) + }); + MaxValue = new ActivityGraph.RGB( + r: Math.Max(max.R, Math.Max(max.G, max.B)), + g: Math.Max(max.R, Math.Max(max.G, max.B)), + b: Math.Max(max.R, Math.Max(max.G, max.B))); + Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}"); + StateHasChanged(); + await Task.Delay(100); + } + } + + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/UserTrace.razor index b3a7487..d78c58a 100644 --- a/MatrixUtils.Web/Pages/Tools/UserTrace.razor +++ b/MatrixUtils.Web/Pages/Tools/UserTrace.razor @@ -80,18 +80,33 @@ Random.Shared.Shuffle(distinctRooms); rooms = new ObservableCollection<GenericRoom>(distinctRooms); rooms.CollectionChanged += (sender, args) => StateHasChanged(); + try { + var stateTasks = rooms.Select(async x => { + for (int i = 0; i < 10; i++) { + try { + return (x, await x.GetMembersListAsync(false)); + } + catch { + // + } + } - var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync(false))).ToAsyncEnumerable(); + return (x, new List<StateEventResponse>().ToFrozenSet()); + }).ToAsyncEnumerable(); - await foreach (var (room, state) in stateTasks) { - roomMembers.Add(room, state); - log.Add($"Got {state.Count} members for {room.RoomId}..."); + await foreach (var (room, state) in stateTasks) { + roomMembers.Add(room, state); + log.Add($"Got {state.Count} members for {room.RoomId}..."); + } + } + catch { + // } log.Add($"Done fetching members!"); - UserIDs.RemoveAll(x=>sessions.Any(y=>y.UserId == x)); - + UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x)); + StateHasChanged(); Console.WriteLine("Rerendered!"); await base.OnInitializedAsync(); @@ -105,7 +120,6 @@ matches[userId].Add(new() { Event = events.First(x => x.StateKey == userId && x.Type == RoomMemberEventContent.EventId), Room = room, - }); } } @@ -132,6 +146,7 @@ private class Matches { public GenericRoom Room; + public StateEventResponse Event; // public } diff --git a/MatrixUtils.Web/Pages/User/DMManager.razor b/MatrixUtils.Web/Pages/User/DMManager.razor index df5cd6b..80bf3b2 100644 --- a/MatrixUtils.Web/Pages/User/DMManager.razor +++ b/MatrixUtils.Web/Pages/User/DMManager.razor @@ -2,6 +2,7 @@ @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Responses @using MatrixUtils.Abstractions +@using LibMatrix <h3>Direct Messages</h3> <hr/> @@ -36,11 +37,19 @@ Status = "Loading DM list from account data..."; var dms = await Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); DMRooms.Clear(); - foreach (var (userId, rooms) in dms) { + var userTasks = dms.Select(async kv => { + var (userId, rooms) = kv; var roomList = new List<RoomInfo>(); - DMRooms.Add(await Homeserver.GetProfileAsync(userId), roomList); + UserProfileResponse? profile = null; + try { + profile = await Homeserver.GetProfileAsync(userId); + } + catch (MatrixException e) { + if (e is { ErrorCode: "M_UNKNOWN" }) profile = new UserProfileResponse() { DisplayName = $"{userId}: {e.Error}" }; + } + foreach (var room in rooms) { - var roomInfo = new RoomInfo() { Room = Homeserver.GetRoom(room) }; + var roomInfo = new RoomInfo(Homeserver.GetRoom(room)); roomList.Add(roomInfo); roomInfo.StateEvents.Add(new() { Type = RoomNameEventContent.EventId, @@ -50,8 +59,13 @@ RoomId = room, Sender = null, EventId = null }); } + + DMRooms.Add(profile ?? new() { DisplayName = userId }, roomList); StateHasChanged(); - } + }).ToList(); + + await Task.WhenAll(userTasks); + await Task.Delay(500); StateHasChanged(); Status = null; diff --git a/MatrixUtils.Web/Pages/User/DMSpace.razor b/MatrixUtils.Web/Pages/User/DMSpace.razor index 519cfff..e3dba30 100644 --- a/MatrixUtils.Web/Pages/User/DMSpace.razor +++ b/MatrixUtils.Web/Pages/User/DMSpace.razor @@ -1,11 +1,14 @@ @page "/User/DMSpace/Setup" @using LibMatrix +@using LibMatrix.Responses +@using MatrixUtils.Abstractions @using MatrixUtils.LibDMSpace @using MatrixUtils.LibDMSpace.StateEvents @using MatrixUtils.Web.Pages.User.DMSpaceStages +@using System.Text.Json.Serialization <h3>DM Space Management</h3> <hr/> -<CascadingValue Value="@DmSpace"> +<CascadingValue Value="@SetupData"> @switch (Stage) { case -1: <p>Initialising...</p> @@ -41,36 +44,29 @@ } } - public AuthenticatedHomeserverGeneric? Homeserver { get; set; } - public DMSpaceConfiguration? DmSpaceConfiguration { get; set; } - - [Parameter] - public DMSpace? DmSpace { get; set; } + public DMSpace? DMSpaceRootPage { get; set; } protected override async Task OnInitializedAsync() { if (NavigationManager.Uri.Contains("?stage=")) { - NavigationManager.NavigateTo("/User/DMSpace", true); + NavigationManager.NavigateTo("/User/DMSpace/Setup", true); } - DmSpace = this; - Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); - if (Homeserver is null) return; + DMSpaceRootPage = this; + SetupData.Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); + if (SetupData.Homeserver is null) return; try { - DmSpaceConfiguration = await Homeserver.GetAccountDataAsync<DMSpaceConfiguration>("gay.rory.dm_space"); - var room = Homeserver.GetRoom(DmSpaceConfiguration.DMSpaceId); - await room.GetStateAsync<object>(DMSpaceInfo.EventId); + SetupData.DmSpaceConfiguration = await SetupData.Homeserver.GetAccountDataAsync<DMSpaceConfiguration>("gay.rory.dm_space"); + var room = SetupData.Homeserver.GetRoom(SetupData.DmSpaceConfiguration.DMSpaceId); + await room.GetStateAsync<DMSpaceInfo>(DMSpaceInfo.EventId); Stage = 1; } catch (MatrixException e) { - if (e.ErrorCode == "M_NOT_FOUND") { + if (e.ErrorCode is "M_NOT_FOUND" or "M_FORBIDDEN") { Stage = 0; - DmSpaceConfiguration = new(); + SetupData.DmSpaceConfiguration = new(); } else throw; } - catch (Exception e) { - throw; - } finally { StateHasChanged(); } @@ -82,4 +78,27 @@ await base.OnParametersSetAsync(); } + public DMSpaceSetupData SetupData { get; set; } = new(); + + public class DMSpaceSetupData { + + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + public DMSpaceConfiguration? DmSpaceConfiguration { get; set; } + + public DMSpaceInfo? DmSpaceInfo { get; set; } = new(); + + public Dictionary<string, RoomInfo>? Spaces; + + public Dictionary<UserProfileWithId, List<RoomInfo>>? DMRooms; + + public RoomInfo? DMSpaceRoomInfo { get; set; } + + + public class UserProfileWithId : UserProfileResponse { + [JsonIgnore] + public string Id { get; set; } + } + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor index 49fd5b4..5f6508c 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor @@ -4,7 +4,7 @@ <p>This wizard will help you set up a DM space.</p> <p>This is useful for eg. sharing DM rooms across multiple accounts.</p> <br/> -<LinkButton href="/User/DMSpace?stage=1">Get started</LinkButton> +<LinkButton href="/User/DMSpace/Setup?stage=1">Get started</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor index 6131617..2176467 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor @@ -6,30 +6,40 @@ @using MatrixUtils.LibDMSpace.StateEvents @using Microsoft.Extensions.Primitives @using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using MatrixUtils.Abstractions <b> <u>DM Space setup tool - stage 1: Configure space</u> </b> <p>You will need a space to use for DM rooms.</p> -@if (DmSpace is not null) { - <p> - Selected space: - <InputSelect @bind-Value="DmSpace.DmSpaceConfiguration.DMSpaceId"> - @foreach (var (id, name) in spaces) { - <option value="@id">@name</option> - } - </InputSelect> - </p> - <p> - <InputCheckbox @bind-Value="DmSpaceInfo.LayerByUser"></InputCheckbox> - Create sub-spaces per user - </p> +@if (SetupData is not null) { + if (SetupData.Spaces is not null) { + <p> + Selected space: + <InputSelect @bind-Value="SetupData.DmSpaceConfiguration.DMSpaceId"> + <option value="">New space</option> + @foreach (var (id, roomInfo) in SetupData.Spaces) { + <option value="@id">@roomInfo.RoomName</option> + } + </InputSelect> + </p> + <p> + <InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox> + Create sub-spaces per user + </p> + + <br/> + <LinkButton OnClick="@Disband" Color="#FF0000">Disband</LinkButton> + <LinkButton OnClick="@Execute">Next</LinkButton> + } + else { + <p>Discovering spaces, please wait...</p> + } } else { - <b>Error: DmSpaceConfiguration is null!</b> + <b>Error: Setup data is null!</b> } -<br/> -<LinkButton OnClick="@Execute">Next</LinkButton> @if (!string.IsNullOrWhiteSpace(Status)) { <p>@Status</p> @@ -45,84 +55,97 @@ else { } } - private Dictionary<string, string> spaces = new() { { "", "New space" } }; private string? _status; [CascadingParameter] - public DMSpace? DmSpace { get; set; } + public DMSpace.DMSpaceSetupData SetupData { get; set; } - public DMSpaceInfo? DmSpaceInfo { get; set; } = new(); + SemaphoreSlim _semaphoreSlim = new(1, 1); protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); - } - - SemaphoreSlim _semaphoreSlim = new(1, 1); - protected override async Task OnParametersSetAsync() { - if (DmSpace is null) + if (SetupData is null) return; + await _semaphoreSlim.WaitAsync(); - DmSpace.DmSpaceConfiguration ??= new(); - if (spaces.Count == 1) { - Status = "Looking for spaces..."; - var userRoomsEnum = DmSpace.Homeserver.GetJoinedRoomsByType("m.space"); - List<GenericRoom> userRooms = new(); - await foreach (var room in userRoomsEnum) { - userRooms.Add(room); - } - var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncEnumerable(); - await foreach(var room in roomChecks) - if(room.HasValue) - spaces.TryAdd(room.Value.id, room.Value.name); - - Status = "Done!"; + + Dictionary<string, RoomInfo> spaces = []; + SetupData.DmSpaceConfiguration ??= new(); + + Status = "Looking for spaces..."; + var userRoomsEnum = SetupData.Homeserver!.GetJoinedRoomsByType("m.space"); + + List<GenericRoom> userRooms = new(); + await foreach (var room in userRoomsEnum) { + userRooms.Add(room); } + + var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncEnumerable(); + await foreach (var room in roomChecks) + if (room.HasValue) + spaces.TryAdd(room.Value.id, room.Value.roomInfo); + + SetupData.Spaces = spaces; + + Status = "Done!"; _semaphoreSlim.Release(); await base.OnParametersSetAsync(); } private async Task Execute() { - if (string.IsNullOrWhiteSpace(DmSpace.DmSpaceConfiguration.DMSpaceId)) { - var crr = CreateRoomRequest.CreatePrivate(DmSpace.Homeserver, "Direct Messages"); - crr.CreationContentBaseType.Type = "m.space"; - DmSpace.DmSpaceConfiguration.DMSpaceId = (await DmSpace.Homeserver.CreateRoom(crr)).RoomId; + if (string.IsNullOrWhiteSpace(SetupData!.DmSpaceConfiguration!.DMSpaceId)) { + var createRoomRequest = CreateRoomRequest.CreatePrivate(SetupData.Homeserver!, "Direct Messages"); + createRoomRequest.CreationContentBaseType.Type = "m.space"; + SetupData.DmSpaceConfiguration.DMSpaceId = (await SetupData.Homeserver!.CreateRoom(createRoomRequest)).RoomId; } - await DmSpace.Homeserver!.SetAccountDataAsync(DMSpaceConfiguration.EventId, DmSpace.DmSpaceConfiguration); - var space = DmSpace.Homeserver.GetRoom(DmSpace.DmSpaceConfiguration.DMSpaceId); - await space.SendStateEventAsync(DMSpaceInfo.EventId, DmSpaceInfo); + + await SetupData.Homeserver!.SetAccountDataAsync(DMSpaceConfiguration.EventId, SetupData.DmSpaceConfiguration); + var space = SetupData.Homeserver.GetRoom(SetupData.DmSpaceConfiguration.DMSpaceId); + await space.SendStateEventAsync(DMSpaceInfo.EventId, SetupData.DmSpaceInfo); + SetupData.DMSpaceRoomInfo = new RoomInfo(space); + await SetupData.DMSpaceRoomInfo.FetchAllStateAsync(); NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=2"); } - public async Task<(string id, string name)?> GetFeasibleSpaces(GenericRoom room) { + public async Task<(string id, RoomInfo roomInfo)?> GetFeasibleSpaces(GenericRoom room) { try { - var pls = await room.GetPowerLevelsAsync(); - if (!pls.UserHasStatePermission(DmSpace.Homeserver.WhoAmI.UserId, "m.space.child")) { + var ri = new RoomInfo(room); + + await foreach(var evt in room.GetFullStateAsync()) + ri.StateEvents.Add(evt); + + var powerLevels = (await ri.GetStateEvent(RoomPowerLevelEventContent.EventId)).TypedContent as RoomPowerLevelEventContent; + if (!powerLevels.UserHasStatePermission(SetupData.Homeserver.WhoAmI.UserId, SpaceChildEventContent.EventId)) { Console.WriteLine($"No permission to send m.space.child in {room.RoomId}..."); return null; } - var roomName = await room.GetNameAsync(); - Status = $"Found viable space: {roomName}"; - if (string.IsNullOrWhiteSpace(DmSpace.DmSpaceConfiguration.DMSpaceId)) { - try { - var dsi = await DmSpace.Homeserver.GetRoom(room.RoomId).GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) ?? new DMSpaceInfo(); - if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is not null && dsi is not null) { - DmSpace.DmSpaceConfiguration.DMSpaceId = room.RoomId; - DmSpaceInfo = dsi; - } - } - catch (MatrixException e) { - if (e.ErrorCode == "M_NOT_FOUND") Console.WriteLine($"{room.RoomId} is not a DM space."); - else throw; + + Status = $"Found viable space: {ri.RoomName}"; + if (!string.IsNullOrWhiteSpace(SetupData.DmSpaceConfiguration!.DMSpaceId)) { + if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is { } dsi) { + SetupData.DmSpaceConfiguration.DMSpaceId = room.RoomId; + SetupData.DmSpaceInfo = dsi; + Console.WriteLine(dsi.ToJson(ignoreNull: true)); } } - return (room.RoomId, roomName); + + if (ri.RoomName == room.RoomId) + ri.RoomName = await room.GetNameOrFallbackAsync(); + + return (room.RoomId, ri); } catch (MatrixException e) { if (e.ErrorCode == "M_NOT_FOUND") Console.WriteLine($"m.room.power_levels does not exist in {room.RoomId}!!!"); else throw; } + return null; } + private async Task Disband() { + var space = new DMSpaceRoom(SetupData.Homeserver, SetupData.DmSpaceConfiguration.DMSpaceId); + await space.DisbandDMSpace(); + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor index 5a53347..a70e9c5 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor @@ -17,18 +17,23 @@ <p>@Status</p> } -@if (DmSpace is not null) { - @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.Id)) { - <InlineUserItem User="@userId"></InlineUserItem> - @foreach (var roomInfo in room) { - <RoomListItem RoomInfo="@roomInfo"> - <LinkButton Round="true" OnClick="@(async () => DmToReassign = roomInfo)">Reassign</LinkButton> - </RoomListItem> +@if (SetupData is not null) { + if (SetupData.DMRooms is { Count: > 0 }) { + @foreach (var (userId, room) in SetupData.DMRooms.OrderBy(x => x.Key.Id)) { + <InlineUserItem User="@userId"></InlineUserItem> + @foreach (var roomInfo in room) { + <RoomListItem RoomInfo="@roomInfo"> + <LinkButton Round="true" OnClick="@(async () => DmToReassign = roomInfo)">Reassign</LinkButton> + </RoomListItem> + } } } + else { + <p>DM room list is loading, please wait...</p> + } } else { - <b>Error: DmSpaceConfiguration is null!</b> + <b>Error: DMSpaceRootPage is null!</b> } <br/> @@ -88,26 +93,21 @@ else { private RoomInfo? _dmToReassign; [CascadingParameter] - public DMSpace? DmSpace { get; set; } + public DMSpace.DMSpaceSetupData SetupData { get; set; } - private Dictionary<UserProfileWithId, List<RoomInfo>> dmRooms { get; set; } = new(); - private Dictionary<RoomInfo, List<UserProfileWithId>> duplicateDmRooms { get; set; } = new(); - private Dictionary<RoomInfo, List<UserProfileWithId>> roomMembers { get; set; } = new(); - - protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); - } + private Dictionary<RoomInfo, List<DMSpace.DMSpaceSetupData.UserProfileWithId>> duplicateDmRooms { get; set; } = new(); + private Dictionary<RoomInfo, List<DMSpace.DMSpaceSetupData.UserProfileWithId>> roomMembers { get; set; } = new(); SemaphoreSlim _semaphore = new(1, 1); - protected override async Task OnParametersSetAsync() { - if (DmSpace is null) + protected override async Task OnInitializedAsync() { + if (SetupData is null) return; await _semaphore.WaitAsync(); DmToReassign = null; - var hs = DmSpace.Homeserver; + var hs = SetupData.Homeserver; Status = "Loading DM list from account data..."; - var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + var dms = await SetupData.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); Status = "Optimising DM list from account data..."; var joinedRooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList(); foreach (var (user, rooms) in dms) { @@ -116,18 +116,22 @@ else { if (!joinedRooms.Contains(roomId)) rooms.RemoveAt(i); } + dms[user] = rooms.Distinct().ToList(); } - dms.RemoveAll((x, y) => y is {Count: 0}); - await DmSpace.Homeserver.SetAccountDataAsync("m.direct", dms); - dmRooms.Clear(); + + dms.RemoveAll((x, y) => y is { Count: 0 }); + await SetupData.Homeserver.SetAccountDataAsync("m.direct", dms); Status = "DM list optimised, fetching info..."; + + SetupData.DMRooms = new Dictionary<DMSpace.DMSpaceSetupData.UserProfileWithId, List<RoomInfo>>(); + var results = dms.Select(async x => { var (userId, rooms) = x; - UserProfileWithId userProfile; + DMSpace.DMSpaceSetupData.UserProfileWithId userProfile; try { - var profile = await DmSpace.Homeserver.GetProfileAsync(userId); + var profile = await SetupData.Homeserver.GetProfileAsync(userId); userProfile = new() { AvatarUrl = profile.AvatarUrl, Id = userId, @@ -141,32 +145,35 @@ else { Id = userId }; } + var roomList = new List<RoomInfo>(); var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable(); await foreach (var result in tasks) roomList.Add(result); return (userProfile, roomList); - // StateHasChanged(); + // StateHasChanged(); }).ToAsyncEnumerable(); await foreach (var res in results) { - dmRooms.Add(res.userProfile, res.roomList); - // Status = $"Listed {dmRooms.Count} users"; + SetupData.DMRooms.Add(res.userProfile, res.roomList); + // Status = $"Listed {dmRooms.Count} users"; } + _semaphore.Release(); - var duplicateDmRoomIds = new Dictionary<string, List<UserProfileWithId>>(); - foreach (var (user, rooms) in dmRooms) { + var duplicateDmRoomIds = new Dictionary<string, List<DMSpace.DMSpaceSetupData.UserProfileWithId>>(); + foreach (var (user, rooms) in SetupData.DMRooms) { foreach (var roomInfo in rooms) { if (!duplicateDmRoomIds.ContainsKey(roomInfo.Room.RoomId)) duplicateDmRoomIds.Add(roomInfo.Room.RoomId, new()); duplicateDmRoomIds[roomInfo.Room.RoomId].Add(user); } } + duplicateDmRoomIds.RemoveAll((x, y) => y.Count == 1); foreach (var (roomId, users) in duplicateDmRoomIds) { - duplicateDmRooms.Add(dmRooms.First(x => x.Value.Any(x => x.Room.RoomId == roomId)).Value.First(x => x.Room.RoomId == roomId), users); + duplicateDmRooms.Add(SetupData.DMRooms.First(x => x.Value.Any(x => x.Room.RoomId == roomId)).Value.First(x => x.Room.RoomId == roomId), users); } - // StateHasChanged(); + // StateHasChanged(); Status = null; await base.OnParametersSetAsync(); } @@ -176,34 +183,29 @@ else { } private async Task<RoomInfo> GetRoomInfo(GenericRoom room) { - var roomInfo = new RoomInfo() { - Room = room - }; + var roomInfo = new RoomInfo(room); + await roomInfo.FetchAllStateAsync(); roomMembers[roomInfo] = new(); - roomInfo.CreationEventContent = await room.GetCreateEventAsync(); - try { - roomInfo.RoomName = await room.GetNameAsync(); - } - catch { } + // roomInfo.CreationEventContent = await room.GetCreateEventAsync(); + + if(roomInfo.RoomName == room.RoomId) + try { + roomInfo.RoomName = await room.GetNameOrFallbackAsync(); + } + catch { } var membersEnum = room.GetMembersEnumerableAsync(true); await foreach (var member in membersEnum) if (member.TypedContent is RoomMemberEventContent memberEvent) roomMembers[roomInfo].Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey }); - - if (string.IsNullOrWhiteSpace(roomInfo.RoomName) || roomInfo.RoomName == room.RoomId) { - List<string> displayNames = new List<string>(); - foreach (var member in roomMembers[roomInfo]) - if (!string.IsNullOrWhiteSpace(member.DisplayName)) - displayNames.Add(member.DisplayName); - roomInfo.RoomName = string.Join(", ", displayNames); - } + try { string? roomIcon = (await room.GetAvatarUrlAsync())?.Url; if (room is not null) roomInfo.RoomIcon = roomIcon; } catch { } + return roomInfo; } @@ -214,29 +216,25 @@ else { } private async Task SetRoomAssignment(string roomId, string userId) { - var hs = DmSpace.Homeserver; + var hs = SetupData.Homeserver; Status = "Loading DM list from account data..."; - var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); + var dms = await SetupData.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); Status = "Updating DM list from account data..."; foreach (var (user, rooms) in dms) { rooms.RemoveAll(x => x == roomId); dms[user] = rooms.Distinct().ToList(); } - if(!dms.ContainsKey(userId)) + + if (!dms.ContainsKey(userId)) dms.Add(userId, new()); dms[userId].Add(roomId); - dms.RemoveAll((x, y) => y is {Count: 0}); - await DmSpace.Homeserver.SetAccountDataAsync("m.direct", dms); + dms.RemoveAll((x, y) => y is { Count: 0 }); + await SetupData.Homeserver.SetAccountDataAsync("m.direct", dms); duplicateDmRooms.RemoveAll((x, y) => x.Room.RoomId == roomId); StateHasChanged(); if (duplicateDmRooms.Count == 0) await OnParametersSetAsync(); } - private class UserProfileWithId : UserProfileResponse { - [JsonIgnore] - public string Id { get; set; } - } - -} \ No newline at end of file +} diff --git a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor index 9307f6a..865e956 100644 --- a/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor +++ b/MatrixUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor @@ -18,15 +18,15 @@ <p>@Status</p> } -@if (DmSpace is not null) { - @if (dmSpaceInfo is not null && dmSpaceRoomInfo is not null) { +@if (SetupData is not null) { + @if (SetupData.DMSpaceRoomInfo is not null) { <p> - <InputCheckbox @bind-Value="dmSpaceInfo.LayerByUser"></InputCheckbox> + <InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox> Create sub-spaces per user </p> - @if (!dmSpaceInfo.LayerByUser) { - <RoomListItem RoomInfo="@dmSpaceRoomInfo"></RoomListItem> - @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.RoomName)) { + @if (!SetupData.DmSpaceInfo.LayerByUser) { + <RoomListItem RoomInfo="@SetupData.DMSpaceRoomInfo"></RoomListItem> + @foreach (var (userId, room) in SetupData.DMRooms.OrderBy(x => x.Key.DisplayName)) { @foreach (var roomInfo in room) { <div style="margin-left: 32px;"> <RoomListItem RoomInfo="@roomInfo"></RoomListItem> @@ -35,10 +35,16 @@ } } else { - <RoomListItem RoomInfo="@dmSpaceRoomInfo"></RoomListItem> - @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.RoomName)) { + <RoomListItem RoomInfo="@SetupData.DMSpaceRoomInfo"></RoomListItem> + @foreach (var (user, room) in SetupData.DMRooms.OrderBy(x => x.Key.DisplayName)) { <div style="margin-left: 32px;"> - <RoomListItem RoomInfo="@userId"></RoomListItem> + @{ + RoomInfo fakeRoom = new(SetupData.DMSpaceRoomInfo.Room) { + RoomName = user.DisplayName ?? user.Id, + RoomIcon = user.AvatarUrl + }; + } + <RoomListItem RoomInfo="@fakeRoom"></RoomListItem> </div> @foreach (var roomInfo in room) { <div style="margin-left: 64px;"> @@ -49,11 +55,11 @@ } } else { - <b>Error: dmSpaceInfo is null!</b> + <b>Error: SetupData.DMSpaceRoomInfo is null!</b> } } else { - <b>Error: DmSpaceConfiguration is null!</b> + <b>Error: DMSpaceRootPageConfiguration is null!</b> } <br/> @@ -72,83 +78,75 @@ else { private string? _status; [CascadingParameter] - public DMSpace? DmSpace { get; set; } - - private Dictionary<RoomInfo, List<RoomInfo>> dmRooms { get; set; } = new(); - private DMSpaceInfo? dmSpaceInfo { get; set; } - private RoomInfo? dmSpaceRoomInfo { get; set; } - - protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); - } + public DMSpace.DMSpaceSetupData SetupData { get; set; } SemaphoreSlim _semaphore = new(1, 1); - protected override async Task OnParametersSetAsync() { - if (DmSpace is null) + protected override async Task OnInitializedAsync() { + if (SetupData is null) return; await _semaphore.WaitAsync(); - var hs = DmSpace.Homeserver; - var dmSpaceRoom = new DMSpaceRoom(hs, DmSpace.DmSpaceConfiguration.DMSpaceId); - dmSpaceRoomInfo = new() { - RoomName = await dmSpaceRoom.GetNameAsync(), - CreationEventContent = await dmSpaceRoom.GetCreateEventAsync(), - RoomIcon = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK", - Room = dmSpaceRoom - }; - dmSpaceInfo = await dmSpaceRoom.GetDmSpaceInfo(); - Status = "Loading DM list from account data..."; - var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); - dmRooms.Clear(); + var hs = SetupData.Homeserver; + // var dmSpaceRoom = new DMSpaceRoom(hs, SetupData.DmSpaceConfiguration.DMSpaceId); + // SetupData. + // dmSpaceRoomInfo = new() { + // RoomName = await dmSpaceRoom.GetNameAsync(), + // CreationEventContent = await dmSpaceRoom.GetCreateEventAsync(), + // RoomIcon = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK", + // Room = dmSpaceRoom + // }; + // dmSpaceInfo = await dmSpaceRoom.GetDMSpaceInfo(); + // Status = "Loading DM list from account data..."; + // var dms = await SetupData.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct"); Status = "DM list optimised, fetching info..."; - var results = dms.Select(async x => { - var (userId, rooms) = x; - UserProfileWithId userProfile; - try { - var profile = await DmSpace.Homeserver.GetProfileAsync(userId); - userProfile = new() { - AvatarUrl = profile.AvatarUrl, - Id = userId, - DisplayName = profile.DisplayName - }; - } - catch { - userProfile = new() { - AvatarUrl = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK", - DisplayName = userId, - Id = userId - }; - } - var roomList = new List<RoomInfo>(); - var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable(); - await foreach (var result in tasks) - roomList.Add(result); - return (userProfile, roomList); - }).ToAsyncEnumerable(); - await foreach (var res in results) { - dmRooms.Add(new RoomInfo() { - Room = dmSpaceRoom, - RoomIcon = res.userProfile.AvatarUrl, - RoomName = res.userProfile.DisplayName, - CreationEventContent = await dmSpaceRoom.GetCreateEventAsync() - }, res.roomList); - } + // var results = dms.Select(async x => { + // var (userId, rooms) = x; + // UserProfileWithId userProfile; + // try { + // var profile = await SetupData.Homeserver.GetProfileAsync(userId); + // userProfile = new() { + // AvatarUrl = profile.AvatarUrl, + // Id = userId, + // DisplayName = profile.DisplayName + // }; + // } + // catch { + // userProfile = new() { + // AvatarUrl = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK", + // DisplayName = userId, + // Id = userId + // }; + // } + // var roomList = new List<RoomInfo>(); + // var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable(); + // await foreach (var result in tasks) + // roomList.Add(result); + // return (userProfile, roomList); + // }).ToAsyncEnumerable(); + // await foreach (var res in results) { + // dmRooms.Add(new RoomInfo() { + // Room = dmSpaceRoom, + // RoomIcon = res.userProfile.AvatarUrl, + // RoomName = res.userProfile.DisplayName, + // CreationEventContent = await dmSpaceRoom.GetCreateEventAsync() + // }, res.roomList); + // } + await SetupData.DMSpaceRoomInfo!.FetchAllStateAsync(); _semaphore.Release(); Status = null; await base.OnParametersSetAsync(); } private async Task Execute() { - var hs = DmSpace.Homeserver; - var dmSpaceRoom = new DMSpaceRoom(hs, DmSpace.DmSpaceConfiguration.DMSpaceId); + var hs = SetupData.Homeserver; + var dmSpaceRoom = new DMSpaceRoom(hs, SetupData.DmSpaceConfiguration!.DMSpaceId!); + await dmSpaceRoom.ImportNativeDMs(); NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=3"); } private async Task<RoomInfo> GetRoomInfo(GenericRoom room) { - var roomInfo = new RoomInfo() { - Room = room - }; + var roomInfo = new RoomInfo(room); var roomMembers = new List<UserProfileWithId>(); roomInfo.CreationEventContent = await room.GetCreateEventAsync(); try { @@ -168,12 +166,14 @@ else { displayNames.Add(member.DisplayName); roomInfo.RoomName = string.Join(", ", displayNames); } + try { string? roomIcon = (await room.GetAvatarUrlAsync())?.Url; if (room is not null) roomInfo.RoomIcon = roomIcon; } catch { } + return roomInfo; } diff --git a/MatrixUtils.Web/Pages/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor index 79b83ae..129f706 100644 --- a/MatrixUtils.Web/Pages/User/Profile.razor +++ b/MatrixUtils.Web/Pages/User/Profile.razor @@ -110,8 +110,7 @@ var room = Homeserver.GetRoom(roomId); var roomNameTask = room.GetNameOrFallbackAsync(); var roomIconTask = room.GetAvatarUrlAsync(); - var roomInfo = new RoomInfo() { - Room = room, + var roomInfo = new RoomInfo(room) { OwnMembership = roomProfile }; try { diff --git a/MatrixUtils.Web/Shared/ActivityGraph.razor b/MatrixUtils.Web/Shared/ActivityGraph.razor new file mode 100644 index 0000000..51fb539 --- /dev/null +++ b/MatrixUtils.Web/Shared/ActivityGraph.razor @@ -0,0 +1,148 @@ +@using System.Drawing +@using System.Runtime.InteropServices +@using System.Diagnostics + +@if (Data is { Count: > 0 }) +{ + @* 12*5=60 *@ + <div style="display: grid; grid-template-columns: 35px repeat(60, 1.5em); grid-template-rows: 1.5em repeat(7, 1.5em); gap: 0;"> + @* row 0: month labels with colspan *@ + @* @foreach (var month in Enumerable.Range(1, 12)) *@ + @* { *@ + @* <div style="grid-row: 1; grid-column: @((int)(month * 4.3) + 1);"> *@ + @* <span aria-hidden="true">@(new DateTime(2021, month, 1).ToString("MMM")[..3])</span> *@ + @* </div> *@ + @* } *@ + + @* column 0: day labels *@ + @* @for (var i = 0; i < 7; i++) *@ + @* { *@ + @* <div style="text-align: left; grid-column: 1; grid-row: @(i + 2)"> *@ + @* @(((DayOfWeek)i).ToString()[..3]) *@ + @* </div> *@ + @* } *@ + + + <div style="grid-row: 1; grid-column: 5;">Jan</div> + <div style="grid-row: 1; grid-column: 9;">Feb</div> + <div style="grid-row: 1; grid-column: 13;">Mar</div> + <div style="grid-row: 1; grid-column: 18;">Apr</div> + <div style="grid-row: 1; grid-column: 22;">May</div> + <div style="grid-row: 1; grid-column: 26;">Jun</div> + <div style="grid-row: 1; grid-column: 31;">Jul</div> + <div style="grid-row: 1; grid-column: 35;">Aug</div> + <div style="grid-row: 1; grid-column: 39;">Sep</div> + <div style="grid-row: 1; grid-column: 44;">Oct</div> + <div style="grid-row: 1; grid-column: 48;">Nov</div> + <div style="grid-row: 1; grid-column: 52;">Dec</div> + <div style="text-align: left; grid-column: 1; grid-row: 2">Sun</div> + <div style="text-align: left; grid-column: 1; grid-row: 3">Mon</div> + <div style="text-align: left; grid-column: 1; grid-row: 4">Tue</div> + <div style="text-align: left; grid-column: 1; grid-row: 5">Wed</div> + <div style="text-align: left; grid-column: 1; grid-row: 6">Thu</div> + <div style="text-align: left; grid-column: 1; grid-row: 7">Fri</div> + <div style="text-align: left; grid-column: 1; grid-row: 8">Sat</div> + + + @* pad activity cell dates... *@ + <div style="grid-column: 2; grid-row: 2 / span @((int)(new DateOnly(Data.Keys.First().Year, 1, 1).DayOfWeek));"></div> + + @* the actual activity cells *@ + + @code{ + bool needsBorder = false; + } + + @for (DateOnly date = new DateOnly(Data.Keys.First().Year, 1, 1); date <= new DateOnly(Data.Keys.First().Year, 1, 1).AddYears(1).AddDays(-1); date = date.AddDays(1)) + { + var hasData = Data.TryGetValue(date, out var color); + var needsTopBorder = date.Day == 1 && date.Month != 1 && date.DayOfWeek != DayOfWeek.Sunday; + if (date.DayOfWeek == DayOfWeek.Sunday) + needsBorder = date.AddDays(7).Day <= 7 && date.Month != 12; + var needsLeftBorder = date.Day <= 7; + + <div class="activity-cell-container" + style="grid-row: @((int)date.DayOfWeek + 2); border-@(needsLeftBorder ? "left" : "right"): @(needsBorder ? "2px solid white" : "none"); border-top: @(needsTopBorder ? "2px solid white" : "none");"> + @if (hasData) + { + <div class="activity-cell" + style="background-color: rgb(@(color.R / GlobalMax.R * 255), @(color.G / GlobalMax.G * 255), @(color.B / GlobalMax.B * 255));" + title="@($"{color.R} {RLabel}, {color.G} {GLabel}, and {color.B} {BLabel} on {date.ToString("D")}")"> + </div> + } + else + { + <div class="activity-cell" + title="@($"No data on {date.ToString("D")}")"> + </div> + } + </div> + } + </div> +} + + +@code { + private Dictionary<DateOnly, RGB> _data = new(); + private RGB? _globalMax = null; + + [Parameter] + public Dictionary<DateOnly, RGB> Data + { + get => _data; + set + { + // var sw = Stopwatch.StartNew(); + if (value is not { Count: > 0 }) return; + // Console.WriteLine($"Recalculating activity graph ({value.Count} datapoints)..."); + + + // var year = (int)value.Keys.Average(x => x.Year); + // value = value + // .Where(x => x.Key.Year == year) + // .OrderBy(x => x.Key) + // .ToDictionary(x => x.Key, x => x.Value); + + _data = value; + // Console.WriteLine($"Recalculated activity graph in {sw.Elapsed}"); + // StateHasChanged(); + } + } + + [Parameter] + public RGB GlobalMax + { + get + { + if (_globalMax is not null) return _globalMax.Value; + if (Data is not { Count: > 0 }) return new RGB() { R = 255, G = 255, B = 255 }; + return new RGB() + { + R = Data.Values.Max(x => x.R), + G = Data.Values.Max(x => x.G), + B = Data.Values.Max(x => x.B) + }; + } + set => _globalMax = value; + } + + [Parameter] public string RLabel { get; set; } = "R"; + [Parameter] public string GLabel { get; set; } = "G"; + [Parameter] public string BLabel { get; set; } = "B"; + + [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * 3, Pack = 1)] + public struct RGB() + { + public float R = 0; + public float G = 0; + public float B = 0; + + public RGB(float r, float g, float b) : this() + { + R = r; + G = g; + B = b; + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/ActivityGraph.razor.css b/MatrixUtils.Web/Shared/ActivityGraph.razor.css new file mode 100644 index 0000000..d8e543c --- /dev/null +++ b/MatrixUtils.Web/Shared/ActivityGraph.razor.css @@ -0,0 +1,16 @@ +.activity-cell-container { + width: 100%; + height: 100%; + align-content: center; + justify-content: center; +} + +.activity-cell { + width: 85%; + height: 85%; + border-radius: 5px; +} + +.day-label { + grid-column: 1; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MainLayout.razor b/MatrixUtils.Web/Shared/MainLayout.razor index d8bf411..41c3d69 100644 --- a/MatrixUtils.Web/Shared/MainLayout.razor +++ b/MatrixUtils.Web/Shared/MainLayout.razor @@ -8,8 +8,8 @@ <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> + <a style="color: #ccc; text-decoration: underline" href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/" target="_blank">Git</a> + <a style="color: #ccc; text-decoration: underline" 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"> diff --git a/MatrixUtils.Web/Shared/MxcImage.razor b/MatrixUtils.Web/Shared/MxcImage.razor index f31c19f..e651c3f 100644 --- a/MatrixUtils.Web/Shared/MxcImage.razor +++ b/MatrixUtils.Web/Shared/MxcImage.razor @@ -30,6 +30,7 @@ StateHasChanged(); } } + [Parameter] public RemoteHomeserver? Homeserver { get; set; } @@ -41,7 +42,7 @@ } } - private string StyleString => $"{Style} {(Circular ? "border-radius: 50%;" : "")} {(Width.HasValue ? $"width: {Width}px;" : "")} {(Height.HasValue ? $"height: {Height}px;" : "")}"; + private string StyleString => $"{Style} {(Circular ? "border-radius: 50%;" : "")} {(Width.HasValue ? $"width: {Width}px;" : "")} {(Height.HasValue ? $"height: {Height}px;" : "")} object-fit: cover;"; private static readonly string Prefix = "mxc://"; private static readonly int PrefixLength = Prefix.Length; diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor index 9c481e3..6954990 100644 --- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor @@ -48,9 +48,7 @@ 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 - }; + roomInfo = new RoomInfo(room); KnownRooms.Add(roomInfo); } if(joinedRooms.Any(x=>x.RoomId == room.RoomId)) diff --git a/MatrixUtils.Web/Shared/UserListItem.razor b/MatrixUtils.Web/Shared/UserListItem.razor index 525296e..daa9c9e 100644 --- a/MatrixUtils.Web/Shared/UserListItem.razor +++ b/MatrixUtils.Web/Shared/UserListItem.razor @@ -2,8 +2,9 @@ @using LibMatrix.EventTypes.Spec.State @using LibMatrix.Homeservers @using LibMatrix.Responses +@using ArcaneLibs <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)"/> + <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@(string.IsNullOrWhiteSpace(User?.AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(UserId) : User.AvatarUrl)"/> <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@User?.DisplayName</span> <div style="display: inline-block;"> @@ -27,6 +28,8 @@ private AuthenticatedHomeserverGeneric _homeserver = null!; + private SvgIdenticonGenerator _identiconGenerator = new(); + protected override async Task OnInitializedAsync() { _homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); if (_homeserver is null) return; @@ -35,6 +38,7 @@ if (UserId == null) { throw new ArgumentNullException(nameof(UserId)); } + User = await _homeserver.GetProfileAsync(UserId); } |