diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a3be3a2..16ae2b264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,33 +1,10 @@ # Changelog - -## [1.0.8-rc24] - unstable - -### Added - -* [PostIt] Reconnection automatique aux salons de discussion au reconnect - -### Changed - -* [PostIt] La liste des billets et l'éditeur Markdown se partagent - l'espace disponible et défilent séparément ; un séparateur permet - de redimensionner les deux zones. - -### Fixed - -* [PostIt] Défilement des articles longs en édition et en aperçu : - suppression de la limite de hauteur qui masquait le contenu Markdown - et du défilement imbriqué autour d'AvaloniaEdit. Six tests headless - couvrent les barres automatiques, le redimensionnement et le - défilement indépendant de la liste. - ## [1.0.8-rc23] - unstable ### Added -* [PostIt] Badge « Publié » à côté du titre des billets publiés dans la - liste, mis à jour lors de la publication ou du retour en brouillon. * [PostIt] Gestion native du compte inspirée de `/Manage` : modification du nom d'utilisateur, du nom complet et de l'adresse, téléversement d'avatar, confirmation e-mail, préférence de courriel mensuel, diff --git a/doc/architecture/postit.md b/doc/architecture/postit.md index b1bf83cf7..70f3f2fdc 100644 --- a/doc/architecture/postit.md +++ b/doc/architecture/postit.md @@ -126,20 +126,6 @@ le DI est construit. Ordre, dans cet ordre : > à cette table, *ne pas* ajouter de registration pour > `Settings` ailleurs que la ligne `AddSingleton(settings)`. -## Défilement de la page des billets - -La page `BlogsPage` utilise une grille à hauteur contrainte : l'en-tête -et les commandes restent visibles, tandis que la liste des billets et -l'éditeur se partagent l'espace disponible. Un `GridSplitter` permet -de modifier cette répartition. La liste et l'article défilent séparément. - -Dans `MarkdownEditorControl`, AvaloniaEdit gère son propre défilement ; -l'aperçu Markdown est placé dans un `ScrollViewer`. Ne pas limiter -la hauteur du `MarkdownViewer` lui-même : son contenu doit pouvoir -dépasser le viewport pour que les barres automatiques s'activent. -Ne pas placer cette grille dans un `StackPanel` vertical ou un -`ScrollViewer` de page, qui lui donnerait une hauteur de mesure illimitée. - ## Navigation Le host de navigation est un `NavigationPage x:Name="NavRoot"` diff --git a/src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs b/src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs deleted file mode 100644 index 1a83330f9..000000000 --- a/src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Headless.XUnit; -using Avalonia.LogicalTree; -using Avalonia.Threading; -using Avalonia.VisualTree; -using AvaloniaEdit; -using PostIt.Controls; -using PostIt.Views.Blogs; - -namespace PostIt.Tests; - -public class MarkdownEditorScrollTests -{ - private static readonly string LongArticle = - string.Join("\n\n", Enumerable.Range(1, 100).Select(i => $"Paragraph {i}: article content.")); - - [AvaloniaTheory] - [InlineData(false)] - [InlineData(true)] - public void Long_article_scrolls_to_the_end_and_adapts_to_resizing(bool preview) - { - var editor = new MarkdownEditorControl - { - MarkdownText = LongArticle, - IsPreviewVisible = preview - }; - var window = new Window { Width = 600, Height = 400, Content = editor }; - try - { - window.Show(); - Dispatcher.UIThread.RunJobs(); - window.UpdateLayout(); - - var scroller = FindArticleScroller(editor, preview); - AssertScrollable(scroller); - var initialViewport = scroller.Viewport.Height; - - window.Height = 300; - Dispatcher.UIThread.RunJobs(); - window.UpdateLayout(); - - Assert.True(scroller.Viewport.Height < initialViewport); - AssertScrollable(scroller); - } - finally - { - CloseWindow(window, editor); - } - } - - [AvaloniaTheory] - [InlineData(false)] - [InlineData(true)] - public void Blog_page_bounds_the_article_viewport(bool preview) - { - var page = new BlogsPage(); - var editor = page.GetLogicalDescendants().OfType().Single(); - editor.MarkdownText = LongArticle; - editor.IsPreviewVisible = preview; - var posts = page.FindControl("PostsListBox")!; - posts.ItemsSource = Enumerable.Range(1, 50) - .Select(i => new Yavsc.Blogspot.BlogPostDto { Title = $"Post {i}" }).ToArray(); - var navigation = new NavigationPage(); - navigation.PushAsync(page).GetAwaiter().GetResult(); - var window = new Window { Width = 800, Height = 700, Content = navigation }; - try - { - window.Show(); - Dispatcher.UIThread.RunJobs(); - window.UpdateLayout(); - - var scroller = FindArticleScroller(editor, preview); - Assert.InRange(scroller.Viewport.Height, 1, window.ClientSize.Height); - AssertScrollable(scroller); - AssertScrollable(posts.GetVisualDescendants().OfType().Single()); - var editorBottom = editor.TranslatePoint(new Point(0, editor.Bounds.Height), window); - Assert.NotNull(editorBottom); - Assert.InRange(editorBottom.Value.Y, 1, window.ClientSize.Height); - } - finally - { - CloseWindow(window, editor); - } - } - - [AvaloniaTheory] - [InlineData(false)] - [InlineData(true)] - public void Short_article_needs_no_vertical_scrollbar(bool preview) - { - var editor = new MarkdownEditorControl - { - MarkdownText = "Short article", - IsPreviewVisible = preview - }; - var window = new Window { Width = 600, Height = 400, Content = editor }; - try - { - window.Show(); - Dispatcher.UIThread.RunJobs(); - window.UpdateLayout(); - - var scroller = FindArticleScroller(editor, preview); - Assert.True(scroller.Extent.Height <= scroller.Viewport.Height); - Assert.DoesNotContain(scroller.GetVisualDescendants().OfType(), - bar => bar.Orientation == Avalonia.Layout.Orientation.Vertical && bar.IsVisible); - } - finally - { - CloseWindow(window, editor); - } - } - - private static void CloseWindow(Window window, MarkdownEditorControl editor) - { - // The package's shared renderer caches the visual; detach it between headless applications. - editor.FindControl("PreviewViewer")!.Content = null; - window.UpdateLayout(); - window.Close(); - } - - private static ScrollViewer FindArticleScroller(MarkdownEditorControl editor, bool preview) - { - if (!preview) - return editor.GetVisualDescendants().OfType().Single() - .GetVisualDescendants().OfType().Single(); - - return editor.FindControl("PreviewScrollViewer")!; - } - - private static void AssertScrollable(ScrollViewer scroller) - { - Assert.True(scroller.Extent.Height > scroller.Viewport.Height, - $"Expected overflow, but extent={scroller.Extent.Height}, viewport={scroller.Viewport.Height}."); - Assert.Contains(scroller.GetVisualDescendants().OfType(), - bar => bar.Orientation == Avalonia.Layout.Orientation.Vertical && bar.IsVisible); - scroller.ScrollToEnd(); - Assert.True(scroller.Offset.Y > 0); - Assert.Equal(scroller.Extent.Height - scroller.Viewport.Height, scroller.Offset.Y, 1); - } -} diff --git a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs index bf7023997..8ecc5bb55 100644 --- a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs @@ -1,47 +1,12 @@ -using Avalonia.Controls; -using Avalonia.Headless.XUnit; -using Avalonia.Threading; -using Avalonia.VisualTree; using Yavsc.Blogspot; using Yavsc.Api.Client; using PostIt.Services; using PostIt.ViewModels; -using PostIt.Views.Blogs; namespace PostIt.Tests; public class PostItViewModelTests { - [AvaloniaFact] - public void Published_badge_tracks_post_publication_state() - { - var post = new BlogPostDto { Id = 42, Title = "Test post", IsPublished = true }; - var page = new BlogsPage(); - var row = page.PostsListBox.ItemTemplate!.Build(post)!; - row.DataContext = post; - var window = new Window { Content = row }; - try - { - window.Show(); - Dispatcher.UIThread.RunJobs(); - var badge = Assert.Single(row.GetVisualDescendants().OfType(), - border => border.Name == "PublishedBadge"); - - Assert.True(badge.IsVisible); - - post.IsPublished = false; - Dispatcher.UIThread.RunJobs(); - Assert.False(badge.IsVisible); - - post.IsPublished = true; - Dispatcher.UIThread.RunJobs(); - Assert.True(badge.IsVisible); - } - finally - { - window.Close(); - } - } [Fact] public void SearchCommand_filters_posts_by_title_article_or_author() @@ -96,22 +61,11 @@ public class PostItViewModelTests var viewModel = new BlogsViewModel(blog); viewModel.SelectedPost = new BlogPostDto { Id = 42, IsPublished = false }; - var changes = new List(); - viewModel.SelectedPost.PropertyChanged += (_, args) => changes.Add(args.PropertyName); await viewModel.SetPublishStateAsync(true); Assert.True(api.LastPublishValue); Assert.True(viewModel.DraftIsPublished); - Assert.True(viewModel.SelectedPost.IsPublished); - Assert.Equal(new[] { nameof(BlogPostDto.IsPublished) }, changes); - - await viewModel.SetPublishStateAsync(false); - - Assert.False(api.LastPublishValue); - Assert.False(viewModel.DraftIsPublished); - Assert.False(viewModel.SelectedPost.IsPublished); - Assert.Equal(new[] { nameof(BlogPostDto.IsPublished), nameof(BlogPostDto.IsPublished) }, changes); } /// Test fake that always throws if the API is invoked. diff --git a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml index 721071d2e..9e8df6dd1 100644 --- a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml +++ b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml @@ -7,27 +7,35 @@ x:Name="Root" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + - + + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> + + + Padding="8" + MinHeight="260"> + VerticalAlignment="Stretch"> + MarkdownText="{Binding MarkdownText, ElementName=Root}" + ClipToBounds="False" /> 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; + } + } } diff --git a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs index 882b5d19e..b827bff26 100644 --- a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs @@ -10,7 +10,9 @@ using Microsoft.Extensions.DependencyInjection; using Yavsc.Blogspot; using Yavsc.Api.Client; using Yavsc.Abstract.Files; +using PostIt.Helpers; using AvaloniaEdit.Document; +using PostIt.Models; namespace PostIt.ViewModels; diff --git a/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs index 7d5b516d6..ba452cb4a 100644 --- a/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs @@ -246,7 +246,7 @@ namespace PostIt.ViewModels.Chat }); return Task.CompletedTask; }; - _connection.Reconnected += async _ => + _connection.Reconnected += _ => { Avalonia.Threading.Dispatcher.UIThread.Post(() => { @@ -254,11 +254,7 @@ namespace PostIt.ViewModels.Chat Status = new StatusNotice("Reconnecté au Hub SignalR", StatusSeverity.Info, new RelayCheckCommand("Déconnecter", async () => await DisconnectAsync())); }); - // La reconnexion crée une nouvelle ConnectionId côté - // serveur : l'appartenance aux salons est perdue, il - // faut rejoindre à nouveau le salon courant. - if (!string.IsNullOrWhiteSpace(CurrentRoom)) - await JoinRoomAsync(); + return Task.CompletedTask; }; _connection.Closed += error => { diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml index 7da9a4ce8..5c926bdee 100644 --- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml +++ b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml @@ -17,11 +17,14 @@ - + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Disabled" + VerticalScrollBarVisibility="Auto" + BringIntoViewOnFocusChange="False"> + @@ -105,14 +107,16 @@ - + + + - - + + @@ -122,27 +126,7 @@ - - - - - - + - - - @@ -219,5 +197,6 @@ - + + diff --git a/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs b/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs index 3f9b30d38..bdb2ccdc8 100644 --- a/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs +++ b/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs @@ -1,16 +1,12 @@ #nullable enable annotations using Yavsc.Abstract.Identity.Security; -using System.ComponentModel; using System.Text.Json.Serialization; namespace Yavsc.Blogspot; -public class BlogPostDto : IBlogPost, INotifyPropertyChanged +public class BlogPostDto : IBlogPost { - public event PropertyChangedEventHandler? PropertyChanged; - private bool _isPublished; - public string AuthorId { get; set; } public BlogPostAuthorDto? Author { get; set; } @@ -34,18 +30,7 @@ public class BlogPostDto : IBlogPost, INotifyPropertyChanged /// follow-up request. Toggled via /// PUT /api/BlogApi/{id}/publish. /// - public bool IsPublished - { - get => _isPublished; - set - { - if (_isPublished == value) - return; - - _isPublished = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsPublished))); - } - } + public bool IsPublished { get; set; } public virtual bool AuthorizeCircle(long circleId) {