Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor invites modal (#1930)

* Refactor invites modal

* Replace in drawer

* Delete stuff from me model

authored by

Eric Bailey and committed by
GitHub
e6efeea7 8a1fd160

+103 -76
+1 -43
src/state/models/me.ts
··· 1 1 import {makeAutoObservable, runInAction} from 'mobx' 2 - import { 3 - ComAtprotoServerDefs, 4 - ComAtprotoServerListAppPasswords, 5 - } from '@atproto/api' 2 + import {ComAtprotoServerListAppPasswords} from '@atproto/api' 6 3 import {RootStoreModel} from './root-store' 7 4 import {isObj, hasProp} from 'lib/type-guards' 8 5 import {logger} from '#/logger' ··· 17 14 avatar: string = '' 18 15 followsCount: number | undefined 19 16 followersCount: number | undefined 20 - invites: ComAtprotoServerDefs.InviteCode[] = [] 21 17 appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = [] 22 18 lastProfileStateUpdate = Date.now() 23 19 24 - get invitesAvailable() { 25 - return this.invites.filter(isInviteAvailable).length 26 - } 27 - 28 20 constructor(public rootStore: RootStoreModel) { 29 21 makeAutoObservable( 30 22 this, ··· 41 33 this.displayName = '' 42 34 this.description = '' 43 35 this.avatar = '' 44 - this.invites = [] 45 36 this.appPasswords = [] 46 37 } 47 38 ··· 90 81 this.did = sess.currentSession?.did || '' 91 82 await this.fetchProfile() 92 83 this.rootStore.emitSessionLoaded() 93 - await this.fetchInviteCodes() 94 84 await this.fetchAppPasswords() 95 85 } else { 96 86 this.clear() ··· 102 92 logger.debug('Updating me profile information') 103 93 this.lastProfileStateUpdate = Date.now() 104 94 await this.fetchProfile() 105 - await this.fetchInviteCodes() 106 95 await this.fetchAppPasswords() 107 96 } 108 97 } ··· 129 118 }) 130 119 } 131 120 132 - async fetchInviteCodes() { 133 - if (this.rootStore.session) { 134 - try { 135 - const res = 136 - await this.rootStore.agent.com.atproto.server.getAccountInviteCodes( 137 - {}, 138 - ) 139 - runInAction(() => { 140 - this.invites = res.data.codes 141 - this.invites.sort((a, b) => { 142 - if (!isInviteAvailable(a)) { 143 - return 1 144 - } 145 - if (!isInviteAvailable(b)) { 146 - return -1 147 - } 148 - return 0 149 - }) 150 - }) 151 - } catch (e) { 152 - logger.error('Failed to fetch user invite codes', { 153 - error: e, 154 - }) 155 - } 156 - } 157 - } 158 - 159 121 async fetchAppPasswords() { 160 122 if (this.rootStore.session) { 161 123 try { ··· 208 170 } 209 171 } 210 172 } 211 - 212 - function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { 213 - return invite.available - invite.uses.length > 0 && !invite.disabled 214 - }
+36
src/state/queries/invites.ts
··· 1 + import {ComAtprotoServerDefs} from '@atproto/api' 2 + import {useQuery} from '@tanstack/react-query' 3 + 4 + import {useSession} from '#/state/session' 5 + 6 + function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { 7 + return invite.available - invite.uses.length > 0 && !invite.disabled 8 + } 9 + 10 + export type InviteCodesQueryResponse = Exclude< 11 + ReturnType<typeof useInviteCodesQuery>['data'], 12 + undefined 13 + > 14 + export function useInviteCodesQuery() { 15 + const {agent} = useSession() 16 + 17 + return useQuery({ 18 + queryKey: ['inviteCodes'], 19 + queryFn: async () => { 20 + const res = await agent.com.atproto.server.getAccountInviteCodes({}) 21 + 22 + if (!res.data?.codes) { 23 + throw new Error(`useInviteCodesQuery: no codes returned`) 24 + } 25 + 26 + const available = res.data.codes.filter(isInviteAvailable) 27 + const used = res.data.codes.filter(code => !isInviteAvailable(code)) 28 + 29 + return { 30 + all: [...available, ...used], 31 + available, 32 + used, 33 + } 34 + }, 35 + }) 36 + }
+43 -11
src/view/com/modals/InviteCodes.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 2 + import { 3 + StyleSheet, 4 + TouchableOpacity, 5 + View, 6 + ActivityIndicator, 7 + } from 'react-native' 3 8 import {observer} from 'mobx-react-lite' 4 9 import {ComAtprotoServerDefs} from '@atproto/api' 5 10 import { ··· 10 15 import {Text} from '../util/text/Text' 11 16 import {Button} from '../util/forms/Button' 12 17 import * as Toast from '../util/Toast' 13 - import {useStores} from 'state/index' 14 18 import {ScrollView} from './util' 15 19 import {usePalette} from 'lib/hooks/usePalette' 16 20 import {isWeb} from 'platform/detection' 17 21 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 18 22 import {Trans} from '@lingui/macro' 23 + import {cleanError} from 'lib/strings/errors' 19 24 import {useModalControls} from '#/state/modals' 20 25 import {useInvitesState, useInvitesAPI} from '#/state/invites' 21 26 import {UserInfoText} from '../util/UserInfoText' 22 27 import {makeProfileLink} from '#/lib/routes/links' 23 28 import {Link} from '../util/Link' 29 + import {ErrorMessage} from '../util/error/ErrorMessage' 30 + import { 31 + useInviteCodesQuery, 32 + InviteCodesQueryResponse, 33 + } from '#/state/queries/invites' 24 34 25 35 export const snapPoints = ['70%'] 26 36 27 - export function Component({}: {}) { 37 + export function Component() { 38 + const {isLoading, data: invites, error} = useInviteCodesQuery() 39 + 40 + return error ? ( 41 + <ErrorMessage message={cleanError(error)} /> 42 + ) : isLoading || !invites ? ( 43 + <View style={{padding: 18}}> 44 + <ActivityIndicator /> 45 + </View> 46 + ) : ( 47 + <Inner invites={invites} /> 48 + ) 49 + } 50 + 51 + export function Inner({invites}: {invites: InviteCodesQueryResponse}) { 28 52 const pal = usePalette('default') 29 - const store = useStores() 30 53 const {closeModal} = useModalControls() 31 54 const {isTabletOrDesktop} = useWebMediaQueries() 32 55 ··· 34 57 closeModal() 35 58 }, [closeModal]) 36 59 37 - if (store.me.invites.length === 0) { 60 + if (invites.all.length === 0) { 38 61 return ( 39 62 <View style={[styles.container, pal.view]} testID="inviteCodesModal"> 40 63 <View style={[styles.empty, pal.viewLight]}> ··· 74 97 </Trans> 75 98 </Text> 76 99 <ScrollView style={[styles.scrollContainer, pal.border]}> 77 - {store.me.invites.map((invite, i) => ( 100 + {invites.available.map((invite, i) => ( 101 + <InviteCode 102 + testID={`inviteCode-${i}`} 103 + key={invite.code} 104 + invite={invite} 105 + invites={invites} 106 + /> 107 + ))} 108 + {invites.used.map((invite, i) => ( 78 109 <InviteCode 110 + used 79 111 testID={`inviteCode-${i}`} 80 112 key={invite.code} 81 113 invite={invite} 82 - used={invite.available - invite.uses.length <= 0 || invite.disabled} 114 + invites={invites} 83 115 /> 84 116 ))} 85 117 </ScrollView> ··· 101 133 testID, 102 134 invite, 103 135 used, 136 + invites, 104 137 }: { 105 138 testID: string 106 139 invite: ComAtprotoServerDefs.InviteCode 107 140 used?: boolean 141 + invites: InviteCodesQueryResponse 108 142 }) { 109 143 const pal = usePalette('default') 110 - const store = useStores() 111 - const {invitesAvailable} = store.me 112 144 const invitesState = useInvitesState() 113 145 const {setInviteCopied} = useInvitesAPI() 114 146 ··· 130 162 onPress={onPress} 131 163 accessibilityRole="button" 132 164 accessibilityLabel={ 133 - invitesAvailable === 1 165 + invites.available.length === 1 134 166 ? 'Invite codes: 1 available' 135 - : `Invite codes: ${invitesAvailable} available` 167 + : `Invite codes: ${invites.available.length} available` 136 168 } 137 169 accessibilityHint="Opens list of invite codes"> 138 170 <Text
+9 -7
src/view/screens/Settings.tsx
··· 60 60 import {useSession, useSessionApi, SessionAccount} from '#/state/session' 61 61 import {useProfileQuery} from '#/state/queries/profile' 62 62 import {useClearPreferencesMutation} from '#/state/queries/preferences' 63 + import {useInviteCodesQuery} from '#/state/queries/invites' 63 64 64 65 // TEMPORARY (APP-700) 65 66 // remove after backend testing finishes ··· 155 156 const {isSwitchingAccounts, accounts, currentAccount} = useSession() 156 157 const {clearCurrentAccount} = useSessionApi() 157 158 const {mutate: clearPreferences} = useClearPreferencesMutation() 159 + const {data: invites} = useInviteCodesQuery() 160 + const invitesAvailable = invites?.available?.length ?? 0 158 161 159 162 const primaryBg = useCustomPalette<ViewStyle>({ 160 163 light: {backgroundColor: colors.blue0}, ··· 362 365 <Text type="xl-bold" style={[pal.text, styles.heading]}> 363 366 <Trans>Invite a Friend</Trans> 364 367 </Text> 368 + 365 369 <TouchableOpacity 366 370 testID="inviteFriendBtn" 367 371 style={[ ··· 376 380 <View 377 381 style={[ 378 382 styles.iconContainer, 379 - store.me.invitesAvailable > 0 ? primaryBg : pal.btn, 383 + invitesAvailable > 0 ? primaryBg : pal.btn, 380 384 ]}> 381 385 <FontAwesomeIcon 382 386 icon="ticket" 383 387 style={ 384 - (store.me.invitesAvailable > 0 388 + (invitesAvailable > 0 385 389 ? primaryText 386 390 : pal.text) as FontAwesomeIconStyle 387 391 } 388 392 /> 389 393 </View> 390 - <Text 391 - type="lg" 392 - style={store.me.invitesAvailable > 0 ? pal.link : pal.text}> 393 - {formatCount(store.me.invitesAvailable)} invite{' '} 394 - {pluralize(store.me.invitesAvailable, 'code')} available 394 + <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}> 395 + {formatCount(invitesAvailable)} invite{' '} 396 + {pluralize(invitesAvailable, 'code')} available 395 397 </Text> 396 398 </TouchableOpacity> 397 399
+7 -7
src/view/shell/Drawer.tsx
··· 17 17 } from '@fortawesome/react-native-fontawesome' 18 18 import {s, colors} from 'lib/styles' 19 19 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' 20 - import {useStores} from 'state/index' 21 20 import { 22 21 HomeIcon, 23 22 HomeIconSolid, ··· 51 50 import {useProfileQuery} from '#/state/queries/profile' 52 51 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 53 52 import {emitSoftReset} from '#/state/events' 53 + import {useInviteCodesQuery} from '#/state/queries/invites' 54 54 55 55 export function DrawerProfileCard({ 56 56 account, ··· 464 464 style?: StyleProp<ViewStyle> 465 465 }) { 466 466 const {track} = useAnalytics() 467 - const store = useStores() 468 467 const setDrawerOpen = useSetDrawerOpen() 469 468 const pal = usePalette('default') 470 - const {invitesAvailable} = store.me 469 + const {data: invites} = useInviteCodesQuery() 470 + const invitesAvailable = invites?.available?.length ?? 0 471 471 const {openModal} = useModalControls() 472 472 const onPress = React.useCallback(() => { 473 473 track('Menu:ItemClicked', {url: '#invite-codes'}) ··· 490 490 icon="ticket" 491 491 style={[ 492 492 styles.inviteCodesIcon, 493 - store.me.invitesAvailable > 0 ? pal.link : pal.textLight, 493 + invitesAvailable > 0 ? pal.link : pal.textLight, 494 494 ]} 495 495 size={18} 496 496 /> 497 497 <Text 498 498 type="lg-medium" 499 - style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}> 500 - {formatCount(store.me.invitesAvailable)} invite{' '} 501 - {pluralize(store.me.invitesAvailable, 'code')} 499 + style={invitesAvailable > 0 ? pal.link : pal.textLight}> 500 + {formatCount(invitesAvailable)} invite{' '} 501 + {pluralize(invitesAvailable, 'code')} 502 502 </Text> 503 503 </TouchableOpacity> 504 504 )
+7 -8
src/view/shell/desktop/RightNav.tsx
··· 9 9 import {TextLink} from 'view/com/util/Link' 10 10 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' 11 11 import {s} from 'lib/styles' 12 - import {useStores} from 'state/index' 13 12 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 14 13 import {pluralize} from 'lib/strings/helpers' 15 14 import {formatCount} from 'view/com/util/numeric/format' 16 15 import {useModalControls} from '#/state/modals' 17 16 import {useSession} from '#/state/session' 17 + import {useInviteCodesQuery} from '#/state/queries/invites' 18 18 19 19 export const DesktopRightNav = observer(function DesktopRightNavImpl() { 20 20 const pal = usePalette('default') ··· 83 83 }) 84 84 85 85 const InviteCodes = observer(function InviteCodesImpl() { 86 - const store = useStores() 87 86 const pal = usePalette('default') 88 87 const {openModal} = useModalControls() 89 - 90 - const {invitesAvailable} = store.me 88 + const {data: invites} = useInviteCodesQuery() 89 + const invitesAvailable = invites?.available?.length ?? 0 91 90 92 91 const onPress = React.useCallback(() => { 93 92 openModal({name: 'invite-codes'}) ··· 107 106 icon="ticket" 108 107 style={[ 109 108 styles.inviteCodesIcon, 110 - store.me.invitesAvailable > 0 ? pal.link : pal.textLight, 109 + invitesAvailable > 0 ? pal.link : pal.textLight, 111 110 ]} 112 111 size={16} 113 112 /> 114 113 <Text 115 114 type="md-medium" 116 - style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}> 117 - {formatCount(store.me.invitesAvailable)} invite{' '} 118 - {pluralize(store.me.invitesAvailable, 'code')} available 115 + style={invitesAvailable > 0 ? pal.link : pal.textLight}> 116 + {formatCount(invitesAvailable)} invite{' '} 117 + {pluralize(invitesAvailable, 'code')} available 119 118 </Text> 120 119 </TouchableOpacity> 121 120 )