about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-11-16 10:40:31 -0600
committerGitHub <noreply@github.com>2023-11-16 08:40:31 -0800
commite6efeea7c07682c981998483bd49d7c01822911e (patch)
tree0da4d0b8ba03648fe8ceaef92d8d53ef4cc9fd9d /src
parent8a1fd160e6a1f9beeb735bb2320c12e5e71963d6 (diff)
downloadvoidsky-e6efeea7c07682c981998483bd49d7c01822911e.tar.zst
Refactor invites modal (#1930)
* Refactor invites modal

* Replace in drawer

* Delete stuff from me model
Diffstat (limited to 'src')
-rw-r--r--src/state/models/me.ts44
-rw-r--r--src/state/queries/invites.ts36
-rw-r--r--src/view/com/modals/InviteCodes.tsx54
-rw-r--r--src/view/screens/Settings.tsx16
-rw-r--r--src/view/shell/Drawer.tsx14
-rw-r--r--src/view/shell/desktop/RightNav.tsx15
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>
   )