diff options
29 files changed, 657 insertions, 152 deletions
diff --git a/.gitignore b/.gitignore index a6807bd..668274e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ -# Dotnet build outputs +# Regular dotnet things **/bin/ **/obj/ +**/*.[Dd]ot[Ss]ettings.[Uu]ser +**/BenchmarkDotNet.Artifacts/ -# User files -*.DotSettings.user -*.patch +# Local files /patches/ - -# Local files: appsettings.Local*.json +appservice.yaml +appservice.json .run/local/*.run.xml \ No newline at end of file diff --git a/.idea/.idea.ModerationClient/.idea/avalonia.xml b/.idea/.idea.ModerationClient/.idea/avalonia.xml index 6bc9c45..b045202 100644 --- a/.idea/.idea.ModerationClient/.idea/avalonia.xml +++ b/.idea/.idea.ModerationClient/.idea/avalonia.xml @@ -8,6 +8,9 @@ <entry key="ModerationClient/Views/LoginView.axaml" value="ModerationClient/ModerationClient.csproj" /> <entry key="ModerationClient/Views/LoginWindow.axaml" value="ModerationClient/ModerationClient.csproj" /> <entry key="ModerationClient/Views/MainWindow.axaml" value="ModerationClient/ModerationClient.csproj" /> + <entry key="ModerationClient/Views/MainWindow/ClientView.axaml" value="ModerationClient/ModerationClient.csproj" /> + <entry key="ModerationClient/Views/MainWindow/LoginView.axaml" value="ModerationClient/ModerationClient.csproj" /> + <entry key="ModerationClient/Views/MainWindow/MainWindow.axaml" value="ModerationClient/ModerationClient.csproj" /> <entry key="ModerationClient/Views/MainWindow/NotificationPopupWindow.axaml" value="ModerationClient/ModerationClient.csproj" /> <entry key="ModerationClient/Views/UserManagementWindow.axaml" value="ModerationClient/ModerationClient.csproj" /> <entry key="ModerationClient/Windows/LoginWindow.axaml" value="ModerationClient/ModerationClient.csproj" /> diff --git a/FilesystemBenchmark/Benchmarks.cs b/FilesystemBenchmark/Benchmarks.cs new file mode 100644 index 0000000..9ab044a --- /dev/null +++ b/FilesystemBenchmark/Benchmarks.cs @@ -0,0 +1,134 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using BenchmarkDotNet.Attributes; + +namespace FilesystemBenchmark; + +public class Benchmarks { + private string testPath = "/home/Rory/.local/share/ModerationClient/default/syncCache"; + + private EnumerationOptions enumOpts = new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }; + + [Benchmark] + public void GetFilesMatching() { + _ = Directory.GetFiles(testPath, "*.*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void EnumerateFilesMatching() { + _ = Directory.EnumerateFiles(testPath, "*.*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void GetFilesMatchingSingleStar() { + _ = Directory.GetFiles(testPath, "*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void EnumerateFilesMatchingSingleStar() { + _ = Directory.EnumerateFiles(testPath, "*", SearchOption.AllDirectories).Count(); + } + + [Benchmark] + public void GetFilesMatchingSingleStarSimple() { + _ = Directory.GetFiles(testPath, "*", new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }).Count(); + } + + [Benchmark] + public void EnumerateFilesMatchingSingleStarSimple() { + _ = Directory.EnumerateFiles(testPath, "*", new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }).Count(); + } + + [Benchmark] + public void GetFilesMatchingSingleStarSimpleCached() { + _ = Directory.GetFiles(testPath, "*", enumOpts).Count(); + } + + [Benchmark] + public void EnumerateFilesMatchingSingleStarSimpleCached() { + _ = Directory.EnumerateFiles(testPath, "*", enumOpts).Count(); + } + + // [Benchmark] + // public void GetFilesRecursiveFunc() { + // GetFilesRecursive(testPath); + // } + // + // [Benchmark] + // public void GetFilesRecursiveParallelFunc() { + // GetFilesRecursiveParallel(testPath); + // } + // + // [Benchmark] + // public void GetFilesRecursiveEntriesFunc() { + // GetFilesRecursiveEntries(testPath); + // } + // + // [Benchmark] + // public void GetFilesRecursiveAsyncFunc() { + // GetFilesRecursiveAsync(testPath).ToBlockingEnumerable(); + // } + + + private List<string> GetFilesRecursive(string path) { + var result = new List<string>(); + foreach (var dir in Directory.GetDirectories(path)) { + result.AddRange(GetFilesRecursive(dir)); + } + + result.AddRange(Directory.GetFiles(path)); + return result; + } + + private List<string> GetFilesRecursiveEntries(string path) { + var result = new List<string>(); + foreach (var entry in Directory.EnumerateFileSystemEntries(path)) { + if (Directory.Exists(entry)) { + result.AddRange(GetFilesRecursiveEntries(entry)); + } + else { + result.Add(entry); + } + } + + return result; + } + + private List<string> GetFilesRecursiveParallel(string path) { + var result = new ConcurrentBag<string>(); + Directory.GetDirectories(path).AsParallel().ForAll(dir => { + GetFilesRecursiveParallel(dir).ForEach(result.Add); + }); + + Directory.GetFiles(path).AsParallel().ForAll(result.Add); + return result.ToList(); + } + + private async IAsyncEnumerable<string> GetFilesRecursiveAsync(string path) { + foreach (var dir in Directory.GetDirectories(path)) { + foreach (var file in GetFilesRecursiveAsync(dir).ToBlockingEnumerable()) { + yield return file; + } + } + + foreach (var file in Directory.GetFiles(path)) { + yield return file; + } + } +} \ No newline at end of file diff --git a/FilesystemBenchmark/FilesystemBenchmark.csproj b/FilesystemBenchmark/FilesystemBenchmark.csproj new file mode 100644 index 0000000..bb0af83 --- /dev/null +++ b/FilesystemBenchmark/FilesystemBenchmark.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <LangVersion>preview</LangVersion> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\LibMatrix\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" /> + </ItemGroup> + +</Project> diff --git a/FilesystemBenchmark/Program.cs b/FilesystemBenchmark/Program.cs new file mode 100644 index 0000000..9454263 --- /dev/null +++ b/FilesystemBenchmark/Program.cs @@ -0,0 +1,8 @@ +// See https://aka.ms/new-console-template for more information + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; +using FilesystemBenchmark; + +BenchmarkRunner.Run<Benchmarks>(); \ No newline at end of file diff --git a/LibMatrix b/LibMatrix -Subproject bba7333ee6581a92bbbc7479d72325e704fe7fa +Subproject a8d20e9d57857296e4600f44807893f4dcad72d diff --git a/ModerationClient.sln b/ModerationClient.sln index c23825d..cff05e1 100644 --- a/ModerationClient.sln +++ b/ModerationClient.sln @@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "Lib EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLib.Tests", "LibMatrix\ArcaneLibs\ArcaneLib.Tests\ArcaneLib.Tests.csproj", "{046D35DF-E1F2-41DA-94D3-80CF960C100A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilesystemBenchmark", "FilesystemBenchmark\FilesystemBenchmark.csproj", "{6F3FA65D-5661-4840-B421-BF64CD4DBBD2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -129,6 +133,14 @@ Global {046D35DF-E1F2-41DA-94D3-80CF960C100A}.Debug|Any CPU.Build.0 = Debug|Any CPU {046D35DF-E1F2-41DA-94D3-80CF960C100A}.Release|Any CPU.ActiveCfg = Release|Any CPU {046D35DF-E1F2-41DA-94D3-80CF960C100A}.Release|Any CPU.Build.0 = Release|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC6C910C-79C7-42D3-ABD9-8B9ED9121AE5}.Release|Any CPU.Build.0 = Release|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F3FA65D-5661-4840-B421-BF64CD4DBBD2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {6FDD7CD7-4526-410B-858D-35FA8292317C} = {C5771E60-0DA8-42AC-B54B-94C91B5B719B} @@ -154,7 +166,7 @@ Global {046D35DF-E1F2-41DA-94D3-80CF960C100A} = {D7A5A7D3-4D9E-4B15-9C48-5E75A91483CF} EndGlobalSection GlobalSection(RiderSharedRunConfigurations) = postSolution - File = .run\local\ModerationClient (Apothecary) test.run.xml File = .run\local\ModerationClient (Apothecary).run.xml + File = .run\local\ModerationClient (matrix.org).run.xml EndGlobalSection EndGlobal diff --git a/ModerationClient/App.axaml.cs b/ModerationClient/App.axaml.cs index c44b5a2..b15c0fa 100644 --- a/ModerationClient/App.axaml.cs +++ b/ModerationClient/App.axaml.cs @@ -1,11 +1,11 @@ using System; using System.IO; +using System.Threading; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using LibMatrix.Services; -using MatrixUtils.Abstractions; using MatrixUtils.Desktop; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -29,7 +29,6 @@ public partial class App : Application { // ReSharper disable once AsyncVoidMethod public override async void OnFrameworkInitializationCompleted() { var builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder(Environment.GetCommandLineArgs()); - builder.Services.AddTransient<MainWindowViewModel>(); ConfigureServices(builder.Services); Host = builder.Build(); @@ -47,10 +46,11 @@ public partial class App : Application { } private static IServiceProvider ConfigureServices(IServiceCollection services) { + var cfg = CommandLineConfiguration.FromProcessArgs(); services.AddRoryLibMatrixServices(new() { AppName = "ModerationClient", }); - services.AddSingleton<CommandLineConfiguration>(CommandLineConfiguration.FromProcessArgs()); + services.AddSingleton<CommandLineConfiguration>(cfg); services.AddSingleton<MatrixAuthenticationService>(); services.AddSingleton<ModerationClientConfiguration>(); @@ -72,9 +72,15 @@ public partial class App : Application { services.AddTransient<ClientView>(); // Register ViewModels + services.AddTransient<MainWindowViewModel>(); services.AddTransient<ClientViewModel>(); services.AddTransient<UserManagementViewModel>(); + if (cfg.TestConfiguration is not null) { + services.AddSingleton(cfg.TestConfiguration); + services.AddHostedService<TestRunner>(); + } + return services.BuildServiceProvider(); } } \ No newline at end of file diff --git a/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs new file mode 100644 index 0000000..76d5aa9 --- /dev/null +++ b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs @@ -0,0 +1,14 @@ +using ArcaneLibs; + +namespace ModerationClient.Models.SpaceTreeNodes; + +public class RoomNode : NotifyPropertyChanged { + private string? _name; + + public string RoomID { get; set; } + + public string? Name { + get => _name; + set => SetField(ref _name, value); + } +} \ No newline at end of file diff --git a/ModerationClient/Models/SpaceTreeNodes/SpaceNode.cs b/ModerationClient/Models/SpaceTreeNodes/SpaceNode.cs new file mode 100644 index 0000000..b8042ae --- /dev/null +++ b/ModerationClient/Models/SpaceTreeNodes/SpaceNode.cs @@ -0,0 +1,15 @@ +using System.Collections.ObjectModel; + +namespace ModerationClient.Models.SpaceTreeNodes; + +public class SpaceNode : RoomNode { + private bool _isExpanded = false; + + public SpaceNode(bool includeSelf = true) { + if(includeSelf) + ChildRooms = [this]; + } + + public ObservableCollection<SpaceNode> ChildSpaces { get; set; } = []; + public ObservableCollection<RoomNode> ChildRooms { get; set; } = []; +} \ No newline at end of file diff --git a/ModerationClient/ModerationClient.csproj b/ModerationClient/ModerationClient.csproj index 9876af9..c64d0c3 100644 --- a/ModerationClient/ModerationClient.csproj +++ b/ModerationClient/ModerationClient.csproj @@ -9,7 +9,6 @@ </PropertyGroup> <ItemGroup> - <Folder Include="Models\"/> <AvaloniaResource Include="Assets\**"/> </ItemGroup> diff --git a/ModerationClient/Program.cs b/ModerationClient/Program.cs index 9229194..82e10aa 100644 --- a/ModerationClient/Program.cs +++ b/ModerationClient/Program.cs @@ -1,10 +1,12 @@ -using Avalonia; +using Avalonia; using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace ModerationClient; -internal sealed class Program -{ +internal sealed class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. @@ -18,4 +20,13 @@ internal sealed class Program .UsePlatformDetect() .WithInterFont() .LogToTrace(); -} \ No newline at end of file + + // private static FileStream f = new("/dev/input/by-path/platform-pcspkr-event-spkr", FileMode.Open, FileAccess.Write, FileShare.Write, 24); + public static void Beep(short freq, short duration) { + // f.Write([..new byte[16], 0x12, 0x00, 0x02, 0x00, (byte)(freq & 0xFF), (byte)((freq >> 8) & 0xFF), 0x00, 0x00]); + // if (duration > 0) { + // Thread.Sleep(duration); + // f.Write([..new byte[16], 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00]); + // } + } +} diff --git a/ModerationClient/Services/ClientContainer.cs b/ModerationClient/Services/ClientContainer.cs new file mode 100644 index 0000000..fa3abef --- /dev/null +++ b/ModerationClient/Services/ClientContainer.cs @@ -0,0 +1,8 @@ +namespace ModerationClient.Services; + +public class ClientContainer { + public ClientContainer(MatrixAuthenticationService authService, CommandLineConfiguration cfg) + { + + } +} \ No newline at end of file diff --git a/ModerationClient/Services/CommandLineConfiguration.cs b/ModerationClient/Services/CommandLineConfiguration.cs index 63c3691..4f7da2d 100644 --- a/ModerationClient/Services/CommandLineConfiguration.cs +++ b/ModerationClient/Services/CommandLineConfiguration.cs @@ -2,15 +2,14 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Text.Json; using ArcaneLibs; -using Microsoft.Extensions.Logging; +using ArcaneLibs.Extensions; namespace ModerationClient.Services; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public record CommandLineConfiguration { - private readonly string? _loginData; - public static CommandLineConfiguration FromProcessArgs() { // logger.LogInformation("Command line arguments: " + string.Join(", ", Environment.GetCommandLineArgs())); CommandLineConfiguration cfg = FromSerialised(Environment.GetCommandLineArgs()); @@ -27,24 +26,27 @@ public record CommandLineConfiguration { if (!string.IsNullOrWhiteSpace(cfg.LoginData)) { File.WriteAllText(Path.Combine(cfg.ProfileDirectory, "login.json"), cfg.LoginData); } + + return cfg; } - public string[] Serialise() { + var current = FromProcessArgs(); List<string> args = new(); - if (Profile != "default") args.AddRange(["--profile", Profile]); + 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 (ProfileDirectory != Util.ExpandPath("$HOME/.local/share/ModerationClient/default")) args.AddRange(["--profile-dir", ProfileDirectory]); - if (!string.IsNullOrWhiteSpace(_loginData)) args.AddRange(["--login-data", _loginData!]); + 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()]); return args.ToArray(); } - public static CommandLineConfiguration FromSerialised(string[] args) { CommandLineConfiguration cfg = new(); for (var i = 0; i < args.Length; i++) { switch (args[i]) { case "--profile": + case "-p": cfg = cfg with { Profile = args[++i] }; break; case "--temporary": @@ -59,12 +61,16 @@ public record CommandLineConfiguration { case "--login-data": cfg = cfg with { LoginData = args[++i] }; break; + case "--test-config": + cfg = cfg with { testConfiguration = args[++i] }; + break; } } return cfg; } + private readonly string? _loginData; public string Profile { get; init; } = "default"; public bool IsTemporary { get; init; } @@ -78,4 +84,15 @@ public record CommandLineConfiguration { _loginData = value; } } + + private string? testConfiguration { + get => TestConfiguration?.ToJson(); + init => TestConfiguration = value is null ? null : JsonSerializer.Deserialize<TestConfig>(value); + } + + public TestConfig? TestConfiguration { get; init; } + + public class TestConfig { + public List<string> Mxids { get; set; } = new(); + } } \ No newline at end of file diff --git a/ModerationClient/Services/FileStorageProvider.cs b/ModerationClient/Services/FileStorageProvider.cs index 3658369..5b43ce4 100644 --- a/ModerationClient/Services/FileStorageProvider.cs +++ b/ModerationClient/Services/FileStorageProvider.cs @@ -1,19 +1,28 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; using LibMatrix.Interfaces.Services; -using Microsoft.Extensions.Logging; -namespace MatrixUtils.Abstractions; +namespace ModerationClient.Services; public class FileStorageProvider : IStorageProvider { - private readonly ILogger<FileStorageProvider> _logger; + // private readonly ILogger<FileStorageProvider> _logger; + private static readonly JsonSerializerOptions Options = new() { + WriteIndented = true + }; + + private static readonly EnumerationOptions EnumOpts = new EnumerationOptions() { + MatchType = MatchType.Simple, + AttributesToSkip = FileAttributes.None, + IgnoreInaccessible = false, + RecurseSubdirectories = true + }; public string TargetPath { get; } @@ -30,25 +39,58 @@ public class FileStorageProvider : IStorageProvider { } } - public async Task SaveObjectAsync<T>(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson()); + public async Task SaveObjectAsync<T>(string key, T value) { + EnsureContainingDirectoryExists(GetFullPath(key)); + await using var fileStream = File.Create(GetFullPath(key)); + await JsonSerializer.SerializeAsync(fileStream, value, Options); + } [RequiresUnreferencedCode("This API uses reflection to deserialize JSON")] - public async Task<T?> LoadObjectAsync<T>(string key) => JsonSerializer.Deserialize<T>(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); + public async Task<T?> LoadObjectAsync<T>(string key) { + await using var fileStream = File.OpenRead(GetFullPath(key)); + return JsonSerializer.Deserialize<T>(fileStream); + } - public Task<bool> ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key))); + public Task<bool> ObjectExistsAsync(string key) => Task.FromResult(File.Exists(GetFullPath(key))); - public Task<List<string>> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList()); + public async Task<IEnumerable<string>> GetAllKeysAsync() { + var sw = Stopwatch.StartNew(); + // var result = Directory.EnumerateFiles(TargetPath, "*", SearchOption.AllDirectories) + var result = Directory.EnumerateFiles(TargetPath, "*", EnumOpts) + .Select(s => s.Replace(TargetPath, "").TrimStart('/')); + // Console.WriteLine($"GetAllKeysAsync got {result.Count()} results in {sw.ElapsedMilliseconds}ms"); + // Environment.Exit(0); + return result; + } public Task DeleteObjectAsync(string key) { - File.Delete(Path.Join(TargetPath, key)); + File.Delete(GetFullPath(key)); return Task.CompletedTask; } public async Task SaveStreamAsync(string key, Stream stream) { - Directory.CreateDirectory(Path.GetDirectoryName(Path.Join(TargetPath, key)) ?? throw new InvalidOperationException()); - await using var fileStream = File.Create(Path.Join(TargetPath, key)); + EnsureContainingDirectoryExists(GetFullPath(key)); + await using var fileStream = File.Create(GetFullPath(key)); await stream.CopyToAsync(fileStream); } - public Task<Stream?> LoadStreamAsync(string key) => Task.FromResult<Stream?>(File.Exists(Path.Join(TargetPath, key)) ? File.OpenRead(Path.Join(TargetPath, key)) : null); -} + public Task<Stream?> LoadStreamAsync(string key) => Task.FromResult<Stream?>(File.Exists(GetFullPath(key)) ? File.OpenRead(GetFullPath(key)) : null); + + public Task CopyObjectAsync(string sourceKey, string destKey) { + EnsureContainingDirectoryExists(GetFullPath(destKey)); + File.Copy(GetFullPath(sourceKey), GetFullPath(destKey)); + return Task.CompletedTask; + } + + public Task MoveObjectAsync(string sourceKey, string destKey) { + EnsureContainingDirectoryExists(GetFullPath(destKey)); + File.Move(GetFullPath(sourceKey), GetFullPath(destKey)); + return Task.CompletedTask; + } + + private string GetFullPath(string key) => Path.Join(TargetPath, key); + + private void EnsureContainingDirectoryExists(string path) { + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException()); + } +} \ No newline at end of file diff --git a/ModerationClient/Services/MatrixAuthenticationService.cs b/ModerationClient/Services/MatrixAuthenticationService.cs index 7e9ce70..46ec067 100644 --- a/ModerationClient/Services/MatrixAuthenticationService.cs +++ b/ModerationClient/Services/MatrixAuthenticationService.cs @@ -1,16 +1,12 @@ -using System; using System.IO; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using ArcaneLibs; using ArcaneLibs.Extensions; -using Avalonia.Controls.Diagnostics; using LibMatrix; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.Services; -using MatrixUtils.Desktop; using Microsoft.Extensions.Logging; namespace ModerationClient.Services; diff --git a/ModerationClient/Services/ModerationClientConfiguration.cs b/ModerationClient/Services/ModerationClientConfiguration.cs index f770fef..3cc4ffb 100644 --- a/ModerationClient/Services/ModerationClientConfiguration.cs +++ b/ModerationClient/Services/ModerationClientConfiguration.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using ArcaneLibs; -using ArcaneLibs.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/ModerationClient/Services/StatusBarService.cs b/ModerationClient/Services/StatusBarService.cs new file mode 100644 index 0000000..57aff21 --- /dev/null +++ b/ModerationClient/Services/StatusBarService.cs @@ -0,0 +1,19 @@ +using System; +using ArcaneLibs; + +namespace ModerationClient.Services; + +public class StatusBarService : NotifyPropertyChanged { + private string _statusText = "Ready"; + private bool _isBusy; + + public string StatusText { + get => _statusText + " " + DateTime.Now.ToString("u")[..^1]; + set => SetField(ref _statusText, value); + } + + public bool IsBusy { + get => _isBusy; + set => SetField(ref _isBusy, value); + } +} \ No newline at end of file diff --git a/ModerationClient/Services/TestRunner.cs b/ModerationClient/Services/TestRunner.cs new file mode 100644 index 0000000..dbacf99 --- /dev/null +++ b/ModerationClient/Services/TestRunner.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace ModerationClient.Services; + +public class TestRunner(CommandLineConfiguration.TestConfig testConfig, MatrixAuthenticationService mas) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + Console.WriteLine("TestRunner: Starting test runner"); + mas.PropertyChanged += (_, args) => { + if (args.PropertyName == nameof(MatrixAuthenticationService.IsLoggedIn) && mas.IsLoggedIn) { + Console.WriteLine("TestRunner: Logged in, starting test"); + _ = Run(); + } + }; + } + + public async Task StopAsync(CancellationToken cancellationToken) { + Console.WriteLine("TestRunner: Stopping test runner"); + } + + private async Task Run() { + var hs = mas.Homeserver!; + Console.WriteLine("TestRunner: Running test on homeserver " + hs); + foreach (var mxid in testConfig.Mxids) { + var room = await hs.CreateRoom(new() { + Name = mxid, + Invite = testConfig.Mxids + }); + + await room.SendMessageEventAsync(new("test")); + + } + } +} \ No newline at end of file diff --git a/ModerationClient/ViewModels/ClientViewModel.cs b/ModerationClient/ViewModels/ClientViewModel.cs index 312b46a..fb3681e 100644 --- a/ModerationClient/ViewModels/ClientViewModel.cs +++ b/ModerationClient/ViewModels/ClientViewModel.cs @@ -1,17 +1,20 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using ArcaneLibs; using ArcaneLibs.Collections; +using LibMatrix; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Helpers; using LibMatrix.Responses; -using MatrixUtils.Abstractions; using Microsoft.Extensions.Logging; +using ModerationClient.Models.SpaceTreeNodes; using ModerationClient.Services; namespace ModerationClient.ViewModels; @@ -22,7 +25,13 @@ public partial class ClientViewModel : ViewModelBase { _authService = authService; _cfg = cfg; DisplayedSpaces.Add(_allRoomsNode = new AllRoomsSpaceNode(this)); - _ = Task.Run(Run); + DisplayedSpaces.Add(DirectMessages = new SpaceNode(false) { Name = "Direct messages" }); + _ = Task.Run(Run).ContinueWith(x => { + if (x.IsFaulted) { + Status = "Critical error running client view model: " + x.Exception?.Message; + _logger.LogError(x.Exception, "Error running client view model."); + } + }); } private readonly ILogger<ClientViewModel> _logger; @@ -33,6 +42,9 @@ public partial class ClientViewModel : ViewModelBase { private string _status = "Loading..."; public ObservableCollection<SpaceNode> DisplayedSpaces { get; } = []; public ObservableDictionary<string, RoomNode> AllRooms { get; } = new(); + public SpaceNode DirectMessages { get; } + + public bool Paused { get; set; } = false; public SpaceNode CurrentSpace { get => _currentSpace ?? _allRoomsNode; @@ -45,25 +57,50 @@ public partial class ClientViewModel : ViewModelBase { } public async Task Run() { - Status = "Interrupted."; - return; + 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")); + Console.WriteLine($"Sync store at {store.TargetPath}"); + + var sh = new SyncHelper(_authService.Homeserver, _logger, storageProvider: store) { + // MinimumDelay = TimeSpan.FromSeconds(1) + }; + Console.WriteLine("Sync helper created."); + + //optimise - we create a new scope here to make ssr go out of scope + // if((await sh.GetUnoptimisedStoreCount()) > 1000) + { + 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(); + Status = "Optimising sync store, please wait... Deleting old intermediate snapshots..."; + await ssr.RemoveOldSnapshots(); + } + + var unoptimised = await sh.GetUnoptimisedStoreCount(); // this is slow, so we cache Status = "Doing initial sync..."; - var sh = new SyncStateResolver(_authService.Homeserver, _logger, storageProvider: new FileStorageProvider(Path.Combine(_cfg.ProfileDirectory, "syncCache"))); - // var res = await sh.SyncAsync(); - //await sh.OptimiseStore(); - while (true) { - // Status = "Syncing..."; - var res = await sh.ContinueAsync(); - Status = $"Processing sync... {res.next.NextBatch}"; - await ApplySpaceChanges(res.next); - //OnPropertyChanged(nameof(CurrentSpace)); - //OnPropertyChanged(nameof(CurrentSpace.ChildRooms)); - // Console.WriteLine($"mow A={AllRooms.Count}|D={DisplayedSpaces.Count}"); - // for (int i = 0; i < GC.MaxGeneration; i++) { - // GC.Collect(i, GCCollectionMode.Forced, blocking: true); - // GC.WaitForPendingFinalizers(); - // } - Status = "Syncing..."; + await foreach (var res in sh.EnumerateSyncAsync()) { + Program.Beep((short)250, 0); + Status = $"Processing sync... {res.NextBatch}"; + await ApplySyncChanges(res); + + Program.Beep(0, 0); + if (Paused) { + Status = "Sync loop interrupted... Press pause/break to resume."; + while (Paused) await Task.Delay(1000); + } + else Status = $"Syncing... {unoptimised++} unoptimised sync responses..."; + } + } + + private async Task ApplySyncChanges(SyncResponse newSync) { + await ApplySpaceChanges(newSync); + if (newSync.AccountData?.Events?.FirstOrDefault(x => x.Type == "m.direct") is { } evt) { + await ApplyDirectMessagesChanges(evt); } } @@ -71,20 +108,25 @@ public partial class ClientViewModel : ViewModelBase { List<Task> tasks = []; foreach (var room in newSync.Rooms?.Join ?? []) { if (!AllRooms.ContainsKey(room.Key)) { - AllRooms.Add(room.Key, new RoomNode { Name = "Loading..." }); + // AllRooms.Add(room.Key, new RoomNode { Name = "Loading..." }); + AllRooms.Add(room.Key, new RoomNode { Name = "", RoomID = room.Key }); } if (room.Value.State?.Events is not null) { var nameEvent = room.Value.State!.Events!.FirstOrDefault(x => x.Type == "m.room.name" && x.StateKey == ""); - AllRooms[room.Key].Name = (nameEvent?.TypedContent as RoomNameEventContent)?.Name ?? ""; - if (string.IsNullOrWhiteSpace(AllRooms[room.Key].Name)) { - AllRooms[room.Key].Name = "Loading..."; - tasks.Add(_authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync().ContinueWith(r => AllRooms[room.Key].Name = r.Result)); - } + if (nameEvent is not null) + AllRooms[room.Key].Name = (nameEvent?.TypedContent as RoomNameEventContent)?.Name ?? ""; + } + + if (string.IsNullOrWhiteSpace(AllRooms[room.Key].Name)) { + AllRooms[room.Key].Name = "Loading..."; + tasks.Add(_authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync().ContinueWith(r => AllRooms[room.Key].Name = r.Result)); + // Status = $"Getting room name for {room.Key}..."; + // AllRooms[room.Key].Name = await _authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync(); } } - - await Task.WhenAll(tasks); + + await AwaitTasks(tasks, "Waiting for {0}/{1} tasks while applying room changes..."); return; @@ -111,20 +153,59 @@ public partial class ClientViewModel : ViewModelBase { handledRoomIds.Add(roomId); } } -} -public class SpaceNode : RoomNode { - public ObservableCollection<SpaceNode> ChildSpaces { get; set; } = []; - public ObservableCollection<RoomNode> ChildRooms { get; set; } = []; -} + private async Task ApplyDirectMessagesChanges(StateEventResponse evt) { + _logger.LogCritical("Direct messages updated!"); + var dms = evt.RawContent.Deserialize<Dictionary<string, string[]?>>(); + List<Task> tasks = []; + foreach (var (userId, roomIds) in dms) { + if (roomIds is null || roomIds.Length == 0) continue; + 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)); + DirectMessages.ChildSpaces.Add(space); + } + + foreach (var roomId in roomIds) { + var room = space.ChildRooms.FirstOrDefault(x => x.RoomID == roomId); + if (room is null) { + room = AllRooms.TryGetValue(roomId, out var existing) ? existing : new RoomNode { Name = "Unknown: " + roomId, RoomID = roomId }; + space.ChildRooms.Add(room); + } + } -public class RoomNode { - public string Name { get; set; } + foreach (var spaceChildRoom in space.ChildRooms.ToList()) { + if (!roomIds.Contains(spaceChildRoom.RoomID)) { + space.ChildRooms.Remove(spaceChildRoom); + } + } + } + + await AwaitTasks(tasks, "Waiting for {0}/{1} tasks while applying DM changes..."); + } + + private async Task AwaitTasks(List<Task> tasks, string message) { + if (tasks.Count > 0) { + 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(0, 0); + Status = string.Format(message, incomplete, total); + await Task.WhenAny(tasks); + tasks.RemoveAll(x => x.IsCompleted); + } + + Program.Beep(0, 0); + } + } } // implementation details public class AllRoomsSpaceNode : SpaceNode { - public AllRoomsSpaceNode(ClientViewModel vm) { + public AllRoomsSpaceNode(ClientViewModel vm) : base(false) { Name = "All rooms"; vm.AllRooms.CollectionChanged += (_, args) => { switch (args.Action) { diff --git a/ModerationClient/ViewModels/MainWindowViewModel.cs b/ModerationClient/ViewModels/MainWindowViewModel.cs index be64de4..5cd5c45 100644 --- a/ModerationClient/ViewModels/MainWindowViewModel.cs +++ b/ModerationClient/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,6 @@ using System; using Avalonia; using ModerationClient.Services; -using ModerationClient.Views; namespace ModerationClient.ViewModels; @@ -9,10 +8,10 @@ public partial class MainWindowViewModel(MatrixAuthenticationService authService // public MainWindow? MainWindow { get; set; } private float _scale = 1.0f; - private ViewModelBase _currentViewModel = new LoginViewModel(authService); + private ViewModelBase? _currentViewModel = null; private Size _physicalSize = new Size(300, 220); - public ViewModelBase CurrentViewModel { + public ViewModelBase? CurrentViewModel { get => _currentViewModel; set => SetProperty(ref _currentViewModel, value); } diff --git a/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs b/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs index 7a2ad63..90020d6 100644 --- a/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs +++ b/ModerationClient/ViewModels/UserManagement/UserManagementViewModel.cs @@ -1,20 +1,10 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.Diagnostics; -using System.IO; -using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using ArcaneLibs.Collections; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; -using LibMatrix.Responses; -using MatrixUtils.Abstractions; using Microsoft.Extensions.Logging; using ModerationClient.Services; @@ -51,6 +41,7 @@ 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>(user.ToJson())!); } @@ -58,6 +49,6 @@ public partial class UserManagementViewModel : ViewModelBase { } } -public class User : AdminUserListResult.AdminUserListResultUser { +public class User : SynapseAdminUserListResult.SynapseAdminUserListResultUser { } \ No newline at end of file diff --git a/ModerationClient/Views/MainWindow/ClientView.axaml b/ModerationClient/Views/MainWindow/ClientView.axaml index ba030e4..e0cd4e0 100644 --- a/ModerationClient/Views/MainWindow/ClientView.axaml +++ b/ModerationClient/Views/MainWindow/ClientView.axaml @@ -4,36 +4,42 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:views="clr-namespace:ModerationClient.Views" xmlns:viewModels="clr-namespace:ModerationClient.ViewModels" + xmlns:spaceTreeNodes="clr-namespace:ModerationClient.Models.SpaceTreeNodes" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ModerationClient.Views.ClientView" x:DataType="viewModels:ClientViewModel"> <Grid Width="{Binding $parent.Width}" Height="{Binding $parent.Height}" RowDefinitions="*, Auto"> <Grid Grid.Row="0"> <Grid.ColumnDefinitions> - <ColumnDefinition Width="128" MinWidth="16" /> + <ColumnDefinition Width="256" MinWidth="16" /> <ColumnDefinition Width="1" /> - <ColumnDefinition Width="128" MinWidth="16" /> + <ColumnDefinition Width="256" MinWidth="16" /> <ColumnDefinition Width="1" /> <ColumnDefinition Width="*" MinWidth="16" /> </Grid.ColumnDefinitions> - <TreeView Grid.Column="0" Background="Red" ItemsSource="{CompiledBinding DisplayedSpaces}" SelectedItem="{CompiledBinding CurrentSpace}"> + <TreeView Grid.Column="0" Background="#202020" ItemsSource="{CompiledBinding DisplayedSpaces}" SelectedItem="{CompiledBinding CurrentSpace}"> <TreeView.ItemTemplate> - <TreeDataTemplate ItemsSource="{Binding ChildSpaces}"> + <!-- <TreeView.Styles> --> + <!-- <Style Selector="TreeViewItem"> --> + <!-- <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> --> + <!-- </Style> --> + <!-- </TreeView.Styles> --> + <TreeDataTemplate ItemsSource="{Binding ChildSpaces}" DataType="spaceTreeNodes:SpaceNode"> <TextBlock Text="{Binding Name}" Height="20" /> </TreeDataTemplate> </TreeView.ItemTemplate> </TreeView> <GridSplitter Grid.Column="1" Background="Black" ResizeDirection="Columns" /> <!-- <Rectangle Grid.Column="2" Fill="Green" /> --> - <ListBox Grid.Column="2" Background="Green" ItemsSource="{CompiledBinding CurrentSpace.ChildRooms}"> + <ListBox Grid.Column="2" Background="#242424" ItemsSource="{CompiledBinding CurrentSpace.ChildRooms}"> <ListBox.ItemTemplate> - <DataTemplate DataType="viewModels:RoomNode"> + <DataTemplate DataType="spaceTreeNodes:RoomNode"> <TextBlock Text="{CompiledBinding Name}" Height="20" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <GridSplitter Grid.Column="3" Background="Black" ResizeDirection="Columns" /> - <Rectangle Grid.Column="4" Fill="Blue" /> + <Rectangle Grid.Column="4" Fill="#282828" /> </Grid> <Grid Grid.Row="1" ColumnDefinitions="Auto, *, Auto" Background="Black"> <Label Grid.Column="2" Content="{CompiledBinding Status}" /> diff --git a/ModerationClient/Views/MainWindow/ClientView.axaml.cs b/ModerationClient/Views/MainWindow/ClientView.axaml.cs index 894e807..5b0f62d 100644 --- a/ModerationClient/Views/MainWindow/ClientView.axaml.cs +++ b/ModerationClient/Views/MainWindow/ClientView.axaml.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Markup.Xaml; namespace ModerationClient.Views; diff --git a/ModerationClient/Views/MainWindow/LoginView.axaml.cs b/ModerationClient/Views/MainWindow/LoginView.axaml.cs index 5e84ace..ea2f59d 100644 --- a/ModerationClient/Views/MainWindow/LoginView.axaml.cs +++ b/ModerationClient/Views/MainWindow/LoginView.axaml.cs @@ -1,12 +1,12 @@ using System; using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; using ModerationClient.ViewModels; namespace ModerationClient.Views; public partial class LoginView : UserControl { + private LoginViewModel? ViewModel => DataContext as LoginViewModel; public LoginView() { InitializeComponent(); } diff --git a/ModerationClient/Views/MainWindow/MainWindow.axaml b/ModerationClient/Views/MainWindow/MainWindow.axaml index ef13553..c45b296 100644 --- a/ModerationClient/Views/MainWindow/MainWindow.axaml +++ b/ModerationClient/Views/MainWindow/MainWindow.axaml @@ -17,23 +17,28 @@ <!-- </Design.DataContext> --> <Grid RowDefinitions="Auto, *"> - <Grid Grid.Row="0" ColumnDefinitions="Auto, *, Auto" x:Name="TopPanel"> - <StackPanel Orientation="Horizontal" Grid.Column="0"> - <Label Content="{CompiledBinding Scale}" /> - <Label>x</Label> - <Rectangle Width="32" /> - <Label Content="{CompiledBinding ChildTargetWidth}" /> - <Label>x</Label> - <Label Content="{CompiledBinding ChildTargetHeight}" /> - </StackPanel> - <Label Grid.Column="2">Press '?' for keybinds</Label> - </Grid> + <Viewbox Grid.Row="1"> - <ContentControl - Width="{CompiledBinding ChildTargetWidth}" - Background="#222222" - Height="{CompiledBinding ChildTargetHeight}" - Content="{CompiledBinding CurrentViewModel}" /> + <Grid RowDefinitions="Auto, *" + Background="#202020" + Width="{CompiledBinding ChildTargetWidth}" + Height="{CompiledBinding ChildTargetHeight}"> + <Grid Grid.Row="0" ColumnDefinitions="Auto, *, Auto" x:Name="TopPanel" Background="#000000"> + <StackPanel Orientation="Horizontal" Grid.Column="0"> + <Label>[F1 -]</Label> + <Label Content="{CompiledBinding Scale}" /> + <Label>x</Label> + <Label>[+ F2]</Label> + <Rectangle Width="32" /> + <Label>VRes =</Label> + <Label Content="{CompiledBinding ChildTargetWidth}" /> + <Label>x</Label> + <Label Content="{CompiledBinding ChildTargetHeight}" /> + </StackPanel> + <Label Grid.Column="2">Press '?' for keybinds</Label> + </Grid> + <ContentControl Grid.Row="1" Content="{CompiledBinding CurrentViewModel}" /> + </Grid> </Viewbox> </Grid> </Window> \ No newline at end of file diff --git a/ModerationClient/Views/MainWindow/MainWindow.axaml.cs b/ModerationClient/Views/MainWindow/MainWindow.axaml.cs index 01027c1..cc3534d 100644 --- a/ModerationClient/Views/MainWindow/MainWindow.axaml.cs +++ b/ModerationClient/Views/MainWindow/MainWindow.axaml.cs @@ -1,23 +1,55 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ArcaneLibs.Extensions; using Avalonia; using Avalonia.Controls; -using Avalonia.Diagnostics; using Avalonia.Input; +using LibMatrix.Responses; +using LibMatrix.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ModerationClient.Services; using ModerationClient.ViewModels; +#if DEBUG +using Avalonia.Diagnostics; +#endif + namespace ModerationClient.Views.MainWindow; public partial class MainWindow : Window { + private readonly CommandLineConfiguration _cfg; + public MainWindow(CommandLineConfiguration cfg, MainWindowViewModel dataContext, IHostApplicationLifetime appLifetime) { + _cfg = cfg; InitializeComponent(); DataContext = dataContext; _ = dataContext.AuthService.LoadProfileAsync().ContinueWith(x => { if (x.IsFaulted) { Console.WriteLine("Failed to load profile: " + x.Exception); } + + dataContext.CurrentViewModel = dataContext.AuthService.IsLoggedIn + ? App.Current.Host.Services.GetRequiredService<ClientViewModel>() + : new LoginViewModel(dataContext.AuthService); + + if (!dataContext.AuthService.IsLoggedIn) { + dataContext.AuthService.PropertyChanged += (sender, args) => { + if (args.PropertyName == nameof(MatrixAuthenticationService.IsLoggedIn)) { + if (dataContext.AuthService.IsLoggedIn) { + // dataContext.CurrentViewModel = new ClientViewModel(dataContext.AuthService); + dataContext.CurrentViewModel = App.Current.Host.Services.GetRequiredService<ClientViewModel>(); + // var window = App.Current.Host.Services.GetRequiredService<UserManagementWindow>(); + // window.Show(); + } + else { + dataContext.CurrentViewModel = new LoginViewModel(dataContext.AuthService); + } + } + }; + } }); Console.WriteLine("mainwnd"); #if DEBUG @@ -35,36 +67,23 @@ public partial class MainWindow : Window { return; } - viewModel.PhysicalSize = new Size(ClientSize.Width, ClientSize.Height - TopPanel.Bounds.Height); + // viewModel.PhysicalSize = new Size(ClientSize.Width, ClientSize.Height - TopPanel.Bounds.Height); + viewModel.PhysicalSize = new Size(ClientSize.Width, ClientSize.Height); break; } } }; - TopPanel.PropertyChanged += (_, args) => { - if (args.Property.Name == nameof(Visual.Bounds)) { - if (DataContext is not MainWindowViewModel viewModel) { - Console.WriteLine("WARN: MainWindowViewModel is null, ignoring TopPanel.Bounds change!"); - return; - } - - viewModel.PhysicalSize = new Size(ClientSize.Width, ClientSize.Height - TopPanel.Bounds.Height); - } - }; - - dataContext.AuthService.PropertyChanged += (sender, args) => { - if (args.PropertyName == nameof(MatrixAuthenticationService.IsLoggedIn)) { - if (dataContext.AuthService.IsLoggedIn) { - // dataContext.CurrentViewModel = new ClientViewModel(dataContext.AuthService); - dataContext.CurrentViewModel = App.Current.Host.Services.GetRequiredService<ClientViewModel>(); - var window = App.Current.Host.Services.GetRequiredService<UserManagementWindow>(); - window.Show(); - } - else { - dataContext.CurrentViewModel = new LoginViewModel(dataContext.AuthService); - } - } - }; + // TopPanel.PropertyChanged += (_, args) => { + // if (args.Property.Name == nameof(Visual.Bounds)) { + // if (DataContext is not MainWindowViewModel viewModel) { + // Console.WriteLine("WARN: MainWindowViewModel is null, ignoring TopPanel.Bounds change!"); + // return; + // } + // + // viewModel.PhysicalSize = new Size(ClientSize.Width, ClientSize.Height - TopPanel.Bounds.Height); + // } + // }; dataContext.Scale = cfg.Scale; Width *= cfg.Scale; @@ -76,9 +95,13 @@ public partial class MainWindow : Window { }); } - protected override void OnKeyDown(KeyEventArgs e) => OnKeyDown(this, e); + protected override void OnKeyDown(KeyEventArgs e) => OnKeyDown(this, e).ContinueWith(t => { + if (t.IsFaulted) { + Console.WriteLine("OnKeyDown faulted: " + t.Exception); + } + }); - private void OnKeyDown(object? _, KeyEventArgs e) { + private async Task OnKeyDown(object? _, KeyEventArgs e) { if (DataContext is not MainWindowViewModel viewModel) { Console.WriteLine($"WARN: DataContext is {DataContext?.GetType().Name ?? "null"}, ignoring key press!"); return; @@ -100,6 +123,11 @@ public partial class MainWindow : Window { viewModel.Scale = 5.0f; } } + else if (e.Key == Key.Pause) { + if (viewModel.CurrentViewModel is ClientViewModel clientViewModel) { + clientViewModel.Paused = !clientViewModel.Paused; + } + } else if (e.KeyModifiers == KeyModifiers.Control) { if (e.Key == Key.K) { if (viewModel.CurrentViewModel is ClientViewModel clientViewModel) { @@ -107,7 +135,7 @@ public partial class MainWindow : Window { } else Console.WriteLine("WARN: CurrentViewModel is not ClientViewModel, ignoring Quick Switcher"); } - else if (e.Key == Key.U ) { + else if (e.Key == Key.U) { Console.WriteLine("UserManagementWindow invoked"); var window = App.Current.Host.Services.GetRequiredService<UserManagementWindow>(); window.Show(); @@ -116,8 +144,31 @@ public partial class MainWindow : Window { Console.WriteLine("Launching new process"); System.Diagnostics.Process.Start(System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName, Environment.GetCommandLineArgs()); } - else if (e.Key == Key.F9) { - + else if (e.Key == Key.F9) { } + else if (e.Key == Key.D) { + List<LoginResponse> mxids = new(); + var hsps = App.Current.Services.GetRequiredService<HomeserverProviderService>(); + var rhs = await hsps.GetRemoteHomeserver("matrixunittests.rory.gay", enableServer: false); + for (int i = 0; i < 64; i++) { + Console.WriteLine("Debugging invoked"); + var main = await rhs.RegisterAsync(Guid.NewGuid().ToString(), "password"); + mxids.Add(main); + Console.WriteLine($"Registered: {main.UserId} {main.AccessToken} - {mxids.Count}"); + } + + foreach (var mxid in mxids) { + Console.WriteLine("Launching new process: "); + var args = (_cfg with { + Profile = "mut-" + mxid.UserId, + ProfileDirectory = null, + LoginData = mxid.ToJson(), + TestConfiguration = new() { + Mxids = mxids.Select(x => x.UserId).ToList() + } + }).Serialise(); + Console.WriteLine(string.Join(' ', args)); + System.Diagnostics.Process.Start(System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName, args); + } } } } diff --git a/Test/Program.cs b/Test/Program.cs new file mode 100644 index 0000000..2739294 --- /dev/null +++ b/Test/Program.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using LibMatrix; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +// var src1 = new SynapseCollectionResult<EventIdResponse>(); +// var src2 = new SynapseCollectionResult<EventIdResponse>(chunkKey: "meow", nextTokenKey: "woof", prevTokenKey: "bark", totalKey: "purr"); +// +// for (int i = 0; i < 10000000; i++) { +// src1.Chunk.Add(new EventIdResponse { EventId = Guid.NewGuid().ToString() }); +// src2.Chunk.Add(new EventIdResponse { EventId = Guid.NewGuid().ToString() }); +// } +// +// File.WriteAllText("src1.json", JsonSerializer.Serialize(src1, new JsonSerializerOptions(){WriteIndented = true})); +// File.WriteAllText("src2.json", JsonSerializer.Serialize(src2, new JsonSerializerOptions(){WriteIndented = true})); + +using var stream1 = File.OpenRead("src1.json"); +var dst1 = new SynapseCollectionResult<EventIdResponse>().FromJson(stream1, (item) => { + ArgumentNullException.ThrowIfNull(item.EventId); +}); + +var a = new StateEventResponse(); \ No newline at end of file diff --git a/Test/Test.csproj b/Test/Test.csproj new file mode 100644 index 0000000..39961c1 --- /dev/null +++ b/Test/Test.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> + </ItemGroup> + +</Project> |