diff --git a/.idea/.idea.Blacklight/.idea/avalonia.xml b/.idea/.idea.Blacklight/.idea/avalonia.xml index 6dab72c4346600c9550bce9c1c32463e1940ebbb..9fd4a891368518cc19195f3795a5827cd9fde7a1 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 f1fd528f5d53e5cbb2a12ef757282ae170f664bb..89c0383e5dfa66cd5bfdeba7a59673c55b20b9bd 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 7253eff06d9441cb40e2cfc342d862a6d5b314d7..58766c3c758ccb1f919d13eab159594db250a1e3 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 cb7d2e5471e976f8639956e696babf9462d6e2ab..1d156443e8516b539b626c7f77f998c1936fecb2 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 0000000000000000000000000000000000000000..06b0e5f04c1d296fab4f231ad73f22966c927da4 --- /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 14e43fe7f7f86548306e478bd6149e7c236c4ba2..3026c107a3ba8738d0eca985fabba254002c9c7f 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 778cd134ecbd1495443fad05fd2138424816a277..d43ac06eeb8fac54ab1311dc9a8503d639313bfe 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 20776a35eb0f68201e6ba4d646724222522e37e1..6f3a5b98fe87e5ab464eb89c890508513b07c9ec 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 d297dbf3cafd23f650f77c5ce77335128188e353..f835523f6d0e5f40263231284711536d5353b088 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 12532ac0cc605fb7077e77b376f055370b4f6f35..87ca236692cd657052f618737cc74b6f789f5413 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 0e17f96a38f11c05604a2ab721755ae8256f181c..2cbaf966f508feeb002cba5856b11f26334f4917 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 0000000000000000000000000000000000000000..36bc7d68c24066e3df49c89bf06eb6d883c20017 --- /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 Binary files /dev/null and b/Lightquark.NET/Assets/missing.png differ diff --git a/Lightquark.NET/Client.cs b/Lightquark.NET/Client.cs index df35aecae41ff78f3554cf6c91e27777bc165683..5bbf9697d4b9955f4eacc13fc7aa45200eb3dfd0 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 0000000000000000000000000000000000000000..27b3725c1b4686b5f108e4e4cf198696c988f6c4 --- /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 8de712c0b6b516a30d08269c6413cdbccb30f7fe..ae5c87b8a1a168809c95f8dff5f1bf1d67e088c6 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 0000000000000000000000000000000000000000..72a532a74d7f225109942d07b75fecf3fead6a38 --- /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 db44a8dd489ce237785b872699bad47c568ebdd6..c834c384eacb10d68f84997aa720d127876570d6 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 a9c0ef9de8f5eee030eb9aeba41492ef24ea1d80..14ce46715dd9ccc49ab2dbaacfcd61331294de0b 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 3b3b9bf9fa613ad7233670cd4cf13ac2cea5660e..c2faf49800f4cb4ad7bc27254e6c2e540ff1cc0c 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 220f8b12f8106c4d360f78cade6023d21e46d877..9385ec0653a198172c333489819e4c65e012ec3c 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 8a5a5adbe6da49af776d1bd86b35b06b62f63aa6..11f14f5fabf6ac685ff511d607400afa1d3451c2 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 e3835c7c5a7cc0243f568da33c22e3ffb686cd2a..d8edd9bc1ccaa0a0e326cd01fb22be8450cbf979 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 968d7bc34e6c31d78ddd12420cab8335d3279d72..23e403a33b758e48be428def9725a31dbee95373 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