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