about summary refs log tree commit diff
path: root/MatrixUtils.Desktop
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixUtils.Desktop')
-rw-r--r--MatrixUtils.Desktop/App.axaml10
-rw-r--r--MatrixUtils.Desktop/App.axaml.cs49
-rw-r--r--MatrixUtils.Desktop/Components/NavigationStack.axaml12
-rw-r--r--MatrixUtils.Desktop/Components/NavigationStack.axaml.cs72
-rw-r--r--MatrixUtils.Desktop/Components/Pages/RoomList.axaml21
-rw-r--r--MatrixUtils.Desktop/Components/Pages/RoomList.axaml.cs15
-rw-r--r--MatrixUtils.Desktop/Components/RoomListEntry.axaml16
-rw-r--r--MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs74
-rw-r--r--MatrixUtils.Desktop/LoginWindow.axaml25
-rw-r--r--MatrixUtils.Desktop/LoginWindow.axaml.cs37
-rw-r--r--MatrixUtils.Desktop/MainWindow.axaml16
-rw-r--r--MatrixUtils.Desktop/MainWindow.axaml.cs57
-rw-r--r--MatrixUtils.Desktop/MatrixUtils.Desktop.csproj50
-rw-r--r--MatrixUtils.Desktop/Program.cs33
-rw-r--r--MatrixUtils.Desktop/Properties/launchSettings.json27
-rw-r--r--MatrixUtils.Desktop/RMUDesktopConfiguration.cs47
-rw-r--r--MatrixUtils.Desktop/RMUStorageWrapper.cs123
-rw-r--r--MatrixUtils.Desktop/SentryService.cs29
-rw-r--r--MatrixUtils.Desktop/app.manifest18
-rw-r--r--MatrixUtils.Desktop/appsettings.Development.json14
-rw-r--r--MatrixUtils.Desktop/appsettings.json13
21 files changed, 758 insertions, 0 deletions
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&amp;::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" + } +}