From 2969b2f326a3e122c0863dbc05df74e80ab767ef Mon Sep 17 00:00:00 2001 From: Emilia <emilia@jumpsca.re> Date: Sun, 26 Jan 2025 20:37:13 +0200 Subject: [PATCH] There is probably something in this commit At least channel selection and basic messaging --- .idea/.idea.Blacklight/.idea/avalonia.xml | 2 +- Blacklight.sln.DotSettings.user | 2 + Blacklight/App.axaml | 51 +++- Blacklight/App.axaml.cs | 9 +- Blacklight/Util/MessageStatusEnumToColor.cs | 37 +++ Blacklight/ViewModels/DockFactory.cs | 140 ++++++++++- .../ViewModels/Documents/DocumentViewModel.cs | 70 +++++- Blacklight/Views/Documents/DocumentView.axaml | 52 +++- Blacklight/Views/MainWindow.axaml | 39 +-- .../Views/Tools/ResourceExplorerView.axaml | 2 +- .../Views/Tools/ResourceExplorerView.axaml.cs | 54 +++- Lightquark.NET/Assets/StaticFallback.cs | 9 + Lightquark.NET/Assets/missing.png | Bin 0 -> 2486 bytes Lightquark.NET/Client.cs | 2 +- Lightquark.NET/ClientMethods/Call.cs | 67 +++++ Lightquark.NET/ClientMethods/Gateway.cs | 19 +- Lightquark.NET/ClientMethods/Message.cs | 70 ++++++ Lightquark.NET/ClientMethods/Quark.cs | 2 + Lightquark.NET/ClientMethods/User.cs | 2 +- Lightquark.NET/Lightquark.NET.csproj | 4 + Lightquark.NET/Objects/Channel.cs | 60 ++++- Lightquark.NET/Objects/Message.cs | 232 ++++++++++++++---- Lightquark.NET/Objects/Reply/BaseReply.cs | 5 - Lightquark.NET/Objects/User.cs | 30 +-- 24 files changed, 798 insertions(+), 162 deletions(-) create mode 100644 Blacklight/Util/MessageStatusEnumToColor.cs create mode 100644 Lightquark.NET/Assets/StaticFallback.cs create mode 100644 Lightquark.NET/Assets/missing.png create mode 100644 Lightquark.NET/ClientMethods/Call.cs create mode 100644 Lightquark.NET/ClientMethods/Message.cs diff --git a/.idea/.idea.Blacklight/.idea/avalonia.xml b/.idea/.idea.Blacklight/.idea/avalonia.xml index 6dab72c..9fd4a89 100644 --- a/.idea/.idea.Blacklight/.idea/avalonia.xml +++ b/.idea/.idea.Blacklight/.idea/avalonia.xml @@ -8,7 +8,7 @@ <entry key="Blacklight/ErrorWindow.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" /> <entry key="Blacklight/Views/App.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" /> <entry key="Blacklight/Views/ClientView.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" /> - <entry key="Blacklight/Views/Documents/DocumentView.axaml" value="Blacklight/Blacklight.csproj" /> + <entry key="Blacklight/Views/Documents/DocumentView.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" /> <entry key="Blacklight/Views/ErrorBoundary.axaml" value="Blacklight.Desktop/Blacklight.Desktop.csproj" /> <entry key="Blacklight/Views/LoadingView.axaml" value="Blacklight/Blacklight.csproj" /> <entry key="Blacklight/Views/LoadingViewModel.axaml" value="Blacklight/Blacklight.csproj" /> diff --git a/Blacklight.sln.DotSettings.user b/Blacklight.sln.DotSettings.user index f1fd528..89c0383 100644 --- a/Blacklight.sln.DotSettings.user +++ b/Blacklight.sln.DotSettings.user @@ -5,10 +5,12 @@ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABinding_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F72cdf2fe6a3b54998bac4691eedd12e9311b7efeae35e65311b545606e2f_003FBinding_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AContentControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa0b45743773945a3bb67ca6440ae9eadf9c00_003F50_003F9d315227_003FContentControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADockFluentTheme_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc13735b26c914624b4ee4a88a575ae253ae00_003Feb_003F23efc6b0_003FDockFluentTheme_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADocument_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F70d6cb8d5417c39b894d858f68789b12b9b5fc997e73e89dab1897b4c6e4bf_003FDocument_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe8b9e6b427dceb6f436414441b760d4dc52661df17f386ce4caa48aab354989_003FFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHtmlControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F58931fa041a846b53462c4c4c5f52620bf9de09eff582aa4b1ed83c83e4e10ea_003FHtmlControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAssetLoader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fda232ecc044f4c3d8e856366253516531d2800_003F_005F27262_003FIAssetLoader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F52dbc4b2fb874f79fb4fec0b07cf7f6ca6ac95616e1d164ef210a0645a1aa9_003FIFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e29f6bc726b4d68bc914cbabf2575377600_003F59_003Fa3291e5a_003FIMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIToolDock_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9c3ecc55c41dc7ed59e07448c63f6e65a746b8d44d381c2de59e8b39f659a968_003FIToolDock_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALowerCaseStringEnumConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e29f6bc726b4d68bc914cbabf2575377600_003F27_003F3006d2c3_003FLowerCaseStringEnumConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANetworkInformation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b1b048ea0e94b28ad7cbac8ef6bc4c26e00_003Fb4_003Fff405095_003FNetworkInformation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> diff --git a/Blacklight/App.axaml b/Blacklight/App.axaml index 7253eff..58766c3 100644 --- a/Blacklight/App.axaml +++ b/Blacklight/App.axaml @@ -8,11 +8,54 @@ xmlns:local="using:Blacklight" xmlns:controls="clr-namespace:Dock.Model.Controls;assembly=Dock.Model" xmlns:core="clr-namespace:Dock.Model.Core;assembly=Dock.Model" + xmlns:documents="clr-namespace:Blacklight.ViewModels.Documents" + xmlns:util="clr-namespace:Blacklight.Util" RequestedThemeVariant="Default"> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <Application.Resources> - <idcr:ControlRecycling x:Key="ControlRecyclingKey" TryToUseIdAsKey="True" /> - <Color x:Key="RegionColor">Transparent</Color> + <ResourceDictionary> + <idcr:ControlRecycling x:Key="ControlRecyclingKey" TryToUseIdAsKey="True" /> + <util:MessageStatusEnumToColor x:Key="MessageStatusEnumToColor"/> + <Color x:Key="RegionColor">Transparent</Color> + <ResourceDictionary.ThemeDictionaries> + <ResourceDictionary x:Key="Light"> + <SolidColorBrush x:Key="RightBeltColor">#E2e9e9e9</SolidColorBrush> + <SolidColorBrush x:Key="NavBarColor">#E2D4DCE2</SolidColorBrush> + <SolidColorBrush x:Key="NavBarBorderColor">#e1e0dd</SolidColorBrush> + <SolidColorBrush x:Key="ResourceExplorerColor">#E2e9e9e9</SolidColorBrush> + <SolidColorBrush x:Key="ResourceExplorerBorderColor">#e1e0dd</SolidColorBrush> + <SolidColorBrush x:Key="BottomBarColor">#e9e9e9</SolidColorBrush> + <SolidColorBrush x:Key="DocumentColor">#D6e9e9e9</SolidColorBrush> + <SolidColorBrush x:Key="TabStripColor">#E2e9e9e9</SolidColorBrush> + <SolidColorBrush x:Key="TabPointerOverColor">#1E00FF00</SolidColorBrush> + <SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush> + <SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush> + <SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush> + <SolidColorBrush x:Key="DockThemeForegroundBrush">#000000</SolidColorBrush> + <SolidColorBrush x:Key="DockApplicationAccentForegroundBrush">#000000</SolidColorBrush> + <SolidColorBrush x:Key="TabItemSelectedBorderColor">#4e4d4a</SolidColorBrush> + <SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush> + <SolidColorBrush x:Key="MessageContentBrush">#000000</SolidColorBrush> + </ResourceDictionary> + <ResourceDictionary x:Key="Dark"> + <SolidColorBrush x:Key="RightBeltColor">#F22b2d30</SolidColorBrush> + <SolidColorBrush x:Key="NavBarColor">#F21e1f22</SolidColorBrush> + <SolidColorBrush x:Key="NavBarBorderColor">#3c3f41</SolidColorBrush> + <SolidColorBrush x:Key="ResourceExplorerColor">#F22b2d30</SolidColorBrush> + <SolidColorBrush x:Key="ResourceExplorerBorderColor">#1e1f22</SolidColorBrush> + <SolidColorBrush x:Key="BottomBarColor">#2a2c2f</SolidColorBrush> + <SolidColorBrush x:Key="DocumentColor">#E6262626</SolidColorBrush> + <SolidColorBrush x:Key="TabStripColor">#F22b2d30</SolidColorBrush> + <SolidColorBrush x:Key="TabPointerOverColor">#0A00FF00</SolidColorBrush> + <SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush> + <SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush> + <SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush> + <SolidColorBrush x:Key="TabItemSelectedBorderColor">#b1b2b5</SolidColorBrush> + <SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush> + <SolidColorBrush x:Key="MessageContentBrush">#FFFFFF</SolidColorBrush> + </ResourceDictionary> + </ResourceDictionary.ThemeDictionaries> + </ResourceDictionary> </Application.Resources> <Application.DataTemplates> <local:ViewLocator /> @@ -55,12 +98,12 @@ <Style Selector="DocumentControl"> <Setter Property="HeaderTemplate"> - <DataTemplate DataType="core:IDockable"> + <DataTemplate DataType="documents:DocumentViewModel"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="6, 2"> <TextBlock Margin="0, 0, 2, 0" FontSize="16" Foreground="{DynamicResource ChannelHashtagColor}"> # </TextBlock> - <TextBlock VerticalAlignment="Center" Text="{Binding Title}" /> + <TextBlock VerticalAlignment="Center" FontStyle="{Binding FontStyle}" Text="{Binding Title}" /> </StackPanel> </DataTemplate> </Setter> diff --git a/Blacklight/App.axaml.cs b/Blacklight/App.axaml.cs index cb7d2e5..1d15644 100644 --- a/Blacklight/App.axaml.cs +++ b/Blacklight/App.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using Blacklight.Util; using Blacklight.ViewModels; using Blacklight.Views; @@ -19,6 +20,7 @@ public partial class App : Application { public static IServiceProvider? Services { get; set; } + public static DockContext DockContext { get; set; } public override void Initialize() { @@ -55,8 +57,11 @@ public partial class App : Application var client = serviceProvider.GetRequiredService<Client>(); client.OnLogOut = () => { - mainWindowVm.CurrentViewModel = loginViewModel; - loginViewModel.CurrentControl = new MainLoginPrompt(); + Dispatcher.UIThread.Invoke(() => + { + mainWindowVm.CurrentViewModel = loginViewModel; + loginViewModel.CurrentControl = new MainLoginPrompt(); + }); var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json"); try { diff --git a/Blacklight/Util/MessageStatusEnumToColor.cs b/Blacklight/Util/MessageStatusEnumToColor.cs new file mode 100644 index 0000000..06b0e5f --- /dev/null +++ b/Blacklight/Util/MessageStatusEnumToColor.cs @@ -0,0 +1,37 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Blacklight.Util; + +public class MessageStatusEnumToColor : IValueConverter +{ + public static readonly MessageStatusEnumToColor Instance = new(); + + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value != null) + { + // If it's not null, just return the value unchanged + return value; + } + + // If null, attempt to retrieve the resource from Application.Current.Resources + var app = Application.Current; + if (app?.Resources.TryGetValue("MessageContentBrush", out var foundResource) == true) + { + return foundResource; + } + + // If the resource is not found, return AvaloniaProperty.UnsetValue or a fallback + return Colors.Orange; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + // Usually not needed; throw or return original + return value; + } +} \ No newline at end of file diff --git a/Blacklight/ViewModels/DockFactory.cs b/Blacklight/ViewModels/DockFactory.cs index 14e43fe..3026c10 100644 --- a/Blacklight/ViewModels/DockFactory.cs +++ b/Blacklight/ViewModels/DockFactory.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Blacklight.Models.Documents; +using System.Linq; using Blacklight.Models.Tools; using Blacklight.ViewModels.Docks; using Blacklight.ViewModels.Documents; @@ -10,6 +10,7 @@ using Dock.Model.Controls; using Dock.Model.Core; using Dock.Model.Mvvm; using Dock.Model.Mvvm.Controls; +using Lightquark.NET.Objects; namespace Blacklight.ViewModels; @@ -22,12 +23,12 @@ public class DockFactory : Factory public override IRootDock CreateLayout() { - var document1 = new DocumentViewModel {Id = "Document1", Title = "Document 1", CanPin = false, CanFloat = false}; - var document2 = new DocumentViewModel {Id = "Document2", Title = "Document 2", CanPin = false, CanFloat = false}; - var document3 = new DocumentViewModel {Id = "Document3", Title = "Document 3", CanPin = false, CanFloat = false}; - var document4 = new DocumentViewModel {Id = "Document4", Title = "Document 4", CanPin = false, CanFloat = false}; - var document5 = new DocumentViewModel {Id = "Document5", Title = "Document 5", CanPin = false, CanFloat = false}; - var document6 = new DocumentViewModel {Id = "Document6", Title = "Document 6", CanPin = false, CanFloat = false}; + // var document1 = new DocumentViewModel {Id = "Document1", Title = "Document 1", CanPin = false, CanFloat = false}; + // var document2 = new DocumentViewModel {Id = "Document2", Title = "Document 2", CanPin = false, CanFloat = false}; + // var document3 = new DocumentViewModel {Id = "Document3", Title = "Document 3", CanPin = false, CanFloat = false}; + // var document4 = new DocumentViewModel {Id = "Document4", Title = "Document 4", CanPin = false, CanFloat = false}; + // var document5 = new DocumentViewModel {Id = "Document5", Title = "Document 5", CanPin = false, CanFloat = false}; + // var document6 = new DocumentViewModel {Id = "Document6", Title = "Document 6", CanPin = false, CanFloat = false}; var resourceExplorer = new ResourceExplorerViewModel { @@ -65,8 +66,8 @@ public class DockFactory : Factory var documentDock = new CustomDocumentDock { IsCollapsable = false, - ActiveDockable = document1, - VisibleDockables = CreateList<IDockable>(document1, document2, document3, document4, document5, document6), + ActiveDockable = null, + VisibleDockables = CreateList<IDockable>(), CanCreateDocument = false }; @@ -93,6 +94,8 @@ public class DockFactory : Factory _documentDock = documentDock; _rootDock = rootDock; + + App.DockContext = new DockContext(_documentDock, mainLayout); return rootDock; } @@ -101,8 +104,6 @@ public class DockFactory : Factory { ContextLocator = new Dictionary<string, Func<object?>> { - ["Document1"] = () => new Document1(), - ["Document2"] = () => new Document1(), ["Resource Explorer"] = () => new ResourceExplorer(), }; @@ -119,4 +120,119 @@ public class DockFactory : Factory base.InitLayout(layout); } -} \ No newline at end of file +} + +public class DockContext +{ + private IDock _mainLayoutDock; + private IDocumentDock DocumentDock => GetDocumentDocks(_mainLayoutDock).First().DocumentDock; + + private DocumentViewModel? _ephemeralDocument; + + public DockContext(IDocumentDock documentDock, IDock mainLayoutDock) + { + _mainLayoutDock = mainLayoutDock; + } + + private List<DocumentDockWithParent> GetDocumentDocks(IDock dock) + { + List<DocumentDockWithParent> documentDocks = []; + if (dock.VisibleDockables == null) return documentDocks; + foreach (var dockable in dock.VisibleDockables) + { + switch (dockable) + { + case IDocumentDock docDock: + documentDocks.Add(new DocumentDockWithParent + { + DocumentDock = docDock, + ParentDock = dock + }); + break; + case IProportionalDock propDock: + documentDocks.AddRange(GetDocumentDocks(propDock)); + break; + } + } + + return documentDocks; + } + + public void AddDocument(DocumentViewModel document, IDock? parent = null) + { + parent ??= DocumentDock; + if (parent.VisibleDockables == null) throw new Exception("wtf? parent.VisibleDockables is null"); + parent.VisibleDockables.Add(document); + parent.ActiveDockable = document; + } + + public void RemoveDocument(DocumentViewModel document) + { + var docks = GetDocumentDocks(_mainLayoutDock); + foreach (var dockWithParent in docks) + { + if (dockWithParent.DocumentDock.VisibleDockables == null) throw new Exception("wtf? _documentDock.VisibleDockables is null"); + dockWithParent.DocumentDock.VisibleDockables.Remove(document); + } + } + + public bool TryFocusChannel(Channel channel, bool makePermanent = false) + { + var docks = GetDocumentDocks(_mainLayoutDock); + foreach (var dockWithParent in docks) + { + if (dockWithParent.DocumentDock.VisibleDockables?.FirstOrDefault(d => (d as DocumentViewModel)?.Channel == channel) is not DocumentViewModel doc) continue; + dockWithParent.DocumentDock.ActiveDockable = doc; + dockWithParent.ParentDock.ActiveDockable = dockWithParent.DocumentDock; + if (makePermanent) + { + if (_ephemeralDocument?.Channel == channel) + { + _ephemeralDocument = null; + } + doc.IsEphemeral = false; + } + + return true; + } + return false; + } + + public void AddDocument(Channel channel, bool isEphemeral, bool shouldCloseEphemeral = true) + { + var document = new DocumentViewModel + { + Id = channel.Id.ToString(), + Channel = channel, + Title = channel.Name, + CanPin = false, + CanFloat = false + }; + if (!isEphemeral) + { + if (_ephemeralDocument?.Channel == channel) + { + // I am not sure this bit is ever actually run, since TryFocusChannel does similar stuff + _ephemeralDocument.IsEphemeral = false; + DocumentDock.ActiveDockable = _ephemeralDocument; + _ephemeralDocument = null; + } + else + { + AddDocument(document); + } + return; + } + + if (shouldCloseEphemeral && _ephemeralDocument != null) RemoveDocument(_ephemeralDocument); + document.IsEphemeral = true; + _ephemeralDocument = document; + AddDocument(_ephemeralDocument); + } + + private record DocumentDockWithParent + { + public required IDocumentDock DocumentDock; + public required IDock ParentDock; + } +} diff --git a/Blacklight/ViewModels/Documents/DocumentViewModel.cs b/Blacklight/ViewModels/Documents/DocumentViewModel.cs index 778cd13..d43ac06 100644 --- a/Blacklight/ViewModels/Documents/DocumentViewModel.cs +++ b/Blacklight/ViewModels/Documents/DocumentViewModel.cs @@ -1,7 +1,75 @@ -using Dock.Model.Mvvm.Controls; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Windows.Input; +using Avalonia.Media; +using CommunityToolkit.Mvvm.Input; +using Dock.Model.Mvvm.Controls; +using Lightquark.NET; +using Lightquark.NET.Objects; +using Microsoft.Extensions.DependencyInjection; +using Serilog; namespace Blacklight.ViewModels.Documents; public class DocumentViewModel : Document { + private bool _isEphemeral; + private string _messageInput = ""; + public required Channel Channel { get; set; } + + public ObservableCollection<Message> Messages + { + get + { + var client = App.Services!.GetRequiredService<Client>(); + if (!client.Messages.TryGetValue(Channel.Id, out var messageCollection)) + { + Log.Information("Channel {ChannelId} has no messages, creating collection", Channel.Id); + messageCollection = new ObservableCollection<Message>(); + client.Messages[Channel.Id] = messageCollection; + } + return messageCollection; + } + } + + public bool IsEphemeral + { + get => _isEphemeral; + set + { + if (SetProperty(ref _isEphemeral, value)) OnPropertyChanged(nameof(FontStyle)); + } + } + + public FontStyle FontStyle => IsEphemeral ? FontStyle.Italic : FontStyle.Normal; + public ICommand SendCommand => new RelayCommand(SendMessage); + + public string MessageInput + { + get => _messageInput; + set => SetProperty(ref _messageInput, value); + } + + public ICommand TestCommand => new RelayCommand(Test); + + private void Test() + { + Log.Information("Channel {ChannelId} has {MessageCount} messages", Channel.Id, Messages.Count); + } + + private async void SendMessage() + { + try + { + Log.Information("Sending message {MessageInput}", MessageInput); + var client = App.Services!.GetRequiredService<Client>(); + client.SendMessage(MessageInput, Channel.Id); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to send message"); + } + MessageInput = ""; + } } \ No newline at end of file diff --git a/Blacklight/Views/Documents/DocumentView.axaml b/Blacklight/Views/Documents/DocumentView.axaml index 20776a3..6f3a5b9 100644 --- a/Blacklight/Views/Documents/DocumentView.axaml +++ b/Blacklight/Views/Documents/DocumentView.axaml @@ -4,14 +4,52 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vm="using:Blacklight.ViewModels.Documents" + xmlns:util="clr-namespace:Blacklight.Util" + xmlns:objects="clr-namespace:Lightquark.NET.Objects;assembly=Lightquark.NET" + xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections" mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="400" x:DataType="vm:DocumentViewModel" x:CompileBindings="True"> - <Grid Focusable="True" Background="{DynamicResource DocumentColor}"> - <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> - <TextBlock Text="{Binding Id}" Padding="2" /> - <TextBlock Text="{Binding Title}" Padding="2" /> - <TextBlock Text="{Binding Context}" Padding="2" /> - </StackPanel> - </Grid> + <!-- <Grid Focusable="True" Background="{DynamicResource DocumentColor}"> --> + <Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="{DynamicResource DocumentColor}"> + <Grid.RowDefinitions> + <RowDefinition Height="*"/> + <RowDefinition Height="Auto"/> + </Grid.RowDefinitions> + <StackPanel Grid.Row="0"> + <!-- <TextBlock Text="{Binding Id}" Padding="2" /> --> + <!-- <TextBlock Text="{Binding Title}" Padding="2" /> --> + <!-- <TextBlock Text="{Binding Context}" Padding="2" /> --> + <!-- <TextBlock Text="{Binding IsEphemeral}" Padding="2" /> --> + <!-- <Button Command="{Binding TestCommand}">Meow</Button> --> + <ItemsControl ItemsSource="{Binding Messages}" > + <ItemsControl.ItemTemplate> + <DataTemplate> + <StackPanel Margin="8 3" Orientation="Vertical"> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" FontWeight="Bold" Text="{Binding VisualAuthor.Username}" /> + <Border Background="DeepPink" Padding="2" CornerRadius="3" Margin="5 0" IsVisible="{Binding VisualAuthor.IsBot}"> + <TextBlock FontWeight="Bold" Text="{Binding VisualAuthor.BotTag}" /> + </Border> + </StackPanel> + <TextBlock Foreground="{Binding Visuals.TextColor, Converter={StaticResource MessageStatusEnumToColor}}" Text="{Binding Content}" /> + </StackPanel> + </DataTemplate> + </ItemsControl.ItemTemplate> + </ItemsControl> + </StackPanel> + <Grid Grid.Row="1" HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <TextBox MaxHeight="200" Grid.Column="0" AcceptsReturn="True" TextWrapping="Wrap" Text="{Binding MessageInput}"> + <TextBox.KeyBindings> + <KeyBinding Gesture="Enter" Command="{Binding SendCommand}"/> + </TextBox.KeyBindings> + </TextBox> + <Button Grid.Column="1" Command="{Binding SendCommand}">Send</Button> + </Grid> + </Grid> + <!-- </Grid> --> </UserControl> \ No newline at end of file diff --git a/Blacklight/Views/MainWindow.axaml b/Blacklight/Views/MainWindow.axaml index d297dbf..f835523 100644 --- a/Blacklight/Views/MainWindow.axaml +++ b/Blacklight/Views/MainWindow.axaml @@ -6,6 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:login="clr-namespace:Blacklight.Views.Login" + xmlns:util="clr-namespace:Blacklight.Util" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Blacklight.Views.MainWindow" x:DataType="vm:MainWindowViewModel" @@ -23,44 +24,6 @@ </Window.Background> <Window.Resources> - <ResourceDictionary> - <ResourceDictionary.ThemeDictionaries> - <ResourceDictionary x:Key="Light"> - <SolidColorBrush x:Key="RightBeltColor">#E2e9e9e9</SolidColorBrush> - <SolidColorBrush x:Key="NavBarColor">#E2D4DCE2</SolidColorBrush> - <SolidColorBrush x:Key="NavBarBorderColor">#e1e0dd</SolidColorBrush> - <SolidColorBrush x:Key="ResourceExplorerColor">#E2e9e9e9</SolidColorBrush> - <SolidColorBrush x:Key="ResourceExplorerBorderColor">#e1e0dd</SolidColorBrush> - <SolidColorBrush x:Key="BottomBarColor">#e9e9e9</SolidColorBrush> - <SolidColorBrush x:Key="DocumentColor">#D6e9e9e9</SolidColorBrush> - <SolidColorBrush x:Key="TabStripColor">#E2e9e9e9</SolidColorBrush> - <SolidColorBrush x:Key="TabPointerOverColor">#1E00FF00</SolidColorBrush> - <SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush> - <SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush> - <SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush> - <SolidColorBrush x:Key="DockThemeForegroundBrush">#000000</SolidColorBrush> - <SolidColorBrush x:Key="DockApplicationAccentForegroundBrush">#000000</SolidColorBrush> - <SolidColorBrush x:Key="TabItemSelectedBorderColor">#4e4d4a</SolidColorBrush> - <SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush> - </ResourceDictionary> - <ResourceDictionary x:Key="Dark"> - <SolidColorBrush x:Key="RightBeltColor">#F22b2d30</SolidColorBrush> - <SolidColorBrush x:Key="NavBarColor">#F21e1f22</SolidColorBrush> - <SolidColorBrush x:Key="NavBarBorderColor">#3c3f41</SolidColorBrush> - <SolidColorBrush x:Key="ResourceExplorerColor">#F22b2d30</SolidColorBrush> - <SolidColorBrush x:Key="ResourceExplorerBorderColor">#1e1f22</SolidColorBrush> - <SolidColorBrush x:Key="BottomBarColor">#2a2c2f</SolidColorBrush> - <SolidColorBrush x:Key="DocumentColor">#E6262626</SolidColorBrush> - <SolidColorBrush x:Key="TabStripColor">#F22b2d30</SolidColorBrush> - <SolidColorBrush x:Key="TabPointerOverColor">#0A00FF00</SolidColorBrush> - <SolidColorBrush x:Key="DockApplicationAccentBrushLow">Transparent</SolidColorBrush> - <SolidColorBrush x:Key="DockApplicationAccentBrushMed">Transparent</SolidColorBrush> - <SolidColorBrush x:Key="DockThemeBorderLowBrush">Transparent</SolidColorBrush> - <SolidColorBrush x:Key="TabItemSelectedBorderColor">#b1b2b5</SolidColorBrush> - <SolidColorBrush x:Key="ChannelHashtagColor">#38ae39</SolidColorBrush> - </ResourceDictionary> - </ResourceDictionary.ThemeDictionaries> - </ResourceDictionary> </Window.Resources> <Window.DataTemplates> diff --git a/Blacklight/Views/Tools/ResourceExplorerView.axaml b/Blacklight/Views/Tools/ResourceExplorerView.axaml index 12532ac..87ca236 100644 --- a/Blacklight/Views/Tools/ResourceExplorerView.axaml +++ b/Blacklight/Views/Tools/ResourceExplorerView.axaml @@ -9,7 +9,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Blacklight.Views.Tools.ResourceExplorerView"> <Grid ColumnDefinitions="*" RowDefinitions="*" Background="{DynamicResource ResourceExplorerColor}"> - <TreeView Grid.Row="0" ItemsSource="{Binding $parent[views:ClientView].((viewModels:ClientViewModel)DataContext).Client.ResourceTree}"> + <TreeView Grid.Row="0" ItemsSource="{Binding $parent[views:ClientView].((viewModels:ClientViewModel)DataContext).Client.ResourceTree}" SelectionChanged="ItemSelected"> <TreeView.DataTemplates> <util:ResourceExplorerTemplateSelector> <TreeDataTemplate x:Key="QuarkTemplate" x:DataType="objects:QuarkListItem" ItemsSource="{Binding Children}"> diff --git a/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs b/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs index 0e17f96..2cbaf96 100644 --- a/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs +++ b/Blacklight/Views/Tools/ResourceExplorerView.axaml.cs @@ -1,18 +1,68 @@ -using Avalonia; +using System; +using System.Timers; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; using Blacklight.Util; using Dock.Model.Controls; using Dock.Model.Core; +using Lightquark.NET.Objects; +using Serilog; namespace Blacklight.Views.Tools; public partial class ResourceExplorerView : UserControl { - + private QuarkListItem? _selectedItem; + private Timer? _doubleTimer; public ResourceExplorerView() { InitializeComponent(); } + + private void ItemSelected(object? sender, SelectionChangedEventArgs e) + { + if (sender is TreeView treeView && treeView?.SelectedItem is QuarkListItem quarkListItem) + { + switch (quarkListItem.Type) + { + case QuarkListItemType.Channel: + if (_selectedItem == quarkListItem) + { + var focus = App.DockContext.TryFocusChannel(quarkListItem.Channel!, true); + if (focus) return; + Log.Information("zamn thats a double click"); + _doubleTimer?.Dispose(); + _selectedItem = null; + App.DockContext.AddDocument(quarkListItem.Channel!, false); + // TODO: Open persistent tab + } + else + { + _selectedItem = quarkListItem; + var focus = App.DockContext.TryFocusChannel(quarkListItem.Channel!); + if (focus) return; + _doubleTimer?.Dispose(); + _doubleTimer = new Timer(TimeSpan.FromSeconds(5)); + _doubleTimer.Elapsed += (_, _) => + { + Log.Information("Welp they took too long to click im tired of waiting"); + _selectedItem = null; + _doubleTimer?.Dispose(); + }; + _doubleTimer.Start(); + + // TODO: Open or update ephemeral tab + App.DockContext.AddDocument(quarkListItem.Channel!, true); + } + break; + // Currently we only care about channels + case QuarkListItemType.Quark: + case QuarkListItemType.Folder: + default: + return; + } + } + } } \ No newline at end of file diff --git a/Lightquark.NET/Assets/StaticFallback.cs b/Lightquark.NET/Assets/StaticFallback.cs new file mode 100644 index 0000000..36bc7d6 --- /dev/null +++ b/Lightquark.NET/Assets/StaticFallback.cs @@ -0,0 +1,9 @@ +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Lightquark.NET.Assets; + +internal class StaticFallback +{ + public static Bitmap QuarkIcon = new Bitmap(AssetLoader.Open(new Uri("avares://Lightquark.NET/Assets/missing.png"))); +} \ No newline at end of file diff --git a/Lightquark.NET/Assets/missing.png b/Lightquark.NET/Assets/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..b2e3d4158502266dfab8b3f8fc89d901e4509b86 GIT binary patch literal 2486 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 W!vZT5b}=w8FnGH9xvX<aXaWG;(a9qK literal 0 HcmV?d00001 diff --git a/Lightquark.NET/Client.cs b/Lightquark.NET/Client.cs index df35aec..5bbf969 100644 --- a/Lightquark.NET/Client.cs +++ b/Lightquark.NET/Client.cs @@ -26,7 +26,7 @@ public partial class Client : ObservableObject public ObservableCollection<QuarkListItem> ResourceTree { get; } = []; public ObservableCollection<Quark> Quarks { get; } = []; public ObservableCollection<Channel> Channels { get; } = []; - public ObservableCollection<Message> Messages { get; } = []; + public Dictionary<ObjectId, ObservableCollection<Message>> Messages = new(); public ObservableCollection<User> Users { get; } = []; public User? CurrentUser diff --git a/Lightquark.NET/ClientMethods/Call.cs b/Lightquark.NET/ClientMethods/Call.cs new file mode 100644 index 0000000..27b3725 --- /dev/null +++ b/Lightquark.NET/ClientMethods/Call.cs @@ -0,0 +1,67 @@ +using System.Net.Http.Headers; +using System.Reflection; +using Lightquark.NET.Objects.Reply; +using Newtonsoft.Json; +using Serilog; + +namespace Lightquark.NET; + +public partial class Client +{ + public async Task<Reply<T>> Call<T>(string method, string route, object? body) where T : BaseReplyResponse + { + Log.Information("Starting HTTP to {Method} {Route}", method, route); + try + { + var finalUri = new Uri(new Uri(NetworkInformation!.BaseUrl!), route); + Log.Information("Using app server {BaseUrl}", NetworkInformation.BaseUrl); + + HttpContent finalBody; + if (body is MultipartFormDataContent formDataBody) + { + finalBody = formDataBody; + } + else + { + finalBody = new StringContent(JsonConvert.SerializeObject(body)); + finalBody.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + } + + var request = new HttpRequestMessage(); + request.Method = method switch + { + "GET" => HttpMethod.Get, + "POST" => HttpMethod.Post, + "PATCH" => HttpMethod.Patch, + "PUT" => HttpMethod.Put, + "HEAD" => HttpMethod.Head, + "DELETE" => HttpMethod.Delete, + _ => throw new ArgumentOutOfRangeException(nameof(method), method, null) + }; + request.RequestUri = finalUri; + request.Content = finalBody; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); + request.Headers.Add("lq-agent", $"Blacklight/{Assembly.GetEntryAssembly()?.GetName().Version?.ToString()}"); + + var res = await _httpClient.SendAsync(request); + + Log.Information("Received response for {Path} with code {StatusCode}", route, res.StatusCode); + + var rawResponse = await res.Content.ReadAsStringAsync(); + + Log.Debug(rawResponse); + + var response = JsonConvert.DeserializeObject<Reply<T>>(rawResponse); + + if (response == null) throw new Exception($"Failed to parse API response: {rawResponse}"); + + return response; + + } + catch (Exception ex) + { + Log.Error(ex, "Failed HTTP call to API"); + throw; + } + } +} \ No newline at end of file diff --git a/Lightquark.NET/ClientMethods/Gateway.cs b/Lightquark.NET/ClientMethods/Gateway.cs index 8de712c..ae5c87b 100644 --- a/Lightquark.NET/ClientMethods/Gateway.cs +++ b/Lightquark.NET/ClientMethods/Gateway.cs @@ -2,6 +2,8 @@ using Lightquark.NET.Objects; using Lightquark.NET.Objects.Reply; using Lightquark.NET.Util; +using Lightquark.NET.Util.Converters; +using MongoDB.Bson; using Newtonsoft.Json; using Serilog; using Websocket.Client; @@ -101,9 +103,13 @@ public partial class Client CurrentUserFromObject(userUpdateMessage.User); } break; + case "messageCreate": + var messageCreateMessage = ParseGateway<MessageCreateGatewayMessage>(msg.Text!); + MessageFromObject(messageCreateMessage.Message, messageCreateMessage.ChannelId); + break; case "gatekeeperMeasure": var measureMessage = ParseGateway<MeasureGatewayMessage>(msg.Text!); - // GatekeeperMeasure(measureMessage); + GatekeeperMeasure(measureMessage); break; case "gatekeeperSelection": var selectionMessage = ParseGateway<SelectionGatewayMessage>(msg.Text!); @@ -124,7 +130,7 @@ public partial class Client { SendGatewayMessage(new {@event = "heartbeat"}); } - + public async Task<Reply<T>> CallRpc<T>(string method, string route, object? body) where T : BaseReplyResponse { Log.Information("Starting RPC to {Method} {Route}", method, route); @@ -224,6 +230,15 @@ public record UserUpdateGatewayMessage : BaseGatewayMessage public required User User { get; init; } } +public record MessageCreateGatewayMessage : BaseGatewayMessage +{ + [JsonProperty("message")] + public required Message Message { get; init; } + [JsonProperty("channelId")] + [JsonConverter(typeof(ObjectIdConverter))] + public required ObjectId ChannelId { get; init; } +} + public record GatewayBusEvent { public required BaseGatewayMessage BaseMessage { get; init; } diff --git a/Lightquark.NET/ClientMethods/Message.cs b/Lightquark.NET/ClientMethods/Message.cs new file mode 100644 index 0000000..72a532a --- /dev/null +++ b/Lightquark.NET/ClientMethods/Message.cs @@ -0,0 +1,70 @@ +using System.Collections.ObjectModel; +using Lightquark.NET.Objects; +using Lightquark.NET.Objects.Reply; +using MongoDB.Bson; +using Newtonsoft.Json; +using Serilog; + +namespace Lightquark.NET; + +public partial class Client +{ + public async void SendMessage(string message, ObjectId channelId) + { + try + { + var body = new MultipartFormDataContent(); + body.Add(new StringContent(JsonConvert.SerializeObject(new + { + content = message + })), "payload"); + var resTask = Call<MessageSendResponse>("POST", $"/{Version}/channel/{channelId}/messages", body); + var fakeId = ObjectId.GenerateNewId(); + var fakeMsg = new Message + { + Id = fakeId, + Author = CurrentUser, + Content = message, + Status = MessageStatus.Sending + }; + Messages[channelId].Add(fakeMsg); + var res = await resTask; + if (res.Request.Success) + { + Messages[channelId].Remove(fakeMsg); + } + else + { + fakeMsg.Status = MessageStatus.Fail; + fakeMsg.Content += $"\nFailed to send: {res.Response.Message}"; + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to send message"); + } + } + + private void MessageFromObject(Message message, ObjectId? channelId = null) + { + channelId ??= message.ChannelId; + message.ChannelId = channelId.Value; + if (!Messages.TryGetValue(channelId.Value, out var messageCollection)) + { + Log.Information("Channel {ChannelId} has no prior messages, creating collection", channelId); + messageCollection = []; + Messages[channelId.Value] = messageCollection; + } + // If no message with same id in collection add message + if (messageCollection.All(m => m.Id != message.Id)) + { + Log.Information("New message {MessageId} in channel {ChannelId}", message.Id, channelId.Value); + messageCollection.Add(message); + } + } +} + +public record MessageSendResponse : BaseReplyResponse +{ + +} \ No newline at end of file diff --git a/Lightquark.NET/ClientMethods/Quark.cs b/Lightquark.NET/ClientMethods/Quark.cs index db44a8d..c834c38 100644 --- a/Lightquark.NET/ClientMethods/Quark.cs +++ b/Lightquark.NET/ClientMethods/Quark.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Avalonia.Media.Imaging; +using Lightquark.NET.Assets; using Lightquark.NET.Objects; using Lightquark.NET.Objects.Reply; using MongoDB.Bson; @@ -251,6 +252,7 @@ public partial class Client catch (Exception ex) { Log.Error(ex, "Failed to get quark icon for {@Quark}", quark); + quark.Icon = StaticFallback.QuarkIcon; } return quark; } diff --git a/Lightquark.NET/ClientMethods/User.cs b/Lightquark.NET/ClientMethods/User.cs index a9c0ef9..14ce467 100644 --- a/Lightquark.NET/ClientMethods/User.cs +++ b/Lightquark.NET/ClientMethods/User.cs @@ -18,7 +18,7 @@ public partial class Client { try { - var avatarRes = await _httpClient.GetAsync(user!.AvatarUriGetter); + var avatarRes = await _httpClient.GetAsync(user!.AvatarUri); user.Avatar = new Bitmap(await avatarRes.Content.ReadAsStreamAsync()); } catch (Exception ex) diff --git a/Lightquark.NET/Lightquark.NET.csproj b/Lightquark.NET/Lightquark.NET.csproj index 3b3b9bf..c2faf49 100644 --- a/Lightquark.NET/Lightquark.NET.csproj +++ b/Lightquark.NET/Lightquark.NET.csproj @@ -17,4 +17,8 @@ <PackageReference Include="Websocket.Client" Version="5.1.2" /> </ItemGroup> + <ItemGroup> + <AvaloniaResource Include="Assets\missing.png" /> + </ItemGroup> + </Project> diff --git a/Lightquark.NET/Objects/Channel.cs b/Lightquark.NET/Objects/Channel.cs index 220f8b1..9385ec0 100644 --- a/Lightquark.NET/Objects/Channel.cs +++ b/Lightquark.NET/Objects/Channel.cs @@ -1,4 +1,4 @@ -using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; using Lightquark.NET.Util.Converters; using Lightquark.Types.Mongo; using MongoDB.Bson; @@ -6,29 +6,63 @@ using Newtonsoft.Json; namespace Lightquark.NET.Objects; -public class Channel : IChannel +public class Channel : ObservableObject, IChannel { + private ObjectId _id; + private string _name; + private string _description; + private int? _index; + private ObjectId _quarkId; + private Quark[]? _virtualQuarks; + [JsonProperty("_id")] [JsonConverter(typeof(ObjectIdConverter))] - public ObjectId Id { get; set; } + public ObjectId Id + { + get => _id; + set => SetProperty(ref _id, value); + } [JsonProperty("name")] - public string Name { get; set; } - + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + [JsonProperty("description")] - public string Description { get; set; } - + public string Description + { + get => _description; + set => SetProperty(ref _description, value); + } + [JsonProperty("index")] - public int? Index { get; set; } - + public int? Index + { + get => _index; + set => SetProperty(ref _index, value); + } + [JsonIgnore] - public ObjectId QuarkId { get; set; } + public ObjectId QuarkId + { + get => _quarkId; + set => SetProperty(ref _quarkId, value); + } // Virtuals - + [JsonIgnore] - public Quark[]? VirtualQuarks { get; set; } - + public Quark[]? VirtualQuarks + { + get => _virtualQuarks; + set + { + if (SetProperty(ref _virtualQuarks, value)) OnPropertyChanged(nameof(Quark)); + } + } + [JsonProperty("quark")] public IQuark? Quark => VirtualQuarks?.First(); } \ No newline at end of file diff --git a/Lightquark.NET/Objects/Message.cs b/Lightquark.NET/Objects/Message.cs index 8a5a5ad..11f14f5 100644 --- a/Lightquark.NET/Objects/Message.cs +++ b/Lightquark.NET/Objects/Message.cs @@ -1,4 +1,8 @@ -using Lightquark.NET.Util.Converters; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using Lightquark.NET.Util.Converters; namespace Lightquark.NET.Objects; @@ -7,33 +11,61 @@ using MongoDB.Bson; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -public class Message() : IMessage +public class Message() : ObservableObject { [JsonProperty("_id")] [JsonConverter(typeof(ObjectIdConverter))] - public ObjectId Id { get; set; } - + public ObjectId Id + { + get => _id; + set => SetProperty(ref _id, value); + } + [JsonIgnore] [JsonConverter(typeof(ObjectIdConverter))] - public ObjectId AuthorId { get; set; } - + public ObjectId AuthorId + { + get => _authorId; + set => SetProperty(ref _authorId, value); + } + [JsonProperty("content")] - public string? Content { get; set; } - + public string? Content + { + get => _content; + set => SetProperty(ref _content, value); + } + [JsonIgnore] [JsonConverter(typeof(ObjectIdConverter))] - public ObjectId ChannelId { get; set; } + public ObjectId ChannelId + { + get => _channelId; + set => SetProperty(ref _channelId, value); + } [JsonProperty("ua")] - public string UserAgent { get; set; } = "Unknown"; - + public string UserAgent + { + get => _userAgent; + set => SetProperty(ref _userAgent, value); + } + [JsonProperty("timestamp")] - public long Timestamp { get; set; } - + public long Timestamp + { + get => _timestamp; + set => SetProperty(ref _timestamp, value); + } + [JsonProperty("edited")] - public bool Edited { get; set; } - + public bool Edited + { + get => _edited; + set => SetProperty(ref _edited, value); + } + [JsonProperty("attachments")] public IAttachment[] Attachments { @@ -50,74 +82,161 @@ public class Message() : IMessage } [JsonIgnore] - public string[]? OldAttachments { get; set; } - - + public string[]? OldAttachments + { + get => _oldAttachments; + set + { + if (SetProperty(ref _oldAttachments, value)) OnPropertyChanged(nameof(Attachments)); + } + } + + [JsonIgnore] - public Attachment[]? NewAttachments { get; set; } + public Attachment[]? NewAttachments + { + get => _newAttachments; + set + { + if (SetProperty(ref _newAttachments, value)) OnPropertyChanged(nameof(Attachments)); + } + } [JsonProperty("specialAttributes")] - public JArray SpecialAttributes { get; set; } = []; - + public JArray SpecialAttributes + { + get => _specialAttributes; + set => SetProperty(ref _specialAttributes, value); + } + // Virtuals and stuff - + // TODO: merge these into one this is really stupid + [JsonIgnore] - public User[]? VirtualAuthors { get; set; } + public User[]? VirtualAuthors + { + get => _virtualAuthors; + set + { + if (SetProperty(ref _virtualAuthors, value)) OnPropertyChanged(nameof(Author)); + } + } [JsonProperty("author")] - public IUser? Author + public User? Author { get { var author = VirtualAuthors?.FirstOrDefault(); return author?.Safe; } + set => VirtualAuthors = [value!]; + } + + public User? VisualAuthor + { + get + { + var botMessage = _specialAttributes.FirstOrDefault(a => a["type"].ToString() == "botMessage"); + if (botMessage != null) + { + var avatarUriString = botMessage["avatarUri"]?.ToString(); + return new User() + { + Username = botMessage["username"]?.ToString() ?? Author!.Username, + AvatarUri = avatarUriString != null ? new Uri(avatarUriString) : Author!.AvatarUri, + BotTag = Author!.Username, + IsBot = true + }; + } + + if (Author!.IsBot) Author.BotTag = "BOT"; + return Author; + } } + private ObjectId _id; + private ObjectId _authorId; + private string? _content; + private ObjectId _channelId; + private string _userAgent = "Unknown"; + private long _timestamp; + private bool _edited; + private string[]? _oldAttachments; + private Attachment[]? _newAttachments; + private JArray _specialAttributes = []; + private User[]? _virtualAuthors; + private MessageStatus _status = MessageStatus.Complete; [JsonIgnore] - public static readonly BsonDocument[] LookupPipeline = - [ - new BsonDocument("$lookup", new BsonDocument + public MessageStatus Status + { + get => _status; + set { - { "from", "users" }, - { "localField", "authorId" }, - { "foreignField", "_id" }, - { "as", "VirtualAuthors" } - }) - ]; - - public Message(IMessage message) : this() - { - Id = message.Id; - AuthorId = message.AuthorId; - Content = message.Content; - ChannelId = message.ChannelId; - UserAgent = message.UserAgent; - Timestamp = message.Timestamp; - Edited = message.Edited; - NewAttachments = message.Attachments.Select(a => new Attachment(a)).ToArray(); - SpecialAttributes = message.SpecialAttributes; - VirtualAuthors = [(User)(message.Author ?? throw new Exception("Failed to convert Author"))]; + if (SetProperty(ref _status, value)) OnPropertyChanged(nameof(Visuals)); + } } + + public MessageVisuals Visuals => new() + { + TextColor = Status switch + { + MessageStatus.Sending => new SolidColorBrush(Colors.Gray), + MessageStatus.Fail => new SolidColorBrush(Colors.Red), + MessageStatus.Complete => null, + _ => throw new ArgumentOutOfRangeException() + } + }; } -public class Attachment() : IAttachment +public record MessageVisuals { + public IBrush? TextColor { get; set; } +} + +public class Attachment() : ObservableObject, IAttachment +{ + private Uri _url = null!; + private long _size; + private string _mimeType = "application/octet-stream"; + private string _filename = "Unknown"; + private ObjectId _fileId; + [JsonProperty("url")] - public Uri Url { get; set; } = null!; + public Uri Url + { + get => _url; + set => SetProperty(ref _url, value); + } [JsonProperty("size")] - public long Size { get; set; } + public long Size + { + get => _size; + set => SetProperty(ref _size, value); + } [JsonProperty("type")] - public string MimeType { get; set; } = "application/octet-stream"; + public string MimeType + { + get => _mimeType; + set => SetProperty(ref _mimeType, value); + } [JsonProperty("filename")] - public string Filename { get; set; } = "Unknown"; - + public string Filename + { + get => _filename; + set => SetProperty(ref _filename, value); + } + [JsonIgnore] - public ObjectId FileId { get; set; } + public ObjectId FileId + { + get => _fileId; + set => SetProperty(ref _fileId, value); + } public Attachment(IAttachment attachment) : this() { @@ -127,4 +246,11 @@ public class Attachment() : IAttachment Filename = attachment.Filename; FileId = attachment.FileId; } +} + +public enum MessageStatus +{ + Sending, + Fail, + Complete } \ No newline at end of file diff --git a/Lightquark.NET/Objects/Reply/BaseReply.cs b/Lightquark.NET/Objects/Reply/BaseReply.cs index e3835c7..d8edd9b 100644 --- a/Lightquark.NET/Objects/Reply/BaseReply.cs +++ b/Lightquark.NET/Objects/Reply/BaseReply.cs @@ -4,9 +4,7 @@ namespace Lightquark.NET.Objects.Reply; public record Reply<T> where T : BaseReplyResponse { - [JsonProperty("request")] public required ReplyRequest Request; - [JsonProperty("response")] public required T Response; } @@ -14,14 +12,11 @@ public record ReplyRequest { [JsonProperty("status_code")] public int StatusCode; - [JsonProperty("success")] public bool Success; - [JsonProperty("cat")] public string? Cat; } public record BaseReplyResponse { - [JsonProperty("message")] public required string Message; } \ No newline at end of file diff --git a/Lightquark.NET/Objects/User.cs b/Lightquark.NET/Objects/User.cs index 968d7bc..23e403a 100644 --- a/Lightquark.NET/Objects/User.cs +++ b/Lightquark.NET/Objects/User.cs @@ -8,18 +8,18 @@ using Newtonsoft.Json; namespace Lightquark.NET.Objects; -public class User : ObservableObject, IUser +public class User : ObservableObject { private Bitmap _avatar; private IStatus? _status; private string? _pronouns; private ObjectId _id; - private string _email; + private string? _email; private string _username; private bool _admin; private bool _isBot; private bool _secretThirdThing; - private Uri _avatarUriGetter; + private Uri _avatarUri; [JsonProperty("_id")] [JsonConverter(typeof(ObjectIdConverter))] @@ -29,19 +29,13 @@ public class User : ObservableObject, IUser set => SetProperty(ref _id, value); } - [JsonIgnore] - public required byte[] PasswordHash { get; init; } - [JsonProperty("email")] - public required string Email + public string? Email { get => _email; [MemberNotNull(nameof(_email))] set => SetProperty(ref _email, value); } - - [JsonIgnore] - public required byte[] Salt { get; init; } - + [JsonProperty("username")] public required string Username { @@ -63,6 +57,8 @@ public class User : ObservableObject, IUser set => SetProperty(ref _isBot, value); } + public string? BotTag { get; set; } = ""; + [JsonProperty("secretThirdThing")] public bool SecretThirdThing { @@ -92,16 +88,12 @@ public class User : ObservableObject, IUser // Calculated field [JsonProperty("avatarUri")] - public Uri AvatarUriGetter + public Uri AvatarUri { - get => _avatarUriGetter; - set => SetProperty(ref _avatarUriGetter, value); + get => _avatarUri; + set => SetProperty(ref _avatarUri, value); } - [JsonIgnore] - [JsonConverter(typeof(ObjectIdConverter))] - public ObjectId? AvatarFileId { get; set; } - - [JsonIgnore] public IUser Safe => this; + [JsonIgnore] public User Safe => this; } \ No newline at end of file -- GitLab