Skip to content

Reader Mastodon: follow / unfollow button on profile pages#110531

Open
jeherve wants to merge 16 commits intotrunkfrom
mastodon-follow
Open

Reader Mastodon: follow / unfollow button on profile pages#110531
jeherve wants to merge 16 commits intotrunkfrom
mastodon-follow

Conversation

@jeherve
Copy link
Copy Markdown
Member

@jeherve jeherve commented May 7, 2026

Fixes CM-681

Backend PR: 215533-ghe-Automattic/wpcom

Proposed Changes

  • Adds a Follow / Follow back / Unfollow / Requested button to the Reader Mastodon author-profile pages, mirroring the ATmosphere implementation in Reader / ATmosphere: Follow button on Bluesky author profile pages #110375.
  • Extends the protocol-agnostic FollowButton (client/reader/social/follow-button.tsx) with an optional isRequested prop for Mastodon's locked-account pending-follow state — ATmosphere call sites are unaffected (default false).
  • Adds new types (MastodonAuthorProfileViewer, MastodonFollowResponse, follow-params), fetchers (createMastodonFollow, deleteMastodonFollow), and TanStack Query mutation factories (followMastodonActorMutation, unfollowMastodonActorMutation) with optimistic-update / rollback / on-success-commit semantics, plus the cancelQueries try/catch hardening from client/reader/AGENTS.md.
  • Wires the button into MastodonAuthorProfilePanel via SocialProfileCard headerActions, gated on viewer && ! is_self for forwards-compat with backends that haven't deployed the viewer projection yet, with click + error Tracks events and an errorNotice toast on mutation failure (cleared on subsequent success).

Why are these changes being made?

  • Brings the Mastodon profile surface to parity with the ATmosphere profile surface for follow interactions — the federated-feed reader needs follow state to be discoverable and actionable on the same surface where users land from in-app mention links and search.
  • Locked-account Requested is meaningful in Mastodon (no AT Proto analogue) and a real fraction of accounts opt into it, so the locked-account state is modeled explicitly rather than collapsed into "Following".
  • The forwards-compat gate (viewer? / is_self? optional fields) lets this frontend ship independently of the wpcom backend changes, with no user-visible behaviour change until the backend deploys.

Testing Instructions

  • Backend dependency: this PR depends on the wpcom Mastodon backend changes (215533-ghe-Automattic/wpcom — adds the viewer block on GET /reader/mastodon/connections/{id}/profile/{actor}, plus POST /follows and DELETE /follows/{account_id} endpoints, plus read:follows + write:follows Keyring scopes). Without the backend, the forwards-compat gate keeps the button hidden — verify nothing regresses on existing Mastodon profile pages.
  • Once the backend lands: open /reader/mastodon/{connectionId}/profile/{actor} for an account you don't follow → the Follow button renders. Click → button switches to "Following" optimistically, network call confirms.
  • Hover or keyboard-focus a "Following" / "Requested" button → label swaps to "Unfollow" / "Cancel request"; click invokes the unfollow path (which also cancels pending requests on Mastodon).
  • Locked-account flow: follow a locked Mastodon account → button lands on "Requested" once the server response commits. Unfollow → request cancelled.
  • Force a 5xx upstream response (e.g. via Chrome DevTools network override) → button reverts (rollback) and an error toast appears; subsequent successful action clears the toast.
  • Existing connections will be flagged for reauth via the existing auth-status flow once the backend ships the read:follows + write:follows scope additions.
  • Visit your own profile → the button is hidden (is_self: true).
  • Run unit tests: yarn test-client client/reader/social client/reader/mastodon/test/author-profile-panel.test.tsx and yarn test-packages packages/api-core/src/reader-mastodon packages/api-queries/src/__tests__/reader-mastodon.test.tsx.

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

jeherve added 11 commits May 7, 2026 01:23
The Mastodon profile page needs a 'Requested' affordance for the
locked-account follow-pending state. ATmosphere call sites are
unaffected (default isRequested=false). Click invokes onUnfollow —
Mastodon's unfollow endpoint cancels pending requests, so consumers
need only one handler for both Following and Requested.
Adds MastodonViewerState, MastodonCreateFollowParams,
MastodonDeleteFollowParams, MastodonFollowResponse, and extends
MastodonAuthorProfile with optional viewer and is_self fields. Fields
are optional during the backend rollout window so this commit can
land independently of the wpcom backend changes.
…ProfileViewer

Match the existing per-feature naming convention in this file
(MastodonFeedItemViewer for the per-status viewer block). Pure
rename — no functional changes.
Adds createMastodonFollow (POST /follows) and deleteMastodonFollow
(DELETE /follows/{account_id}) targeting the new wpcom backend
endpoints. Both return a uniform MastodonFollowResponse with the
post-action viewer block, so optimistic patches and server-state
commits use the same shape.
Mirrors followAtmosphereActorMutation / unfollowAtmosphereActorMutation
shape: optimistic patch on the scoped author-profile cache entry,
rollback on error, server-state commit in onSuccess (so locked-account
'requested: true' lands correctly even though we optimistically set
'following: true'). Factories accept the consumer's QueryClient per
the Calypso/Dashboard split documented in client/reader/AGENTS.md.
Renders FollowButton in the SocialProfileCard headerActions slot when
the backend has projected the viewer block (forwards-compat gate)
and the resolved actor is not the viewer's own profile. Wires the
two mutation hooks at component-level so isPending is reactive,
emits Tracks events for click + error, and forwards locked-account
'requested' state into the new FollowButton prop.
Mirrors atmosphere parity — when a follow or unfollow mutation fails
(rate-limited, auth-expired, upstream unavailable), dispatch errorNotice
in addition to the existing Tracks event so users see why the button
reverted. Clears the notice on subsequent success. Adds a regression
test for the error path.
The scoped author-profile query normalizes the actor input (trim,
strip leading @, lowercase) before building its cache key. The new
follow / unfollow mutation factories built their key directly from
the raw vars.actor, so webfinger or mixed-case actors keyed to a
different cache entry than the query — leaving the optimistic patch
and rollback as silent no-ops. Numeric ids worked because
normalization is a no-op for digits.

