diff --git a/CHANGELOG.md b/CHANGELOG.md index f14f285b..9bef40e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [1.0.8-rc12] - unstable + +### Added + +* [PostIt] Nouveau helper d'image `ImageHelper` pour charger des bitmaps depuis les ressources et depuis le web. +* [PostIt] Affichage de l'avatar XS dans la liste des performers d'activites, avec fallback visuel (initiale utilisateur). +* [PostIt.Tests] Nouveaux tests autour des URLs avatar et de la source d'autorite. +* [contrib] Ajout d'un `README.md` utilitaire pour les symboles/icones. + +### Changed + +* [PostIt] Les avatars ne sont plus relies en string sur `Image.Source`: ils sont telecharges et lies en `Bitmap`. +* [Yavsc.Api.Client] `ActivityApiClient` accepte une base d'avatar dediee et construit les URLs avatar depuis l'autorite d'identification. +* [PostIt] Le header de `MainPage` n'utilise plus `ScrollViewer`; remplacement par une barre de commandes basee sur `WrapPanel`. +* [PostIt] Alignement de la navigation blogs: renommage `PushMainPageAsync` -> `PushBlogsPageAsync` et ajustement de `HomePageViewModel`. + +### Fixed + +néant + ## [1.0.8-rc11] - unstable ### Added diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 00000000..01156156 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,5 @@ +# Read me + +## Note aux icones + +㝉®🅬⛒⛑🩎🩺🞫🞮🞕🞖🞆🔴🔵🔲🖂🔧🔩🔐🔌💾💼💬💭👿👾🏷🎯🏹🌍🎎💩 diff --git a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs index 3f602fc4..2e8d3e54 100644 --- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs @@ -7,6 +7,20 @@ namespace PostIt.Tests; public class ActivitiesPageViewModelTests { + [Fact] + public void ActivityApiClient_uses_avatar_authority_when_provided() + { + var api = new StubActivityApi(); + var client = new ActivityApiClient( + api, + "https://api.pschneider.fr/api/v1/", + "https://yavsc.pschneider.fr/"); + + var url = client.BuildAvatarXsUrl("paul"); + + Assert.Equal("https://yavsc.pschneider.fr/avatars/paul.xs.png", url); + } + [Fact] public async Task ActivityApiClient_uses_business_absolute_paths() { @@ -40,6 +54,7 @@ public class ActivitiesPageViewModelTests Assert.Equal("brush", vm.CurrentActivity?.Code); Assert.Single(vm.Performers); Assert.Equal("Alice", vm.Performers[0].UserName); + Assert.Equal("https://business.example/avatars/Alice.xs.png", vm.Performers[0].AvatarXsUrl); Assert.True(vm.Performers[0].HasPerformerProfile); Assert.True(vm.Performers[0].IsPerformerActive); Assert.Equal("Actif", vm.Performers[0].PerformerStatusBadgeLabel); @@ -50,6 +65,7 @@ public class ActivitiesPageViewModelTests Assert.Equal("brush-pro", vm.CurrentActivity?.Code); Assert.Single(vm.Performers); Assert.Equal("Bob", vm.Performers[0].UserName); + Assert.Equal("https://business.example/avatars/Bob.xs.png", vm.Performers[0].AvatarXsUrl); Assert.True(vm.Performers[0].HasPerformerProfile); Assert.False(vm.Performers[0].IsPerformerActive); Assert.Equal("Inactif", vm.Performers[0].PerformerStatusBadgeLabel); diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index d3fbf651..59d5e412 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -135,9 +135,6 @@ private void ConfigureRootView(MainView rootView) var homePage = provider.GetRequiredService(); var app = (App)Current!; await app.PushPageAsync(homePage); - if (!refreshed) return; - - await PushMainPageAsync().ConfigureAwait(true); } /// @@ -148,7 +145,7 @@ private void ConfigureRootView(MainView rootView) /// (interactive login from the banner). Pulled out as a helper so /// the two callers can't drift apart. /// - public static async Task PushMainPageAsync() + public static async Task PushBlogsPageAsync() { var app = (App)Current!; var mainVm = app.ServiceProvider!.GetRequiredService(); diff --git a/src/PostIt/PostIt/Helpers/ImageHelper.cs b/src/PostIt/PostIt/Helpers/ImageHelper.cs new file mode 100644 index 00000000..30e7e34e --- /dev/null +++ b/src/PostIt/PostIt/Helpers/ImageHelper.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace PostIt.Helpers; + +public static class ImageHelper +{ + private static readonly HttpClient HttpClient = new(); + + public static Bitmap LoadFromResource(Uri resourceUri) + { + return new Bitmap(AssetLoader.Open(resourceUri)); + } + + public static async Task LoadFromWeb(Uri url) + { + try + { + var response = await HttpClient.GetAsync(url).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var data = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + return new Bitmap(new MemoryStream(data)); + } + catch (HttpRequestException ex) + { + Console.WriteLine($"An error occurred while downloading image '{url}': {ex.Message}"); + return null; + } + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index 1b71e873..f95f31e3 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -24,7 +24,10 @@ public static class ServiceCollectionHelpers var circleClient = new CircleApiClient(api, settings.BlogsApiUrl); var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl); var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl); - var activityClient = new ActivityApiClient(api, settings.ApiUrl); + var activityClient = new ActivityApiClient( + api, + settings.ApiUrl, + settings.Authentication?.Authority); var billingClient = new BillingApiClient(api, settings.ApiUrl); var userDirectory = new UserDirectory(userSearchClient); diff --git a/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs index 2c776d16..2dc8938f 100644 --- a/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs @@ -209,8 +209,26 @@ public partial class ActivitiesPageViewModel : ViewModelBase try { var list = await _client.GetUsersAsync(activity.Code); - Performers = new ObservableCollection((list ?? new()) - .Select(ActivityUserDisplayItem.FromDto)); + var items = (list ?? new()) + .Select(dto => ActivityUserDisplayItem.FromDto(dto, _client.BuildAvatarXsUrl(dto.UserName))) + .ToList(); + + await Task.WhenAll(items.Select(async item => + { + if (string.IsNullOrWhiteSpace(item.AvatarXsUrl)) + { + return; + } + + if (!Uri.TryCreate(item.AvatarXsUrl, UriKind.Absolute, out var avatarUri)) + { + return; + } + + item.AvatarImage = await ImageHelper.LoadFromWeb(avatarUri); + })); + + Performers = new ObservableCollection(items); SelectedPerformer = null; StatusMessage = $"{activity.Name} · {Performers.Count} utilisateur(s)"; } diff --git a/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs b/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs index 3e4426b4..d89a66e9 100644 --- a/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs +++ b/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs @@ -1,10 +1,13 @@ +using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; using Yavsc.Abstract.Workflow; namespace PostIt.ViewModels; -public sealed class ActivityUserDisplayItem +public sealed partial class ActivityUserDisplayItem : ObservableObject { public string PerformerId { get; init; } = string.Empty; + public string AvatarXsUrl { get; init; } = string.Empty; public bool HasPerformerProfile { get; init; } public string PerformerBadgeLabel { get; init; } = "Profil pro"; public bool IsPerformerActive { get; init; } @@ -13,17 +16,25 @@ public sealed class ActivityUserDisplayItem public string PerformerStatusBadgeBorder { get; init; } = "#C62828"; public string PerformerStatusBadgeForeground { get; init; } = "#8E0000"; public string UserName { get; init; } = string.Empty; + public string AvatarFallbackLabel { get; init; } = "?"; public string WebSite { get; init; } = string.Empty; public int ExtraActivityCount { get; init; } public string ExtraActivityLabel { get; init; } = "Pas d'autre activité"; - public static ActivityUserDisplayItem FromDto(PerformerActivity dto) + [ObservableProperty] + public partial Bitmap? AvatarImage { get; set; } + + public static ActivityUserDisplayItem FromDto(PerformerActivity dto, string avatarXsUrl) { return new ActivityUserDisplayItem { PerformerId = dto.PerformerId, + AvatarXsUrl = avatarXsUrl, HasPerformerProfile = dto.HasPerformerProfile, UserName = dto.UserName, + AvatarFallbackLabel = string.IsNullOrWhiteSpace(dto.UserName) + ? "?" + : dto.UserName.Trim()[0].ToString().ToUpperInvariant(), WebSite = dto.WebSite, IsPerformerActive = dto.Active, PerformerStatusBadgeLabel = dto.Active ? "Actif" : "Inactif", diff --git a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs index cfd30650..830be4dd 100644 --- a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs @@ -32,7 +32,7 @@ public class HomePageViewModel : ViewModelBase OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync); } - public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushMainPageAsync); + public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushBlogsPageAsync); public IAsyncRelayCommand OpenActivities { get; } private async Task OpenActivitiesAsync() diff --git a/src/PostIt/PostIt/Views/ActivitiesPage.axaml b/src/PostIt/PostIt/Views/ActivitiesPage.axaml index fa87051a..16d88344 100644 --- a/src/PostIt/PostIt/Views/ActivitiesPage.axaml +++ b/src/PostIt/PostIt/Views/ActivitiesPage.axaml @@ -21,8 +21,8 @@ -