yavsc/src/PostIt/PostIt.Android/Services/ContactService.Mobile.cs

84 lines
3 KiB
C#
Raw Normal View History

2026-08-17 23:13:35 +01:00
#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;
2026-08-19 14:09:31 +01:00
using PostIt.Services;
using System.Linq;
2026-08-17 23:13:35 +01:00
2026-08-19 14:09:31 +01:00
namespace PostIt.Android.Services;
2026-08-17 23:13:35 +01:00
/// <summary>
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
/// Mobile implementation backed by MAUI Essentials
/// <c>Contacts.Default</c>.
2026-08-17 23:13:35 +01:00
///
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
/// <para>Compiled only for ANDROID and IOS. On desktop targets,
/// see <c>ContactService.Desktop.cs</c> (the stub that wins at
/// compile time).</para>
2026-08-17 23:13:35 +01:00
///
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
/// <para>Note: at runtime, this class throws
/// <c>NotImplementedInReferenceAssemblyException</c> unless
/// the host application project also references the
/// platform-specific Microsoft.Maui.Essentials implementation
/// (typically <c>PostIt.Android</c>). On iOS the same is
/// required via <c>PostIt.iOS</c>. On desktop the stub is used
/// and this file is excluded.</para>
2026-08-17 23:13:35 +01:00
/// </summary>
public sealed class ContactService : IContactService
{
public async Task<IReadOnlyList<ContactDto>> GetDeviceContactsAsync(CancellationToken ct = default)
{
if (DeviceInfo.Current.Platform == DevicePlatform.Unknown)
return Array.Empty<ContactDto>();
try
{
var status = await Permissions.RequestAsync<Permissions.ContactsRead>();
if (status != PermissionStatus.Granted)
return Array.Empty<ContactDto>();
var contacts = await Contacts.Default.GetAllAsync();
if (contacts is null) return Array.Empty<ContactDto>();
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
// 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.
2026-08-19 14:09:31 +01:00
var result = new List<ContactDto>(contacts.Count());
2026-08-17 23:13:35 +01:00
foreach (var c in contacts)
{
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
var emails = ExtractEmails(c.Emails);
result.Add(new ContactDto(
c.Id,
c.DisplayName ?? string.Empty,
emails));
2026-08-17 23:13:35 +01:00
}
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
return result;
2026-08-17 23:13:35 +01:00
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ContactService: {ex.Message}");
return Array.Empty<ContactDto>();
}
}
feat(postit): wire Desktop address book to /api/user-search Replaces the empty ContactService.Desktop stub with a real implementation backed by UserSearchClient. Closes the loop between the server-side /api/user-search endpoint (b3056f1c), the client wrapper (6e7e0414), and the platform abstraction. IContactService gains: - SearchAsync(string query, CancellationToken): on desktop, hits /api/user-search and appends results to an in-memory cache. On mobile, throws PlatformNotSupportedException — mobile providers use the device-local address book (GetDeviceContactsAsync) and don't talk to a network search. - Contacts (ObservableCollection<ContactDto>): live view of the cache; UI binds directly to it. Mobile populates it inside GetDeviceContactsAsync (eager load); desktop populates it via SearchAsync (lazy, on-demand). ContactDto shape changes: - Emails (IReadOnlyList<string>) -> Email (string?). The /api/user-search endpoint returns one email per user. The use case ('invite / add to a circle') only needs one. - Mobile provider flattens its per-contact email list down to the first non-empty entry (a small functional loss that matches the wire shape). App.axaml.cs constructs a ContactService from the UserSearchClient singleton and registers it as IContactService so future ViewModels can take the interface by constructor injection. Build + 51/51 tests green. The mobile provider is still gated by #if ANDROID || IOS and not exercised by the Desktop test target — runtime behaviour on Android will need a smoke test on device when PostIt.Android lands.
2026-08-18 00:36:36 +01:00
2026-08-19 14:09:31 +01:00
private static IReadOnlyList<string> ExtractEmails(IEnumerable<ContactEmail>? emails)
feat(postit): wire Desktop address book to /api/user-search Replaces the empty ContactService.Desktop stub with a real implementation backed by UserSearchClient. Closes the loop between the server-side /api/user-search endpoint (b3056f1c), the client wrapper (6e7e0414), and the platform abstraction. IContactService gains: - SearchAsync(string query, CancellationToken): on desktop, hits /api/user-search and appends results to an in-memory cache. On mobile, throws PlatformNotSupportedException — mobile providers use the device-local address book (GetDeviceContactsAsync) and don't talk to a network search. - Contacts (ObservableCollection<ContactDto>): live view of the cache; UI binds directly to it. Mobile populates it inside GetDeviceContactsAsync (eager load); desktop populates it via SearchAsync (lazy, on-demand). ContactDto shape changes: - Emails (IReadOnlyList<string>) -> Email (string?). The /api/user-search endpoint returns one email per user. The use case ('invite / add to a circle') only needs one. - Mobile provider flattens its per-contact email list down to the first non-empty entry (a small functional loss that matches the wire shape). App.axaml.cs constructs a ContactService from the UserSearchClient singleton and registers it as IContactService so future ViewModels can take the interface by constructor injection. Build + 51/51 tests green. The mobile provider is still gated by #if ANDROID || IOS and not exercised by the Desktop test target — runtime behaviour on Android will need a smoke test on device when PostIt.Android lands.
2026-08-18 00:36:36 +01:00
{
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
if (emails is null) return Array.Empty<string>();
var list = new List<string>();
feat(postit): wire Desktop address book to /api/user-search Replaces the empty ContactService.Desktop stub with a real implementation backed by UserSearchClient. Closes the loop between the server-side /api/user-search endpoint (b3056f1c), the client wrapper (6e7e0414), and the platform abstraction. IContactService gains: - SearchAsync(string query, CancellationToken): on desktop, hits /api/user-search and appends results to an in-memory cache. On mobile, throws PlatformNotSupportedException — mobile providers use the device-local address book (GetDeviceContactsAsync) and don't talk to a network search. - Contacts (ObservableCollection<ContactDto>): live view of the cache; UI binds directly to it. Mobile populates it inside GetDeviceContactsAsync (eager load); desktop populates it via SearchAsync (lazy, on-demand). ContactDto shape changes: - Emails (IReadOnlyList<string>) -> Email (string?). The /api/user-search endpoint returns one email per user. The use case ('invite / add to a circle') only needs one. - Mobile provider flattens its per-contact email list down to the first non-empty entry (a small functional loss that matches the wire shape). App.axaml.cs constructs a ContactService from the UserSearchClient singleton and registers it as IContactService so future ViewModels can take the interface by constructor injection. Build + 51/51 tests green. The mobile provider is still gated by #if ANDROID || IOS and not exercised by the Desktop test target — runtime behaviour on Android will need a smoke test on device when PostIt.Android lands.
2026-08-18 00:36:36 +01:00
foreach (var e in emails)
{
if (!string.IsNullOrEmpty(e.EmailAddress))
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
list.Add(e.EmailAddress);
feat(postit): wire Desktop address book to /api/user-search Replaces the empty ContactService.Desktop stub with a real implementation backed by UserSearchClient. Closes the loop between the server-side /api/user-search endpoint (b3056f1c), the client wrapper (6e7e0414), and the platform abstraction. IContactService gains: - SearchAsync(string query, CancellationToken): on desktop, hits /api/user-search and appends results to an in-memory cache. On mobile, throws PlatformNotSupportedException — mobile providers use the device-local address book (GetDeviceContactsAsync) and don't talk to a network search. - Contacts (ObservableCollection<ContactDto>): live view of the cache; UI binds directly to it. Mobile populates it inside GetDeviceContactsAsync (eager load); desktop populates it via SearchAsync (lazy, on-demand). ContactDto shape changes: - Emails (IReadOnlyList<string>) -> Email (string?). The /api/user-search endpoint returns one email per user. The use case ('invite / add to a circle') only needs one. - Mobile provider flattens its per-contact email list down to the first non-empty entry (a small functional loss that matches the wire shape). App.axaml.cs constructs a ContactService from the UserSearchClient singleton and registers it as IContactService so future ViewModels can take the interface by constructor injection. Build + 51/51 tests green. The mobile provider is still gated by #if ANDROID || IOS and not exercised by the Desktop test target — runtime behaviour on Android will need a smoke test on device when PostIt.Android lands.
2026-08-18 00:36:36 +01:00
}
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
return list;
feat(postit): wire Desktop address book to /api/user-search Replaces the empty ContactService.Desktop stub with a real implementation backed by UserSearchClient. Closes the loop between the server-side /api/user-search endpoint (b3056f1c), the client wrapper (6e7e0414), and the platform abstraction. IContactService gains: - SearchAsync(string query, CancellationToken): on desktop, hits /api/user-search and appends results to an in-memory cache. On mobile, throws PlatformNotSupportedException — mobile providers use the device-local address book (GetDeviceContactsAsync) and don't talk to a network search. - Contacts (ObservableCollection<ContactDto>): live view of the cache; UI binds directly to it. Mobile populates it inside GetDeviceContactsAsync (eager load); desktop populates it via SearchAsync (lazy, on-demand). ContactDto shape changes: - Emails (IReadOnlyList<string>) -> Email (string?). The /api/user-search endpoint returns one email per user. The use case ('invite / add to a circle') only needs one. - Mobile provider flattens its per-contact email list down to the first non-empty entry (a small functional loss that matches the wire shape). App.axaml.cs constructs a ContactService from the UserSearchClient singleton and registers it as IContactService so future ViewModels can take the interface by constructor injection. Build + 51/51 tests green. The mobile provider is still gated by #if ANDROID || IOS and not exercised by the Desktop test target — runtime behaviour on Android will need a smoke test on device when PostIt.Android lands.
2026-08-18 00:36:36 +01:00
}
2026-08-17 23:13:35 +01:00
}
refactor(postit): split IContactService from IUserDirectory IContactService used to be the catch-all for "people you can reach from PostIt": on mobile it read the device-local address book, on desktop it queried the central /api/user-search endpoint and merged both worlds into a single ContactDto (a flat Email field, an ObservableCollection cache, a SearchAsync method). Two unrelated flows under the same name, with a wire shape (Email) silently flattening the mobile provider's multi-email list. Split into two interfaces, each with a single responsibility: - IContactService: device-local address book only. Mobile provider reads MAUI Essentials Contacts.Default and carries the full email list per contact. Desktop provider is an honest stub returning an empty list — the desktop has no local address book, and inviting external people from desktop is a separate flow (manual email entry + invitation endpoint) that doesn't belong here. - IUserDirectory: central Yavsc user directory, the only consumer of /api/user-search. Both Desktop and Mobile providers delegate to UserSearchClient; the platform split exists so future platform-specific sources (offline cache, directory-scoped providers) can plug in without disturbing consumers. ContactDto restores IReadOnlyList<string> Emails (the flat Email from d0e0f4c1 was a regression that matched the wire shape of /api/user-search at the cost of the mobile provider's per-contact list). UserSummary is a separate platform-neutral record that mirrors the server's UserSearchResultDto without leaking transport concerns. App.axaml.cs registers both interfaces as singletons. Build + 51/51 PostIt.Tests green. No UI consumer yet — these interfaces are still plomberie; the ViewModel that joins them for the "add to a circle" / "invite someone" flows is a follow-up.
2026-08-18 13:22:54 +01:00
#endif