forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {ScrollView, View} from 'react-native'
2import {moderateProfile, type ModerationOpts} from '@atproto/api'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import {useNavigation} from '@react-navigation/native'
6
7import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
8import {type NavigationProp} from '#/lib/routes/types'
9import {sanitizeDisplayName} from '#/lib/strings/display-names'
10import {sanitizeHandle} from '#/lib/strings/handles'
11import {useProfileShadow} from '#/state/cache/profile-shadow'
12import {useModerationOpts} from '#/state/preferences/moderation-opts'
13import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
14import {useSession} from '#/state/session'
15import {UserAvatar} from '#/view/com/util/UserAvatar'
16import {atoms as a, tokens, useTheme} from '#/alf'
17import {Button} from '#/components/Button'
18import {useDialogContext} from '#/components/Dialog'
19import {Text} from '#/components/Typography'
20import {useSimpleVerificationState} from '#/components/verification'
21import {VerificationCheck} from '#/components/verification/VerificationCheck'
22import {useAnalytics} from '#/analytics'
23import type * as bsky from '#/types/bsky'
24
25export function RecentChats({postUri}: {postUri: string}) {
26 const ax = useAnalytics()
27 const control = useDialogContext()
28 const {currentAccount} = useSession()
29 const {data} = useListConvosQuery({status: 'accepted'})
30 const convos = data?.pages[0]?.convos?.slice(0, 10)
31 const moderationOpts = useModerationOpts()
32 const navigation = useNavigation<NavigationProp>()
33
34 const onSelectChat = (convoId: string) => {
35 control.close(() => {
36 ax.metric('share:press:recentDm', {})
37 navigation.navigate('MessagesConversation', {
38 conversation: convoId,
39 embed: postUri,
40 })
41 })
42 }
43
44 if (!moderationOpts) return null
45
46 return (
47 <View
48 style={[a.relative, a.flex_1, {marginHorizontal: tokens.space.md * -1}]}>
49 <ScrollView
50 horizontal
51 style={[a.flex_1, a.pt_2xs, {minHeight: 98}]}
52 contentContainerStyle={[a.gap_sm, a.px_md]}
53 showsHorizontalScrollIndicator={false}
54 nestedScrollEnabled>
55 {convos && convos.length > 0 ? (
56 convos.map(convo => {
57 const otherMember = convo.members.find(
58 member => member.did !== currentAccount?.did,
59 )
60
61 if (
62 !otherMember ||
63 otherMember.handle === 'missing.invalid' ||
64 convo.muted
65 )
66 return null
67
68 return (
69 <RecentChatItem
70 key={convo.id}
71 profile={otherMember}
72 onPress={() => onSelectChat(convo.id)}
73 moderationOpts={moderationOpts}
74 />
75 )
76 })
77 ) : (
78 <>
79 <ConvoSkeleton />
80 <ConvoSkeleton />
81 <ConvoSkeleton />
82 <ConvoSkeleton />
83 <ConvoSkeleton />
84 </>
85 )}
86 </ScrollView>
87 {convos && convos.length === 0 && <NoConvos />}
88 </View>
89 )
90}
91
92const WIDTH = 80
93
94function RecentChatItem({
95 profile: profileUnshadowed,
96 onPress,
97 moderationOpts,
98}: {
99 profile: bsky.profile.AnyProfileView
100 onPress: () => void
101 moderationOpts: ModerationOpts
102}) {
103 const {_} = useLingui()
104 const t = useTheme()
105
106 const profile = useProfileShadow(profileUnshadowed)
107
108 const moderation = moderateProfile(profile, moderationOpts)
109 const name = sanitizeDisplayName(
110 profile.displayName || sanitizeHandle(profile.handle),
111 moderation.ui('displayName'),
112 )
113 const verification = useSimpleVerificationState({profile})
114
115 if (isBlockedOrBlocking(profile) || isMuted(profile)) {
116 return null
117 }
118
119 return (
120 <Button
121 onPress={onPress}
122 label={_(msg`Send post to ${name}`)}
123 style={[
124 a.flex_col,
125 {width: WIDTH},
126 a.gap_sm,
127 a.justify_start,
128 a.align_center,
129 ]}>
130 <UserAvatar
131 avatar={profile.avatar}
132 size={WIDTH - 8}
133 type={profile.associated?.labeler ? 'labeler' : 'user'}
134 moderation={moderation.ui('avatar')}
135 />
136 <View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}>
137 <Text
138 emoji
139 style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}
140 numberOfLines={1}>
141 {name}
142 </Text>
143 {verification.showBadge && (
144 <View style={[a.pl_2xs]}>
145 <VerificationCheck
146 width={10}
147 verifier={verification.role === 'verifier'}
148 />
149 </View>
150 )}
151 </View>
152 </Button>
153 )
154}
155
156function ConvoSkeleton() {
157 const t = useTheme()
158 return (
159 <View
160 style={[
161 a.flex_col,
162 {width: WIDTH, height: WIDTH + 15},
163 a.gap_xs,
164 a.justify_start,
165 a.align_center,
166 ]}>
167 <View
168 style={[
169 t.atoms.bg_contrast_50,
170 {width: WIDTH - 8, height: WIDTH - 8},
171 a.rounded_full,
172 ]}
173 />
174 <View
175 style={[
176 t.atoms.bg_contrast_50,
177 {width: WIDTH - 8, height: 10},
178 a.rounded_xs,
179 ]}
180 />
181 </View>
182 )
183}
184
185function NoConvos() {
186 const t = useTheme()
187
188 return (
189 <View
190 style={[
191 a.absolute,
192 a.inset_0,
193 a.justify_center,
194 a.align_center,
195 a.px_2xl,
196 ]}>
197 <View
198 style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]}
199 />
200 <Text
201 style={[
202 a.text_sm,
203 t.atoms.text_contrast_high,
204 a.text_center,
205 a.font_semi_bold,
206 ]}>
207 <Trans>Start a conversation, and it will appear here.</Trans>
208 </Text>
209 </View>
210 )
211}