diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/auth/create/CaptchaWebView.tsx | 86 | ||||
-rw-r--r-- | src/view/com/auth/create/CaptchaWebView.web.tsx | 61 | ||||
-rw-r--r-- | src/view/com/auth/create/CreateAccount.tsx | 71 | ||||
-rw-r--r-- | src/view/com/auth/create/Step1.tsx | 9 | ||||
-rw-r--r-- | src/view/com/auth/create/Step2.tsx | 308 | ||||
-rw-r--r-- | src/view/com/auth/create/Step3.tsx | 120 | ||||
-rw-r--r-- | src/view/com/auth/create/StepHeader.tsx | 2 | ||||
-rw-r--r-- | src/view/com/auth/create/state.ts | 301 | ||||
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 15 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 19 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 86 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 32 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/PostSandboxWarning.tsx | 35 |
14 files changed, 512 insertions, 636 deletions
diff --git a/src/view/com/auth/create/CaptchaWebView.tsx b/src/view/com/auth/create/CaptchaWebView.tsx new file mode 100644 index 000000000..b0de8b4a4 --- /dev/null +++ b/src/view/com/auth/create/CaptchaWebView.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import {WebView, WebViewNavigation} from 'react-native-webview' +import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes' +import {StyleSheet} from 'react-native' +import {CreateAccountState} from 'view/com/auth/create/state' + +const ALLOWED_HOSTS = [ + 'bsky.social', + 'bsky.app', + 'staging.bsky.app', + 'staging.bsky.dev', + 'js.hcaptcha.com', + 'newassets.hcaptcha.com', + 'api2.hcaptcha.com', +] + +export function CaptchaWebView({ + url, + stateParam, + uiState, + onSuccess, + onError, +}: { + url: string + stateParam: string + uiState?: CreateAccountState + onSuccess: (code: string) => void + onError: () => void +}) { + const redirectHost = React.useMemo(() => { + if (!uiState?.serviceUrl) return 'bsky.app' + + return uiState?.serviceUrl && + new URL(uiState?.serviceUrl).host === 'staging.bsky.dev' + ? 'staging.bsky.app' + : 'bsky.app' + }, [uiState?.serviceUrl]) + + const wasSuccessful = React.useRef(false) + + const onShouldStartLoadWithRequest = React.useCallback( + (event: ShouldStartLoadRequest) => { + const urlp = new URL(event.url) + return ALLOWED_HOSTS.includes(urlp.host) + }, + [], + ) + + const onNavigationStateChange = React.useCallback( + (e: WebViewNavigation) => { + if (wasSuccessful.current) return + + const urlp = new URL(e.url) + if (urlp.host !== redirectHost) return + + const code = urlp.searchParams.get('code') + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError() + return + } + + wasSuccessful.current = true + onSuccess(code) + }, + [redirectHost, stateParam, onSuccess, onError], + ) + + return ( + <WebView + source={{uri: url}} + javaScriptEnabled + style={styles.webview} + onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} + onNavigationStateChange={onNavigationStateChange} + scrollEnabled={false} + /> + ) +} + +const styles = StyleSheet.create({ + webview: { + flex: 1, + backgroundColor: 'transparent', + borderRadius: 10, + }, +}) diff --git a/src/view/com/auth/create/CaptchaWebView.web.tsx b/src/view/com/auth/create/CaptchaWebView.web.tsx new file mode 100644 index 000000000..7791a58dd --- /dev/null +++ b/src/view/com/auth/create/CaptchaWebView.web.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import {StyleSheet} from 'react-native' + +// @ts-ignore web only, we will always redirect to the app on web (CORS) +const REDIRECT_HOST = new URL(window.location.href).host + +export function CaptchaWebView({ + url, + stateParam, + onSuccess, + onError, +}: { + url: string + stateParam: string + onSuccess: (code: string) => void + onError: () => void +}) { + const onLoad = React.useCallback(() => { + // @ts-ignore web + const frame: HTMLIFrameElement = document.getElementById( + 'captcha-iframe', + ) as HTMLIFrameElement + + try { + // @ts-ignore web + const href = frame?.contentWindow?.location.href + if (!href) return + const urlp = new URL(href) + + // This shouldn't happen with CORS protections, but for good measure + if (urlp.host !== REDIRECT_HOST) return + + const code = urlp.searchParams.get('code') + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError() + return + } + onSuccess(code) + } catch (e) { + // We don't need to handle this + } + }, [stateParam, onSuccess, onError]) + + return ( + <iframe + src={url} + style={styles.iframe} + id="captcha-iframe" + onLoad={onLoad} + /> + ) +} + +const styles = StyleSheet.create({ + iframe: { + flex: 1, + borderWidth: 0, + borderRadius: 10, + backgroundColor: 'transparent', + }, +}) diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx index 5d452736a..8aefffa6d 100644 --- a/src/view/com/auth/create/CreateAccount.tsx +++ b/src/view/com/auth/create/CreateAccount.tsx @@ -13,33 +13,25 @@ import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useOnboardingDispatch} from '#/state/shell' -import {useSessionApi} from '#/state/session' -import {useCreateAccount, submit} from './state' +import {useCreateAccount, useSubmitCreateAccount} from './state' import {useServiceQuery} from '#/state/queries/service' -import { - usePreferencesSetBirthDateMutation, - useSetSaveFeedsMutation, - DEFAULT_PROD_FEEDS, -} from '#/state/queries/preferences' -import {FEEDBACK_FORM_URL, HITSLOP_10, IS_PROD} from '#/lib/constants' +import {FEEDBACK_FORM_URL, HITSLOP_10} from '#/lib/constants' import {Step1} from './Step1' import {Step2} from './Step2' import {Step3} from './Step3' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {TextLink} from '../../util/Link' +import {getAgent} from 'state/session' +import {createFullHandle} from 'lib/strings/handles' export function CreateAccount({onPressBack}: {onPressBack: () => void}) { const {screen} = useAnalytics() const pal = usePalette('default') const {_} = useLingui() const [uiState, uiDispatch] = useCreateAccount() - const onboardingDispatch = useOnboardingDispatch() - const {createAccount} = useSessionApi() - const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() - const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() const {isTabletOrDesktop} = useWebMediaQueries() + const submit = useSubmitCreateAccount(uiState, uiDispatch) React.useEffect(() => { screen('CreateAccount') @@ -84,33 +76,48 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) { if (!uiState.canNext) { return } - if (uiState.step < 3) { - uiDispatch({type: 'next'}) - } else { + + if (uiState.step === 2) { + uiDispatch({type: 'set-processing', value: true}) try { - await submit({ - onboardingDispatch, - createAccount, - uiState, - uiDispatch, - _, + const res = await getAgent().resolveHandle({ + handle: createFullHandle(uiState.handle, uiState.userDomain), }) - setBirthDate({birthDate: uiState.birthDate}) - if (IS_PROD(uiState.serviceUrl)) { - setSavedFeeds(DEFAULT_PROD_FEEDS) + + if (res.data.did) { + uiDispatch({ + type: 'set-error', + value: _(msg`That handle is already taken.`), + }) + return } - } catch { - // dont need to handle here + } catch (e) { + // Don't need to handle + } finally { + uiDispatch({type: 'set-processing', value: false}) + } + + if (!uiState.isCaptchaRequired) { + try { + await submit() + } catch { + // dont need to handle here + } + // We don't need to go to the next page if there wasn't a captcha required + return } } + + uiDispatch({type: 'next'}) }, [ - uiState, + uiState.canNext, + uiState.step, + uiState.isCaptchaRequired, + uiState.handle, + uiState.userDomain, uiDispatch, - onboardingDispatch, - createAccount, - setBirthDate, - setSavedFeeds, _, + submit, ]) // rendering diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index a7abbfaa8..4c7018485 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -73,6 +73,10 @@ export function Step1({ /> <StepHeader uiState={uiState} title={_(msg`Your account`)} /> + {uiState.error ? ( + <ErrorMessage message={uiState.error} style={styles.error} /> + ) : undefined} + <View style={s.pb20}> <Text type="md-medium" style={[pal.text, s.mb2]}> <Trans>Hosting provider</Trans> @@ -259,9 +263,6 @@ export function Step1({ )} </> )} - {uiState.error ? ( - <ErrorMessage message={uiState.error} style={styles.error} /> - ) : undefined} </View> ) } @@ -269,7 +270,7 @@ export function Step1({ const styles = StyleSheet.create({ error: { borderRadius: 6, - marginTop: 10, + marginBottom: 10, }, dateInputButton: { borderWidth: 1, diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index 2e16b13bb..87d414bb9 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -1,35 +1,19 @@ import React from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableWithoutFeedback, - View, -} from 'react-native' -import RNPickerSelect from 'react-native-picker-select' -import { - CreateAccountState, - CreateAccountDispatch, - requestVerificationCode, -} from './state' +import {StyleSheet, View} from 'react-native' +import {CreateAccountState, CreateAccountDispatch} from './state' import {Text} from 'view/com/util/text/Text' import {StepHeader} from './StepHeader' import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' import {TextInput} from '../util/TextInput' -import {Button} from '../../util/forms/Button' +import {createFullHandle} from 'lib/strings/handles' +import {usePalette} from 'lib/hooks/usePalette' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {isAndroid, isWeb} from 'platform/detection' -import {Trans, msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import parsePhoneNumber from 'libphonenumber-js' -import {COUNTRY_CODES} from '#/lib/country-codes' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {HITSLOP_10} from '#/lib/constants' +/** STEP 3: Your user handle + * @field User handle + */ export function Step2({ uiState, uiDispatch, @@ -39,258 +23,34 @@ export function Step2({ }) { const pal = usePalette('default') const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - - const onPressRequest = React.useCallback(() => { - const phoneNumber = parsePhoneNumber( - uiState.verificationPhone, - uiState.phoneCountry, - ) - if (phoneNumber && phoneNumber.isValid()) { - requestVerificationCode({uiState, uiDispatch, _}) - } else { - uiDispatch({ - type: 'set-error', - value: _( - msg`There's something wrong with this number. Please choose your country and enter your full phone number!`, - ), - }) - } - }, [uiState, uiDispatch, _]) - - const onPressRetry = React.useCallback(() => { - uiDispatch({type: 'set-has-requested-verification-code', value: false}) - }, [uiDispatch]) - - const phoneNumberFormatted = React.useMemo( - () => - uiState.hasRequestedVerificationCode - ? parsePhoneNumber( - uiState.verificationPhone, - uiState.phoneCountry, - )?.formatInternational() - : '', - [ - uiState.hasRequestedVerificationCode, - uiState.verificationPhone, - uiState.phoneCountry, - ], - ) - return ( <View> - <StepHeader uiState={uiState} title={_(msg`SMS verification`)} /> - - {!uiState.hasRequestedVerificationCode ? ( - <> - <View style={s.pb10}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="phoneCountry"> - <Trans>Country</Trans> - </Text> - <View - style={[ - {position: 'relative'}, - isAndroid && { - borderWidth: 1, - borderColor: pal.border.borderColor, - borderRadius: 4, - }, - ]}> - <RNPickerSelect - placeholder={{}} - value={uiState.phoneCountry} - onValueChange={value => - uiDispatch({type: 'set-phone-country', value}) - } - items={COUNTRY_CODES.filter(l => Boolean(l.code2)).map(l => ({ - label: l.name, - value: l.code2, - key: l.code2, - }))} - style={{ - inputAndroid: { - backgroundColor: pal.view.backgroundColor, - color: pal.text.color, - fontSize: 21, - letterSpacing: 0.5, - fontWeight: '500', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 4, - }, - inputIOS: { - backgroundColor: pal.view.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '500', - paddingHorizontal: 14, - paddingVertical: 8, - borderWidth: 1, - borderColor: pal.border.borderColor, - borderRadius: 4, - }, - inputWeb: { - // @ts-ignore web only - cursor: 'pointer', - '-moz-appearance': 'none', - '-webkit-appearance': 'none', - appearance: 'none', - outline: 0, - borderWidth: 1, - borderColor: pal.border.borderColor, - backgroundColor: pal.view.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '500', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 4, - }, - }} - accessibilityLabel={_(msg`Select your phone's country`)} - accessibilityHint="" - accessibilityLabelledBy="phoneCountry" - /> - <View - style={{ - position: 'absolute', - top: 1, - right: 1, - bottom: 1, - width: 40, - pointerEvents: 'none', - alignItems: 'center', - justifyContent: 'center', - }}> - <FontAwesomeIcon - icon="chevron-down" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </View> - </View> - - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="phoneNumber"> - <Trans>Phone number</Trans> - </Text> - <TextInput - testID="phoneInput" - icon="phone" - placeholder={_(msg`Enter your phone number`)} - value={uiState.verificationPhone} - editable - onChange={value => - uiDispatch({type: 'set-verification-phone', value}) - } - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_( - msg`Input phone number for SMS verification`, - )} - accessibilityLabelledBy="phoneNumber" - keyboardType="phone-pad" - autoCapitalize="none" - autoComplete="tel" - autoCorrect={false} - autoFocus={true} - /> - <Text type="sm" style={[pal.textLight, s.mt5]}> - <Trans> - Please enter a phone number that can receive SMS text messages. - </Trans> - </Text> - </View> - - <View style={isMobile ? {} : {flexDirection: 'row'}}> - {uiState.isProcessing ? ( - <ActivityIndicator /> - ) : ( - <Button - testID="requestCodeBtn" - type="primary" - label={_(msg`Request code`)} - labelStyle={isMobile ? [s.flex1, s.textCenter, s.f17] : []} - style={ - isMobile ? {paddingVertical: 12, paddingHorizontal: 20} : {} - } - onPress={onPressRequest} - /> - )} - </View> - </> - ) : ( - <> - <View style={s.pb20}> - <View - style={[ - s.flexRow, - s.mb5, - s.alignCenter, - {justifyContent: 'space-between'}, - ]}> - <Text - type="md-medium" - style={pal.text} - nativeID="verificationCode"> - <Trans>Verification code</Trans>{' '} - </Text> - <TouchableWithoutFeedback - onPress={onPressRetry} - accessibilityLabel={_(msg`Retry.`)} - accessibilityHint="" - hitSlop={HITSLOP_10}> - <View style={styles.touchable}> - <Text - type="md-medium" - style={pal.link} - nativeID="verificationCode"> - <Trans>Retry</Trans> - </Text> - </View> - </TouchableWithoutFeedback> - </View> - <TextInput - testID="codeInput" - icon="hashtag" - placeholder={_(msg`XXXXXX`)} - value={uiState.verificationCode} - editable - onChange={value => - uiDispatch({type: 'set-verification-code', value}) - } - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_( - msg`Input the verification code we have texted to you`, - )} - accessibilityLabelledBy="verificationCode" - keyboardType="phone-pad" - autoCapitalize="none" - autoComplete="one-time-code" - textContentType="oneTimeCode" - autoCorrect={false} - autoFocus={true} - /> - <Text type="sm" style={[pal.textLight, s.mt5]}> - <Trans> - Please enter the verification code sent to{' '} - {phoneNumberFormatted}. - </Trans> - </Text> - </View> - </> - )} - + <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> {uiState.error ? ( <ErrorMessage message={uiState.error} style={styles.error} /> ) : undefined} + <View style={s.pb10}> + <TextInput + testID="handleInput" + icon="at" + placeholder="e.g. alice" + value={uiState.handle} + editable + autoFocus + autoComplete="off" + autoCorrect={false} + onChange={value => uiDispatch({type: 'set-handle', value})} + // TODO: Add explicit text label + accessibilityLabel={_(msg`User handle`)} + accessibilityHint={_(msg`Input your user handle`)} + /> + <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> + <Trans>Your full handle will be</Trans>{' '} + <Text type="lg-bold" style={pal.text}> + @{createFullHandle(uiState.handle, uiState.userDomain)} + </Text> + </Text> + </View> </View> ) } @@ -298,10 +58,6 @@ export function Step2({ const styles = StyleSheet.create({ error: { borderRadius: 6, - marginTop: 10, - }, - // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. - touchable: { - ...(isWeb && {cursor: 'pointer'}), + marginBottom: 10, }, }) diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx index afd21a320..53fdfdde8 100644 --- a/src/view/com/auth/create/Step3.tsx +++ b/src/view/com/auth/create/Step3.tsx @@ -1,19 +1,23 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' -import {CreateAccountState, CreateAccountDispatch} from './state' -import {Text} from 'view/com/util/text/Text' +import {ActivityIndicator, StyleSheet, View} from 'react-native' +import { + CreateAccountState, + CreateAccountDispatch, + useSubmitCreateAccount, +} from './state' import {StepHeader} from './StepHeader' -import {s} from 'lib/styles' -import {TextInput} from '../util/TextInput' -import {createFullHandle} from 'lib/strings/handles' -import {usePalette} from 'lib/hooks/usePalette' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {msg, Trans} from '@lingui/macro' +import {isWeb} from 'platform/detection' +import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -/** STEP 3: Your user handle - * @field User handle - */ +import {nanoid} from 'nanoid/non-secure' +import {CaptchaWebView} from 'view/com/auth/create/CaptchaWebView' +import {useTheme} from 'lib/ThemeContext' +import {createFullHandle} from 'lib/strings/handles' + +const CAPTCHA_PATH = '/gate/signup' + export function Step3({ uiState, uiDispatch, @@ -21,33 +25,66 @@ export function Step3({ uiState: CreateAccountState uiDispatch: CreateAccountDispatch }) { - const pal = usePalette('default') const {_} = useLingui() + const theme = useTheme() + const submit = useSubmitCreateAccount(uiState, uiDispatch) + + const [completed, setCompleted] = React.useState(false) + + const stateParam = React.useMemo(() => nanoid(15), []) + const url = React.useMemo(() => { + const newUrl = new URL(uiState.serviceUrl) + newUrl.pathname = CAPTCHA_PATH + newUrl.searchParams.set( + 'handle', + createFullHandle(uiState.handle, uiState.userDomain), + ) + newUrl.searchParams.set('state', stateParam) + newUrl.searchParams.set('colorScheme', theme.colorScheme) + + console.log(newUrl) + + return newUrl.href + }, [ + uiState.serviceUrl, + uiState.handle, + uiState.userDomain, + stateParam, + theme.colorScheme, + ]) + + const onSuccess = React.useCallback( + (code: string) => { + setCompleted(true) + submit(code) + }, + [submit], + ) + + const onError = React.useCallback(() => { + uiDispatch({ + type: 'set-error', + value: _(msg`Error receiving captcha response.`), + }) + }, [_, uiDispatch]) + return ( <View> - <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> - <View style={s.pb10}> - <TextInput - testID="handleInput" - icon="at" - placeholder={_(msg`e.g. alice`)} - value={uiState.handle} - editable - autoFocus - autoComplete="off" - autoCorrect={false} - onChange={value => uiDispatch({type: 'set-handle', value})} - // TODO: Add explicit text label - accessibilityLabel={_(msg`User handle`)} - accessibilityHint={_(msg`Input your user handle`)} - /> - <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> - <Trans>Your full handle will be</Trans>{' '} - <Text type="lg-bold" style={pal.text}> - @{createFullHandle(uiState.handle, uiState.userDomain)} - </Text> - </Text> + <StepHeader uiState={uiState} title={_(msg`Complete the challenge`)} /> + <View style={[styles.container, completed && styles.center]}> + {!completed ? ( + <CaptchaWebView + url={url} + stateParam={stateParam} + uiState={uiState} + onSuccess={onSuccess} + onError={onError} + /> + ) : ( + <ActivityIndicator size="large" /> + )} </View> + {uiState.error ? ( <ErrorMessage message={uiState.error} style={styles.error} /> ) : undefined} @@ -58,5 +95,20 @@ export function Step3({ const styles = StyleSheet.create({ error: { borderRadius: 6, + marginTop: 10, + }, + // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. + touchable: { + ...(isWeb && {cursor: 'pointer'}), + }, + container: { + minHeight: 500, + width: '100%', + paddingBottom: 20, + overflow: 'hidden', + }, + center: { + alignItems: 'center', + justifyContent: 'center', }, }) diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx index af6bf5478..a98b392d8 100644 --- a/src/view/com/auth/create/StepHeader.tsx +++ b/src/view/com/auth/create/StepHeader.tsx @@ -11,7 +11,7 @@ export function StepHeader({ children, }: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) { const pal = usePalette('default') - const numSteps = uiState.isPhoneVerificationRequired ? 3 : 2 + const numSteps = 3 return ( <View style={styles.container}> <View> diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts index e8a7cd4ed..276eaf924 100644 --- a/src/view/com/auth/create/state.ts +++ b/src/view/com/auth/create/state.ts @@ -1,8 +1,7 @@ -import {useReducer} from 'react' +import {useCallback, useReducer} from 'react' import { ComAtprotoServerDescribeServer, ComAtprotoServerCreateAccount, - BskyAgent, } from '@atproto/api' import {I18nContext, useLingui} from '@lingui/react' import {msg} from '@lingui/macro' @@ -11,10 +10,14 @@ import {getAge} from 'lib/strings/time' import {logger} from '#/logger' import {createFullHandle} from '#/lib/strings/handles' import {cleanError} from '#/lib/strings/errors' -import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding' -import {ApiContext as SessionApiContext} from '#/state/session' -import {DEFAULT_SERVICE} from '#/lib/constants' -import parsePhoneNumber, {CountryCode} from 'libphonenumber-js' +import {useOnboardingDispatch} from '#/state/shell/onboarding' +import {useSessionApi} from '#/state/session' +import {DEFAULT_SERVICE, IS_PROD} from '#/lib/constants' +import { + DEFAULT_PROD_FEEDS, + usePreferencesSetBirthDateMutation, + useSetSaveFeedsMutation, +} from 'state/queries/preferences' export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago @@ -29,10 +32,6 @@ export type CreateAccountAction = | {type: 'set-invite-code'; value: string} | {type: 'set-email'; value: string} | {type: 'set-password'; value: string} - | {type: 'set-phone-country'; value: CountryCode} - | {type: 'set-verification-phone'; value: string} - | {type: 'set-verification-code'; value: string} - | {type: 'set-has-requested-verification-code'; value: boolean} | {type: 'set-handle'; value: string} | {type: 'set-birth-date'; value: Date} | {type: 'next'} @@ -49,10 +48,6 @@ export interface CreateAccountState { inviteCode: string email: string password: string - phoneCountry: CountryCode - verificationPhone: string - verificationCode: string - hasRequestedVerificationCode: boolean handle: string birthDate: Date @@ -60,13 +55,14 @@ export interface CreateAccountState { canBack: boolean canNext: boolean isInviteCodeRequired: boolean - isPhoneVerificationRequired: boolean + isCaptchaRequired: boolean } export type CreateAccountDispatch = (action: CreateAccountAction) => void export function useCreateAccount() { const {_} = useLingui() + return useReducer(createReducer({_}), { step: 1, error: undefined, @@ -77,144 +73,126 @@ export function useCreateAccount() { inviteCode: '', email: '', password: '', - phoneCountry: 'US', - verificationPhone: '', - verificationCode: '', - hasRequestedVerificationCode: false, handle: '', birthDate: DEFAULT_DATE, canBack: false, canNext: false, isInviteCodeRequired: false, - isPhoneVerificationRequired: false, + isCaptchaRequired: false, }) } -export async function requestVerificationCode({ - uiState, - uiDispatch, - _, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch - _: I18nContext['_'] -}) { - const phoneNumber = parsePhoneNumber( - uiState.verificationPhone, - uiState.phoneCountry, - )?.number - if (!phoneNumber) { - return - } - uiDispatch({type: 'set-error', value: ''}) - uiDispatch({type: 'set-processing', value: true}) - uiDispatch({type: 'set-verification-phone', value: phoneNumber}) - try { - const agent = new BskyAgent({service: uiState.serviceUrl}) - await agent.com.atproto.temp.requestPhoneVerification({ - phoneNumber, - }) - uiDispatch({type: 'set-has-requested-verification-code', value: true}) - } catch (e: any) { - logger.error( - `Failed to request sms verification code (${e.status} status)`, - {message: e}, - ) - uiDispatch({type: 'set-error', value: cleanError(e.toString())}) - } - uiDispatch({type: 'set-processing', value: false}) -} +export function useSubmitCreateAccount( + uiState: CreateAccountState, + uiDispatch: CreateAccountDispatch, +) { + const {_} = useLingui() + const {createAccount} = useSessionApi() + const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() + const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() + const onboardingDispatch = useOnboardingDispatch() -export async function submit({ - createAccount, - onboardingDispatch, - uiState, - uiDispatch, - _, -}: { - createAccount: SessionApiContext['createAccount'] - onboardingDispatch: OnboardingDispatchContext - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch - _: I18nContext['_'] -}) { - if (!uiState.email) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please enter your email.`), - }) - } - if (!EmailValidator.validate(uiState.email)) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Your email appears to be invalid.`), - }) - } - if (!uiState.password) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please choose your password.`), - }) - } - if ( - uiState.isPhoneVerificationRequired && - (!uiState.verificationPhone || !uiState.verificationCode) - ) { - uiDispatch({type: 'set-step', value: 2}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please enter the code you received by SMS.`), - }) - } - if (!uiState.handle) { - uiDispatch({type: 'set-step', value: 3}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please choose your handle.`), - }) - } - uiDispatch({type: 'set-error', value: ''}) - uiDispatch({type: 'set-processing', value: true}) + return useCallback( + async (verificationCode?: string) => { + if (!uiState.email) { + uiDispatch({type: 'set-step', value: 1}) + console.log('no email?') + return uiDispatch({ + type: 'set-error', + value: _(msg`Please enter your email.`), + }) + } + if (!EmailValidator.validate(uiState.email)) { + uiDispatch({type: 'set-step', value: 1}) + return uiDispatch({ + type: 'set-error', + value: _(msg`Your email appears to be invalid.`), + }) + } + if (!uiState.password) { + uiDispatch({type: 'set-step', value: 1}) + return uiDispatch({ + type: 'set-error', + value: _(msg`Please choose your password.`), + }) + } + if (!uiState.handle) { + uiDispatch({type: 'set-step', value: 2}) + return uiDispatch({ + type: 'set-error', + value: _(msg`Please choose your handle.`), + }) + } + if (uiState.isCaptchaRequired && !verificationCode) { + uiDispatch({type: 'set-step', value: 3}) + return uiDispatch({ + type: 'set-error', + value: _(msg`Please complete the verification captcha.`), + }) + } + uiDispatch({type: 'set-error', value: ''}) + uiDispatch({type: 'set-processing', value: true}) - try { - onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view - await createAccount({ - service: uiState.serviceUrl, - email: uiState.email, - handle: createFullHandle(uiState.handle, uiState.userDomain), - password: uiState.password, - inviteCode: uiState.inviteCode.trim(), - verificationPhone: uiState.verificationPhone.trim(), - verificationCode: uiState.verificationCode.trim(), - }) - } catch (e: any) { - onboardingDispatch({type: 'skip'}) // undo starting the onboard - let errMsg = e.toString() - if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { - errMsg = _( - msg`Invite code not accepted. Check that you input it correctly and try again.`, - ) - uiDispatch({type: 'set-step', value: 1}) - } else if (e.error === 'InvalidPhoneVerification') { - uiDispatch({type: 'set-step', value: 2}) - } + try { + onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view + await createAccount({ + service: uiState.serviceUrl, + email: uiState.email, + handle: createFullHandle(uiState.handle, uiState.userDomain), + password: uiState.password, + inviteCode: uiState.inviteCode.trim(), + verificationCode: uiState.isCaptchaRequired + ? verificationCode + : undefined, + }) + setBirthDate({birthDate: uiState.birthDate}) + if (IS_PROD(uiState.serviceUrl)) { + setSavedFeeds(DEFAULT_PROD_FEEDS) + } + } catch (e: any) { + onboardingDispatch({type: 'skip'}) // undo starting the onboard + let errMsg = e.toString() + if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { + errMsg = _( + msg`Invite code not accepted. Check that you input it correctly and try again.`, + ) + uiDispatch({type: 'set-step', value: 1}) + } - if ([400, 429].includes(e.status)) { - logger.warn('Failed to create account', {message: e}) - } else { - logger.error(`Failed to create account (${e.status} status)`, { - message: e, - }) - } + if ([400, 429].includes(e.status)) { + logger.warn('Failed to create account', {message: e}) + } else { + logger.error(`Failed to create account (${e.status} status)`, { + message: e, + }) + } - uiDispatch({type: 'set-processing', value: false}) - uiDispatch({type: 'set-error', value: cleanError(errMsg)}) - throw e - } + const error = cleanError(errMsg) + const isHandleError = error.toLowerCase().includes('handle') + + uiDispatch({type: 'set-processing', value: false}) + uiDispatch({type: 'set-error', value: cleanError(errMsg)}) + uiDispatch({type: 'set-step', value: isHandleError ? 2 : 1}) + } + }, + [ + uiState.email, + uiState.password, + uiState.handle, + uiState.isCaptchaRequired, + uiState.serviceUrl, + uiState.userDomain, + uiState.inviteCode, + uiState.birthDate, + uiDispatch, + _, + onboardingDispatch, + createAccount, + setBirthDate, + setSavedFeeds, + ], + ) } export function is13(state: CreateAccountState) { @@ -269,22 +247,6 @@ function createReducer({_}: {_: I18nContext['_']}) { case 'set-password': { return compute({...state, password: action.value}) } - case 'set-phone-country': { - return compute({...state, phoneCountry: action.value}) - } - case 'set-verification-phone': { - return compute({ - ...state, - verificationPhone: action.value, - hasRequestedVerificationCode: false, - }) - } - case 'set-verification-code': { - return compute({...state, verificationCode: action.value.trim()}) - } - case 'set-has-requested-verification-code': { - return compute({...state, hasRequestedVerificationCode: action.value}) - } case 'set-handle': { return compute({...state, handle: action.value}) } @@ -302,18 +264,10 @@ function createReducer({_}: {_: I18nContext['_']}) { }) } } - let increment = 1 - if (state.step === 1 && !state.isPhoneVerificationRequired) { - increment = 2 - } - return compute({...state, error: '', step: state.step + increment}) + return compute({...state, error: '', step: state.step + 1}) } case 'back': { - let decrement = 1 - if (state.step === 3 && !state.isPhoneVerificationRequired) { - decrement = 2 - } - return compute({...state, error: '', step: state.step - decrement}) + return compute({...state, error: '', step: state.step - 1}) } } } @@ -328,23 +282,16 @@ function compute(state: CreateAccountState): CreateAccountState { !!state.email && !!state.password } else if (state.step === 2) { - canNext = - !state.isPhoneVerificationRequired || - (!!state.verificationPhone && - isValidVerificationCode(state.verificationCode)) - } else if (state.step === 3) { canNext = !!state.handle + } else if (state.step === 3) { + // Step 3 will automatically redirect as soon as the captcha completes + canNext = false } return { ...state, canBack: state.step > 1, canNext, isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired, - isPhoneVerificationRequired: - !!state.serviceDescription?.phoneVerificationRequired, + isCaptchaRequired: !!state.serviceDescription?.phoneVerificationRequired, } } - -function isValidVerificationCode(str: string): boolean { - return /[0-9]{6}/.test(str) -} diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 9595e77e5..d8da569b1 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -46,7 +46,7 @@ export function FeedPage({ renderEmptyState: () => JSX.Element renderEndOfFeed?: () => JSX.Element }) { - const {isSandbox, hasSession} = useSession() + const {hasSession} = useSession() const pal = usePalette('default') const {_} = useLingui() const navigation = useNavigation() @@ -119,7 +119,7 @@ export function FeedPage({ style={[pal.text, {fontWeight: 'bold'}]} text={ <> - {isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} + Bluesky{' '} {hasNew && ( <View style={{ @@ -154,16 +154,7 @@ export function FeedPage({ ) } return <></> - }, [ - isDesktop, - pal.view, - pal.text, - pal.textLight, - hasNew, - _, - isSandbox, - hasSession, - ]) + }, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, hasSession]) return ( <View testID={testID} style={s.h100pct}> diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 9fe03b7e9..fb52b913a 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -37,7 +37,6 @@ export function FeedsTabBar( function FeedsTabBarPublic() { const pal = usePalette('default') - const {isSandbox} = useSession() return ( <CenteredView sideBorders> @@ -56,23 +55,7 @@ function FeedsTabBarPublic() { type="title-lg" href="/" style={[pal.text, {fontWeight: 'bold'}]} - text={ - <> - {isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} - {/*hasNew && ( - <View - style={{ - top: -8, - backgroundColor: colors.blue3, - width: 8, - height: 8, - borderRadius: 4, - }} - /> - )*/} - </> - } - // onPress={emitSoftReset} + text="Bluesky " /> </View> </CenteredView> diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 2b08bc402..434f018fc 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -43,10 +43,13 @@ import { usePreferencesQuery, } from '#/state/queries/preferences' import {useSession} from '#/state/session' -import {isAndroid, isNative} from '#/platform/detection' -import {logger} from '#/logger' +import {isAndroid, isNative, isWeb} from '#/platform/detection' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +// FlatList maintainVisibleContentPosition breaks if too many items +// are prepended. This seems to be an optimal number based on *shrug*. +const PARENTS_CHUNK_SIZE = 15 + const MAINTAIN_VISIBLE_CONTENT_POSITION = { // We don't insert any elements before the root row while loading. // So the row we want to use as the scroll anchor is the first row. @@ -165,8 +168,10 @@ function PostThreadLoaded({ const {isMobile, isTabletOrMobile} = useWebMediaQueries() const ref = useRef<ListMethods>(null) const highlightedPostRef = useRef<View | null>(null) - const [maxVisible, setMaxVisible] = React.useState(100) - const [isPTRing, setIsPTRing] = React.useState(false) + const [maxParents, setMaxParents] = React.useState( + isWeb ? Infinity : PARENTS_CHUNK_SIZE, + ) + const [maxReplies, setMaxReplies] = React.useState(100) const treeView = React.useMemo( () => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread), [threadViewPrefs, thread], @@ -206,10 +211,18 @@ function PostThreadLoaded({ // maintainVisibleContentPosition and onContentSizeChange // to "hold onto" the correct row instead of the first one. } else { - // Everything is loaded. - arr.push(TOP_COMPONENT) - for (const parent of parents) { - arr.push(parent) + // Everything is loaded + let startIndex = Math.max(0, parents.length - maxParents) + if (startIndex === 0) { + arr.push(TOP_COMPONENT) + } else { + // When progressively revealing parents, rendering a placeholder + // here will cause scrolling jumps. Don't add it unless you test it. + // QT'ing this thread is a great way to test all the scrolling hacks: + // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o + } + for (let i = startIndex; i < parents.length; i++) { + arr.push(parents[i]) } } } @@ -220,17 +233,18 @@ function PostThreadLoaded({ if (highlightedPost.ctx.isChildLoading) { arr.push(CHILD_SPINNER) } else { - for (const reply of replies) { - arr.push(reply) + for (let i = 0; i < replies.length; i++) { + arr.push(replies[i]) + if (i === maxReplies) { + arr.push(LOAD_MORE) + break + } } arr.push(BOTTOM_COMPONENT) } } - if (arr.length > maxVisible) { - arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) - } return arr - }, [skeleton, maxVisible, deferParents]) + }, [skeleton, deferParents, maxParents, maxReplies]) // This is only used on the web to keep the post in view when its parents load. // On native, we rely on `maintainVisibleContentPosition` instead. @@ -258,15 +272,28 @@ function PostThreadLoaded({ } }, [thread]) - const onPTR = React.useCallback(async () => { - setIsPTRing(true) - try { - await onRefresh() - } catch (err) { - logger.error('Failed to refresh posts thread', {message: err}) + // On native, we reveal parents in chunks. Although they're all already + // loaded and FlatList already has its own virtualization, unfortunately FlatList + // has a bug that causes the content to jump around if too many items are getting + // prepended at once. It also jumps around if items get prepended during scroll. + // To work around this, we prepend rows after scroll bumps against the top and rests. + const needsBumpMaxParents = React.useRef(false) + const onStartReached = React.useCallback(() => { + if (maxParents < skeleton.parents.length) { + needsBumpMaxParents.current = true + } + }, [maxParents, skeleton.parents.length]) + const bumpMaxParentsIfNeeded = React.useCallback(() => { + if (!isNative) { + return + } + if (needsBumpMaxParents.current) { + needsBumpMaxParents.current = false + setMaxParents(n => n + PARENTS_CHUNK_SIZE) } - setIsPTRing(false) - }, [setIsPTRing, onRefresh]) + }, []) + const onMomentumScrollEnd = bumpMaxParentsIfNeeded + const onScrollToTop = bumpMaxParentsIfNeeded const renderItem = React.useCallback( ({item, index}: {item: RowItem; index: number}) => { @@ -301,7 +328,7 @@ function PostThreadLoaded({ } else if (item === LOAD_MORE) { return ( <Pressable - onPress={() => setMaxVisible(n => n + 50)} + onPress={() => setMaxReplies(n => n + 50)} style={[pal.border, pal.view, styles.itemContainer]} accessibilityLabel={_(msg`Load more posts`)} accessibilityHint=""> @@ -345,6 +372,8 @@ function PostThreadLoaded({ const next = isThreadPost(posts[index - 1]) ? (posts[index - 1] as ThreadPost) : undefined + const hasUnrevealedParents = + index === 0 && maxParents < skeleton.parents.length return ( <View ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined} @@ -360,7 +389,9 @@ function PostThreadLoaded({ hasMore={item.ctx.hasMore} showChildReplyLine={item.ctx.showChildReplyLine} showParentReplyLine={item.ctx.showParentReplyLine} - hasPrecedingItem={!!prev?.ctx.showChildReplyLine} + hasPrecedingItem={ + !!prev?.ctx.showChildReplyLine || hasUnrevealedParents + } onPostReply={onRefresh} /> </View> @@ -383,6 +414,8 @@ function PostThreadLoaded({ onRefresh, deferParents, treeView, + skeleton.parents.length, + maxParents, _, ], ) @@ -393,9 +426,10 @@ function PostThreadLoaded({ data={posts} keyExtractor={item => item._reactKey} renderItem={renderItem} - refreshing={isPTRing} - onRefresh={onPTR} onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} + onStartReached={onStartReached} + onMomentumScrollEnd={onMomentumScrollEnd} + onScrollToTop={onScrollToTop} maintainVisibleContentPosition={ isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined } diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index fc13fa0eb..c66947d44 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -27,7 +27,6 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' -import {PostSandboxWarning} from '../util/PostSandboxWarning' import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' import {formatCount} from '../util/numeric/format' @@ -44,6 +43,7 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {ThreadPost} from '#/state/queries/post-thread' import {useSession} from 'state/session' import {WhoCanReply} from '../threadgate/WhoCanReply' +import {LoadingPlaceholder} from '../util/LoadingPlaceholder' export function PostThreadItem({ post, @@ -164,8 +164,6 @@ let PostThreadItemLoaded = ({ () => countLines(richText?.text) >= MAX_POST_LINES, ) const {currentAccount} = useSession() - const hasEngagement = post.likeCount || post.repostCount - const rootUri = record.reply?.root?.uri || post.uri const postHref = React.useMemo(() => { const urip = new AtUri(post.uri) @@ -248,7 +246,6 @@ let PostThreadItemLoaded = ({ testID={`postThreadItem-by-${post.author.handle}`} style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} accessible={false}> - <PostSandboxWarning /> <View style={[styles.layout]}> <View style={[styles.layoutAvi, {paddingBottom: 8}]}> <PreviewableUserAvatar @@ -357,9 +354,16 @@ let PostThreadItemLoaded = ({ translatorUrl={translatorUrl} needsTranslation={needsTranslation} /> - {hasEngagement ? ( + {post.repostCount !== 0 || post.likeCount !== 0 ? ( + // Show this section unless we're *sure* it has no engagement. <View style={[styles.expandedInfo, pal.border]}> - {post.repostCount ? ( + {post.repostCount == null && post.likeCount == null && ( + // If we're still loading and not sure, assume this post has engagement. + // This lets us avoid a layout shift for the common case (embedded post with likes/reposts). + // TODO: embeds should include metrics to avoid us having to guess. + <LoadingPlaceholder width={50} height={20} /> + )} + {post.repostCount != null && post.repostCount !== 0 ? ( <Link style={styles.expandedInfoItem} href={repostsHref} @@ -374,10 +378,8 @@ let PostThreadItemLoaded = ({ {pluralize(post.repostCount, 'repost')} </Text> </Link> - ) : ( - <></> - )} - {post.likeCount ? ( + ) : null} + {post.likeCount != null && post.likeCount !== 0 ? ( <Link style={styles.expandedInfoItem} href={likesHref} @@ -392,13 +394,9 @@ let PostThreadItemLoaded = ({ {pluralize(post.likeCount, 'like')} </Text> </Link> - ) : ( - <></> - )} + ) : null} </View> - ) : ( - <></> - )} + ) : null} <View style={[s.pl10, s.pr10, s.pb5]}> <PostCtrls big @@ -438,8 +436,6 @@ let PostThreadItemLoaded = ({ ? {marginRight: 4} : {marginLeft: 2, marginRight: 2} }> - <PostSandboxWarning /> - <View style={{ flexDirection: 'row', diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 920409ec6..8d0f2bef2 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -21,7 +21,6 @@ import {PostEmbeds} from '../util/post-embeds' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' import {RichText} from '../util/text/RichText' -import {PostSandboxWarning} from '../util/PostSandboxWarning' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -160,8 +159,6 @@ let FeedItemInner = ({ href={href} noFeedback accessible={false}> - <PostSandboxWarning /> - <View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}> <View style={{width: 52}}> {isThreadChild && ( diff --git a/src/view/com/util/PostSandboxWarning.tsx b/src/view/com/util/PostSandboxWarning.tsx deleted file mode 100644 index b2375c703..000000000 --- a/src/view/com/util/PostSandboxWarning.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {Text} from './text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {useSession} from '#/state/session' - -export function PostSandboxWarning() { - const {isSandbox} = useSession() - const pal = usePalette('default') - if (isSandbox) { - return ( - <View style={styles.container}> - <Text - type="title-2xl" - style={[pal.text, styles.text]} - accessible={false}> - SANDBOX - </Text> - </View> - ) - } - return null -} - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - top: 6, - right: 10, - }, - text: { - fontWeight: 'bold', - opacity: 0.07, - }, -}) |