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
|