forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useMemo} from 'react'
2import {View} from 'react-native'
3import {
4 ChatBskyConvoDefs,
5 moderateProfile,
6 type ModerationOpts,
7} from '@atproto/api'
8import {useLingui} from '@lingui/react/macro'
9import {useNavigation} from '@react-navigation/native'
10
11import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
12import {makeProfileLink} from '#/lib/routes/links'
13import {type NavigationProp} from '#/lib/routes/types'
14import {useProfileShadow} from '#/state/cache/profile-shadow'
15import {useModerationOpts} from '#/state/preferences/moderation-opts'
16import {useSession} from '#/state/session'
17import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
18import {atoms as a, useTheme} from '#/alf'
19import {AvatarBubbles} from '#/components/AvatarBubbles'
20import {Button, ButtonIcon} from '#/components/Button'
21import {ConvoMenu} from '#/components/dms/ConvoMenu'
22import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2'
23import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
24import * as Layout from '#/components/Layout'
25import {Link} from '#/components/Link'
26import {ProfileBadges} from '#/components/ProfileBadges'
27import {Text} from '#/components/Typography'
28import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
29import {type ConvoWithDetails} from './util'
30
31const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
32
33export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) {
34 const t = useTheme()
35 const moderationOpts = useModerationOpts()
36
37 return (
38 <Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}>
39 <View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}>
40 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
41 <Layout.Header.BackButton />
42 </View>
43 {convo && moderationOpts ? (
44 convo.kind === 'direct' ? (
45 <ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} />
46 ) : (
47 <GroupHeaderReady convo={convo} />
48 )
49 ) : (
50 <>
51 <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
52 <View
53 style={[
54 {width: PFP_SIZE, height: PFP_SIZE},
55 a.rounded_full,
56 t.atoms.bg_contrast_25,
57 ]}
58 />
59 <View style={a.gap_xs}>
60 <View
61 style={[
62 {width: 150, height: 16},
63 a.rounded_xs,
64 t.atoms.bg_contrast_25,
65 a.mt_xs,
66 ]}
67 />
68 </View>
69 </View>
70
71 <Layout.Header.Slot />
72 </>
73 )}
74 </View>
75 </Layout.Header.Outer>
76 )
77}
78
79function ProfileHeaderReady({
80 convo,
81 moderationOpts,
82}: {
83 convo: Extract<ConvoWithDetails, {kind: 'direct'}>
84 moderationOpts: ModerationOpts
85}) {
86 const {t: l} = useLingui()
87 const {currentAccount} = useSession()
88 const profile = useProfileShadow(convo.primaryMember)
89
90 const moderation = moderateProfile(profile, moderationOpts)
91
92 const blockInfo = useMemo(() => {
93 const modui = moderation.ui('profileView')
94 const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
95 const listBlocks = blocks.filter(alert => alert.source.type === 'list')
96 const userBlock = blocks.find(alert => alert.source.type === 'user')
97 return {
98 listBlocks,
99 userBlock,
100 }
101 }, [moderation])
102
103 const isDeletedAccount = profile?.handle === 'missing.invalid'
104 const displayName = isDeletedAccount
105 ? l`Deleted Account`
106 : createSanitizedDisplayName(profile, true, moderation.ui('displayName'))
107
108 const latestReportableMessage =
109 ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) &&
110 convo.view.lastMessage.sender?.did !== currentAccount?.did
111 ? convo.view.lastMessage
112 : undefined
113
114 return (
115 <Wrapper
116 heading={
117 <Link
118 label={l`View ${displayName}鈥檚 profile`}
119 style={[a.flex_row, a.gap_md, a.flex_1, a.pr_md]}
120 to={makeProfileLink(profile)}>
121 <PreviewableUserAvatar
122 size={PFP_SIZE}
123 profile={profile}
124 moderation={moderation.ui('avatar')}
125 disableHoverCard={moderation.blocked}
126 />
127 <View style={[a.flex_row, a.align_center, a.flex_1]}>
128 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
129 {displayName}
130 </Text>
131 <ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
132 </View>
133 </Link>
134 }
135 muted={convo.view.muted}
136 settings={
137 <ConvoMenu
138 convo={convo.view}
139 profile={profile}
140 currentScreen="conversation"
141 blockInfo={blockInfo}
142 latestReportableMessage={latestReportableMessage}
143 />
144 }
145 />
146 )
147}
148
149function GroupHeaderReady({
150 convo,
151}: {
152 convo: Extract<ConvoWithDetails, {kind: 'group'}>
153}) {
154 const {t: l} = useLingui()
155
156 const navigation = useNavigation<NavigationProp>()
157
158 const handleNavigateToSettings = () => {
159 navigation.navigate('MessagesConversationSettings', {
160 conversation: convo.view.id,
161 })
162 }
163
164 return (
165 <Wrapper
166 heading={
167 <>
168 <AvatarBubbles size="small" profiles={convo.members} />
169 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
170 {convo.details.name}
171 </Text>
172 </>
173 }
174 muted={convo.view.muted}
175 settings={
176 <Button
177 label={l`Open group chat settings`}
178 size="small"
179 color="secondary"
180 shape="round"
181 variant="ghost"
182 style={[a.bg_transparent]}
183 onPress={handleNavigateToSettings}>
184 <ButtonIcon icon={DotsHorizontalIcon} size="md" />
185 </Button>
186 }
187 />
188 )
189}
190
191function Wrapper({
192 heading,
193 muted,
194 settings,
195}: {
196 heading: React.ReactNode
197 muted: boolean
198 settings: React.ReactNode
199}) {
200 return (
201 <View style={[a.flex_1]}>
202 <View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
203 <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}>
204 {heading}
205 <MuteStatus muted={muted} />
206 </View>
207
208 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
209 <Layout.Header.Slot>{settings}</Layout.Header.Slot>
210 </View>
211 </View>
212 </View>
213 )
214}
215
216function MuteStatus({muted}: {muted: boolean}) {
217 const t = useTheme()
218
219 return muted ? (
220 <>
221 <Text style={[a.text_md, t.atoms.text_contrast_medium]}> · </Text>
222 <BellOffIcon size="sm" style={t.atoms.text_contrast_medium} />
223 </>
224 ) : undefined
225}