Skip to main content

Profile Viewers — Technical Reference

← Back to Profile Viewers

Updated — the missing write-side wiring described below has been fixed: PublicProfilePage.tsx now calls recordProfileVisit on load. The feature is fully functional end to end as of this pass.

Where this lives

Backend

Frontend

  • apps/frontend-nextjs/src/page-components/settings/ProfileViewersPage.tsx — queries myProfileViewers(limit: 50, offset: 0) and myProfileViewerCount on mount (network-only), renders the list + count, and a Clear button calling clearMyProfileViewers. limit: 50 is hardcoded — no pagination UI despite the resolver accepting offset.
  • Route: apps/frontend-nextjs/src/app/settings/profile-viewers/page.tsx
  • Entry points: SettingsPage.tsx ("Profile visitors" settings item) and ProfilePage.tsx (a menu item on your own profile), both linking to /settings/profile-viewers.
  • recordProfileVisit is now called from PublicProfilePage.tsx (fixed this pass) — a useEffect fires once per page load, reusing the page's existing isOwnProfile check to skip self-views, and only fires for authenticated visitors. The mutation swallows its own errors client-side (.catch(() => {})) so a failed call never breaks the profile page. The backend's own no-op-on-self-view and 60-second debounce still apply as a second layer of protection.

Technical implementation checklist

  • myProfileViewers — real query, dedupes to one row per distinct viewer (most recent visit) in application code (profile-view.manager.js)
  • myProfileViewerCount — real query
  • clearMyProfileViewers — deletes all rows for the caller
  • recordProfileVisit (backend) — fully implemented: requires auth, no-ops on self-view, 60s debounce, checks the hide_profile_visits flag (see below), which is real and wired. Correction: the argument is named targetUserId, not viewedUserId as an earlier version of this doc assumed — verify against the type file rather than this doc if in doubt.
  • recordProfileVisit (frontend call site) — fixed this pass. PublicProfilePage.tsx now calls it in a useEffect on load, guarded to skip self-views and logged-out visitors.
  • hide_profile_visits privacy flag — real and wired end to end: defaults to false in privacy-settings.manager.js's default settings object, included in validatePrivacySettings's boolean allow-list, exposed as hideProfileVisits on the PrivacySettings GraphQL type and its input, and has a toggle in AccountPrivacyPage.tsx. Enforced in profile-view.manager.js's gate check — a viewer with it enabled records no visit for any profile they browse.
  • No premium/coin gating — free for all authenticated users.
  • No retention/expiry job — history is unlimited until the user calls clearMyProfileViewers.
  • No iOS implementation exists (confirmed by grep across apps/ios).

GraphQL API

query MyProfileViewers($limit: Int, $offset: Int) {
myProfileViewers(limit: $limit, offset: $offset) {
viewer { id username profilePicture isVerified }
viewedAt
}
}

query MyProfileViewerCount { myProfileViewerCount }

# Now called from PublicProfilePage.tsx on load (fixed this pass)
mutation RecordProfileVisit($targetUserId: ID!) {
recordProfileVisit(targetUserId: $targetUserId) # returns Boolean!, not an object
}

mutation ClearMyProfileViewers { clearMyProfileViewers } # also returns Boolean!, not an object