From 8838a3b20ba95bca34954b6ec828991adb028d4d Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 10 Mar 2025 07:41:43 +0100 Subject: Various work --- ModerationClient/App.axaml.cs | 4 + ModerationClient/Models/SpaceTreeNodes/RoomNode.cs | 5 + ModerationClient/ModerationClient.csproj | 5 +- ModerationClient/Properties/launchSettings.json | 26 +++++ ModerationClient/Services/ClientContainer.cs | 34 +++++- .../Services/CommandLineConfiguration.cs | 8 +- .../Services/MatrixAuthenticationService.cs | 12 +- ModerationClient/Services/StatusBarService.cs | 18 +++ ModerationClient/ViewLocator.cs | 5 +- ModerationClient/ViewModels/ClientViewModel.cs | 128 ++++++++++++++------- ModerationClient/ViewModels/EventViewModel.cs | 9 ++ ModerationClient/ViewModels/LoginViewModel.cs | 13 ++- ModerationClient/ViewModels/RoomViewModel.cs | 113 ++++++++++++++++++ .../UserManagement/UserManagementViewModel.cs | 8 +- ModerationClient/Views/MainWindow/ClientView.axaml | 9 +- ModerationClient/Views/MainWindow/EventView.axaml | 10 ++ .../Views/MainWindow/EventView.axaml.cs | 11 ++ ModerationClient/Views/MainWindow/LoginView.axaml | 4 + .../Views/MainWindow/MainWindow.axaml.cs | 6 +- ModerationClient/Views/MainWindow/RoomView.axaml | 53 +++++++++ .../Views/MainWindow/RoomView.axaml.cs | 11 ++ ModerationClient/Views/MainWindow/StatusBar.axaml | 10 ++ .../Views/MainWindow/StatusBar.axaml.cs | 15 +++ ModerationClient/Views/UserManagementWindow.axaml | 2 +- .../Views/UserManagementWindow.axaml.cs | 17 ++- ModerationClient/avatar.png | 0 26 files changed, 465 insertions(+), 71 deletions(-) create mode 100644 ModerationClient/Properties/launchSettings.json create mode 100644 ModerationClient/ViewModels/EventViewModel.cs create mode 100644 ModerationClient/ViewModels/RoomViewModel.cs create mode 100644 ModerationClient/Views/MainWindow/EventView.axaml create mode 100644 ModerationClient/Views/MainWindow/EventView.axaml.cs create mode 100644 ModerationClient/Views/MainWindow/RoomView.axaml create mode 100644 ModerationClient/Views/MainWindow/RoomView.axaml.cs create mode 100644 ModerationClient/Views/MainWindow/StatusBar.axaml create mode 100644 ModerationClient/Views/MainWindow/StatusBar.axaml.cs create mode 100644 ModerationClient/avatar.png (limited to 'ModerationClient') diff --git a/ModerationClient/App.axaml.cs b/ModerationClient/App.axaml.cs index b15c0fa..7addc19 100644 --- a/ModerationClient/App.axaml.cs +++ b/ModerationClient/App.axaml.cs @@ -70,11 +70,15 @@ public partial class App : Application { // Register views services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // Register ViewModels services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); if (cfg.TestConfiguration is not null) { services.AddSingleton(cfg.TestConfiguration); diff --git a/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs index 76d5aa9..98b923a 100644 --- a/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs +++ b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs @@ -1,4 +1,6 @@ +using System.Collections.ObjectModel; using ArcaneLibs; +using LibMatrix; namespace ModerationClient.Models.SpaceTreeNodes; @@ -11,4 +13,7 @@ public class RoomNode : NotifyPropertyChanged { get => _name; set => SetField(ref _name, value); } + + public ObservableCollection Timeline { get; } = new(); + public ObservableCollection State { get; } = new(); } \ No newline at end of file diff --git a/ModerationClient/ModerationClient.csproj b/ModerationClient/ModerationClient.csproj index c64d0c3..bf95720 100644 --- a/ModerationClient/ModerationClient.csproj +++ b/ModerationClient/ModerationClient.csproj @@ -1,11 +1,12 @@ - + WinExe - net8.0 + net9.0 enable true app.manifest true + preview diff --git a/ModerationClient/Properties/launchSettings.json b/ModerationClient/Properties/launchSettings.json new file mode 100644 index 0000000..997e294 --- /dev/null +++ b/ModerationClient/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Default": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + + } + }, + "Development": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Local config": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Local" + } + } + } +} diff --git a/ModerationClient/Services/ClientContainer.cs b/ModerationClient/Services/ClientContainer.cs index fa3abef..957e3cc 100644 --- a/ModerationClient/Services/ClientContainer.cs +++ b/ModerationClient/Services/ClientContainer.cs @@ -1,8 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + namespace ModerationClient.Services; public class ClientContainer { - public ClientContainer(MatrixAuthenticationService authService, CommandLineConfiguration cfg) + private readonly ILogger _logger; + private readonly MatrixAuthenticationService _authService; + private readonly CommandLineConfiguration _cfg; + + public ClientContainer(ILogger logger, MatrixAuthenticationService authService, CommandLineConfiguration cfg) { + _logger = logger; + _authService = authService; + _cfg = cfg; + } + + private bool _isRunning = false; + + public void EnsureRunning() + { + if (_isRunning) return; + _isRunning = true; + _ = Task.Run(Run).ContinueWith(t => { + if (t.IsFaulted) + { + _logger.LogError(t.Exception, "Error in client container task"); + } + return _isRunning = false; + }); + } + + private async Task Run() { + Console.WriteLine("Running client view model loop..."); + ArgumentNullException.ThrowIfNull(_authService.Homeserver, nameof(_authService.Homeserver)); } } \ No newline at end of file diff --git a/ModerationClient/Services/CommandLineConfiguration.cs b/ModerationClient/Services/CommandLineConfiguration.cs index 4f7da2d..fcb5072 100644 --- a/ModerationClient/Services/CommandLineConfiguration.cs +++ b/ModerationClient/Services/CommandLineConfiguration.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Text.Json; +using System.Text.Json.Serialization; using ArcaneLibs; using ArcaneLibs.Extensions; @@ -35,10 +37,12 @@ public record CommandLineConfiguration { List args = new(); if (Profile != current.Profile) args.AddRange(["--profile", Profile]); if (IsTemporary) args.Add("--temporary"); - if (Math.Abs(Scale - 1f) > float.Epsilon) args.AddRange(["--scale", Scale.ToString()]); + if (Math.Abs(Scale - 1f) > float.Epsilon) args.AddRange(["--scale", Scale.ToString(CultureInfo.InvariantCulture)]); if (ProfileDirectory != current.ProfileDirectory) args.AddRange(["--profile-dir", ProfileDirectory]); if (!string.IsNullOrWhiteSpace(_loginData) && _loginData != current.LoginData) args.AddRange(["--login-data", _loginData!]); if (TestConfiguration is not null && TestConfiguration != current.TestConfiguration) args.AddRange(["--test-config", TestConfiguration!.ToJson()]); + + Console.WriteLine("Serialised CommandLineConfiguration: " + string.Join(" ", args)); return args.ToArray(); } public static CommandLineConfiguration FromSerialised(string[] args) { @@ -85,11 +89,13 @@ public record CommandLineConfiguration { } } + [JsonIgnore] private string? testConfiguration { get => TestConfiguration?.ToJson(); init => TestConfiguration = value is null ? null : JsonSerializer.Deserialize(value); } + [JsonIgnore] public TestConfig? TestConfiguration { get; init; } public class TestConfig { diff --git a/ModerationClient/Services/MatrixAuthenticationService.cs b/ModerationClient/Services/MatrixAuthenticationService.cs index 46ec067..e4fb99b 100644 --- a/ModerationClient/Services/MatrixAuthenticationService.cs +++ b/ModerationClient/Services/MatrixAuthenticationService.cs @@ -28,6 +28,7 @@ public class MatrixAuthenticationService(ILogger lo if (login is null) return; try { Homeserver = await hsProvider.GetAuthenticatedWithToken(login.Homeserver, login.AccessToken); + await Homeserver.UpdateProfilePropertyAsync("meow", "h"); IsLoggedIn = true; } catch (MatrixException e) { @@ -35,10 +36,15 @@ public class MatrixAuthenticationService(ILogger lo } } - public async Task LoginAsync(string username, string password) { + public async Task LoginAsync(string username, string password, string? homeserver = null) { Directory.CreateDirectory(Util.ExpandPath($"{cfg.ProfileDirectory}")!); - var mxidParts = username.Split(':', 2); - var res = await hsProvider.Login(mxidParts[1], username, password); + + if (string.IsNullOrWhiteSpace(homeserver)) { + var mxidParts = username.Split(':', 2); + homeserver = mxidParts[1]; + } + + var res = await hsProvider.Login(homeserver, username, password); await File.WriteAllTextAsync(Path.Combine(cfg.ProfileDirectory, "login.json"), res.ToJson()); await LoadProfileAsync(); diff --git a/ModerationClient/Services/StatusBarService.cs b/ModerationClient/Services/StatusBarService.cs index 57aff21..f1d7223 100644 --- a/ModerationClient/Services/StatusBarService.cs +++ b/ModerationClient/Services/StatusBarService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.ObjectModel; using ArcaneLibs; namespace ModerationClient.Services; @@ -16,4 +17,21 @@ public class StatusBarService : NotifyPropertyChanged { get => _isBusy; set => SetField(ref _isBusy, value); } + + public ObservableCollection ProgressBars { get; } = new(); + + + public class Progress : NotifyPropertyChanged { + public Progress(int total) { + Total = total; + } + + public int Total { get; } + public int Current { get; private set; } + + public void Increment() { + Current++; + OnPropertyChanged(nameof(Current)); + } + } } \ No newline at end of file diff --git a/ModerationClient/ViewLocator.cs b/ModerationClient/ViewLocator.cs index 3de8107..73ceef4 100644 --- a/ModerationClient/ViewLocator.cs +++ b/ModerationClient/ViewLocator.cs @@ -9,14 +9,15 @@ namespace ModerationClient; public class ViewLocator : IDataTemplate { public Control? Build(object? data) { try { + Console.WriteLine($"ViewLocator: Searching viewmodel for {data?.GetType().FullName ?? "null"}"); if (data is null) return null; - + var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); Console.WriteLine($"ViewLocator: Locating {name} for {data.GetType().FullName}"); var type = Type.GetType(name); Console.WriteLine($"ViewLocator: Got {type?.FullName ?? "null"}"); - + if (type != null) { var control = (Control)App.Current.Services.GetRequiredService(type); Console.WriteLine($"ViewLocator: Created {control.GetType().FullName}"); diff --git a/ModerationClient/ViewModels/ClientViewModel.cs b/ModerationClient/ViewModels/ClientViewModel.cs index fb3681e..ab4f2da 100644 --- a/ModerationClient/ViewModels/ClientViewModel.cs +++ b/ModerationClient/ViewModels/ClientViewModel.cs @@ -6,14 +6,18 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using ArcaneLibs; using ArcaneLibs.Collections; +using ArcaneLibs.Extensions; using LibMatrix; using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Helpers; using LibMatrix.Responses; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using ModerationClient.Models.SpaceTreeNodes; using ModerationClient.Services; @@ -40,60 +44,111 @@ public partial class ClientViewModel : ViewModelBase { private SpaceNode? _currentSpace; private readonly SpaceNode _allRoomsNode; private string _status = "Loading..."; + private RoomNode? _currentRoom; public ObservableCollection DisplayedSpaces { get; } = []; public ObservableDictionary AllRooms { get; } = new(); public SpaceNode DirectMessages { get; } - public bool Paused { get; set; } = false; + public bool Paused { get; set; } = true; + + public string Status { + get => _status + " " + DateTime.Now; + set => SetProperty(ref _status, value); + } public SpaceNode CurrentSpace { get => _currentSpace ?? _allRoomsNode; set => SetProperty(ref _currentSpace, value); } - public string Status { - get => _status + " " + DateTime.Now; - set => SetProperty(ref _status, value); + public RoomNode? CurrentRoom { + get => _currentRoom; + set { + if (SetProperty(ref _currentRoom, value)) OnPropertyChanged(nameof(CurrentRoomViewModel)); + } } + public RoomViewModel? CurrentRoomViewModel => CurrentRoom is not null ? new RoomViewModel(_authService.Homeserver!, CurrentRoom) : null; + public async Task Run() { Console.WriteLine("Running client view model loop..."); ArgumentNullException.ThrowIfNull(_authService.Homeserver, nameof(_authService.Homeserver)); // var sh = new SyncStateResolver(_authService.Homeserver, _logger, storageProvider: new FileStorageProvider(Path.Combine(_cfg.ProfileDirectory, "syncCache"))); var store = new FileStorageProvider(Path.Combine(_cfg.ProfileDirectory, "syncCache")); + var mediaCache = new FileStorageProvider(Path.Combine(_cfg.ProfileDirectory, "mediaCache")); Console.WriteLine($"Sync store at {store.TargetPath}"); - var sh = new SyncHelper(_authService.Homeserver, _logger, storageProvider: store) { + var sh = new SyncHelper(_authService.Homeserver, new NullLogger(), storageProvider: store) { // MinimumDelay = TimeSpan.FromSeconds(1) }; Console.WriteLine("Sync helper created."); + var unoptimised = await sh.GetUnoptimisedStoreCount(); // this is slow, so we cache //optimise - we create a new scope here to make ssr go out of scope - // if((await sh.GetUnoptimisedStoreCount()) > 1000) - { + if (unoptimised > 100) { Console.WriteLine("RUN - Optimising sync store..."); Status = "Optimising sync store, please wait..."; var ssr = new SyncStateResolver(_authService.Homeserver, _logger, storageProvider: store); Console.WriteLine("Created sync state resolver..."); Status = "Optimising sync store, please wait... Creating new snapshot..."; - await ssr.OptimiseStore(); + await ssr.OptimiseStore((remaining, total) => { + if (remaining % 25 == 0) + Status = $"Optimising sync store, please wait... {remaining}/{total} remaining..."; + }); Status = "Optimising sync store, please wait... Deleting old intermediate snapshots..."; await ssr.RemoveOldSnapshots(); + for (int gen = 0; gen < GC.MaxGeneration; gen++) { + Status = $"Collecting garbage #{gen}: {Util.BytesToString(GC.GetGCMemoryInfo().TotalCommittedBytes)}"; + await Task.Delay(1000); + } + + Status = $"Collecting garbage: {GC.GetTotalMemory(true) / 1024 / 1024}MB"; + await Task.Delay(1000); } - var unoptimised = await sh.GetUnoptimisedStoreCount(); // this is slow, so we cache + GC.Collect(); + unoptimised = 0; Status = "Doing initial sync..."; + var currentSyncRes = "init"; + var lastGc = DateTime.Now; await foreach (var res in sh.EnumerateSyncAsync()) { - Program.Beep((short)250, 0); + var sw = Stopwatch.StartNew(); + //log thing + + // Program.Beep((short)250, 0); Status = $"Processing sync... {res.NextBatch}"; + + foreach (var (key, value) in res.Rooms?.Leave ?? []) { + List events = [..value.Timeline?.Events ?? [], ..value.State?.Events ?? []]; + var ownEvents = events.Where(x => x is { StateKey: "@emma:rory.gay", Type: RoomMemberEventContent.EventId }).ToList(); + foreach (var evt in ownEvents) { + var ct = evt.ContentAs()!; + Console.WriteLine($"Room {key,60} removed: {evt.Type} {evt.StateKey} by {evt.Sender,80}, membership: {ct.Membership,6}, reason: {ct.Reason}"); + } + } + + if (res.Rooms?.Leave is { Count: > 0 }) + Console.WriteLine($"Processed {res.Rooms?.Leave?.Count} left rooms"); + // await Task.Delay(10000); + + var applySw = Stopwatch.StartNew(); await ApplySyncChanges(res); + applySw.Stop(); Program.Beep(0, 0); + if (DateTime.Now - lastGc > TimeSpan.FromMinutes(1)) { + lastGc = DateTime.Now; + GC.Collect(); + } + + Console.WriteLine($"Processed sync {currentSyncRes} in {sw.ElapsedMilliseconds}ms (applied in: {applySw.ElapsedMilliseconds}ms)"); if (Paused) { Status = "Sync loop interrupted... Press pause/break to resume."; while (Paused) await Task.Delay(1000); } - else Status = $"Syncing... {unoptimised++} unoptimised sync responses..."; + else Status = $"Syncing... {unoptimised++} unoptimised sync responses, last={currentSyncRes}..."; + + currentSyncRes = res.NextBatch; } } @@ -113,7 +168,7 @@ public partial class ClientViewModel : ViewModelBase { } if (room.Value.State?.Events is not null) { - var nameEvent = room.Value.State!.Events!.FirstOrDefault(x => x.Type == "m.room.name" && x.StateKey == ""); + var nameEvent = room.Value.State!.Events!.FirstOrDefault(x => x is { Type: "m.room.name", StateKey: "" }); if (nameEvent is not null) AllRooms[room.Key].Name = (nameEvent?.TypedContent as RoomNameEventContent)?.Name ?? ""; } @@ -124,36 +179,24 @@ public partial class ClientViewModel : ViewModelBase { // Status = $"Getting room name for {room.Key}..."; // AllRooms[room.Key].Name = await _authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync(); } - } - await AwaitTasks(tasks, "Waiting for {0}/{1} tasks while applying room changes..."); + if (room.Value.Timeline?.Events is not null) { + foreach (var evt in room.Value.Timeline!.Events!) { + AllRooms[room.Key].Timeline.Add(evt); + } + } - return; - - List handledRoomIds = []; - var spaces = newSync.Rooms?.Join? - .Where(x => x.Value.State?.Events is not null) - .Where(x => x.Value.State!.Events!.Any(y => y.Type == "m.room.create" && (y.TypedContent as RoomCreateEventContent)!.Type == "m.space")) - .ToList(); - Console.WriteLine("spaces: " + spaces.Count); - var nonRootSpaces = spaces - .Where(x => spaces.Any(x => x.Value.State!.Events!.Any(y => y.Type == "m.space.child" && y.StateKey == x.Key))) - .ToDictionary(); - - var rootSpaces = spaces - .Where(x => !nonRootSpaces.ContainsKey(x.Key)) - .ToDictionary(); - // var rootSpaces = spaces - // .Where(x=>!spaces.Any(x=>x.Value.State!.Events!.Any(y=>y.Type == "m.space.child" && y.StateKey == x.Key))) - // .ToList(); - - foreach (var (roomId, room) in rootSpaces) { - var space = new SpaceNode { Name = (room.State!.Events!.First(x => x.Type == "m.room.name")!.TypedContent as RoomNameEventContent).Name }; - DisplayedSpaces.Add(space); - handledRoomIds.Add(roomId); + if (room.Value.State?.Events is not null) { + foreach (var evt in room.Value.State!.Events!) { + AllRooms[room.Key].State.Add(evt); + } + } } + + await AwaitTasks(tasks, "Waiting for {0}/{1} tasks while applying room changes..."); } + private ExpiringSemaphoreCache _profileCache = new(); private async Task ApplyDirectMessagesChanges(StateEventResponse evt) { _logger.LogCritical("Direct messages updated!"); var dms = evt.RawContent.Deserialize>(); @@ -163,8 +206,10 @@ public partial class ClientViewModel : ViewModelBase { var space = DirectMessages.ChildSpaces.FirstOrDefault(x => x.RoomID == userId); if (space is null) { space = new SpaceNode { Name = userId, RoomID = userId }; - tasks.Add(_authService.Homeserver!.GetProfileAsync(userId) - .ContinueWith(r => space.Name = string.IsNullOrWhiteSpace(r.Result?.DisplayName) ? userId : r.Result.DisplayName)); + // tasks.Add(_authService.Homeserver!.GetProfileAsync(userId) + // .ContinueWith(r => space.Name = string.IsNullOrWhiteSpace(r.Result.DisplayName) ? userId : r.Result.DisplayName)); + tasks.Add(_profileCache.GetOrAdd(userId, async () => await _authService.Homeserver!.GetProfileAsync(userId), TimeSpan.FromMinutes(5)) + .ContinueWith(r => string.IsNullOrWhiteSpace(r.Result.DisplayName) ? userId : r.Result.DisplayName)); DirectMessages.ChildSpaces.Add(space); } @@ -191,9 +236,10 @@ public partial class ClientViewModel : ViewModelBase { int total = tasks.Count; while (tasks.Any(x => !x.IsCompleted)) { int incomplete = tasks.Count(x => !x.IsCompleted); - Program.Beep((short)MathUtil.Map(incomplete, 0, total, 20, 7500), 5); + // Program.Beep((short)MathUtil.Map(incomplete, 0, total, 20, 7500), 5); // Program.Beep(0, 0); - Status = string.Format(message, incomplete, total); + if (incomplete < 10 || incomplete % (total / 10) == 0) + Status = string.Format(message, incomplete, total); await Task.WhenAny(tasks); tasks.RemoveAll(x => x.IsCompleted); } diff --git a/ModerationClient/ViewModels/EventViewModel.cs b/ModerationClient/ViewModels/EventViewModel.cs new file mode 100644 index 0000000..fe5de32 --- /dev/null +++ b/ModerationClient/ViewModels/EventViewModel.cs @@ -0,0 +1,9 @@ +namespace ModerationClient.ViewModels; + +public class EventViewModel : ViewModelBase { + + public EventViewModel() { + + } + +} \ No newline at end of file diff --git a/ModerationClient/ViewModels/LoginViewModel.cs b/ModerationClient/ViewModels/LoginViewModel.cs index 32f0d6e..34c8d28 100644 --- a/ModerationClient/ViewModels/LoginViewModel.cs +++ b/ModerationClient/ViewModels/LoginViewModel.cs @@ -1,13 +1,15 @@ using System; using System.Threading.Tasks; +using LibMatrix.Services; using ModerationClient.Services; namespace ModerationClient.ViewModels; -public partial class LoginViewModel(MatrixAuthenticationService authService) : ViewModelBase +public partial class LoginViewModel(MatrixAuthenticationService authService, HomeserverResolverService hsResolver) : ViewModelBase { private Exception? _exception; public string Username { get; set; } + public string? Homeserver { get; set; } public string Password { get; set; } public Exception? Exception { @@ -15,6 +17,15 @@ public partial class LoginViewModel(MatrixAuthenticationService authService) : V private set => SetProperty(ref _exception, value); } + public async Task ResolveHomeserverAsync() { + try { + string[] parts = Username.Split(':', 2); + Homeserver = (await hsResolver.ResolveHomeserverFromWellKnown(Username, enableServer: false)).Client; + } catch (Exception e) { + Console.WriteLine(e); + } + } + public async Task LoginAsync() { try { Exception = null; diff --git a/ModerationClient/ViewModels/RoomViewModel.cs b/ModerationClient/ViewModels/RoomViewModel.cs new file mode 100644 index 0000000..c18b842 --- /dev/null +++ b/ModerationClient/ViewModels/RoomViewModel.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Specialized; +using System.ComponentModel; +using System.IO; +using System.Linq; +using ArcaneLibs; +using Avalonia; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using LibMatrix; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Homeservers; +using ModerationClient.Models.SpaceTreeNodes; + +namespace ModerationClient.ViewModels; + +public class RoomViewModel : ViewModelBase { + public RoomViewModel(AuthenticatedHomeserverGeneric homeserver, RoomNode room) { + Homeserver = homeserver; + Room = room; + room.Timeline.CollectionChanged += (_, args) => { + foreach (var obj in args.NewItems ?? ImmutableList.Empty) { + if (obj is StateEventResponse evt) { + if (evt.Type == "m.room.member") + Users = room.State.Where(x => x.Type == "m.room.member").Select(x => { + var tc = x.TypedContent as RoomMemberEventContent; + return new RoomMember(homeserver) { + AvatarUrl = tc.AvatarUrl, + DisplayName = tc.DisplayName, + UserId = x.StateKey! + }; + }).ToFrozenSet(); + } + } + }; + + Users = room.State.Where(x => x.Type == "m.room.member" && x.ContentAs()?.Membership == "join").Select(x => { + var tc = x.TypedContent as RoomMemberEventContent; + return new RoomMember(homeserver) { + AvatarUrl = tc.AvatarUrl, + DisplayName = tc.DisplayName, + UserId = x.StateKey! + }; + }).ToFrozenSet(); + } + + public AuthenticatedHomeserverGeneric Homeserver { get; } + public RoomNode Room { get; } + public Bitmap? RoomIconBitmap { get; } + public FrozenSet Users { get; private set; } = FrozenSet.Empty; +} + +public class RoomMember(AuthenticatedHomeserverGeneric homeserver) : NotifyPropertyChanged { + public string UserId { get; set; } + public string DisplayName { get; set; } + private static readonly string MediaDir = Path.Combine("/tmp", "ModerationClient", "media"); + + public string AvatarUrl { + get; + set { + field = value; + + if(!Directory.Exists(MediaDir)) + Directory.CreateDirectory(MediaDir); + + var fsPath = Path.Combine(MediaDir, AvatarUrl.Replace("/", "_")); + Console.WriteLine($"Avatar path for {UserId}: {fsPath}"); + if (File.Exists(fsPath)) { + UserAvatarBitmap = new Bitmap(fsPath); + Console.WriteLine($"Avatar bitmap for {UserId} loaded: {UserAvatarBitmap.GetHashCode()}, Path: {fsPath}"); + return; + } + + homeserver.GetMediaStreamAsync(field).ContinueWith(async streamTask => { + try { + await using var stream = await streamTask; + Console.WriteLine($"Avatar stream for {UserId} received: {stream.GetHashCode()}"); + // await using var ms = new MemoryStream(); + // await stream.CopyToAsync(ms); + // var fs = new FileStream("avatar.png", FileMode.Create); + // ms.Seek(0, SeekOrigin.Begin); + // Console.WriteLine($"Avatar stream for {UserId} copied: {ms.GetHashCode()}"); + // var bm = new Bitmap(ms); + // Console.WriteLine($"Avatar bitmap for {UserId} loaded: {bm.GetHashCode()}"); + // var sbm = bm.CreateScaledBitmap(new(32, 32)); + // Console.WriteLine($"Avatar bitmap for {UserId} loaded: {sbm.GetHashCode()}"); + // UserAvatarBitmap = sbm; + // Console.WriteLine($"Bitmap for {UserId} set to {UserAvatarBitmap.GetHashCode()}"); + + var fs = new FileStream(fsPath, FileMode.Create); + await stream.CopyToAsync(fs); + fs.Close(); + UserAvatarBitmap = new Bitmap(fsPath); + Console.WriteLine($"Avatar bitmap for {UserId} loaded: {UserAvatarBitmap.GetHashCode()}, Path: {fsPath}"); + } + catch (Exception e) { + Console.WriteLine($"Failed to load avatar for {UserId}: {e}"); + UserAvatarBitmap = new Bitmap(PixelFormat.Rgba8888, AlphaFormat.Unpremul, 1, PixelSize.Empty, Vector.Zero, 0); + } + }); + // UserAvatarBitmap = new Bitmap(); + } + } + + public Bitmap UserAvatarBitmap { + get; + private set => SetField(ref field, value); + } +} \ No newline at end of file diff --git a/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs b/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs index 90020d6..0e99298 100644 --- a/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs +++ b/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs @@ -25,7 +25,7 @@ public partial class UserManagementViewModel : ViewModelBase { private readonly MatrixAuthenticationService _authService; private readonly CommandLineConfiguration _cfg; private string _status = "Loading..."; - public ObservableCollection Users { get; set; } = []; + public ObservableCollection Users { get; set; } = []; public string Status { get => _status + " " + DateTime.Now; @@ -34,7 +34,7 @@ public partial class UserManagementViewModel : ViewModelBase { public async Task Run() { Users.Clear(); - Status = "Doing initial sync..."; + Status = "Fetching user list..."; if (_authService.Homeserver is not AuthenticatedHomeserverSynapse synapse) { Console.WriteLine("This client only supports Synapse homeservers."); return; @@ -43,12 +43,12 @@ public partial class UserManagementViewModel : ViewModelBase { await foreach (var user in synapse.Admin.SearchUsersAsync(chunkLimit: 100)) { Program.Beep(250, 1); Console.WriteLine("USERMANAGER GOT USER: " + user.ToJson(indent:false, ignoreNull: true)); - Users.Add(JsonSerializer.Deserialize(user.ToJson())!); + Users.Add(JsonSerializer.Deserialize(user.ToJson())!); } Console.WriteLine("Done."); } } -public class User : SynapseAdminUserListResult.SynapseAdminUserListResultUser { +public class SynapseAdminUser : SynapseAdminUserListResult.SynapseAdminUserListResultUser { } \ No newline at end of file diff --git a/ModerationClient/Views/MainWindow/ClientView.axaml b/ModerationClient/Views/MainWindow/ClientView.axaml index e0cd4e0..6db948a 100644 --- a/ModerationClient/Views/MainWindow/ClientView.axaml +++ b/ModerationClient/Views/MainWindow/ClientView.axaml @@ -19,11 +19,6 @@ - - - - - @@ -31,7 +26,7 @@ - + @@ -39,7 +34,7 @@ - +