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