about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-03-12 04:44:19 +0100
committerRory& <root@rory.gay>2025-03-12 04:44:19 +0100
commit1a224441228d07440f279ce4a15c9a043b8cda6d (patch)
tree3ea4f4e2b694bf4a64079b084ea41d3c363937bb
parentVarious work (diff)
downloadModerationClient-1a224441228d07440f279ce4a15c9a043b8cda6d.tar.xz
Part of event rendering
-rw-r--r--.idea/.idea.ModerationClient/.idea/vcs.xml1
m---------LibMatrix0
-rw-r--r--ModerationClient/App.axaml2
-rw-r--r--ModerationClient/Models/SpaceTreeNodes/RoomNode.cs5
-rw-r--r--ModerationClient/Services/EventRenderer.cs82
-rw-r--r--ModerationClient/ViewModels/ClientViewModel.cs20
-rw-r--r--ModerationClient/ViewModels/RoomViewModel.cs82
-rw-r--r--ModerationClient/Views/MainWindow/RoomView.axaml21
-rw-r--r--ModerationClient/avatar.png0
-rw-r--r--ModerationClient/flake.lock61
-rw-r--r--ModerationClient/flake.nix37
11 files changed, 255 insertions, 56 deletions
diff --git a/.idea/.idea.ModerationClient/.idea/vcs.xml b/.idea/.idea.ModerationClient/.idea/vcs.xml

