about summary refs log tree commit diff
path: root/src/view/com/modals/InviteCodes.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/modals/InviteCodes.tsx')
-rw-r--r--src/view/com/modals/InviteCodes.tsx183
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,