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