Skip to content

feat(mobile): full-screen avatar viewer on profile screen tap#14444

Open
dylanjeffers wants to merge 1 commit into
mainfrom
feat/mobile-avatar-viewer
Open

feat(mobile): full-screen avatar viewer on profile screen tap#14444
dylanjeffers wants to merge 1 commit into
mainfrom
feat/mobile-avatar-viewer

Conversation

@dylanjeffers
Copy link
Copy Markdown
Contributor

Tapping the avatar on a profile page now opens a full-window viewer of the user's profile picture at the largest cached image size.

What's new

packages/mobile/src/screens/profile-screen/AvatarViewer.tsx (new)

Self-contained viewer component — no nav stack changes, no provider.

  • React Native <Modal> with transparent + animationType="fade" handles the open/close fade-in/out automatically. statusBarTranslucent keeps the image flush to the top edge under the notch.
  • Image rendered with resizeMode="contain" so the square avatar fits the rectangular window with letterboxing instead of cropping. Width capped at useWindowDimensions().width.
  • Highest-resolution source: useProfilePicture({ userId, size: SquareSizes.SIZE_1000_BY_1000 }). The header uses SIZE_150_BY_150 for the small tile; the viewer uses 1000 so it isn't a blurry upscale.
  • Close button: IconClose in the top-right at insets.top + 8 with a generous hitSlop. Accessible role + label.
  • Swipe to dismiss: Gesture.Pan() from react-native-gesture-handler. The image follows the finger in any direction via Reanimated useSharedValue/useAnimatedStyle. On release:
    • Drag distance > 120 px OR velocity > 800 px/srunOnJS(onClose)() (the Modal fade-out handles the rest)
    • Otherwise → spring back to center with withTiming(0, { duration: 200 }) on both axes
  • Reset on open: translation shared values reset to 0 in a useEffect keyed on isOpen, so a partial-then-cancelled drag from a previous open doesn't carry over.
  • Black #000 backdrop; StatusBar set to light-content + black while mounted.

packages/mobile/src/screens/profile-screen/ProfileHeader/ArtistProfilePicture.tsx

  • Wraps the existing <ProfilePicture> in a <TouchableOpacity> with activeOpacity={0.85} to signal tappability (no visual change beyond a brief fade on press).
  • Local isViewerOpen state with useState.
  • Renders <AvatarViewer userId={userId} isOpen={isViewerOpen} onClose={…}> as a sibling.

The fan-club badge overlay keeps its own tap target — it sits at zIndex: PROFILE_PAGE_PROFILE_PICTURE + 1 over the avatar, so badge taps still route to CoinDetailsScreen (the avatar tap doesn't fire under it).

Verification

  • tsc --noEmit clean in packages/mobile.
  • Manual: tap an avatar on any profile screen → black full-screen viewer fades in, image at the largest cached resolution.
  • Manual: tap the X in the top-right → viewer fades out.
  • Manual: short drag in any direction (< ~100 px, slow) → image springs back, viewer stays open.
  • Manual: longer drag or quick flick in any of the four directions → viewer dismisses.
  • Manual: tap the fan-club badge (where present) → still navigates to CoinDetailsScreen without opening the viewer.
  • Manual: Android hardware back → viewer dismisses (handled by Modal's onRequestClose).

Out of scope

  • Pinch-to-zoom — the spec listed dismiss-via-any-swipe; zoom can be a separate enhancement (would conflict with the dismiss gesture without a more elaborate gesture composition).
  • Avatar viewer from other screens (lineup tiles, comments, etc.) — only wired into the profile screen header per the request. The AvatarViewer component itself is general-purpose if we want to add more entry points later.

🤖 Generated with Claude Code

Tapping the avatar on a profile page now opens a full-window viewer of
the user's profile picture at SquareSizes.SIZE_1000_BY_1000 (the largest
size the image API caches) on a black backdrop. The fan-club badge
overlay keeps its own tap target via the higher zIndex it already had,
so tapping the badge still routes to CoinDetailsScreen — only the
underlying avatar opens the viewer.

The viewer (packages/mobile/src/screens/profile-screen/AvatarViewer.tsx):
- React Native Modal with transparent + animationType="fade" handles the
  open/close fade-in/out automatically. statusBarTranslucent keeps the
  image flush to the top edge under the notch.
- Image rendered with resizeMode="contain" so the square avatar fits the
  rectangular window with letterboxing instead of cropping.
- IconClose button in the top-right at safe-area top inset + 8 px with a
  generous hitSlop. Accessible role/label.
- Pan gesture (react-native-gesture-handler) follows the user's finger
  in any direction via Reanimated shared values. On release, if the drag
  distance exceeds 120 px or the velocity exceeds 800 px/s, the viewer
  dismisses; otherwise the image springs back to center with a 200 ms
  withTiming. Translation values are reset on each open.
- StatusBar light-content + black background while the modal is mounted.

ArtistProfilePicture wraps the existing ProfilePicture in a
TouchableOpacity with `activeOpacity={0.85}` to signal tappability, and
manages a local isOpen state for the viewer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 3, 2026

⚠️ No Changeset found

Latest commit: e287f10

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant