mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 ActivityIndicator,
4 StyleSheet,
5 TouchableOpacity,
6 View,
7} from 'react-native'
8import {setStringAsync} from 'expo-clipboard'
9import {ComAtprotoServerDefs} from '@atproto/api'
10import {
11 FontAwesomeIcon,
12 FontAwesomeIconStyle,
13} from '@fortawesome/react-native-fontawesome'
14import {msg, Trans} from '@lingui/macro'
15import {useLingui} from '@lingui/react'
16
17import {makeProfileLink} from '#/lib/routes/links'
18import {useInvitesAPI, useInvitesState} from '#/state/invites'
19import {useModalControls} from '#/state/modals'
20import {
21 InviteCodesQueryResponse,
22 useInviteCodesQuery,
23} from '#/state/queries/invites'
24import {usePalette} from 'lib/hooks/usePalette'
25import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
26import {cleanError} from 'lib/strings/errors'
27import {isWeb} from 'platform/detection'
28import {ErrorMessage} from '../util/error/ErrorMessage'
29import {Button} from '../util/forms/Button'
30import {Link} from '../util/Link'
31import {Text} from '../util/text/Text'
32import * as Toast from '../util/Toast'
33import {UserInfoText} from '../util/UserInfoText'
34import {ScrollView} from './util'
35
36export const snapPoints = ['70%']
37
38export function Component() {
39 const {isLoading, data: invites, error} = useInviteCodesQuery()
40
41 return error ? (
42 <ErrorMessage message={cleanError(error)} />
43 ) : isLoading || !invites ? (
44 <View style={{padding: 18}}>
45 <ActivityIndicator />
46 </View>
47 ) : (
48 <Inner invites={invites} />
49 )
50}
51
52export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
53 const pal = usePalette('default')
54 const {_} = useLingui()
55 const {closeModal} = useModalControls()
56 const {isTabletOrDesktop} = useWebMediaQueries()
57
58 const onClose = React.useCallback(() => {
59 closeModal()
60 }, [closeModal])
61
62 if (invites.all.length === 0) {
63 return (
64 <View style={[styles.container, pal.view]} testID="inviteCodesModal">
65 <View style={[styles.empty, pal.viewLight]}>
66 <Text type="lg" style={[pal.text, styles.emptyText]}>
67 <Trans>
68 You don't have any invite codes yet! We'll send you some when
69 you've been on Bluesky for a little longer.
70 </Trans>
71 </Text>
72 </View>
73 <View style={styles.flex1} />
74 <View
75 style={[
76 styles.btnContainer,
77 isTabletOrDesktop && styles.btnContainerDesktop,
78 ]}>
79 <Button
80 type="primary"
81 label={_(msg`Done`)}
82 style={styles.btn}
83 labelStyle={styles.btnLabel}
84 onPress={onClose}
85 />
86 </View>
87 </View>
88 )
89 }
90
91 return (
92 <View style={[styles.container, pal.view]} testID="inviteCodesModal">
93 <Text type="title-xl" style={[styles.title, pal.text]}>
94 <Trans>Invite a Friend</Trans>
95 </Text>
96 <Text type="lg" style={[styles.description, pal.text]}>
97 <Trans>
98 Each code works once. You'll receive more invite codes periodically.
99 </Trans>
100 </Text>
101 <ScrollView style={[styles.scrollContainer, pal.border]}>
102 {invites.available.map((invite, i) => (
103 <InviteCode
104 testID={`inviteCode-${i}`}
105 key={invite.code}
106 invite={invite}
107 invites={invites}
108 />
109 ))}
110 {invites.used.map((invite, i) => (
111 <InviteCode
112 used
113 testID={`inviteCode-${i}`}
114 key={invite.code}
115 invite={invite}
116 invites={invites}
117 />
118 ))}
119 </ScrollView>
120 <View style={styles.btnContainer}>
121 <Button
122 testID="closeBtn"
123 type="primary"
124 label={_(msg`Done`)}
125 style={styles.btn}
126 labelStyle={styles.btnLabel}
127 onPress={onClose}
128 />
129 </View>
130 </View>
131 )
132}
133
134function InviteCode({
135 testID,
136 invite,
137 used,
138 invites,
139}: {
140 testID: string
141 invite: ComAtprotoServerDefs.InviteCode
142 used?: boolean
143 invites: InviteCodesQueryResponse
144}) {
145 const pal = usePalette('default')
146 const {_} = useLingui()
147 const invitesState = useInvitesState()
148 const {setInviteCopied} = useInvitesAPI()
149 const uses = invite.uses
150
151 const onPress = React.useCallback(() => {
152 setStringAsync(invite.code)
153 Toast.show(_(msg`Copied to clipboard`))
154 setInviteCopied(invite.code)
155 }, [setInviteCopied, invite, _])
156
157 return (
158 <View
159 style={[
160 pal.border,
161 {borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14},
162 ]}>
163 <TouchableOpacity
164 testID={testID}
165 style={[styles.inviteCode]}
166 onPress={onPress}
167 accessibilityRole="button"
168 accessibilityLabel={
169 invites.available.length === 1
170 ? _(msg`Invite codes: 1 available`)
171 : _(msg`Invite codes: ${invites.available.length} available`)
172 }
173 accessibilityHint={_(msg`Opens list of invite codes`)}>
174 <Text
175 testID={`${testID}-code`}
176 type={used ? 'md' : 'md-bold'}
177 style={used ? [pal.textLight, styles.strikeThrough] : pal.text}>
178 {invite.code}
179 </Text>
180 <View style={styles.flex1} />
181 {!used && invitesState.copiedInvites.includes(invite.code) && (
182 <Text style={[pal.textLight, styles.codeCopied]}>
183 <Trans>Copied</Trans>
184 </Text>
185 )}
186 {!used && (
187 <FontAwesomeIcon
188 icon={['far', 'clone']}
189 style={pal.text as FontAwesomeIconStyle}
190 />
191 )}
192 </TouchableOpacity>
193 {uses.length > 0 ? (
194 <View
195 style={{
196 flexDirection: 'column',
197 gap: 8,
198 paddingTop: 6,
199 }}>
200 <Text style={pal.text}>
201 <Trans>Used by:</Trans>{' '}
202 {uses.map((use, i) => (
203 <Link
204 key={use.usedBy}
205 href={makeProfileLink({handle: use.usedBy, did: ''})}
206 style={{
207 flexDirection: 'row',
208 }}>
209 <UserInfoText did={use.usedBy} style={pal.link} />
210 {i !== uses.length - 1 && <Text style={pal.text}>, </Text>}
211 </Link>
212 ))}
213 </Text>
214 </View>
215 ) : null}
216 </View>
217 )
218}
219
220const styles = StyleSheet.create({
221 container: {
222 flex: 1,
223 paddingBottom: isWeb ? 0 : 50,
224 },
225 title: {
226 textAlign: 'center',
227 marginTop: 12,
228 marginBottom: 12,
229 },
230 description: {
231 textAlign: 'center',
232 paddingHorizontal: 42,
233 marginBottom: 14,
234 },
235
236 scrollContainer: {
237 flex: 1,
238 borderTopWidth: 1,
239 marginTop: 4,
240 marginBottom: 16,
241 },
242
243 flex1: {
244 flex: 1,
245 },
246 empty: {
247 paddingHorizontal: 20,
248 paddingVertical: 20,
249 borderRadius: 16,
250 marginHorizontal: 24,
251 marginTop: 10,
252 },
253 emptyText: {
254 textAlign: 'center',
255 },
256
257 inviteCode: {
258 flexDirection: 'row',
259 alignItems: 'center',
260 },
261 codeCopied: {
262 marginRight: 8,
263 },
264 strikeThrough: {
265 textDecorationLine: 'line-through',
266 textDecorationStyle: 'solid',
267 },
268
269 btnContainer: {
270 flexDirection: 'row',
271 justifyContent: 'center',
272 },
273 btnContainerDesktop: {
274 marginTop: 14,
275 },
276 btn: {
277 flexDirection: 'row',
278 alignItems: 'center',
279 justifyContent: 'center',
280 borderRadius: 32,
281 paddingHorizontal: 60,
282 paddingVertical: 14,
283 },
284 btnLabel: {
285 fontSize: 18,
286 },
287})