Replaces the empty ContactService.Desktop stub with a real
implementation backed by UserSearchClient. Closes the loop
between the server-side /api/user-search endpoint (ac9eb2f0),
the client wrapper (633cf5a1), 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.