index d6f86ef..94a25f7 100644 --- a/.idea/.idea.ModerationClient/.idea/vcs.xml +++ b/.idea/.idea.ModerationClient/.idea/vcs.xml
@@ -2,6 +2,5 @@ <project version="4"> <component name="VcsDirectoryMappings"> <mapping directory="$PROJECT_DIR$" vcs="Git" /> - <mapping directory="$PROJECT_DIR$/SynapseDataMiner/Resources" vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/LibMatrix b/LibMatrix -Subproject 1ec9feb19e9dbf57c57628226b5130d222d59ec +Subproject 43a22d86c4224632ae1adc4dd9fc308c22807e1 diff --git a/ModerationClient/App.axaml b/ModerationClient/App.axaml
index 82366b6..cdf90de 100644 --- a/ModerationClient/App.axaml +++ b/ModerationClient/App.axaml
@@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ModerationClient.App" xmlns:local="using:ModerationClient" - RequestedThemeVariant="Default"> + RequestedThemeVariant="Dark"> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <Application.DataTemplates> diff --git a/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs
index 98b923a..e6715d8 100644 --- a/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs +++ b/ModerationClient/Models/SpaceTreeNodes/RoomNode.cs
@@ -1,6 +1,10 @@ +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using ArcaneLibs; +using Avalonia.Controls; using LibMatrix; +using ModerationClient.Services; namespace ModerationClient.Models.SpaceTreeNodes; @@ -16,4 +20,5 @@ public class RoomNode : NotifyPropertyChanged { public ObservableCollection<StateEventResponse> Timeline { get; } = new(); public ObservableCollection<StateEventResponse> State { get; } = new(); + public List<Control> RenderedTimeline => Timeline.Select(EventRenderer.RenderEvent).ToList(); } \ No newline at end of file diff --git a/ModerationClient/Services/EventRenderer.cs b/ModerationClient/Services/EventRenderer.cs new file mode 100644
index 0000000..d06523b --- /dev/null +++ b/ModerationClient/Services/EventRenderer.cs
@@ -0,0 +1,82 @@ +using System.Collections.Generic; +using ArcaneLibs.Extensions; +using Avalonia.Controls; +using Avalonia.Media; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.EventTypes.Spec.State.RoomInfo; + +namespace ModerationClient.Services; + +public class EventRenderer { + private static Dictionary<StateEventResponse, Control> _eventRenderers = new(); + public static Control RenderEvent(StateEventResponse evt) { + // if (_eventRenderers.TryGetValue(evt, out var @event)) { + // @event. + // return @event; + // } + + if (evt.Type == "m.room.member") { + return _eventRenderers[evt] = RenderMemberEvent(evt); + } + if (evt.Type == "m.room.message") { + return _eventRenderers[evt] = RenderMessageEvent(evt); + } + if (evt.Type == "m.room.server_acl") { + return _eventRenderers[evt] = RenderACLStateEvent(evt); + } + + return new TextBlock { + Text = $"Unknown event type: {evt.Type}", + Foreground = Brushes.Red, + Classes = {"message"}, TextWrapping = TextWrapping.WrapWithOverflow + }; + } + + private static Control RenderMemberEvent(StateEventResponse evt) { + var content = evt.ContentAs<RoomMemberEventContent>()!; + return new TextBlock { + Text = $"{evt.Sender} changed {evt.StateKey}'s membership: {content.Membership} as {content.DisplayName} (reason: {content.Reason})", + Classes = {"message"}, Foreground = Brushes.Teal + }; + } + + private static Control RenderACLStateEvent(StateEventResponse evt) { + var content = evt.ContentAs<RoomCreateEventContent>()!; + return new TextBlock { + Text = $"{evt.Sender} changed the room's ACL.", + Classes = {"message"}, Foreground = Brushes.Teal + }; + } + + private static Control RenderMessageEvent(StateEventResponse evt) { + var content = evt.ContentAs<RoomMessageEventContent>()!; + if (content.Format == null) { + return new TextBlock { + Text = $"{evt.Sender}: {content.Body}", + Classes = {"message"}, TextWrapping = TextWrapping.WrapWithOverflow + }; + } + else if (content.Format == "org.matrix.custom.html") { + // parse html + var html = content.FormattedBody; + return new TextBlock { + Text = $"{evt.Sender}: {html}", + Classes = {"message"}, TextWrapping = TextWrapping.WrapWithOverflow + }; + } + else { + return new TextBlock { + Text = $"{evt.Sender} sent a message with an unknown format: {content.Format}", + Foreground = Brushes.Red, + Classes = {"message"} + }; + } + + return new TextBlock { + Text = $"{evt.Sender} sent an unknown message type: {content.MessageType}: {content.ToJson(ignoreNull: true)}", + Foreground = Brushes.Red, + Classes = {"message"} + }; + } +} \ No newline at end of file diff --git a/ModerationClient/ViewModels/ClientViewModel.cs b/ModerationClient/ViewModels/ClientViewModel.cs
index ab4f2da..9403123 100644 --- a/ModerationClient/ViewModels/ClientViewModel.cs +++ b/ModerationClient/ViewModels/ClientViewModel.cs
@@ -175,7 +175,14 @@ public partial class ClientViewModel : ViewModelBase { 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)); + tasks.Add(_authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync().ContinueWith(r => { + if (r.IsFaulted) { + _logger.LogError(r.Exception, "Error getting room name for {RoomKey}", room.Key); + return AllRooms[room.Key].Name = "Error loading room name"; + } + + return AllRooms[room.Key].Name = r.Result; + })); // Status = $"Getting room name for {room.Key}..."; // AllRooms[room.Key].Name = await _authService.Homeserver!.GetRoom(room.Key).GetNameOrFallbackAsync(); } @@ -196,7 +203,8 @@ public partial class ClientViewModel : ViewModelBase { await AwaitTasks(tasks, "Waiting for {0}/{1} tasks while applying room changes..."); } - private ExpiringSemaphoreCache<UserProfileResponse> _profileCache = new(); + private ExpiringSemaphoreCache<UserProfileResponse> _profileCache = new(); + private async Task ApplyDirectMessagesChanges(StateEventResponse evt) { _logger.LogCritical("Direct messages updated!"); var dms = evt.RawContent.Deserialize<Dictionary<string, string[]?>>(); @@ -207,9 +215,13 @@ public partial class ClientViewModel : ViewModelBase { 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)); + // .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)); + .ContinueWith(r => { + if (!r.IsFaulted) + return string.IsNullOrWhiteSpace(r.Result.DisplayName) ? userId : r.Result.DisplayName; + return userId; + })); DirectMessages.ChildSpaces.Add(space); } diff --git a/ModerationClient/ViewModels/RoomViewModel.cs b/ModerationClient/ViewModels/RoomViewModel.cs
index c18b842..2ce0363 100644 --- a/ModerationClient/ViewModels/RoomViewModel.cs +++ b/ModerationClient/ViewModels/RoomViewModel.cs
@@ -6,6 +6,8 @@ using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using ArcaneLibs; using Avalonia; using Avalonia.Media.Imaging; @@ -63,50 +65,48 @@ public class RoomMember(AuthenticatedHomeserverGeneric homeserver) : NotifyPrope 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(); + _ = UpdateAvatar(); + } + } + + private static readonly SemaphoreSlim MediaFetchLock = new(16, 16); + private async Task UpdateAvatar() { + if (string.IsNullOrEmpty(AvatarUrl)) { + UserAvatarBitmap = new WriteableBitmap(new PixelSize(1, 1), Vector.One); + return; + } + + 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(fsPath); + Console.WriteLine($"Avatar bitmap for {UserId} loaded: {UserAvatarBitmap.GetHashCode()}, Path: {fsPath}"); + return; + } + + await MediaFetchLock.WaitAsync(); + try { + var stream = await homeserver.GetMediaStreamAsync(AvatarUrl); + Console.WriteLine($"Avatar stream for {UserId} received: {stream.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 WriteableBitmap(new PixelSize(1, 1), Vector.One); + } + finally { + MediaFetchLock.Release(); } } - public Bitmap UserAvatarBitmap { + public Bitmap? UserAvatarBitmap { get; private set => SetField(ref field, value); } diff --git a/ModerationClient/Views/MainWindow/RoomView.axaml b/ModerationClient/Views/MainWindow/RoomView.axaml
index cd74b0d..42a5992 100644 --- a/ModerationClient/Views/MainWindow/RoomView.axaml +++ b/ModerationClient/Views/MainWindow/RoomView.axaml
@@ -15,16 +15,19 @@ <Grid Grid.Row="1" Grid.ColumnDefinitions="*,1,200"> <!-- timeline --> <ScrollViewer Grid.Column="0" Background="#222222"> - <ItemsControl ItemsSource="{CompiledBinding Room.Timeline}"> + <ItemsControl ItemsSource="{CompiledBinding Room.RenderedTimeline}"> <ItemsControl.ItemTemplate> - <DataTemplate DataType="libMatrix:StateEventResponse"> - <Grid ColumnDefinitions="32, *"> - <Image></Image> - <Grid Grid.Column="1" RowDefinitions="*,*"> - <TextBlock Text="{CompiledBinding Sender}" /> - <TextBlock Grid.Row="1" Text="{CompiledBinding RawContent}" /> - </Grid> - </Grid> + <!-- <DataTemplate DataType="libMatrix:StateEventResponse"> --> + <!-- <Grid ColumnDefinitions="32, *"> --> + <!-- <Image></Image> --> + <!-- <Grid Grid.Column="1" RowDefinitions="*,*"> --> + <!-- <TextBlock Text="{CompiledBinding Sender}" /> --> + <!-- <TextBlock Grid.Row="1" Text="{CompiledBinding RawContent}" /> --> + <!-- </Grid> --> + <!-- </Grid> --> + <!-- </DataTemplate> --> + <DataTemplate DataType="Control"> + <ContentControl Content="{Binding}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> diff --git a/ModerationClient/avatar.png b/ModerationClient/avatar.png deleted file mode 100644
index e69de29..0000000 --- a/ModerationClient/avatar.png +++ /dev/null
diff --git a/ModerationClient/flake.lock b/ModerationClient/flake.lock new file mode 100644
index 0000000..7a75871 --- /dev/null +++ b/ModerationClient/flake.lock
@@ -0,0 +1,61 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1739866667, + "narHash": "sha256-EO1ygNKZlsAC9avfcwHkKGMsmipUk1Uc0TbrEZpkn64=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "73cf49b8ad837ade2de76f87eb53fc85ed5d4680", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/ModerationClient/flake.nix b/ModerationClient/flake.nix new file mode 100644
index 0000000..6c55fde --- /dev/null +++ b/ModerationClient/flake.nix
@@ -0,0 +1,37 @@ +{ + description = ".NET project template"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + utils.url = "github:numtide/flake-utils"; + }; + + outputs = inputs@{ nixpkgs, ... }: + inputs.utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + xorgLibs = with pkgs.xorg; [ + libICE + libSM + libX11 + libX11.dev + ]; + in + rec { + # `nix develop` + devShells.default = with pkgs; mkShell { + buildInputs = [ + dotnetCorePackages.dotnet_9.sdk + fontconfig + gnumake + icu + openssl + ] ++ xorgLibs; + + shellHook = '' + export DOTNET_ROOT=${dotnet-sdk} + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${lib.makeLibraryPath ([ fontconfig icu openssl ] ++ xorgLibs) } + ''; + }; + }); +} \ No newline at end of file