diff options
Diffstat (limited to 'MatrixUtils.Web/Pages/Rooms/Index2Components')
6 files changed, 495 insertions, 0 deletions
diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor new file mode 100644 index 0000000..4216824 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor @@ -0,0 +1,27 @@ +@using MatrixUtils.Abstractions +<div class="spaceListItem" onclick="@ToggleSpace"> + <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage> + <span class="spaceNameEllipsis">@Space.RoomName</span> +</div> + +@code { + + [Parameter] + public RoomInfo Space { get; set; } + + [Parameter] + public List<RoomInfo> OpenedSpaces { get; set; } + + protected override Task OnInitializedAsync() { + Space.PropertyChanged += (sender, args) => { StateHasChanged(); }; + return base.OnInitializedAsync(); + } + + public void ToggleSpace() { + if (OpenedSpaces.Contains(Space)) { + OpenedSpaces.Remove(Space); + } else { + OpenedSpaces.Add(Space); + } + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css new file mode 100644 index 0000000..c174567 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/MainTabComponents/MainTabSpaceItem.razor.css @@ -0,0 +1,15 @@ +.spaceNameEllipsis { + padding-left: 8px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + width: calc(100% - 38px); +} + +.spaceListItem { + display: block; + width: 100%; + height: 50px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor new file mode 100644 index 0000000..f4cf849 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2DMsTab.razor @@ -0,0 +1,53 @@ +@using MatrixUtils.Abstractions +@using System.Security.Cryptography +@using ArcaneLibs.Extensions +<h3>RoomsIndex2MainTab</h3> + +<div> + <div class="row"> + <div class="col-3" style="background-color: #ffffff66;"> + <LinkButton>Uncategorised rooms</LinkButton> + @foreach (var space in Data.Rooms.Where(x => x.RoomType == "m.space")) { + <div style="@("width: 100%; height: 50px; background-color: #" + RandomNumberGenerator.GetBytes(3).Append((byte)0x11).ToArray().AsHexString().Replace(" ",""))"> + <p>@space.RoomName</p> + </div> + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>omae wa mou shindeiru</p> + </div> + </div> +</div> + +@code { + + [CascadingParameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + Data.Rooms.CollectionChanged += (sender, args) => { + DebouncedStateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var newItem in args.NewItems) { + (newItem as RoomInfo).PropertyChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + }; + await base.OnInitializedAsync(); + } + + //debounce StateHasChanged, we dont want to reredner on every key stroke + + private CancellationTokenSource _debounceCts = new CancellationTokenSource(); + + private async Task DebouncedStateHasChanged() { + _debounceCts.Cancel(); + _debounceCts = new CancellationTokenSource(); + try { + await Task.Delay(100, _debounceCts.Token); + Console.WriteLine("DebouncedStateHasChanged - Calling StateHasChanged!"); + StateHasChanged(); + } + catch (TaskCanceledException) { } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor new file mode 100644 index 0000000..2b7c5ac --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor @@ -0,0 +1,198 @@ +@using MatrixUtils.Abstractions +@using System.Security.Cryptography +@using ArcaneLibs.Extensions +@using System.ComponentModel +@using System.Diagnostics +@using LibMatrix.EventTypes.Spec.State +@using MatrixUtils.Web.Pages.Rooms.Index2Components.MainTabComponents +@using Microsoft.AspNetCore.Components.Rendering +<h3>RoomsIndex2MainTab</h3> + +@* <div> *@ +@* <div class="row"> *@ +@* <div class="col-3" style="background-color: #ffffff66;"> *@ +@* <LinkButton>Uncategorised rooms</LinkButton> *@ +@* @foreach (var space in GetTopLevelSpaces()) { *@ +@* <a style="@("display:block; width: 100%; height: 50px; background-color: #" + RandomNumberGenerator.GetBytes(3).Append((byte)0x11).ToArray().AsHexString().Replace(" ", ""))"> *@ +@* <div style="vertical-align: middle;"> *@ +@* <div style="overflow:hidden; text-overflow: ellipsis; white-space: nowrap; ">@space.RoomName</div> *@ +@* </div> *@ +@* </a> *@ +@* } *@ +@* </div> *@ +@* <div class="col-9" style="background-color: #ff00ff66;"> *@ +@* <p>Placeholder for rooms list...</p> *@ +@* </div> *@ +@* </div> *@ +@* </div> *@ + +<div> + <div class="row"> + <div class="col-3" style="background-color: #ffffff22;"> + <LinkButton>Uncategorised rooms</LinkButton> + @foreach (var space in GetTopLevelSpaces()) { + @RecursingSpaceChildren(space) + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>Placeholder for rooms list...</p> + </div> + </div> +</div> + + +@code { + + [CascadingParameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + Data.Rooms.CollectionChanged += (sender, args) => { + DebouncedStateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var newItem in args.NewItems) { + (newItem as RoomInfo).PropertyChanged += OnRoomListChanged; + (newItem as RoomInfo).StateEvents.CollectionChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + }; + foreach (var newItem in Data.Rooms) { + newItem.PropertyChanged += OnRoomListChanged; + newItem.StateEvents.CollectionChanged += (sender, args) => { DebouncedStateHasChanged(); }; + } + + await base.OnInitializedAsync(); + StateHasChanged(); + } + + private void OnRoomListChanged(object? sender, PropertyChangedEventArgs e) { + if (e.PropertyName == "RoomName" || e.PropertyName == "RoomType") + DebouncedStateHasChanged(); + } + + private CancellationTokenSource _debounceCts = new CancellationTokenSource(); + + private async Task DebouncedStateHasChanged() { + _debounceCts.Cancel(); + _debounceCts = new CancellationTokenSource(); + try { + Console.WriteLine("DebouncedStateHasChanged - Waiting 50ms..."); + await Task.Delay(50, _debounceCts.Token); + Console.WriteLine("DebouncedStateHasChanged - Calling StateHasChanged!"); + StateHasChanged(); + } + catch (TaskCanceledException) { } + } + + private List<RoomInfo> GetTopLevelSpaces() { + var spaces = Data.Rooms.Where(x => x.RoomType == "m.space").OrderBy(x => x.RoomName).ToList(); + var allSpaceChildEvents = spaces.SelectMany(x => x.StateEvents.Where(y => + y.Type == SpaceChildEventContent.EventId && + y.RawContent!.Count > 0 + )).ToList(); + + Console.WriteLine($"Child count: {allSpaceChildEvents.Count}"); + + spaces.RemoveAll(x => allSpaceChildEvents.Any(y => y.StateKey == x.Room.RoomId)); + + if (allSpaceChildEvents.Count == 0) { + Console.WriteLine("No space children found, returning nothing..."); + return []; + } + + return spaces.ToList(); + } + + private List<RoomInfo> GetSpaceChildren(RoomInfo space) { + var childEvents = space.StateEvents.Where(x => + x.Type == SpaceChildEventContent.EventId && + x.RawContent!.Count > 0 + ).ToList(); + var children = childEvents.Select(x => Data.Rooms.FirstOrDefault(y => y.Room.RoomId == x.StateKey)).Where(x => x is not null).ToList(); + return children; + } + + private List<RoomInfo> GetSpaceChildSpaces(RoomInfo space) { + var children = GetSpaceChildren(space); + var childSpaces = children.Where(x => x.RoomType == "m.space").ToList(); + return childSpaces; + } + + private RoomInfo? SelectedSpace { get; set; } + private List<RoomInfo> OpenedSpaces { get; set; } = new List<RoomInfo>(); + + private RenderFragment RecursingSpaceChildren(RoomInfo space, List<RoomInfo>? parents = null, int depth = 0) { + parents ??= []; + var totalSw = Stopwatch.StartNew(); + var children = GetSpaceChildSpaces(space); + + var randomColor = RandomNumberGenerator.GetBytes(3).Append((byte)0x33).ToArray().AsHexString().Replace(" ", ""); + var isExpanded = OpenedSpaces.Contains(space); + + // Console.WriteLine($"RecursingSpaceChildren::FetchData - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + + // var renderSw = Stopwatch.StartNew(); + var rf = new RenderFragment(builder => { + builder.OpenElement(0, "div"); + //space list entry render fragment + // builder.AddContent(1, SpaceListEntry(space)); + builder.OpenComponent<MainTabSpaceItem>(1); + builder.AddAttribute(2, "Space", space); + builder.AddAttribute(2, "OpenedSpaces", OpenedSpaces); + builder.CloseComponent(); + builder.CloseElement(); + //space children render fragment + if (isExpanded) { + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "style", "padding-left: 10px;"); + foreach (var child in children) { + builder.AddContent(4, RecursingSpaceChildren(child, parents.Append(space).ToList(), depth + 1)); + } + + builder.CloseElement(); + } + }); + + // Console.WriteLine($"RecursingSpaceChildren::Render - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {renderSw.Elapsed}"); + if (totalSw.ElapsedMilliseconds > 20) + Console.WriteLine($"RecursingSpaceChildren::Total - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + // Console.WriteLine($"RecursingSpaceChildren::Total - Depth: {depth}, Space: {space.RoomName}, Children: {children.Count} - {totalSw.Elapsed}"); + return rf; + } + + // private RenderFragment SpaceListEntry(RoomInfo space) { + // return builder => { + // { + // builder.OpenElement(0, "div"); + // builder.AddAttribute(1, "style", "display: block; width: 100%; height: 50px;"); + // builder.AddAttribute(2, "onclick", EventCallback.Factory.Create(this, () => { + // if (OpenedSpaces.Contains(space)) { + // OpenedSpaces.Remove(space); + // } + // else { + // OpenedSpaces.Add(space); + // } + // + // StateHasChanged(); + // })); + // { + // builder.OpenComponent<MxcImage>(5); + // builder.AddAttribute(6, "Homeserver", Data.Homeserver); + // builder.AddAttribute(7, "MxcUri", space.RoomIcon); + // builder.AddAttribute(8, "Circular", true); + // builder.AddAttribute(9, "Width", 32); + // builder.AddAttribute(10, "Height", 32); + // builder.CloseComponent(); + // } + // { + // // room name, ellipsized + // builder.OpenElement(11, "span"); + // builder.AddAttribute(12, "class", "spaceNameEllipsis"); + // builder.AddContent(13, space.RoomName); + // builder.CloseElement(); + // } + // builder.CloseElement(); + // } + // }; + // } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2MainTab.razor.css diff --git a/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor new file mode 100644 index 0000000..bbc63eb --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Index2Components/RoomsIndex2SyncContainer.razor @@ -0,0 +1,202 @@ +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using MatrixUtils.Abstractions +@using System.Diagnostics +@using System.Diagnostics.CodeAnalysis +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.Extensions +@using LibMatrix.Utilities +@using System.Collections.ObjectModel +@using ArcaneLibs +@inject ILogger<RoomsIndex2SyncContainer> logger +<pre>RoomsIndex2SyncContainer</pre> +@foreach (var (name, value) in _statusList) { + <pre>[@name] @value.Status</pre> +} + +@code { + + [Parameter] + public Index2.RoomListViewData Data { get; set; } = null!; + + private SyncHelper syncHelper; + + private Queue<KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>> queue = new(); + + private ObservableCollection<(string name, ObservableStatus value)> _statusList = new(); + + protected override async Task OnInitializedAsync() { + _statusList.CollectionChanged += (sender, args) => { + StateHasChanged(); + if (args.NewItems is { Count: > 0 }) + foreach (var item in args.NewItems) { + if (item is not (string name, ObservableStatus value)) continue; + value.PropertyChanged += (sender, args) => { + if(value.Show) StateHasChanged(); + }; + } + }; + + while (Data.Homeserver is null) { + await Task.Delay(100); + } + + await SetUpSync(); + } + + private async Task SetUpSync() { + var status = await GetOrAddStatus("Main"); + var syncHelpers = new Dictionary<string, SyncHelper>() { + ["Main"] = new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetBasicRoomInfo), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + } + }; + status.Status = "Initial sync... Checking server filter capability..."; + var syncRes = await syncHelpers["Main"].SyncAsync(); + if (!syncRes.Rooms?.Join?.Any(x => x.Value.State?.Events?.Any(y => y.Type == SpaceChildEventContent.EventId) ?? false) ?? true) { + status.Status = "Initial sync indicates that server supports filters, starting helpers!"; + syncHelpers.Add("SpaceRelations", new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetSpaceRelations), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + }); + + syncHelpers.Add("Profile", new SyncHelper(Data.Homeserver, logger) { + Timeout = 30000, + FilterId = await Data.Homeserver.GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetOwnMemberEvents), + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + }); + } + else status.Status = "Initial sync indicates that server does not support filters, continuing without extra filters!"; + + await HandleSyncResponse(syncRes); + + // profileSyncHelper = new SyncHelper(Homeserver, logger) { + // Timeout = 10000, + // Filter = profileUpdateFilter, + // MinimumDelay = TimeSpan.FromMilliseconds(5000) + // }; + // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId); + RunQueueProcessor(); + foreach (var helper in syncHelpers) { + Console.WriteLine($"Starting sync loop for {helper.Key}"); + RunSyncLoop(helper.Value, helper.Key); + } + } + + private async Task RunQueueProcessor() { + var status = await GetOrAddStatus("QueueProcessor"); + var statusd = await GetOrAddStatus("QueueProcessor/D", show: false); + while (true) { + await Task.Delay(1000); + try { + var renderTimeSw = Stopwatch.StartNew(); + while (queue.Count == 0) { + var delay = 1000; + Console.WriteLine("Queue is empty, waiting..."); + // Status2 = $"Queue is empty, waiting for {delay}ms..."; + await Task.Delay(delay); + } + + status.Status = $"Queue no longer empty after {renderTimeSw.Elapsed}!"; + renderTimeSw.Restart(); + + int maxUpdates = 5000; + while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { + var (roomId, roomData) = queueEntry; + statusd.Status = $"Dequeued room {roomId}"; + RoomInfo room; + + if (Data.Rooms.Any(x => x.Room.RoomId == roomId)) { + room = Data.Rooms.First(x => x.Room.RoomId == roomId); + statusd.Status = $"{roomId} already known with {room.StateEvents?.Count ?? 0} state events"; + } + else { + statusd.Status = $"Eencountered new room {roomId}!"; + room = new RoomInfo(Data.Homeserver!.GetRoom(roomId), roomData.State?.Events); + Data.Rooms.Add(room); + } + + if (roomData.State?.Events is { Count: > 0 }) + room.StateEvents!.MergeStateEventLists(roomData.State.Events); + else { + statusd.Status = $"Could not merge state for {room.Room.RoomId} as new data contains no state events!"; + } + + // await Task.Delay(10); + } + + status.Status = $"Got {Data.Rooms.Count} rooms so far! {queue.Count} entries left in processing queue... Parsed last response in {renderTimeSw.Elapsed}"; + + // RenderContents |= queue.Count == 0; + // await Task.Delay(Data.Rooms.Count); + } + catch (Exception e) { + Console.WriteLine("QueueWorker exception: " + e); + } + } + } + + private async Task RunSyncLoop(SyncHelper syncHelper, string name = "Unknown") { + var status = await GetOrAddStatus($"SYNC/{name}"); + status.Status = $"Initial syncing..."; + + var syncs = syncHelper.EnumerateSyncAsync(); + await foreach (var sync in syncs) { + var sw = Stopwatch.StartNew(); + status.Status = $"[{DateTime.Now}] Got {Data.Rooms.Count} rooms so far! {sync.Rooms?.Join?.Count ?? 0} new updates!"; + + await HandleSyncResponse(sync); + status.Status += $"\nProcessed sync in {sw.ElapsedMilliseconds}ms, queue length: {queue.Count}"; + } + } + + private async Task HandleSyncResponse(SyncResponse? sync) { + if (sync?.Rooms?.Join is { Count: > 0 }) + foreach (var joinedRoom in sync.Rooms.Join) + queue.Enqueue(joinedRoom); + + if (sync.Rooms.Leave is { Count: > 0 }) + foreach (var leftRoom in sync.Rooms.Leave) + if (Data.Rooms.Any(x => x.Room.RoomId == leftRoom.Key)) + Data.Rooms.Remove(Data.Rooms.First(x => x.Room.RoomId == leftRoom.Key)); + } + + private SemaphoreSlim _syncLock = new(1, 1); + + private async Task<ObservableStatus> GetOrAddStatus(string name, bool show = true, bool log = true) { + await _syncLock.WaitAsync(); + try { + if (_statusList.Any(x => x.name == name)) + return _statusList.First(x => x.name == name).value; + var status = new ObservableStatus() { + Name = name, + Log = log, + Show = show + }; + _statusList.Add((name, status)); + return status; + } + finally { + _syncLock.Release(); + } + } + + private class ObservableStatus : NotifyPropertyChanged { + private string _status = "Initialising..."; + public string Name { get; set; } = "Unknown"; + public bool Show { get; set; } = true; + public bool Log { get; set; } = true; + + public string Status { + get => _status; + set { + if(SetField(ref _status, value) && Log) + Console.WriteLine($"[{Name}]: {value}"); + } + } + } + +} \ No newline at end of file |