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