Commit 7de135c
feat(mobile): circle-clip avatar viewer + banner viewer on profile cover 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>1 parent be907a3 commit 7de135c
4 files changed
Lines changed: 288 additions & 144 deletions
File tree
- packages/mobile/src/screens/profile-screen
Lines changed: 32 additions & 135 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | | - | |
3 | 1 | | |
4 | 2 | | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
| 3 | + | |
21 | 4 | | |
22 | | - | |
23 | 5 | | |
24 | 6 | | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
31 | 10 | | |
32 | 11 | | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | 12 | | |
46 | 13 | | |
47 | 14 | | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | 15 | | |
57 | 16 | | |
58 | 17 | | |
| |||
63 | 22 | | |
64 | 23 | | |
65 | 24 | | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
70 | 29 | | |
71 | 30 | | |
72 | | - | |
73 | | - | |
| 31 | + | |
74 | 32 | | |
75 | 33 | | |
76 | 34 | | |
| |||
79 | 37 | | |
80 | 38 | | |
81 | 39 | | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | | - | |
131 | | - | |
132 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
133 | 43 | | |
134 | 44 | | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
141 | 49 | | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
169 | 66 | | |
170 | 67 | | |
Lines changed: 52 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
0 commit comments