Delete unused modals and invite code stuff (#8244)

* delete waitlist modal, which doesn't exist

* delete invite code modal

* rip out copied invites persisted state

* delete invite query

* delete unused modal implementations
This commit is contained in:
Samuel Newman
2025-09-23 21:45:51 +03:00
committed by GitHub
parent e90cfdc849
commit 46a97fadcb
11 changed files with 27 additions and 1019 deletions
+16 -19
View File
@@ -38,7 +38,6 @@ import {
} from '#/state/geolocation'
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
import {Provider as ModalStateProvider} from '#/state/modals'
@@ -225,24 +224,22 @@ function App() {
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<InnerApp />
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<InnerApp />
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
+11 -14
View File
@@ -26,7 +26,6 @@ import {
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
import {Provider as ModalStateProvider} from '#/state/modals'
@@ -199,19 +198,17 @@ function App() {
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<InnerApp />
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
-59
View File
@@ -1,59 +0,0 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type StateContext = persisted.Schema['invites']
type ApiContext = {
setInviteCopied: (code: string) => void
}
const stateContext = React.createContext<StateContext>(
persisted.defaults.invites,
)
stateContext.displayName = 'InvitesStateContext'
const apiContext = React.createContext<ApiContext>({
setInviteCopied(_: string) {},
})
apiContext.displayName = 'InvitesApiContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(persisted.get('invites'))
const api = React.useMemo(
() => ({
setInviteCopied(code: string) {
setState(state => {
state = {
...state,
copiedInvites: state.copiedInvites.includes(code)
? state.copiedInvites
: state.copiedInvites.concat([code]),
}
persisted.write('invites', state)
return state
})
},
}),
[setState],
)
React.useEffect(() => {
return persisted.onUpdate('invites', nextInvites => {
setState(nextInvites)
})
}, [setState])
return (
<stateContext.Provider value={state}>
<apiContext.Provider value={api}>{children}</apiContext.Provider>
</stateContext.Provider>
)
}
export function useInvitesState() {
return React.useContext(stateContext)
}
export function useInvitesAPI() {
return React.useContext(apiContext)
}
-12
View File
@@ -15,14 +15,6 @@ export interface DeleteAccountModal {
name: 'delete-account'
}
export interface WaitlistModal {
name: 'waitlist'
}
export interface InviteCodesModal {
name: 'invite-codes'
}
export interface ContentLanguagesSettingsModal {
name: 'content-languages-settings'
}
@@ -40,10 +32,6 @@ export type Modal =
// Lists
| UserAddRemoveListsModal
// Bluesky access
| WaitlistModal
| InviteCodesModal
const ModalContext = React.createContext<{
isModalActive: boolean
activeModals: Modal[]
-65
View File
@@ -1,65 +0,0 @@
import {type ComAtprotoServerDefs} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
return invite.available - invite.uses.length > 0 && !invite.disabled
}
const inviteCodesQueryKeyRoot = 'inviteCodes'
export type InviteCodesQueryResponse = Exclude<
ReturnType<typeof useInviteCodesQuery>['data'],
undefined
>
export function useInviteCodesQuery() {
const agent = useAgent()
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: [inviteCodesQueryKeyRoot],
queryFn: async () => {
const res = await agent.com.atproto.server
.getAccountInviteCodes({})
.catch(e => {
if (cleanError(e) === 'Bad token scope') {
return null
} else {
throw e
}
})
if (res === null) {
return {
disabled: true,
all: [],
available: [],
used: [],
}
}
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))
.sort((a, b) => {
return (
new Date(b.uses[0].usedAt).getTime() -
new Date(a.uses[0].usedAt).getTime()
)
})
return {
disabled: false,
all: [...available, ...used],
available,
used,
}
},
})
}
-403
View File
@@ -1,403 +0,0 @@
import {useCallback, useMemo, useState} from 'react'
import {
ActivityIndicator,
KeyboardAvoidingView,
ScrollView,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {enforceLen} from '#/lib/strings/helpers'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {colors, gradients, s} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext'
import {type ImageMeta} from '#/state/gallery'
import {useModalControls} from '#/state/modals'
import {
useListCreateMutation,
useListMetadataMutation,
} from '#/state/queries/list'
import {useAgent} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
const MAX_NAME = 64 // todo
const MAX_DESCRIPTION = 300 // todo
export const snapPoints = ['fullscreen']
export function Component({
purpose,
onSave,
list,
}: {
purpose?: string
onSave?: (uri: string) => void
list?: AppBskyGraphDefs.ListView
}) {
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const [error, setError] = useState<string>('')
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
const listCreateMutation = useListCreateMutation()
const listMetadataMutation = useListMetadataMutation()
const agent = useAgent()
const activePurpose = useMemo(() => {
if (list?.purpose) {
return list.purpose
}
if (purpose) {
return purpose
}
return 'app.bsky.graph.defs#curatelist'
}, [list, purpose])
const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist'
const [isProcessing, setProcessing] = useState<boolean>(false)
const [name, setName] = useState<string>(list?.name || '')
const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => {
const text = list?.description
const facets = list?.descriptionFacets
if (!text || !facets) {
return new RichTextAPI({text: text || ''})
}
// We want to be working with a blank state here, so let's get the
// serialized version and turn it back into a RichText
const serialized = richTextToString(new RichTextAPI({text, facets}), false)
const richText = new RichTextAPI({text: serialized})
richText.detectFacetsWithoutResolution()
return richText
})
const graphemeLength = useMemo(() => {
return shortenLinks(descriptionRt).graphemeLength
}, [descriptionRt])
const isDescriptionOver = graphemeLength > MAX_DESCRIPTION
const [avatar, setAvatar] = useState<string | undefined>(list?.avatar)
const [newAvatar, setNewAvatar] = useState<ImageMeta | undefined | null>()
const onDescriptionChange = useCallback(
(newText: string) => {
const richText = new RichTextAPI({text: newText})
richText.detectFacetsWithoutResolution()
setDescriptionRt(richText)
},
[setDescriptionRt],
)
const onPressCancel = useCallback(() => {
closeModal()
}, [closeModal])
const onSelectNewAvatar = useCallback(
(img: ImageMeta | null) => {
if (!img) {
setNewAvatar(null)
setAvatar(undefined)
return
}
try {
setNewAvatar(img)
setAvatar(img.path)
} catch (e: any) {
setError(cleanError(e))
}
},
[setNewAvatar, setAvatar, setError],
)
const onPressSave = useCallback(async () => {
const nameTrimmed = name.trim()
if (!nameTrimmed) {
setError(_(msg`Name is required`))
return
}
setProcessing(true)
if (error) {
setError('')
}
try {
let richText = new RichTextAPI(
{text: descriptionRt.text.trimEnd()},
{cleanNewlines: true},
)
await richText.detectFacets(agent)
richText = shortenLinks(richText)
richText = stripInvalidMentions(richText)
if (list) {
await listMetadataMutation.mutateAsync({
uri: list.uri,
name: nameTrimmed,
description: richText.text,
descriptionFacets: richText.facets,
avatar: newAvatar,
})
Toast.show(
isCurateList
? _(msg({message: 'User list updated', context: 'toast'}))
: _(msg({message: 'Moderation list updated', context: 'toast'})),
)
onSave?.(list.uri)
} else {
const res = await listCreateMutation.mutateAsync({
purpose: activePurpose,
name,
description: richText.text,
descriptionFacets: richText.facets,
avatar: newAvatar,
})
Toast.show(
isCurateList
? _(msg({message: 'User list created', context: 'toast'}))
: _(msg({message: 'Moderation list created', context: 'toast'})),
)
onSave?.(res.uri)
}
closeModal()
} catch (e: any) {
if (isNetworkError(e)) {
setError(
_(
msg`Failed to create the list. Check your internet connection and try again.`,
),
)
} else {
setError(cleanError(e))
}
}
setProcessing(false)
}, [
setProcessing,
setError,
error,
onSave,
closeModal,
activePurpose,
isCurateList,
name,
descriptionRt,
newAvatar,
list,
listMetadataMutation,
listCreateMutation,
_,
agent,
])
return (
<KeyboardAvoidingView behavior="height">
<ScrollView
style={[
pal.view,
{
paddingHorizontal: isMobile ? 16 : 0,
},
]}
testID="createOrEditListModal">
<Text style={[styles.title, pal.text]}>
{isCurateList ? (
list ? (
<Trans>Edit User List</Trans>
) : (
<Trans>New User List</Trans>
)
) : list ? (
<Trans>Edit Moderation List</Trans>
) : (
<Trans>New Moderation List</Trans>
)}
</Text>
{error !== '' && (
<View style={styles.errorContainer}>
<ErrorMessage message={error} />
</View>
)}
<Text style={[styles.label, pal.text]}>
<Trans>List Avatar</Trans>
</Text>
<View style={[styles.avi, {borderColor: pal.colors.background}]}>
<EditableUserAvatar
type="list"
size={80}
avatar={avatar}
onSelectNewAvatar={onSelectNewAvatar}
/>
</View>
<View style={styles.form}>
<View>
<View style={styles.labelWrapper}>
<Text style={[styles.label, pal.text]} nativeID="list-name">
<Trans>List Name</Trans>
</Text>
</View>
<TextInput
testID="editNameInput"
style={[styles.textInput, pal.border, pal.text]}
placeholder={
isCurateList
? _(msg`e.g. Great Posters`)
: _(msg`e.g. Spammers`)
}
placeholderTextColor={colors.gray4}
value={name}
onChangeText={v => setName(enforceLen(v, MAX_NAME))}
accessible={true}
accessibilityLabel={_(msg`Name`)}
accessibilityHint=""
accessibilityLabelledBy="list-name"
/>
</View>
<View style={s.pb10}>
<View style={styles.labelWrapper}>
<Text
style={[styles.label, pal.text]}
nativeID="list-description">
<Trans>Description</Trans>
</Text>
<Text
style={[!isDescriptionOver ? pal.textLight : s.red3, s.f13]}>
{graphemeLength}/{MAX_DESCRIPTION}
</Text>
</View>
<TextInput
testID="editDescriptionInput"
style={[styles.textArea, pal.border, pal.text]}
placeholder={
isCurateList
? _(msg`e.g. The posters who never miss.`)
: _(msg`e.g. Users that repeatedly reply with ads.`)
}
placeholderTextColor={colors.gray4}
keyboardAppearance={theme.colorScheme}
multiline
value={descriptionRt.text}
onChangeText={onDescriptionChange}
accessible={true}
accessibilityLabel={_(msg`Description`)}
accessibilityHint=""
accessibilityLabelledBy="list-description"
/>
</View>
{isProcessing ? (
<View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}>
<ActivityIndicator />
</View>
) : (
<TouchableOpacity
testID="saveBtn"
style={[s.mt10, isDescriptionOver && s.dimmed]}
disabled={isDescriptionOver}
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel={_(msg`Save`)}
accessibilityHint="">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.btn}>
<Text style={[s.white, s.bold]}>
<Trans context="action">Save</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
)}
<TouchableOpacity
testID="cancelBtn"
style={s.mt5}
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
onAccessibilityEscape={onPressCancel}>
<View style={[styles.btn]}>
<Text style={[s.black, s.bold, pal.text]}>
<Trans context="action">Cancel</Trans>
</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: '600',
fontSize: 24,
marginBottom: 18,
},
labelWrapper: {
flexDirection: 'row',
gap: 8,
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 4,
paddingBottom: 4,
marginTop: 20,
},
label: {
fontWeight: '600',
},
form: {
paddingHorizontal: 6,
},
textInput: {
borderWidth: 1,
borderRadius: 6,
paddingHorizontal: 14,
paddingVertical: 10,
fontSize: 16,
},
textArea: {
borderWidth: 1,
borderRadius: 6,
paddingHorizontal: 12,
paddingTop: 10,
fontSize: 16,
height: 100,
textAlignVertical: 'top',
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 10,
marginBottom: 10,
},
avi: {
width: 84,
height: 84,
borderWidth: 2,
borderRadius: 42,
marginTop: 4,
},
errorContainer: {marginTop: 20},
})
-145
View File
@@ -1,145 +0,0 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import ReactCrop, {type PercentCrop} from 'react-image-crop'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type PickerImage} from '#/lib/media/picker.shared'
import {getDataUriSize} from '#/lib/media/util'
import {gradients, s} from '#/lib/styles'
import {useModalControls} from '#/state/modals'
import {Text} from '#/view/com/util/text/Text'
export const snapPoints = ['0%']
export function Component({
uri,
aspect,
circular,
onSelect,
}: {
uri: string
aspect?: number
circular?: boolean
onSelect: (img?: PickerImage) => void
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const imageRef = React.useRef<HTMLImageElement>(null)
const [crop, setCrop] = React.useState<PercentCrop>()
const isEmpty = !crop || (crop.width || crop.height) === 0
const onPressCancel = () => {
onSelect(undefined)
closeModal()
}
const onPressDone = async () => {
const img = imageRef.current!
const result = await manipulateAsync(
uri,
isEmpty
? []
: [
{
crop: {
originX: (crop.x * img.naturalWidth) / 100,
originY: (crop.y * img.naturalHeight) / 100,
width: (crop.width * img.naturalWidth) / 100,
height: (crop.height * img.naturalHeight) / 100,
},
},
],
{
base64: true,
format: SaveFormat.JPEG,
},
)
onSelect({
path: result.uri,
mime: 'image/jpeg',
size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0,
width: result.width,
height: result.height,
})
closeModal()
}
return (
<View>
<View style={[styles.cropper, pal.borderDark]}>
<ReactCrop
aspect={aspect}
crop={crop}
onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)}
circularCrop={circular}>
<img ref={imageRef} src={uri} style={{maxHeight: '75vh'}} />
</ReactCrop>
</View>
<View style={[styles.btns, isMobile && {paddingHorizontal: 16}]}>
<TouchableOpacity
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel image crop`)}
accessibilityHint={_(msg`Exits image cropping process`)}>
<Text type="xl" style={pal.link}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity
onPress={onPressDone}
accessibilityRole="button"
accessibilityLabel={_(msg`Save image crop`)}
accessibilityHint={_(msg`Saves image crop settings`)}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="xl-medium" style={s.white}>
<Trans>Done</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
)
}
const styles = StyleSheet.create({
cropper: {
marginLeft: 'auto',
marginRight: 'auto',
borderWidth: 1,
borderRadius: 4,
overflow: 'hidden',
alignItems: 'center',
},
ctrls: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
btns: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
btn: {
borderRadius: 4,
paddingVertical: 8,
paddingHorizontal: 24,
},
})
-287
View File
@@ -1,287 +0,0 @@
import React from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import {type ComAtprotoServerDefs} from '@atproto/api'
import {
FontAwesomeIcon,
type FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {makeProfileLink} from '#/lib/routes/links'
import {cleanError} from '#/lib/strings/errors'
import {isWeb} from '#/platform/detection'
import {useInvitesAPI, useInvitesState} from '#/state/invites'
import {useModalControls} from '#/state/modals'
import {
type InviteCodesQueryResponse,
useInviteCodesQuery,
} from '#/state/queries/invites'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {UserInfoText} from '../util/UserInfoText'
import {ScrollView} from './util'
export const snapPoints = ['70%']
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 {_} = useLingui()
const {closeModal} = useModalControls()
const {isTabletOrDesktop} = useWebMediaQueries()
const onClose = React.useCallback(() => {
closeModal()
}, [closeModal])
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]}>
<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} />
<View
style={[
styles.btnContainer,
isTabletOrDesktop && styles.btnContainerDesktop,
]}>
<Button
type="primary"
label={_(msg`Done`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onClose}
/>
</View>
</View>
)
}
return (
<View style={[styles.container, pal.view]} testID="inviteCodesModal">
<Text type="title-xl" style={[styles.title, pal.text]}>
<Trans>Invite a Friend</Trans>
</Text>
<Text type="lg" style={[styles.description, pal.text]}>
<Trans>
Each code works once. You'll receive more invite codes periodically.
</Trans>
</Text>
<ScrollView style={[styles.scrollContainer, pal.border]}>
{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}
invites={invites}
/>
))}
</ScrollView>
<View style={styles.btnContainer}>
<Button
testID="closeBtn"
type="primary"
label={_(msg`Done`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onClose}
/>
</View>
</View>
)
}
function InviteCode({
testID,
invite,
used,
invites,
}: {
testID: string
invite: ComAtprotoServerDefs.InviteCode
used?: boolean
invites: InviteCodesQueryResponse
}) {
const pal = usePalette('default')
const {_} = useLingui()
const invitesState = useInvitesState()
const {setInviteCopied} = useInvitesAPI()
const uses = invite.uses
const onPress = React.useCallback(() => {
setStringAsync(invite.code)
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setInviteCopied(invite.code)
}, [setInviteCopied, invite, _])
return (
<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
? _(msg`Invite codes: 1 available`)
: _(msg`Invite codes: ${invites.available.length} available`)
}
accessibilityHint={_(msg`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>
{uses.length > 0 ? (
<View
style={{
flexDirection: 'column',
gap: 8,
paddingTop: 6,
}}>
<Text style={pal.text}>
<Trans>Used by:</Trans>{' '}
{uses.map((use, i) => (
<Link
key={use.usedBy}
href={makeProfileLink({handle: use.usedBy, did: ''})}
style={{
flexDirection: 'row',
}}>
<UserInfoText did={use.usedBy} style={pal.link} />
{i !== uses.length - 1 && <Text style={pal.text}>, </Text>}
</Link>
))}
</Text>
</View>
) : null}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: isWeb ? 0 : 50,
},
title: {
textAlign: 'center',
marginTop: 12,
marginBottom: 12,
},
description: {
textAlign: 'center',
paddingHorizontal: 42,
marginBottom: 14,
},
scrollContainer: {
flex: 1,
borderTopWidth: 1,
marginTop: 4,
marginBottom: 16,
},
flex1: {
flex: 1,
},
empty: {
paddingHorizontal: 20,
paddingVertical: 20,
borderRadius: 16,
marginHorizontal: 24,
marginTop: 10,
},
emptyText: {
textAlign: 'center',
},
inviteCode: {
flexDirection: 'row',
alignItems: 'center',
},
codeCopied: {
marginRight: 8,
},
strikeThrough: {
textDecorationLine: 'line-through',
textDecorationStyle: 'solid',
},
btnContainer: {
flexDirection: 'row',
justifyContent: 'center',
},
btnContainerDesktop: {
marginTop: 14,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
paddingHorizontal: 60,
paddingVertical: 14,
},
btnLabel: {
fontSize: 18,
},
})
-4
View File
@@ -8,7 +8,6 @@ import {useModalControls, useModals} from '#/state/modals'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as DeleteAccountModal from './DeleteAccount'
import * as InviteCodesModal from './InviteCodes'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
@@ -49,9 +48,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'delete-account') {
snapPoints = DeleteAccountModal.snapPoints
element = <DeleteAccountModal.Component />
} else if (activeModal?.name === 'invite-codes') {
snapPoints = InviteCodesModal.snapPoints
element = <InviteCodesModal.Component />
} else if (activeModal?.name === 'content-languages-settings') {
snapPoints = ContentLanguagesSettingsModal.snapPoints
element = <ContentLanguagesSettingsModal.Component />
-3
View File
@@ -7,7 +7,6 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type Modal as ModalIface} from '#/state/modals'
import {useModalControls, useModals} from '#/state/modals'
import * as DeleteAccountModal from './DeleteAccount'
import * as InviteCodesModal from './InviteCodes'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as UserAddRemoveLists from './UserAddRemoveLists'
@@ -51,8 +50,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <UserAddRemoveLists.Component {...modal} />
} else if (modal.name === 'delete-account') {
element = <DeleteAccountModal.Component />
} else if (modal.name === 'invite-codes') {
element = <InviteCodesModal.Component />
} else if (modal.name === 'content-languages-settings') {
element = <ContentLanguagesSettingsModal.Component />
} else {
-8
View File
@@ -3,7 +3,6 @@ import {LogBox, Pressable, View, TextInput} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {BLUESKY_PROXY_HEADER} from '#/lib/constants'
import {useModalControls} from '#/state/modals'
import {useSessionApi, useAgent} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useOnboardingDispatch} from '#/state/shell/onboarding'
@@ -23,7 +22,6 @@ export function TestCtrls() {
const agent = useAgent()
const queryClient = useQueryClient()
const {logoutEveryAccount, login} = useSessionApi()
const {openModal} = useModalControls()
const onboardingDispatch = useOnboardingDispatch()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onPressSignInAlice = async () => {
@@ -121,12 +119,6 @@ export function TestCtrls() {
accessibilityRole="button"
style={BTN}
/>
<Pressable
testID="e2eOpenInviteCodesModal"
onPress={() => openModal({name: 'invite-codes'})}
accessibilityRole="button"
style={BTN}
/>
<Pressable
testID="e2eOpenLoggedOutView"
onPress={() => setShowLoggedOut(true)}