From 7e9e7613bac34962c126ddaeaec899956e1d8a6c Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 4 Oct 2026 20:03:01 +0100 Subject: [PATCH 01/46] Fix markdown draft preview sync in PostIt blog editor --- .../Controls/MarkdownEditorControl.axaml | 51 +++++++ .../Controls/MarkdownEditorControl.axaml.cs | 63 +++++++++ .../PostIt/ViewModels/Blogs/BlogsViewModel.cs | 52 ++++++- src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml | 133 +++++++----------- 4 files changed, 209 insertions(+), 90 deletions(-) create mode 100644 src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml create mode 100644 src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs diff --git a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml new file mode 100644 index 000000000..7c639043d --- /dev/null +++ b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs new file mode 100644 index 000000000..2538f7168 --- /dev/null +++ b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using AvaloniaEdit.Document; + +namespace PostIt.Controls; + +public partial class MarkdownEditorControl : UserControl +{ + public static readonly StyledProperty MarkdownTextProperty = + AvaloniaProperty.Register(nameof(MarkdownText)); + + public static readonly StyledProperty DocumentProperty = + AvaloniaProperty.Register(nameof(Document)); + + public static readonly StyledProperty IsPreviewVisibleProperty = + AvaloniaProperty.Register(nameof(IsPreviewVisible), false); + + public string? MarkdownText + { + get => GetValue(MarkdownTextProperty); + set => SetValue(MarkdownTextProperty, value); + } + + public TextDocument? Document + { + get => GetValue(DocumentProperty); + set => SetValue(DocumentProperty, value); + } + + public bool IsPreviewVisible + { + get => GetValue(IsPreviewVisibleProperty); + set => SetValue(IsPreviewVisibleProperty, value); + } + + public MarkdownEditorControl() + { + InitializeComponent(); + PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == MarkdownTextProperty) + { + var text = e.NewValue as string ?? string.Empty; + if (Document is null || !string.Equals(Document.Text, text, StringComparison.Ordinal)) + { + Document = new TextDocument(text); + } + } + else if (e.Property == DocumentProperty) + { + var newDocument = e.NewValue as TextDocument; + var text = newDocument?.Text ?? string.Empty; + if (!string.Equals(MarkdownText, text, StringComparison.Ordinal)) + { + MarkdownText = text; + } + } + } +} diff --git a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs index 6fe6eccc9..9de63c24c 100644 --- a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs @@ -39,18 +39,56 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel /// Editor buffer for the post body. Same pattern as /// . - public string DraftArticle + public string? DraftArticle { - get => DraftArticleDocument.Text; - set { + get => DraftArticleDocument?.Text ?? null; + set + { + var currentText = DraftArticleDocument?.Text; + if (string.Equals(currentText, value, StringComparison.Ordinal)) + { + OnPropertyChanged(nameof(DraftArticle)); + return; + } + OnPropertyChanging(nameof(DraftArticle)); - DraftArticleDocument = new TextDocument(value); - this.OnPropertyChanged(nameof(DraftArticle)); + DraftArticleDocument = value is not null ? new TextDocument(value) : null; + OnPropertyChanged(nameof(DraftArticle)); } } - [ObservableProperty] - public partial TextDocument DraftArticleDocument {get; set;} + private TextDocument? _draftArticleDocument; + + public TextDocument? DraftArticleDocument + { + get => _draftArticleDocument; + set + { + if (ReferenceEquals(_draftArticleDocument, value)) + { + return; + } + + if (_draftArticleDocument is not null) + { + _draftArticleDocument.TextChanged -= DraftArticleDocument_TextChanged; + } + + _draftArticleDocument = value; + + if (_draftArticleDocument is not null) + { + _draftArticleDocument.TextChanged += DraftArticleDocument_TextChanged; + } + + OnPropertyChanged(nameof(DraftArticleDocument)); + } + } + + private void DraftArticleDocument_TextChanged(object? sender, EventArgs e) + { + OnPropertyChanged(nameof(DraftArticle)); + } /// Editor buffer for the post's publication state. /// Reflects the server-side IsPublished flag (the diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml index 90397f09d..f8534a094 100644 --- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml +++ b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml @@ -17,10 +17,7 @@ - - + - + @@ -137,91 +134,61 @@ - + + + + + + + + + + + + + + - - + IsPreviewVisible="{Binding IsMarkdownViewerVisible, Mode=TwoWay}" + MarkdownText="{Binding DraftArticle, Mode=TwoWay}" + Document="{Binding DraftArticleDocument, Mode=TwoWay}" /> - - - - - - - - - - - + VerticalAlignment="Stretch"> + + + + + + + + + + + - - + - - - - - - - - - - - - - - - - - - - - + - From bba5fe093c031e0ca41a2f3b57aca6cc2b5d9b12 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 4 Oct 2026 20:12:34 +0100 Subject: [PATCH 02/46] + scroll --- src/PostIt/PostIt.Tests/MD/le-chant-1.md | 34 ++++++++++++ .../Controls/MarkdownEditorControl.axaml | 40 ++++++++------ src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml | 12 ++++- .../PostIt/Views/Blogs/BlogsPage.axaml.cs | 53 ------------------- src/Yavsc.Org/wwwroot/css/site.css | 5 +- src/Yavsc.Org/wwwroot/css/site.scss | 5 +- 6 files changed, 74 insertions(+), 75 deletions(-) create mode 100644 src/PostIt/PostIt.Tests/MD/le-chant-1.md diff --git a/src/PostIt/PostIt.Tests/MD/le-chant-1.md b/src/PostIt/PostIt.Tests/MD/le-chant-1.md new file mode 100644 index 000000000..f4a7506fe --- /dev/null +++ b/src/PostIt/PostIt.Tests/MD/le-chant-1.md @@ -0,0 +1,34 @@ + + Loin vers l'infini s'étendent + Les grands prés marécageux . + Pas un seul oiseau ne chante + Sur les arbres secs et creux ! + +*(Refrain)* + + Ô terre de détresse, + Où nous devons sans cesse, + Piocher, piocher . + + Dans ce camp morne et sauvage + Entouré de murs de fer, + Il nous semble vivre en cage + Au milieu d'un grand désert . + + Bruit des pas et bruit des armes, + Sentinelles jours et nuits, + Et du sang, et des cris, des larmes, + La mort pour celui qui fuit . + + Mais un jour dans notre vie, + Le printemps refleurira . + Liberté, liberté chérie + Je dirai : « Tu es à moi ! » + +*(Dernier refrain)* + + Ô terre d'allégresse, + Où nous pourrons sans cesse, + Aimer, aimer ! + +[audio : Mario Hacquard - Youtube](https://www.youtube.com/watch?v=ogw_mgk1pFs) diff --git a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml index 7c639043d..9e8df6dd1 100644 --- a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml +++ b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml @@ -7,26 +7,35 @@ x:Name="Root" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + - + + VerticalScrollBarVisibility="Auto" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + + + Padding="8" + MinHeight="260"> - + + - + @@ -191,4 +198,5 @@ + diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs index 629727870..b40021276 100644 --- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs +++ b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs @@ -4,69 +4,16 @@ using Avalonia.Controls; using Avalonia.Platform.Storage; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; -using Avalonia.Controls.Platform; namespace PostIt.Views.Blogs; public partial class BlogsPage : ContentPage { - private IInputPane? _inputPane; - public BlogsPage() { InitializeComponent(); } - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - // Récupère l'accès au TopLevel de la fenêtre/application - var topLevel = TopLevel.GetTopLevel(this); - if (topLevel != null) - { - _inputPane = topLevel!.InputPane; - if (_inputPane != null) - { - // S'abonne aux changements d'état du clavier virtuel - _inputPane.StateChanged += OnInputPaneStateChanged; - - // Applique la configuration initiale si le clavier est déjà présent - UpdateLayoutForKeyboard(_inputPane.OccludedRect.Height); - } - } - } - - protected override void OnUnloaded(RoutedEventArgs e) - { - if (_inputPane != null) - { - _inputPane.StateChanged -= OnInputPaneStateChanged; - } - base.OnUnloaded(e); - } - - - private void UpdateLayoutForKeyboard(double keyboardHeight) - { - double visibleHeight = this.Bounds.Height - keyboardHeight; - if (keyboardHeight > 0) - { - DraftArticleTextEditor.MaxHeight = visibleHeight; - } - else - { - DraftArticleTextEditor.MaxHeight = double.PositiveInfinity; - } - } - - private void OnInputPaneStateChanged(object? sender, InputPaneStateEventArgs e) - { - // e.EndRect contient le rectangle final du clavier virtuel - // e.EndRect.Height vous donne sa hauteur exacte en pixels indépendants du périphérique (pixels logiques) - UpdateLayoutForKeyboard(e.EndRect.Height); - } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); diff --git a/src/Yavsc.Org/wwwroot/css/site.css b/src/Yavsc.Org/wwwroot/css/site.css index bafc2d571..2e4720ff3 100644 --- a/src/Yavsc.Org/wwwroot/css/site.css +++ b/src/Yavsc.Org/wwwroot/css/site.css @@ -1,4 +1 @@ -.user-icon { - max-width: 1.5em; - max-height: 1.5em; -} +.user-icon{max-width:1.5em;max-height:1.5em}.card{padding:.5em} diff --git a/src/Yavsc.Org/wwwroot/css/site.scss b/src/Yavsc.Org/wwwroot/css/site.scss index 1bf7a4d34..546aac220 100644 --- a/src/Yavsc.Org/wwwroot/css/site.scss +++ b/src/Yavsc.Org/wwwroot/css/site.scss @@ -1,4 +1,7 @@ -user-icon { +.user-icon { max-width: 1.5em; max-height: 1.5em; } +.card { + padding: .5em; +} From 1897a6807df4648efbe279d46f3587d409f26c58 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 4 Oct 2026 20:18:43 +0100 Subject: [PATCH 03/46] Fix long-form markdown editor scrolling and focus --- .../Controls/MarkdownEditorControl.axaml.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs index 2538f7168..0431b726e 100644 --- a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs +++ b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs @@ -38,6 +38,8 @@ public partial class MarkdownEditorControl : UserControl { InitializeComponent(); PropertyChanged += OnPropertyChanged; + SizeChanged += OnSizeChanged; + UpdateEditorLimits(); } private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) @@ -60,4 +62,27 @@ public partial class MarkdownEditorControl : UserControl } } } + + private void OnSizeChanged(object? sender, SizeChangedEventArgs e) + { + UpdateEditorLimits(); + } + + private void UpdateEditorLimits() + { + var visibleHeight = Bounds.Height > 0 ? Bounds.Height - 24 : 260; + visibleHeight = Math.Max(visibleHeight, 80); + + if (SourceEditor is not null) + { + SourceEditor.MaxHeight = visibleHeight; + SourceEditor.Height = double.NaN; + } + + if (PreviewViewer is not null) + { + PreviewViewer.MaxHeight = visibleHeight; + PreviewViewer.Height = double.NaN; + } + } } From 8b6188786567066468e5033233b9347acc37b3ae Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 4 Oct 2026 21:58:08 +0100 Subject: [PATCH 04/46] release 1.0.8-rc21 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658ae46ec..df5813afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog + +## [1.0.8-rc21] - unstable + +### Fixed + +User experience editing the blog article. + ## [1.0.8-rc20] - unstable Chat SignalR fonctionnel de bout en bout (connexion, salons multiples, From eeae7379e63a2eeb06063a626048715a9e4c5403 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 4 Oct 2026 21:58:35 +0100 Subject: [PATCH 05/46] release 1.0.8-rc21 --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df5813afe..904d740dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,5 @@ # Changelog - ## [1.0.8-rc21] - unstable ### Fixed From 4ff072ba06e7f004f959b67d4110f75b9b5795a5 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 5 Oct 2026 14:07:15 +0100 Subject: [PATCH 06/46] load the CodeBlock --- src/PostIt/Directory.Packages.props | 2 +- src/PostIt/PostIt/App.axaml.cs | 4 +++- src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs | 10 +++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 580a8e521..a353e4b3a 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -20,7 +20,7 @@ - + diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 5c986d1dd..fca8e0313 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Styling; +using Avalonia.Threading; using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels; @@ -200,7 +201,8 @@ public partial class App : Application, ILiveView, IVMPusherApp public void ConfigureRootView(MainView rootView) { - rootView.AttachedToVisualTree += async (_, _) => await BootOnceAsync(); + rootView.AttachedToVisualTree += (_, _) => + Dispatcher.UIThread.Post(() => _ = BootOnceAsync()); var sessionStatus = ServiceProvider!.GetRequiredService(); sessionStatus.LogoutCompleted += () => diff --git a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs index 9de63c24c..b827bff26 100644 --- a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs @@ -52,7 +52,15 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel } OnPropertyChanging(nameof(DraftArticle)); - DraftArticleDocument = value is not null ? new TextDocument(value) : null; + try + { + DraftArticleDocument = value is not null ? new TextDocument(value) : null; + } + catch (Exception ex) + { + StatusMessage = $"Error setting DraftArticleDocument: {ex.Message}"; + // Handle any exceptions that might occur when setting the DraftArticleDocument + } OnPropertyChanged(nameof(DraftArticle)); } } From 7d10060efd32f693c440ebf59493c8bc3db6e947 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 5 Oct 2026 14:48:45 +0100 Subject: [PATCH 07/46] Release 1.0.8-rc22 --- CHANGELOG.md | 6 ++++++ src/Yavsc.Org/Extensions/HostingExtensions.cs | 1 + 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 904d740dd..8f77b68af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.0.8-rc22] - unstable + +### Fixed + +User experience login into PostIt + ## [1.0.8-rc21] - unstable ### Fixed diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 563a74789..481d604bf 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -1048,6 +1048,7 @@ public static class HostingExtensions app.UseSwaggerUI(); app.UseStaticFiles(); app.UseRouting(); + app.UseAuthentication(); app.UseIdentityServer(); app.UseAuthorization(); app.UseCors("default"); From 0c72c49ec1b9cb042f9487ebb25aaf7a766832c9 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 5 Oct 2026 15:22:25 +0100 Subject: [PATCH 08/46] rollback on test artifacts --- .forgejo/workflows/buildAndTest.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 54274d95e..e85a9b167 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -50,10 +50,6 @@ jobs: echo "🏗️ Building the solution..." cd /src/_src && dotnet build --verbosity normal - name: Test - artifacts: - name: test-results - path: test-results.html - run: | echo "🚀 Lancement des tests..." cd /src/_src && dotnet test \ From a843e77ae998ca362f07fdbaa47322ae979a321a Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Tue, 6 Oct 2026 17:22:48 +0100 Subject: [PATCH 09/46] mise en forme du web --- src/Yavsc.Org/Views/Shared/_Nav.cshtml | 2 +- src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.css | 4 ---- src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.min.css | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Yavsc.Org/Views/Shared/_Nav.cshtml b/src/Yavsc.Org/Views/Shared/_Nav.cshtml index 4af820fc8..6d5e7bb78 100644 --- a/src/Yavsc.Org/Views/Shared/_Nav.cshtml +++ b/src/Yavsc.Org/Views/Shared/_Nav.cshtml @@ -3,7 +3,7 @@ background-position: 0%; background-attachment: fixed;"> public class YavscApiClientTests { + [Fact] + public async Task GetAnonymousAsync_surfaces_http_errors_without_attempting_login_or_retry() + { + using var apiServer = new StubApiServer(forceFirstRequest: true); + await apiServer.StartAsync(); + var tokensPath = TokensPath(); + try + { + await using var client = new YavscApiClient(new Settings(), new TokenStore(tokensPath)); + await Assert.ThrowsAsync(() => + client.GetAnonymousAsync>( + apiServer.BaseUrl + "/posts", TestContext.Current.CancellationToken)); + Assert.Equal(1, apiServer.RequestCount); + Assert.Empty(apiServer.SeenBearers); + } + finally + { + File.Delete(tokensPath); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GetAnonymousAsync_does_not_require_or_send_session_credentials(bool hasSession) + { + using var apiServer = new StubApiServer(); + await apiServer.StartAsync(); + var tokensPath = TokensPath(); + try + { + var store = new TokenStore(tokensPath); + if (hasSession) + store.Save(new RefreshTokenRecord("test-access-token", "test-refresh-token", + DateTimeOffset.UtcNow.AddHours(1), null)); + await using var client = new YavscApiClient(new Settings(), store); + + var posts = await client.GetAnonymousAsync>( + apiServer.BaseUrl + "/posts", TestContext.Current.CancellationToken); + + Assert.Equal(2, posts.Count); + Assert.Equal(1, apiServer.RequestCount); + Assert.Empty(apiServer.SeenBearers); + } + finally + { + File.Delete(tokensPath); + } + } + private static int GetFreePort() { var l = new TcpListener(IPAddress.Loopback, 0); diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index 22885aca8..c88cbaac5 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -36,6 +36,7 @@ public static class ServiceCollectionHelpers var estimateClient = new EstimateApiClient(api, () => settings.ApiUrl); var frontClient = new FrontOfficeApiClient(api, () => settings.ApiUrl); var resourceUsageClient = new ResourceUsageApiClient(api, () => settings.ApiUrl); + var notificationsClient = new NotificationsApiClient(api, () => settings.ApiUrl); var userDirectory = new UserDirectory(userSearchClient); var reverseGeocoding = new NominatimReverseGeocodingService(); @@ -93,6 +94,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(estimateClient); services.AddSingleton(frontClient); services.AddSingleton(resourceUsageClient); + services.AddSingleton(notificationsClient); services.AddSingleton(userFilesClient); services.AddSingleton(reverseGeocoding); services.AddSingleton(userDirectory); diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs index 7dfdb3bae..296e8b3b1 100644 --- a/src/PostIt/PostIt/Services/YavscApiClient.cs +++ b/src/PostIt/PostIt/Services/YavscApiClient.cs @@ -39,6 +39,16 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable private readonly SemaphoreSlim _refreshGate = new(1, 1); private RefreshTokenRecord? _tokens; + private static readonly HttpRequestOptionsKey AnonymousRequest = new("PostIt.AnonymousRequest"); + + public virtual async Task GetAnonymousAsync(string path, CancellationToken ct = default) + { + using var request = new HttpRequestMessage(HttpMethod.Get, path); + request.Options.Set(AnonymousRequest, true); + using var response = await Http.SendAsync(request, ct).ConfigureAwait(false); + await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false); + return await ReadJsonAsync(response, ct).ConfigureAwait(false); + } public YavscApiClient(Settings settings, TokenStore store, OidcClient? oidc = null) { @@ -614,7 +624,8 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { - if (_owner._tokens is not null) + if (_owner._tokens is not null + && !(request.Options.TryGetValue(AnonymousRequest, out var anonymous) && anonymous)) request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", _owner._tokens.AccessToken); return base.SendAsync(request, cancellationToken); diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs index 6a29afbc6..da81d9152 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs @@ -1,11 +1,16 @@ using System; +using System.Collections.ObjectModel; +using System.ComponentModel; using System.Threading.Tasks; using Avalonia; +using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels.Chat; +using Yavsc.Abstract.Models.Messaging; using Yavsc.Api.Client; + namespace PostIt.ViewModels; public class HomePageViewModel : ViewModelBase @@ -13,6 +18,26 @@ public class HomePageViewModel : ViewModelBase public YavscApiClient Api { get; } public Settings Settings { get; } public SessionStatusViewModel SessionStatus { get; } + public ObservableCollection Notifications { get; } = new(); + private readonly NotificationsApiClient? _notificationsClient; + private int _notificationsLoadVersion; + private bool _isLoadingNotifications; + private string? _notificationsError; + private bool _notificationsLoaded; + + public bool IsLoadingNotifications + { + get => _isLoadingNotifications; + private set => SetProperty(ref _isLoadingNotifications, value); + } + + public string? NotificationsError + { + get => _notificationsError; + private set => SetProperty(ref _notificationsError, value); + } + + public bool HasNoNotifications => _notificationsLoaded && Notifications.Count == 0; private string _welcomeText = "Welcome to PostIt!"; public string WelcomeText @@ -24,11 +49,16 @@ public class HomePageViewModel : ViewModelBase public override bool CanNavigateNext { get => true; protected set => throw new System.NotImplementedException(); } public override bool CanNavigatePrevious { get => false; protected set => throw new System.NotImplementedException(); } - public HomePageViewModel(YavscApiClient api, Settings settings, SessionStatusViewModel sessionStatus) + public HomePageViewModel(YavscApiClient api, Settings settings, SessionStatusViewModel sessionStatus, + NotificationsApiClient notificationsClient) { Api = api; Settings = settings; SessionStatus = sessionStatus; + _notificationsClient = notificationsClient; + SessionStatus.PropertyChanged += OnSessionStatusChanged; + RefreshNotificationsCommand = new AsyncRelayCommand(RefreshNotificationsAsync, + () => _notificationsClient is not null); OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync); OpenProviderRequests = new AsyncRelayCommand(OpenProviderRequestsAsync); @@ -55,6 +85,59 @@ public class HomePageViewModel : ViewModelBase public IAsyncRelayCommand OpenClientEstimates { get; } public IAsyncRelayCommand OpenProviderEstimates { get; } public IAsyncRelayCommand OpenMyFiles { get; } + public IAsyncRelayCommand RefreshNotificationsCommand { get; } + + public async Task RefreshNotificationsAsync() + { + if (_notificationsClient is null) + throw new InvalidOperationException("Notifications client is not available."); + + var version = ++_notificationsLoadVersion; + Notifications.Clear(); + NotificationsError = null; + _notificationsLoaded = false; + OnPropertyChanged(nameof(HasNoNotifications)); + IsLoadingNotifications = true; + try + { + var notifications = SessionStatus.IsLoggedIn + ? await _notificationsClient.GetCurrentAsync() + : await _notificationsClient.GetPublicAsync(); + if (version != _notificationsLoadVersion) + return; + if (notifications is null) + throw new InvalidOperationException("The notifications response is empty."); + + foreach (var notification in notifications) + Notifications.Add(notification); + _notificationsLoaded = true; + } + catch (Exception ex) + { + if (version == _notificationsLoadVersion) + NotificationsError = $"Impossible de charger les notifications : {ex.Message}"; + } + finally + { + if (version == _notificationsLoadVersion) + { + IsLoadingNotifications = false; + OnPropertyChanged(nameof(HasNoNotifications)); + } + } + } + + private void OnSessionStatusChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(SessionStatusViewModel.IsLoggedIn) || _notificationsClient is null) + return; + + // Logout can complete on a worker thread; collections and bindings belong to the UI thread. + if (Dispatcher.UIThread.CheckAccess()) + _ = RefreshNotificationsAsync(); + else + Dispatcher.UIThread.Post(() => _ = RefreshNotificationsAsync()); + } private async Task OpenActivitiesAsync() { @@ -228,7 +311,7 @@ public class HomePageViewModel : ViewModelBase /// (thread-safe dispatcher marshalling on PropertyChanged) — a /// designer-only duplicate instance is therefore harmless. /// - public HomePageViewModel() : this(null!, new Settings(), new SessionStatusViewModel()) + public HomePageViewModel() : this(null!, new Settings(), new SessionStatusViewModel(), null!) { } diff --git a/src/PostIt/PostIt/Views/Layout/HomePage.axaml b/src/PostIt/PostIt/Views/Layout/HomePage.axaml index 178bd42d8..8489bb9ee 100644 --- a/src/PostIt/PostIt/Views/Layout/HomePage.axaml +++ b/src/PostIt/PostIt/Views/Layout/HomePage.axaml @@ -2,20 +2,50 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:PostIt.ViewModels" xmlns:lang="using:Yavsc.Lang" + xmlns:messaging="using:Yavsc.Abstract.Models.Messaging" x:Class="PostIt.Views.HomePage" x:DataType="vm:HomePageViewModel" Header="Home"> - + + + + +