diff --git a/InterlinedList/Models/MutualFollowCounts.cs b/InterlinedList/Models/MutualFollowCounts.cs
new file mode 100644
index 0000000..e0cb738
--- /dev/null
+++ b/InterlinedList/Models/MutualFollowCounts.cs
@@ -0,0 +1,40 @@
+namespace InterlinedList.Models;
+
+///
+/// How many follows two accounts have in common, from
+/// GET /api/follow/{userId}/mutual.
+///
+///
+///
+/// Counts only — there is no endpoint that lists the mutual users.
+/// Verified live 2026-09-16 across both documented parameter forms:
+///
+///
+/// GET /api/follow/{userId}/mutual -> {"mutualFollowers":1,"mutualFollowing":1}
+/// GET /api/follow/{userId}/mutual?otherUserId={other} -> {"mutualFollowers":1,"mutualFollowing":1}
+///
+///
+/// So clickable "mutual connections" chips cannot be built — don't reinstate
+/// them against this endpoint. The previous GetMutualAsync asked for an
+/// array under a mutual key, which is absent, and the tolerant
+/// array reader turned that into an empty list: the feature rendered nothing
+/// and reported no error. See #160.
+///
+///
+public sealed class MutualFollowCounts
+{
+ /// Accounts that follow both the viewer and the profile.
+ public int MutualFollowers { get; init; }
+
+ /// Accounts both the viewer and the profile follow.
+ public int MutualFollowing { get; init; }
+
+ public bool HasAny => MutualFollowers > 0 || MutualFollowing > 0;
+
+ /// A single human line, or null when there is nothing to say.
+ public string? Summary => HasAny
+ ? $"{MutualFollowing} mutual following · {MutualFollowers} mutual followers"
+ : null;
+
+ public static MutualFollowCounts None => new();
+}
diff --git a/InterlinedList/Services/InterlinedApiClient.Follow.cs b/InterlinedList/Services/InterlinedApiClient.Follow.cs
index a65263f..65781ce 100644
--- a/InterlinedList/Services/InterlinedApiClient.Follow.cs
+++ b/InterlinedList/Services/InterlinedApiClient.Follow.cs
@@ -39,6 +39,31 @@ public Task> GetFollowersAsync(string userId, CancellationToken
public Task> GetFollowingAsync(string userId, CancellationToken ct = default)
=> GetUserArrayAsync($"api/follow/{userId}/following", "following", ct);
+ ///
+ /// Mutual-follow counts between the caller and .
+ ///
+ ///
+ ///
+ /// This replaces a Task<List<FollowUser>> signature that the API
+ /// cannot satisfy. The endpoint returns
+ /// {"mutualFollowers":n,"mutualFollowing":n} — never a user array —
+ /// so the old call asked for an absent
+ /// mutual key and silently received an empty list, which is why the
+ /// profile's "Mutual connections" chips never rendered. See #160.
+ ///
+ ///
+ /// There is no endpoint that lists the mutual users — both this
+ /// route's parameter forms (bare, and with the spec-documented
+ /// otherUserId) return the same counts object.
+ ///
+ ///
+ public async Task GetMutualCountsAsync(string userId, CancellationToken ct = default)
+ {
+ var json = await GetElementAsync($"api/follow/{userId}/mutual", ct);
+ return json.ValueKind == JsonValueKind.Object
+ ? json.Deserialize(JsonOptions) ?? MutualFollowCounts.None
+ : MutualFollowCounts.None;
+ }
/// One page of a user's followers.
///
/// Optional server-side filter on the follow edge's state. Live values seen:
@@ -62,11 +87,15 @@ public Task GetFollowingPageAsync(
string userId, int limit = 50, int offset = 0, string? status = null, CancellationToken ct = default)
=> GetUserPageAsync($"api/follow/{userId}/following", "following", limit, offset, status, ct);
- public Task> GetMutualAsync(string userId, CancellationToken ct = default)
- => GetUserArrayAsync($"api/follow/{userId}/mutual", "mutual", ct);
-
- // Follow lists wrap their array under different property names
- // (requests/followers/following/mutual) — pull the named array, tolerate a miss.
+ // Follow lists wrap their array under different property names — pull the
+ // named array, tolerate a miss.
+ //
+ // Keys verified live 2026-09-16: `requests` -> {"requests":[]},
+ // `followers`/`following` -> {"":[…],"pagination":{…}}. NOTE `mutual`
+ // is NOT one of these — that endpoint returns counts, not an array, and
+ // this helper's tolerance silently hid the mismatch for weeks (#160). If you
+ // add a key here, confirm the array actually exists in a live payload; a
+ // wrong key fails as "feature renders nothing", not as an error.
private async Task> GetUserArrayAsync(string path, string property, CancellationToken ct)
{
var json = await GetElementAsync(path, ct);
diff --git a/InterlinedList/ViewModels/ProfileViewModel.cs b/InterlinedList/ViewModels/ProfileViewModel.cs
index d34726d..2241f34 100644
--- a/InterlinedList/ViewModels/ProfileViewModel.cs
+++ b/InterlinedList/ViewModels/ProfileViewModel.cs
@@ -17,7 +17,15 @@ public partial class ProfileViewModel : ObservableObject
public ObservableCollection FollowRequests { get; } = new();
public ObservableCollection Messages { get; } = new();
- public ObservableCollection Mutuals { get; } = new();
+ ///
+ /// Mutual-follow counts. The API exposes counts only — there is no endpoint
+ /// listing the mutual users, so the old clickable chips could never work
+ /// (#160).
+ ///
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(HasMutuals))]
+ [NotifyPropertyChangedFor(nameof(MutualsSummary))]
+ private MutualFollowCounts? mutuals;
/// The profile's followers, paged. Server default page is 50.
public ObservableCollection Followers { get; } = new();
@@ -82,7 +90,9 @@ public partial class ProfileViewModel : ObservableObject
public bool HasProfile => Profile is not null;
- public bool HasMutuals => Mutuals.Count > 0;
+ public bool HasMutuals => Mutuals?.HasAny == true;
+
+ public string MutualsSummary => Mutuals?.Summary ?? string.Empty;
public string FollowButtonText =>
Relationship?.IsFollowing == true ? "Following"
@@ -97,7 +107,6 @@ public ProfileViewModel(SessionService session)
{
_session = session;
FollowRequests.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasRequests));
- Mutuals.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasMutuals));
}
[RelayCommand]
@@ -158,9 +167,7 @@ private async Task LoadProfileAsync()
foreach (var message in page.Messages)
Messages.Add(new MessageItemViewModel(message, _session.Api, _session.CurrentUser?.Id));
- Mutuals.Clear();
- foreach (var mutual in await _session.Api.GetMutualAsync(profile.Id))
- Mutuals.Add(mutual);
+ Mutuals = await _session.Api.GetMutualCountsAsync(profile.Id);
IsOwnProfile = profile.Id == _session.CurrentUser?.Id;
diff --git a/InterlinedList/Views/PeopleView.xaml b/InterlinedList/Views/PeopleView.xaml
index d04cb2b..388441d 100644
--- a/InterlinedList/Views/PeopleView.xaml
+++ b/InterlinedList/Views/PeopleView.xaml
@@ -428,40 +428,22 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+