feat(mobile): full-screen avatar viewer on profile screen tap#14444
Merged
Conversation
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>
|
8 tasks
dylanjeffers
added a commit
that referenced
this pull request
Jun 4, 2026
…ver photo (#14445) Follow-ups to #14444. ## 1. Avatar viewer keeps the circle The avatar on the profile screen is a circle, so the full-screen viewer should match — not a letterboxed square on black. `AvatarViewer` now renders the image inside a fixed square wrapper with `borderRadius: size / 2` + `overflow: 'hidden'` and `resizeMode='cover'`, sized to `min(windowWidth, windowHeight) - 48 px` so it doesn't touch the screen edges on landscape phones. ## 2. Banner viewer on the profile cover photo Tapping the cover photo at the top of the profile screen now opens a parallel full-screen viewer for the banner. Uses `WidthSizes.SIZE_2000` (the largest cached cover-photo size — the header uses `SIZE_640`) and `resizeMode='contain'` to preserve the cover photo's native aspect ratio on the black backdrop. No circular clip. ## Refactor: shared `FullscreenImageViewer` Rather than duplicate the modal/gesture/close-button machinery between Avatar and Banner viewers, the chrome is factored into a new **`FullscreenImageViewer`** that owns: - `<Modal transparent animationType='fade' statusBarTranslucent>` + `light-content` StatusBar - Pan gesture via `react-native-gesture-handler` — Reanimated shared values follow the finger in any direction, dismiss thresholds at **120 px** distance or **800 px/s** velocity, **200 ms** `withTiming` spring-back, translation reset on each open - X close button at `insets.top + 8 px` with `hitSlop`, accessible label/role - Black `#000` backdrop `AvatarViewer` and `BannerViewer` are now thin wrappers that supply the image content as `children` and a `closeAccessibilityLabel`. Same dismiss UX in both. ## ProfileCoverPhoto tap wiring The existing `<CoverPhoto>` is wrapped in a `<Pressable>` that flips local `isViewerOpen` state. The artist-badge overlay (which has no own `onPress`) sits absolute on top and stays visually unchanged — taps in the badge corner still hit the Pressable underneath. ```diff + <Pressable onPress={handleOpenViewer} accessibilityRole='imagebutton' …> <CoverPhoto userId={user_id}> <AnimatedBlurView … /> {isArtist ? <Animated.View style={styles.darkOverlay} /> : null} </CoverPhoto> + </Pressable> {isArtist ? <Animated.View style={[styles.artistBadge, badgeStyle]}>…</Animated.View> : null} + <BannerViewer userId={user_id} isOpen={isViewerOpen} onClose={handleCloseViewer} /> ``` ## Files | File | Change | |---|---| | `packages/mobile/src/screens/profile-screen/FullscreenImageViewer.tsx` | **new** — shared chrome | | `packages/mobile/src/screens/profile-screen/AvatarViewer.tsx` | refactored — circle clip child of FullscreenImageViewer | | `packages/mobile/src/screens/profile-screen/BannerViewer.tsx` | **new** — `SIZE_2000`, `resizeMode='contain'`, child of FullscreenImageViewer | | `packages/mobile/src/screens/profile-screen/ProfileCoverPhoto.tsx` | Pressable wrap + BannerViewer mount | Net diff: **+288 / −144**. ## Verification - [x] `tsc --noEmit` clean in `packages/mobile` - [ ] Manual: tap an avatar → fades in, image rendered as a circle on black backdrop. - [ ] Manual: tap the cover photo → fades in, image rendered with `contain` aspect ratio. - [ ] Manual: short drag on either viewer → springs back. - [ ] Manual: longer drag or flick in any of the four directions → dismisses. - [ ] Manual: tap the X in the top-right → dismisses. - [ ] Manual: tap the artist badge (where present) → still navigates to `CoinDetailsScreen` without opening the avatar viewer. - [ ] Manual: cover-photo parallax/blur on scroll still works the same once the Pressable is in place. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
<Modal>withtransparent+animationType="fade"handles the open/close fade-in/out automatically.statusBarTranslucentkeeps the image flush to the top edge under the notch.resizeMode="contain"so the square avatar fits the rectangular window with letterboxing instead of cropping. Width capped atuseWindowDimensions().width.useProfilePicture({ userId, size: SquareSizes.SIZE_1000_BY_1000 }). The header usesSIZE_150_BY_150for the small tile; the viewer uses 1000 so it isn't a blurry upscale.IconClosein the top-right atinsets.top + 8with a generoushitSlop. Accessible role + label.Gesture.Pan()fromreact-native-gesture-handler. The image follows the finger in any direction via ReanimateduseSharedValue/useAnimatedStyle. On release:120 pxOR velocity >800 px/s→runOnJS(onClose)()(the Modal fade-out handles the rest)withTiming(0, { duration: 200 })on both axesuseEffectkeyed onisOpen, so a partial-then-cancelled drag from a previous open doesn't carry over.#000backdrop;StatusBarset tolight-content+ black while mounted.packages/mobile/src/screens/profile-screen/ProfileHeader/ArtistProfilePicture.tsx<ProfilePicture>in a<TouchableOpacity>withactiveOpacity={0.85}to signal tappability (no visual change beyond a brief fade on press).isViewerOpenstate withuseState.<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 + 1over the avatar, so badge taps still route toCoinDetailsScreen(the avatar tap doesn't fire under it).Verification
tsc --noEmitclean inpackages/mobile.CoinDetailsScreenwithout opening the viewer.onRequestClose).Out of scope
AvatarViewercomponent itself is general-purpose if we want to add more entry points later.🤖 Generated with Claude Code