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 comments
This commit is contained in:
parent
82059b7ee1
commit
4c7850f8c4
108 changed files with 10334 additions and 1365 deletions
|
@ -45,6 +45,7 @@ module.exports = function (api) {
|
|||
},
|
||||
},
|
||||
],
|
||||
'macros',
|
||||
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
|
||||
],
|
||||
env: {
|
||||
|
|
113
docs/internationalization.md
Normal file
113
docs/internationalization.md
Normal file
|
@ -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>;
|
||||
}
|
||||
```
|
11
lingui.config.js
Normal file
11
lingui.config.js
Normal file
|
@ -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}>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</I18nProvider>
|
||||
<ToastContainer />
|
||||
</RootStoreProvider>
|
||||
</analytics.Provider>
|
||||
|
|
20
src/locale/i18n.ts
Normal file
20
src/locale/i18n.ts
Normal file
|
@ -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)
|
||||
}
|
1
src/locale/locales/cs/messages.js
Normal file
1
src/locale/locales/cs/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/cs/messages.po
Normal file
1544
src/locale/locales/cs/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/en/messages.js
Normal file
1
src/locale/locales/en/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/en/messages.po
Normal file
1544
src/locale/locales/en/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/es/messages.js
Normal file
1
src/locale/locales/es/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/es/messages.po
Normal file
1544
src/locale/locales/es/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/fr/messages.js
Normal file
1
src/locale/locales/fr/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/fr/messages.po
Normal file
1544
src/locale/locales/fr/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/hi/messages.js
Normal file
1
src/locale/locales/hi/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/hi/messages.po
Normal file
1544
src/locale/locales/hi/messages.po
Normal file
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>
|
||||
|
|
119
src/view/com/auth/login/ChooseAccountForm.tsx
Normal file
119
src/view/com/auth/login/ChooseAccountForm.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
197
src/view/com/auth/login/ForgotPasswordForm.tsx
Normal file
197
src/view/com/auth/login/ForgotPasswordForm.tsx
Normal file
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
288
src/view/com/auth/login/LoginForm.tsx
Normal file
288
src/view/com/auth/login/LoginForm.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
48
src/view/com/auth/login/PasswordUpdatedForm.tsx
Normal file
48
src/view/com/auth/login/PasswordUpdatedForm.tsx
Normal file
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
181
src/view/com/auth/login/SetNewPasswordForm.tsx
Normal file
181
src/view/com/auth/login/SetNewPasswordForm.tsx
Normal file
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
118
src/view/com/auth/login/styles.ts
Normal file
118
src/view/com/auth/login/styles.ts
Normal file
|
@ -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]}>
|
||||
Your posts, likes, and blocks are public. Mutes are private.
|
||||
<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]}>
|
||||
Choose the algorithms that power your experience with custom
|
||||
feeds.
|
||||
<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]}>
|
||||
Alt text describes images for blind and low-vision users, and helps
|
||||
give context to everyone.
|
||||
<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>{' '}
|
||||
Use this to sign into the other app along with your handle.
|
||||
<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]}>
|
||||
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>
|
||||
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]}>
|
||||
Can only contain letters, numbers, spaces, dashes, and underscores.
|
||||
Must be at least 4 characters long, but no more than 32 characters
|
||||
long.
|
||||
<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]}>
|
||||
{list ? 'Edit' : 'New'} {purposeLabel} List
|
||||
<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]}>
|
||||
For security reasons, we'll need to send a confirmation code to
|
||||
your email address.
|
||||
<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]}>
|
||||
Each code works once. You'll receive more invite codes periodically.
|
||||
<Trans>
|
||||
Each code works once. You'll receive more invite codes periodically.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ScrollView style={[styles.scrollContainer, pal.border]}>
|
||||
{store.me.invites.map((invite, i) => (
|
||||
|
@ -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]}>
|
||||
Your email has been saved! We'll be in touch soon.
|
||||
<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]}>
|
||||
Which languages would you like to see in your algorithmic feeds?
|
||||
<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]}>
|
||||
Set this setting to "No" to hide all replies from your feed.
|
||||
<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]}>
|
||||
Adjust the number of likes a reply must have to be shown in your
|
||||
feed.
|
||||
<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]}>
|
||||
Set this setting to "No" to hide all reposts from your feed.
|
||||
<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]}>
|
||||
Set this setting to "No" to hide all quote posts from your feed.
|
||||
Reposts will still be visible.
|
||||
<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]}>
|
||||
Set this setting to "Yes" to show samples of your saved feeds in
|
||||
your following feed. This is an experimental feature.
|
||||
<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]}>
|
||||
Show replies by people you follow before all other replies.
|
||||
<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…
Add table
Add a link
Reference in a new issue