Adds a regression test that drives the mutation with
'@alice@MASTODON.social' and asserts the patch lands on the entry
seeded under the normalized 'alice@mastodon.social' key.
Mirrors the per-protocol adapter pattern documented in
client/reader/AGENTS.md and used by use-mastodon-{like,repost}-action.ts:
the pipeline-level error log lives in the client adapter, not in
packages/api-queries (which can't import calypso/lib/logstash).

Tests mock calypso/lib/logstash so the new call doesn't trigger an
unmocked-request alarm in nock.
- Validate the MastodonFollowResponse shape at the fetcher boundary so a
  malformed payload (missing viewer) fails loudly instead of writing
  viewer: undefined into the cache during the optimistic-update commit.
- In follow/unfollow onSuccess, invalidate the profile query when the
  cache entry was evicted between onMutate and onSuccess so the
  authoritative server viewer (which carries requested: true for locked
  accounts) is refetched instead of silently dropped.
- In onError, invalidate when no previous snapshot exists so the
  optimistic patch can't outlive the failure as a stale value.
- Tighten viewer? / requested JSDoc per comment review (drop literal
  default object; drop the federation-queue speculation).
- Add tests for the malformed-payload guard, the not_found-specific
  follow/unfollow copy, removeNotice on retry success, and a
  logToLogstash assertion.
@jeherve jeherve self-assigned this May 7, 2026
# Conflicts:
#	packages/api-queries/src/reader-mastodon.ts
@matticbot
Copy link
Copy Markdown
Contributor

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • help-center
  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug mastodon-follow on your sandbox.

# Conflicts:
#	packages/api-core/src/reader-mastodon/types.ts
#	packages/api-queries/src/__tests__/reader-mastodon.test.tsx
@jeherve jeherve marked this pull request as ready for review May 7, 2026 18:14
@jeherve jeherve requested a review from a team as a code owner May 7, 2026 18:14
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label May 7, 2026
jeherve added 3 commits May 7, 2026 20:25
Tighten `assertMastodonFollowResponse` to verify the `viewer` block
carries the three required booleans (not just that `viewer` is an
object), and throw with a distinct message so a backend-shape
regression is grep-able in Logstash / Sentry rather than
indistinguishable from a real 400.

Add the missing optimistic-mutation test cases:
- follow: rollback path when no previous snapshot exists
  (`onError` falls back to `invalidateQueries`)
- follow: cache eviction between `onMutate` and `onSuccess`
  (`onSuccess` falls back to `invalidateQueries`)
- unfollow: webfinger actor normalization parity with follow
- FollowButton: `isRequested` precedence over `isFollowedBy`
… awaits

Three fixes from code review:

- Optimistically write `requested: true` (not `following: true`) when the
  target is locked. Without this the button paints "Following / Unfollow"
  for the round-trip and snaps to "Requested" on commit — a UX flip-flop
  that also misleads AT users mid-flight. Threads `locked` through
  `FollowMastodonActorVars` with a fallback to the cached profile's
  `locked` field for backwards-compat.
- Return the `invalidateQueries` promise from both factories' `onError`
  and `onSuccess` fallbacks so TanStack waits for the refetch before
  transitioning the mutation out of `pending`.
- Capture `accountId` at click time in the panel handlers so error
  analytics survive a profile refetch racing with the in-flight
  mutation. `showFollowError` now takes the captured id rather than
  re-reading `profile.data?.id`.

Adds two regression tests covering the locked-account optimistic patch
both with explicit `vars.locked` and via the cached `old.locked`
fallback.
…rrors

Adversarial-pass follow-ups:

- Panel-level unfollow click test: DELETE wire call, the
  `calypso_reader_mastodon_profile_unfollow_clicked` Tracks payload
  (`was_requested` included), `removeNotice` clearing, the Cancel-request
  variant for locked-account pending requests, and a 502-unfollow
  failure asserting `action: 'unfollow'` in Tracks + Logstash plus the
  `not_found` unfollow-specific copy on a 404.
- Panel-level Tracks payload assertions: `toContainEqual` on the full
  `follow_clicked` props (including `was_locked`), with a separate test
  exercising `was_locked: true` end-to-end.
- FollowButton: regression test for `disabled={ isPending }` on the new
  Requested branch.
- api-queries: `vars.locked: false` wins over `old.locked: true`
  precedence test — covers the third arm of the
  `vars.locked ?? previous?.locked ?? false` fallback chain.
- Fetchers: parameterized malformed-payload coverage (null, missing
  field, non-boolean values) and parameterized error-kind coverage
  (`not_found`, `rate_limited`, `upstream_unavailable`, `bad_request`)
  for both create and delete fetchers.
- `assertMastodonFollowResponse` now throws a real `Error` (with the
  `code` property) instead of a bare object, so dev-tools rejection
  logs surface a usable stack trace. Classification through
  `classifyMastodonError` is unchanged.
- Tightened the `if ( ! updated )` comment in the optimistic-update
  fallbacks to acknowledge both eviction and never-populated cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants