diff options
author | Rory& <root@rory.gay> | 2024-05-14 17:49:09 +0200 |
---|---|---|
committer | Rory& <root@rory.gay> | 2024-05-14 17:49:09 +0200 |
commit | 41c5a84dacfd036b8d8f01f72226ac5a519995e3 (patch) | |
tree | a4bfc76541692cbbb0fc18f34463cf31a57440f5 /MatrixUtils.Web/Pages/Labs | |
parent | Improve the heatmap layout (diff) | |
download | MatrixUtils-41c5a84dacfd036b8d8f01f72226ac5a519995e3.tar.xz |
Organise tools somewhat, set proper icons for nav menu
Diffstat (limited to 'MatrixUtils.Web/Pages/Labs')
20 files changed, 1587 insertions, 0 deletions
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor new file mode 100644 index 0000000..b370080 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor @@ -0,0 +1,15 @@ +@using ClientContext = MatrixUtils.Web.Pages.Labs.Client.Index.ClientContext +@* user header and room list *@ +@foreach (var room in Data.SyncWrapper.Rooms) { + <LinkButton OnClick="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")"> + @room.RoomName + </LinkButton> + <br/> +} + +@code { + + [Parameter] + public ClientContext Data { get; set; } = null!; + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor new file mode 100644 index 0000000..c680c13 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor @@ -0,0 +1,35 @@ +@using ClientContext = MatrixUtils.Web.Pages.Labs.Client.Index.ClientContext; +@using System.Collections.ObjectModel + +@foreach (var ctx in Data) { + <pre> + @ctx.Homeserver.UserId - @ctx.SyncWrapper.Status + </pre> +} + +@code { + + [Parameter] + public ObservableCollection<ClientContext> Data { get; set; } = null!; + + protected override void OnInitialized() { + Data.CollectionChanged += (_, e) => { + foreach (var item in e.NewItems?.Cast<ClientContext>() ?? []) { + item.SyncWrapper.PropertyChanged += (_, pe) => { + if (pe.PropertyName == nameof(item.SyncWrapper.Status)) + StateHasChanged(); + }; + } + + StateHasChanged(); + }; + + Data.ToList().ForEach(ctx => { + ctx.SyncWrapper.PropertyChanged += (_, pe) => { + if (pe.PropertyName == nameof(ctx.SyncWrapper.Status)) + StateHasChanged(); + }; + }); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs new file mode 100644 index 0000000..16051b8 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs @@ -0,0 +1,41 @@ +using System.Collections.ObjectModel; +using ArcaneLibs; +using LibMatrix; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using MatrixUtils.Abstractions; + +namespace MatrixUtils.Web.Pages.Client.ClientComponents; + +public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : NotifyPropertyChanged { + private SyncHelper _syncHelper = new SyncHelper(homeserver) { + MinimumDelay = TimeSpan.FromMilliseconds(2000), + IsInitialSync = false + }; + private string _status = "Loading..."; + + public ObservableCollection<StateEvent> AccountData { get; set; } = new(); + public ObservableCollection<RoomInfo> Rooms { get; set; } = new(); + + public string Status { + get => _status; + set => SetField(ref _status, value); + } + + public async Task Start() { + Task.Yield(); + var resp = _syncHelper.EnumerateSyncAsync(); + Status = $"[{DateTime.Now:s}] Syncing..."; + await foreach (var response in resp) { + Task.Yield(); + Status = $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates"; + await HandleSyncResponse(response); + await Task.Yield(); + } + } + + private async Task HandleSyncResponse(SyncResponse resp) { + + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor new file mode 100644 index 0000000..7d3e52a --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor @@ -0,0 +1,31 @@ +@using Index = MatrixUtils.Web.Pages.Labs.Client.Index +@using MatrixUtils.Web.Pages.Client.ClientComponents + +<div class="container-fluid"> + <div class="row"> + <div class="col-3"> + <ClientRoomList Data="@Data"/> + </div> + <div class="col-6"> + @if (Data.SelectedRoom != null) { + <Index.RoomHeader Data="@Data"/> + <Index.RoomTimeline Data="@Data"/> + } + else { + <p>No room selected</p> + } + </div> + @if (Data.SelectedRoom != null) { + <div class="col-3"> + <Index.UserList Data="@Data"/> + </div> + } + </div> +</div> + +@code { + + [Parameter] + public Index.ClientContext Data { get; set; } = null!; + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Client/Index.razor b/MatrixUtils.Web/Pages/Labs/Client/Index.razor new file mode 100644 index 0000000..5b489b0 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Client/Index.razor @@ -0,0 +1,72 @@ +@page "/Labs/Client" +@using LibMatrix +@using MatrixUtils.Abstractions +@using MatrixUtils.Web.Pages.Client.ClientComponents +@using System.Collections.ObjectModel + +<h3>Client</h3> + + +@foreach (var client in Clients) { + <LinkButton Color="@(SelectedClient == client ? "#ff00ff" : "")" OnClick="@(async () => SelectedClient = client)"> + @client.Homeserver.WhoAmI.UserId + </LinkButton> +} +<ClientStatusList Data="@Clients"></ClientStatusList> + + +@* @foreach (var client in Clients) { *@ +@* <div class="card"> *@ +@* <span>@client.Homeserver.UserId - @client.SyncWrapper.Status</span> *@ +@* </div> *@ +@* } *@ + +@if (SelectedClient != null) { + <div class="card"> + <MatrixClient Data="@SelectedClient"/> + </div> +} + +@code { + + private static readonly ObservableCollection<ClientContext> Clients = []; + private static ClientContext _selectedClient; + + private ClientContext SelectedClient { + get => _selectedClient; + set { + _selectedClient = value; + StateHasChanged(); + } + } + + protected override async Task OnInitializedAsync() { + var tokens = await RMUStorage.GetAllTokens(); + var tasks = tokens.Select(async token => { + try { + var cc = new ClientContext() { + Homeserver = await RMUStorage.GetSession(token) + }; + cc.SyncWrapper = new ClientSyncWrapper(cc.Homeserver); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + cc.SyncWrapper.Start(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + Clients.Add(cc); + StateHasChanged(); + } + catch { } + }).ToList(); + await Task.WhenAll(tasks); + } + + public class ClientContext { + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + public ClientSyncWrapper SyncWrapper { get; set; } + + public RoomInfo? SelectedRoom { get; set; } + } + +} + diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor new file mode 100644 index 0000000..c0dc8a6 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor @@ -0,0 +1,104 @@ +@page "/Labs/DMSpace/Setup" +@using LibMatrix +@using LibMatrix.Responses +@using MatrixUtils.Abstractions +@using MatrixUtils.LibDMSpace +@using MatrixUtils.LibDMSpace.StateEvents +@using MatrixUtils.Web.Pages.Labs.DMSpace.DMSpaceStages +@using System.Text.Json.Serialization +<h3>DM Space Management</h3> +<hr/> +<CascadingValue Value="@SetupData"> + @switch (Stage) { + case -1: + <p>Initialising...</p> + break; + case 0: + <DMSpaceStage0/> + break; + case 1: + <DMSpaceStage1/> + break; + case 2: + <DMSpaceStage2/> + break; + case 3: + <DMSpaceStage3/> + break; + default: + <p>Stage is unknown value: @Stage!</p> + break; + } +</CascadingValue> + +@code { + private int _stage = -1; + + [Parameter, SupplyParameterFromQuery(Name = "stage")] + public int Stage { + get => _stage; + set { + _stage = value; + Console.WriteLine($"Stage is now {value}"); + StateHasChanged(); + } + } + + + public DMSpace? DMSpaceRootPage { get; set; } + + protected override async Task OnInitializedAsync() { + if (NavigationManager.Uri.Contains("?stage=")) { + NavigationManager.NavigateTo(NavigationManager.Uri.Replace("stage=", ""), true); //"/User/DMSpace/Setup" + } + DMSpaceRootPage = this; + SetupData.Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); + if (SetupData.Homeserver is null) return; + try { + 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 is "M_NOT_FOUND" or "M_FORBIDDEN") { + Stage = 0; + SetupData.DmSpaceConfiguration = new(); + } + else throw; + } + finally { + StateHasChanged(); + } + await base.OnInitializedAsync(); + } + + protected override async Task OnParametersSetAsync() { + StateHasChanged(); + 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/Labs/DMSpace/DMSpaceStages/DMSpaceStage0.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage0.razor new file mode 100644 index 0000000..5f6508c --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage0.razor @@ -0,0 +1,11 @@ +<b> + <u>Welcome to the DM Space tool!</u> +</b> +<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/Setup?stage=1">Get started</LinkButton> + +@code { + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor new file mode 100644 index 0000000..2176467 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor @@ -0,0 +1,151 @@ +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +@using LibMatrix +@using LibMatrix.Responses +@using MatrixUtils.LibDMSpace +@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 (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: Setup data is null!</b> +} + + +@if (!string.IsNullOrWhiteSpace(Status)) { + <p>@Status</p> +} + +@code { + + private string? Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + private string? _status; + + [CascadingParameter] + public DMSpace.DMSpaceSetupData SetupData { get; set; } + + SemaphoreSlim _semaphoreSlim = new(1, 1); + + protected override async Task OnInitializedAsync() { + if (SetupData is null) + return; + + await _semaphoreSlim.WaitAsync(); + + 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(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 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, RoomInfo roomInfo)?> GetFeasibleSpaces(GenericRoom room) { + try { + 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; + } + + 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)); + } + } + + 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/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor new file mode 100644 index 0000000..a70e9c5 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor @@ -0,0 +1,240 @@ +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@using MatrixUtils.LibDMSpace +@using MatrixUtils.LibDMSpace.StateEvents +@using ArcaneLibs.Extensions +@using System.Text.Json.Serialization +@using MatrixUtils.Abstractions +<b> + <u>DM Space setup tool - stage 2: Fix DM room attribution</u> +</b> +<p>This is just to make sure that your DMs are attributed to the right person!</p> + +@if (!string.IsNullOrWhiteSpace(Status)) { + <p>@Status</p> +} + +@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: DMSpaceRootPage is null!</b> +} + +<br/> +<LinkButton OnClick="@Execute">Next</LinkButton> + +@{ + var _offset = 0; +} +@foreach (var (room, usersList) in duplicateDmRooms) { + <ModalWindow Title="Duplicate room found" X="_offset += 30" Y="_offset"> + <p>Found room assigned to multiple users: <RoomListItem RoomInfo="@room"></RoomListItem></p> + <p>Users:</p> + @foreach (var userProfileResponse in usersList) { + <LinkButton OnClick="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))"> + <span>Assign to </span> + <InlineUserItem User="userProfileResponse"></InlineUserItem> + </LinkButton> + <br/> + } + </ModalWindow> +} + +@if (DmToReassign is not null) { + <ModalWindow Title="Re-assign DM" OnCloseClicked="@(() => DmToReassign = null)"> + <RoomListItem RoomInfo="@DmToReassign"></RoomListItem> + @foreach (var userProfileResponse in roomMembers[DmToReassign]) { + <LinkButton OnClick="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))"> + <span>Assign to </span> + <InlineUserItem User="userProfileResponse"></InlineUserItem> + </LinkButton> + <br/> + } + </ModalWindow> +} + +@code { + + private string newMxid { get; set; } = ""; + + private RoomInfo? DmToReassign { + get => _dmToReassign; + set { + _dmToReassign = value; + StateHasChanged(); + } + } + + private string? Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + private string? _status; + private RoomInfo? _dmToReassign; + + [CascadingParameter] + public DMSpace.DMSpaceSetupData SetupData { get; set; } + + 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 OnInitializedAsync() { + if (SetupData is null) + return; + await _semaphore.WaitAsync(); + DmToReassign = null; + var hs = SetupData.Homeserver; + Status = "Loading DM list from account data..."; + 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) { + for (var i = rooms.Count - 1; i >= 0; i--) { + var roomId = rooms[i]; + if (!joinedRooms.Contains(roomId)) + rooms.RemoveAt(i); + } + + dms[user] = rooms.Distinct().ToList(); + } + + 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; + DMSpace.DMSpaceSetupData.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); + // StateHasChanged(); + }).ToAsyncEnumerable(); + await foreach (var res in results) { + SetupData.DMRooms.Add(res.userProfile, res.roomList); + // Status = $"Listed {dmRooms.Count} users"; + } + + _semaphore.Release(); + 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(SetupData.DMRooms.First(x => x.Value.Any(x => x.Room.RoomId == roomId)).Value.First(x => x.Room.RoomId == roomId), users); + } + + // StateHasChanged(); + Status = null; + await base.OnParametersSetAsync(); + } + + private async Task Execute() { + NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=3"); + } + + private async Task<RoomInfo> GetRoomInfo(GenericRoom room) { + var roomInfo = new RoomInfo(room); + await roomInfo.FetchAllStateAsync(); + roomMembers[roomInfo] = new(); + // 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 }); + + try { + string? roomIcon = (await room.GetAvatarUrlAsync())?.Url; + if (room is not null) + roomInfo.RoomIcon = roomIcon; + } + catch { } + + return roomInfo; + } + + private async Task<List<RoomInfo>> GetRoomInfoForRooms(List<GenericRoom> rooms) { + var tasks = rooms.Select(GetRoomInfo).ToList(); + await Task.WhenAll(tasks); + return tasks.Select(x => x.Result).ToList(); + } + + private async Task SetRoomAssignment(string roomId, string userId) { + var hs = SetupData.Homeserver; + Status = "Loading DM list from account data..."; + 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)) + dms.Add(userId, new()); + dms[userId].Add(roomId); + 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(); + } + +} diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor new file mode 100644 index 0000000..865e956 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor @@ -0,0 +1,191 @@ +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Responses +@using MatrixUtils.LibDMSpace +@using MatrixUtils.LibDMSpace.StateEvents +@using ArcaneLibs.Extensions +@using System.Text.Json.Serialization +@using MatrixUtils.Abstractions + +<b> + <u>DM Space setup tool - stage 3: Preview space layout</u> +</b> +<p>This gives you a preview of how your settings would impact layout!</p> + +@if (!string.IsNullOrWhiteSpace(Status)) { + <p>@Status</p> +} + +@if (SetupData is not null) { + @if (SetupData.DMSpaceRoomInfo is not null) { + <p> + <InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox> + Create sub-spaces per user + </p> + @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> + </div> + } + } + } + else { + <RoomListItem RoomInfo="@SetupData.DMSpaceRoomInfo"></RoomListItem> + @foreach (var (user, room) in SetupData.DMRooms.OrderBy(x => x.Key.DisplayName)) { + <div style="margin-left: 32px;"> + @{ + 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;"> + <RoomListItem RoomInfo="@roomInfo"></RoomListItem> + </div> + } + } + } + } + else { + <b>Error: SetupData.DMSpaceRoomInfo is null!</b> + } +} +else { + <b>Error: DMSpaceRootPageConfiguration is null!</b> +} + +<br/> +<LinkButton OnClick="@Execute">Next</LinkButton> + +@code { + + private string? Status { + get => _status; + set { + _status = value; + StateHasChanged(); + } + } + + private string? _status; + + [CascadingParameter] + public DMSpace.DMSpaceSetupData SetupData { get; set; } + + SemaphoreSlim _semaphore = new(1, 1); + + protected override async Task OnInitializedAsync() { + if (SetupData is null) + return; + await _semaphore.WaitAsync(); + 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 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 = 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); + var roomMembers = new List<UserProfileWithId>(); + roomInfo.CreationEventContent = await room.GetCreateEventAsync(); + try { + roomInfo.RoomName = await room.GetNameAsync(); + } + catch { } + + var membersEnum = room.GetMembersEnumerableAsync(true); + await foreach (var member in membersEnum) + if (member.TypedContent is RoomMemberEventContent memberEvent) + roomMembers.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) + 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; + } + + private async Task<List<RoomInfo>> GetRoomInfoForRooms(List<GenericRoom> rooms) { + var tasks = rooms.Select(GetRoomInfo).ToList(); + await Task.WhenAll(tasks); + return tasks.Select(x => x.Result).ToList(); + } + + private class UserProfileWithId : UserProfileResponse { + [JsonIgnore] + public string Id { get; set; } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Index.razor b/MatrixUtils.Web/Pages/Labs/Index.razor new file mode 100644 index 0000000..fbe4b62 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Index.razor @@ -0,0 +1,11 @@ +@page "/Labs" +<h3>Index of /Labs</h3> +<p>Welcome to RMU Laboratories! We wish you a safe and informative time!</p> +<p>These pages are a work in progress, and may not work <b>or cause permanent account changes!</b></p> +<p>We do not claim responsibility in case something goes wrong here!</p> +<p><b style="color: red;">Here be dragons!!</b></p> +<br/> + +<a href="/Labs/Rooms2">Room List v3</a><br/> +<a href="/Labs/Client">Client implementation attempt</a><br/> +<a href="/Labs/DMSpace/Setup">DM Space setup</a><br/> \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Index.razor.css b/MatrixUtils.Web/Pages/Labs/Index.razor.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Index.razor.css diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor new file mode 100644 index 0000000..3392960 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor @@ -0,0 +1,87 @@ +@page "/Labs/Rooms2" +@using LibMatrix.Responses +@using System.Collections.ObjectModel +@using System.ComponentModel +@using MatrixUtils.Abstractions +@using MatrixUtils.Web.Pages.Labs.Rooms2.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> + <RoomsIndex2DMsTab></RoomsIndex2DMsTab> + break; + case Tab.ByRoomType: + <h3>By room type tab</h3> + <RoomsIndex2ByRoomTypeTab></RoomsIndex2ByRoomTypeTab> + 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/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor new file mode 100644 index 0000000..6483f01 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor @@ -0,0 +1,56 @@ +@using MatrixUtils.Abstractions +<div class="spaceListItem" style="@(SelectedSpace == Space ? "background-color: #FFFFFF33;" : "")" onclick="@SelectSpace"> + <div class="spaceListItemContainer"> + @if (IsSpaceOpened()) { + <span onclick="@ToggleSpace">▼ </span> + } + else { + <span onclick="@ToggleSpace">▶ </span> + } + + <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage> + <span class="spaceNameEllipsis">@Space.RoomName</span> + </div> + @if (IsSpaceOpened()) { + <span>meow</span> + } +</div> + +@code { + + [Parameter] + public RoomInfo Space { get; set; } + + [Parameter] + public RoomInfo SelectedSpace { get; set; } + + [Parameter] + public EventCallback<RoomInfo> SelectedSpaceChanged { 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); + } + } + + public void SelectSpace() { + SelectedSpace = Space; + SelectedSpaceChanged.InvokeAsync(Space); + } + + public bool IsSpaceOpened() { + return OpenedSpaces.Contains(Space); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css new file mode 100644 index 0000000..d6e413f --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css @@ -0,0 +1,29 @@ +.spaceNameEllipsis { + padding-left: 8px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + width: calc(100% - 64px); +} + +.spaceListItem { + display: block; + width: 100%; + height: 3em; +} + +.spaceListItemContainer { + display: flex; + align-items: center; + vertical-align: center; + justify-content: space-between; + padding: 0 16px; + width: 100%; + height: 100%; +} + +.spaceListItem > img { + display: inline-block; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor new file mode 100644 index 0000000..f4cf849 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.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/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor new file mode 100644 index 0000000..f4cf849 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/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/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor new file mode 100644 index 0000000..7ccfae2 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor @@ -0,0 +1,206 @@ +@using MatrixUtils.Abstractions +@using System.ComponentModel +@using LibMatrix.EventTypes.Spec.State +@using MatrixUtils.Web.Pages.Labs.Rooms2.Index2Components.MainTabComponents +<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) *@ + <MainTabSpaceItem Space="space" OpenedSpaces="OpenedSpaces" @bind-SelectedSpace="SelectedSpace" /> + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>Placeholder for rooms list...</p> + @if (SelectedSpace != null) { + foreach (var room in GetSpaceChildRooms(SelectedSpace)) { + <p>@room.RoomName</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 List<RoomInfo> GetSpaceChildRooms(RoomInfo space) { + var children = GetSpaceChildren(space); + var childRooms = children.Where(x => x.RoomType != "m.space").ToList(); + return childRooms; + } + + 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/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor.css b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor.css diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor new file mode 100644 index 0000000..91f228d --- /dev/null +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor @@ -0,0 +1,201 @@ +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using MatrixUtils.Abstractions +@using System.Diagnostics +@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.NamedCaches.FilterCache.GetOrSetValueAsync(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.NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetSpaceRelations), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + }); + + syncHelpers.Add("Profile", new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(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 |