forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {Pressable, View} from 'react-native'
3import Animated, {
4 measure,
5 type MeasuredDimensions,
6 runOnJS,
7 runOnUI,
8 useAnimatedRef,
9} from 'react-native-reanimated'
10import {type AppBskyGraphDefs} from '@atproto/api'
11import {msg, Trans} from '@lingui/macro'
12import {useLingui} from '@lingui/react'
13import {useNavigation} from '@react-navigation/native'
14
15import {usePalette} from '#/lib/hooks/usePalette'
16import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
17import {makeProfileLink} from '#/lib/routes/links'
18import {type NavigationProp} from '#/lib/routes/types'
19import {sanitizeHandle} from '#/lib/strings/handles'
20import {emitSoftReset} from '#/state/events'
21import {useLightboxControls} from '#/state/lightbox'
22import {TextLink} from '#/view/com/util/Link'
23import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
24import {Text} from '#/view/com/util/text/Text'
25import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar'
26import {StarterPack} from '#/components/icons/StarterPack'
27import * as Layout from '#/components/Layout'
28
29export function ProfileSubpageHeader({
30 isLoading,
31 href,
32 title,
33 avatar,
34 isOwner,
35 purpose,
36 creator,
37 avatarType,
38 children,
39}: React.PropsWithChildren<{
40 isLoading?: boolean
41 href: string
42 title: string | undefined
43 avatar: string | undefined
44 isOwner: boolean | undefined
45 purpose: AppBskyGraphDefs.ListPurpose | undefined
46 creator:
47 | {
48 did: string
49 handle: string
50 }
51 | undefined
52 avatarType: UserAvatarType | 'starter-pack'
53}>) {
54 const navigation = useNavigation<NavigationProp>()
55 const {_} = useLingui()
56 const {isMobile} = useWebMediaQueries()
57 const {openLightbox} = useLightboxControls()
58 const pal = usePalette('default')
59 const canGoBack = navigation.canGoBack()
60 const aviRef = useAnimatedRef()
61
62 const _openLightbox = React.useCallback(
63 (uri: string, thumbRect: MeasuredDimensions | null) => {
64 openLightbox({
65 images: [
66 {
67 uri,
68 thumbUri: uri,
69 thumbRect,
70 dimensions: {
71 // It's fine if it's actually smaller but we know it's 1:1.
72 height: 1000,
73 width: 1000,
74 },
75 thumbDimensions: null,
76 type: 'rect-avi',
77 },
78 ],
79 index: 0,
80 })
81 },
82 [openLightbox],
83 )
84
85 const onPressAvi = React.useCallback(() => {
86 if (
87 avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
88 ) {
89 runOnUI(() => {
90 'worklet'
91 const rect = measure(aviRef)
92 runOnJS(_openLightbox)(avatar, rect)
93 })()
94 }
95 }, [_openLightbox, avatar, aviRef])
96
97 return (
98 <>
99 <Layout.Header.Outer>
100 {canGoBack ? (
101 <Layout.Header.BackButton />
102 ) : (
103 <Layout.Header.MenuButton />
104 )}
105 <Layout.Header.Content />
106 {children}
107 </Layout.Header.Outer>
108
109 <View
110 style={{
111 flexDirection: 'row',
112 alignItems: 'flex-start',
113 gap: 10,
114 paddingTop: 14,
115 paddingBottom: 14,
116 paddingHorizontal: isMobile ? 12 : 14,
117 }}>
118 <Animated.View ref={aviRef} collapsable={false}>
119 <Pressable
120 testID="headerAviButton"
121 onPress={onPressAvi}
122 accessibilityRole="image"
123 accessibilityLabel={_(msg`View the avatar`)}
124 accessibilityHint=""
125 style={{width: 58}}>
126 {avatarType === 'starter-pack' ? (
127 <StarterPack width={58} gradient="sky" />
128 ) : (
129 <UserAvatar type={avatarType} size={58} avatar={avatar} />
130 )}
131 </Pressable>
132 </Animated.View>
133 <View style={{flex: 1, gap: 4}}>
134 {isLoading ? (
135 <LoadingPlaceholder
136 width={200}
137 height={32}
138 style={{marginVertical: 6}}
139 />
140 ) : (
141 <TextLink
142 testID="headerTitle"
143 type="title-xl"
144 href={href}
145 style={[pal.text, {fontWeight: '600'}]}
146 text={title || ''}
147 onPress={emitSoftReset}
148 numberOfLines={4}
149 />
150 )}
151
152 {isLoading || !creator ? (
153 <LoadingPlaceholder width={50} height={8} />
154 ) : (
155 <Text type="lg" style={[pal.textLight]} numberOfLines={1}>
156 {purpose === 'app.bsky.graph.defs#curatelist' ? (
157 isOwner ? (
158 <Trans>List by you</Trans>
159 ) : (
160 <Trans>
161 List by{' '}
162 <TextLink
163 text={sanitizeHandle(creator.handle || '', '@')}
164 href={makeProfileLink(creator)}
165 style={pal.textLight}
166 />
167 </Trans>
168 )
169 ) : purpose === 'app.bsky.graph.defs#modlist' ? (
170 isOwner ? (
171 <Trans>Moderation list by you</Trans>
172 ) : (
173 <Trans>
174 Moderation list by{' '}
175 <TextLink
176 text={sanitizeHandle(creator.handle || '', '@')}
177 href={makeProfileLink(creator)}
178 style={pal.textLight}
179 />
180 </Trans>
181 )
182 ) : purpose === 'app.bsky.graph.defs#referencelist' ? (
183 isOwner ? (
184 <Trans>Starter pack by you</Trans>
185 ) : (
186 <Trans>
187 Starter pack by{' '}
188 <TextLink
189 text={sanitizeHandle(creator.handle || '', '@')}
190 href={makeProfileLink(creator)}
191 style={pal.textLight}
192 />
193 </Trans>
194 )
195 ) : null}
196 </Text>
197 )}
198 </View>
199 </View>
200 </>
201 )
202}