From 46a97fadcb71d1044be9796c814cfdc82e348605 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 23 Sep 2025 21:45:51 +0300 Subject: [PATCH] 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 --- src/App.native.tsx | 35 +- src/App.web.tsx | 25 +- src/state/invites.tsx | 59 ---- src/state/modals/index.tsx | 12 - src/state/queries/invites.ts | 65 ---- src/view/com/modals/CreateOrEditList.tsx | 403 ----------------------- src/view/com/modals/CropImage.web.tsx | 145 -------- src/view/com/modals/InviteCodes.tsx | 287 ---------------- src/view/com/modals/Modal.tsx | 4 - src/view/com/modals/Modal.web.tsx | 3 - src/view/com/testing/TestCtrls.e2e.tsx | 8 - 11 files changed, 27 insertions(+), 1019 deletions(-) delete mode 100644 src/state/invites.tsx delete mode 100644 src/state/queries/invites.ts delete mode 100644 src/view/com/modals/CreateOrEditList.tsx delete mode 100644 src/view/com/modals/CropImage.web.tsx delete mode 100644 src/view/com/modals/InviteCodes.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index 036ecff60e..104a7ecaec 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -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() { - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index c86960172a..569c9be799 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -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() { - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/state/invites.tsx b/src/state/invites.tsx deleted file mode 100644 index 4f12cb12f4..0000000000 --- a/src/state/invites.tsx +++ /dev/null @@ -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( - persisted.defaults.invites, -) -stateContext.displayName = 'InvitesStateContext' -const apiContext = React.createContext({ - 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 ( - - {children} - - ) -} - -export function useInvitesState() { - return React.useContext(stateContext) -} - -export function useInvitesAPI() { - return React.useContext(apiContext) -} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 661ab3a15a..dab90c0af3 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -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[] diff --git a/src/state/queries/invites.ts b/src/state/queries/invites.ts deleted file mode 100644 index ed7fc534f9..0000000000 --- a/src/state/queries/invites.ts +++ /dev/null @@ -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['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, - } - }, - }) -} diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx deleted file mode 100644 index 3687dce901..0000000000 --- a/src/view/com/modals/CreateOrEditList.tsx +++ /dev/null @@ -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('') - 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(false) - const [name, setName] = useState(list?.name || '') - - const [descriptionRt, setDescriptionRt] = useState(() => { - 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(list?.avatar) - const [newAvatar, setNewAvatar] = useState() - - 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 ( - - - - {isCurateList ? ( - list ? ( - Edit User List - ) : ( - New User List - ) - ) : list ? ( - Edit Moderation List - ) : ( - New Moderation List - )} - - {error !== '' && ( - - - - )} - - List Avatar - - - - - - - - - List Name - - - setName(enforceLen(v, MAX_NAME))} - accessible={true} - accessibilityLabel={_(msg`Name`)} - accessibilityHint="" - accessibilityLabelledBy="list-name" - /> - - - - - Description - - - {graphemeLength}/{MAX_DESCRIPTION} - - - - - {isProcessing ? ( - - - - ) : ( - - - - Save - - - - )} - - - - Cancel - - - - - - - ) -} - -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}, -}) diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx deleted file mode 100644 index 78c0466f0b..0000000000 --- a/src/view/com/modals/CropImage.web.tsx +++ /dev/null @@ -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(null) - const [crop, setCrop] = React.useState() - - 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 ( - - - setCrop(percentCrop)} - circularCrop={circular}> - - - - - - - Cancel - - - - - - - Done - - - - - - ) -} - -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, - }, -}) diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx deleted file mode 100644 index 93f7490625..0000000000 --- a/src/view/com/modals/InviteCodes.tsx +++ /dev/null @@ -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 ? ( - - ) : isLoading || !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 ( - - - - - You don't have any invite codes yet! We'll send you some when - you've been on Bluesky for a little longer. - - - - - -