diff --git a/MatrixUtils.Desktop/App.axaml b/MatrixUtils.Desktop/App.axaml
new file mode 100644
index 0000000..bc69400
--- /dev/null
+++ b/MatrixUtils.Desktop/App.axaml
@@ -0,0 +1,10 @@
+<Application xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ x:Class="MatrixUtils.Desktop.App"
+ RequestedThemeVariant="Default">
+ <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
+
+ <Application.Styles>
+ <FluentTheme />
+ </Application.Styles>
+</Application>
\ No newline at end of file
diff --git a/MatrixUtils.Desktop/App.axaml.cs b/MatrixUtils.Desktop/App.axaml.cs
new file mode 100644
index 0000000..3a106ab
--- /dev/null
+++ b/MatrixUtils.Desktop/App.axaml.cs
@@ -0,0 +1,49 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Styling;
+using LibMatrix.Services;
+using MatrixUtils.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace MatrixUtils.Desktop;
+
+public partial class App : Application {
+ public IHost host { get; set; }
+
+ public override void OnFrameworkInitializationCompleted() {
+ host = Host.CreateDefaultBuilder().ConfigureServices((ctx, services) => {
+ services.AddSingleton<RMUDesktopConfiguration>();
+ services.AddSingleton<SentryService>();
+ services.AddSingleton<TieredStorageService>(x =>
+ new TieredStorageService(
+ cacheStorageProvider: new FileStorageProvider(x.GetService<RMUDesktopConfiguration>()!.CacheStoragePath),
+ dataStorageProvider: new FileStorageProvider(x.GetService<RMUDesktopConfiguration>()!.DataStoragePath)
+ )
+ );
+ services.AddSingleton(new RoryLibMatrixConfiguration {
+ AppName = "MatrixUtils.Desktop"
+ });
+ services.AddRoryLibMatrixServices();
+ // foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) {
+ // Console.WriteLine($"Adding command {commandClass.Name}");
+ // services.AddScoped(typeof(ICommand), commandClass);
+ // }
+ services.AddSingleton<RMUStorageWrapper>();
+ services.AddSingleton<MainWindow>();
+ services.AddSingleton(this);
+ }).UseConsoleLifetime().Build();
+
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
+ var scopeFac = host.Services.GetService<IServiceScopeFactory>();
+ var scope = scopeFac.CreateScope();
+ desktop.MainWindow = scope.ServiceProvider.GetRequiredService<MainWindow>();
+ }
+
+ if(Environment.GetEnvironmentVariable("AVALONIA_THEME")?.Equals("dark", StringComparison.OrdinalIgnoreCase) ?? false)
+ RequestedThemeVariant = ThemeVariant.Dark;
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Desktop/Components/NavigationStack.axaml b/MatrixUtils.Desktop/Components/NavigationStack.axaml
new file mode 100644
index 0000000..b24895d
--- /dev/null
+++ b/MatrixUtils.Desktop/Components/NavigationStack.axaml
@@ -0,0 +1,12 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="MatrixUtils.Desktop.Components.NavigationStack">
+ <StackPanel x:Name="dock">
+ <Label>NagivationStack</Label>
+ <StackPanel x:Name="navPanel" Orientation="Horizontal"></StackPanel>
+ <ContentControl x:Name="content"></ContentControl>
+ </StackPanel>
+</UserControl>
\ No newline at end of file
diff --git a/MatrixUtils.Desktop/Components/NavigationStack.axaml.cs b/MatrixUtils.Desktop/Components/NavigationStack.axaml.cs
new file mode 100644
index 0000000..632ae3c
--- /dev/null
+++ b/MatrixUtils.Desktop/Components/NavigationStack.axaml.cs
@@ -0,0 +1,72 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace MatrixUtils.Desktop.Components;
+
+public partial class NavigationStack : UserControl {
+ public NavigationStack() {
+ InitializeComponent();
+ }
+
+ // private void InitializeComponent() {
+ // AvaloniaXamlLoader.Load(this);
+ // buildView();
+ // }
+
+ protected override void OnLoaded(RoutedEventArgs e) {
+ base.OnLoaded(e);
+ buildView();
+ }
+
+ private void buildView() {
+ if (navPanel is null) {
+ Console.WriteLine("NavigationStack buildView called while navpanel is null!");
+ // await Task.Delay(100);
+ // if (navPanel is null)
+ // await buildView();
+ // else Console.WriteLine("navpanel is not null!");
+ }
+ navPanel.Children.Clear();
+ foreach (var item in _stack) {
+ Button btn = new() {
+ Content = item.Name
+ };
+ btn.Click += (_, _) => {
+ PopTo(_stack.IndexOf(item));
+ buildView();
+ };
+ navPanel.Children.Add(btn);
+ }
+ content.Content = Current?.View ?? new UserControl();
+ }
+
+
+ public class NavigationStackItem {
+ public string Name { get; set; }
+ public string Description { get; set; } = "";
+ public UserControl View { get; set; }
+ }
+
+ private List<NavigationStackItem> _stack = new();
+
+ public NavigationStackItem? Current => _stack.LastOrDefault();
+
+ public void Push(string name, UserControl view) {
+ _stack.Add(new NavigationStackItem {
+ Name = name,
+ View = view
+ });
+ buildView();
+ }
+
+ public void Pop() {
+ _stack.RemoveAt(_stack.Count - 1);
+ buildView();
+ }
+
+ public void PopTo(int index) {
+ _stack.RemoveRange(index, _stack.Count - index);
+ buildView();
+ }
+}
diff --git a/MatrixUtils.Desktop/Components/Pages/RoomList.axaml b/MatrixUtils.Desktop/Components/Pages/RoomList.axaml
new file mode 100644
index 0000000..45778f3
--- /dev/null
+++ b/MatrixUtils.Desktop/Components/Pages/RoomList.axaml
@@ -0,0 +1,21 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:components="clr-namespace:MatrixUtils.Desktop.Components.Pages"
+ xmlns:components1="clr-namespace:MatrixUtils.Desktop.Components"
+ xmlns:abstractions="clr-namespace:MatrixUtils.Abstractions;assembly=MatrixUtils.Abstractions"
+
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="MatrixUtils.Desktop.Components.Pages.RoomList"
+ x:DataType="components:RoomList"
+ DataContext="{Binding $self}"
+ >
+ <ListBox ItemsSource="{Binding Rooms}">
+ <ListBox.ItemTemplate>
+ <DataTemplate DataType="abstractions:RoomInfo">
+ <components1:RoomListEntry Room="{Binding Path=.}"/>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+</UserControl>
diff --git a/MatrixUtils.Desktop/Components/Pages/RoomList.axaml.cs b/MatrixUtils.Desktop/Components/Pages/RoomList.axaml.cs
new file mode 100644
index 0000000..a0c9fcc
--- /dev/null
+++ b/MatrixUtils.Desktop/Components/Pages/RoomList.axaml.cs
@@ -0,0 +1,15 @@
+using System.Collections.ObjectModel;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using MatrixUtils.Abstractions;
+
+namespace MatrixUtils.Desktop.Components.Pages;
+
+public partial class RoomList : UserControl {
+ private ObservableCollection<RoomInfo> Rooms { get; set; } = new();
+
+ public RoomList() {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Desktop/Components/RoomListEntry.axaml b/MatrixUtils.Desktop/Components/RoomListEntry.axaml
new file mode 100644
index 0000000..97e6fdc
--- /dev/null
+++ b/MatrixUtils.Desktop/Components/RoomListEntry.axaml
@@ -0,0 +1,16 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:components="clr-namespace:MatrixUtils.Desktop.Components"
+ mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="32"
+ x:Class="MatrixUtils.Desktop.Components.RoomListEntry"
+
+ x:DataType="components:RoomListEntry"
+ DataContext="{Binding $self}"
+ >
+ <StackPanel Orientation="Horizontal">
+ <Image MaxWidth="64" x:Name="RoomIcon"></Image>
+ <Label x:Name="RoomName" Content="{Binding Room.RoomName}"></Label>
+ </StackPanel>
+</UserControl>
diff --git a/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs b/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs
new file mode 100644
index 0000000..1e4a127
--- /dev/null
+++ b/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs
@@ -0,0 +1,74 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Media.Imaging;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Helpers;
+using LibMatrix.Interfaces.Services;
+using LibMatrix.Services;
+using MatrixUtils.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace MatrixUtils.Desktop.Components;
+
+public partial class RoomListEntry : UserControl {
+ public RoomInfo Room { get; set; }
+
+ public RoomListEntry() {
+ InitializeComponent();
+ }
+
+ protected override void OnLoaded(RoutedEventArgs e) {
+ base.OnLoaded(e);
+ RoomName.Content = Room.Room.RoomId;
+ Task.WhenAll(GetRoomName(), GetRoomIcon());
+ }
+
+ private async Task GetRoomName() {
+ try {
+ var nameEvent = await Room.GetStateEvent("m.room.name");
+ if (nameEvent?.TypedContent is RoomNameEventContent nameData)
+ RoomName.Content = nameData.Name;
+ }
+ catch (MatrixException e) {
+ if (e.ErrorCode != "M_NOT_FOUND")
+ throw;
+ }
+ }
+
+ private async Task GetRoomIcon() {
+ try {
+ using var hc = new HttpClient();
+ var avatarEvent = await Room.GetStateEvent("m.room.avatar");
+ if (avatarEvent?.TypedContent is RoomAvatarEventContent avatarData) {
+ var mxcUrl = avatarData.Url;
+ var resolvedUrl = await Room.Room.GetResolvedRoomAvatarUrlAsync();
+
+ // await using var svc = _serviceScopeFactory.CreateAsyncScope();
+ // var hs = await svc.ServiceProvider.GetService<RMUStorageWrapper>()?.GetCurrentSessionOrPrompt()!;
+ // var hsResolver = svc.ServiceProvider.GetService<HomeserverResolverService>();
+ // var storage = svc.ServiceProvider.GetService<TieredStorageService>()?.CacheStorageProvider;
+ // var resolvedUrl = await hsResolver.ResolveMediaUri(hs.ServerName, mxcUrl);
+ var storage = new FileStorageProvider("cache");
+ var storageKey = $"media/{mxcUrl.Replace("mxc://", "").Replace("/", ".")}";
+ try {
+ if (!await storage.ObjectExistsAsync(storageKey))
+ await storage.SaveStreamAsync(storageKey, await hc.GetStreamAsync(resolvedUrl));
+
+ RoomIcon.Source = new Bitmap(await storage.LoadStreamAsync(storageKey) ?? throw new NullReferenceException());
+ }
+ catch (IOException) { }
+ catch (MatrixException e) {
+ if (e.ErrorCode != "M_UNKNOWN")
+ throw;
+ }
+ }
+ }
+ catch (MatrixException e) {
+ if (e.ErrorCode != "M_NOT_FOUND")
+ throw;
+ }
+ }
+}
diff --git a/MatrixUtils.Desktop/LoginWindow.axaml b/MatrixUtils.Desktop/LoginWindow.axaml
new file mode 100644
index 0000000..ecfa3f5
--- /dev/null
+++ b/MatrixUtils.Desktop/LoginWindow.axaml
@@ -0,0 +1,25 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:desktop="clr-namespace:MatrixUtils.Desktop"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ Title="LoginWindow"
+ x:Class="MatrixUtils.Desktop.LoginWindow"
+ x:DataType="desktop:LoginWindow"
+ DataContext="{Binding $self}"
+ SizeToContent="WidthAndHeight" CanResize="False"
+ MinWidth="250">
+ <StackPanel>
+ <Label>Log in</Label>
+ <StackPanel Orientation="Horizontal">
+ <Label Width="100">User ID</Label>
+ <TextBox MinWidth="250" Text="{Binding Username, Mode=TwoWay}" />
+ </StackPanel>
+ <StackPanel Orientation="Horizontal">
+ <Label Width="100">Password</Label>
+ <MaskedTextBox MinWidth="250" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" />
+ </StackPanel>
+ <Button Click="Login">Login</Button>
+ </StackPanel>
+</Window>
\ No newline at end of file
diff --git a/MatrixUtils.Desktop/LoginWindow.axaml.cs b/MatrixUtils.Desktop/LoginWindow.axaml.cs
new file mode 100644
index 0000000..ac59317
--- /dev/null
+++ b/MatrixUtils.Desktop/LoginWindow.axaml.cs
@@ -0,0 +1,37 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+
+namespace MatrixUtils.Desktop;
+
+public partial class LoginWindow : Window {
+ private readonly RMUStorageWrapper _storage;
+
+ public LoginWindow(RMUStorageWrapper storage) {
+ _storage = storage;
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+
+ private void InitializeComponent() {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public string Username { get; set; }
+ public string Password { get; set; }
+ // ReSharper disable once AsyncVoidMethod
+ private async void Login(object? sender, RoutedEventArgs e) {
+ var res = await _storage.Login(Username.Split(':')[1], Username.Split(':')[0][1..], Password);
+ if (res is not null) {
+ await _storage.AddToken(res);
+ Close();
+ }
+ else {
+ Password = "";
+ }
+ }
+}
diff --git a/MatrixUtils.Desktop/MainWindow.axaml b/MatrixUtils.Desktop/MainWindow.axaml
new file mode 100644
index 0000000..6457678
--- /dev/null
+++ b/MatrixUtils.Desktop/MainWindow.axaml
@@ -0,0 +1,16 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:desktop="clr-namespace:MatrixUtils.Desktop"
+ xmlns:components="clr-namespace:MatrixUtils.Desktop.Components"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="MatrixUtils.Desktop.MainWindow"
+ Title="Rory&::MatrixUtils">
+ <!-- <Interaction.Behaviors> -->
+ <!-- <EventTriggerBehavior EventName="Loaded"> -->
+ <!-- <InvokeCommandAction Command="{Binding LoadedCommand}"></InvokeCommandAction> -->
+ <!-- </EventTriggerBehavior> -->
+ <!-- </Interaction.Behaviors> -->
+ <components:NavigationStack x:Name="windowContent"/>
+</Window>
diff --git a/MatrixUtils.Desktop/MainWindow.axaml.cs b/MatrixUtils.Desktop/MainWindow.axaml.cs
new file mode 100644
index 0000000..562ab1a
--- /dev/null
+++ b/MatrixUtils.Desktop/MainWindow.axaml.cs
@@ -0,0 +1,57 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using MatrixUtils.Abstractions;
+using MatrixUtils.Desktop.Components;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace MatrixUtils.Desktop;
+
+public partial class MainWindow : Window {
+ private readonly ILogger<MainWindow> _logger;
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly RMUStorageWrapper _storageWrapper;
+ private readonly RMUDesktopConfiguration _configuration;
+ public static MainWindow Instance { get; private set; } = null!;
+
+ public MainWindow(ILogger<MainWindow> logger, IServiceScopeFactory scopeFactory, SentryService _) {
+ Instance = this;
+ _logger = logger;
+ _scopeFactory = scopeFactory;
+ _configuration = scopeFactory.CreateScope().ServiceProvider.GetRequiredService<RMUDesktopConfiguration>();
+ _storageWrapper = scopeFactory.CreateScope().ServiceProvider.GetRequiredService<RMUStorageWrapper>();
+
+ _logger.LogInformation("Initialising MainWindow");
+
+ InitializeComponent();
+
+ _logger.LogInformation("Cache location: {}", _configuration.CacheStoragePath);
+ _logger.LogInformation("Data location: {}", _configuration.DataStoragePath);
+
+ // for (int i = 0; i < 100; i++) {
+ // roomList.Children.Add(new RoomListEntry());
+ // }
+ }
+
+ // ReSharper disable once AsyncVoidMethod
+ protected override async void OnLoaded(RoutedEventArgs e) {
+ _logger.LogInformation("async onloaded override");
+ var hs = await _storageWrapper.GetCurrentSessionOrPrompt();
+ var rooms = await hs.GetJoinedRooms();
+ foreach (var room in rooms) {
+ // roomList.Children.Add(new RoomListEntry(_scopeFactory, new RoomInfo(room)));
+
+ windowContent.Push("home", new RoomListEntry() {
+ Room = new RoomInfo() {
+ Room = room
+ }
+ });
+ base.OnLoaded(e);
+ }
+ }
+
+ // public Command
+ // protected void LoadedCommand() {
+ // _logger.LogInformation("async command");
+ // }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj b/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
new file mode 100644
index 0000000..f1bd2b6
--- /dev/null
+++ b/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
@@ -0,0 +1,50 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
+ <ApplicationManifest>app.manifest</ApplicationManifest>
+ <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
+
+ <LangVersion>preview</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <InvariantGlobalization>true</InvariantGlobalization>
+<!-- <PublishTrimmed>true</PublishTrimmed>-->
+<!-- <PublishReadyToRun>true</PublishReadyToRun>-->
+<!-- <PublishSingleFile>true</PublishSingleFile>-->
+<!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>-->
+<!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>-->
+<!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>-->
+ </PropertyGroup>
+
+
+ <ItemGroup>
+ <PackageReference Include="Avalonia" Version="11.0.6" />
+ <PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
+ <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
+ <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
+ <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
+ <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.6" />
+ <PackageReference Include="Sentry" Version="3.36.0" />
+ </ItemGroup>
+
+
+
+
+ <ItemGroup>
+ <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.5" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="appsettings*.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ <Content Update="appsettings.Local.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/MatrixUtils.Desktop/Program.cs b/MatrixUtils.Desktop/Program.cs
new file mode 100644
index 0000000..0f4c09c
--- /dev/null
+++ b/MatrixUtils.Desktop/Program.cs
@@ -0,0 +1,33 @@
+using Avalonia;
+using Microsoft.Extensions.Hosting;
+using Tmds.DBus.Protocol;
+
+namespace MatrixUtils.Desktop;
+
+internal class Program {
+ private static IHost appHost;
+ // 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.
+ // [STAThread]
+ public static Task Main(string[] args) {
+ try {
+ BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+ }
+ catch (DBusException e) { }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ throw;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure<App>()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/MatrixUtils.Desktop/Properties/launchSettings.json b/MatrixUtils.Desktop/Properties/launchSettings.json
new file mode 100644
index 0000000..36405e8
--- /dev/null
+++ b/MatrixUtils.Desktop/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Default": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+
+ }
+ },
+ "Development": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development",
+ "AVALONIA_THEME": "Dark"
+ }
+ },
+ "Local config": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Local"
+ }
+ }
+ }
+}
diff --git a/MatrixUtils.Desktop/RMUDesktopConfiguration.cs b/MatrixUtils.Desktop/RMUDesktopConfiguration.cs
new file mode 100644
index 0000000..62646ca
--- /dev/null
+++ b/MatrixUtils.Desktop/RMUDesktopConfiguration.cs
@@ -0,0 +1,47 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using ArcaneLibs.Extensions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MatrixUtils.Desktop;
+
+public class RMUDesktopConfiguration {
+ private static ILogger<RMUDesktopConfiguration> _logger;
+
+ [RequiresUnreferencedCode("Uses reflection binding")]
+ public RMUDesktopConfiguration(ILogger<RMUDesktopConfiguration> logger, IConfiguration config, HostBuilderContext host) {
+ _logger = logger;
+ logger.LogInformation("Loading configuration for environment: {}...", host.HostingEnvironment.EnvironmentName);
+ config.GetSection("RMUDesktop").Bind(this);
+ DataStoragePath = ExpandPath(DataStoragePath);
+ CacheStoragePath = ExpandPath(CacheStoragePath);
+ }
+
+ public string DataStoragePath { get; set; } = "";
+ public string CacheStoragePath { get; set; } = "";
+ public string? SentryDsn { get; set; }
+
+ private static string ExpandPath(string path, bool retry = true) {
+ _logger.LogInformation("Expanding path `{}`", path);
+
+ if (path.StartsWith('~')) {
+ path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), path[1..]);
+ }
+
+ Environment.GetEnvironmentVariables().Cast<DictionaryEntry>().OrderByDescending(x => x.Key.ToString()!.Length).ToList().ForEach(x => {
+ path = path.Replace($"${x.Key}", x.Value.ToString());
+ });
+
+ _logger.LogInformation("Expanded path to `{}`", path);
+ var tries = 0;
+ while (retry && path.ContainsAnyOf("~$".Split())) {
+ if (tries++ > 100)
+ throw new Exception($"Path `{path}` contains unrecognised environment variables");
+ path = ExpandPath(path, false);
+ }
+
+ return path;
+ }
+}
diff --git a/MatrixUtils.Desktop/RMUStorageWrapper.cs b/MatrixUtils.Desktop/RMUStorageWrapper.cs
new file mode 100644
index 0000000..c8172d0
--- /dev/null
+++ b/MatrixUtils.Desktop/RMUStorageWrapper.cs
@@ -0,0 +1,123 @@
+using Avalonia;
+using LibMatrix;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+
+namespace MatrixUtils.Desktop;
+
+public class RMUStorageWrapper(TieredStorageService storageService, HomeserverProviderService homeserverProviderService) {
+ public async Task<List<LoginResponse>?> GetAllTokens() {
+ if (!await storageService.DataStorageProvider.ObjectExistsAsync("rmu.tokens")) {
+ return null;
+ }
+ return await storageService.DataStorageProvider.LoadObjectAsync<List<LoginResponse>>("rmu.tokens") ??
+ new List<LoginResponse>();
+ }
+
+ public async Task<LoginResponse?> GetCurrentToken() {
+ if (!await storageService.DataStorageProvider.ObjectExistsAsync("token")) {
+ return null;
+ }
+ var currentToken = await storageService.DataStorageProvider.LoadObjectAsync<LoginResponse>("token");
+ var allTokens = await GetAllTokens();
+ if (allTokens is null or { Count: 0 }) {
+ await SetCurrentToken(null);
+ return null;
+ }
+
+ if (currentToken is null) {
+ await SetCurrentToken(currentToken = allTokens[0]);
+ }
+
+ if (!allTokens.Any(x => x.AccessToken == currentToken.AccessToken)) {
+ await SetCurrentToken(currentToken = allTokens[0]);
+ }
+
+ return currentToken;
+ }
+
+ public async Task AddToken(LoginResponse loginResponse) {
+ var tokens = await GetAllTokens() ?? new List<LoginResponse>();
+
+ tokens.Add(loginResponse);
+ await storageService.DataStorageProvider.SaveObjectAsync("rmu.tokens", tokens);
+ if (await GetCurrentToken() is null)
+ await SetCurrentToken(loginResponse);
+ }
+
+ private async Task<AuthenticatedHomeserverGeneric?> GetCurrentSession() {
+ var token = await GetCurrentToken();
+ if (token == null) {
+ return null;
+ }
+
+ return await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken);
+ }
+
+ public async Task<AuthenticatedHomeserverGeneric?> GetCurrentSessionOrPrompt() {
+ AuthenticatedHomeserverGeneric? session = null;
+
+ try {
+ //catch if the token is invalid
+ session = await GetCurrentSession();
+ }
+ catch (MatrixException e) {
+ if (e.ErrorCode == "M_UNKNOWN_TOKEN") {
+ var token = await GetCurrentToken();
+ // _navigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken);
+ return null;
+ }
+
+ throw;
+ }
+
+ if (session is null) {
+ // _navigationManager.NavigateTo("/Login");
+ var wnd = new LoginWindow(this);
+ wnd.Position = MainWindow.Instance.Position + new PixelPoint(50, 50);
+ await wnd.ShowDialog(MainWindow.Instance);
+ while (wnd.IsVisible) {
+ await Task.Delay(100);
+ }
+ session = await GetCurrentSession();
+ }
+
+ return session;
+ }
+
+ public class Settings {
+ public DeveloperSettings DeveloperSettings { get; set; } = new();
+ }
+
+ public class DeveloperSettings {
+ public bool EnableLogViewers { get; set; } = false;
+ public bool EnableConsoleLogging { get; set; } = true;
+ public bool EnablePortableDevtools { get; set; } = false;
+ }
+
+ public async Task RemoveToken(LoginResponse auth) {
+ var tokens = await GetAllTokens();
+ if (tokens == null) {
+ return;
+ }
+
+ tokens.RemoveAll(x => x.AccessToken == auth.AccessToken);
+ await storageService.DataStorageProvider.SaveObjectAsync("rmu.tokens", tokens);
+ }
+
+ public async Task SetCurrentToken(LoginResponse? auth) => await storageService.DataStorageProvider.SaveObjectAsync("token", auth);
+
+ public async Task<LoginResponse?> Login(string homeserver, string username, string password) {
+ try {
+ return await homeserverProviderService.Login(homeserver, username, password);
+ }
+ catch (MatrixException e) {
+ if (e.ErrorCode == "M_FORBIDDEN") {
+ return null;
+ }
+
+ throw;
+ }
+ }
+}
diff --git a/MatrixUtils.Desktop/SentryService.cs b/MatrixUtils.Desktop/SentryService.cs
new file mode 100644
index 0000000..c965632
--- /dev/null
+++ b/MatrixUtils.Desktop/SentryService.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Sentry;
+
+namespace MatrixUtils.Desktop;
+
+public class SentryService : IDisposable {
+ private IDisposable? _sentrySdkDisposable;
+ public SentryService(IServiceScopeFactory scopeFactory, ILogger<SentryService> logger) {
+ var config = scopeFactory.CreateScope().ServiceProvider.GetRequiredService<RMUDesktopConfiguration>();
+ if (config.SentryDsn is null) {
+ logger.LogWarning("Sentry DSN is not set, skipping Sentry initialisation");
+ return;
+ }
+ _sentrySdkDisposable = SentrySdk.Init(o => {
+ o.Dsn = config.SentryDsn;
+ // When configuring for the first time, to see what the SDK is doing:
+ o.Debug = true;
+ // Set traces_sample_rate to 1.0 to capture 100% of transactions for performance monitoring.
+ // We recommend adjusting this value in production.
+ o.TracesSampleRate = 1.0;
+ // Enable Global Mode if running in a client app
+ o.IsGlobalModeEnabled = true;
+ });
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose() => _sentrySdkDisposable?.Dispose();
+}
diff --git a/MatrixUtils.Desktop/app.manifest b/MatrixUtils.Desktop/app.manifest
new file mode 100644
index 0000000..1c4a2e1
--- /dev/null
+++ b/MatrixUtils.Desktop/app.manifest
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+ <!-- This manifest is used on Windows only.
+ Don't remove it as it might cause problems with window transparency and embeded controls.
+ For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
+ <assemblyIdentity version="1.0.0.0" name="MatrixUtils.Desktop.Desktop"/>
+
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- A list of the Windows versions that this application has been tested on
+ and is designed to work with. Uncomment the appropriate elements
+ and Windows will automatically select the most compatible environment. -->
+
+ <!-- Windows 10 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+ </application>
+ </compatibility>
+</assembly>
diff --git a/MatrixUtils.Desktop/appsettings.Development.json b/MatrixUtils.Desktop/appsettings.Development.json
new file mode 100644
index 0000000..a1add03
--- /dev/null
+++ b/MatrixUtils.Desktop/appsettings.Development.json
@@ -0,0 +1,14 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ },
+ "RMUDesktop": {
+ "DataStoragePath": "rmu-desktop/data",
+ "CacheStoragePath": "rmu-desktop/cache",
+ "SentryDsn": "https://a41e99dd2fdd45f699c432b21ebce632@sentry.thearcanebrony.net/15"
+ }
+}
diff --git a/MatrixUtils.Desktop/appsettings.json b/MatrixUtils.Desktop/appsettings.json
new file mode 100644
index 0000000..058723c
--- /dev/null
+++ b/MatrixUtils.Desktop/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ },
+ "RMUDesktop": {
+ "DataStoragePath": "~/.local/share/rmu-desktop",
+ "CacheStoragePath": "~/.cache/rmu-desktop"
+ }
+}
|