diff --git a/README.md b/README.md index 9747c9af..d1ed912a 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,6 @@ C'est une application mettant en oeuvre une prise de contact entre un demandeur de services et son éventuel prestataire associé. -# Statut actuel des actions Forgejo - -![Build and test](https://forgejo.pschneider.fr/notazof/yavsc/badges/workflows/buildAndTest.yml/badge.svg) - -![Release](https://forgejo.pschneider.fr/notazof/yavsc/badges/workflows/release.yml/badge.svg) - # Statut actuel des actions GitHub * [![Build and Push Yavsc Apk](https://github.com/pazof/yavsc/actions/workflows/docker-publish-android.yml/badge.svg)](https://github.com/pazof/yavsc/actions/workflows/docker-publish-android.yml) diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 62b3a343..900f1428 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -14,7 +14,6 @@ - diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 6f93edf9..e59e0d33 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -59,9 +59,6 @@ public partial class App : Application var client = new BlogApiClient(api, settings.BlogsApiUrl); var circleClient = new CircleApiClient(api, settings.BlogsApiUrl); var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl); - var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl); - var contactService = new ContactService(); - var userDirectory = new UserDirectory(userSearchClient); var services = new ServiceCollection(); @@ -90,9 +87,6 @@ public partial class App : Application services.AddSingleton(client); services.AddSingleton(circleClient); services.AddSingleton(blogAclClient); - services.AddSingleton(userSearchClient); - services.AddSingleton(contactService); - services.AddSingleton(userDirectory); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/PostIt/PostIt/Services/ContactService.Desktop.cs b/src/PostIt/PostIt/Services/ContactService.Desktop.cs deleted file mode 100644 index fa7d37f6..00000000 --- a/src/PostIt/PostIt/Services/ContactService.Desktop.cs +++ /dev/null @@ -1,36 +0,0 @@ -#if !ANDROID && !IOS -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace PostIt.Services; - -/// -/// Desktop stub for . -/// -/// The desktop has no equivalent of the mobile address -/// book (no Contacts.Default, no CardDAV out of the -/// box). Rather than synthesise a list from a different -/// source, this provider returns an empty list and lets the -/// UI render an honest "no local contacts on this platform" -/// message. -/// -/// If desktop users want to invite people who aren't -/// Yavsc members, that flow goes through a separate path -/// (manual email entry + invitation endpoint) — not through -/// . Finding existing Yavsc -/// members is 's job, not this -/// one's. -/// -/// Future CardDAV / Google Contacts / Exchange -/// providers can plug in here as additional -/// implementations selected -/// from DI by configuration. -/// -public sealed class ContactService : IContactService -{ - public Task> GetDeviceContactsAsync(CancellationToken ct = default) - => Task.FromResult>(Array.Empty()); -} -#endif diff --git a/src/PostIt/PostIt/Services/ContactService.Mobile.cs b/src/PostIt/PostIt/Services/ContactService.Mobile.cs deleted file mode 100644 index 8dbd134d..00000000 --- a/src/PostIt/PostIt/Services/ContactService.Mobile.cs +++ /dev/null @@ -1,82 +0,0 @@ -#if ANDROID || IOS -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Maui.ApplicationModel.Communication; -using Microsoft.Maui.ApplicationModel; -using Microsoft.Maui.Devices; - -namespace PostIt.Services; - -/// -/// Mobile implementation backed by MAUI Essentials -/// Contacts.Default. -/// -/// Compiled only for ANDROID and IOS. On desktop targets, -/// see ContactService.Desktop.cs (the stub that wins at -/// compile time). -/// -/// Note: at runtime, this class throws -/// NotImplementedInReferenceAssemblyException unless -/// the host application project also references the -/// platform-specific Microsoft.Maui.Essentials implementation -/// (typically PostIt.Android). On iOS the same is -/// required via PostIt.iOS. On desktop the stub is used -/// and this file is excluded. -/// -public sealed class ContactService : IContactService -{ - public async Task> GetDeviceContactsAsync(CancellationToken ct = default) - { - if (DeviceInfo.Current.Platform == DevicePlatform.Unknown) - return Array.Empty(); - - try - { - var status = await Permissions.RequestAsync(); - if (status != PermissionStatus.Granted) - return Array.Empty(); - - var contacts = await Contacts.Default.GetAllAsync(); - if (contacts is null) return Array.Empty(); - - // Carry the per-contact email list as-is. A real - // device contact can carry several addresses (home / - // work / other); the UI use case ("invite / add to a - // circle") can then decide which address to use, or - // let the user pick. The platform-neutral ContactDto - // shape is intentionally richer than the Yavsc - // directory's single-Email shape — the two flows - // answer different questions. - var result = new List(contacts.Count); - foreach (var c in contacts) - { - var emails = ExtractEmails(c.Emails); - result.Add(new ContactDto( - c.Id, - c.DisplayName ?? string.Empty, - emails)); - } - return result; - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"ContactService: {ex.Message}"); - return Array.Empty(); - } - } - - private static IReadOnlyList ExtractEmails(IEnumerable? emails) - { - if (emails is null) return Array.Empty(); - var list = new List(); - foreach (var e in emails) - { - if (!string.IsNullOrEmpty(e.EmailAddress)) - list.Add(e.EmailAddress); - } - return list; - } -} -#endif diff --git a/src/PostIt/PostIt/Services/IContactService.cs b/src/PostIt/PostIt/Services/IContactService.cs deleted file mode 100644 index 49ca3064..00000000 --- a/src/PostIt/PostIt/Services/IContactService.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace PostIt.Services; - -/// -/// Abstraction over the device-local address book. Used by -/// the "invite someone" flow to enumerate people the user -/// already has in their phone — including people who have -/// never heard of Yavsc. -/// -/// Distinct from , which -/// reads the central Yavsc user table. A device contact may -/// not have a Yavsc account; a directory entry always does. -/// The two are exposed as separate interfaces so a UI that -/// needs both can take both by constructor injection and -/// present them under separate sections (e.g. "Contacts from -/// your phone" vs "Yavsc members"). -/// -/// Implementations live next to this file in -/// platform-conditional source files: -/// ContactService.Mobile.cs (ANDROID/IOS) and -/// ContactService.Desktop.cs (everything else). On -/// desktop the implementation is a stub that returns an -/// empty list: the desktop has no equivalent of the mobile -/// address book, and inviting from a desktop is a separate -/// flow. -/// -public interface IContactService -{ - /// - /// Read the device address book. Returns the contacts - /// known to the local provider; on desktop (no local - /// provider) this is always an empty list. - /// - Task> GetDeviceContactsAsync(CancellationToken ct = default); -} - -/// -/// Platform-neutral contact DTO. Source-of-truth shape for -/// the UI layer; concrete providers (MAUI Essentials on -/// mobile) map to this type. -/// -/// Emails is a list on purpose: a real device -/// contact may carry several addresses (home / work / other). -/// The UI use case ("invite / add to a circle") can then -/// decide which address to use, or let the user pick. This -/// is intentionally richer than the Yavsc directory's -/// single-Email shape — the two flows answer different -/// questions and shouldn't be flattened onto the same -/// wire. -/// -public sealed record ContactDto( - string Id, - string DisplayName, - IReadOnlyList Emails); diff --git a/src/PostIt/PostIt/Services/IUserDirectory.cs b/src/PostIt/PostIt/Services/IUserDirectory.cs deleted file mode 100644 index 7d4c1eb4..00000000 --- a/src/PostIt/PostIt/Services/IUserDirectory.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace PostIt.Services; - -/// -/// Abstraction over the central Yavsc user directory. Used by -/// the "add to a circle" flow to find Yavsc users by display -/// name or email. -/// -/// Distinct from , which -/// reads the device-local address book. A Yavsc user -/// directory entry is always a registered account; a device -/// contact may be anyone in the user's phone — including -/// people who have never heard of Yavsc. -/// -/// Implementations live next to this file in -/// platform-conditional source files: -/// UserDirectory.Desktop.cs and -/// UserDirectory.Mobile.cs. Both currently delegate to -/// UserSearchClient (the central /api/user-search -/// endpoint); the split exists so future platform-specific -/// sources (offline cache, directory-scoped providers) can be -/// plugged in without disturbing the consumer. -/// -public interface IUserDirectory -{ - /// - /// Search the directory by display name (substring) and/or - /// email (exact). - /// - /// Substring filter on the user's - /// display name. Empty or whitespace short-circuits to an - /// empty list (matches the client UX of "type to search", - /// not "show me a directory"). - /// Cancellation token. - /// A flat list of matching directory entries. - /// Never null; may be empty. - Task> SearchAsync(string query, CancellationToken ct = default); -} - -/// -/// Platform-neutral summary of a Yavsc directory entry. Mirrors -/// the wire shape of /api/user-search (see -/// UserSearchResultDto) but expressed in terms that -/// don't leak transport concerns. -/// -/// Kept as a record on purpose: directory entries are -/// immutable snapshots from the server, so structural equality -/// makes "did the user already pick this one?" trivial. -/// -public sealed record UserSummary( - string Id, - string UserName, - string? FullName, - string? Avatar, - string? Email) -{ - /// - /// Convenience for "what to show in a picker". Falls back - /// to when - /// is null or empty. - /// - public string DisplayName => - string.IsNullOrWhiteSpace(FullName) ? UserName : FullName; -} diff --git a/src/PostIt/PostIt/Services/UserDirectory.Desktop.cs b/src/PostIt/PostIt/Services/UserDirectory.Desktop.cs deleted file mode 100644 index c821bb87..00000000 --- a/src/PostIt/PostIt/Services/UserDirectory.Desktop.cs +++ /dev/null @@ -1,52 +0,0 @@ -#if !ANDROID && !IOS -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Yavsc.Api.Client; - -namespace PostIt.Services; - -/// -/// Desktop implementation of . -/// Delegates to the central /api/user-search endpoint -/// via . -/// -/// The desktop has no device-local address book, so the -/// "add to a circle" flow on desktop is Yavsc-users-only. -/// Inviting someone who doesn't have a Yavsc account from -/// desktop is a separate feature (manual email entry + -/// invitation endpoint) and lives outside this interface. -/// -public sealed class UserDirectory : IUserDirectory -{ - private readonly UserSearchClient _client; - - public UserDirectory(UserSearchClient client) - { - _client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public async Task> SearchAsync( - string query, CancellationToken ct = default) - { - // UserSearchClient already short-circuits on empty - // queries, but do it here too so the contract is - // obvious to anyone reading IUserDirectory alone - // without having to chase the client wrapper. - if (string.IsNullOrWhiteSpace(query)) - return Array.Empty(); - - var results = await _client.SearchAsync(query: query, ct: ct).ConfigureAwait(false); - if (results is null) return Array.Empty(); - - return results.Select(u => new UserSummary( - Id: u.Id, - UserName: u.UserName, - FullName: u.FullName, - Avatar: u.Avatar, - Email: u.Email)).ToList(); - } -} -#endif diff --git a/src/PostIt/PostIt/Services/UserDirectory.Mobile.cs b/src/PostIt/PostIt/Services/UserDirectory.Mobile.cs deleted file mode 100644 index 5cba6e4a..00000000 --- a/src/PostIt/PostIt/Services/UserDirectory.Mobile.cs +++ /dev/null @@ -1,49 +0,0 @@ -#if ANDROID || IOS -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Yavsc.Api.Client; - -namespace PostIt.Services; - -/// -/// Mobile implementation of . -/// Same backing as the desktop provider (the central -/// /api/user-search endpoint via -/// ) — mobile devices have the -/// network too, and "add to a circle" needs the same directory -/// regardless of platform. -/// -/// The split exists so a future mobile-only provider -/// (offline cache, device-local mirror of the user's own -/// circles) can be plugged in without touching consumers. -/// -public sealed class UserDirectory : IUserDirectory -{ - private readonly UserSearchClient _client; - - public UserDirectory(UserSearchClient client) - { - _client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public async Task> SearchAsync( - string query, CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(query)) - return Array.Empty(); - - var results = await _client.SearchAsync(query: query, ct: ct).ConfigureAwait(false); - if (results is null) return Array.Empty(); - - return results.Select(u => new UserSummary( - Id: u.Id, - UserName: u.UserName, - FullName: u.FullName, - Avatar: u.Avatar, - Email: u.Email)).ToList(); - } -} -#endif diff --git a/src/Yavsc.Api.Client/Dtos/UserSearchResultDto.cs b/src/Yavsc.Api.Client/Dtos/UserSearchResultDto.cs deleted file mode 100644 index d77f50e7..00000000 --- a/src/Yavsc.Api.Client/Dtos/UserSearchResultDto.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Yavsc.Api.Client.Dtos; - -/// -/// Wire format for GET /api/user-search. -/// -/// Mirrors the server-side -/// Yavsc.Blogs.Controllers.UserSearchResultDto but stops -/// short of any entity navigation properties. Only the fields -/// a client address book needs (id, name, avatar, email) are -/// included. -/// -/// Field names match the JSON the server emits (camelCase -/// via the default policy), so -/// no [JsonPropertyName] attributes are required. -/// -public sealed class UserSearchResultDto -{ - public string Id { get; set; } = string.Empty; - public string UserName { get; set; } = string.Empty; - public string? FullName { get; set; } - public string? Avatar { get; set; } - public string? Email { get; set; } -} \ No newline at end of file diff --git a/src/Yavsc.Api.Client/UserSearchClient.cs b/src/Yavsc.Api.Client/UserSearchClient.cs deleted file mode 100644 index d1aef6be..00000000 --- a/src/Yavsc.Api.Client/UserSearchClient.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Yavsc.Api.Client.Dtos; - -namespace Yavsc.Api.Client; - -/// -/// HTTP client for /api/user-search on the Yavsc Blogs -/// server. Used by client-side address books (PostIt.Desktop, -/// future PostIt.Browser CLI, …) to look up Yavsc users by -/// display name or email. -/// -/// The server scopes every endpoint to the authenticated -/// caller; any authenticated user can search the user table of -/// the instance. There is no per-user filtering on the response -/// side — this is by design on single-tenant deployments -/// (closed community). Multi-tenant deployments should gate -/// this controller behind a tenant-scoped policy before -/// exposing it; see the server-side -/// UserSearchApiController doc for details. -/// -public sealed class UserSearchClient -{ - private const string Path = "user-search"; - - private readonly IYavscApiClient _api; - - public UserSearchClient(IYavscApiClient api, string blogsBaseAddress) - { - _api = api ?? throw new ArgumentNullException(nameof(api)); - if (string.IsNullOrEmpty(blogsBaseAddress)) - throw new ArgumentException("Base address is required.", nameof(blogsBaseAddress)); - - if (api.Http.BaseAddress is null) - api.Http.BaseAddress = new Uri(blogsBaseAddress); - } - - /// - /// Search users by display name (substring) or email (exact). - /// - /// Substring filter on FullName or - /// UserName. Empty or null returns an empty list (the server - /// would return all users, which we don't want by - /// default). - /// Optional exact-match filter on - /// Email. - /// Maximum results, capped at 100. - /// Default 25. - public Task> SearchAsync( - string? query = null, - string? email = null, - int take = 25, - CancellationToken ct = default) - { - // Match the server's contract: at least one filter is - // expected. The server doesn't enforce this (an empty - // query + empty email returns the first `take` users - // alphabetically), but the address-book UX is "type - // something to search", so we short-circuit empty - // queries client-side. - if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(email)) - return Task.FromResult(new List()); - - var qs = new List(); - if (!string.IsNullOrWhiteSpace(query)) - qs.Add($"q={Uri.EscapeDataString(query)}"); - if (!string.IsNullOrWhiteSpace(email)) - qs.Add($"e={Uri.EscapeDataString(email)}"); - qs.Add($"take={Math.Clamp(take, 1, 100)}"); - - return _api.CallAsync>( - HttpMethod.Get, - $"{Path}?{string.Join('&', qs)}", - ct: ct); - } -} \ No newline at end of file