Internationalization & localization (#1822)
* install and setup lingui * setup dynamic locale activation and async loading * first pass of automated replacement of text messages * add some more documentaton * fix nits * add `es` and `hi`locales for testing purposes * make accessibilityLabel localized * compile and extract new messages * fix merge conflicts * fix eslint warning * change instructions from sending email to opening PR * fix commentszio/stable
parent
82059b7ee1
commit
4c7850f8c4
|
@ -45,6 +45,7 @@ module.exports = function (api) {
|
|||
},
|
||||
},
|
||||
],
|
||||
'macros',
|
||||
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
|
||||
],
|
||||
env: {
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
# Internationalization
|
||||
|
||||
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please open a PR or issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app)
|
||||
|
||||
## Tools
|
||||
We are using Lingui to manage translations. You can find the documentation [here](https://lingui.dev/).
|
||||
|
||||
### Adding new strings
|
||||
When adding a new string, do it as follows:
|
||||
```jsx
|
||||
// Before
|
||||
import { Text } from "react-native";
|
||||
|
||||
<Text>Hello World</Text>
|
||||
```
|
||||
|
||||
```jsx
|
||||
// After
|
||||
import { Text } from "react-native";
|
||||
import { Trans } from "@lingui/macro";
|
||||
|
||||
<Text><Trans>Hello World</Trans></Text>
|
||||
```
|
||||
|
||||
The `<Trans>` macro will extract the string and add it to the catalog. It is not really a component, but a macro. Further reading [here](https://lingui.dev/ref/macro.html)
|
||||
|
||||
However sometimes you will run into this case:
|
||||
```jsx
|
||||
// Before
|
||||
import { Text } from "react-native";
|
||||
|
||||
const text = "Hello World";
|
||||
<Text accessibilityLabel="Label is here">{text}</Text>
|
||||
```
|
||||
In this case, you cannot use the `useLingui()` hook:
|
||||
```jsx
|
||||
import { msg } from "@lingui/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
|
||||
const { _ } = useLingui();
|
||||
return <Text accessibilityLabel={_(msg`Label is here`)}>{text}</Text>
|
||||
```
|
||||
|
||||
If you want to do this outside of a React component, you can use the `t` macro instead (note: this won't react to changes if the locale is switched dynamically within the app):
|
||||
```jsx
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
const text = t`Hello World`;
|
||||
```
|
||||
|
||||
We can then run `yarn intl:extract` to update the catalog in `src/locale/locales/{locale}/messages.po`. This will add the new string to the catalog.
|
||||
We can then run `yarn intl:compile` to update the translation files in `src/locale/locales/{locale}/messages.js`. This will add the new string to the translation files.
|
||||
The configuration for translations is defined in `lingui.config.js`
|
||||
|
||||
So the workflow is as follows:
|
||||
1. Wrap messages in Trans macro
|
||||
2. Run `yarn intl:extract` command to generate message catalogs
|
||||
3. Translate message catalogs (send them to translators usually)
|
||||
4. Run `yarn intl:compile` to create runtime catalogs
|
||||
5. Load runtime catalog
|
||||
6. Enjoy translated app!
|
||||
|
||||
### Common pitfalls
|
||||
These pitfalls are memoization pitfalls that will cause the components to not re-render when the locale is changed -- causing stale translations to be shown.
|
||||
|
||||
```jsx
|
||||
import { msg } from "@lingui/macro";
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
const welcomeMessage = msg`Welcome!`;
|
||||
|
||||
// ❌ Bad! This code won't work
|
||||
export function Welcome() {
|
||||
const buggyWelcome = useMemo(() => {
|
||||
return i18n._(welcomeMessage);
|
||||
}, []);
|
||||
|
||||
return <div>{buggyWelcome}</div>;
|
||||
}
|
||||
|
||||
// ❌ Bad! This code won't work either because the reference to i18n does not change
|
||||
export function Welcome() {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const buggyWelcome = useMemo(() => {
|
||||
return i18n._(welcomeMessage);
|
||||
}, [i18n]);
|
||||
|
||||
return <div>{buggyWelcome}</div>;
|
||||
}
|
||||
|
||||
// ✅ Good! `useMemo` has i18n context in the dependency
|
||||
export function Welcome() {
|
||||
const linguiCtx = useLingui();
|
||||
|
||||
const welcome = useMemo(() => {
|
||||
return linguiCtx.i18n._(welcomeMessage);
|
||||
}, [linguiCtx]);
|
||||
|
||||
return <div>{welcome}</div>;
|
||||
}
|
||||
|
||||
// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context
|
||||
export function Welcome() {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const welcome = useMemo(() => {
|
||||
return _(welcomeMessage);
|
||||
}, [_]);
|
||||
|
||||
return <div>{welcome}</div>;
|
||||
}
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('@lingui/conf').LinguiConfig} */
|
||||
module.exports = {
|
||||
locales: ['en', 'cs', 'fr', 'hi', 'es'],
|
||||
catalogs: [
|
||||
{
|
||||
path: '<rootDir>/src/locale/locales/{locale}/messages',
|
||||
include: ['src'],
|
||||
},
|
||||
],
|
||||
format: 'po',
|
||||
}
|
10
package.json
10
package.json
|
@ -28,7 +28,9 @@
|
|||
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
|
||||
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
|
||||
"perf:measure": "NODE_ENV=test flashlight measure",
|
||||
"build:apk": "eas build -p android --profile dev-android-apk"
|
||||
"build:apk": "eas build -p android --profile dev-android-apk",
|
||||
"intl:extract": "lingui extract",
|
||||
"intl:compile": "lingui compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.6.23",
|
||||
|
@ -42,6 +44,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
||||
"@gorhom/bottom-sheet": "^4.5.1",
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@react-native-async-storage/async-storage": "1.18.2",
|
||||
|
@ -164,10 +167,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.2.5",
|
||||
"@babel/core": "^7.20.0",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.20.0",
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@did-plc/server": "^0.0.1",
|
||||
"@lingui/cli": "^4.5.0",
|
||||
"@lingui/macro": "^4.5.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||
"@react-native-community/eslint-config": "^3.0.0",
|
||||
"@testing-library/jest-native": "^5.4.1",
|
||||
|
@ -192,6 +197,7 @@
|
|||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"babel-jest": "^29.4.2",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"babel-plugin-react-native-web": "^0.18.12",
|
||||
"detox": "^20.13.0",
|
||||
|
|
|
@ -16,6 +16,9 @@ import {Shell} from 'view/shell/index'
|
|||
import {ToastContainer} from 'view/com/util/Toast.web'
|
||||
import {ThemeProvider} from 'lib/ThemeContext'
|
||||
import {queryClient} from 'lib/react-query'
|
||||
import {i18n} from '@lingui/core'
|
||||
import {I18nProvider} from '@lingui/react'
|
||||
import {defaultLocale, dynamicActivate} from './locale/i18n'
|
||||
import {Provider as ShellStateProvider} from 'state/shell'
|
||||
import {Provider as ModalStateProvider} from 'state/modals'
|
||||
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
||||
|
@ -34,6 +37,7 @@ const InnerApp = observer(function AppImpl() {
|
|||
setRootStore(store)
|
||||
analytics.init(store)
|
||||
})
|
||||
dynamicActivate(defaultLocale) // async import of locale data
|
||||
}, [])
|
||||
|
||||
// show nothing prior to init
|
||||
|
@ -47,9 +51,11 @@ const InnerApp = observer(function AppImpl() {
|
|||
<RootSiblingParent>
|
||||
<analytics.Provider>
|
||||
<RootStoreProvider value={rootStore}>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</I18nProvider>
|
||||
<ToastContainer />
|
||||
</RootStoreProvider>
|
||||
</analytics.Provider>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import {i18n} from '@lingui/core'
|
||||
|
||||
export const locales = {
|
||||
en: 'English',
|
||||
cs: 'Česky',
|
||||
fr: 'Français',
|
||||
hi: 'हिंदी',
|
||||
es: 'Español',
|
||||
}
|
||||
export const defaultLocale = 'en'
|
||||
|
||||
/**
|
||||
* We do a dynamic import of just the catalog that we need
|
||||
* @param locale any locale string
|
||||
*/
|
||||
export async function dynamicActivate(locale: string) {
|
||||
const {messages} = await import(`./locales/${locale}/messages`)
|
||||
i18n.load(locale, messages)
|
||||
i18n.activate(locale)
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,8 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
|||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const SplashScreen = ({
|
||||
onPressSignin,
|
||||
|
@ -14,14 +16,18 @@ export const SplashScreen = ({
|
|||
onPressCreateAccount: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<CenteredView style={[styles.container, pal.view]}>
|
||||
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||
<ErrorBoundary>
|
||||
<View style={styles.hero}>
|
||||
<Text style={[styles.title, pal.link]}>Bluesky</Text>
|
||||
<Text style={[styles.title, pal.link]}>
|
||||
<Trans>Bluesky</Trans>
|
||||
</Text>
|
||||
<Text style={[styles.subtitle, pal.textLight]}>
|
||||
See what's next
|
||||
<Trans>See what's next</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||
|
@ -30,10 +36,10 @@ export const SplashScreen = ({
|
|||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Create new account"
|
||||
accessibilityLabel={_(msg`Create new account`)}
|
||||
accessibilityHint="Opens flow to create a new Bluesky account">
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
Create a new account
|
||||
<Trans>Create a new account</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
|
@ -41,9 +47,11 @@ export const SplashScreen = ({
|
|||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Sign in"
|
||||
accessibilityLabel={_(msg`Sign in`)}
|
||||
accessibilityHint="Opens flow to sign into your existing Bluesky account">
|
||||
<Text style={[pal.text, styles.btnLabel]}>Sign In</Text>
|
||||
<Text style={[pal.text, styles.btnLabel]}>
|
||||
<Trans>Sign In</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {CenteredView} from '../util/Views'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
export const SplashScreen = ({
|
||||
onPressSignin,
|
||||
|
@ -54,7 +55,9 @@ export const SplashScreen = ({
|
|||
onPress={onPressSignin}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text style={[pal.text, styles.btnLabel]}>Sign In</Text>
|
||||
<Text style={[pal.text, styles.btnLabel]}>
|
||||
<Trans>Sign In</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -15,6 +15,8 @@ import {s} from 'lib/styles'
|
|||
import {useStores} from 'state/index'
|
||||
import {CreateAccountModel} from 'state/models/ui/create-account'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
|
||||
import {Step1} from './Step1'
|
||||
|
@ -30,6 +32,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
|||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const model = React.useMemo(() => new CreateAccountModel(store), [store])
|
||||
const {_} = useLingui()
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -73,8 +76,8 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
|||
return (
|
||||
<LoggedOutLayout
|
||||
leadin={`Step ${model.step}`}
|
||||
title="Create Account"
|
||||
description="We're so excited to have you join us!">
|
||||
title={_(msg`Create Account`)}
|
||||
description={_(msg`We're so excited to have you join us!`)}>
|
||||
<ScrollView testID="createAccount" style={pal.view}>
|
||||
<KeyboardAvoidingView behavior="padding">
|
||||
<View style={styles.stepContainer}>
|
||||
|
@ -88,7 +91,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
|||
testID="backBtn"
|
||||
accessibilityRole="button">
|
||||
<Text type="xl" style={pal.link}>
|
||||
Back
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
|
@ -101,7 +104,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
|||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
<Trans>Next</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
@ -110,18 +113,18 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
|||
testID="retryConnectBtn"
|
||||
onPress={onPressRetryConnect}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityLabel={_(msg`Retry`)}
|
||||
accessibilityHint="Retries account creation"
|
||||
accessibilityLiveRegion="polite">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Retry
|
||||
<Trans>Retry</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : model.isFetchingServiceDescription ? (
|
||||
<>
|
||||
<ActivityIndicator color="#fff" />
|
||||
<Text type="xl" style={[pal.text, s.pr5]}>
|
||||
Connecting...
|
||||
<Trans>Connecting...</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : undefined}
|
||||
|
|
|
@ -12,6 +12,8 @@ import {HelpTip} from '../util/HelpTip'
|
|||
import {TextInput} from '../util/TextInput'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
|
||||
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
|
||||
|
@ -27,6 +29,7 @@ export const Step1 = observer(function Step1Impl({
|
|||
}) {
|
||||
const pal = usePalette('default')
|
||||
const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
|
||||
const {_} = useLingui()
|
||||
|
||||
const onPressDefault = React.useCallback(() => {
|
||||
setIsDefaultSelected(true)
|
||||
|
@ -63,9 +66,9 @@ export const Step1 = observer(function Step1Impl({
|
|||
|
||||
return (
|
||||
<View>
|
||||
<StepHeader step="1" title="Your hosting provider" />
|
||||
<StepHeader step="1" title={_(msg`Your hosting provider`)} />
|
||||
<Text style={[pal.text, s.mb10]}>
|
||||
This is the service that keeps you online.
|
||||
<Trans>This is the service that keeps you online.</Trans>
|
||||
</Text>
|
||||
<Option
|
||||
testID="blueskyServerBtn"
|
||||
|
@ -81,17 +84,17 @@ export const Step1 = observer(function Step1Impl({
|
|||
onPress={onPressOther}>
|
||||
<View style={styles.otherForm}>
|
||||
<Text nativeID="addressProvider" style={[pal.text, s.mb5]}>
|
||||
Enter the address of your provider:
|
||||
<Trans>Enter the address of your provider:</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="customServerInput"
|
||||
icon="globe"
|
||||
placeholder="Hosting provider address"
|
||||
placeholder={_(msg`Hosting provider address`)}
|
||||
value={model.serviceUrl}
|
||||
editable
|
||||
onChange={onChangeServiceUrl}
|
||||
accessibilityHint="Input hosting provider address"
|
||||
accessibilityLabel="Hosting provider address"
|
||||
accessibilityLabel={_(msg`Hosting provider address`)}
|
||||
accessibilityLabelledBy="addressProvider"
|
||||
/>
|
||||
{LOGIN_INCLUDE_DEV_SERVERS && (
|
||||
|
@ -100,13 +103,13 @@ export const Step1 = observer(function Step1Impl({
|
|||
testID="stagingServerBtn"
|
||||
type="default"
|
||||
style={s.mr5}
|
||||
label="Staging"
|
||||
label={_(msg`Staging`)}
|
||||
onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
|
||||
/>
|
||||
<Button
|
||||
testID="localDevServerBtn"
|
||||
type="default"
|
||||
label="Dev Server"
|
||||
label={_(msg`Dev Server`)}
|
||||
onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
|
||||
/>
|
||||
</View>
|
||||
|
@ -116,7 +119,7 @@ export const Step1 = observer(function Step1Impl({
|
|||
{model.error ? (
|
||||
<ErrorMessage message={model.error} style={styles.error} />
|
||||
) : (
|
||||
<HelpTip text="You can change hosting providers at any time." />
|
||||
<HelpTip text={_(msg`You can change hosting providers at any time.`)} />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -11,6 +11,8 @@ import {TextInput} from '../util/TextInput'
|
|||
import {Policies} from './Policies'
|
||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
/** STEP 2: Your account
|
||||
|
@ -28,6 +30,7 @@ export const Step2 = observer(function Step2Impl({
|
|||
model: CreateAccountModel
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
const onPressWaitlist = React.useCallback(() => {
|
||||
|
@ -36,7 +39,7 @@ export const Step2 = observer(function Step2Impl({
|
|||
|
||||
return (
|
||||
<View>
|
||||
<StepHeader step="2" title="Your account" />
|
||||
<StepHeader step="2" title={_(msg`Your account`)} />
|
||||
|
||||
{model.isInviteCodeRequired && (
|
||||
<View style={s.pb20}>
|
||||
|
@ -46,11 +49,11 @@ export const Step2 = observer(function Step2Impl({
|
|||
<TextInput
|
||||
testID="inviteCodeInput"
|
||||
icon="ticket"
|
||||
placeholder="Required for this provider"
|
||||
placeholder={_(msg`Required for this provider`)}
|
||||
value={model.inviteCode}
|
||||
editable
|
||||
onChange={model.setInviteCode}
|
||||
accessibilityLabel="Invite code"
|
||||
accessibilityLabel={_(msg`Invite code`)}
|
||||
accessibilityHint="Input invite code to proceed"
|
||||
/>
|
||||
</View>
|
||||
|
@ -61,10 +64,12 @@ export const Step2 = observer(function Step2Impl({
|
|||
Don't have an invite code?{' '}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPressWaitlist}
|
||||
accessibilityLabel="Join the waitlist."
|
||||
accessibilityLabel={_(msg`Join the waitlist.`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.touchable}>
|
||||
<Text style={pal.link}>Join the waitlist.</Text>
|
||||
<Text style={pal.link}>
|
||||
<Trans>Join the waitlist.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Text>
|
||||
|
@ -72,16 +77,16 @@ export const Step2 = observer(function Step2Impl({
|
|||
<>
|
||||
<View style={s.pb20}>
|
||||
<Text type="md-medium" style={[pal.text, s.mb2]} nativeID="email">
|
||||
Email address
|
||||
<Trans>Email address</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="emailInput"
|
||||
icon="envelope"
|
||||
placeholder="Enter your email address"
|
||||
placeholder={_(msg`Enter your email address`)}
|
||||
value={model.email}
|
||||
editable
|
||||
onChange={model.setEmail}
|
||||
accessibilityLabel="Email"
|
||||
accessibilityLabel={_(msg`Email`)}
|
||||
accessibilityHint="Input email for Bluesky waitlist"
|
||||
accessibilityLabelledBy="email"
|
||||
/>
|
||||
|
@ -92,17 +97,17 @@ export const Step2 = observer(function Step2Impl({
|
|||
type="md-medium"
|
||||
style={[pal.text, s.mb2]}
|
||||
nativeID="password">
|
||||
Password
|
||||
<Trans>Password</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="passwordInput"
|
||||
icon="lock"
|
||||
placeholder="Choose your password"
|
||||
placeholder={_(msg`Choose your password`)}
|
||||
value={model.password}
|
||||
editable
|
||||
secureTextEntry
|
||||
onChange={model.setPassword}
|
||||
accessibilityLabel="Password"
|
||||
accessibilityLabel={_(msg`Password`)}
|
||||
accessibilityHint="Set password"
|
||||
accessibilityLabelledBy="password"
|
||||
/>
|
||||
|
@ -113,7 +118,7 @@ export const Step2 = observer(function Step2Impl({
|
|||
type="md-medium"
|
||||
style={[pal.text, s.mb2]}
|
||||
nativeID="birthDate">
|
||||
Your birth date
|
||||
<Trans>Your birth date</Trans>
|
||||
</Text>
|
||||
<DateInput
|
||||
testID="birthdayInput"
|
||||
|
@ -122,7 +127,7 @@ export const Step2 = observer(function Step2Impl({
|
|||
buttonType="default-light"
|
||||
buttonStyle={[pal.border, styles.dateInputButton]}
|
||||
buttonLabelType="lg"
|
||||
accessibilityLabel="Birthday"
|
||||
accessibilityLabel={_(msg`Birthday`)}
|
||||
accessibilityHint="Enter your birth date"
|
||||
accessibilityLabelledBy="birthDate"
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,8 @@ 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 {useLingui} from '@lingui/react'
|
||||
|
||||
/** STEP 3: Your user handle
|
||||
* @field User handle
|
||||
|
@ -19,9 +21,10 @@ export const Step3 = observer(function Step3Impl({
|
|||
model: CreateAccountModel
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<View>
|
||||
<StepHeader step="3" title="Your user handle" />
|
||||
<StepHeader step="3" title={_(msg`Your user handle`)} />
|
||||
<View style={s.pb10}>
|
||||
<TextInput
|
||||
testID="handleInput"
|
||||
|
@ -31,12 +34,12 @@ export const Step3 = observer(function Step3Impl({
|
|||
editable
|
||||
onChange={model.setHandle}
|
||||
// TODO: Add explicit text label
|
||||
accessibilityLabel="User handle"
|
||||
accessibilityLabel={_(msg`User handle`)}
|
||||
accessibilityHint="Input your user handle"
|
||||
/>
|
||||
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
||||
Your full handle will be{' '}
|
||||
<Text type="lg-bold" style={pal.text}>
|
||||
<Trans>Your full handle will be</Trans>
|
||||
<Text type="lg-bold" style={[pal.text, s.ml5]}>
|
||||
@{createFullHandle(model.handle, model.userDomain)}
|
||||
</Text>
|
||||
</Text>
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {UserAvatar} from '../../util/UserAvatar'
|
||||
import {s} from 'lib/styles'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
import {AccountData} from 'state/models/session'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {styles} from './styles'
|
||||
|
||||
export const ChooseAccountForm = ({
|
||||
store,
|
||||
onSelectAccount,
|
||||
onPressBack,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
onSelectAccount: (account?: AccountData) => void
|
||||
onPressBack: () => void
|
||||
}) => {
|
||||
const {track, screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||
const {_} = useLingui()
|
||||
|
||||
React.useEffect(() => {
|
||||
screen('Choose Account')
|
||||
}, [screen])
|
||||
|
||||
const onTryAccount = async (account: AccountData) => {
|
||||
if (account.accessJwt && account.refreshJwt) {
|
||||
setIsProcessing(true)
|
||||
if (await store.session.resumeSession(account)) {
|
||||
track('Sign In', {resumedSession: true})
|
||||
setIsProcessing(false)
|
||||
return
|
||||
}
|
||||
setIsProcessing(false)
|
||||
}
|
||||
onSelectAccount(account)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
|
||||
<Trans>Sign in as...</Trans>
|
||||
</Text>
|
||||
{store.session.accounts.map(account => (
|
||||
<TouchableOpacity
|
||||
testID={`chooseAccountBtn-${account.handle}`}
|
||||
key={account.did}
|
||||
style={[pal.view, pal.border, styles.account]}
|
||||
onPress={() => onTryAccount(account)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
|
||||
accessibilityHint="Double tap to sign in">
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<View style={s.p10}>
|
||||
<UserAvatar avatar={account.aviUrl} size={30} />
|
||||
</View>
|
||||
<Text style={styles.accountText}>
|
||||
<Text type="lg-bold" style={pal.text}>
|
||||
{account.displayName || account.handle}{' '}
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.textLight]}>
|
||||
{account.handle}
|
||||
</Text>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
testID="chooseNewAccountBtn"
|
||||
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
||||
onPress={() => onSelectAccount(undefined)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Login to account that is not listed`)}
|
||||
accessibilityHint="">
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<Text style={[styles.accountText, styles.accountTextOther]}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Other account</Trans>
|
||||
</Text>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing && <ActivityIndicator />}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
import React, {useState, useEffect} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import * as EmailValidator from 'email-validator'
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
import {ServiceDescription} from 'state/models/session'
|
||||
import {isNetworkError} from 'lib/strings/errors'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {styles} from './styles'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const ForgotPasswordForm = ({
|
||||
error,
|
||||
serviceUrl,
|
||||
serviceDescription,
|
||||
setError,
|
||||
setServiceUrl,
|
||||
onPressBack,
|
||||
onEmailSent,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
error: string
|
||||
serviceUrl: string
|
||||
serviceDescription: ServiceDescription | undefined
|
||||
setError: (v: string) => void
|
||||
setServiceUrl: (v: string) => void
|
||||
onPressBack: () => void
|
||||
onEmailSent: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const {screen} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
useEffect(() => {
|
||||
screen('Signin:ForgotPassword')
|
||||
}, [screen])
|
||||
|
||||
const onPressSelectService = () => {
|
||||
openModal({
|
||||
name: 'server-input',
|
||||
initialService: serviceUrl,
|
||||
onSelect: setServiceUrl,
|
||||
})
|
||||
}
|
||||
|
||||
const onPressNext = async () => {
|
||||
if (!EmailValidator.validate(email)) {
|
||||
return setError('Your email appears to be invalid.')
|
||||
}
|
||||
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
const agent = new BskyAgent({service: serviceUrl})
|
||||
await agent.com.atproto.server.requestPasswordReset({email})
|
||||
onEmailSent()
|
||||
} catch (e: any) {
|
||||
const errMsg = e.toString()
|
||||
logger.warn('Failed to request password reset', {error: e})
|
||||
setIsProcessing(false)
|
||||
if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Unable to contact your service. Please check your Internet connection.',
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(errMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
<Trans>Reset password</Trans>
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, styles.instructions]}>
|
||||
<Trans>
|
||||
Enter the email you used to create your account. We'll send you a
|
||||
"reset code" so you can set a new password.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View
|
||||
testID="forgotPasswordView"
|
||||
style={[pal.borderDark, pal.view, styles.group]}>
|
||||
<TouchableOpacity
|
||||
testID="forgotPasswordSelectServiceButton"
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
||||
onPress={onPressSelectService}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Hosting provider`)}
|
||||
accessibilityHint="Sets hosting provider for password reset">
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="envelope"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="forgotPasswordEmail"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Email address"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel={_(msg`Email`)}
|
||||
accessibilityHint="Sets email for password reset"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : !email ? (
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||
<Trans>Next</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="newPasswordButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Go to next`)}
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
<Trans>Next</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
<Trans>Processing...</Trans>
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,37 +1,19 @@
|
|||
import React, {useState, useEffect, useRef} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import * as EmailValidator from 'email-validator'
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {KeyboardAvoidingView} from 'react-native'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {UserAvatar} from '../../util/UserAvatar'
|
||||
import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {createFullHandle} from 'lib/strings/handles'
|
||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||
import {useStores, RootStoreModel, DEFAULT_SERVICE} from 'state/index'
|
||||
import {useStores, DEFAULT_SERVICE} from 'state/index'
|
||||
import {ServiceDescription} from 'state/models/session'
|
||||
import {AccountData} from 'state/models/session'
|
||||
import {isNetworkError} from 'lib/strings/errors'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {logger} from '#/logger'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {ChooseAccountForm} from './ChooseAccountForm'
|
||||
import {LoginForm} from './LoginForm'
|
||||
import {ForgotPasswordForm} from './ForgotPasswordForm'
|
||||
import {SetNewPasswordForm} from './SetNewPasswordForm'
|
||||
import {PasswordUpdatedForm} from './PasswordUpdatedForm'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
enum Forms {
|
||||
Login,
|
||||
|
@ -45,6 +27,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const [error, setError] = useState<string>('')
|
||||
const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({})
|
||||
const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
|
||||
|
@ -87,14 +70,16 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
error: err,
|
||||
})
|
||||
setError(
|
||||
'Unable to contact your service. Please check your Internet connection.',
|
||||
_(
|
||||
msg`Unable to contact your service. Please check your Internet connection.`,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
return () => {
|
||||
aborted = true
|
||||
}
|
||||
}, [store.session, serviceUrl, retryDescribeTrigger])
|
||||
}, [store.session, serviceUrl, retryDescribeTrigger, _])
|
||||
|
||||
const onPressRetryConnect = () => setRetryDescribeTrigger({})
|
||||
const onPressForgotPassword = () => {
|
||||
|
@ -107,8 +92,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
{currentForm === Forms.Login ? (
|
||||
<LoggedOutLayout
|
||||
leadin=""
|
||||
title="Sign in"
|
||||
description="Enter your username and password">
|
||||
title={_(msg`Sign in`)}
|
||||
description={_(msg`Enter your username and password`)}>
|
||||
<LoginForm
|
||||
store={store}
|
||||
error={error}
|
||||
|
@ -126,8 +111,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
{currentForm === Forms.ChooseAccount ? (
|
||||
<LoggedOutLayout
|
||||
leadin=""
|
||||
title="Sign in as..."
|
||||
description="Select from an existing account">
|
||||
title={_(msg`Sign in as...`)}
|
||||
description={_(msg`Select from an existing account`)}>
|
||||
<ChooseAccountForm
|
||||
store={store}
|
||||
onSelectAccount={onSelectAccount}
|
||||
|
@ -138,8 +123,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
{currentForm === Forms.ForgotPassword ? (
|
||||
<LoggedOutLayout
|
||||
leadin=""
|
||||
title="Forgot Password"
|
||||
description="Let's get your password reset!">
|
||||
title={_(msg`Forgot Password`)}
|
||||
description={_(msg`Let's get your password reset!`)}>
|
||||
<ForgotPasswordForm
|
||||
store={store}
|
||||
error={error}
|
||||
|
@ -155,8 +140,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
{currentForm === Forms.SetNewPassword ? (
|
||||
<LoggedOutLayout
|
||||
leadin=""
|
||||
title="Forgot Password"
|
||||
description="Let's get your password reset!">
|
||||
title={_(msg`Forgot Password`)}
|
||||
description={_(msg`Let's get your password reset!`)}>
|
||||
<SetNewPasswordForm
|
||||
store={store}
|
||||
error={error}
|
||||
|
@ -173,830 +158,3 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
|
||||
const ChooseAccountForm = ({
|
||||
store,
|
||||
onSelectAccount,
|
||||
onPressBack,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
onSelectAccount: (account?: AccountData) => void
|
||||
onPressBack: () => void
|
||||
}) => {
|
||||
const {track, screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
screen('Choose Account')
|
||||
}, [screen])
|
||||
|
||||
const onTryAccount = async (account: AccountData) => {
|
||||
if (account.accessJwt && account.refreshJwt) {
|
||||
setIsProcessing(true)
|
||||
if (await store.session.resumeSession(account)) {
|
||||
track('Sign In', {resumedSession: true})
|
||||
setIsProcessing(false)
|
||||
return
|
||||
}
|
||||
setIsProcessing(false)
|
||||
}
|
||||
onSelectAccount(account)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
|
||||
Sign in as...
|
||||
</Text>
|
||||
{store.session.accounts.map(account => (
|
||||
<TouchableOpacity
|
||||
testID={`chooseAccountBtn-${account.handle}`}
|
||||
key={account.did}
|
||||
style={[pal.view, pal.border, styles.account]}
|
||||
onPress={() => onTryAccount(account)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Sign in as ${account.handle}`}
|
||||
accessibilityHint="Double tap to sign in">
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<View style={s.p10}>
|
||||
<UserAvatar avatar={account.aviUrl} size={30} />
|
||||
</View>
|
||||
<Text style={styles.accountText}>
|
||||
<Text type="lg-bold" style={pal.text}>
|
||||
{account.displayName || account.handle}{' '}
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.textLight]}>
|
||||
{account.handle}
|
||||
</Text>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
testID="chooseNewAccountBtn"
|
||||
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
||||
onPress={() => onSelectAccount(undefined)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Login to account that is not listed"
|
||||
accessibilityHint="">
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<Text style={[styles.accountText, styles.accountTextOther]}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
Other account
|
||||
</Text>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing && <ActivityIndicator />}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const LoginForm = ({
|
||||
store,
|
||||
error,
|
||||
serviceUrl,
|
||||
serviceDescription,
|
||||
initialHandle,
|
||||
setError,
|
||||
setServiceUrl,
|
||||
onPressRetryConnect,
|
||||
onPressBack,
|
||||
onPressForgotPassword,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
error: string
|
||||
serviceUrl: string
|
||||
serviceDescription: ServiceDescription | undefined
|
||||
initialHandle: string
|
||||
setError: (v: string) => void
|
||||
setServiceUrl: (v: string) => void
|
||||
onPressRetryConnect: () => void
|
||||
onPressBack: () => void
|
||||
onPressForgotPassword: () => void
|
||||
}) => {
|
||||
const {track} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [identifier, setIdentifier] = useState<string>(initialHandle)
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const passwordInputRef = useRef<TextInput>(null)
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
const onPressSelectService = () => {
|
||||
openModal({
|
||||
name: 'server-input',
|
||||
initialService: serviceUrl,
|
||||
onSelect: setServiceUrl,
|
||||
})
|
||||
Keyboard.dismiss()
|
||||
track('Signin:PressedSelectService')
|
||||
}
|
||||
|
||||
const onPressNext = async () => {
|
||||
Keyboard.dismiss()
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
// try to guess the handle if the user just gave their own username
|
||||
let fullIdent = identifier
|
||||
if (
|
||||
!identifier.includes('@') && // not an email
|
||||
!identifier.includes('.') && // not a domain
|
||||
serviceDescription &&
|
||||
serviceDescription.availableUserDomains.length > 0
|
||||
) {
|
||||
let matched = false
|
||||
for (const domain of serviceDescription.availableUserDomains) {
|
||||
if (fullIdent.endsWith(domain)) {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
fullIdent = createFullHandle(
|
||||
identifier,
|
||||
serviceDescription.availableUserDomains[0],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await store.session.login({
|
||||
service: serviceUrl,
|
||||
identifier: fullIdent,
|
||||
password,
|
||||
})
|
||||
} catch (e: any) {
|
||||
const errMsg = e.toString()
|
||||
logger.warn('Failed to login', {error: e})
|
||||
setIsProcessing(false)
|
||||
if (errMsg.includes('Authentication Required')) {
|
||||
setError('Invalid username or password')
|
||||
} else if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Unable to contact your service. Please check your Internet connection.',
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(errMsg))
|
||||
}
|
||||
} finally {
|
||||
track('Sign In', {resumedSession: false})
|
||||
}
|
||||
}
|
||||
|
||||
const isReady = !!serviceDescription && !!identifier && !!password
|
||||
return (
|
||||
<View testID="loginForm">
|
||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||
Sign into
|
||||
</Text>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="loginSelectServiceButton"
|
||||
style={styles.textBtn}
|
||||
onPress={onPressSelectService}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Select service"
|
||||
accessibilityHint="Sets server for the Bluesky client">
|
||||
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||
Account
|
||||
</Text>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="at"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="loginUsernameInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Username or email address"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
autoCorrect={false}
|
||||
autoComplete="username"
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus()
|
||||
}}
|
||||
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
value={identifier}
|
||||
onChangeText={str =>
|
||||
setIdentifier((str || '').toLowerCase().trim())
|
||||
}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel="Username or email address"
|
||||
accessibilityHint="Input the username or email address you used at signup"
|
||||
/>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="loginPasswordInput"
|
||||
ref={passwordInputRef}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Password"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoComplete="password"
|
||||
returnKeyType="done"
|
||||
enablesReturnKeyAutomatically={true}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
secureTextEntry={true}
|
||||
textContentType="password"
|
||||
clearButtonMode="while-editing"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
onSubmitEditing={onPressNext}
|
||||
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel="Password"
|
||||
accessibilityHint={
|
||||
identifier === ''
|
||||
? 'Input your password'
|
||||
: `Input the password tied to ${identifier}`
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="forgotPasswordButton"
|
||||
style={styles.textInputInnerBtn}
|
||||
onPress={onPressForgotPassword}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Forgot password"
|
||||
accessibilityHint="Opens password reset form">
|
||||
<Text style={pal.link}>Forgot</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{!serviceDescription && error ? (
|
||||
<TouchableOpacity
|
||||
testID="loginRetryButton"
|
||||
onPress={onPressRetryConnect}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityHint="Retries login">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : !serviceDescription ? (
|
||||
<>
|
||||
<ActivityIndicator />
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
Connecting...
|
||||
</Text>
|
||||
</>
|
||||
) : isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : isReady ? (
|
||||
<TouchableOpacity
|
||||
testID="loginNextButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go to next"
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const ForgotPasswordForm = ({
|
||||
error,
|
||||
serviceUrl,
|
||||
serviceDescription,
|
||||
setError,
|
||||
setServiceUrl,
|
||||
onPressBack,
|
||||
onEmailSent,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
error: string
|
||||
serviceUrl: string
|
||||
serviceDescription: ServiceDescription | undefined
|
||||
setError: (v: string) => void
|
||||
setServiceUrl: (v: string) => void
|
||||
onPressBack: () => void
|
||||
onEmailSent: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const {screen} = useAnalytics()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
useEffect(() => {
|
||||
screen('Signin:ForgotPassword')
|
||||
}, [screen])
|
||||
|
||||
const onPressSelectService = () => {
|
||||
openModal({
|
||||
name: 'server-input',
|
||||
initialService: serviceUrl,
|
||||
onSelect: setServiceUrl,
|
||||
})
|
||||
}
|
||||
|
||||
const onPressNext = async () => {
|
||||
if (!EmailValidator.validate(email)) {
|
||||
return setError('Your email appears to be invalid.')
|
||||
}
|
||||
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
const agent = new BskyAgent({service: serviceUrl})
|
||||
await agent.com.atproto.server.requestPasswordReset({email})
|
||||
onEmailSent()
|
||||
} catch (e: any) {
|
||||
const errMsg = e.toString()
|
||||
logger.warn('Failed to request password reset', {error: e})
|
||||
setIsProcessing(false)
|
||||
if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Unable to contact your service. Please check your Internet connection.',
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(errMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
Reset password
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, styles.instructions]}>
|
||||
Enter the email you used to create your account. We'll send you a
|
||||
"reset code" so you can set a new password.
|
||||
</Text>
|
||||
<View
|
||||
testID="forgotPasswordView"
|
||||
style={[pal.borderDark, pal.view, styles.group]}>
|
||||
<TouchableOpacity
|
||||
testID="forgotPasswordSelectServiceButton"
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
||||
onPress={onPressSelectService}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Hosting provider"
|
||||
accessibilityHint="Sets hosting provider for password reset">
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="envelope"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="forgotPasswordEmail"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Email address"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel="Email"
|
||||
accessibilityHint="Sets email for password reset"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : !email ? (
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||
Next
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="newPasswordButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go to next"
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
Processing...
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SetNewPasswordForm = ({
|
||||
error,
|
||||
serviceUrl,
|
||||
setError,
|
||||
onPressBack,
|
||||
onPasswordSet,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
error: string
|
||||
serviceUrl: string
|
||||
setError: (v: string) => void
|
||||
onPressBack: () => void
|
||||
onPasswordSet: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {screen} = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
screen('Signin:SetNewPasswordForm')
|
||||
}, [screen])
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [resetCode, setResetCode] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
||||
const onPressNext = async () => {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
const agent = new BskyAgent({service: serviceUrl})
|
||||
const token = resetCode.replace(/\s/g, '')
|
||||
await agent.com.atproto.server.resetPassword({
|
||||
token,
|
||||
password,
|
||||
})
|
||||
onPasswordSet()
|
||||
} catch (e: any) {
|
||||
const errMsg = e.toString()
|
||||
logger.warn('Failed to set new password', {error: e})
|
||||
setIsProcessing(false)
|
||||
if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Unable to contact your service. Please check your Internet connection.',
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(errMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
Set new password
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||
You will receive an email with a "reset code." Enter that code here,
|
||||
then enter your new password.
|
||||
</Text>
|
||||
<View
|
||||
testID="newPasswordView"
|
||||
style={[pal.view, pal.borderDark, styles.group]}>
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ticket"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="resetCodeInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Reset code"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
autoFocus
|
||||
value={resetCode}
|
||||
onChangeText={setResetCode}
|
||||
editable={!isProcessing}
|
||||
accessible={true}
|
||||
accessibilityLabel="Reset code"
|
||||
accessibilityHint="Input code sent to your email for password reset"
|
||||
/>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="newPasswordInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="New password"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isProcessing}
|
||||
accessible={true}
|
||||
accessibilityLabel="Password"
|
||||
accessibilityHint="Input new password"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : !resetCode || !password ? (
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||
Next
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="setNewPasswordButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Go to next"
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isProcessing ? (
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
Updating...
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
|
||||
const {screen} = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
screen('Signin:PasswordUpdatedForm')
|
||||
}, [screen])
|
||||
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
Password updated!
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||
You can now sign in with your new password.
|
||||
</Text>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Close alert"
|
||||
accessibilityHint="Closes password update alert">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Okay
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screenTitle: {
|
||||
marginBottom: 10,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
instructions: {
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
group: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
groupLabel: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
groupContent: {
|
||||
borderTopWidth: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
noTopBorder: {
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
groupContentIcon: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
account: {
|
||||
borderTopWidth: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
accountLast: {
|
||||
borderBottomWidth: 1,
|
||||
marginBottom: 20,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 17,
|
||||
letterSpacing: 0.25,
|
||||
fontWeight: '400',
|
||||
borderRadius: 10,
|
||||
},
|
||||
textInputInnerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
textBtn: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
textBtnLabel: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
textBtnFakeInnerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 6,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
accountText: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
accountTextOther: {
|
||||
paddingLeft: 12,
|
||||
},
|
||||
error: {
|
||||
backgroundColor: colors.red4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: -5,
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 15,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
errorIcon: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.white,
|
||||
color: colors.white,
|
||||
borderRadius: 30,
|
||||
width: 16,
|
||||
height: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 5,
|
||||
},
|
||||
dimmed: {opacity: 0.5},
|
||||
|
||||
maxHeight: {
|
||||
// @ts-ignore web only -prf
|
||||
maxHeight: isWeb ? '100vh' : undefined,
|
||||
height: !isWeb ? '100%' : undefined,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
import React, {useState, useRef} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Keyboard,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {createFullHandle} from 'lib/strings/handles'
|
||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
import {ServiceDescription} from 'state/models/session'
|
||||
import {isNetworkError} from 'lib/strings/errors'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {styles} from './styles'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const LoginForm = ({
|
||||
store,
|
||||
error,
|
||||
serviceUrl,
|
||||
serviceDescription,
|
||||
initialHandle,
|
||||
setError,
|
||||
setServiceUrl,
|
||||
onPressRetryConnect,
|
||||
onPressBack,
|
||||
onPressForgotPassword,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
error: string
|
||||
serviceUrl: string
|
||||
serviceDescription: ServiceDescription | undefined
|
||||
initialHandle: string
|
||||
setError: (v: string) => void
|
||||
setServiceUrl: (v: string) => void
|
||||
onPressRetryConnect: () => void
|
||||
onPressBack: () => void
|
||||
onPressForgotPassword: () => void
|
||||
}) => {
|
||||
const {track} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [identifier, setIdentifier] = useState<string>(initialHandle)
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const passwordInputRef = useRef<TextInput>(null)
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
const onPressSelectService = () => {
|
||||
openModal({
|
||||
name: 'server-input',
|
||||
initialService: serviceUrl,
|
||||
onSelect: setServiceUrl,
|
||||
})
|
||||
Keyboard.dismiss()
|
||||
track('Signin:PressedSelectService')
|
||||
}
|
||||
|
||||
const onPressNext = async () => {
|
||||
Keyboard.dismiss()
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
// try to guess the handle if the user just gave their own username
|
||||
let fullIdent = identifier
|
||||
if (
|
||||
!identifier.includes('@') && // not an email
|
||||
!identifier.includes('.') && // not a domain
|
||||
serviceDescription &&
|
||||
serviceDescription.availableUserDomains.length > 0
|
||||
) {
|
||||
let matched = false
|
||||
for (const domain of serviceDescription.availableUserDomains) {
|
||||
if (fullIdent.endsWith(domain)) {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
fullIdent = createFullHandle(
|
||||
identifier,
|
||||
serviceDescription.availableUserDomains[0],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await store.session.login({
|
||||
service: serviceUrl,
|
||||
identifier: fullIdent,
|
||||
password,
|
||||
})
|
||||
} catch (e: any) {
|
||||
const errMsg = e.toString()
|
||||
logger.warn('Failed to login', {error: e})
|
||||
setIsProcessing(false)
|
||||
if (errMsg.includes('Authentication Required')) {
|
||||
setError(_(msg`Invalid username or password`))
|
||||
} else if (isNetworkError(e)) {
|
||||
setError(
|
||||
_(
|
||||
msg`Unable to contact your service. Please check your Internet connection.`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(errMsg))
|
||||
}
|
||||
} finally {
|
||||
track('Sign In', {resumedSession: false})
|
||||
}
|
||||
}
|
||||
|
||||
const isReady = !!serviceDescription && !!identifier && !!password
|
||||
return (
|
||||
<View testID="loginForm">
|
||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||
<Trans>Sign into</Trans>
|
||||
</Text>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="loginSelectServiceButton"
|
||||
style={styles.textBtn}
|
||||
onPress={onPressSelectService}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Select service`)}
|
||||
accessibilityHint="Sets server for the Bluesky client">
|
||||
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||
<Trans>Account</Trans>
|
||||
</Text>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="at"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="loginUsernameInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder={_(msg`Username or email address`)}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
autoCorrect={false}
|
||||
autoComplete="username"
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus()
|
||||
}}
|
||||
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
value={identifier}
|
||||
onChangeText={str =>
|
||||
setIdentifier((str || '').toLowerCase().trim())
|
||||
}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel={_(msg`Username or email address`)}
|
||||
accessibilityHint="Input the username or email address you used at signup"
|
||||
/>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="loginPasswordInput"
|
||||
ref={passwordInputRef}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Password"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoComplete="password"
|
||||
returnKeyType="done"
|
||||
enablesReturnKeyAutomatically={true}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
secureTextEntry={true}
|
||||
textContentType="password"
|
||||
clearButtonMode="while-editing"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
onSubmitEditing={onPressNext}
|
||||
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
||||
editable={!isProcessing}
|
||||
accessibilityLabel={_(msg`Password`)}
|
||||
accessibilityHint={
|
||||
identifier === ''
|
||||
? 'Input your password'
|
||||
: `Input the password tied to ${identifier}`
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="forgotPasswordButton"
|
||||
style={styles.textInputInnerBtn}
|
||||
onPress={onPressForgotPassword}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Forgot password`)}
|
||||
accessibilityHint="Opens password reset form">
|
||||
<Text style={pal.link}>
|
||||
<Trans>Forgot</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{!serviceDescription && error ? (
|
||||
<TouchableOpacity
|
||||
testID="loginRetryButton"
|
||||
onPress={onPressRetryConnect}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Retry`)}
|
||||
accessibilityHint="Retries login">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
<Trans>Retry</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : !serviceDescription ? (
|
||||
<>
|
||||
<ActivityIndicator />
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
<Trans>Connecting...</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : isReady ? (
|
||||
<TouchableOpacity
|
||||
testID="loginNextButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Go to next`)}
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
<Trans>Next</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import React, {useEffect} from 'react'
|
||||
import {TouchableOpacity, View} from 'react-native'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {styles} from './styles'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const PasswordUpdatedForm = ({
|
||||
onPressNext,
|
||||
}: {
|
||||
onPressNext: () => void
|
||||
}) => {
|
||||
const {screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
useEffect(() => {
|
||||
screen('Signin:PasswordUpdatedForm')
|
||||
}, [screen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
<Trans>Password updated!</Trans>
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||
<Trans>You can now sign in with your new password.</Trans>
|
||||
</Text>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Close alert`)}
|
||||
accessibilityHint="Closes password update alert">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
<Trans>Okay</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
import React, {useState, useEffect} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
import {isNetworkError} from 'lib/strings/errors'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {styles} from './styles'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const SetNewPasswordForm = ({
|
||||
error,
|
||||
serviceUrl,
|
||||
setError,
|
||||
onPressBack,
|
||||
onPasswordSet,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
error: string
|
||||
serviceUrl: string
|
||||
setError: (v: string) => void
|
||||
onPressBack: () => void
|
||||
onPasswordSet: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {screen} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
|
||||
useEffect(() => {
|
||||
screen('Signin:SetNewPasswordForm')
|
||||
}, [screen])
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [resetCode, setResetCode] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
||||
const onPressNext = async () => {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
const agent = new BskyAgent({service: serviceUrl})
|
||||
const token = resetCode.replace(/\s/g, '')
|
||||
await agent.com.atproto.server.resetPassword({
|
||||
token,
|
||||
password,
|
||||
})
|
||||
onPasswordSet()
|
||||
} catch (e: any) {
|
||||
const errMsg = e.toString()
|
||||
logger.warn('Failed to set new password', {error: e})
|
||||
setIsProcessing(false)
|
||||
if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Unable to contact your service. Please check your Internet connection.',
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(errMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
<Trans>Set new password</Trans>
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||
<Trans>
|
||||
You will receive an email with a "reset code." Enter that code here,
|
||||
then enter your new password.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View
|
||||
testID="newPasswordView"
|
||||
style={[pal.view, pal.borderDark, styles.group]}>
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ticket"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="resetCodeInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Reset code"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
autoFocus
|
||||
value={resetCode}
|
||||
onChangeText={setResetCode}
|
||||
editable={!isProcessing}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Reset code`)}
|
||||
accessibilityHint="Input code sent to your email for password reset"
|
||||
/>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="newPasswordInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="New password"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isProcessing}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Password`)}
|
||||
accessibilityHint="Input new password"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : !resetCode || !password ? (
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||
<Trans>Next</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="setNewPasswordButton"
|
||||
onPress={onPressNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Go to next`)}
|
||||
accessibilityHint="Navigates to the next screen">
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
<Trans>Next</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isProcessing ? (
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
<Trans>Updating...</Trans>
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
import {StyleSheet} from 'react-native'
|
||||
import {colors} from 'lib/styles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
screenTitle: {
|
||||
marginBottom: 10,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
instructions: {
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
group: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
groupLabel: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
groupContent: {
|
||||
borderTopWidth: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
noTopBorder: {
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
groupContentIcon: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
account: {
|
||||
borderTopWidth: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
accountLast: {
|
||||
borderBottomWidth: 1,
|
||||
marginBottom: 20,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 17,
|
||||
letterSpacing: 0.25,
|
||||
fontWeight: '400',
|
||||
borderRadius: 10,
|
||||
},
|
||||
textInputInnerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
textBtn: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
textBtnLabel: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
textBtnFakeInnerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 6,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
accountText: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
accountTextOther: {
|
||||
paddingLeft: 12,
|
||||
},
|
||||
error: {
|
||||
backgroundColor: colors.red4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: -5,
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 15,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
errorIcon: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.white,
|
||||
color: colors.white,
|
||||
borderRadius: 30,
|
||||
width: 16,
|
||||
height: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 5,
|
||||
},
|
||||
dimmed: {opacity: 0.5},
|
||||
|
||||
maxHeight: {
|
||||
// @ts-ignore web only -prf
|
||||
maxHeight: isWeb ? '100vh' : undefined,
|
||||
height: !isWeb ? '100%' : undefined,
|
||||
},
|
||||
})
|
|
@ -14,6 +14,7 @@ import {Text} from 'view/com/util/text/Text'
|
|||
import Animated, {FadeInRight} from 'react-native-reanimated'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
type Props = {
|
||||
item: SuggestedActor
|
||||
|
@ -115,7 +116,9 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
|||
{addingMoreSuggestions ? (
|
||||
<View style={styles.addingMoreContainer}>
|
||||
<ActivityIndicator size="small" color={pal.colors.text} />
|
||||
<Text style={[pal.text]}>Finding similar accounts...</Text>
|
||||
<Text style={[pal.text]}>
|
||||
<Trans>Finding similar accounts...</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
|
@ -32,7 +33,9 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
|||
accessibilityRole="button"
|
||||
style={[s.flexRow, s.alignCenter]}
|
||||
onPress={skip}>
|
||||
<Text style={[pal.link]}>Skip</Text>
|
||||
<Text style={[pal.link]}>
|
||||
<Trans>Skip</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon={'chevron-right'}
|
||||
size={14}
|
||||
|
@ -45,17 +48,21 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
|||
<View>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
Welcome to{' '}
|
||||
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
|
||||
<Text style={[pal.text, pal.link, styles.title]}>
|
||||
<Trans>Bluesky</Trans>
|
||||
</Text>
|
||||
</Text>
|
||||
<View style={styles.spacer} />
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is public.
|
||||
<Trans>Bluesky is public.</Trans>
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
<Trans>
|
||||
Your posts, likes, and blocks are public. Mutes are private.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -63,10 +70,10 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
|||
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is open.
|
||||
<Trans>Bluesky is open.</Trans>
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
Never lose access to your followers and data.
|
||||
<Trans>Never lose access to your followers and data.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -74,11 +81,13 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
|||
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is flexible.
|
||||
<Trans>Bluesky is flexible.</Trans>
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
<Trans>
|
||||
Choose the algorithms that power your experience with custom
|
||||
feeds.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -49,6 +49,8 @@ import {LabelsBtn} from './labels/LabelsBtn'
|
|||
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
||||
import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
|
||||
import {insertMentionAt} from 'lib/strings/mention-manip'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModals, useModalControls} from '#/state/modals'
|
||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||
import {
|
||||
|
@ -70,6 +72,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
const pal = usePalette('default')
|
||||
const {isDesktop, isMobile} = useWebMediaQueries()
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const requireAltTextEnabled = useRequireAltTextEnabled()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const setLangPrefs = useLanguagePrefsApi()
|
||||
|
@ -273,9 +276,11 @@ export const ComposePost = observer(function ComposePost({
|
|||
onPress={onPressCancel}
|
||||
onAccessibilityEscape={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint="Closes post composer and discards post draft">
|
||||
<Text style={[pal.link, s.f18]}>Cancel</Text>
|
||||
<Text style={[pal.link, s.f18]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing ? (
|
||||
|
@ -316,7 +321,9 @@ export const ComposePost = observer(function ComposePost({
|
|||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={[styles.postBtn, pal.btn]}>
|
||||
<Text style={[pal.textLight, s.f16, s.bold]}>Post</Text>
|
||||
<Text style={[pal.textLight, s.f16, s.bold]}>
|
||||
<Trans>Post</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
|
@ -332,7 +339,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
/>
|
||||
</View>
|
||||
<Text style={[pal.text, s.flex1]}>
|
||||
One or more images is missing alt text.
|
||||
<Trans>One or more images is missing alt text.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
@ -388,7 +395,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
onSuggestedLinksChanged={setSuggestedLinks}
|
||||
onError={setError}
|
||||
accessible={true}
|
||||
accessibilityLabel="Write post"
|
||||
accessibilityLabel={_(msg`Write post`)}
|
||||
accessibilityHint={`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`}
|
||||
/>
|
||||
</View>
|
||||
|
@ -417,11 +424,11 @@ export const ComposePost = observer(function ComposePost({
|
|||
style={[pal.borderDark, styles.addExtLinkBtn]}
|
||||
onPress={() => onPressAddLinkCard(url)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Add link card"
|
||||
accessibilityLabel={_(msg`Add link card`)}
|
||||
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
|
||||
<Text style={pal.text}>
|
||||
Add link card:{' '}
|
||||
<Text style={pal.link}>{toShortUrl(url)}</Text>
|
||||
<Trans>Add link card:</Trans>
|
||||
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
|
|
@ -11,6 +11,8 @@ import {Text} from '../util/text/Text'
|
|||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ExternalEmbedDraft} from 'lib/api/index'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
export const ExternalEmbed = ({
|
||||
link,
|
||||
|
@ -21,6 +23,7 @@ export const ExternalEmbed = ({
|
|||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const palError = usePalette('error')
|
||||
const {_} = useLingui()
|
||||
if (!link) {
|
||||
return <View />
|
||||
}
|
||||
|
@ -64,7 +67,7 @@ export const ExternalEmbed = ({
|
|||
style={styles.removeBtn}
|
||||
onPress={onRemove}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Remove image preview"
|
||||
accessibilityLabel={_(msg`Remove image preview`)}
|
||||
accessibilityHint={`Removes default thumbnail from ${link.uri}`}
|
||||
onAccessibilityEscape={onRemove}>
|
||||
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />
|
||||
|
|
|
@ -5,10 +5,13 @@ import {Text} from '../util/text/Text'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useStores} from 'state/index'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
@ -16,7 +19,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
|||
style={[pal.view, pal.border, styles.prompt]}
|
||||
onPress={() => onPressCompose()}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Compose reply"
|
||||
accessibilityLabel={_(msg`Compose reply`)}
|
||||
accessibilityHint="Opens composer">
|
||||
<UserAvatar avatar={store.me.avatar} size={38} />
|
||||
<Text
|
||||
|
@ -25,7 +28,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
|||
pal.text,
|
||||
isDesktop ? styles.labelDesktopWeb : styles.labelMobile,
|
||||
]}>
|
||||
Write your reply
|
||||
<Trans>Write your reply</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
|
|
@ -7,6 +7,8 @@ import {ShieldExclamation} from 'lib/icons'
|
|||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const LabelsBtn = observer(function LabelsBtn({
|
||||
|
@ -19,6 +21,7 @@ export const LabelsBtn = observer(function LabelsBtn({
|
|||
onChange: (v: string[]) => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
return (
|
||||
|
@ -26,7 +29,7 @@ export const LabelsBtn = observer(function LabelsBtn({
|
|||
type="default-light"
|
||||
testID="labelsBtn"
|
||||
style={[styles.button, !hasMedia && styles.dimmed]}
|
||||
accessibilityLabel="Content warnings"
|
||||
accessibilityLabel={_(msg`Content warnings`)}
|
||||
accessibilityHint=""
|
||||
onPress={() => {
|
||||
if (isNative) {
|
||||
|
|
|
@ -10,6 +10,8 @@ import {Text} from 'view/com/util/text/Text'
|
|||
import {Dimensions} from 'lib/media/types'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {isNative} from 'platform/detection'
|
||||
|
||||
|
@ -48,6 +50,7 @@ const GalleryInner = observer(function GalleryImpl({
|
|||
containerInfo,
|
||||
}: GalleryInnerProps) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
|
@ -113,7 +116,7 @@ const GalleryInner = observer(function GalleryImpl({
|
|||
<TouchableOpacity
|
||||
testID="altTextButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Add alt text"
|
||||
accessibilityLabel={_(msg`Add alt text`)}
|
||||
accessibilityHint=""
|
||||
onPress={() => {
|
||||
Keyboard.dismiss()
|
||||
|
@ -124,7 +127,7 @@ const GalleryInner = observer(function GalleryImpl({
|
|||
}}
|
||||
style={[styles.altTextControl, altTextControlStyle]}>
|
||||
<Text style={styles.altTextControlLabel} accessible={false}>
|
||||
ALT
|
||||
<Trans>ALT</Trans>
|
||||
</Text>
|
||||
{image.altText.length > 0 ? (
|
||||
<FontAwesomeIcon
|
||||
|
@ -138,7 +141,7 @@ const GalleryInner = observer(function GalleryImpl({
|
|||
<TouchableOpacity
|
||||
testID="editPhotoButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Edit image"
|
||||
accessibilityLabel={_(msg`Edit image`)}
|
||||
accessibilityHint=""
|
||||
onPress={() => {
|
||||
if (isNative) {
|
||||
|
@ -161,7 +164,7 @@ const GalleryInner = observer(function GalleryImpl({
|
|||
<TouchableOpacity
|
||||
testID="removePhotoButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Remove image"
|
||||
accessibilityLabel={_(msg`Remove image`)}
|
||||
accessibilityHint=""
|
||||
onPress={() => gallery.remove(image)}
|
||||
style={styles.imageControl}>
|
||||
|
@ -174,7 +177,7 @@ const GalleryInner = observer(function GalleryImpl({
|
|||
</View>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Add alt text"
|
||||
accessibilityLabel={_(msg`Add alt text`)}
|
||||
accessibilityHint=""
|
||||
onPress={() => {
|
||||
Keyboard.dismiss()
|
||||
|
@ -203,8 +206,10 @@ const GalleryInner = observer(function GalleryImpl({
|
|||
<FontAwesomeIcon icon="info" size={12} color={pal.colors.text} />
|
||||
</View>
|
||||
<Text type="sm" style={[pal.textLight, s.flex1]}>
|
||||
<Trans>
|
||||
Alt text describes images for blind and low-vision users, and helps
|
||||
give context to everyone.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
|
|
|
@ -13,6 +13,8 @@ import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants'
|
|||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {isMobileWeb, isNative} from 'platform/detection'
|
||||
import {logger} from '#/logger'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
type Props = {
|
||||
gallery: GalleryModel
|
||||
|
@ -22,6 +24,7 @@ export function OpenCameraBtn({gallery}: Props) {
|
|||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
|
||||
const onPressTakePicture = useCallback(async () => {
|
||||
|
@ -56,7 +59,7 @@ export function OpenCameraBtn({gallery}: Props) {
|
|||
style={styles.button}
|
||||
hitSlop={HITSLOP_10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Camera"
|
||||
accessibilityLabel={_(msg`Camera`)}
|
||||
accessibilityHint="Opens camera on device">
|
||||
<FontAwesomeIcon
|
||||
icon="camera"
|
||||
|
|
|
@ -10,6 +10,8 @@ import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
|
|||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
type Props = {
|
||||
gallery: GalleryModel
|
||||
|
@ -18,6 +20,7 @@ type Props = {
|
|||
export function SelectPhotoBtn({gallery}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
|
||||
const onPressSelectPhotos = useCallback(async () => {
|
||||
|
@ -37,7 +40,7 @@ export function SelectPhotoBtn({gallery}: Props) {
|
|||
style={styles.button}
|
||||
hitSlop={HITSLOP_10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Gallery"
|
||||
accessibilityLabel={_(msg`Gallery`)}
|
||||
accessibilityHint="Opens device photo gallery">
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'image']}
|
||||
|
|
|
@ -21,9 +21,12 @@ import {
|
|||
toPostLanguages,
|
||||
hasPostLanguage,
|
||||
} from '#/state/preferences/languages'
|
||||
import {t, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const SelectLangBtn = observer(function SelectLangBtn() {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const setLangPrefs = useLanguagePrefsApi()
|
||||
|
@ -82,11 +85,11 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
|
|||
}
|
||||
|
||||
return [
|
||||
{heading: true, label: 'Post language'},
|
||||
{heading: true, label: t`Post language`},
|
||||
...arr.slice(0, 6),
|
||||
{sep: true},
|
||||
{
|
||||
label: 'Other...',
|
||||
label: t`Other...`,
|
||||
onPress: onPressMore,
|
||||
},
|
||||
]
|
||||
|
@ -99,7 +102,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
|
|||
items={items}
|
||||
openUpwards
|
||||
style={styles.button}
|
||||
accessibilityLabel="Language selection"
|
||||
accessibilityLabel={_(msg`Language selection`)}
|
||||
accessibilityHint="">
|
||||
{postLanguagesPref.length > 0 ? (
|
||||
<Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>
|
||||
|
|
|
@ -21,6 +21,8 @@ import {FAB} from '../util/fab/FAB'
|
|||
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
|
||||
import useAppState from 'react-native-appstate-hook'
|
||||
import {logger} from '#/logger'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const FeedPage = observer(function FeedPageImpl({
|
||||
testID,
|
||||
|
@ -37,6 +39,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
|||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
|
||||
const {screen, track} = useAnalytics()
|
||||
|
@ -157,7 +160,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
|||
type="title-lg"
|
||||
href="/settings/home-feed"
|
||||
style={{fontWeight: 'bold'}}
|
||||
accessibilityLabel="Feed Preferences"
|
||||
accessibilityLabel={_(msg`Feed Preferences`)}
|
||||
accessibilityHint=""
|
||||
text={
|
||||
<FontAwesomeIcon
|
||||
|
@ -170,7 +173,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
|||
)
|
||||
}
|
||||
return <></>
|
||||
}, [isDesktop, pal, store, hasNew])
|
||||
}, [isDesktop, pal.view, pal.text, pal.textLight, store, hasNew, _])
|
||||
|
||||
return (
|
||||
<View testID={testID} style={s.h100pct}>
|
||||
|
@ -188,7 +191,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
|||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onPressLoadLatest}
|
||||
label="Load new posts"
|
||||
label={_(msg`Load new posts`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
|
@ -197,7 +200,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
|||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="New post"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {createHitslop} from 'lib/constants'
|
||||
import React from 'react'
|
||||
import {createHitslop} from 'lib/constants'
|
||||
import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
|
||||
import {t} from '@lingui/macro'
|
||||
|
||||
type Props = {
|
||||
onRequestClose: () => void
|
||||
|
@ -23,7 +23,7 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
|
|||
onPress={onRequestClose}
|
||||
hitSlop={HIT_SLOP}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Close image"
|
||||
accessibilityLabel={t`Close image`}
|
||||
accessibilityHint="Closes viewer for header image"
|
||||
onAccessibilityEscape={onRequestClose}>
|
||||
<Text style={styles.closeText}>✕</Text>
|
||||
|
|
|
@ -14,6 +14,8 @@ import * as models from 'state/models/ui/shell'
|
|||
import {colors, s} from 'lib/styles'
|
||||
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
interface Img {
|
||||
uri: string
|
||||
|
@ -62,6 +64,7 @@ function LightboxInner({
|
|||
initialIndex: number
|
||||
onClose: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const [index, setIndex] = useState<number>(initialIndex)
|
||||
const [isAltExpanded, setAltExpanded] = useState(false)
|
||||
|
||||
|
@ -101,7 +104,7 @@ function LightboxInner({
|
|||
<TouchableWithoutFeedback
|
||||
onPress={onClose}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Close image viewer"
|
||||
accessibilityLabel={_(msg`Close image viewer`)}
|
||||
accessibilityHint="Exits image view"
|
||||
onAccessibilityEscape={onClose}>
|
||||
<View style={styles.imageCenterer}>
|
||||
|
@ -117,7 +120,7 @@ function LightboxInner({
|
|||
onPress={onPressLeft}
|
||||
style={[styles.btn, styles.leftBtn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Previous image"
|
||||
accessibilityLabel={_(msg`Previous image`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="angle-left"
|
||||
|
@ -131,7 +134,7 @@ function LightboxInner({
|
|||
onPress={onPressRight}
|
||||
style={[styles.btn, styles.rightBtn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Next image"
|
||||
accessibilityLabel={_(msg`Next image`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
|
@ -145,7 +148,7 @@ function LightboxInner({
|
|||
{imgs[index].alt ? (
|
||||
<View style={styles.footer}>
|
||||
<Pressable
|
||||
accessibilityLabel="Expand alt text"
|
||||
accessibilityLabel={_(msg`Expand alt text`)}
|
||||
accessibilityHint="If alt text is long, toggles alt text expanded state"
|
||||
onPress={() => {
|
||||
setAltExpanded(!isAltExpanded)
|
||||
|
|
|
@ -20,6 +20,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {FlatList} from '../util/Views'
|
||||
import {s} from 'lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
const LOADING = {_reactKey: '__loading__'}
|
||||
const EMPTY = {_reactKey: '__empty__'}
|
||||
|
@ -107,7 +108,9 @@ export const ListsList = observer(function ListsListImpl({
|
|||
<View
|
||||
testID="listsEmpty"
|
||||
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
|
||||
<Text style={pal.textLight}>You have no lists.</Text>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>You have no lists.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
} else if (item === ERROR_ITEM) {
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['70%']
|
||||
|
@ -55,6 +57,7 @@ const shadesOfBlue: string[] = [
|
|||
export function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const [name, setName] = useState(
|
||||
shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
|
||||
|
@ -121,15 +124,19 @@ export function Component({}: {}) {
|
|||
<View>
|
||||
{!appPassword ? (
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
Please enter a unique name for this App Password or use our randomly
|
||||
generated one.
|
||||
<Trans>
|
||||
Please enter a unique name for this App Password or use our
|
||||
randomly generated one.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Here is your app password.
|
||||
</Text>{' '}
|
||||
<Text type="lg-bold" style={[pal.text, s.mr5]}>
|
||||
<Trans>Here is your app password.</Trans>
|
||||
</Text>
|
||||
<Trans>
|
||||
Use this to sign into the other app along with your handle.
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{!appPassword ? (
|
||||
|
@ -154,7 +161,7 @@ export function Component({}: {}) {
|
|||
returnKeyType="done"
|
||||
onEndEditing={createAppPassword}
|
||||
accessible={true}
|
||||
accessibilityLabel="Name"
|
||||
accessibilityLabel={_(msg`Name`)}
|
||||
accessibilityHint="Input name for app password"
|
||||
/>
|
||||
</View>
|
||||
|
@ -163,13 +170,15 @@ export function Component({}: {}) {
|
|||
style={[pal.border, styles.passwordContainer, pal.btn]}
|
||||
onPress={onCopy}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Copy"
|
||||
accessibilityLabel={_(msg`Copy`)}
|
||||
accessibilityHint="Copies app password">
|
||||
<Text type="2xl-bold" style={[pal.text]}>
|
||||
{appPassword}
|
||||
</Text>
|
||||
{wasCopied ? (
|
||||
<Text style={[pal.textLight]}>Copied</Text>
|
||||
<Text style={[pal.textLight]}>
|
||||
<Trans>Copied</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'clone']}
|
||||
|
@ -182,14 +191,18 @@ export function Component({}: {}) {
|
|||
</View>
|
||||
{appPassword ? (
|
||||
<Text type="lg" style={[pal.textLight, s.mb10]}>
|
||||
<Trans>
|
||||
For security reasons, you won't be able to view this again. If you
|
||||
lose this password, you'll need to generate a new one.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
|
||||
<Trans>
|
||||
Can only contain letters, numbers, spaces, dashes, and underscores.
|
||||
Must be at least 4 characters long, but no more than 32 characters
|
||||
long.
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.btnContainer}>
|
||||
|
|
|
@ -19,6 +19,8 @@ import {Text} from '../util/text/Text'
|
|||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {isAndroid, isWeb} from 'platform/detection'
|
||||
import {ImageModel} from 'state/models/media/image'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
@ -30,6 +32,7 @@ interface Props {
|
|||
export function Component({image}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const [altText, setAltText] = useState(image.altText)
|
||||
const windim = useWindowDimensions()
|
||||
const {closeModal} = useModalControls()
|
||||
|
@ -90,7 +93,7 @@ export function Component({image}: Props) {
|
|||
placeholderTextColor={pal.colors.textLight}
|
||||
value={altText}
|
||||
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
||||
accessibilityLabel="Image alt text"
|
||||
accessibilityLabel={_(msg`Image alt text`)}
|
||||
accessibilityHint=""
|
||||
accessibilityLabelledBy="imageAltText"
|
||||
autoFocus
|
||||
|
@ -99,7 +102,7 @@ export function Component({image}: Props) {
|
|||
<TouchableOpacity
|
||||
testID="altTextImageSaveBtn"
|
||||
onPress={onPressSave}
|
||||
accessibilityLabel="Save alt text"
|
||||
accessibilityLabel={_(msg`Save alt text`)}
|
||||
accessibilityHint={`Saves alt text, which reads: ${altText}`}
|
||||
accessibilityRole="button">
|
||||
<LinearGradient
|
||||
|
@ -108,7 +111,7 @@ export function Component({image}: Props) {
|
|||
end={{x: 1, y: 1}}
|
||||
style={[styles.button]}>
|
||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||
Save
|
||||
<Trans>Save</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
@ -116,12 +119,12 @@ export function Component({image}: Props) {
|
|||
testID="altTextImageCancelBtn"
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel add image alt text"
|
||||
accessibilityLabel={_(msg`Cancel add image alt text`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}>
|
||||
<View style={[styles.button]}>
|
||||
<Text type="button-lg" style={[pal.textLight]}>
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -15,6 +15,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['50%']
|
||||
|
@ -22,6 +24,7 @@ export const snapPoints = ['50%']
|
|||
export const Component = observer(function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const [date, setDate] = useState<Date>(
|
||||
store.preferences.birthDate || new Date(),
|
||||
|
@ -49,12 +52,12 @@ export const Component = observer(function Component({}: {}) {
|
|||
style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
My Birthday
|
||||
<Trans>My Birthday</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
||||
This information is not shared with other users.
|
||||
<Trans>This information is not shared with other users.</Trans>
|
||||
</Text>
|
||||
|
||||
<View>
|
||||
|
@ -65,7 +68,7 @@ export const Component = observer(function Component({}: {}) {
|
|||
buttonType="default-light"
|
||||
buttonStyle={[pal.border, styles.dateInputButton]}
|
||||
buttonLabelType="lg"
|
||||
accessibilityLabel="Birthday"
|
||||
accessibilityLabel={_(msg`Birthday`)}
|
||||
accessibilityHint="Enter your birth date"
|
||||
accessibilityLabelledBy="birthDate"
|
||||
/>
|
||||
|
@ -86,9 +89,11 @@ export const Component = observer(function Component({}: {}) {
|
|||
onPress={onSave}
|
||||
style={styles.btn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Save"
|
||||
accessibilityLabel={_(msg`Save`)}
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>Save</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Save</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
|
|
@ -12,6 +12,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
enum Stages {
|
||||
|
@ -25,6 +27,7 @@ export const snapPoints = ['90%']
|
|||
export const Component = observer(function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
|
||||
const [email, setEmail] = useState<string>(
|
||||
store.session.currentSession?.email || '',
|
||||
|
@ -62,7 +65,9 @@ export const Component = observer(function Component({}: {}) {
|
|||
// you can remove this any time after Oct2023
|
||||
// -prf
|
||||
if (err === 'email must be confirmed (temporary)') {
|
||||
err = `Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`
|
||||
err = _(
|
||||
msg`Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`,
|
||||
)
|
||||
}
|
||||
setError(err)
|
||||
} finally {
|
||||
|
@ -103,26 +108,26 @@ export const Component = observer(function Component({}: {}) {
|
|||
style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
{stage === Stages.InputEmail ? 'Change Your Email' : ''}
|
||||
{stage === Stages.ConfirmCode ? 'Security Step Required' : ''}
|
||||
{stage === Stages.Done ? 'Email Updated' : ''}
|
||||
{stage === Stages.InputEmail ? _(msg`Change Your Email`) : ''}
|
||||
{stage === Stages.ConfirmCode ? _(msg`Security Step Required`) : ''}
|
||||
{stage === Stages.Done ? _(msg`Email Updated`) : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
||||
{stage === Stages.InputEmail ? (
|
||||
<>Enter your new email address below.</>
|
||||
<Trans>Enter your new email address below.</Trans>
|
||||
) : stage === Stages.ConfirmCode ? (
|
||||
<>
|
||||
<Trans>
|
||||
An email has been sent to your previous address,{' '}
|
||||
{store.session.currentSession?.email || ''}. It includes a
|
||||
confirmation code which you can enter below.
|
||||
</>
|
||||
</Trans>
|
||||
) : (
|
||||
<>
|
||||
<Trans>
|
||||
Your email has been updated but not verified. As a next step,
|
||||
please verify your new email.
|
||||
</>
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
|
@ -135,7 +140,7 @@ export const Component = observer(function Component({}: {}) {
|
|||
value={email}
|
||||
onChangeText={setEmail}
|
||||
accessible={true}
|
||||
accessibilityLabel="Email"
|
||||
accessibilityLabel={_(msg`Email`)}
|
||||
accessibilityHint=""
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
|
@ -151,7 +156,7 @@ export const Component = observer(function Component({}: {}) {
|
|||
value={confirmationCode}
|
||||
onChangeText={setConfirmationCode}
|
||||
accessible={true}
|
||||
accessibilityLabel="Confirmation code"
|
||||
accessibilityLabel={_(msg`Confirmation code`)}
|
||||
accessibilityHint=""
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
|
@ -175,9 +180,9 @@ export const Component = observer(function Component({}: {}) {
|
|||
testID="requestChangeBtn"
|
||||
type="primary"
|
||||
onPress={onRequestChange}
|
||||
accessibilityLabel="Request Change"
|
||||
accessibilityLabel={_(msg`Request Change`)}
|
||||
accessibilityHint=""
|
||||
label="Request Change"
|
||||
label={_(msg`Request Change`)}
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
labelStyle={[s.f18]}
|
||||
/>
|
||||
|
@ -187,9 +192,9 @@ export const Component = observer(function Component({}: {}) {
|
|||
testID="confirmBtn"
|
||||
type="primary"
|
||||
onPress={onConfirm}
|
||||
accessibilityLabel="Confirm Change"
|
||||
accessibilityLabel={_(msg`Confirm Change`)}
|
||||
accessibilityHint=""
|
||||
label="Confirm Change"
|
||||
label={_(msg`Confirm Change`)}
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
labelStyle={[s.f18]}
|
||||
/>
|
||||
|
@ -199,9 +204,9 @@ export const Component = observer(function Component({}: {}) {
|
|||
testID="verifyBtn"
|
||||
type="primary"
|
||||
onPress={onVerify}
|
||||
accessibilityLabel="Verify New Email"
|
||||
accessibilityLabel={_(msg`Verify New Email`)}
|
||||
accessibilityHint=""
|
||||
label="Verify New Email"
|
||||
label={_(msg`Verify New Email`)}
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
labelStyle={[s.f18]}
|
||||
/>
|
||||
|
@ -210,9 +215,9 @@ export const Component = observer(function Component({}: {}) {
|
|||
testID="cancelBtn"
|
||||
type="default"
|
||||
onPress={() => closeModal()}
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint=""
|
||||
label="Cancel"
|
||||
label={_(msg`Cancel`)}
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
labelStyle={[s.f18]}
|
||||
/>
|
||||
|
|
|
@ -22,6 +22,8 @@ import {useTheme} from 'lib/ThemeContext'
|
|||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['100%']
|
||||
|
@ -31,6 +33,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
|||
const [error, setError] = useState<string>('')
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
|
||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||
|
@ -141,7 +144,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
|||
<TouchableOpacity
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel change handle"
|
||||
accessibilityLabel={_(msg`Cancel change handle`)}
|
||||
accessibilityHint="Exits handle change process"
|
||||
onAccessibilityEscape={onPressCancel}>
|
||||
<Text type="lg" style={pal.textLight}>
|
||||
|
@ -153,7 +156,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
|||
type="2xl-bold"
|
||||
style={[styles.titleMiddle, pal.text]}
|
||||
numberOfLines={1}>
|
||||
Change Handle
|
||||
<Trans>Change Handle</Trans>
|
||||
</Text>
|
||||
<View style={styles.titleRight}>
|
||||
{isProcessing ? (
|
||||
|
@ -163,7 +166,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
|||
testID="retryConnectButton"
|
||||
onPress={onPressRetryConnect}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Retry change handle"
|
||||
accessibilityLabel={_(msg`Retry change handle`)}
|
||||
accessibilityHint={`Retries handle change to ${handle}`}>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Retry
|
||||
|
@ -173,10 +176,10 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
|||
<TouchableOpacity
|
||||
onPress={onPressSave}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Save handle change"
|
||||
accessibilityLabel={_(msg`Save handle change`)}
|
||||
accessibilityHint={`Saves handle change to ${handle}`}>
|
||||
<Text type="2xl-medium" style={pal.link}>
|
||||
Save
|
||||
<Trans>Save</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
|
@ -234,6 +237,7 @@ function ProvidedHandleForm({
|
|||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
// events
|
||||
// =
|
||||
|
@ -266,12 +270,12 @@ function ProvidedHandleForm({
|
|||
onChangeText={onChangeHandle}
|
||||
editable={!isProcessing}
|
||||
accessible={true}
|
||||
accessibilityLabel="Handle"
|
||||
accessibilityLabel={_(msg`Handle`)}
|
||||
accessibilityHint="Sets Bluesky username"
|
||||
/>
|
||||
</View>
|
||||
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
|
||||
Your full handle will be{' '}
|
||||
<Trans>Your full handle will be </Trans>
|
||||
<Text type="md-bold" style={pal.textLight}>
|
||||
@{createFullHandle(handle, userDomain)}
|
||||
</Text>
|
||||
|
@ -280,9 +284,9 @@ function ProvidedHandleForm({
|
|||
onPress={onToggleCustom}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint="Hosting provider"
|
||||
accessibilityLabel="Opens modal for using custom domain">
|
||||
accessibilityLabel={_(msg`Opens modal for using custom domain`)}>
|
||||
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
|
||||
I have my own domain
|
||||
<Trans>I have my own domain</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
|
@ -314,6 +318,7 @@ function CustomHandleForm({
|
|||
const palSecondary = usePalette('secondary')
|
||||
const palError = usePalette('error')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const [isVerifying, setIsVerifying] = React.useState(false)
|
||||
const [error, setError] = React.useState<string>('')
|
||||
const [isDNSForm, setDNSForm] = React.useState<boolean>(true)
|
||||
|
@ -367,7 +372,7 @@ function CustomHandleForm({
|
|||
return (
|
||||
<>
|
||||
<Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain">
|
||||
Enter the domain you want to use
|
||||
<Trans>Enter the domain you want to use</Trans>
|
||||
</Text>
|
||||
<View style={[pal.btn, styles.textInputWrapper]}>
|
||||
<FontAwesomeIcon
|
||||
|
@ -385,7 +390,7 @@ function CustomHandleForm({
|
|||
onChangeText={onChangeHandle}
|
||||
editable={!isProcessing}
|
||||
accessibilityLabelledBy="customDomain"
|
||||
accessibilityLabel="Custom domain"
|
||||
accessibilityLabel={_(msg`Custom domain`)}
|
||||
accessibilityHint="Input your preferred hosting provider"
|
||||
/>
|
||||
</View>
|
||||
|
@ -413,7 +418,7 @@ function CustomHandleForm({
|
|||
{isDNSForm ? (
|
||||
<>
|
||||
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
|
||||
Add the following DNS record to your domain:
|
||||
<Trans>Add the following DNS record to your domain:</Trans>
|
||||
</Text>
|
||||
<View style={[styles.dnsTable, pal.btn]}>
|
||||
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
|
||||
|
@ -451,7 +456,7 @@ function CustomHandleForm({
|
|||
) : (
|
||||
<>
|
||||
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
|
||||
Upload a text file to:
|
||||
<Trans>Upload a text file to:</Trans>
|
||||
</Text>
|
||||
<View style={[styles.valueContainer, pal.btn]}>
|
||||
<View style={[styles.dnsValue]}>
|
||||
|
@ -483,7 +488,7 @@ function CustomHandleForm({
|
|||
{canSave === true && (
|
||||
<View style={[styles.message, palSecondary.view]}>
|
||||
<Text type="md-medium" style={palSecondary.text}>
|
||||
Domain verified!
|
||||
<Trans>Domain verified!</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
@ -511,7 +516,7 @@ function CustomHandleForm({
|
|||
<View style={styles.spacer} />
|
||||
<TouchableOpacity
|
||||
onPress={onToggleCustom}
|
||||
accessibilityLabel="Use default provider"
|
||||
accessibilityLabel={_(msg`Use default provider`)}
|
||||
accessibilityHint="Use bsky.social as hosting provider">
|
||||
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
|
||||
Nevermind, create a handle for me
|
||||
|
|
|
@ -11,6 +11,8 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
|
|||
import {cleanError} from 'lib/strings/errors'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import type {ConfirmModal} from '#/state/modals'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
|
@ -26,6 +28,7 @@ export function Component({
|
|||
cancelBtnText,
|
||||
}: ConfirmModal) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
|
@ -69,7 +72,7 @@ export function Component({
|
|||
onPress={onPress}
|
||||
style={[styles.btn, confirmBtnStyle]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm"
|
||||
accessibilityLabel={_(msg`Confirm`)}
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
{confirmBtnText ?? 'Confirm'}
|
||||
|
@ -82,7 +85,7 @@ export function Component({
|
|||
onPress={onPressCancel}
|
||||
style={[styles.btnCancel, s.mt10]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint="">
|
||||
<Text type="button-lg" style={pal.textLight}>
|
||||
{cancelBtnText ?? 'Cancel'}
|
||||
|
|
|
@ -16,6 +16,8 @@ import {isIOS} from 'platform/detection'
|
|||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
@ -25,6 +27,7 @@ export const Component = observer(
|
|||
const store = useStores()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -37,7 +40,9 @@ export const Component = observer(
|
|||
|
||||
return (
|
||||
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
|
||||
<Text style={[pal.text, styles.title]}>Content Filtering</Text>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>Content Filtering</Trans>
|
||||
</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
<AdultContentEnabledPref />
|
||||
<ContentLabelPref
|
||||
|
@ -71,14 +76,16 @@ export const Component = observer(
|
|||
testID="sendReportBtn"
|
||||
onPress={onPressDone}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Done"
|
||||
accessibilityLabel={_(msg`Done`)}
|
||||
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, s.f18]}>Done</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
|
|
@ -24,6 +24,8 @@ import {useTheme} from 'lib/ThemeContext'
|
|||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
const MAX_NAME = 64 // todo
|
||||
|
@ -47,6 +49,7 @@ export function Component({
|
|||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
|
||||
const activePurpose = useMemo(() => {
|
||||
if (list?.data?.purpose) {
|
||||
|
@ -164,14 +167,18 @@ export function Component({
|
|||
]}
|
||||
testID="createOrEditListModal">
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
<Trans>
|
||||
{list ? 'Edit' : 'New'} {purposeLabel} List
|
||||
</Trans>
|
||||
</Text>
|
||||
{error !== '' && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={error} />
|
||||
</View>
|
||||
)}
|
||||
<Text style={[styles.label, pal.text]}>List Avatar</Text>
|
||||
<Text style={[styles.label, pal.text]}>
|
||||
<Trans>List Avatar</Trans>
|
||||
</Text>
|
||||
<View style={[styles.avi, {borderColor: pal.colors.background}]}>
|
||||
<EditableUserAvatar
|
||||
type="list"
|
||||
|
@ -183,7 +190,7 @@ export function Component({
|
|||
<View style={styles.form}>
|
||||
<View>
|
||||
<Text style={[styles.label, pal.text]} nativeID="list-name">
|
||||
List Name
|
||||
<Trans>List Name</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editNameInput"
|
||||
|
@ -195,14 +202,14 @@ export function Component({
|
|||
value={name}
|
||||
onChangeText={v => setName(enforceLen(v, MAX_NAME))}
|
||||
accessible={true}
|
||||
accessibilityLabel="Name"
|
||||
accessibilityLabel={_(msg`Name`)}
|
||||
accessibilityHint=""
|
||||
accessibilityLabelledBy="list-name"
|
||||
/>
|
||||
</View>
|
||||
<View style={s.pb10}>
|
||||
<Text style={[styles.label, pal.text]} nativeID="list-description">
|
||||
Description
|
||||
<Trans>Description</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editDescriptionInput"
|
||||
|
@ -218,7 +225,7 @@ export function Component({
|
|||
value={description}
|
||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
||||
accessible={true}
|
||||
accessibilityLabel="Description"
|
||||
accessibilityLabel={_(msg`Description`)}
|
||||
accessibilityHint=""
|
||||
accessibilityLabelledBy="list-description"
|
||||
/>
|
||||
|
@ -233,14 +240,16 @@ export function Component({
|
|||
style={s.mt10}
|
||||
onPress={onPressSave}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Save"
|
||||
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]}>Save</Text>
|
||||
<Text style={[s.white, s.bold]}>
|
||||
<Trans>Save</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
@ -249,11 +258,13 @@ export function Component({
|
|||
style={s.mt5}
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}>
|
||||
<View style={[styles.btn]}>
|
||||
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
||||
<Text style={[s.black, s.bold, pal.text]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
@ -17,6 +17,8 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {resetToTab} from '../../../Navigation'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['60%']
|
||||
|
@ -25,6 +27,7 @@ export function Component({}: {}) {
|
|||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
||||
|
@ -71,7 +74,7 @@ export function Component({}: {}) {
|
|||
<View style={[styles.innerContainer, pal.view]}>
|
||||
<View style={[styles.titleContainer, pal.view]}>
|
||||
<Text type="title-xl" style={[s.textCenter, pal.text]}>
|
||||
Delete Account
|
||||
<Trans>Delete Account</Trans>
|
||||
</Text>
|
||||
<View style={[pal.view, s.flexRow]}>
|
||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
||||
|
@ -95,8 +98,10 @@ export function Component({}: {}) {
|
|||
{!isEmailSent ? (
|
||||
<>
|
||||
<Text type="lg" style={[styles.description, pal.text]}>
|
||||
<Trans>
|
||||
For security reasons, we'll need to send a confirmation code to
|
||||
your email address.
|
||||
</Trans>
|
||||
</Text>
|
||||
{error ? (
|
||||
<View style={s.mt10}>
|
||||
|
@ -113,7 +118,7 @@ export function Component({}: {}) {
|
|||
style={styles.mt20}
|
||||
onPress={onPressSendEmail}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Send email"
|
||||
accessibilityLabel={_(msg`Send email`)}
|
||||
accessibilityHint="Sends email with confirmation code for account deletion">
|
||||
<LinearGradient
|
||||
colors={[
|
||||
|
@ -124,7 +129,7 @@ export function Component({}: {}) {
|
|||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||
Send Email
|
||||
<Trans>Send Email</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
@ -132,11 +137,11 @@ export function Component({}: {}) {
|
|||
style={[styles.btn, s.mt10]}
|
||||
onPress={onCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel account deletion"
|
||||
accessibilityLabel={_(msg`Cancel account deletion`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onCancel}>
|
||||
<Text type="button-lg" style={pal.textLight}>
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
|
@ -149,8 +154,10 @@ export function Component({}: {}) {
|
|||
type="lg"
|
||||
style={styles.description}
|
||||
nativeID="confirmationCode">
|
||||
Check your inbox for an email with the confirmation code to enter
|
||||
below:
|
||||
<Trans>
|
||||
Check your inbox for an email with the confirmation code to
|
||||
enter below:
|
||||
</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
|
||||
|
@ -160,11 +167,11 @@ export function Component({}: {}) {
|
|||
value={confirmCode}
|
||||
onChangeText={setConfirmCode}
|
||||
accessibilityLabelledBy="confirmationCode"
|
||||
accessibilityLabel="Confirmation code"
|
||||
accessibilityLabel={_(msg`Confirmation code`)}
|
||||
accessibilityHint="Input confirmation code for account deletion"
|
||||
/>
|
||||
<Text type="lg" style={styles.description} nativeID="password">
|
||||
Please enter your password as well:
|
||||
<Trans>Please enter your password as well:</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.textInput, pal.borderDark, pal.text]}
|
||||
|
@ -175,7 +182,7 @@ export function Component({}: {}) {
|
|||
value={password}
|
||||
onChangeText={setPassword}
|
||||
accessibilityLabelledBy="password"
|
||||
accessibilityLabel="Password"
|
||||
accessibilityLabel={_(msg`Password`)}
|
||||
accessibilityHint="Input password for account deletion"
|
||||
/>
|
||||
{error ? (
|
||||
|
@ -193,21 +200,21 @@ export function Component({}: {}) {
|
|||
style={[styles.btn, styles.evilBtn, styles.mt20]}
|
||||
onPress={onPressConfirmDelete}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm delete account"
|
||||
accessibilityLabel={_(msg`Confirm delete account`)}
|
||||
accessibilityHint="">
|
||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||
Delete my account
|
||||
<Trans>Delete my account</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, s.mt10]}
|
||||
onPress={onCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel account deletion"
|
||||
accessibilityLabel={_(msg`Cancel account deletion`)}
|
||||
accessibilityHint="Exits account deletion process"
|
||||
onAccessibilityEscape={onCancel}>
|
||||
<Text type="button-lg" style={pal.textLight}>
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
|
|
|
@ -18,6 +18,8 @@ import {Slider} from '@miblanchard/react-native-slider'
|
|||
import {MaterialIcons} from '@expo/vector-icons'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {getKeys} from 'lib/type-assertions'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['80%']
|
||||
|
@ -52,6 +54,7 @@ export const Component = observer(function EditImageImpl({
|
|||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const windowDimensions = useWindowDimensions()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {closeModal} = useModalControls()
|
||||
|
@ -200,7 +203,9 @@ export const Component = observer(function EditImageImpl({
|
|||
paddingHorizontal: isMobile ? 16 : undefined,
|
||||
},
|
||||
]}>
|
||||
<Text style={[styles.title, pal.text]}>Edit image</Text>
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
<Trans>Edit image</Trans>
|
||||
</Text>
|
||||
<View style={[styles.gap18, s.flexRow]}>
|
||||
<View>
|
||||
<View
|
||||
|
@ -228,7 +233,7 @@ export const Component = observer(function EditImageImpl({
|
|||
<View>
|
||||
{!isMobile ? (
|
||||
<Text type="sm-bold" style={pal.text}>
|
||||
Ratios
|
||||
<Trans>Ratios</Trans>
|
||||
</Text>
|
||||
) : null}
|
||||
<View style={imgControlStyles}>
|
||||
|
@ -263,7 +268,7 @@ export const Component = observer(function EditImageImpl({
|
|||
</View>
|
||||
{!isMobile ? (
|
||||
<Text type="sm-bold" style={[pal.text, styles.subsection]}>
|
||||
Transformations
|
||||
<Trans>Transformations</Trans>
|
||||
</Text>
|
||||
) : null}
|
||||
<View style={imgControlStyles}>
|
||||
|
@ -291,7 +296,7 @@ export const Component = observer(function EditImageImpl({
|
|||
</View>
|
||||
<View style={[styles.gap18, styles.bottomSection, pal.border]}>
|
||||
<Text type="sm-bold" style={pal.text} nativeID="alt-text">
|
||||
Accessibility
|
||||
<Trans>Accessibility</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="altTextImageInput"
|
||||
|
@ -307,7 +312,7 @@ export const Component = observer(function EditImageImpl({
|
|||
multiline
|
||||
value={altText}
|
||||
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
||||
accessibilityLabel="Alt text"
|
||||
accessibilityLabel={_(msg`Alt text`)}
|
||||
accessibilityHint=""
|
||||
accessibilityLabelledBy="alt-text"
|
||||
/>
|
||||
|
@ -315,7 +320,7 @@ export const Component = observer(function EditImageImpl({
|
|||
<View style={styles.btns}>
|
||||
<Pressable onPress={onPressCancel} accessibilityRole="button">
|
||||
<Text type="xl" style={pal.link}>
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={onPressSave} accessibilityRole="button">
|
||||
|
@ -325,7 +330,7 @@ export const Component = observer(function EditImageImpl({
|
|||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text type="xl-medium" style={s.white}>
|
||||
Done
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
|
|
|
@ -26,6 +26,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
|
|||
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
||||
import Animated, {FadeOut} from 'react-native-reanimated'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
const AnimatedTouchableOpacity =
|
||||
|
@ -44,6 +46,7 @@ export function Component({
|
|||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
|
||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||
|
@ -151,7 +154,9 @@ export function Component({
|
|||
return (
|
||||
<KeyboardAvoidingView style={s.flex1} behavior="height">
|
||||
<ScrollView style={[pal.view]} testID="editProfileModal">
|
||||
<Text style={[styles.title, pal.text]}>Edit my profile</Text>
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
<Trans>Edit my profile</Trans>
|
||||
</Text>
|
||||
<View style={styles.photos}>
|
||||
<UserBanner
|
||||
banner={userBanner}
|
||||
|
@ -172,7 +177,9 @@ export function Component({
|
|||
)}
|
||||
<View style={styles.form}>
|
||||
<View>
|
||||
<Text style={[styles.label, pal.text]}>Display Name</Text>
|
||||
<Text style={[styles.label, pal.text]}>
|
||||
<Trans>Display Name</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editProfileDisplayNameInput"
|
||||
style={[styles.textInput, pal.border, pal.text]}
|
||||
|
@ -183,12 +190,14 @@ export function Component({
|
|||
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
||||
}
|
||||
accessible={true}
|
||||
accessibilityLabel="Display name"
|
||||
accessibilityLabel={_(msg`Display name`)}
|
||||
accessibilityHint="Edit your display name"
|
||||
/>
|
||||
</View>
|
||||
<View style={s.pb10}>
|
||||
<Text style={[styles.label, pal.text]}>Description</Text>
|
||||
<Text style={[styles.label, pal.text]}>
|
||||
<Trans>Description</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editProfileDescriptionInput"
|
||||
style={[styles.textArea, pal.border, pal.text]}
|
||||
|
@ -199,7 +208,7 @@ export function Component({
|
|||
value={description}
|
||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
||||
accessible={true}
|
||||
accessibilityLabel="Description"
|
||||
accessibilityLabel={_(msg`Description`)}
|
||||
accessibilityHint="Edit your profile description"
|
||||
/>
|
||||
</View>
|
||||
|
@ -213,14 +222,16 @@ export function Component({
|
|||
style={s.mt10}
|
||||
onPress={onPressSave}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Save"
|
||||
accessibilityLabel={_(msg`Save`)}
|
||||
accessibilityHint="Saves any changes to your profile">
|
||||
<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]}>Save Changes</Text>
|
||||
<Text style={[s.white, s.bold]}>
|
||||
<Trans>Save Changes</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
@ -231,11 +242,13 @@ export function Component({
|
|||
style={s.mt5}
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel profile editing"
|
||||
accessibilityLabel={_(msg`Cancel profile editing`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}>
|
||||
<View style={[styles.btn]}>
|
||||
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
||||
<Text style={[s.black, s.bold, pal.text]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</AnimatedTouchableOpacity>
|
||||
)}
|
||||
|
|
|
@ -15,6 +15,7 @@ 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 {useModalControls} from '#/state/modals'
|
||||
import {useInvitesState, useInvitesAPI} from '#/state/invites'
|
||||
import {UserInfoText} from '../util/UserInfoText'
|
||||
|
@ -38,8 +39,10 @@ export function Component({}: {}) {
|
|||
<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} />
|
||||
|
@ -63,10 +66,12 @@ 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]}>
|
||||
<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) => (
|
||||
|
@ -138,7 +143,9 @@ const InviteCode = observer(function InviteCodeImpl({
|
|||
</Text>
|
||||
<View style={styles.flex1} />
|
||||
{!used && invitesState.copiedInvites.includes(invite.code) && (
|
||||
<Text style={[pal.textLight, styles.codeCopied]}>Copied</Text>
|
||||
<Text style={[pal.textLight, styles.codeCopied]}>
|
||||
<Trans>Copied</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{!used && (
|
||||
<FontAwesomeIcon
|
||||
|
@ -154,7 +161,9 @@ const InviteCode = observer(function InviteCodeImpl({
|
|||
gap: 8,
|
||||
paddingTop: 6,
|
||||
}}>
|
||||
<Text style={pal.text}>Used by:</Text>
|
||||
<Text style={pal.text}>
|
||||
<Trans>Used by:</Trans>
|
||||
</Text>
|
||||
{invite.uses.map(use => (
|
||||
<Link
|
||||
key={use.usedBy}
|
||||
|
|
|
@ -10,6 +10,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['50%']
|
||||
|
@ -24,6 +26,7 @@ export const Component = observer(function Component({
|
|||
const pal = usePalette('default')
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {_} = useLingui()
|
||||
const potentiallyMisleading = isPossiblyAUrl(text)
|
||||
|
||||
const onPressVisit = () => {
|
||||
|
@ -45,26 +48,26 @@ export const Component = observer(function Component({
|
|||
size={18}
|
||||
/>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
Potentially Misleading Link
|
||||
<Trans>Potentially Misleading Link</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
Leaving Bluesky
|
||||
<Trans>Leaving Bluesky</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{gap: 10}}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
This link is taking you to the following website:
|
||||
<Trans>This link is taking you to the following website:</Trans>
|
||||
</Text>
|
||||
|
||||
<LinkBox href={href} />
|
||||
|
||||
{potentiallyMisleading && (
|
||||
<Text type="lg" style={pal.text}>
|
||||
Make sure this is where you intend to go!
|
||||
<Trans>Make sure this is where you intend to go!</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
@ -74,7 +77,7 @@ export const Component = observer(function Component({
|
|||
testID="confirmBtn"
|
||||
type="primary"
|
||||
onPress={onPressVisit}
|
||||
accessibilityLabel="Visit Site"
|
||||
accessibilityLabel={_(msg`Visit Site`)}
|
||||
accessibilityHint=""
|
||||
label="Visit Site"
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
|
@ -84,7 +87,7 @@ export const Component = observer(function Component({
|
|||
testID="cancelBtn"
|
||||
type="default"
|
||||
onPress={() => closeModal()}
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint=""
|
||||
label="Cancel"
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
|
|
|
@ -26,6 +26,8 @@ import {cleanError} from 'lib/strings/errors'
|
|||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
@ -39,6 +41,7 @@ export const Component = observer(function Component({
|
|||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [query, setQuery] = useState('')
|
||||
|
@ -85,7 +88,7 @@ export const Component = observer(function Component({
|
|||
value={query}
|
||||
onChangeText={onChangeQuery}
|
||||
accessible={true}
|
||||
accessibilityLabel="Search"
|
||||
accessibilityLabel={_(msg`Search`)}
|
||||
accessibilityHint=""
|
||||
autoFocus
|
||||
autoCapitalize="none"
|
||||
|
@ -97,7 +100,7 @@ export const Component = observer(function Component({
|
|||
<Pressable
|
||||
onPress={onPressCancelSearch}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel search"
|
||||
accessibilityLabel={_(msg`Cancel search`)}
|
||||
accessibilityHint="Exits inputting search query"
|
||||
onAccessibilityEscape={onPressCancelSearch}
|
||||
hitSlop={HITSLOP_20}>
|
||||
|
@ -136,7 +139,7 @@ export const Component = observer(function Component({
|
|||
pal.textLight,
|
||||
{paddingHorizontal: 12, paddingVertical: 16},
|
||||
]}>
|
||||
No results found for {autocompleteView.prefix}
|
||||
<Trans>No results found for {autocompleteView.prefix}</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
@ -149,7 +152,7 @@ export const Component = observer(function Component({
|
|||
testID="doneBtn"
|
||||
type="default"
|
||||
onPress={() => closeModal()}
|
||||
accessibilityLabel="Done"
|
||||
accessibilityLabel={_(msg`Done`)}
|
||||
accessibilityHint=""
|
||||
label="Done"
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
|
|
|
@ -6,6 +6,8 @@ import {Text} from '../util/text/Text'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {RepostIcon} from 'lib/icons'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = [250]
|
||||
|
@ -21,6 +23,7 @@ export function Component({
|
|||
// TODO: Add author into component
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const onPress = async () => {
|
||||
closeModal()
|
||||
|
@ -38,7 +41,7 @@ export function Component({
|
|||
accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
|
||||
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
|
||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||
{!isReposted ? 'Repost' : 'Undo repost'}
|
||||
<Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
|
@ -46,11 +49,11 @@ export function Component({
|
|||
style={[styles.actionBtn]}
|
||||
onPress={onQuote}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Quote post"
|
||||
accessibilityLabel={_(msg`Quote post`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
|
||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||
Quote Post
|
||||
<Trans>Quote Post</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
@ -58,7 +61,7 @@ export function Component({
|
|||
testID="cancelBtn"
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel quote post"
|
||||
accessibilityLabel={_(msg`Cancel quote post`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPress}>
|
||||
<LinearGradient
|
||||
|
@ -66,7 +69,9 @@ export function Component({
|
|||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold, s.f18]}>Cancel</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
@ -9,6 +9,8 @@ import {isWeb} from 'platform/detection'
|
|||
import {Button} from '../util/forms/Button'
|
||||
import {SelectableBtn} from '../util/forms/SelectableBtn'
|
||||
import {ScrollView} from 'view/com/modals/util'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
|
||||
|
@ -28,6 +30,7 @@ export const Component = observer(function Component({
|
|||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [selected, setSelected] = useState(labels)
|
||||
const {_} = useLingui()
|
||||
|
||||
const toggleAdultLabel = (label: string) => {
|
||||
const hadLabel = selected.includes(label)
|
||||
|
@ -51,7 +54,7 @@ export const Component = observer(function Component({
|
|||
<View testID="selfLabelModal" style={[pal.view, styles.container]}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
Add a content warning
|
||||
<Trans>Add a content warning</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
@ -70,7 +73,7 @@ export const Component = observer(function Component({
|
|||
paddingBottom: 8,
|
||||
}}>
|
||||
<Text type="title" style={pal.text}>
|
||||
Adult Content
|
||||
<Trans>Adult Content</Trans>
|
||||
</Text>
|
||||
{hasAdultSelection ? (
|
||||
<Button
|
||||
|
@ -78,7 +81,7 @@ export const Component = observer(function Component({
|
|||
onPress={removeAdultLabel}
|
||||
style={{paddingTop: 0, paddingBottom: 0, paddingRight: 0}}>
|
||||
<Text type="md" style={pal.link}>
|
||||
Remove
|
||||
<Trans>Remove</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
) : null}
|
||||
|
@ -116,23 +119,25 @@ export const Component = observer(function Component({
|
|||
|
||||
<Text style={[pal.text, styles.adultExplainer]}>
|
||||
{selected.includes('sexual') ? (
|
||||
<>Pictures meant for adults.</>
|
||||
<Trans>Pictures meant for adults.</Trans>
|
||||
) : selected.includes('nudity') ? (
|
||||
<>Artistic or non-erotic nudity.</>
|
||||
<Trans>Artistic or non-erotic nudity.</Trans>
|
||||
) : selected.includes('porn') ? (
|
||||
<>Sexual activity or erotic nudity.</>
|
||||
<Trans>Sexual activity or erotic nudity.</Trans>
|
||||
) : (
|
||||
<>If none are selected, suitable for all ages.</>
|
||||
<Trans>If none are selected, suitable for all ages.</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<View>
|
||||
<Text style={[pal.textLight]}>
|
||||
<Text type="md-bold" style={[pal.textLight]}>
|
||||
Not Applicable
|
||||
<Text type="md-bold" style={[pal.textLight, s.mr5]}>
|
||||
<Trans>Not Applicable.</Trans>
|
||||
</Text>
|
||||
. This warning is only available for posts with media attached.
|
||||
<Trans>
|
||||
This warning is only available for posts with media attached.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
@ -147,9 +152,11 @@ export const Component = observer(function Component({
|
|||
}}
|
||||
style={styles.btn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm"
|
||||
accessibilityLabel={_(msg`Confirm`)}
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -11,6 +11,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
|
||||
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['80%']
|
||||
|
@ -19,6 +21,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
|||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const [customUrl, setCustomUrl] = useState<string>('')
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
|
||||
const doSelect = (url: string) => {
|
||||
|
@ -32,7 +35,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
|||
return (
|
||||
<View style={[pal.view, s.flex1]} testID="serverInputModal">
|
||||
<Text type="2xl-bold" style={[pal.text, s.textCenter]}>
|
||||
Choose Service
|
||||
<Trans>Choose Service</Trans>
|
||||
</Text>
|
||||
<ScrollView style={styles.inner}>
|
||||
<View style={styles.group}>
|
||||
|
@ -43,7 +46,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
|||
style={styles.btn}
|
||||
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
|
||||
accessibilityRole="button">
|
||||
<Text style={styles.btnText}>Local dev server</Text>
|
||||
<Text style={styles.btnText}>
|
||||
<Trans>Local dev server</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-right"
|
||||
style={s.white as FontAwesomeIconStyle}
|
||||
|
@ -53,7 +58,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
|||
style={styles.btn}
|
||||
onPress={() => doSelect(STAGING_SERVICE)}
|
||||
accessibilityRole="button">
|
||||
<Text style={styles.btnText}>Staging</Text>
|
||||
<Text style={styles.btnText}>
|
||||
<Trans>Staging</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-right"
|
||||
style={s.white as FontAwesomeIconStyle}
|
||||
|
@ -65,9 +72,11 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
|||
style={styles.btn}
|
||||
onPress={() => doSelect(PROD_SERVICE)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Select Bluesky Social"
|
||||
accessibilityLabel={_(msg`Select Bluesky Social`)}
|
||||
accessibilityHint="Sets Bluesky Social as your service provider">
|
||||
<Text style={styles.btnText}>Bluesky.Social</Text>
|
||||
<Text style={styles.btnText}>
|
||||
<Trans>Bluesky.Social</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-right"
|
||||
style={s.white as FontAwesomeIconStyle}
|
||||
|
@ -75,7 +84,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
|||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.group}>
|
||||
<Text style={[pal.text, styles.label]}>Other service</Text>
|
||||
<Text style={[pal.text, styles.label]}>
|
||||
<Trans>Other service</Trans>
|
||||
</Text>
|
||||
<View style={s.flexRow}>
|
||||
<TextInput
|
||||
testID="customServerTextInput"
|
||||
|
@ -88,7 +99,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
|||
keyboardAppearance={theme.colorScheme}
|
||||
value={customUrl}
|
||||
onChangeText={setCustomUrl}
|
||||
accessibilityLabel="Custom domain"
|
||||
accessibilityLabel={_(msg`Custom domain`)}
|
||||
// TODO: Simplify this wording further to be understandable by everyone
|
||||
accessibilityHint="Use your domain as your Bluesky client service provider"
|
||||
/>
|
||||
|
|
|
@ -17,12 +17,15 @@ import {Link} from '../util/Link'
|
|||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
|
||||
import {Haptics} from 'lib/haptics'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const snapPoints = ['40%', '90%']
|
||||
|
||||
export function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const {_: _lingui} = useLingui()
|
||||
|
||||
const store = useStores()
|
||||
const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
|
||||
|
@ -41,7 +44,7 @@ export function Component({}: {}) {
|
|||
style={[styles.container, pal.view]}
|
||||
contentContainerStyle={[styles.innerContainer, pal.view]}>
|
||||
<Text type="title-xl" style={[styles.title, pal.text]}>
|
||||
Switch Account
|
||||
<Trans>Switch Account</Trans>
|
||||
</Text>
|
||||
{isSwitching ? (
|
||||
<View style={[pal.view, styles.linkCard]}>
|
||||
|
@ -65,10 +68,10 @@ export function Component({}: {}) {
|
|||
testID="signOutBtn"
|
||||
onPress={isSwitching ? undefined : onPressSignout}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Sign out"
|
||||
accessibilityLabel={_lingui(msg`Sign out`)}
|
||||
accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
|
||||
<Text type="lg" style={pal.link}>
|
||||
Sign out
|
||||
<Trans>Sign out</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
@ -21,6 +21,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {isWeb, isAndroid} from 'platform/detection'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
@ -39,6 +41,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
|||
const store = useStores()
|
||||
const {closeModal} = useModalControls()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const palPrimary = usePalette('primary')
|
||||
const palInverted = usePalette('inverted')
|
||||
const [originalSelections, setOriginalSelections] = React.useState<string[]>(
|
||||
|
@ -181,7 +184,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
|||
return (
|
||||
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
Update {displayName} in Lists
|
||||
<Trans>Update {displayName} in Lists</Trans>
|
||||
</Text>
|
||||
<ListsList
|
||||
listsList={listsList}
|
||||
|
@ -195,7 +198,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
|||
type="default"
|
||||
onPress={onPressCancel}
|
||||
style={styles.footerBtn}
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}
|
||||
label="Cancel"
|
||||
|
@ -206,7 +209,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
|||
type="primary"
|
||||
onPress={onPressSave}
|
||||
style={styles.footerBtn}
|
||||
accessibilityLabel="Save changes"
|
||||
accessibilityLabel={_(msg`Save changes`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressSave}
|
||||
label="Save Changes"
|
||||
|
|
|
@ -20,6 +20,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
@ -37,6 +39,7 @@ export const Component = observer(function Component({
|
|||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const [stage, setStage] = useState<Stages>(
|
||||
showReminder ? Stages.Reminder : Stages.Email,
|
||||
)
|
||||
|
@ -98,21 +101,21 @@ export const Component = observer(function Component({
|
|||
|
||||
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
||||
{stage === Stages.Reminder ? (
|
||||
<>
|
||||
<Trans>
|
||||
Your email has not yet been verified. This is an important
|
||||
security step which we recommend.
|
||||
</>
|
||||
</Trans>
|
||||
) : stage === Stages.Email ? (
|
||||
<>
|
||||
<Trans>
|
||||
This is important in case you ever need to change your email or
|
||||
reset your password.
|
||||
</>
|
||||
</Trans>
|
||||
) : stage === Stages.ConfirmCode ? (
|
||||
<>
|
||||
<Trans>
|
||||
An email has been sent to{' '}
|
||||
{store.session.currentSession?.email || ''}. It includes a
|
||||
confirmation code which you can enter below.
|
||||
</>
|
||||
</Trans>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
@ -132,7 +135,7 @@ export const Component = observer(function Component({
|
|||
</View>
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel="Change my email"
|
||||
accessibilityLabel={_(msg`Change my email`)}
|
||||
accessibilityHint=""
|
||||
onPress={onEmailIncorrect}
|
||||
style={styles.changeEmailLink}>
|
||||
|
@ -150,7 +153,7 @@ export const Component = observer(function Component({
|
|||
value={confirmationCode}
|
||||
onChangeText={setConfirmationCode}
|
||||
accessible={true}
|
||||
accessibilityLabel="Confirmation code"
|
||||
accessibilityLabel={_(msg`Confirmation code`)}
|
||||
accessibilityHint=""
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
|
@ -174,7 +177,7 @@ export const Component = observer(function Component({
|
|||
testID="getStartedBtn"
|
||||
type="primary"
|
||||
onPress={() => setStage(Stages.Email)}
|
||||
accessibilityLabel="Get Started"
|
||||
accessibilityLabel={_(msg`Get Started`)}
|
||||
accessibilityHint=""
|
||||
label="Get Started"
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
|
@ -187,7 +190,7 @@ export const Component = observer(function Component({
|
|||
testID="sendEmailBtn"
|
||||
type="primary"
|
||||
onPress={onSendEmail}
|
||||
accessibilityLabel="Send Confirmation Email"
|
||||
accessibilityLabel={_(msg`Send Confirmation Email`)}
|
||||
accessibilityHint=""
|
||||
label="Send Confirmation Email"
|
||||
labelContainerStyle={{
|
||||
|
@ -199,7 +202,7 @@ export const Component = observer(function Component({
|
|||
<Button
|
||||
testID="haveCodeBtn"
|
||||
type="default"
|
||||
accessibilityLabel="I have a code"
|
||||
accessibilityLabel={_(msg`I have a code`)}
|
||||
accessibilityHint=""
|
||||
label="I have a confirmation code"
|
||||
labelContainerStyle={{
|
||||
|
@ -216,7 +219,7 @@ export const Component = observer(function Component({
|
|||
testID="confirmBtn"
|
||||
type="primary"
|
||||
onPress={onConfirm}
|
||||
accessibilityLabel="Confirm"
|
||||
accessibilityLabel={_(msg`Confirm`)}
|
||||
accessibilityHint=""
|
||||
label="Confirm"
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
|
|
|
@ -17,6 +17,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['80%']
|
||||
|
@ -24,6 +26,7 @@ export const snapPoints = ['80%']
|
|||
export function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const [email, setEmail] = React.useState<string>('')
|
||||
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
||||
|
@ -61,12 +64,14 @@ export function Component({}: {}) {
|
|||
<View style={[styles.container, pal.view]}>
|
||||
<View style={[styles.innerContainer, pal.view]}>
|
||||
<Text type="title-xl" style={[styles.title, pal.text]}>
|
||||
Join the waitlist
|
||||
<Trans>Join the waitlist</Trans>
|
||||
</Text>
|
||||
<Text type="lg" style={[styles.description, pal.text]}>
|
||||
Bluesky uses invites to build a healthier community. If you don't know
|
||||
anybody with an invite, you can sign up for the waitlist and we'll
|
||||
send one soon.
|
||||
<Trans>
|
||||
Bluesky uses invites to build a healthier community. If you don't
|
||||
know anybody with an invite, you can sign up for the waitlist and
|
||||
we'll send one soon.
|
||||
</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
|
||||
|
@ -80,7 +85,7 @@ export function Component({}: {}) {
|
|||
onSubmitEditing={onPressSignup}
|
||||
enterKeyHint="done"
|
||||
accessible={true}
|
||||
accessibilityLabel="Email"
|
||||
accessibilityLabel={_(msg`Email`)}
|
||||
accessibilityHint="Input your email to get on the Bluesky waitlist"
|
||||
/>
|
||||
{error ? (
|
||||
|
@ -99,7 +104,9 @@ export function Component({}: {}) {
|
|||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
<Text style={[s.ml10, pal.text]}>
|
||||
<Trans>
|
||||
Your email has been saved! We'll be in touch soon.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
|
@ -114,7 +121,7 @@ export function Component({}: {}) {
|
|||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||
Join Waitlist
|
||||
<Trans>Join Waitlist</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
@ -122,11 +129,11 @@ export function Component({}: {}) {
|
|||
style={[styles.btn, s.mt10]}
|
||||
onPress={onCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel waitlist signup"
|
||||
accessibilityLabel={_(msg`Cancel waitlist signup`)}
|
||||
accessibilityHint={`Exits signing up for waitlist with ${email}`}
|
||||
onAccessibilityEscape={onCancel}>
|
||||
<Text type="button-lg" style={pal.textLight}>
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
|
|
|
@ -10,6 +10,8 @@ import {s, gradients} from 'lib/styles'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
enum AspectRatio {
|
||||
|
@ -35,6 +37,7 @@ export function Component({
|
|||
}) {
|
||||
const {closeModal} = useModalControls()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
|
||||
const [scale, setScale] = React.useState<number>(1)
|
||||
const editorRef = React.useRef<ImageEditor>(null)
|
||||
|
@ -96,7 +99,7 @@ export function Component({
|
|||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Wide)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Wide"
|
||||
accessibilityLabel={_(msg`Wide`)}
|
||||
accessibilityHint="Sets image aspect ratio to wide">
|
||||
<RectWideIcon
|
||||
size={24}
|
||||
|
@ -106,7 +109,7 @@ export function Component({
|
|||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Tall)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Tall"
|
||||
accessibilityLabel={_(msg`Tall`)}
|
||||
accessibilityHint="Sets image aspect ratio to tall">
|
||||
<RectTallIcon
|
||||
size={24}
|
||||
|
@ -116,7 +119,7 @@ export function Component({
|
|||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Square)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Square"
|
||||
accessibilityLabel={_(msg`Square`)}
|
||||
accessibilityHint="Sets image aspect ratio to square">
|
||||
<SquareIcon
|
||||
size={24}
|
||||
|
@ -128,7 +131,7 @@ export function Component({
|
|||
<TouchableOpacity
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel image crop"
|
||||
accessibilityLabel={_(msg`Cancel image crop`)}
|
||||
accessibilityHint="Exits image cropping process">
|
||||
<Text type="xl" style={pal.link}>
|
||||
Cancel
|
||||
|
@ -138,7 +141,7 @@ export function Component({
|
|||
<TouchableOpacity
|
||||
onPress={onPressDone}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Save image crop"
|
||||
accessibilityLabel={_(msg`Save image crop`)}
|
||||
accessibilityHint="Saves image crop settings">
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
|
@ -146,7 +149,7 @@ export function Component({
|
|||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text type="xl-medium" style={s.white}>
|
||||
Done
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -4,6 +4,8 @@ import LinearGradient from 'react-native-linear-gradient'
|
|||
import {s, colors, gradients} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const ConfirmLanguagesButton = ({
|
||||
onPress,
|
||||
|
@ -13,6 +15,7 @@ export const ConfirmLanguagesButton = ({
|
|||
extraText?: string
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
return (
|
||||
<View
|
||||
|
@ -28,14 +31,16 @@ export const ConfirmLanguagesButton = ({
|
|||
testID="confirmContentLanguagesBtn"
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm content language settings"
|
||||
accessibilityLabel={_(msg`Confirm content language settings`)}
|
||||
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, s.f18]}>Done{extraText}</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Done{extraText}</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {deviceLocales} from 'platform/detection'
|
|||
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
|
||||
import {LanguageToggle} from './LanguageToggle'
|
||||
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
useLanguagePrefs,
|
||||
|
@ -69,12 +70,16 @@ export function Component({}: {}) {
|
|||
maxHeight: '90vh',
|
||||
},
|
||||
]}>
|
||||
<Text style={[pal.text, styles.title]}>Content Languages</Text>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>Content Languages</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, styles.description]}>
|
||||
<Trans>
|
||||
Which languages would you like to see in your algorithmic feeds?
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.textLight, styles.description]}>
|
||||
Leave them all unchecked to see any language.
|
||||
<Trans>Leave them all unchecked to see any language.</Trans>
|
||||
</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
{languages.map(lang => (
|
||||
|
|
|
@ -9,6 +9,7 @@ import {deviceLocales} from 'platform/detection'
|
|||
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
|
||||
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
|
||||
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
useLanguagePrefs,
|
||||
|
@ -71,9 +72,11 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
|
|||
maxHeight: '90vh',
|
||||
},
|
||||
]}>
|
||||
<Text style={[pal.text, styles.title]}>Post Languages</Text>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>Post Languages</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, styles.description]}>
|
||||
Which languages are used in this post?
|
||||
<Trans>Which languages are used in this post?</Trans>
|
||||
</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
{languages.map(lang => {
|
||||
|
|
|
@ -8,6 +8,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {s} from 'lib/styles'
|
||||
import {SendReportButton} from './SendReportButton'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export function InputIssueDetails({
|
||||
details,
|
||||
|
@ -23,6 +25,7 @@ export function InputIssueDetails({
|
|||
isProcessing: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
return (
|
||||
|
@ -35,14 +38,16 @@ export function InputIssueDetails({
|
|||
style={[s.mb10, styles.backBtn]}
|
||||
onPress={goBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Add details"
|
||||
accessibilityLabel={_(msg`Add details`)}
|
||||
accessibilityHint="Add more details to your report">
|
||||
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
|
||||
<Text style={[pal.text, s.f18, pal.link]}> Back</Text>
|
||||
<Text style={[pal.text, s.f18, pal.link]}>
|
||||
<Trans> Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={[pal.btn, styles.detailsInputContainer]}>
|
||||
<TextInput
|
||||
accessibilityLabel="Text input field"
|
||||
accessibilityLabel={_(msg`Text input field`)}
|
||||
accessibilityHint="Enter a reason for reporting this post."
|
||||
placeholder="Enter a reason or any other details here."
|
||||
placeholderTextColor={pal.textLight.color}
|
||||
|
|
|
@ -14,6 +14,8 @@ import {SendReportButton} from './SendReportButton'
|
|||
import {InputIssueDetails} from './InputIssueDetails'
|
||||
import {ReportReasonOptions} from './ReasonOptions'
|
||||
import {CollectionId} from './types'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
|
||||
|
@ -148,6 +150,7 @@ const SelectIssue = ({
|
|||
atUri: AtUri | null
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const collectionName = getCollectionNameForReport(atUri)
|
||||
const onSelectIssue = (v: string) => setIssue(v)
|
||||
const goToDetails = () => {
|
||||
|
@ -160,9 +163,11 @@ const SelectIssue = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Text style={[pal.text, styles.title]}>Report {collectionName}</Text>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>Report {collectionName}</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.textLight, styles.description]}>
|
||||
What is the issue with this {collectionName}?
|
||||
<Trans>What is the issue with this {collectionName}?</Trans>
|
||||
</Text>
|
||||
<View style={{marginBottom: 10}}>
|
||||
<ReportReasonOptions
|
||||
|
@ -184,9 +189,11 @@ const SelectIssue = ({
|
|||
style={styles.addDetailsBtn}
|
||||
onPress={goToDetails}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Add details"
|
||||
accessibilityLabel={_(msg`Add details`)}
|
||||
accessibilityHint="Add more details to your report">
|
||||
<Text style={[s.f18, pal.link]}>Add details to report</Text>
|
||||
<Text style={[s.f18, pal.link]}>
|
||||
<Trans>Add details to report</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : undefined}
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
} from 'react-native'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {s, gradients, colors} from 'lib/styles'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export function SendReportButton({
|
||||
onPress,
|
||||
|
@ -16,6 +18,7 @@ export function SendReportButton({
|
|||
onPress: () => void
|
||||
isProcessing: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
// loading state
|
||||
// =
|
||||
if (isProcessing) {
|
||||
|
@ -31,14 +34,16 @@ export function SendReportButton({
|
|||
style={s.mt10}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Report post"
|
||||
accessibilityLabel={_(msg`Report post`)}
|
||||
accessibilityHint={`Reports post with reason and details`}>
|
||||
<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, s.f18]}>Send Report</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Send Report</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
|
|
@ -40,6 +40,8 @@ import {formatCount} from '../util/numeric/format'
|
|||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {TimeElapsed} from '../util/TimeElapsed'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
const MAX_AUTHORS = 5
|
||||
|
||||
|
@ -232,7 +234,9 @@ export const FeedItem = observer(function FeedItemImpl({
|
|||
/>
|
||||
{authors.length > 1 ? (
|
||||
<>
|
||||
<Text style={[pal.text]}> and </Text>
|
||||
<Text style={[pal.text, s.mr5, s.ml5]}>
|
||||
<Trans>and</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.bold]}>
|
||||
{formatCount(authors.length - 1)}{' '}
|
||||
{pluralize(authors.length - 1, 'other')}
|
||||
|
@ -292,6 +296,8 @@ function CondensedAuthorsList({
|
|||
onToggleAuthorsExpanded: () => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
if (!visible) {
|
||||
return (
|
||||
<View style={styles.avis}>
|
||||
|
@ -299,7 +305,7 @@ function CondensedAuthorsList({
|
|||
style={styles.expandedAuthorsCloseBtn}
|
||||
onPress={onToggleAuthorsExpanded}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Hide user list"
|
||||
accessibilityLabel={_(msg`Hide user list`)}
|
||||
accessibilityHint="Collapses list of users for a given notification">
|
||||
<FontAwesomeIcon
|
||||
icon="angle-up"
|
||||
|
@ -307,7 +313,7 @@ function CondensedAuthorsList({
|
|||
style={[styles.expandedAuthorsCloseBtnIcon, pal.text]}
|
||||
/>
|
||||
<Text type="sm-medium" style={pal.text}>
|
||||
Hide
|
||||
<Trans>Hide</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
@ -328,7 +334,7 @@ function CondensedAuthorsList({
|
|||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
accessibilityLabel="Show users"
|
||||
accessibilityLabel={_(msg`Show users`)}
|
||||
accessibilityHint="Opens an expanded list of users in this notification"
|
||||
onPress={onToggleAuthorsExpanded}>
|
||||
<View style={styles.avis}>
|
||||
|
|
|
@ -14,6 +14,8 @@ import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
|||
import {s} from 'lib/styles'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
||||
|
||||
|
@ -22,6 +24,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
|||
) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const items = useHomeTabs(store.preferences.pinnedFeeds)
|
||||
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
|
||||
|
@ -45,7 +48,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
|||
testID="viewHeaderDrawerBtn"
|
||||
onPress={onPressAvi}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Open navigation"
|
||||
accessibilityLabel={_(msg`Open navigation`)}
|
||||
accessibilityHint="Access profile and other navigation links"
|
||||
hitSlop={HITSLOP_10}>
|
||||
<FontAwesomeIcon
|
||||
|
@ -64,7 +67,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
|||
href="/settings/home-feed"
|
||||
hitSlop={HITSLOP_10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Home Feed Preferences"
|
||||
accessibilityLabel={_(msg`Home Feed Preferences`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="sliders"
|
||||
|
|
|
@ -31,6 +31,8 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2}
|
||||
|
||||
|
@ -79,6 +81,7 @@ export const PostThread = observer(function PostThread({
|
|||
treeView: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isTablet, isDesktop} = useWebMediaQueries()
|
||||
const ref = useRef<FlatList>(null)
|
||||
const hasScrolledIntoView = useRef<boolean>(false)
|
||||
|
@ -197,7 +200,7 @@ export const PostThread = observer(function PostThread({
|
|||
return (
|
||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||
<Text type="lg-bold" style={pal.textLight}>
|
||||
Deleted post.
|
||||
<Trans>Deleted post.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
@ -205,7 +208,7 @@ export const PostThread = observer(function PostThread({
|
|||
return (
|
||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||
<Text type="lg-bold" style={pal.textLight}>
|
||||
Blocked post.
|
||||
<Trans>Blocked post.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
@ -214,7 +217,7 @@ export const PostThread = observer(function PostThread({
|
|||
<Pressable
|
||||
onPress={() => setMaxVisible(n => n + 50)}
|
||||
style={[pal.border, pal.view, styles.itemContainer]}
|
||||
accessibilityLabel="Load more posts"
|
||||
accessibilityLabel={_(msg`Load more posts`)}
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[
|
||||
|
@ -222,7 +225,7 @@ export const PostThread = observer(function PostThread({
|
|||
{paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
|
||||
]}>
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
Load more posts
|
||||
<Trans>Load more posts</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
@ -275,6 +278,7 @@ export const PostThread = observer(function PostThread({
|
|||
posts,
|
||||
onRefresh,
|
||||
treeView,
|
||||
_,
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -302,15 +306,15 @@ export const PostThread = observer(function PostThread({
|
|||
<CenteredView>
|
||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||
<Text type="title-lg" style={[pal.text, s.mb5]}>
|
||||
Post not found
|
||||
<Trans>Post not found</Trans>
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, s.mb10]}>
|
||||
The post may have been deleted.
|
||||
<Trans>The post may have been deleted.</Trans>
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Back"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<Text type="2xl" style={pal.link}>
|
||||
<FontAwesomeIcon
|
||||
|
@ -318,7 +322,7 @@ export const PostThread = observer(function PostThread({
|
|||
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
|
||||
size={14}
|
||||
/>
|
||||
Back
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
@ -336,15 +340,18 @@ export const PostThread = observer(function PostThread({
|
|||
<CenteredView>
|
||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||
<Text type="title-lg" style={[pal.text, s.mb5]}>
|
||||
Post hidden
|
||||
<Trans>Post hidden</Trans>
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, s.mb10]}>
|
||||
You have blocked the author or you have been blocked by the author.
|
||||
<Trans>
|
||||
You have blocked the author or you have been blocked by the
|
||||
author.
|
||||
</Trans>
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Back"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<Text type="2xl" style={pal.link}>
|
||||
<FontAwesomeIcon
|
||||
|
@ -352,7 +359,7 @@ export const PostThread = observer(function PostThread({
|
|||
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
|
||||
size={14}
|
||||
/>
|
||||
Back
|
||||
<Trans>Back</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
@ -37,6 +37,7 @@ import {makeProfileLink} from 'lib/routes/links'
|
|||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {MAX_POST_LINES} from 'lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
|
||||
|
@ -176,7 +177,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
icon={['far', 'trash-can']}
|
||||
style={pal.icon as FontAwesomeIconStyle}
|
||||
/>
|
||||
<Text style={[pal.textLight, s.ml10]}>This post has been deleted.</Text>
|
||||
<Text style={[pal.textLight, s.ml10]}>
|
||||
<Trans>This post has been deleted.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -650,9 +653,11 @@ function ExpandedPostDetails({
|
|||
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
||||
{needsTranslation && (
|
||||
<>
|
||||
<Text style={pal.textLight}> • </Text>
|
||||
<Text style={[pal.textLight, s.ml5, s.mr5]}>•</Text>
|
||||
<Link href={translatorUrl} title="Translate">
|
||||
<Text style={pal.link}>Translate</Text>
|
||||
<Text style={pal.link}>
|
||||
<Trans>Translate</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -40,6 +40,8 @@ import {makeProfileLink} from 'lib/routes/links'
|
|||
import {Link} from '../util/Link'
|
||||
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
interface Props {
|
||||
|
@ -114,6 +116,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
|
@ -369,10 +372,10 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
onPress={onPressEditProfile}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Edit profile"
|
||||
accessibilityLabel={_(msg`Edit profile`)}
|
||||
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
|
||||
<Text type="button" style={pal.text}>
|
||||
Edit Profile
|
||||
<Trans>Edit Profile</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : view.viewer.blocking ? (
|
||||
|
@ -382,10 +385,10 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
onPress={onPressUnblockAccount}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Unblock"
|
||||
accessibilityLabel={_(msg`Unblock`)}
|
||||
accessibilityHint="">
|
||||
<Text type="button" style={[pal.text, s.bold]}>
|
||||
Unblock
|
||||
<Trans>Unblock</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
@ -439,7 +442,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
size={14}
|
||||
/>
|
||||
<Text type="button" style={pal.text}>
|
||||
Following
|
||||
<Trans>Following</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
|
@ -455,7 +458,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
style={[palInverted.text, s.mr5]}
|
||||
/>
|
||||
<Text type="button" style={[palInverted.text, s.bold]}>
|
||||
Follow
|
||||
<Trans>Follow</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
@ -465,7 +468,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
<NativeDropdown
|
||||
testID="profileHeaderDropdownBtn"
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="More options"
|
||||
accessibilityLabel={_(msg`More options`)}
|
||||
accessibilityHint="">
|
||||
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
||||
<FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
|
||||
|
@ -488,7 +491,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
{view.viewer.followedBy && !blockHide ? (
|
||||
<View style={[styles.pill, pal.btn, s.mr5]}>
|
||||
<Text type="xs" style={[pal.text]}>
|
||||
Follows you
|
||||
<Trans>Follows you</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
|
@ -533,7 +536,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
{following}{' '}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
following
|
||||
<Trans>following</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
|
@ -572,7 +575,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Back"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.backBtnWrapper}>
|
||||
<BlurView style={styles.backBtn} blurType="dark">
|
||||
|
|
|
@ -17,6 +17,8 @@ import {NavigationProp} from 'lib/routes/types'
|
|||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {ImagesLightbox} from 'state/models/ui/shell'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
|
||||
export const ProfileSubpageHeader = observer(function HeaderImpl({
|
||||
|
@ -45,6 +47,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
|
|||
const store = useStores()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const canGoBack = navigation.canGoBack()
|
||||
|
@ -123,7 +126,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
|
|||
testID="headerAviButton"
|
||||
onPress={onPressAvi}
|
||||
accessibilityRole="image"
|
||||
accessibilityLabel="View the avatar"
|
||||
accessibilityLabel={_(msg`View the avatar`)}
|
||||
accessibilityHint=""
|
||||
style={{width: 58}}>
|
||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||
|
|
|
@ -11,6 +11,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
|
||||
interface Props {
|
||||
|
@ -36,6 +38,7 @@ export function HeaderWithInput({
|
|||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {track} = useAnalytics()
|
||||
const textInput = React.useRef<TextInput>(null)
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
@ -65,7 +68,7 @@ export function HeaderWithInput({
|
|||
hitSlop={HITSLOP_10}
|
||||
style={styles.headerMenuBtn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Menu"
|
||||
accessibilityLabel={_(msg`Menu`)}
|
||||
accessibilityHint="Access navigation links and settings">
|
||||
<FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} />
|
||||
</TouchableOpacity>
|
||||
|
@ -95,7 +98,7 @@ export function HeaderWithInput({
|
|||
onSubmitEditing={onSubmitQuery}
|
||||
autoFocus={false}
|
||||
accessibilityRole="search"
|
||||
accessibilityLabel="Search"
|
||||
accessibilityLabel={_(msg`Search`)}
|
||||
accessibilityHint=""
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
|
@ -105,7 +108,7 @@ export function HeaderWithInput({
|
|||
testID="searchTextInputClearBtn"
|
||||
onPress={onPressClearQuery}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Clear search query"
|
||||
accessibilityLabel={_(msg`Clear search query`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="xmark"
|
||||
|
@ -120,7 +123,9 @@ export function HeaderWithInput({
|
|||
<TouchableOpacity
|
||||
onPress={onPressCancelSearchInner}
|
||||
accessibilityRole="button">
|
||||
<Text style={pal.text}>Cancel</Text>
|
||||
<Text style={pal.text}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : undefined}
|
||||
|
|
|
@ -9,10 +9,14 @@ import {useStores} from 'state/index'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||
import * as Toast from '../../com/util/Toast'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
export function AccountDropdownBtn({handle}: {handle: string}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
const items: DropdownItem[] = [
|
||||
{
|
||||
label: 'Remove account',
|
||||
|
@ -34,7 +38,7 @@ export function AccountDropdownBtn({handle}: {handle: string}) {
|
|||
<NativeDropdown
|
||||
testID="accountSettingsDropdownBtn"
|
||||
items={items}
|
||||
accessibilityLabel="Account options"
|
||||
accessibilityLabel={_(msg`Account options`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis-h"
|
||||
|
|
|
@ -6,6 +6,7 @@ import Animated, {
|
|||
interpolate,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import {t} from '@lingui/macro'
|
||||
|
||||
export function createCustomBackdrop(
|
||||
onClose?: (() => void) | undefined,
|
||||
|
@ -29,7 +30,7 @@ export function createCustomBackdrop(
|
|||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onClose}
|
||||
accessibilityLabel="Close bottom drawer"
|
||||
accessibilityLabel={t`Close bottom drawer`}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={() => {
|
||||
if (onClose !== undefined) {
|
||||
|
|
|
@ -16,6 +16,8 @@ import {isWeb, isAndroid} from 'platform/detection'
|
|||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {UserPreviewLink} from './UserPreviewLink'
|
||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
export type UserAvatarType = 'user' | 'algo' | 'list'
|
||||
|
||||
|
@ -184,6 +186,7 @@ export function EditableUserAvatar({
|
|||
}: EditableUserAvatarProps) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
|
||||
|
@ -294,7 +297,7 @@ export function EditableUserAvatar({
|
|||
<NativeDropdown
|
||||
testID="changeAvatarBtn"
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="Image options"
|
||||
accessibilityLabel={_(msg`Image options`)}
|
||||
accessibilityHint="">
|
||||
{avatar ? (
|
||||
<HighPriorityImage
|
||||
|
|
|
@ -14,6 +14,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {isWeb, isAndroid} from 'platform/detection'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
export function UserBanner({
|
||||
banner,
|
||||
|
@ -26,6 +28,7 @@ export function UserBanner({
|
|||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
|
||||
|
@ -112,7 +115,7 @@ export function UserBanner({
|
|||
<NativeDropdown
|
||||
testID="changeBannerBtn"
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="Image options"
|
||||
accessibilityLabel={_(msg`Image options`)}
|
||||
accessibilityHint="">
|
||||
{banner ? (
|
||||
<Image
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
import {Text} from '../text/Text'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
export function ErrorMessage({
|
||||
message,
|
||||
|
@ -27,6 +29,7 @@ export function ErrorMessage({
|
|||
}) {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('error')
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<View testID="errorMessageView" style={[styles.outer, pal.view, style]}>
|
||||
<View
|
||||
|
@ -49,7 +52,7 @@ export function ErrorMessage({
|
|||
style={styles.btn}
|
||||
onPress={onPressTryAgain}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityLabel={_(msg`Retry`)}
|
||||
accessibilityHint="Retries the last action, which errored out">
|
||||
<FontAwesomeIcon
|
||||
icon="arrows-rotate"
|
||||
|
|
|
@ -9,6 +9,8 @@ import {useTheme} from 'lib/ThemeContext'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Button} from '../forms/Button'
|
||||
import {CenteredView} from '../Views'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export function ErrorScreen({
|
||||
title,
|
||||
|
@ -25,6 +27,8 @@ export function ErrorScreen({
|
|||
}) {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
|
||||
<View style={styles.errorIconContainer}>
|
||||
|
@ -58,7 +62,7 @@ export function ErrorScreen({
|
|||
type="default"
|
||||
style={[styles.btn]}
|
||||
onPress={onPressTryAgain}
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityLabel={_(msg`Retry`)}
|
||||
accessibilityHint="Retries the last action, which errored out">
|
||||
<FontAwesomeIcon
|
||||
icon="arrows-rotate"
|
||||
|
@ -66,7 +70,7 @@ export function ErrorScreen({
|
|||
size={16}
|
||||
/>
|
||||
<Text type="button" style={[styles.btnText, pal.link]}>
|
||||
Try again
|
||||
<Trans>Try again</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
|
|
@ -17,6 +17,8 @@ import {colors} from 'lib/styles'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
const ESTIMATED_BTN_HEIGHT = 50
|
||||
const ESTIMATED_SEP_HEIGHT = 16
|
||||
|
@ -207,6 +209,7 @@ const DropdownItems = ({
|
|||
}: DropDownItemProps) => {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const dropDownBackgroundColor =
|
||||
theme.colorScheme === 'dark' ? pal.btn : pal.view
|
||||
const separatorColor =
|
||||
|
@ -224,7 +227,7 @@ const DropdownItems = ({
|
|||
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onOuterPress}
|
||||
accessibilityLabel="Toggle dropdown"
|
||||
accessibilityLabel={_(msg`Toggle dropdown`)}
|
||||
accessibilityHint="">
|
||||
<View style={[styles.bg]} />
|
||||
</TouchableWithoutFeedback>
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
DropdownItem as NativeDropdownItem,
|
||||
} from './NativeDropdown'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export function PostDropdownBtn({
|
||||
|
@ -38,6 +40,7 @@ export function PostDropdownBtn({
|
|||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
|
@ -152,7 +155,7 @@ export function PostDropdownBtn({
|
|||
<NativeDropdown
|
||||
testID={testID}
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="More post options"
|
||||
accessibilityLabel={_(msg`More post options`)}
|
||||
accessibilityHint="">
|
||||
<View style={style}>
|
||||
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
import {MagnifyingGlassIcon} from 'lib/icons'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
interface Props {
|
||||
query: string
|
||||
|
@ -33,6 +35,7 @@ export function SearchInput({
|
|||
}: Props) {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const textInput = React.useRef<TextInput>(null)
|
||||
|
||||
const onPressCancelSearchInner = React.useCallback(() => {
|
||||
|
@ -58,7 +61,7 @@ export function SearchInput({
|
|||
onChangeText={onChangeQuery}
|
||||
onSubmitEditing={onSubmitQuery}
|
||||
accessibilityRole="search"
|
||||
accessibilityLabel="Search"
|
||||
accessibilityLabel={_(msg`Search`)}
|
||||
accessibilityHint=""
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
|
@ -67,7 +70,7 @@ export function SearchInput({
|
|||
<TouchableOpacity
|
||||
onPress={onPressCancelSearchInner}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Clear search query"
|
||||
accessibilityLabel={_(msg`Clear search query`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="xmark"
|
||||
|
|
|
@ -6,6 +6,8 @@ import {ModerationUI} from '@atproto/api'
|
|||
import {Text} from '../text/Text'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export function ContentHider({
|
||||
|
@ -23,6 +25,7 @@ export function ContentHider({
|
|||
childContainerStyle?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const {openModal} = useModalControls()
|
||||
|
@ -69,7 +72,7 @@ export function ContentHider({
|
|||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint="">
|
||||
<ShieldExclamation size={18} style={pal.text} />
|
||||
</Pressable>
|
||||
|
|
|
@ -5,6 +5,8 @@ import {Text} from '../text/Text'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export function PostAlerts({
|
||||
|
@ -16,6 +18,7 @@ export function PostAlerts({
|
|||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
const shouldAlert = !!moderation.cause && moderation.alert
|
||||
|
@ -34,14 +37,14 @@ export function PostAlerts({
|
|||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<ShieldExclamation style={pal.text} size={16} />
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
{desc.name}{' '}
|
||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||
Learn More
|
||||
<Trans>Learn More</Trans>
|
||||
</Text>
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
|
|
@ -8,6 +8,8 @@ import {Text} from '../text/Text'
|
|||
import {addStyle} from 'lib/styles'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
interface Props extends ComponentProps<typeof Link> {
|
||||
|
@ -26,6 +28,7 @@ export function PostHider({
|
|||
...props
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const {openModal} = useModalControls()
|
||||
|
@ -70,7 +73,7 @@ export function PostHider({
|
|||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint="">
|
||||
<ShieldExclamation size={18} style={pal.text} />
|
||||
</Pressable>
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
describeModerationCause,
|
||||
getProfileModerationCauses,
|
||||
} from 'lib/moderation'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export function ProfileHeaderAlerts({
|
||||
|
@ -18,6 +20,7 @@ export function ProfileHeaderAlerts({
|
|||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
const causes = getProfileModerationCauses(moderation)
|
||||
|
@ -41,7 +44,7 @@ export function ProfileHeaderAlerts({
|
|||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<ShieldExclamation style={pal.text} size={24} />
|
||||
|
@ -49,7 +52,7 @@ export function ProfileHeaderAlerts({
|
|||
{desc.name}
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||
Learn More
|
||||
<Trans>Learn More</Trans>
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
|
|
|
@ -18,7 +18,10 @@ import {NavigationProp} from 'lib/routes/types'
|
|||
import {Text} from '../text/Text'
|
||||
import {Button} from '../forms/Button'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {s} from '#/lib/styles'
|
||||
|
||||
export function ScreenHider({
|
||||
testID,
|
||||
|
@ -36,6 +39,7 @@ export function ScreenHider({
|
|||
}>) {
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const {_} = useLingui()
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
@ -62,14 +66,13 @@ export function ScreenHider({
|
|||
</View>
|
||||
</View>
|
||||
<Text type="title-2xl" style={[styles.title, pal.text]}>
|
||||
Content Warning
|
||||
<Trans>Content Warning</Trans>
|
||||
</Text>
|
||||
<Text type="2xl" style={[styles.description, pal.textLight]}>
|
||||
This {screenDescription} has been flagged:{' '}
|
||||
<Text type="2xl-medium" style={pal.text}>
|
||||
{desc.name}
|
||||
<Trans>This {screenDescription} has been flagged:</Trans>
|
||||
<Text type="2xl-medium" style={[pal.text, s.ml5]}>
|
||||
{desc.name}.
|
||||
</Text>
|
||||
.{' '}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => {
|
||||
openModal({
|
||||
|
@ -79,10 +82,10 @@ export function ScreenHider({
|
|||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint="">
|
||||
<Text type="2xl" style={pal.link}>
|
||||
Learn More
|
||||
<Trans>Learn More</Trans>
|
||||
</Text>
|
||||
</TouchableWithoutFeedback>
|
||||
</Text>
|
||||
|
@ -99,7 +102,7 @@ export function ScreenHider({
|
|||
}}
|
||||
style={styles.btn}>
|
||||
<Text type="button-lg" style={pal.textInverted}>
|
||||
Go back
|
||||
<Trans>Go back</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
{!moderation.noOverride && (
|
||||
|
@ -108,7 +111,7 @@ export function ScreenHider({
|
|||
onPress={() => setOverride(v => !v)}
|
||||
style={styles.btn}>
|
||||
<Text type="button-lg" style={pal.text}>
|
||||
Show anyway
|
||||
<Trans>Show anyway</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
DropdownItem as NativeDropdownItem,
|
||||
} from '../forms/NativeDropdown'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
interface Props {
|
||||
isReposted: boolean
|
||||
|
@ -28,6 +30,7 @@ export const RepostButton = ({
|
|||
onQuote,
|
||||
}: Props) => {
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const defaultControlColor = React.useMemo(
|
||||
() => ({
|
||||
|
@ -63,7 +66,7 @@ export const RepostButton = ({
|
|||
<EventStopper>
|
||||
<NativeDropdown
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="Repost or quote post"
|
||||
accessibilityLabel={_(msg`Repost or quote post`)}
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[
|
||||
|
|
|
@ -16,6 +16,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
|
|||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
|
@ -55,8 +57,10 @@ export const AppPasswords = withAuthRequired(
|
|||
<AppPasswordsHeader />
|
||||
<View style={[styles.empty, pal.viewLight]}>
|
||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||
You have not created any app passwords yet. You can create one by
|
||||
pressing the button below.
|
||||
<Trans>
|
||||
You have not created any app passwords yet. You can create one
|
||||
by pressing the button below.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{!isTabletOrDesktop && <View style={styles.flex1} />}
|
||||
|
@ -146,8 +150,10 @@ function AppPasswordsHeader() {
|
|||
pal.text,
|
||||
isTabletOrDesktop && styles.descriptionDesktop,
|
||||
]}>
|
||||
Use app passwords to login to other Bluesky clients without giving full
|
||||
access to your account or password.
|
||||
<Trans>
|
||||
Use app passwords to login to other Bluesky clients without giving
|
||||
full access to your account or password.
|
||||
</Trans>
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
|
@ -164,6 +170,7 @@ function AppPassword({
|
|||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
const {contentLanguages} = useLanguagePrefs()
|
||||
|
||||
|
@ -188,7 +195,7 @@ function AppPassword({
|
|||
style={[styles.item, pal.border]}
|
||||
onPress={onDelete}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Delete app password"
|
||||
accessibilityLabel={_(msg`Delete app password`)}
|
||||
accessibilityHint="">
|
||||
<View>
|
||||
<Text type="md-bold" style={pal.text}>
|
||||
|
|
|
@ -27,6 +27,8 @@ import {FeedSourceModel} from 'state/models/content/feed-source'
|
|||
import {FlatList} from 'view/com/util/Views'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
|
||||
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
|
||||
|
@ -34,6 +36,7 @@ export const FeedsScreen = withAuthRequired(
|
|||
observer<Props>(function FeedsScreenImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||
const myFeeds = store.me.myFeeds
|
||||
|
@ -88,12 +91,12 @@ export const FeedsScreen = withAuthRequired(
|
|||
href="/settings/saved-feeds"
|
||||
hitSlop={10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Edit Saved Feeds"
|
||||
accessibilityLabel={_(msg`Edit Saved Feeds`)}
|
||||
accessibilityHint="Opens screen to edit Saved Feeds">
|
||||
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
|
||||
</Link>
|
||||
)
|
||||
}, [pal])
|
||||
}, [pal, _])
|
||||
|
||||
const onRefresh = React.useCallback(() => {
|
||||
myFeeds.refresh()
|
||||
|
@ -124,11 +127,11 @@ export const FeedsScreen = withAuthRequired(
|
|||
},
|
||||
]}>
|
||||
<Text type="title-lg" style={[pal.text, s.bold]}>
|
||||
My Feeds
|
||||
<Trans>My Feeds</Trans>
|
||||
</Text>
|
||||
<Link
|
||||
href="/settings/saved-feeds"
|
||||
accessibilityLabel="Edit My Feeds"
|
||||
accessibilityLabel={_(msg`Edit My Feeds`)}
|
||||
accessibilityHint="">
|
||||
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
|
||||
</Link>
|
||||
|
@ -139,7 +142,7 @@ export const FeedsScreen = withAuthRequired(
|
|||
} else if (item.type === 'saved-feeds-loading') {
|
||||
return (
|
||||
<>
|
||||
{Array.from(Array(item.numItems)).map((_, i) => (
|
||||
{Array.from(Array(item.numItems)).map((_i, i) => (
|
||||
<SavedFeedLoadingPlaceholder key={`placeholder-${i}`} />
|
||||
))}
|
||||
</>
|
||||
|
@ -161,7 +164,7 @@ export const FeedsScreen = withAuthRequired(
|
|||
},
|
||||
]}>
|
||||
<Text type="title-lg" style={[pal.text, s.bold]}>
|
||||
Discover new feeds
|
||||
<Trans>Discover new feeds</Trans>
|
||||
</Text>
|
||||
{!isMobile && (
|
||||
<SearchInput
|
||||
|
@ -203,14 +206,22 @@ export const FeedsScreen = withAuthRequired(
|
|||
paddingBottom: '150%',
|
||||
}}>
|
||||
<Text type="lg" style={pal.textLight}>
|
||||
No results found for "{query}"
|
||||
<Trans>No results found for "{query}"</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
[isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery],
|
||||
[
|
||||
isMobile,
|
||||
pal,
|
||||
query,
|
||||
onChangeQuery,
|
||||
onPressCancelSearch,
|
||||
onSubmitQuery,
|
||||
_,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -249,7 +260,7 @@ export const FeedsScreen = withAuthRequired(
|
|||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="New post"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</View>
|
||||
|
@ -289,7 +300,7 @@ function SavedFeed({feed}: {feed: FeedSourceModel}) {
|
|||
{feed.error ? (
|
||||
<View style={[styles.offlineSlug, pal.borderDark]}>
|
||||
<Text type="xs" style={pal.textLight}>
|
||||
Feed offline
|
||||
<Trans>Feed offline</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
|
|
@ -11,6 +11,8 @@ import {Text} from '../com/util/text/Text'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {getEntries} from '#/logger/logDump'
|
||||
import {ago} from 'lib/strings/time'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
|
||||
export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
||||
|
@ -18,6 +20,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
|||
'Log'
|
||||
>) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const [expanded, setExpanded] = React.useState<string[]>([])
|
||||
|
||||
|
@ -47,7 +50,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
|||
<TouchableOpacity
|
||||
style={[styles.entry, pal.border, pal.view]}
|
||||
onPress={toggler(entry.id)}
|
||||
accessibilityLabel="View debug entry"
|
||||
accessibilityLabel={_(msg`View debug entry`)}
|
||||
accessibilityHint="Opens additional details for a debug entry">
|
||||
{entry.level === 'debug' ? (
|
||||
<FontAwesomeIcon icon="info" />
|
||||
|
|
|
@ -14,6 +14,8 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
|||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import debounce from 'lodash.debounce'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
function RepliesThresholdInput({enabled}: {enabled: boolean}) {
|
||||
const store = useStores()
|
||||
|
@ -66,6 +68,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
|
||||
return (
|
||||
|
@ -84,7 +87,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
|
||||
]}>
|
||||
<Text type="xl" style={[pal.textLight, styles.description]}>
|
||||
Fine-tune the content you see on your home screen.
|
||||
<Trans>Fine-tune the content you see on your home screen.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
@ -92,10 +95,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
<View style={styles.cardsContainer}>
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
Show Replies
|
||||
<Trans>Show Replies</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Set this setting to "No" to hide all replies from your feed.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
testID="toggleRepliesBtn"
|
||||
|
@ -112,10 +117,13 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
store.preferences.homeFeed.hideReplies && styles.dimmed,
|
||||
]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
Reply Filters
|
||||
<Trans>Reply Filters</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
Enable this setting to only see replies between people you follow.
|
||||
<Trans>
|
||||
Enable this setting to only see replies between people you
|
||||
follow.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
|
@ -129,8 +137,10 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
style={[s.mb10]}
|
||||
/>
|
||||
<Text style={[pal.text]}>
|
||||
<Trans>
|
||||
Adjust the number of likes a reply must have to be shown in your
|
||||
feed.
|
||||
</Trans>
|
||||
</Text>
|
||||
<RepliesThresholdInput
|
||||
enabled={!store.preferences.homeFeed.hideReplies}
|
||||
|
@ -139,10 +149,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
Show Reposts
|
||||
<Trans>Show Reposts</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Set this setting to "No" to hide all reposts from your feed.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
|
@ -154,11 +166,13 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
Show Quote Posts
|
||||
<Trans>Show Quote Posts</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Set this setting to "No" to hide all quote posts from your feed.
|
||||
Reposts will still be visible.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
|
@ -170,12 +184,14 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Show
|
||||
Posts from My Feeds
|
||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} />
|
||||
<Trans>Show Posts from My Feeds</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Set this setting to "Yes" to show samples of your saved feeds in
|
||||
your following feed. This is an experimental feature.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
|
@ -204,9 +220,11 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
}}
|
||||
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm"
|
||||
accessibilityLabel={_(msg`Confirm`)}
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</CenteredView>
|
||||
|
|
|
@ -12,6 +12,8 @@ import {RadioGroup} from 'view/com/util/forms/RadioGroup'
|
|||
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
|
||||
export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||
|
@ -19,6 +21,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
|||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
|
||||
return (
|
||||
|
@ -37,7 +40,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
|||
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
|
||||
]}>
|
||||
<Text type="xl" style={[pal.textLight, styles.description]}>
|
||||
Fine-tune the discussion threads.
|
||||
<Trans>Fine-tune the discussion threads.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
@ -45,10 +48,10 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
|||
<View style={styles.cardsContainer}>
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
Sort Replies
|
||||
<Trans>Sort Replies</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
Sort replies to the same post by:
|
||||
<Trans>Sort replies to the same post by:</Trans>
|
||||
</Text>
|
||||
<View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
|
||||
<RadioGroup
|
||||
|
@ -67,10 +70,12 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
|||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
Prioritize Your Follows
|
||||
<Trans>Prioritize Your Follows</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Show replies by people you follow before all other replies.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
|
@ -84,12 +89,14 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
|||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Threaded
|
||||
Mode
|
||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
|
||||
<Trans>Threaded Mode</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
Set this setting to "Yes" to show replies in a threaded view. This
|
||||
is an experimental feature.
|
||||
<Trans>
|
||||
Set this setting to "Yes" to show replies in a threaded view.
|
||||
This is an experimental feature.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
|
@ -118,9 +125,11 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
|||
}}
|
||||
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm"
|
||||
accessibilityLabel={_(msg`Confirm`)}
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
||||
<Text style={[s.white, s.bold, s.f18]}>
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</CenteredView>
|
||||
|
|
|
@ -30,6 +30,8 @@ import {FeedSourceModel} from 'state/models/content/feed-source'
|
|||
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||
import {combinedDisplayName} from 'lib/strings/display-names'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
||||
|
@ -38,6 +40,7 @@ export const ProfileScreen = withAuthRequired(
|
|||
const store = useStores()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen, track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
|
||||
const name = route.params.name === 'me' ? store.me.did : route.params.name
|
||||
|
||||
|
@ -206,7 +209,11 @@ export const ProfileScreen = withAuthRequired(
|
|||
// if section is posts or posts & replies
|
||||
} else {
|
||||
if (item === ProfileUiModel.END_ITEM) {
|
||||
return <Text style={styles.endItem}>- end of feed -</Text>
|
||||
return (
|
||||
<Text style={styles.endItem}>
|
||||
<Trans>- end of feed -</Trans>
|
||||
</Text>
|
||||
)
|
||||
} else if (item === ProfileUiModel.LOADING_ITEM) {
|
||||
return <PostFeedLoadingPlaceholder />
|
||||
} else if (item._reactKey === '__error__') {
|
||||
|
@ -296,7 +303,7 @@ export const ProfileScreen = withAuthRequired(
|
|||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="New post"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</ScreenHider>
|
||||
|
|
|
@ -47,6 +47,8 @@ import {sanitizeHandle} from 'lib/strings/handles'
|
|||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
const SECTION_TITLES = ['Posts', 'About']
|
||||
|
@ -60,6 +62,7 @@ export const ProfileFeedScreen = withAuthRequired(
|
|||
observer(function ProfileFeedScreenImpl(props: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const {name: handleOrDid} = props.route.params
|
||||
|
@ -98,7 +101,7 @@ export const ProfileFeedScreen = withAuthRequired(
|
|||
<CenteredView>
|
||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
||||
Could not load feed
|
||||
<Trans>Could not load feed</Trans>
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, s.mb20]}>
|
||||
{error}
|
||||
|
@ -107,12 +110,12 @@ export const ProfileFeedScreen = withAuthRequired(
|
|||
<View style={{flexDirection: 'row'}}>
|
||||
<Button
|
||||
type="default"
|
||||
accessibilityLabel="Go Back"
|
||||
accessibilityLabel={_(msg`Go Back`)}
|
||||
accessibilityHint="Return to previous page"
|
||||
onPress={onPressBack}
|
||||
style={{flexShrink: 1}}>
|
||||
<Text type="button" style={pal.text}>
|
||||
Go Back
|
||||
<Trans>Go Back</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
@ -142,6 +145,7 @@ export const ProfileFeedScreenInner = observer(
|
|||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const feedSectionRef = React.useRef<SectionRef>(null)
|
||||
const {rkey, name: handleOrDid} = route.params
|
||||
const uri = useMemo(
|
||||
|
@ -313,7 +317,7 @@ export const ProfileFeedScreenInner = observer(
|
|||
<NativeDropdown
|
||||
testID="headerDropdownBtn"
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="More options"
|
||||
accessibilityLabel={_(msg`More options`)}
|
||||
accessibilityHint="">
|
||||
<View style={[pal.viewLight, styles.btn]}>
|
||||
<FontAwesomeIcon
|
||||
|
@ -334,6 +338,7 @@ export const ProfileFeedScreenInner = observer(
|
|||
onTogglePinned,
|
||||
onToggleSaved,
|
||||
dropdownItems,
|
||||
_,
|
||||
])
|
||||
|
||||
return (
|
||||
|
@ -374,7 +379,7 @@ export const ProfileFeedScreenInner = observer(
|
|||
/>
|
||||
}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="New post"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</View>
|
||||
|
@ -448,6 +453,7 @@ const AboutSection = observer(function AboutPageImpl({
|
|||
onScroll: (e: NativeScrollEvent) => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const scrollHandler = useAnimatedScrollHandler({onScroll})
|
||||
|
||||
if (!feedInfo) {
|
||||
|
@ -478,14 +484,14 @@ const AboutSection = observer(function AboutPageImpl({
|
|||
/>
|
||||
) : (
|
||||
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
|
||||
No description
|
||||
<Trans>No description</Trans>
|
||||
</Text>
|
||||
)}
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
|
||||
<Button
|
||||
type="default"
|
||||
testID="toggleLikeBtn"
|
||||
accessibilityLabel="Like this feed"
|
||||
accessibilityLabel={_(msg`Like this feed`)}
|
||||
accessibilityHint=""
|
||||
onPress={onToggleLiked}
|
||||
style={{paddingHorizontal: 10}}>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue