diff options
author | Eric Bailey <git@esb.lol> | 2023-11-16 10:40:31 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-16 08:40:31 -0800 |
commit | e6efeea7c07682c981998483bd49d7c01822911e (patch) | |
tree | 0da4d0b8ba03648fe8ceaef92d8d53ef4cc9fd9d | |
parent | 8a1fd160e6a1f9beeb735bb2320c12e5e71963d6 (diff) | |
download | voidsky-e6efeea7c07682c981998483bd49d7c01822911e.tar.zst |
Refactor invites modal (#1930)
* Refactor invites modal * Replace in drawer * Delete stuff from me model
-rw-r--r-- | src/state/models/me.ts | 44 | ||||
-rw-r--r-- | src/state/queries/invites.ts | 36 | ||||
-rw-r--r-- | src/view/com/modals/InviteCodes.tsx | 54 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 16 | ||||
-rw-r--r-- | src/view/shell/Drawer.tsx | 14 | ||||
-rw-r--r-- | src/view/shell/desktop/RightNav.tsx | 15 |
6 files changed, 103 insertions, 76 deletions
diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 586be4f42..7e7a48b51 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -1,8 +1,5 @@ import {makeAutoObservable, runInAction} from 'mobx' -import { - ComAtprotoServerDefs, - ComAtprotoServerListAppPasswords, -} from '@atproto/api' +import {ComAtprotoServerListAppPasswords} from '@atproto/api' import {RootStoreModel} from './root-store' import {isObj, hasProp} from 'lib/type-guards' import {logger} from '#/logger' @@ -17,14 +14,9 @@ export class MeModel { avatar: string = '' followsCount: number | undefined followersCount: number | undefined - invites: ComAtprotoServerDefs.InviteCode[] = [] appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = [] lastProfileStateUpdate = Date.now() - get invitesAvailable() { - return this.invites.filter(isInviteAvailable).length - } - constructor(public rootStore: RootStoreModel) { makeAutoObservable( this, @@ -41,7 +33,6 @@ export class MeModel { this.displayName = '' this.description = '' this.avatar = '' - this.invites = [] this.appPasswords = [] } @@ -90,7 +81,6 @@ export class MeModel { this.did = sess.currentSession?.did || '' await this.fetchProfile() this.rootStore.emitSessionLoaded() - await this.fetchInviteCodes() await this.fetchAppPasswords() } else { this.clear() @@ -102,7 +92,6 @@ export class MeModel { logger.debug('Updating me profile information') this.lastProfileStateUpdate = Date.now() await this.fetchProfile() - await this.fetchInviteCodes() await this.fetchAppPasswords() } } @@ -129,33 +118,6 @@ export class MeModel { }) } - async fetchInviteCodes() { - if (this.rootStore.session) { - try { - const res = - await this.rootStore.agent.com.atproto.server.getAccountInviteCodes( - {}, - ) - runInAction(() => { - this.invites = res.data.codes - this.invites.sort((a, b) => { - if (!isInviteAvailable(a)) { - return 1 - } - if (!isInviteAvailable(b)) { - return -1 - } - return 0 - }) - }) - } catch (e) { - logger.error('Failed to fetch user invite codes', { - error: e, - }) - } - } - } - async fetchAppPasswords() { if (this.rootStore.session) { try { @@ -208,7 +170,3 @@ export class MeModel { } } } - -function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { - return invite.available - invite.uses.length > 0 && !invite.disabled -} diff --git a/src/state/queries/invites.ts b/src/state/queries/invites.ts new file mode 100644 index 000000000..77494d273 --- /dev/null +++ b/src/state/queries/invites.ts @@ -0,0 +1,36 @@ +import {ComAtprotoServerDefs} from '@atproto/api' +import {useQuery} from '@tanstack/react-query' + +import {useSession} from '#/state/session' + +function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { + return invite.available - invite.uses.length > 0 && !invite.disabled +} + +export type InviteCodesQueryResponse = Exclude< + ReturnType<typeof useInviteCodesQuery>['data'], + undefined +> +export function useInviteCodesQuery() { + const {agent} = useSession() + + return useQuery({ + queryKey: ['inviteCodes'], + queryFn: async () => { + const res = await agent.com.atproto.server.getAccountInviteCodes({}) + + if (!res.data?.codes) { + throw new Error(`useInviteCodesQuery: no codes returned`) + } + + const available = res.data.codes.filter(isInviteAvailable) + const used = res.data.codes.filter(code => !isInviteAvailable(code)) + + return { + all: [...available, ...used], + available, + used, + } + }, + }) +} diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index a90a9eab6..973c7c3a7 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -1,5 +1,10 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' +import { + StyleSheet, + TouchableOpacity, + View, + ActivityIndicator, +} from 'react-native' import {observer} from 'mobx-react-lite' import {ComAtprotoServerDefs} from '@atproto/api' import { @@ -10,23 +15,41 @@ 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() @@ -34,7 +57,7 @@ export function Component({}: {}) { 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]}> @@ -74,12 +97,21 @@ export function Component({}: {}) { </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} + invite={invite} + invites={invites} + /> + ))} + {invites.used.map((invite, i) => ( <InviteCode + used testID={`inviteCode-${i}`} key={invite.code} invite={invite} - used={invite.available - invite.uses.length <= 0 || invite.disabled} + invites={invites} /> ))} </ScrollView> @@ -101,14 +133,14 @@ const InviteCode = observer(function InviteCodeImpl({ testID, invite, used, + invites, }: { testID: 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() @@ -130,9 +162,9 @@ const InviteCode = observer(function InviteCodeImpl({ onPress={onPress} accessibilityRole="button" accessibilityLabel={ - invitesAvailable === 1 + invites.available.length === 1 ? 'Invite codes: 1 available' - : `Invite codes: ${invitesAvailable} available` + : `Invite codes: ${invites.available.length} available` } accessibilityHint="Opens list of invite codes"> <Text diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index baad2227b..3f7ef146a 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -60,6 +60,7 @@ import { import {useSession, useSessionApi, SessionAccount} from '#/state/session' import {useProfileQuery} from '#/state/queries/profile' import {useClearPreferencesMutation} from '#/state/queries/preferences' +import {useInviteCodesQuery} from '#/state/queries/invites' // TEMPORARY (APP-700) // remove after backend testing finishes @@ -155,6 +156,8 @@ export const SettingsScreen = withAuthRequired( const {isSwitchingAccounts, accounts, currentAccount} = useSession() const {clearCurrentAccount} = useSessionApi() const {mutate: clearPreferences} = useClearPreferencesMutation() + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 const primaryBg = useCustomPalette<ViewStyle>({ light: {backgroundColor: colors.blue0}, @@ -362,6 +365,7 @@ export const SettingsScreen = withAuthRequired( <Text type="xl-bold" style={[pal.text, styles.heading]}> <Trans>Invite a Friend</Trans> </Text> + <TouchableOpacity testID="inviteFriendBtn" style={[ @@ -376,22 +380,20 @@ export const SettingsScreen = withAuthRequired( <View style={[ styles.iconContainer, - store.me.invitesAvailable > 0 ? primaryBg : pal.btn, + invitesAvailable > 0 ? primaryBg : pal.btn, ]}> <FontAwesomeIcon icon="ticket" style={ - (store.me.invitesAvailable > 0 + (invitesAvailable > 0 ? primaryText : pal.text) as FontAwesomeIconStyle } /> </View> - <Text - type="lg" - style={store.me.invitesAvailable > 0 ? pal.link : pal.text}> - {formatCount(store.me.invitesAvailable)} invite{' '} - {pluralize(store.me.invitesAvailable, 'code')} available + <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}> + {formatCount(invitesAvailable)} invite{' '} + {pluralize(invitesAvailable, 'code')} available </Text> </TouchableOpacity> diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index c5dcb150c..1ee359be0 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -17,7 +17,6 @@ import { } from '@fortawesome/react-native-fontawesome' import {s, colors} from 'lib/styles' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' -import {useStores} from 'state/index' import { HomeIcon, HomeIconSolid, @@ -51,6 +50,7 @@ import {useSession, SessionAccount} from '#/state/session' import {useProfileQuery} from '#/state/queries/profile' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {emitSoftReset} from '#/state/events' +import {useInviteCodesQuery} from '#/state/queries/invites' export function DrawerProfileCard({ account, @@ -464,10 +464,10 @@ const InviteCodes = observer(function InviteCodesImpl({ style?: StyleProp<ViewStyle> }) { const {track} = useAnalytics() - const store = useStores() const setDrawerOpen = useSetDrawerOpen() const pal = usePalette('default') - const {invitesAvailable} = store.me + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 const {openModal} = useModalControls() const onPress = React.useCallback(() => { track('Menu:ItemClicked', {url: '#invite-codes'}) @@ -490,15 +490,15 @@ const InviteCodes = observer(function InviteCodesImpl({ icon="ticket" style={[ styles.inviteCodesIcon, - store.me.invitesAvailable > 0 ? pal.link : pal.textLight, + invitesAvailable > 0 ? pal.link : pal.textLight, ]} size={18} /> <Text type="lg-medium" - style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}> - {formatCount(store.me.invitesAvailable)} invite{' '} - {pluralize(store.me.invitesAvailable, 'code')} + style={invitesAvailable > 0 ? pal.link : pal.textLight}> + {formatCount(invitesAvailable)} invite{' '} + {pluralize(invitesAvailable, 'code')} </Text> </TouchableOpacity> ) diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 98f54c7ed..9e17cdcd9 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -9,12 +9,12 @@ import {Text} from 'view/com/util/text/Text' import {TextLink} from 'view/com/util/Link' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' import {s} from 'lib/styles' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {pluralize} from 'lib/strings/helpers' import {formatCount} from 'view/com/util/numeric/format' import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' +import {useInviteCodesQuery} from '#/state/queries/invites' export const DesktopRightNav = observer(function DesktopRightNavImpl() { const pal = usePalette('default') @@ -83,11 +83,10 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { }) const InviteCodes = observer(function InviteCodesImpl() { - const store = useStores() const pal = usePalette('default') const {openModal} = useModalControls() - - const {invitesAvailable} = store.me + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 const onPress = React.useCallback(() => { openModal({name: 'invite-codes'}) @@ -107,15 +106,15 @@ const InviteCodes = observer(function InviteCodesImpl() { icon="ticket" style={[ styles.inviteCodesIcon, - store.me.invitesAvailable > 0 ? pal.link : pal.textLight, + invitesAvailable > 0 ? pal.link : pal.textLight, ]} size={16} /> <Text type="md-medium" - style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}> - {formatCount(store.me.invitesAvailable)} invite{' '} - {pluralize(store.me.invitesAvailable, 'code')} available + style={invitesAvailable > 0 ? pal.link : pal.textLight}> + {formatCount(invitesAvailable)} invite{' '} + {pluralize(invitesAvailable, 'code')} available </Text> </TouchableOpacity> ) |