Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ebdf51f468 |
|||
|
cd7351a783 |
|||
|
95e8f251d2 |
|||
|
e7721bc3ab |
10 changed files with 308 additions and 74 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -1,10 +1,33 @@
|
|||
# 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,
|
||||
|
|
|
|||
|
|
@ -126,6 +126,20 @@ 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"`
|
||||
|
|
|
|||
143
src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs
Normal file
143
src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
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<MarkdownEditorControl>().Single();
|
||||
editor.MarkdownText = LongArticle;
|
||||
editor.IsPreviewVisible = preview;
|
||||
var posts = page.FindControl<ListBox>("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<ScrollViewer>().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<ScrollBar>(),
|
||||
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<MarkdownViewer.Core.Controls.MarkdownViewer>("PreviewViewer")!.Content = null;
|
||||
window.UpdateLayout();
|
||||
window.Close();
|
||||
}
|
||||
|
||||
private static ScrollViewer FindArticleScroller(MarkdownEditorControl editor, bool preview)
|
||||
{
|
||||
if (!preview)
|
||||
return editor.GetVisualDescendants().OfType<TextEditor>().Single()
|
||||
.GetVisualDescendants().OfType<ScrollViewer>().Single();
|
||||
|
||||
return editor.FindControl<ScrollViewer>("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<ScrollBar>(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,47 @@
|
|||
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 => 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()
|
||||
|
|
@ -61,11 +96,22 @@ public class PostItViewModelTests
|
|||
var viewModel = new BlogsViewModel(blog);
|
||||
|
||||
viewModel.SelectedPost = new BlogPostDto { Id = 42, IsPublished = false };
|
||||
var changes = new List<string?>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Test fake that always throws if the API is invoked.</summary>
|
||||
|
|
|
|||
|
|
@ -7,20 +7,13 @@
|
|||
x:Name="Root"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="260">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Border
|
||||
BorderBrush="LightGray"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding !IsPreviewVisible, ElementName=Root}"
|
||||
Padding="0"
|
||||
MinHeight="260">
|
||||
<ScrollViewer
|
||||
BringIntoViewOnFocusChange="False"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
Padding="0">
|
||||
<AvaloniaEdit:TextEditor
|
||||
x:Name="SourceEditor"
|
||||
Document="{Binding Document, ElementName=Root, Mode=TwoWay}"
|
||||
|
|
@ -35,7 +28,6 @@
|
|||
VerticalScrollBarVisibility="Auto"
|
||||
TextInputOptions.Multiline="True"
|
||||
TextInputOptions.ReturnKeyType="Default" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
|
|
@ -43,18 +35,21 @@
|
|||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding IsPreviewVisible, ElementName=Root}"
|
||||
Padding="8"
|
||||
MinHeight="260">
|
||||
Padding="8">
|
||||
<ScrollViewer
|
||||
x:Name="PreviewScrollViewer"
|
||||
BringIntoViewOnFocusChange="False"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto">
|
||||
<md:MarkdownViewer
|
||||
x:Name="PreviewViewer"
|
||||
MarkdownText="{Binding MarkdownText, ElementName=Root}"
|
||||
ClipToBounds="False" />
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
ClipToBounds="False"
|
||||
UseLayoutRounding="True"
|
||||
MarkdownText="{Binding MarkdownText, ElementName=Root}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ public partial class MarkdownEditorControl : UserControl
|
|||
{
|
||||
InitializeComponent();
|
||||
PropertyChanged += OnPropertyChanged;
|
||||
SizeChanged += OnSizeChanged;
|
||||
UpdateEditorLimits();
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
|
|
@ -62,27 +60,4 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ namespace PostIt.ViewModels.Chat
|
|||
});
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
_connection.Reconnected += _ =>
|
||||
_connection.Reconnected += async _ =>
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
|
|
@ -254,7 +254,11 @@ namespace PostIt.ViewModels.Chat
|
|||
Status = new StatusNotice("Reconnecté au Hub SignalR", StatusSeverity.Info,
|
||||
new RelayCheckCommand("Déconnecter", async () => await DisconnectAsync()));
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
// 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();
|
||||
};
|
||||
_connection.Closed += error =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,14 +17,11 @@
|
|||
<Design.DataContext>
|
||||
<vm:BlogsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,*"
|
||||
Margin="5,5,10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
BringIntoViewOnFocusChange="False">
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
VerticalAlignment="Stretch">
|
||||
|
||||
<WrapPanel x:Name="BlogsHeaderPanel"
|
||||
Margin="5"
|
||||
|
|
@ -39,6 +36,7 @@
|
|||
Text="{Binding SearchText, Mode=TwoWay}" />
|
||||
</WrapPanel>
|
||||
<CommandBar x:Name="BlogsCommandBar"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right" IsDynamicOverflowEnabled="True">
|
||||
<CommandBar.PrimaryCommands>
|
||||
<CommandBarButton Command="{Binding RefreshAsync}" Label="Refresh">
|
||||
|
|
@ -107,16 +105,14 @@
|
|||
</CommandBar.PrimaryCommands>
|
||||
</CommandBar>
|
||||
|
||||
<Grid Margin="12" RowSpacing="12" x:Name="MainGrid" MinHeight="500">
|
||||
<Grid Grid.Row="2" Margin="12" RowSpacing="12" x:Name="MainGrid">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="2*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Grid.Row="0"
|
||||
Padding="8"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
|
|
@ -126,7 +122,27 @@
|
|||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:BlogPostDto">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Title}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding Title}" />
|
||||
<Border
|
||||
x:Name="PublishedBadge"
|
||||
VerticalAlignment="Center"
|
||||
Padding="6,2"
|
||||
Background="#F2F7F2"
|
||||
BorderBrush="#2F6D2F"
|
||||
BorderThickness="1"
|
||||
CornerRadius="6"
|
||||
IsVisible="{Binding IsPublished}">
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#2F6D2F"
|
||||
Text="Publié" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
Foreground="Gray"
|
||||
|
|
@ -141,6 +157,11 @@
|
|||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<GridSplitter
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ResizeDirection="Rows" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
|
|
@ -151,6 +172,7 @@
|
|||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
|
|
@ -197,6 +219,5 @@
|
|||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
#nullable enable annotations
|
||||
|
||||
using Yavsc.Abstract.Identity.Security;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Yavsc.Blogspot;
|
||||
|
||||
public class BlogPostDto : IBlogPost
|
||||
public class BlogPostDto : IBlogPost, INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private bool _isPublished;
|
||||
|
||||
public string AuthorId { get; set; }
|
||||
|
||||
public BlogPostAuthorDto? Author { get; set; }
|
||||
|
|
@ -30,7 +34,18 @@ public class BlogPostDto : IBlogPost
|
|||
/// follow-up request. Toggled via
|
||||
/// <c>PUT /api/BlogApi/{id}/publish</c>.
|
||||
/// </summary>
|
||||
public bool IsPublished { get; set; }
|
||||
public bool IsPublished
|
||||
{
|
||||
get => _isPublished;
|
||||
set
|
||||
{
|
||||
if (_isPublished == value)
|
||||
return;
|
||||
|
||||
_isPublished = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsPublished)));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool AuthorizeCircle(long circleId)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue