forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useMemo} from 'react'
2import {View} from 'react-native'
3import {
4 type AppBskyActorDefs,
5 type ModerationCause,
6 type ModerationDecision,
7} from '@atproto/api'
8import {msg} from '@lingui/core/macro'
9import {useLingui} from '@lingui/react'
10
11import {makeProfileLink} from '#/lib/routes/links'
12import {sanitizeDisplayName} from '#/lib/strings/display-names'
13import {type Shadow} from '#/state/cache/profile-shadow'
14import {isConvoActive, useConvo} from '#/state/messages/convo'
15import {type ConvoItem} from '#/state/messages/convo/types'
16import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
17import {atoms as a, useTheme, web} from '#/alf'
18import {ConvoMenu} from '#/components/dms/ConvoMenu'
19import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
20import * as Layout from '#/components/Layout'
21import {Link} from '#/components/Link'
22import {PostAlerts} from '#/components/moderation/PostAlerts'
23import {PdsBadge} from '#/components/PdsBadge'
24import {Text} from '#/components/Typography'
25import {useSimpleVerificationState} from '#/components/verification'
26import {VerificationCheck} from '#/components/verification/VerificationCheck'
27import {IS_WEB} from '#/env'
28
29const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
30
31export function MessagesListHeader({
32 profile,
33 moderation,
34}: {
35 profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
36 moderation?: ModerationDecision
37}) {
38 const t = useTheme()
39
40 const blockInfo = useMemo(() => {
41 if (!moderation) return
42 const modui = moderation.ui('profileView')
43 const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
44 const listBlocks = blocks.filter(alert => alert.source.type === 'list')
45 const userBlock = blocks.find(alert => alert.source.type === 'user')
46 return {
47 listBlocks,
48 userBlock,
49 }
50 }, [moderation])
51
52 return (
53 <Layout.Header.Outer>
54 <View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}>
55 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
56 <Layout.Header.BackButton />
57 </View>
58 {profile && moderation && blockInfo ? (
59 <HeaderReady
60 profile={profile}
61 moderation={moderation}
62 blockInfo={blockInfo}
63 />
64 ) : (
65 <>
66 <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
67 <View
68 style={[
69 {width: PFP_SIZE, height: PFP_SIZE},
70 a.rounded_full,
71 t.atoms.bg_contrast_25,
72 ]}
73 />
74 <View style={a.gap_xs}>
75 <View
76 style={[
77 {width: 120, height: 16},
78 a.rounded_xs,
79 t.atoms.bg_contrast_25,
80 a.mt_xs,
81 ]}
82 />
83 <View
84 style={[
85 {width: 175, height: 12},
86 a.rounded_xs,
87 t.atoms.bg_contrast_25,
88 ]}
89 />
90 </View>
91 </View>
92
93 <Layout.Header.Slot />
94 </>
95 )}
96 </View>
97 </Layout.Header.Outer>
98 )
99}
100
101function HeaderReady({
102 profile,
103 moderation,
104 blockInfo,
105}: {
106 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
107 moderation: ModerationDecision
108 blockInfo: {
109 listBlocks: ModerationCause[]
110 userBlock?: ModerationCause
111 }
112}) {
113 const {_} = useLingui()
114 const t = useTheme()
115 const convoState = useConvo()
116 const verification = useSimpleVerificationState({
117 profile,
118 })
119
120 const isDeletedAccount = profile?.handle === 'missing.invalid'
121 const displayName = isDeletedAccount
122 ? _(msg`Deleted Account`)
123 : sanitizeDisplayName(
124 profile.displayName || profile.handle,
125 moderation.ui('displayName'),
126 )
127
128 // @ts-ignore findLast is polyfilled - esb
129 const latestMessageFromOther = convoState.items.findLast(
130 (item: ConvoItem) =>
131 item.type === 'message' && item.message.sender.did === profile.did,
132 )
133
134 const latestReportableMessage =
135 latestMessageFromOther?.type === 'message'
136 ? latestMessageFromOther.message
137 : undefined
138
139 return (
140 <View style={[a.flex_1]}>
141 <View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
142 <Link
143 label={_(msg`View ${displayName}'s profile`)}
144 style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
145 to={makeProfileLink(profile)}>
146 <PreviewableUserAvatar
147 size={PFP_SIZE}
148 profile={profile}
149 moderation={moderation.ui('avatar')}
150 disableHoverCard={moderation.blocked}
151 />
152 <View style={[a.flex_1]}>
153 <View style={[a.flex_row, a.align_center]}>
154 <Text
155 emoji
156 style={[
157 a.text_md,
158 a.font_semi_bold,
159 a.self_start,
160 web(a.leading_normal),
161 ]}
162 numberOfLines={1}>
163 {displayName}
164 </Text>
165 <View style={[a.pl_xs]}>
166 <PdsBadge did={profile.did} size="sm" />
167 </View>
168 {verification.showBadge && (
169 <View style={[a.pl_xs]}>
170 <VerificationCheck
171 width={14}
172 verifier={verification.role === 'verifier'}
173 />
174 </View>
175 )}
176 </View>
177 {!isDeletedAccount && (
178 <Text
179 style={[
180 t.atoms.text_contrast_medium,
181 a.text_xs,
182 web([a.leading_normal, {marginTop: -2}]),
183 ]}
184 numberOfLines={1}>
185 @{profile.handle}
186 {convoState.convo?.muted && (
187 <>
188 {' '}
189 ·{' '}
190 <BellStroke
191 size="xs"
192 style={t.atoms.text_contrast_medium}
193 />
194 </>
195 )}
196 </Text>
197 )}
198 </View>
199 </Link>
200
201 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
202 <Layout.Header.Slot>
203 {isConvoActive(convoState) && (
204 <ConvoMenu
205 convo={convoState.convo}
206 profile={profile}
207 currentScreen="conversation"
208 blockInfo={blockInfo}
209 latestReportableMessage={latestReportableMessage}
210 />
211 )}
212 </Layout.Header.Slot>
213 </View>
214 </View>
215
216 <View
217 style={[
218 {
219 paddingLeft: PFP_SIZE + a.gap_md.gap,
220 },
221 ]}>
222 <PostAlerts
223 modui={moderation.ui('contentList')}
224 size="lg"
225 style={[a.pt_xs]}
226 />
227 </View>
228 </View>
229 )
230}