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