diff --git a/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor b/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor
index 9b0f61c..a1e928f 100644
--- a/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor
+++ b/MatrixRoomUtils.Web/Pages/Dev/DevOptions.razor
@@ -1,5 +1,8 @@
@page "/Dev/Options"
@using ArcaneLibs.Extensions
+@using System.Text.Unicode
+@using System.Text
+@using System.Text.Json
@inject NavigationManager NavigationManager
@inject ILocalStorageService LocalStorage
@@ -8,23 +11,61 @@
<h3>Rory&::MatrixUtils - Developer options</h3>
<hr/>
-<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox><label> Enable log views</label><br/>
-<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnableConsoleLogging" @oninput="@LogStuff"></InputCheckbox><label> Enable console logging</label><br/>
-<InputCheckbox @bind-Value="@settings.DeveloperSettings.EnablePortableDevtools" @oninput="@LogStuff"></InputCheckbox><label> Enable portable devtools</label><br/>
+<p>
+ <span>Import local storage: </span>
+ <InputFile OnChange="ImportLocalStorage"></InputFile>
+</p>
+<p>
+ <span>Export local storage: </span>
+ <button @onclick="@ExportLocalStorage">Export</button>
+</p>
+
+@if (userSettings is not null) {
+ <InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox>
+ <label> Enable log views</label>
+ <br/>
+ <InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnableConsoleLogging" @oninput="@LogStuff"></InputCheckbox>
+ <label> Enable console logging</label>
+ <br/>
+ <InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnablePortableDevtools" @oninput="@LogStuff"></InputCheckbox>
+ <label> Enable portable devtools</label>
+ <br/>
+}
<br/>
@code {
- MRUStorageWrapper.Settings settings { get; set; } = new();
+ private MRUStorageWrapper.Settings? userSettings { get; set; }
protected override async Task OnInitializedAsync() {
- settings = await TieredStorage.DataStorageProvider.LoadObjectAsync<MRUStorageWrapper.Settings>("mru.settings");
+ // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<MRUStorageWrapper.Settings>("mru.settings");
+
await base.OnInitializedAsync();
}
private async Task LogStuff() {
await Task.Delay(100);
- Console.WriteLine($"Settings: {settings.ToJson()}");
- await TieredStorage.DataStorageProvider.SaveObjectAsync("mru.settings", settings);
+ Console.WriteLine($"Settings: {userSettings.ToJson()}");
+ await TieredStorage.DataStorageProvider.SaveObjectAsync("mru.settings", userSettings);
+ }
+
+ private async Task ExportLocalStorage() {
+ var keys = await TieredStorage.DataStorageProvider.GetAllKeysAsync();
+ var data = new Dictionary<string, object>();
+ foreach (var key in keys) {
+ data.Add(key, await TieredStorage.DataStorageProvider.LoadObjectAsync<object>(key));
+ }
+ var dataUri = "data:application/json;base64,";
+ dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)));
+ await JSRuntime.InvokeVoidAsync("window.open", dataUri, "_blank");
+ }
+
+ private async Task ImportLocalStorage(InputFileChangeEventArgs obj) {
+ if (obj.FileCount != 1) return;
+ var data = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(obj.File.OpenReadStream());
+ foreach (var (key, value) in data) {
+ await TieredStorage.DataStorageProvider.SaveObjectAsync(key, value);
+ }
+ NavigationManager.NavigateTo(NavigationManager.Uri, true, true);
}
}
diff --git a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
index 4d98402..516e9ca 100644
--- a/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
+++ b/MatrixRoomUtils.Web/Pages/Rooms/Index.razor
@@ -22,7 +22,7 @@
@code {
private ObservableCollection<RoomInfo> Rooms { get; } = new();
- private ProfileResponseEventContent GlobalProfile { get; set; }
+ private UserProfileResponse GlobalProfile { get; set; }
private AuthenticatedHomeserverGeneric? Homeserver { get; set; }
diff --git a/MatrixRoomUtils.Web/Pages/Tools/CopyPowerlevel.razor b/MatrixRoomUtils.Web/Pages/Tools/CopyPowerlevel.razor
new file mode 100644
index 0000000..aaeb5a3
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/Tools/CopyPowerlevel.razor
@@ -0,0 +1,84 @@
+@page "/CopyPowerlevel"
+@using System.Diagnostics
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Homeservers
+@using LibMatrix.RoomTypes
+<h3>Copy powerlevel</h3>
+<hr/>
+
+<p>Users: </p>
+@foreach (var hs in hss) {
+ <p>@hs.WhoAmI.UserId</p>
+}
+
+<br/>
+<LinkButton OnClick="Execute">Execute</LinkButton>
+<br/>
+@foreach (var line in Enumerable.Reverse(log)) {
+ <p>@line</p>
+}
+
+@code {
+ private List<string> log { get; set; } = new();
+ List<AuthenticatedHomeserverGeneric> hss { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+ if (hs is null) return;
+ var sessions = await MRUStorage.GetAllTokens();
+ foreach (var userAuth in sessions) {
+ var session = await MRUStorage.GetSession(userAuth);
+ if (session is not null) {
+ hss.Add(session);
+ StateHasChanged();
+ }
+ }
+
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ }
+
+ private async Task Execute() {
+ foreach (var hs in hss) {
+ var rooms = await hs.GetJoinedRooms();
+ var tasks = rooms.Select(x=>Execute(hs, x)).ToAsyncEnumerable();
+ await foreach (var a in tasks) {
+ if (!string.IsNullOrWhiteSpace(a)) {
+ log.Add(a);
+ StateHasChanged();
+ }
+ }
+ }
+ }
+
+ private async Task<string> Execute(AuthenticatedHomeserverGeneric hs, GenericRoom room) {
+ try {
+ var pls = await room.GetPowerLevelsAsync();
+ // if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) == pls.UsersDefault) return "I am default PL in " + room.RoomId;
+ if (!pls.UserHasPermission(hs.WhoAmI.UserId, RoomPowerLevelEventContent.EventId)) return "I do not have permission to send PL in " + room.RoomId;
+ foreach (var ahs in hss) {
+ if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) == pls.GetUserPowerLevel(ahs.WhoAmI.UserId)) {
+ log.Add("I am same PL in " + room.RoomId);
+ continue;
+ }
+
+ pls.SetUserPowerLevel(ahs.WhoAmI.UserId, pls.GetUserPowerLevel(hs.WhoAmI.UserId));
+ await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls);
+ log.Add($"Updated powerlevel of {room.RoomId} to {pls.GetUserPowerLevel(ahs.WhoAmI.UserId)}");
+ }
+
+ }
+ catch (MatrixException e) {
+ return $"Failed to update PLs in {room.RoomId}: {e.Message}";
+ }
+ catch (Exception e) {
+ return $"Failed to update PLs in {room.RoomId}: {e.Message}";
+ }
+ StateHasChanged();
+ return "";
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/Tools/MassJoinRoom.razor b/MatrixRoomUtils.Web/Pages/Tools/MassJoinRoom.razor
new file mode 100644
index 0000000..bcf8095
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/Tools/MassJoinRoom.razor
@@ -0,0 +1,110 @@
+@page "/MassRoomJoin"
+@using System.Diagnostics
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Homeservers
+@using LibMatrix.RoomTypes
+<h3>Mass join room</h3>
+<hr/>
+<p>Room: </p>
+<FancyTextBox @bind-Value="@roomId"></FancyTextBox>
+
+<p>Users: </p>
+@foreach (var hs in hss) {
+ <p>@hs.WhoAmI.UserId</p>
+}
+
+<br/>
+<LinkButton OnClick="Execute">Execute</LinkButton>
+<br/>
+@foreach (var line in Enumerable.Reverse(log)) {
+ <p>@line</p>
+}
+
+@code {
+ private List<string> log { get; set; } = new();
+ List<AuthenticatedHomeserverGeneric> hss { get; set; } = new();
+ string roomId { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await MRUStorage.GetCurrentSessionOrNavigate();
+ if (hs is null) return;
+ var sessions = await MRUStorage.GetAllTokens();
+ foreach (var userAuth in sessions) {
+ var session = await MRUStorage.GetSession(userAuth);
+ if (session is not null) {
+ hss.Add(session);
+ StateHasChanged();
+ }
+ }
+
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ }
+
+ private async Task Execute() {
+ // foreach (var hs in hss) {
+ // var rooms = await hs.GetJoinedRooms();
+ var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable();
+ await foreach (var a in tasks) {
+ if (!string.IsNullOrWhiteSpace(a)) {
+ log.Add(a);
+ StateHasChanged();
+ }
+ }
+ tasks = hss.Select(ExecuteJoin).ToAsyncEnumerable();
+ await foreach (var a in tasks) {
+ if (!string.IsNullOrWhiteSpace(a)) {
+ log.Add(a);
+ StateHasChanged();
+ }
+ }
+ // }
+ }
+
+ private async Task<string> ExecuteInvite(AuthenticatedHomeserverGeneric hs) {
+ var room = hs.GetRoom(roomId);
+ try {
+ try {
+ var joinRule = await room.GetJoinRuleAsync();
+ if (joinRule.JoinRule == "public") return "Room is public, no invite needed";
+ }
+ catch { }
+ var pls = await room.GetPowerLevelsAsync();
+ if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) < pls.Invite) return "I do not have permission to send invite in " + room.RoomId;
+ await room.InviteUsersAsync(hss.Select(x => x.WhoAmI.UserId).ToList());
+ log.Add($"Invited to {room.RoomId} to {pls.GetUserPowerLevel(hs.WhoAmI.UserId)}");
+ }
+ catch (MatrixException e) {
+ return $"Failed to invite in {room.RoomId}: {e.Message}";
+ }
+ catch (Exception e) {
+ return $"Failed to invite in {room.RoomId}: {e.Message}";
+ }
+ StateHasChanged();
+ return "";
+ }
+
+ private async Task<string> ExecuteJoin(AuthenticatedHomeserverGeneric hs) {
+ var room = hs.GetRoom(roomId);
+ try {
+ try {
+ var mse = await room.GetStateOrNullAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, hs.WhoAmI.UserId);
+ if (mse?.Membership == "join") return $"User {hs.WhoAmI.UserId} already in room";
+ }
+ catch { }
+ await room.JoinAsync();
+ }
+ catch (MatrixException e) {
+ return $"Failed to join {hs.WhoAmI.UserId} to {room.RoomId}: {e.Message}";
+ }
+ catch (Exception e) {
+ return $"Failed to join {hs.WhoAmI.UserId} to {room.RoomId}: {e.Message}";
+ }
+ StateHasChanged();
+ return "";
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/DMManager.razor b/MatrixRoomUtils.Web/Pages/User/DMManager.razor
new file mode 100644
index 0000000..04ff6e5
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/User/DMManager.razor
@@ -0,0 +1,56 @@
+@page "/User/DirectMessages"
+@using LibMatrix.Homeservers
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+<h3>Direct Messages</h3>
+<hr/>
+
+@foreach (var (targetUser, rooms) in DMRooms) {
+ <div>
+ <InlineUserItem User="targetUser"></InlineUserItem>
+ @foreach (var room in rooms) {
+ <RoomListItem RoomInfo="room" LoadData="true"></RoomListItem>
+ }
+ </div>
+}
+
+@code {
+ private string? _status;
+ private AuthenticatedHomeserverGeneric? HomeServer { get; set; }
+ private Dictionary<UserProfileResponse, List<RoomInfo>> DMRooms { get; set; } = new();
+
+ public string? Status {
+ get => _status;
+ set {
+ _status = value;
+ StateHasChanged();
+ }
+ }
+
+ protected override async Task OnInitializedAsync() {
+ HomeServer = await MRUStorage.GetCurrentSessionOrNavigate();
+ if (HomeServer is null) return;
+ Status = "Loading global profile...";
+ if (HomeServer.WhoAmI?.UserId is null) return;
+
+ Status = "Loading DM list from account data...";
+ var dms = await HomeServer.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct");
+ DMRooms.Clear();
+ foreach (var (userId, rooms) in dms) {
+ var roomList = new List<RoomInfo>();
+ DMRooms.Add(await HomeServer.GetProfileAsync(userId), roomList);
+ foreach (var room in rooms) {
+ roomList.Add(new RoomInfo() { Room = HomeServer.GetRoom(room) });
+ }
+ StateHasChanged();
+ }
+
+ StateHasChanged();
+ Status = null;
+
+ await base.OnInitializedAsync();
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpace.razor b/MatrixRoomUtils.Web/Pages/User/DMSpace.razor
new file mode 100644
index 0000000..21cc264
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/User/DMSpace.razor
@@ -0,0 +1,86 @@
+@page "/User/DMSpace/Setup"
+@using LibMatrix.Homeservers
+@using LibMatrix
+@using MatrixRoomUtils.LibDMSpace
+@using MatrixRoomUtils.LibDMSpace.StateEvents
+@using MatrixRoomUtils.Web.Pages.User.DMSpaceStages
+<h3>DM Space Management</h3>
+<hr/>
+<CascadingValue Value="@DmSpace">
+ @switch (Stage) {
+ case -1:
+ <p>Initialising...</p>
+ break;
+ case 0:
+ <DMSpaceStage0/>
+ break;
+ case 1:
+ <DMSpaceStage1/>
+ break;
+ case 2:
+ <DMSpaceStage2/>
+ break;
+ case 3:
+ <DMSpaceStage3/>
+ break;
+ default:
+ <p>Stage is unknown value: @Stage!</p>
+ break;
+ }
+</CascadingValue>
+
+@code {
+ private int _stage = -1;
+
+ [Parameter, SupplyParameterFromQuery(Name = "stage")]
+ public int Stage {
+ get => _stage;
+ set {
+ _stage = value;
+ Console.WriteLine($"Stage is now {value}");
+ StateHasChanged();
+ }
+ }
+
+ public AuthenticatedHomeserverGeneric? Homeserver { get; set; }
+
+ public DMSpaceConfiguration? DmSpaceConfiguration { get; set; }
+
+ [Parameter]
+ public DMSpace? DmSpace { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ if (NavigationManager.Uri.Contains("?stage=")) {
+ NavigationManager.NavigateTo("/User/DMSpace", true);
+ }
+ DmSpace = this;
+ Homeserver ??= await MRUStorage.GetCurrentSessionOrNavigate();
+ if (Homeserver is null) return;
+ try {
+ DmSpaceConfiguration = await Homeserver.GetAccountDataAsync<DMSpaceConfiguration>("gay.rory.dm_space");
+ var room = Homeserver.GetRoom(DmSpaceConfiguration.DMSpaceId);
+ await room.GetStateAsync<object>(DMSpaceInfo.EventId);
+ Stage = 1;
+ }
+ catch (MatrixException e) {
+ if (e.ErrorCode == "M_NOT_FOUND") {
+ Stage = 0;
+ DmSpaceConfiguration = new();
+ }
+ else throw;
+ }
+ catch (Exception e) {
+ throw;
+ }
+ finally {
+ StateHasChanged();
+ }
+ await base.OnInitializedAsync();
+ }
+
+ protected override async Task OnParametersSetAsync() {
+ StateHasChanged();
+ await base.OnParametersSetAsync();
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor
new file mode 100644
index 0000000..49fd5b4
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage0.razor
@@ -0,0 +1,11 @@
+<b>
+ <u>Welcome to the DM Space tool!</u>
+</b>
+<p>This wizard will help you set up a DM space.</p>
+<p>This is useful for eg. sharing DM rooms across multiple accounts.</p>
+<br/>
+<LinkButton href="/User/DMSpace?stage=1">Get started</LinkButton>
+
+@code {
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor
new file mode 100644
index 0000000..3642da5
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage1.razor
@@ -0,0 +1,128 @@
+@using LibMatrix.Homeservers
+@using LibMatrix.RoomTypes
+@using LibMatrix
+@using LibMatrix.Responses
+@using MatrixRoomUtils.LibDMSpace
+@using MatrixRoomUtils.LibDMSpace.StateEvents
+@using Microsoft.Extensions.Primitives
+@using ArcaneLibs.Extensions
+<b>
+ <u>DM Space setup tool - stage 1: Configure space</u>
+</b>
+<p>You will need a space to use for DM rooms.</p>
+@if (DmSpace is not null) {
+ <p>
+ Selected space:
+ <InputSelect @bind-Value="DmSpace.DmSpaceConfiguration.DMSpaceId">
+ @foreach (var (id, name) in spaces) {
+ <option value="@id">@name</option>
+ }
+ </InputSelect>
+ </p>
+ <p>
+ <InputCheckbox @bind-Value="DmSpaceInfo.LayerByUser"></InputCheckbox>
+ Create sub-spaces per user
+ </p>
+}
+else {
+ <b>Error: DmSpaceConfiguration is null!</b>
+}
+
+<br/>
+<LinkButton OnClick="@Execute">Next</LinkButton>
+
+@if (!string.IsNullOrWhiteSpace(Status)) {
+ <p>@Status</p>
+}
+
+@code {
+
+ private string? Status {
+ get => _status;
+ set {
+ _status = value;
+ StateHasChanged();
+ }
+ }
+
+ private Dictionary<string, string> spaces = new() { { "", "New space" } };
+ private string? _status;
+
+ [CascadingParameter]
+ public DMSpace? DmSpace { get; set; }
+
+ public DMSpaceInfo? DmSpaceInfo { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ await base.OnInitializedAsync();
+ }
+
+ SemaphoreSlim _semaphoreSlim = new(1, 1);
+ protected override async Task OnParametersSetAsync() {
+ if (DmSpace is null)
+ return;
+ await _semaphoreSlim.WaitAsync();
+ DmSpace.DmSpaceConfiguration ??= new();
+ if (spaces.Count == 1) {
+ Status = "Looking for spaces...";
+ var userRoomsEnum = DmSpace.Homeserver.GetJoinedRoomsByType("m.space");
+ List<GenericRoom> userRooms = new();
+ await foreach (var room in userRoomsEnum) {
+ userRooms.Add(room);
+ }
+ var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncEnumerable();
+ await foreach(var room in roomChecks)
+ if(room.HasValue)
+ spaces.TryAdd(room.Value.id, room.Value.name);
+
+ Status = "Done!";
+ }
+ _semaphoreSlim.Release();
+ await base.OnParametersSetAsync();
+ }
+
+ private async Task Execute() {
+ if (string.IsNullOrWhiteSpace(DmSpace.DmSpaceConfiguration.DMSpaceId)) {
+ var crr = CreateRoomRequest.CreatePrivate(DmSpace.Homeserver, "Direct Messages");
+ crr._creationContentBaseType.Type = "m.space";
+ DmSpace.DmSpaceConfiguration.DMSpaceId = (await DmSpace.Homeserver.CreateRoom(crr)).RoomId;
+ }
+ await DmSpace.Homeserver!.SetAccountDataAsync(DMSpaceConfiguration.EventId, DmSpace.DmSpaceConfiguration);
+ var space = DmSpace.Homeserver.GetRoom(DmSpace.DmSpaceConfiguration.DMSpaceId);
+ await space.SendStateEventAsync(DMSpaceInfo.EventId, DmSpaceInfo);
+
+ NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=2");
+ }
+
+ public async Task<(string id, string name)?> GetFeasibleSpaces(GenericRoom room) {
+ try {
+ var pls = await room.GetPowerLevelsAsync();
+ if (!pls.UserHasPermission(DmSpace.Homeserver.WhoAmI.UserId, "m.space.child")) {
+ Console.WriteLine($"No permission to send m.space.child in {room.RoomId}...");
+ return null;
+ }
+ var roomName = await room.GetNameAsync();
+ Status = $"Found viable space: {roomName}";
+ if (string.IsNullOrWhiteSpace(DmSpace.DmSpaceConfiguration.DMSpaceId)) {
+ try {
+ var dsi = await DmSpace.Homeserver.GetRoom(room.RoomId).GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) ?? new DMSpaceInfo();
+ if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is not null && dsi is not null) {
+ DmSpace.DmSpaceConfiguration.DMSpaceId = room.RoomId;
+ DmSpaceInfo = dsi;
+ }
+ }
+ catch (MatrixException e) {
+ if (e.ErrorCode == "M_NOT_FOUND") Console.WriteLine($"{room.RoomId} is not a DM space.");
+ else throw;
+ }
+ }
+ return (room.RoomId, roomName);
+ }
+ 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;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor
new file mode 100644
index 0000000..553f46d
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage2.razor
@@ -0,0 +1,241 @@
+@using LibMatrix.Homeservers
+@using LibMatrix.RoomTypes
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@using MatrixRoomUtils.LibDMSpace
+@using MatrixRoomUtils.LibDMSpace.StateEvents
+@using ArcaneLibs.Extensions
+@using System.Text.Json.Serialization
+<b>
+ <u>DM Space setup tool - stage 2: Fix DM room attribution</u>
+</b>
+<p>This is just to make sure that your DMs are attributed to the right person!</p>
+
+@if (!string.IsNullOrWhiteSpace(Status)) {
+ <p>@Status</p>
+}
+
+@if (DmSpace is not null) {
+ @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.Id)) {
+ <InlineUserItem User="@userId"></InlineUserItem>
+ @foreach (var roomInfo in room) {
+ <RoomListItem RoomInfo="@roomInfo">
+ <LinkButton Round="true" OnClick="@(async () => DmToReassign = roomInfo)">Reassign</LinkButton>
+ </RoomListItem>
+ }
+ }
+}
+else {
+ <b>Error: DmSpaceConfiguration is null!</b>
+}
+
+<br/>
+<LinkButton OnClick="@Execute">Next</LinkButton>
+
+@{
+ var _offset = 0;
+}
+@foreach (var (room, usersList) in duplicateDmRooms) {
+ <ModalWindow Title="Duplicate room found" X="_offset += 30" Y="_offset">
+ <p>Found room assigned to multiple users: <RoomListItem RoomInfo="@room"></RoomListItem></p>
+ <p>Users:</p>
+ @foreach (var userProfileResponse in usersList) {
+ <LinkButton OnClick="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))">
+ <span>Assign to </span>
+ <InlineUserItem User="userProfileResponse"></InlineUserItem>
+ </LinkButton>
+ <br/>
+ }
+ </ModalWindow>
+}
+
+@if (DmToReassign is not null) {
+ <ModalWindow Title="Re-assign DM" OnCloseClicked="@(() => DmToReassign = null)">
+ <RoomListItem RoomInfo="@DmToReassign"></RoomListItem>
+ @foreach (var userProfileResponse in roomMembers[DmToReassign]) {
+ <LinkButton OnClick="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))">
+ <span>Assign to </span>
+ <InlineUserItem User="userProfileResponse"></InlineUserItem>
+ </LinkButton>
+ <br/>
+ }
+ </ModalWindow>
+}
+
+@code {
+
+ private string newMxid { get; set; } = "";
+
+ private RoomInfo? DmToReassign {
+ get => _dmToReassign;
+ set {
+ _dmToReassign = value;
+ StateHasChanged();
+ }
+ }
+
+ private string? Status {
+ get => _status;
+ set {
+ _status = value;
+ StateHasChanged();
+ }
+ }
+
+ private string? _status;
+ private RoomInfo? _dmToReassign;
+
+ [CascadingParameter]
+ public DMSpace? DmSpace { get; set; }
+
+ private Dictionary<UserProfileWithId, List<RoomInfo>> dmRooms { get; set; } = new();
+ private Dictionary<RoomInfo, List<UserProfileWithId>> duplicateDmRooms { get; set; } = new();
+ private Dictionary<RoomInfo, List<UserProfileWithId>> roomMembers { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ await base.OnInitializedAsync();
+ }
+
+ SemaphoreSlim _semaphore = new(1, 1);
+
+ protected override async Task OnParametersSetAsync() {
+ if (DmSpace is null)
+ return;
+ await _semaphore.WaitAsync();
+ DmToReassign = null;
+ var hs = DmSpace.Homeserver;
+ Status = "Loading DM list from account data...";
+ var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct");
+ Status = "Optimising DM list from account data...";
+ var joinedRooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList();
+ foreach (var (user, rooms) in dms) {
+ for (var i = rooms.Count - 1; i >= 0; i--) {
+ var roomId = rooms[i];
+ if (!joinedRooms.Contains(roomId))
+ rooms.RemoveAt(i);
+ }
+ dms[user] = rooms.Distinct().ToList();
+ }
+ dms.RemoveAll((x, y) => y is {Count: 0});
+ await DmSpace.Homeserver.SetAccountDataAsync("m.direct", dms);
+ dmRooms.Clear();
+
+ Status = "DM list optimised, fetching info...";
+ var results = dms.Select(async x => {
+ var (userId, rooms) = x;
+ UserProfileWithId userProfile;
+ try {
+ var profile = await DmSpace.Homeserver.GetProfileAsync(userId);
+ userProfile = new() {
+ AvatarUrl = profile.AvatarUrl,
+ Id = userId,
+ DisplayName = profile.DisplayName
+ };
+ }
+ catch {
+ userProfile = new() {
+ AvatarUrl = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK",
+ DisplayName = userId,
+ Id = userId
+ };
+ }
+ var roomList = new List<RoomInfo>();
+ var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable();
+ await foreach (var result in tasks)
+ roomList.Add(result);
+ return (userProfile, roomList);
+ // StateHasChanged();
+ }).ToAsyncEnumerable();
+ await foreach (var res in results) {
+ dmRooms.Add(res.userProfile, res.roomList);
+ // Status = $"Listed {dmRooms.Count} users";
+ }
+ _semaphore.Release();
+ var duplicateDmRoomIds = new Dictionary<string, List<UserProfileWithId>>();
+ foreach (var (user, rooms) in dmRooms) {
+ foreach (var roomInfo in rooms) {
+ if (!duplicateDmRoomIds.ContainsKey(roomInfo.Room.RoomId))
+ duplicateDmRoomIds.Add(roomInfo.Room.RoomId, new());
+ duplicateDmRoomIds[roomInfo.Room.RoomId].Add(user);
+ }
+ }
+ duplicateDmRoomIds.RemoveAll((x, y) => y.Count == 1);
+ foreach (var (roomId, users) in duplicateDmRoomIds) {
+ duplicateDmRooms.Add(dmRooms.First(x => x.Value.Any(x => x.Room.RoomId == roomId)).Value.First(x => x.Room.RoomId == roomId), users);
+ }
+
+ // StateHasChanged();
+ Status = null;
+ await base.OnParametersSetAsync();
+ }
+
+ private async Task Execute() {
+ NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=3");
+ }
+
+ private async Task<RoomInfo> GetRoomInfo(GenericRoom room) {
+ var roomInfo = new RoomInfo() {
+ Room = room
+ };
+ roomMembers[roomInfo] = new();
+ roomInfo.CreationEventContent = await room.GetCreateEventAsync();
+ try {
+ roomInfo.RoomName = await room.GetNameAsync();
+ }
+ catch { }
+
+ var membersEnum = room.GetMembersAsync();
+ await foreach (var member in membersEnum)
+ if (member.TypedContent is RoomMemberEventContent memberEvent && !string.IsNullOrWhiteSpace(memberEvent.Membership) && memberEvent.Membership == "join")
+ roomMembers[roomInfo].Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey });
+
+ if (string.IsNullOrWhiteSpace(roomInfo.RoomName) || roomInfo.RoomName == room.RoomId) {
+ List<string> displayNames = new List<string>();
+ foreach (var member in roomMembers[roomInfo])
+ if (!string.IsNullOrWhiteSpace(member.DisplayName))
+ displayNames.Add(member.DisplayName);
+ roomInfo.RoomName = string.Join(", ", displayNames);
+ }
+ try {
+ string? roomIcon = (await room.GetAvatarUrlAsync())?.Url;
+ if (room is not null)
+ roomInfo.RoomIcon = roomIcon;
+ }
+ catch { }
+ return roomInfo;
+ }
+
+ private async Task<List<RoomInfo>> GetRoomInfoForRooms(List<GenericRoom> rooms) {
+ var tasks = rooms.Select(GetRoomInfo).ToList();
+ await Task.WhenAll(tasks);
+ return tasks.Select(x => x.Result).ToList();
+ }
+
+ private async Task SetRoomAssignment(string roomId, string userId) {
+ var hs = DmSpace.Homeserver;
+ Status = "Loading DM list from account data...";
+ var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct");
+ Status = "Updating DM list from account data...";
+
+ foreach (var (user, rooms) in dms) {
+ rooms.RemoveAll(x => x == roomId);
+ dms[user] = rooms.Distinct().ToList();
+ }
+ if(!dms.ContainsKey(userId))
+ dms.Add(userId, new());
+ dms[userId].Add(roomId);
+ dms.RemoveAll((x, y) => y is {Count: 0});
+ await DmSpace.Homeserver.SetAccountDataAsync("m.direct", dms);
+
+ duplicateDmRooms.RemoveAll((x, y) => x.Room.RoomId == roomId);
+ StateHasChanged();
+ if (duplicateDmRooms.Count == 0) await OnParametersSetAsync();
+ }
+
+ private class UserProfileWithId : UserProfileResponse {
+ [JsonIgnore]
+ public string Id { get; set; }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor
new file mode 100644
index 0000000..854b09c
--- /dev/null
+++ b/MatrixRoomUtils.Web/Pages/User/DMSpaceStages/DMSpaceStage3.razor
@@ -0,0 +1,189 @@
+@using LibMatrix.Homeservers
+@using LibMatrix.RoomTypes
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@using MatrixRoomUtils.LibDMSpace
+@using MatrixRoomUtils.LibDMSpace.StateEvents
+@using ArcaneLibs.Extensions
+@using System.Text.Json.Serialization
+<b>
+ <u>DM Space setup tool - stage 3: Preview space layout</u>
+</b>
+<p>This gives you a preview of how your settings would impact layout!</p>
+
+@if (!string.IsNullOrWhiteSpace(Status)) {
+ <p>@Status</p>
+}
+
+@if (DmSpace is not null) {
+ @if (dmSpaceInfo is not null && dmSpaceRoomInfo is not null) {
+ <p>
+ <InputCheckbox @bind-Value="dmSpaceInfo.LayerByUser"></InputCheckbox>
+ Create sub-spaces per user
+ </p>
+ @if (!dmSpaceInfo.LayerByUser) {
+ <RoomListItem RoomInfo="@dmSpaceRoomInfo"></RoomListItem>
+ @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.RoomName)) {
+ @foreach (var roomInfo in room) {
+ <div style="margin-left: 32px;">
+ <RoomListItem RoomInfo="@roomInfo"></RoomListItem>
+ </div>
+ }
+ }
+ }
+ else {
+ <RoomListItem RoomInfo="@dmSpaceRoomInfo"></RoomListItem>
+ @foreach (var (userId, room) in dmRooms.OrderBy(x => x.Key.RoomName)) {
+ <div style="margin-left: 32px;">
+ <RoomListItem RoomInfo="@userId"></RoomListItem>
+ </div>
+ @foreach (var roomInfo in room) {
+ <div style="margin-left: 64px;">
+ <RoomListItem RoomInfo="@roomInfo"></RoomListItem>
+ </div>
+ }
+ }
+ }
+ }
+ else {
+ <b>Error: dmSpaceInfo is null!</b>
+ }
+}
+else {
+ <b>Error: DmSpaceConfiguration is null!</b>
+}
+
+<br/>
+<LinkButton OnClick="@Execute">Next</LinkButton>
+
+@code {
+
+ private string? Status {
+ get => _status;
+ set {
+ _status = value;
+ StateHasChanged();
+ }
+ }
+
+ private string? _status;
+
+ [CascadingParameter]
+ public DMSpace? DmSpace { get; set; }
+
+ private Dictionary<RoomInfo, List<RoomInfo>> dmRooms { get; set; } = new();
+ private DMSpaceInfo? dmSpaceInfo { get; set; }
+ private RoomInfo? dmSpaceRoomInfo { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ await base.OnInitializedAsync();
+ }
+
+ SemaphoreSlim _semaphore = new(1, 1);
+
+ protected override async Task OnParametersSetAsync() {
+ if (DmSpace is null)
+ return;
+ await _semaphore.WaitAsync();
+ var hs = DmSpace.Homeserver;
+ var dmSpaceRoom = new DMSpaceRoom(hs, DmSpace.DmSpaceConfiguration.DMSpaceId);
+ dmSpaceRoomInfo = new() {
+ RoomName = await dmSpaceRoom.GetNameAsync(),
+ CreationEventContent = await dmSpaceRoom.GetCreateEventAsync(),
+ RoomIcon = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK",
+ Room = dmSpaceRoom
+ };
+ dmSpaceInfo = await dmSpaceRoom.GetDmSpaceInfo();
+ Status = "Loading DM list from account data...";
+ var dms = await DmSpace.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct");
+ dmRooms.Clear();
+
+ Status = "DM list optimised, fetching info...";
+ var results = dms.Select(async x => {
+ var (userId, rooms) = x;
+ UserProfileWithId userProfile;
+ try {
+ var profile = await DmSpace.Homeserver.GetProfileAsync(userId);
+ userProfile = new() {
+ AvatarUrl = profile.AvatarUrl,
+ Id = userId,
+ DisplayName = profile.DisplayName
+ };
+ }
+ catch {
+ userProfile = new() {
+ AvatarUrl = "mxc://feline.support/uUxBwaboPkMGtbZcAGZaIzpK",
+ DisplayName = userId,
+ Id = userId
+ };
+ }
+ var roomList = new List<RoomInfo>();
+ var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable();
+ await foreach (var result in tasks)
+ roomList.Add(result);
+ return (userProfile, roomList);
+ }).ToAsyncEnumerable();
+ await foreach (var res in results) {
+ dmRooms.Add(new RoomInfo() {
+ Room = dmSpaceRoom,
+ RoomIcon = res.userProfile.AvatarUrl,
+ RoomName = res.userProfile.DisplayName,
+ CreationEventContent = await dmSpaceRoom.GetCreateEventAsync()
+ }, res.roomList);
+ }
+ _semaphore.Release();
+ Status = null;
+ await base.OnParametersSetAsync();
+ }
+
+ private async Task Execute() {
+ var hs = DmSpace.Homeserver;
+ var dmSpaceRoom = new DMSpaceRoom(hs, DmSpace.DmSpaceConfiguration.DMSpaceId);
+ NavigationManager.NavigateTo("/User/DMSpace/Setup?stage=3");
+ }
+
+ private async Task<RoomInfo> GetRoomInfo(GenericRoom room) {
+ var roomInfo = new RoomInfo() {
+ Room = room
+ };
+ var roomMembers = new List<UserProfileWithId>();
+ roomInfo.CreationEventContent = await room.GetCreateEventAsync();
+ try {
+ roomInfo.RoomName = await room.GetNameAsync();
+ }
+ catch { }
+
+ var membersEnum = room.GetMembersAsync();
+ await foreach (var member in membersEnum)
+ if (member.TypedContent is RoomMemberEventContent memberEvent && !string.IsNullOrWhiteSpace(memberEvent.Membership) && memberEvent.Membership == "join")
+ roomMembers.Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey });
+
+ if (string.IsNullOrWhiteSpace(roomInfo.RoomName) || roomInfo.RoomName == room.RoomId) {
+ List<string> displayNames = new List<string>();
+ foreach (var member in roomMembers)
+ if (!string.IsNullOrWhiteSpace(member.DisplayName))
+ displayNames.Add(member.DisplayName);
+ roomInfo.RoomName = string.Join(", ", displayNames);
+ }
+ try {
+ string? roomIcon = (await room.GetAvatarUrlAsync())?.Url;
+ if (room is not null)
+ roomInfo.RoomIcon = roomIcon;
+ }
+ catch { }
+ return roomInfo;
+ }
+
+ private async Task<List<RoomInfo>> GetRoomInfoForRooms(List<GenericRoom> rooms) {
+ var tasks = rooms.Select(GetRoomInfo).ToList();
+ await Task.WhenAll(tasks);
+ return tasks.Select(x => x.Result).ToList();
+ }
+
+ private class UserProfileWithId : UserProfileResponse {
+ [JsonIgnore]
+ public string Id { get; set; }
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Pages/User/Manage.razor b/MatrixRoomUtils.Web/Pages/User/Manage.razor
index 25debf7..eac47e8 100644
--- a/MatrixRoomUtils.Web/Pages/User/Manage.razor
+++ b/MatrixRoomUtils.Web/Pages/User/Manage.razor
@@ -1,8 +1,9 @@
-@page "/User/Manage"
+@page "/User/Profile"
@using LibMatrix.Homeservers
@using LibMatrix.EventTypes.Spec.State
@using ArcaneLibs.Extensions
-<h3>Manage user - @HomeServer?.WhoAmI?.UserId</h3>
+@using LibMatrix.Responses
+<h3>Manage Profile - @HomeServer?.WhoAmI?.UserId</h3>
<hr/>
@if (Profile is not null) {
@@ -48,8 +49,8 @@
private string? _status = null;
private AuthenticatedHomeserverGeneric? HomeServer { get; set; }
- private ProfileResponseEventContent? Profile { get; set; }
- private ProfileResponseEventContent? OldProfile { get; set; }
+ private UserProfileResponse? Profile { get; set; }
+ private UserProfileResponse? OldProfile { get; set; }
private string? Status {
get => _status;
@@ -77,7 +78,7 @@
var roomNameTasks = RoomProfiles.Keys.Select(x => HomeServer.GetRoom(x)).Select(async x => {
var name = await x.GetNameAsync();
- return new KeyValuePair<string, string>(x.RoomId, name);
+ return new KeyValuePair<string, string?>(x.RoomId, name);
}).ToAsyncEnumerable();
await foreach (var (roomId, roomName) in roomNameTasks) {
// Status = $"Got room name for {roomId}: {roomName}";
|