diff options
20 files changed, 375 insertions, 140 deletions
diff --git a/LibMatrix b/LibMatrix -Subproject bf2da30c7ae9d4c15a5e22f3ee0b1bae2ca66e4 +Subproject b7dbc011e0eee55c011623d2747e517436d0410 diff --git a/MatrixUtils.Abstractions/RoomInfo.cs b/MatrixUtils.Abstractions/RoomInfo.cs index 84a5940..0cd4dc1 100644 --- a/MatrixUtils.Abstractions/RoomInfo.cs +++ b/MatrixUtils.Abstractions/RoomInfo.cs @@ -13,42 +13,55 @@ public class RoomInfo : NotifyPropertyChanged { public ObservableCollection<StateEventResponse?> StateEvents { get; } = new(); public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") { - var @event = StateEvents.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey); + var @event = StateEvents.FirstOrDefault(x => x?.Type == type && x.StateKey == stateKey); if (@event is not null) return @event; - @event = new StateEventResponse { - RoomId = Room.RoomId, - Type = type, - StateKey = stateKey, - Sender = null, //TODO implement - EventId = null - }; - // if (Room is null) return null; + // @event = new StateEventResponse { + // RoomId = Room.RoomId, + // Type = type, + // StateKey = stateKey, + // Sender = null, //TODO implement + // EventId = null + // }; + // // if (Room is null) return null; + // try { + // @event.RawContent = await Room.GetStateAsync<JsonObject>(type, stateKey); + // } + // catch (MatrixException e) { + // if (e is { ErrorCode: "M_NOT_FOUND" }) { + // if (type == "m.room.name") + // @event = new() { + // Type = type, + // StateKey = stateKey, + // TypedContent = new RoomNameEventContent() { + // Name = await Room.GetNameOrFallbackAsync() + // }, + // //TODO implement + // RoomId = null, + // Sender = null, + // EventId = null + // }; + // else + // @event.RawContent = default!; + // } + // else { + // throw; + // } + // } + // catch (Exception e) { + // await Task.Delay(1000); + // return await GetStateEvent(type, stateKey); + // } + try { - @event.RawContent = await Room.GetStateAsync<JsonObject>(type, stateKey); + @event = await Room.GetStateEventOrNullAsync(type, stateKey); + StateEvents.Add(@event); } - catch (MatrixException e) { - if (e is { ErrorCode: "M_NOT_FOUND" }) { - if (type == "m.room.name") - @event = new() { - Type = type, - StateKey = stateKey, - TypedContent = new RoomNameEventContent() { - Name = await Room.GetNameOrFallbackAsync() - }, - //TODO implement - RoomId = null, - Sender = null, - EventId = null - }; - else - @event.RawContent = default!; - } - else { - throw; - } + catch (Exception e) { + Console.Error.WriteLine(e); + await Task.Delay(1000); + return await GetStateEvent(type, stateKey); } - StateEvents.Add(@event); return @event; } @@ -81,11 +94,14 @@ public class RoomInfo : NotifyPropertyChanged { private string? _roomCreator; public string? DefaultRoomName { get; set; } + public string? OverrideRoomType { get; set; } + public string? RoomType => OverrideRoomType ?? CreationEventContent?.Type; public RoomInfo() { StateEvents.CollectionChanged += (_, args) => { if (args.NewItems is { Count: > 0 }) - foreach (StateEventResponse newState in args.NewItems) { // TODO: switch statement benchmark? + foreach (StateEventResponse? newState in args.NewItems) { // TODO: switch statement benchmark? + if(newState is null) continue; if (newState.Type == RoomNameEventContent.EventId && newState.TypedContent is RoomNameEventContent roomNameContent) RoomName = roomNameContent.Name; else if (newState is { Type: RoomAvatarEventContent.EventId, TypedContent: RoomAvatarEventContent roomAvatarContent }) diff --git a/MatrixUtils.Web/Classes/RMUStorageWrapper.cs b/MatrixUtils.Web/Classes/RMUStorageWrapper.cs index 31e7734..fa79268 100644 --- a/MatrixUtils.Web/Classes/RMUStorageWrapper.cs +++ b/MatrixUtils.Web/Classes/RMUStorageWrapper.cs @@ -12,7 +12,7 @@ public class RMUStorageWrapper(TieredStorageService storageService, HomeserverPr } public async Task<UserAuth?> GetCurrentToken() { - var currentToken = await storageService.DataStorageProvider.LoadObjectAsync<UserAuth>("token"); + var currentToken = await storageService.DataStorageProvider.LoadObjectAsync<UserAuth>("rmu.token"); var allTokens = await GetAllTokens(); if (allTokens is null or { Count: 0 }) { await SetCurrentToken(null); @@ -94,5 +94,32 @@ public class RMUStorageWrapper(TieredStorageService storageService, HomeserverPr await storageService.DataStorageProvider.SaveObjectAsync("rmu.tokens", tokens); } - public async Task SetCurrentToken(UserAuth? auth) => await storageService.DataStorageProvider.SaveObjectAsync("token", auth); + public async Task SetCurrentToken(UserAuth? auth) => await storageService.DataStorageProvider.SaveObjectAsync("rmu.token", auth); + + public async Task MigrateFromMRU() { + var dsp = storageService.DataStorageProvider!; + if(await dsp.ObjectExistsAsync("token")) { + var oldToken = await dsp.LoadObjectAsync<UserAuth>("token"); + if (oldToken != null) { + await dsp.SaveObjectAsync("rmu.token", oldToken); + await dsp.DeleteObjectAsync("tokens"); + } + } + + if(await dsp.ObjectExistsAsync("tokens")) { + var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("tokens"); + if (oldTokens != null) { + await dsp.SaveObjectAsync("rmu.tokens", oldTokens); + await dsp.DeleteObjectAsync("tokens"); + } + } + + if(await dsp.ObjectExistsAsync("mru.tokens")) { + var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens"); + if (oldTokens != null) { + await dsp.SaveObjectAsync("rmu.tokens", oldTokens); + await dsp.DeleteObjectAsync("mru.tokens"); + } + } + } } diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj index d5977a0..515b235 100644 --- a/MatrixUtils.Web/MatrixUtils.Web.csproj +++ b/MatrixUtils.Web/MatrixUtils.Web.csproj @@ -9,6 +9,7 @@ <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <UseBlazorWebAssembly>true</UseBlazorWebAssembly> + <BlazorEnableCompression>false</BlazorEnableCompression> <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> <!-- <RunAOTCompilation>true</RunAOTCompilation>--> </PropertyGroup> diff --git a/MatrixUtils.Web/Pages/Index.razor b/MatrixUtils.Web/Pages/Index.razor index 9c1bab6..e1cb60e 100644 --- a/MatrixUtils.Web/Pages/Index.razor +++ b/MatrixUtils.Web/Pages/Index.razor @@ -20,7 +20,7 @@ Small collection of tools to do not-so-everyday things. var _auth = session.UserAuth; <tr class="user-entry"> <td> - <img class="avatar" src="@session.UserInfo.AvatarUrl"/> + <img class="avatar" src="@session.UserInfo.AvatarUrl" crossorigin="anonymous"/> </td> <td class="user-info"> <p> @@ -108,6 +108,16 @@ Small collection of tools to do not-so-everyday things. _sessions.Clear(); _offlineSessions.Clear(); var tokens = await RMUStorage.GetAllTokens(); + if (tokens is not { Count: > 0 }) { + Console.WriteLine("No tokens found, trying migration from MRU..."); + await RMUStorage.MigrateFromMRU(); + tokens = await RMUStorage.GetAllTokens(); + if (tokens is not { Count: > 0 }) { + Console.WriteLine("No tokens found"); + return; + } + } + var profileTasks = tokens.Select(async token => { UserInfo userInfo = new(); AuthenticatedHomeserverGeneric hs; @@ -119,14 +129,13 @@ Small collection of tools to do not-so-everyday things. if (e.ErrorCode != "M_UNKNOWN_TOKEN") throw; NavigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken); return; - } catch (Exception e) { logger.LogError(e, $"Failed to instantiate AuthenticatedHomeserver for {token.ToJson()}, homeserver may be offline?", token.UserId); _offlineSessions.Add(token); return; } - + Console.WriteLine($"Got hs for {token.ToJson()}"); var roomCountTask = hs.GetJoinedRooms(); @@ -143,8 +152,8 @@ Small collection of tools to do not-so-everyday things. ServerVersion = await (hs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)), Homeserver = hs }); - }); - Console.WriteLine("Waiting for profile tasks"); + }).ToList(); + Console.WriteLine($"Waiting for {profileTasks.Count} profile tasks"); await Task.WhenAll(profileTasks); Console.WriteLine("Done waiting for profile tasks"); await base.OnInitializedAsync(); @@ -177,7 +186,6 @@ Small collection of tools to do not-so-everyday things. await OnInitializedAsync(); } - private async Task SwitchSession(UserAuth auth) { Console.WriteLine($"Switching to {auth.Homeserver} {auth.UserId} via {auth.Proxy}"); await RMUStorage.SetCurrentToken(auth); diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor index 0ec9487..170f489 100644 --- a/MatrixUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor @@ -18,7 +18,13 @@ <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents"></RoomList> @code { - private ObservableCollection<RoomInfo> Rooms { get; } = new(); + + private ObservableCollection<RoomInfo> _rooms = new(); + private ObservableCollection<RoomInfo> Rooms { + get => _rooms; + set => _rooms = value; + } + private UserProfileResponse GlobalProfile { get; set; } private AuthenticatedHomeserverGeneric? Homeserver { get; set; } @@ -97,25 +103,29 @@ if (Homeserver is null) return; var rooms = await Homeserver.GetJoinedRooms(); // SemaphoreSlim _semaphore = new(160, 160); + GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId); - var roomTasks = rooms.Select(async room => { - RoomInfo ri; - // await _semaphore.WaitAsync(); - ri = new() { Room = room }; - await Task.WhenAll((filter.Room?.State?.Types ?? []).Select(x => ri.GetStateEvent(x))); - return ri; - }).ToAsyncEnumerable(); - - await foreach (var room in roomTasks) { - Rooms.Add(room); - StateHasChanged(); - // await Task.Delay(50); - // _semaphore.Release(); + Rooms = new ObservableCollection<RoomInfo>(rooms.Select(x => new RoomInfo() { Room = x })); + foreach (var stateType in filter.Room?.State?.Types ?? []) { + var tasks = Rooms.Select(async room => { + try { + + await room.GetStateEvent(stateType); + } + catch (Exception e) { + Console.WriteLine($"Failed to get state event {stateType} for room {room.Room.RoomId}: {e}"); + } + }); + await Task.WhenAll(tasks); + Status = $"Fetched all {stateType} events..."; + // StateHasChanged(); } + - if (rooms.Count >= 150) RenderContents = true; - - GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId); + RenderContents = true; + Status = "Initial fetch done! Starting initial sync..."; + // StateHasChanged(); + await Task.Delay(1000); syncHelper = new SyncHelper(Homeserver, logger) { Timeout = 30000, Filter = filter, @@ -147,7 +157,7 @@ Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!"); - int maxUpdates = 10; + int maxUpdates = 50; isInitialSync = false; while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { var (roomId, roomData) = queueEntry; @@ -176,6 +186,8 @@ else { Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!"); } + + await Task.Delay(100); } Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}"); Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue..."; @@ -212,9 +224,10 @@ } private Queue<KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>> queue = new(); + private async Task RunSyncLoop(SyncHelper syncHelper) { - Status = "Initial syncing..."; + // Status = "Initial syncing..."; Console.WriteLine("starting sync"); var syncs = syncHelper.EnumerateSyncAsync(); diff --git a/MatrixUtils.Web/Pages/Tools/ToolsIndex.razor b/MatrixUtils.Web/Pages/Tools/Index.razor index f4092d7..f1e04a3 100644 --- a/MatrixUtils.Web/Pages/Tools/ToolsIndex.razor +++ b/MatrixUtils.Web/Pages/Tools/Index.razor @@ -6,3 +6,5 @@ <a href="/Tools/MassRoomJoin">Join room across all session</a><br/> <a href="/Tools/MediaLocator">Locate lost media</a><br/> <a href="/Tools/SpaceDebug">Debug space relationships</a><br/> +<a href="/Tools/MigrateRoom">Migrate users from a split room to a new room</a><br/> +<a href="/Tools/LeaveRoom">Leave room by ID</a><br/> diff --git a/MatrixUtils.Web/Pages/Tools/LeaveRoom.razor b/MatrixUtils.Web/Pages/Tools/LeaveRoom.razor new file mode 100644 index 0000000..b5df05f --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/LeaveRoom.razor @@ -0,0 +1,56 @@ +@page "/Tools/LeaveRoom" +@using System.Diagnostics +@using ArcaneLibs.Extensions +@using LibMatrix.Homeservers +@using LibMatrix.RoomTypes +@using System.Collections.ObjectModel +<h3>Leave room</h3> +<hr/> +<span>Room ID: </span> +<InputText @bind-Value="@RoomId"></InputText> +<br/> +<LinkButton OnClick="@Leave">Leave</LinkButton> +<br/><br/> +@foreach (var line in Log) { + <p>@line</p> +} +@code { + AuthenticatedHomeserverGeneric? hs { get; set; } + ObservableCollection<string> Log { get; set; } = new ObservableCollection<string>(); + [Parameter, SupplyParameterFromQuery(Name = "roomId")] + public string? RoomId { get; set; } + + protected override async Task OnInitializedAsync() { + hs = await RMUStorage.GetCurrentSessionOrNavigate(); + if (hs is null) return; + Log.CollectionChanged += (sender, args) => StateHasChanged(); + + StateHasChanged(); + Console.WriteLine("Rerendered!"); + await base.OnInitializedAsync(); + } + + private async Task Leave() { + if(string.IsNullOrWhiteSpace(RoomId)) return; + var room = hs.GetRoom(RoomId); + Log.Add("Got room object..."); + try { + await room.LeaveAsync(); + Log.Add("Left room!"); + } + catch (Exception e) { + Log.Add(e.ToString()); + } + + try { + await room.ForgetAsync(); + Log.Add("Forgot room!"); + } + catch (Exception e) { + Log.Add(e.ToString()); + } + + Log.Add("Done!"); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MainLayout.razor b/MatrixUtils.Web/Shared/MainLayout.razor index 92194b2..d8bf411 100644 --- a/MatrixUtils.Web/Shared/MainLayout.razor +++ b/MatrixUtils.Web/Shared/MainLayout.razor @@ -16,4 +16,6 @@ @Body </article> </main> -</div> \ No newline at end of file +</div> + +<UpdateAvailableDetector/> \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MxcImage.razor b/MatrixUtils.Web/Shared/MxcImage.razor index fb8c248..f31c19f 100644 --- a/MatrixUtils.Web/Shared/MxcImage.razor +++ b/MatrixUtils.Web/Shared/MxcImage.razor @@ -1,4 +1,4 @@ -<img class="@Class" src="@ResolvedUri" style="@Style"/> +<img src="@ResolvedUri" style="@StyleString"/> @code { private string _mxcUri; private string _style; @@ -13,9 +13,14 @@ UriHasChanged(value); } } + [Parameter] + public bool Circular { get; set; } - //mxcuri binding + [Parameter] + public int? Width { get; set; } + [Parameter] + public int? Height { get; set; } [Parameter] public string Style { @@ -36,8 +41,18 @@ } } + private string StyleString => $"{Style} {(Circular ? "border-radius: 50%;" : "")} {(Width.HasValue ? $"width: {Width}px;" : "")} {(Height.HasValue ? $"height: {Height}px;" : "")}"; + + private static readonly string Prefix = "mxc://"; + private static readonly int PrefixLength = Prefix.Length; + private async Task UriHasChanged(string value) { - var uri = value[5..].Split('/'); + if (!value.StartsWith(Prefix)) { + Console.WriteLine($"UriHasChanged: {value} does not start with {Prefix}, passing as resolved URI!!!"); + ResolvedUri = value; + return; + } + var uri = value[PrefixLength..].Split('/'); Console.WriteLine($"UriHasChanged: {value} {uri[0]}"); if (Homeserver is null) { Console.WriteLine($"Homeserver is null, creating new remotehomeserver for {uri[0]}"); @@ -47,7 +62,7 @@ Console.WriteLine($"ResolvedUri: {ResolvedUri}"); } - [Parameter] - public string Class { get; set; } + // [Parameter] + // public string Class { get; set; } } \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/RoomList.razor b/MatrixUtils.Web/Shared/RoomList.razor index ed443dd..2ab3cef 100644 --- a/MatrixUtils.Web/Shared/RoomList.razor +++ b/MatrixUtils.Web/Shared/RoomList.razor @@ -20,9 +20,24 @@ else { } @code { + private ObservableCollection<RoomInfo> _rooms; [Parameter] - public ObservableCollection<RoomInfo> Rooms { get; set; } + public ObservableCollection<RoomInfo> Rooms { + get => _rooms; + set { + if(_rooms != value) + value.CollectionChanged += (_, args) => { + foreach (RoomInfo item in args.NewItems??(object[])[]) { + item.PropertyChanged += (_, args2) => { + if (args2.PropertyName == nameof(item.CreationEventContent)) + StateHasChanged(); + }; + } + }; + _rooms = value; + } + } [Parameter] public UserProfileResponse? GlobalProfile { get; set; } @@ -33,65 +48,21 @@ else { [Parameter] public EventCallback<bool> StillFetchingChanged { get; set; } - private Dictionary<string, List<RoomInfo>> RoomsWithTypes => Rooms is null ? new() : Rooms.GroupBy(x => GetRoomTypeName(x.CreationEventContent?.Type)).ToDictionary(x => x.Key, x => x.ToList()); - - private bool hooked; - protected override async Task OnParametersSetAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); - if (hs is null) return; - if (!hooked) { - Rooms.CollectionChanged += (_, args) => { - foreach (RoomInfo item in args.NewItems) { - item.PropertyChanged += (_, args2) => { - // Console.WriteLine(args2); - - if (args2.PropertyName == nameof(item.CreationEventContent)) - StateHasChanged(); - }; - } - }; - hooked = true; - } - - // GlobalProfile ??= await hs.GetProfileAsync(hs.WhoAmI.UserId); - - await base.OnParametersSetAsync(); - } + + private Dictionary<string, List<RoomInfo>> RoomsWithTypes => Rooms is null ? new() : Rooms.GroupBy(x => GetRoomTypeName(x.RoomType)).ToDictionary(x => x.Key, x => x.ToList()); private string GetRoomTypeName(string? roomType) => roomType switch { null => "Room", "m.space" => "Space", "msc3588.stories.stories-room" => "Story room", - "support.feline.policy.lists.msc.v1" => "MSC3784 Policy list (v1)", + "support.feline.policy.lists.msc.v1" => "MSC3784 policy list (v1)", + // custom names + "gay.rory.moderation_bot.policy_room" => "Rory&::ModerationBot policy room", + "gay.rory.moderation_bot.log_room" => "Rory&::ModerationBot log room", + "gay.rory.moderation_bot.control_room" => "Rory&::ModerationBot control room", + // fallback + "gay.rory.rmu.fallback.policy_list" => "\"Legacy\" policy list (unmarked room)", _ => roomType - }; - - // private static SemaphoreSlim _semaphoreSlim = new(8, 8); - - // private async Task ProcessRoom(RoomInfo room) { - // await _semaphoreSlim.WaitAsync(); - // string roomType; - // try { - // var createEvent = (await room.GetStateEvent("m.room.create")).TypedContent as RoomCreateEventContent; - // roomType = GetRoomTypeName(createEvent.Type); - // - // if (roomType == "Room") { - // var mjolnirData = await room.GetStateEvent("org.matrix.mjolnir.shortcode"); - // if (mjolnirData?.RawContent?.ToJson(ignoreNull: true) is not null and not "{}") - // roomType = "Legacy policy room"; - // } - // } - // catch (MatrixException e) { - // roomType = $"Error: {e.ErrorCode}"; - // } - // - // // if (!RoomsWithTypes.ContainsKey(roomType)) { - // // RoomsWithTypes.Add(roomType, new List<RoomInfo>()); - // // } - // // RoomsWithTypes[roomType].Add(room); - // - // StateHasChanged(); - // _semaphoreSlim.Release(); - // } + }; } \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor index 3d0070f..4b24c18 100644 --- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor @@ -16,6 +16,7 @@ <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Timeline")">View timeline</LinkButton> <LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/View")">View state</LinkButton> <LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/Edit")">Edit state</LinkButton> + <LinkButton href="@($"/Tools/LeaveRoom?roomId={room.Room.RoomId}")" Color="#FF0000">Leave room</LinkButton> @if (room.CreationEventContent?.Type == "m.space") { <RoomListSpace Space="@room"></RoomListSpace> @@ -49,15 +50,5 @@ : RoomConstants.DangerousRoomVersions.Contains(roomVersionContent.RoomVersion) ? 2 : roomVersionContent.RoomVersion != RoomConstants.RecommendedRoomVersion ? 1 : 0; } - - public static string GetRoomTypeName(string roomType) { - return roomType switch { - null => "Room", - "m.space" => "Space", - "org.matrix.mjolnir.policy" => "Policy room", - - _ => roomType - }; - } } diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor index 895d642..9c481e3 100644 --- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor @@ -1,15 +1,23 @@ @using System.Collections.ObjectModel @using MatrixUtils.Abstractions -<MatrixUtils.Web.Shared.SimpleComponents.LinkButton href="@($"/Rooms/{Space.Room.RoomId}/Space")">Manage space</MatrixUtils.Web.Shared.SimpleComponents.LinkButton> +<LinkButton href="@($"/Rooms/{Space.Room.RoomId}/Space")">Manage space</LinkButton> <br/> <details @ontoggle="SpaceChildrenOpened"> <summary>@Children.Count children</summary> @if (_shouldRenderChildren) { <p>Breadcrumb: @Breadcrumbs</p> + <p>Joined:</p> <div style="margin-left: 8px;"> <RoomList Rooms="Children"></RoomList> </div> + <p>Unjoined:</p> + @foreach (var room in Unjoined) { + <p>@room.Room.RoomId</p> + } + @* <div style="margin-left: 8px;"> *@ + @* <RoomList Rooms="Children"></RoomList> *@ + @* </div> *@ } </details> @@ -28,11 +36,14 @@ } private ObservableCollection<RoomInfo> Children { get; set; } = new(); + private Collection<RoomInfo> Unjoined { get; set; } = new(); protected override async Task OnInitializedAsync() { if (Breadcrumbs == null) throw new ArgumentNullException(nameof(Breadcrumbs)); await Task.Delay(Random.Shared.Next(1000, 10000)); var rooms = Space.Room.AsSpace.GetChildrenAsync(); + var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var joinedRooms = await hs.GetJoinedRooms(); await foreach (var room in rooms) { if (Breadcrumbs.Contains(room.RoomId)) continue; var roomInfo = KnownRooms.FirstOrDefault(x => x.Room.RoomId == room.RoomId); @@ -42,7 +53,9 @@ }; KnownRooms.Add(roomInfo); } - Children.Add(roomInfo); + if(joinedRooms.Any(x=>x.RoomId == room.RoomId)) + Children.Add(roomInfo); + else Unjoined.Add(roomInfo); } await base.OnInitializedAsync(); } diff --git a/MatrixUtils.Web/Shared/RoomListItem.razor b/MatrixUtils.Web/Shared/RoomListItem.razor index 1046dd1..2e7a372 100644 --- a/MatrixUtils.Web/Shared/RoomListItem.razor +++ b/MatrixUtils.Web/Shared/RoomListItem.razor @@ -17,7 +17,7 @@ </span> <span class="centerVertical noLeftPadding">-></span> } - <MxcImage Class="avatar32" MxcUri="@RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/> + <MxcImage class="avatar32" MxcUri="@RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/> <div class="inlineBlock"> <span class="centerVertical">@RoomInfo.RoomName</span> @if (ChildContent is not null) { diff --git a/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor b/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor new file mode 100644 index 0000000..5197a6f --- /dev/null +++ b/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor @@ -0,0 +1,38 @@ +@* Source: https://whuysentruit.medium.com/blazor-wasm-pwa-adding-a-new-update-available-notification-d9f65c4ad13 *@ +@inject IJSRuntime _jsRuntime + +@if (_newVersionAvailable) +{ + <button type="button" class="btn btn-warning shadow floating-update-button" onclick="window.location.reload()"> + A new version of the application is available. Click here to reload. + </button> +} + +@code { + + private bool _newVersionAvailable = false; + + protected override async Task OnInitializedAsync() + { + await RegisterForUpdateAvailableNotification(); + } + + private async Task RegisterForUpdateAvailableNotification() + { + await _jsRuntime.InvokeAsync<object>( + identifier: "registerForUpdateAvailableNotification", + DotNetObjectReference.Create(this), + nameof(OnUpdateAvailable)); + } + + [JSInvokable(nameof(OnUpdateAvailable))] + public Task OnUpdateAvailable() + { + _newVersionAvailable = true; + + StateHasChanged(); + + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor.css b/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor.css new file mode 100644 index 0000000..32bff09 --- /dev/null +++ b/MatrixUtils.Web/Shared/UpdateAvailableDetector.razor.css @@ -0,0 +1,15 @@ +.floating-update-button { + position: fixed; + + right: 2rem; + bottom: 2rem; + + padding: 1rem 1.5rem; + + animation: fadein 2s ease-out; +} + +@keyframes fadein { + from { right: -100%; } + to { right: 2rem; } +} \ No newline at end of file diff --git a/MatrixUtils.Web/wwwroot/index.html b/MatrixUtils.Web/wwwroot/index.html index a40f38c..5182193 100644 --- a/MatrixUtils.Web/wwwroot/index.html +++ b/MatrixUtils.Web/wwwroot/index.html @@ -59,7 +59,8 @@ } </script> <script src="_framework/blazor.webassembly.js"></script> - <script>navigator.serviceWorker.register('service-worker.js');</script> +<!-- <script>navigator.serviceWorker.register('service-worker.js');</script>--> + <script src="sw-registrator.js"></script> </body> </html> diff --git a/MatrixUtils.Web/wwwroot/service-worker.published.js b/MatrixUtils.Web/wwwroot/service-worker.published.js index 003e3e7..9219755 100644 --- a/MatrixUtils.Web/wwwroot/service-worker.published.js +++ b/MatrixUtils.Web/wwwroot/service-worker.published.js @@ -8,8 +8,10 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event))); const cacheNamePrefix = 'offline-cache-'; const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; -const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; -const offlineAssetsExclude = [ /^service-worker\.js$/ ]; +const offlineAssetsInclude = [// Standard resources + /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /* Extra known-static paths */ + /\/_matrix\/media\/.{2}\/download\//, /api\.dicebear\.com\/6\.x\/identicon\/svg/]; +const offlineAssetsExclude = [/^service-worker\.js$/]; // Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'. const base = "/"; @@ -19,11 +21,14 @@ const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.ur async function onInstall(event) { console.info('Service worker: Install'); + // Activate the new service worker as soon as the old one is retired. + self.skipWaiting(); + // Fetch and cache all matching items from the assets manifest const assetsRequests = self.assetsManifest.assets .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) - .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); + .map(asset => new Request(asset.url, {integrity: asset.hash, cache: 'no-cache'})); await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); } @@ -43,12 +48,32 @@ async function onFetch(event) { // For all navigation requests, try to serve index.html from cache, // unless that request is for an offline resource. // If you need some URLs to be server-rendered, edit the following check to exclude those URLs - const shouldServeIndexHtml = event.request.mode === 'navigate' - && !manifestUrlList.some(url => url === event.request.url); + const shouldServeIndexHtml = event.request.mode === 'navigate' && !manifestUrlList.some(url => url === event.request.url); const request = shouldServeIndexHtml ? 'index.html' : event.request; + const shouldCache = offlineAssetsInclude.some(pattern => pattern.test(request.url)); + const cache = await caches.open(cacheName); cachedResponse = await cache.match(request); + let exception; + let fetched; + if (!cachedResponse && shouldCache) { + console.log("Service worker caching: fetching ", request.url) + try { + fetched = true; + await cache.add(request); + cachedResponse = await cache.match(request); + } catch (e) { + exception = e; + console.error("cache.add error: ", e, request.url) + } + } + let consoleLog = { + fetched, shouldCache, request, exception, cachedResponse, url: request.url, + } + Object.keys(consoleLog).forEach(key => consoleLog[key] == null && delete consoleLog[key]) + if(consoleLog.exception) + console.log("Service worker caching: ", consoleLog) } return cachedResponse || fetch(event.request); diff --git a/MatrixUtils.Web/wwwroot/sw-registrator.js b/MatrixUtils.Web/wwwroot/sw-registrator.js new file mode 100644 index 0000000..94b96b2 --- /dev/null +++ b/MatrixUtils.Web/wwwroot/sw-registrator.js @@ -0,0 +1,41 @@ +// source: https://whuysentruit.medium.com/blazor-wasm-pwa-adding-a-new-update-available-notification-d9f65c4ad13 + +window.updateAvailable = new Promise((resolve, reject) => { + if (!('serviceWorker' in navigator)) { + const errorMessage = `This browser doesn't support service workers`; + console.error(errorMessage); + reject(errorMessage); + return; + } + + navigator.serviceWorker.register('/service-worker.js') + .then(registration => { + console.info(`Service worker registration successful (scope: ${registration.scope})`); + + // detect updates every minute + setInterval(() => { + registration.update(); + }, 5 * 1000); // 60000ms -> check each minute + + registration.onupdatefound = () => { + const installingServiceWorker = registration.installing; + installingServiceWorker.onstatechange = () => { + if (installingServiceWorker.state === 'installed') { + resolve(!!navigator.serviceWorker.controller); + } + } + }; + }) + .catch(error => { + console.error('Service worker registration failed with error:', error); + reject(error); + }); +}); + +window.registerForUpdateAvailableNotification = (caller, methodName) => { + window.updateAvailable.then(isUpdateAvailable => { + if (isUpdateAvailable) { + caller.invokeMethodAsync(methodName).then(); + } + }); +}; \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh index f7117b1..4c60728 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -11,4 +11,4 @@ BASE_DIR=`pwd` rm -rf **/bin/Release cd MatrixUtils.Web dotnet publish -c Release -rsync -raP bin/Release/net8.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/ \ No newline at end of file +rsync -raP bin/Release/net8.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/ |