diff options
Diffstat (limited to 'src/view/com/modals/InviteCodes.tsx')
-rw-r--r-- | src/view/com/modals/InviteCodes.tsx | 183 |
1 files changed, 128 insertions, 55 deletions
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index 09cfd4de7..82a826aca 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -1,6 +1,11 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' +import { + StyleSheet, + TouchableOpacity, + View, + ActivityIndicator, +} from 'react-native' +import {ComAtprotoServerDefs} from '@atproto/api' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -9,30 +14,57 @@ import Clipboard from '@react-native-clipboard/clipboard' import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import * as Toast from '../util/Toast' -import {useStores} from 'state/index' import {ScrollView} from './util' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {Trans} from '@lingui/macro' +import {cleanError} from 'lib/strings/errors' +import {useModalControls} from '#/state/modals' +import {useInvitesState, useInvitesAPI} from '#/state/invites' +import {UserInfoText} from '../util/UserInfoText' +import {makeProfileLink} from '#/lib/routes/links' +import {Link} from '../util/Link' +import {ErrorMessage} from '../util/error/ErrorMessage' +import { + useInviteCodesQuery, + InviteCodesQueryResponse, +} from '#/state/queries/invites' export const snapPoints = ['70%'] -export function Component({}: {}) { +export function Component() { + const {isLoading, data: invites, error} = useInviteCodesQuery() + + return error ? ( + <ErrorMessage message={cleanError(error)} /> + ) : isLoading || !invites ? ( + <View style={{padding: 18}}> + <ActivityIndicator /> + </View> + ) : ( + <Inner invites={invites} /> + ) +} + +export function Inner({invites}: {invites: InviteCodesQueryResponse}) { const pal = usePalette('default') - const store = useStores() + const {closeModal} = useModalControls() const {isTabletOrDesktop} = useWebMediaQueries() const onClose = React.useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) - if (store.me.invites.length === 0) { + if (invites.all.length === 0) { return ( <View style={[styles.container, pal.view]} testID="inviteCodesModal"> <View style={[styles.empty, pal.viewLight]}> <Text type="lg" style={[pal.text, styles.emptyText]}> - You don't have any invite codes yet! We'll send you some when you've - been on Bluesky for a little longer. + <Trans> + You don't have any invite codes yet! We'll send you some when + you've been on Bluesky for a little longer. + </Trans> </Text> </View> <View style={styles.flex1} /> @@ -56,18 +88,29 @@ export function Component({}: {}) { return ( <View style={[styles.container, pal.view]} testID="inviteCodesModal"> <Text type="title-xl" style={[styles.title, pal.text]}> - Invite a Friend + <Trans>Invite a Friend</Trans> </Text> <Text type="lg" style={[styles.description, pal.text]}> - Each code works once. You'll receive more invite codes periodically. + <Trans> + Each code works once. You'll receive more invite codes periodically. + </Trans> </Text> <ScrollView style={[styles.scrollContainer, pal.border]}> - {store.me.invites.map((invite, i) => ( + {invites.available.map((invite, i) => ( <InviteCode testID={`inviteCode-${i}`} key={invite.code} - code={invite.code} - used={invite.available - invite.uses.length <= 0 || invite.disabled} + invite={invite} + invites={invites} + /> + ))} + {invites.used.map((invite, i) => ( + <InviteCode + used + testID={`inviteCode-${i}`} + key={invite.code} + invite={invite} + invites={invites} /> ))} </ScrollView> @@ -85,56 +128,89 @@ export function Component({}: {}) { ) } -const InviteCode = observer(function InviteCodeImpl({ +function InviteCode({ testID, - code, + invite, used, + invites, }: { testID: string - code: string + invite: ComAtprotoServerDefs.InviteCode used?: boolean + invites: InviteCodesQueryResponse }) { const pal = usePalette('default') - const store = useStores() - const {invitesAvailable} = store.me + const invitesState = useInvitesState() + const {setInviteCopied} = useInvitesAPI() const onPress = React.useCallback(() => { - Clipboard.setString(code) + Clipboard.setString(invite.code) Toast.show('Copied to clipboard') - store.invitedUsers.setInviteCopied(code) - }, [store, code]) + setInviteCopied(invite.code) + }, [setInviteCopied, invite]) return ( - <TouchableOpacity - testID={testID} - style={[styles.inviteCode, pal.border]} - onPress={onPress} - accessibilityRole="button" - accessibilityLabel={ - invitesAvailable === 1 - ? 'Invite codes: 1 available' - : `Invite codes: ${invitesAvailable} available` - } - accessibilityHint="Opens list of invite codes"> - <Text - testID={`${testID}-code`} - type={used ? 'md' : 'md-bold'} - style={used ? [pal.textLight, styles.strikeThrough] : pal.text}> - {code} - </Text> - <View style={styles.flex1} /> - {!used && store.invitedUsers.isInviteCopied(code) && ( - <Text style={[pal.textLight, styles.codeCopied]}>Copied</Text> - )} - {!used && ( - <FontAwesomeIcon - icon={['far', 'clone']} - style={pal.text as FontAwesomeIconStyle} - /> - )} - </TouchableOpacity> + <View + style={[ + pal.border, + {borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14}, + ]}> + <TouchableOpacity + testID={testID} + style={[styles.inviteCode]} + onPress={onPress} + accessibilityRole="button" + accessibilityLabel={ + invites.available.length === 1 + ? 'Invite codes: 1 available' + : `Invite codes: ${invites.available.length} available` + } + accessibilityHint="Opens list of invite codes"> + <Text + testID={`${testID}-code`} + type={used ? 'md' : 'md-bold'} + style={used ? [pal.textLight, styles.strikeThrough] : pal.text}> + {invite.code} + </Text> + <View style={styles.flex1} /> + {!used && invitesState.copiedInvites.includes(invite.code) && ( + <Text style={[pal.textLight, styles.codeCopied]}> + <Trans>Copied</Trans> + </Text> + )} + {!used && ( + <FontAwesomeIcon + icon={['far', 'clone']} + style={pal.text as FontAwesomeIconStyle} + /> + )} + </TouchableOpacity> + {invite.uses.length > 0 ? ( + <View + style={{ + flexDirection: 'column', + gap: 8, + paddingTop: 6, + }}> + <Text style={pal.text}> + <Trans>Used by:</Trans> + </Text> + {invite.uses.map(use => ( + <Link + key={use.usedBy} + href={makeProfileLink({handle: use.usedBy, did: ''})} + style={{ + flexDirection: 'row', + }}> + <Text style={pal.text}>• </Text> + <UserInfoText did={use.usedBy} style={pal.link} /> + </Link> + ))} + </View> + ) : null} + </View> ) -}) +} const styles = StyleSheet.create({ container: { @@ -176,9 +252,6 @@ const styles = StyleSheet.create({ inviteCode: { flexDirection: 'row', alignItems: 'center', - borderBottomWidth: 1, - paddingHorizontal: 20, - paddingVertical: 14, }, codeCopied: { marginRight: 8, |