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