From 41c5a84dacfd036b8d8f01f72226ac5a519995e3 Mon Sep 17 00:00:00 2001 From: Rory& Date: Tue, 14 May 2024 17:49:09 +0200 Subject: Organise tools somewhat, set proper icons for nav menu --- .../Client/ClientComponents/ClientRoomList.razor | 15 ++ .../Client/ClientComponents/ClientStatusList.razor | 35 +++ .../Client/ClientComponents/ClientSyncWrapper.cs | 41 ++++ .../Client/ClientComponents/MatrixClient.razor | 31 +++ MatrixUtils.Web/Pages/Labs/Client/Index.razor | 72 +++++++ MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor | 104 +++++++++ .../Labs/DMSpace/DMSpaceStages/DMSpaceStage0.razor | 11 + .../Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor | 151 +++++++++++++ .../Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor | 240 +++++++++++++++++++++ .../Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor | 191 ++++++++++++++++ MatrixUtils.Web/Pages/Labs/Index.razor | 11 + MatrixUtils.Web/Pages/Labs/Index.razor.css | 0 MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor | 87 ++++++++ .../MainTabComponents/MainTabSpaceItem.razor | 56 +++++ .../MainTabComponents/MainTabSpaceItem.razor.css | 29 +++ .../RoomsIndex2ByRoomTypeTab.razor | 53 +++++ .../Index2Components/RoomsIndex2DMsTab.razor | 53 +++++ .../Index2Components/RoomsIndex2MainTab.razor | 206 ++++++++++++++++++ .../Index2Components/RoomsIndex2MainTab.razor.css | 0 .../RoomsIndex2SyncContainer.razor | 201 +++++++++++++++++ 20 files changed, 1587 insertions(+) create mode 100644 MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs create mode 100644 MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Client/Index.razor create mode 100644 MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor create mode 100644 MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage0.razor create mode 100644 MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor create mode 100644 MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor create mode 100644 MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Index.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Index.razor.css create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor.css create mode 100644 MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor (limited to 'MatrixUtils.Web/Pages/Labs') 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) { + + @room.RoomName + +
+} + +@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) { +
+        @ctx.Homeserver.UserId - @ctx.SyncWrapper.Status
+    
+} + +@code { + + [Parameter] + public ObservableCollection Data { get; set; } = null!; + + protected override void OnInitialized() { + Data.CollectionChanged += (_, e) => { + foreach (var item in e.NewItems?.Cast() ?? []) { + 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 AccountData { get; set; } = new(); + public ObservableCollection 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 + +
+
+
+ +
+
+ @if (Data.SelectedRoom != null) { + + + } + else { +

No room selected

+ } +
+ @if (Data.SelectedRoom != null) { +
+ +
+ } +
+
+ +@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 + +

Client

+ + +@foreach (var client in Clients) { + + @client.Homeserver.WhoAmI.UserId + +} + + + +@* @foreach (var client in Clients) { *@ +@*
*@ +@* @client.Homeserver.UserId - @client.SyncWrapper.Status *@ +@*
*@ +@* } *@ + +@if (SelectedClient != null) { +
+ +
+} + +@code { + + private static readonly ObservableCollection 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 +

DM Space Management

+
+ + @switch (Stage) { + case -1: +

Initialising...

+ break; + case 0: + + break; + case 1: + + break; + case 2: + + break; + case 3: + + break; + default: +

Stage is unknown value: @Stage!

+ break; + } +
+ +@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("gay.rory.dm_space"); + var room = SetupData.Homeserver.GetRoom(SetupData.DmSpaceConfiguration.DMSpaceId); + await room.GetStateAsync(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? Spaces; + + public Dictionary>? 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 @@ + + Welcome to the DM Space tool! + +

This wizard will help you set up a DM space.

+

This is useful for eg. sharing DM rooms across multiple accounts.

+
+Get started + +@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 + + DM Space setup tool - stage 1: Configure space + +

You will need a space to use for DM rooms.

+@if (SetupData is not null) { + if (SetupData.Spaces is not null) { +

+ Selected space: + + + @foreach (var (id, roomInfo) in SetupData.Spaces) { + + } + +

+

+ + Create sub-spaces per user +

+ +
+ Disband + Next + } + else { +

Discovering spaces, please wait...

+ } +} +else { + Error: Setup data is null! +} + + +@if (!string.IsNullOrWhiteSpace(Status)) { +

@Status

+} + +@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 spaces = []; + SetupData.DmSpaceConfiguration ??= new(); + + Status = "Looking for spaces..."; + var userRoomsEnum = SetupData.Homeserver!.GetJoinedRoomsByType("m.space"); + + List 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.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 + + DM Space setup tool - stage 2: Fix DM room attribution + +

This is just to make sure that your DMs are attributed to the right person!

+ +@if (!string.IsNullOrWhiteSpace(Status)) { +

@Status

+} + +@if (SetupData is not null) { + if (SetupData.DMRooms is { Count: > 0 }) { + @foreach (var (userId, room) in SetupData.DMRooms.OrderBy(x => x.Key.Id)) { + + @foreach (var roomInfo in room) { + + Reassign + + } + } + } + else { +

DM room list is loading, please wait...

+ } +} +else { + Error: DMSpaceRootPage is null! +} + +
+Next + +@{ + var _offset = 0; +} +@foreach (var (room, usersList) in duplicateDmRooms) { + +

Found room assigned to multiple users:

+

Users:

+ @foreach (var userProfileResponse in usersList) { + + Assign to + + +
+ } +
+} + +@if (DmToReassign is not null) { + + + @foreach (var userProfileResponse in roomMembers[DmToReassign]) { + + Assign to + + +
+ } +
+} + +@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> duplicateDmRooms { get; set; } = new(); + private Dictionary> 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>>("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>(); + + 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(); + 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>(); + 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 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> GetRoomInfoForRooms(List 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>>("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 + + + DM Space setup tool - stage 3: Preview space layout + +

This gives you a preview of how your settings would impact layout!

+ +@if (!string.IsNullOrWhiteSpace(Status)) { +

@Status

+} + +@if (SetupData is not null) { + @if (SetupData.DMSpaceRoomInfo is not null) { +

+ + Create sub-spaces per user +

+ @if (!SetupData.DmSpaceInfo.LayerByUser) { + + @foreach (var (userId, room) in SetupData.DMRooms.OrderBy(x => x.Key.DisplayName)) { + @foreach (var roomInfo in room) { +
+ +
+ } + } + } + else { + + @foreach (var (user, room) in SetupData.DMRooms.OrderBy(x => x.Key.DisplayName)) { +
+ @{ + RoomInfo fakeRoom = new(SetupData.DMSpaceRoomInfo.Room) { + RoomName = user.DisplayName ?? user.Id, + RoomIcon = user.AvatarUrl + }; + } + +
+ @foreach (var roomInfo in room) { +
+ +
+ } + } + } + } + else { + Error: SetupData.DMSpaceRoomInfo is null! + } +} +else { + Error: DMSpaceRootPageConfiguration is null! +} + +
+Next + +@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>>("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(); + // 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 GetRoomInfo(GenericRoom room) { + var roomInfo = new RoomInfo(room); + var roomMembers = new List(); + 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 displayNames = new List(); + 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> GetRoomInfoForRooms(List 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" +

Index of /Labs

+

Welcome to RMU Laboratories! We wish you a safe and informative time!

+

These pages are a work in progress, and may not work or cause permanent account changes!

+

We do not claim responsibility in case something goes wrong here!

+

Here be dragons!!

+
+ +Room List v3
+Client implementation attempt
+DM Space setup
\ 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 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 logger +

Room list

+ + +@if (Data.Homeserver is null || Data.GlobalProfile is null) { +

Creating homeserver instance and fetching global profile...

+ return; +} + +
+ Main + DMs + By room type +
+
+ + @switch (SelectedTab) { + case Tab.Main: +

Main tab

+ + break; + case Tab.DMs: +

DMs tab

+ + break; + case Tab.ByRoomType: +

By room type tab

+ + break; + default: + throw new InvalidEnumArgumentException(); + } +
+
+ +@* Create new room *@ + + +@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 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 +
+
+ @if (IsSpaceOpened()) { + + } + else { + + } + + + @Space.RoomName +
+ @if (IsSpaceOpened()) { + meow + } +
+ +@code { + + [Parameter] + public RoomInfo Space { get; set; } + + [Parameter] + public RoomInfo SelectedSpace { get; set; } + + [Parameter] + public EventCallback SelectedSpaceChanged { get; set; } + + [Parameter] + public List 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 +

RoomsIndex2MainTab

+ +
+
+
+ Uncategorised rooms + @foreach (var space in Data.Rooms.Where(x => x.RoomType == "m.space")) { +
+

@space.RoomName

+
+ } +
+
+

omae wa mou shindeiru

+
+
+
+ +@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 +

RoomsIndex2MainTab

+ +
+
+
+ Uncategorised rooms + @foreach (var space in Data.Rooms.Where(x => x.RoomType == "m.space")) { +
+

@space.RoomName

+
+ } +
+
+

omae wa mou shindeiru

+
+
+
+ +@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 +

RoomsIndex2MainTab

+ +@*
*@ +@*
*@ +@*
*@ +@* Uncategorised rooms *@ +@* @foreach (var space in GetTopLevelSpaces()) { *@ +@* *@ +@*
*@ +@*
@space.RoomName
*@ +@*
*@ +@*
*@ +@* } *@ +@*
*@ +@*
*@ +@*

Placeholder for rooms list...

*@ +@*
*@ +@*
*@ +@*
*@ + +
+
+
+ Uncategorised rooms + @foreach (var space in GetTopLevelSpaces()) { + @* @RecursingSpaceChildren(space) *@ + + } +
+
+

Placeholder for rooms list...

+ @if (SelectedSpace != null) { + foreach (var room in GetSpaceChildRooms(SelectedSpace)) { +

@room.RoomName

+ } + } +
+
+
+ + +@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 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 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 GetSpaceChildSpaces(RoomInfo space) { + var children = GetSpaceChildren(space); + var childSpaces = children.Where(x => x.RoomType == "m.space").ToList(); + return childSpaces; + } + + private List 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 OpenedSpaces { get; set; } = new List(); + + // private RenderFragment RecursingSpaceChildren(RoomInfo space, List? 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(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(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 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 logger +
RoomsIndex2SyncContainer
+@foreach (var (name, value) in _statusList) { +
[@name] @value.Status
+} + +@code { + + [Parameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + private SyncHelper syncHelper; + + private Queue> 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() { + ["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 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 -- cgit 1.5.1