Internationalization & localization (#1822)
* install and setup lingui * setup dynamic locale activation and async loading * first pass of automated replacement of text messages * add some more documentaton * fix nits * add `es` and `hi`locales for testing purposes * make accessibilityLabel localized * compile and extract new messages * fix merge conflicts * fix eslint warning * change instructions from sending email to opening PR * fix commentszio/stable
parent
82059b7ee1
commit
4c7850f8c4
|
@ -45,6 +45,7 @@ module.exports = function (api) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'macros',
|
||||||
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
|
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
# Internationalization
|
||||||
|
|
||||||
|
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please open a PR or issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app)
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
We are using Lingui to manage translations. You can find the documentation [here](https://lingui.dev/).
|
||||||
|
|
||||||
|
### Adding new strings
|
||||||
|
When adding a new string, do it as follows:
|
||||||
|
```jsx
|
||||||
|
// Before
|
||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
|
<Text>Hello World</Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// After
|
||||||
|
import { Text } from "react-native";
|
||||||
|
import { Trans } from "@lingui/macro";
|
||||||
|
|
||||||
|
<Text><Trans>Hello World</Trans></Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<Trans>` macro will extract the string and add it to the catalog. It is not really a component, but a macro. Further reading [here](https://lingui.dev/ref/macro.html)
|
||||||
|
|
||||||
|
However sometimes you will run into this case:
|
||||||
|
```jsx
|
||||||
|
// Before
|
||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
|
const text = "Hello World";
|
||||||
|
<Text accessibilityLabel="Label is here">{text}</Text>
|
||||||
|
```
|
||||||
|
In this case, you cannot use the `useLingui()` hook:
|
||||||
|
```jsx
|
||||||
|
import { msg } from "@lingui/macro";
|
||||||
|
import { useLingui } from "@lingui/react";
|
||||||
|
|
||||||
|
const { _ } = useLingui();
|
||||||
|
return <Text accessibilityLabel={_(msg`Label is here`)}>{text}</Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to do this outside of a React component, you can use the `t` macro instead (note: this won't react to changes if the locale is switched dynamically within the app):
|
||||||
|
```jsx
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
const text = t`Hello World`;
|
||||||
|
```
|
||||||
|
|
||||||
|
We can then run `yarn intl:extract` to update the catalog in `src/locale/locales/{locale}/messages.po`. This will add the new string to the catalog.
|
||||||
|
We can then run `yarn intl:compile` to update the translation files in `src/locale/locales/{locale}/messages.js`. This will add the new string to the translation files.
|
||||||
|
The configuration for translations is defined in `lingui.config.js`
|
||||||
|
|
||||||
|
So the workflow is as follows:
|
||||||
|
1. Wrap messages in Trans macro
|
||||||
|
2. Run `yarn intl:extract` command to generate message catalogs
|
||||||
|
3. Translate message catalogs (send them to translators usually)
|
||||||
|
4. Run `yarn intl:compile` to create runtime catalogs
|
||||||
|
5. Load runtime catalog
|
||||||
|
6. Enjoy translated app!
|
||||||
|
|
||||||
|
### Common pitfalls
|
||||||
|
These pitfalls are memoization pitfalls that will cause the components to not re-render when the locale is changed -- causing stale translations to be shown.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { msg } from "@lingui/macro";
|
||||||
|
import { i18n } from "@lingui/core";
|
||||||
|
|
||||||
|
const welcomeMessage = msg`Welcome!`;
|
||||||
|
|
||||||
|
// ❌ Bad! This code won't work
|
||||||
|
export function Welcome() {
|
||||||
|
const buggyWelcome = useMemo(() => {
|
||||||
|
return i18n._(welcomeMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div>{buggyWelcome}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Bad! This code won't work either because the reference to i18n does not change
|
||||||
|
export function Welcome() {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
|
const buggyWelcome = useMemo(() => {
|
||||||
|
return i18n._(welcomeMessage);
|
||||||
|
}, [i18n]);
|
||||||
|
|
||||||
|
return <div>{buggyWelcome}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Good! `useMemo` has i18n context in the dependency
|
||||||
|
export function Welcome() {
|
||||||
|
const linguiCtx = useLingui();
|
||||||
|
|
||||||
|
const welcome = useMemo(() => {
|
||||||
|
return linguiCtx.i18n._(welcomeMessage);
|
||||||
|
}, [linguiCtx]);
|
||||||
|
|
||||||
|
return <div>{welcome}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context
|
||||||
|
export function Welcome() {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const welcome = useMemo(() => {
|
||||||
|
return _(welcomeMessage);
|
||||||
|
}, [_]);
|
||||||
|
|
||||||
|
return <div>{welcome}</div>;
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,11 @@
|
||||||
|
/** @type {import('@lingui/conf').LinguiConfig} */
|
||||||
|
module.exports = {
|
||||||
|
locales: ['en', 'cs', 'fr', 'hi', 'es'],
|
||||||
|
catalogs: [
|
||||||
|
{
|
||||||
|
path: '<rootDir>/src/locale/locales/{locale}/messages',
|
||||||
|
include: ['src'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
format: 'po',
|
||||||
|
}
|
10
package.json
10
package.json
|
@ -28,7 +28,9 @@
|
||||||
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
|
"perf:test: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:test:results": "NODE_ENV=test flashlight report .perf/results.json",
|
||||||
"perf:measure": "NODE_ENV=test flashlight measure",
|
"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": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.6.23",
|
"@atproto/api": "^0.6.23",
|
||||||
|
@ -42,6 +44,7 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
||||||
"@gorhom/bottom-sheet": "^4.5.1",
|
"@gorhom/bottom-sheet": "^4.5.1",
|
||||||
|
"@lingui/react": "^4.5.0",
|
||||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||||
"@miblanchard/react-native-slider": "^2.3.1",
|
"@miblanchard/react-native-slider": "^2.3.1",
|
||||||
"@react-native-async-storage/async-storage": "1.18.2",
|
"@react-native-async-storage/async-storage": "1.18.2",
|
||||||
|
@ -164,10 +167,12 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@atproto/dev-env": "^0.2.5",
|
"@atproto/dev-env": "^0.2.5",
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.23.2",
|
||||||
"@babel/preset-env": "^7.20.0",
|
"@babel/preset-env": "^7.20.0",
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@did-plc/server": "^0.0.1",
|
"@did-plc/server": "^0.0.1",
|
||||||
|
"@lingui/cli": "^4.5.0",
|
||||||
|
"@lingui/macro": "^4.5.0",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||||
"@react-native-community/eslint-config": "^3.0.0",
|
"@react-native-community/eslint-config": "^3.0.0",
|
||||||
"@testing-library/jest-native": "^5.4.1",
|
"@testing-library/jest-native": "^5.4.1",
|
||||||
|
@ -192,6 +197,7 @@
|
||||||
"@typescript-eslint/parser": "^5.48.2",
|
"@typescript-eslint/parser": "^5.48.2",
|
||||||
"babel-jest": "^29.4.2",
|
"babel-jest": "^29.4.2",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.0",
|
"babel-plugin-module-resolver": "^5.0.0",
|
||||||
"babel-plugin-react-native-web": "^0.18.12",
|
"babel-plugin-react-native-web": "^0.18.12",
|
||||||
"detox": "^20.13.0",
|
"detox": "^20.13.0",
|
||||||
|
|
|
@ -16,6 +16,9 @@ import {Shell} from 'view/shell/index'
|
||||||
import {ToastContainer} from 'view/com/util/Toast.web'
|
import {ToastContainer} from 'view/com/util/Toast.web'
|
||||||
import {ThemeProvider} from 'lib/ThemeContext'
|
import {ThemeProvider} from 'lib/ThemeContext'
|
||||||
import {queryClient} from 'lib/react-query'
|
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 ShellStateProvider} from 'state/shell'
|
||||||
import {Provider as ModalStateProvider} from 'state/modals'
|
import {Provider as ModalStateProvider} from 'state/modals'
|
||||||
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
||||||
|
@ -34,6 +37,7 @@ const InnerApp = observer(function AppImpl() {
|
||||||
setRootStore(store)
|
setRootStore(store)
|
||||||
analytics.init(store)
|
analytics.init(store)
|
||||||
})
|
})
|
||||||
|
dynamicActivate(defaultLocale) // async import of locale data
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// show nothing prior to init
|
// show nothing prior to init
|
||||||
|
@ -47,9 +51,11 @@ const InnerApp = observer(function AppImpl() {
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
<analytics.Provider>
|
<analytics.Provider>
|
||||||
<RootStoreProvider value={rootStore}>
|
<RootStoreProvider value={rootStore}>
|
||||||
<SafeAreaProvider>
|
<I18nProvider i18n={i18n}>
|
||||||
<Shell />
|
<SafeAreaProvider>
|
||||||
</SafeAreaProvider>
|
<Shell />
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</I18nProvider>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</RootStoreProvider>
|
</RootStoreProvider>
|
||||||
</analytics.Provider>
|
</analytics.Provider>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import {i18n} from '@lingui/core'
|
||||||
|
|
||||||
|
export const locales = {
|
||||||
|
en: 'English',
|
||||||
|
cs: 'Česky',
|
||||||
|
fr: 'Français',
|
||||||
|
hi: 'हिंदी',
|
||||||
|
es: 'Español',
|
||||||
|
}
|
||||||
|
export const defaultLocale = 'en'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We do a dynamic import of just the catalog that we need
|
||||||
|
* @param locale any locale string
|
||||||
|
*/
|
||||||
|
export async function dynamicActivate(locale: string) {
|
||||||
|
const {messages} = await import(`./locales/${locale}/messages`)
|
||||||
|
i18n.load(locale, messages)
|
||||||
|
i18n.activate(locale)
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,8 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
import {s, colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export const SplashScreen = ({
|
export const SplashScreen = ({
|
||||||
onPressSignin,
|
onPressSignin,
|
||||||
|
@ -14,14 +16,18 @@ export const SplashScreen = ({
|
||||||
onPressCreateAccount: () => void
|
onPressCreateAccount: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView style={[styles.container, pal.view]}>
|
<CenteredView style={[styles.container, pal.view]}>
|
||||||
<SafeAreaView testID="noSessionView" style={styles.container}>
|
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<View style={styles.hero}>
|
<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]}>
|
<Text style={[styles.subtitle, pal.textLight]}>
|
||||||
See what's next
|
<Trans>See what's next</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||||
|
@ -30,10 +36,10 @@ export const SplashScreen = ({
|
||||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||||
onPress={onPressCreateAccount}
|
onPress={onPressCreateAccount}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Create new account"
|
accessibilityLabel={_(msg`Create new account`)}
|
||||||
accessibilityHint="Opens flow to create a new Bluesky account">
|
accessibilityHint="Opens flow to create a new Bluesky account">
|
||||||
<Text style={[s.white, styles.btnLabel]}>
|
<Text style={[s.white, styles.btnLabel]}>
|
||||||
Create a new account
|
<Trans>Create a new account</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -41,9 +47,11 @@ export const SplashScreen = ({
|
||||||
style={[styles.btn, pal.btn]}
|
style={[styles.btn, pal.btn]}
|
||||||
onPress={onPressSignin}
|
onPress={onPressSignin}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Sign in"
|
accessibilityLabel={_(msg`Sign in`)}
|
||||||
accessibilityHint="Opens flow to sign into your existing Bluesky account">
|
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>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
export const SplashScreen = ({
|
export const SplashScreen = ({
|
||||||
onPressSignin,
|
onPressSignin,
|
||||||
|
@ -54,7 +55,9 @@ export const SplashScreen = ({
|
||||||
onPress={onPressSignin}
|
onPress={onPressSignin}
|
||||||
// TODO: web accessibility
|
// TODO: web accessibility
|
||||||
accessibilityRole="button">
|
accessibilityRole="button">
|
||||||
<Text style={[pal.text, styles.btnLabel]}>Sign In</Text>
|
<Text style={[pal.text, styles.btnLabel]}>
|
||||||
|
<Trans>Sign In</Trans>
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {s} from 'lib/styles'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {CreateAccountModel} from 'state/models/ui/create-account'
|
import {CreateAccountModel} from 'state/models/ui/create-account'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
|
||||||
import {Step1} from './Step1'
|
import {Step1} from './Step1'
|
||||||
|
@ -30,6 +32,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const model = React.useMemo(() => new CreateAccountModel(store), [store])
|
const model = React.useMemo(() => new CreateAccountModel(store), [store])
|
||||||
|
const {_} = useLingui()
|
||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -73,8 +76,8 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
||||||
return (
|
return (
|
||||||
<LoggedOutLayout
|
<LoggedOutLayout
|
||||||
leadin={`Step ${model.step}`}
|
leadin={`Step ${model.step}`}
|
||||||
title="Create Account"
|
title={_(msg`Create Account`)}
|
||||||
description="We're so excited to have you join us!">
|
description={_(msg`We're so excited to have you join us!`)}>
|
||||||
<ScrollView testID="createAccount" style={pal.view}>
|
<ScrollView testID="createAccount" style={pal.view}>
|
||||||
<KeyboardAvoidingView behavior="padding">
|
<KeyboardAvoidingView behavior="padding">
|
||||||
<View style={styles.stepContainer}>
|
<View style={styles.stepContainer}>
|
||||||
|
@ -88,7 +91,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
||||||
testID="backBtn"
|
testID="backBtn"
|
||||||
accessibilityRole="button">
|
accessibilityRole="button">
|
||||||
<Text type="xl" style={pal.link}>
|
<Text type="xl" style={pal.link}>
|
||||||
Back
|
<Trans>Back</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
|
@ -101,7 +104,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : (
|
) : (
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Next
|
<Trans>Next</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -110,18 +113,18 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
||||||
testID="retryConnectBtn"
|
testID="retryConnectBtn"
|
||||||
onPress={onPressRetryConnect}
|
onPress={onPressRetryConnect}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Retry"
|
accessibilityLabel={_(msg`Retry`)}
|
||||||
accessibilityHint="Retries account creation"
|
accessibilityHint="Retries account creation"
|
||||||
accessibilityLiveRegion="polite">
|
accessibilityLiveRegion="polite">
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Retry
|
<Trans>Retry</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : model.isFetchingServiceDescription ? (
|
) : model.isFetchingServiceDescription ? (
|
||||||
<>
|
<>
|
||||||
<ActivityIndicator color="#fff" />
|
<ActivityIndicator color="#fff" />
|
||||||
<Text type="xl" style={[pal.text, s.pr5]}>
|
<Text type="xl" style={[pal.text, s.pr5]}>
|
||||||
Connecting...
|
<Trans>Connecting...</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {HelpTip} from '../util/HelpTip'
|
||||||
import {TextInput} from '../util/TextInput'
|
import {TextInput} from '../util/TextInput'
|
||||||
import {Button} from 'view/com/util/forms/Button'
|
import {Button} from 'view/com/util/forms/Button'
|
||||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
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 {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
|
||||||
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
|
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
|
||||||
|
@ -27,6 +29,7 @@ export const Step1 = observer(function Step1Impl({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
|
const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
const onPressDefault = React.useCallback(() => {
|
const onPressDefault = React.useCallback(() => {
|
||||||
setIsDefaultSelected(true)
|
setIsDefaultSelected(true)
|
||||||
|
@ -63,9 +66,9 @@ export const Step1 = observer(function Step1Impl({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<StepHeader step="1" title="Your hosting provider" />
|
<StepHeader step="1" title={_(msg`Your hosting provider`)} />
|
||||||
<Text style={[pal.text, s.mb10]}>
|
<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>
|
</Text>
|
||||||
<Option
|
<Option
|
||||||
testID="blueskyServerBtn"
|
testID="blueskyServerBtn"
|
||||||
|
@ -81,17 +84,17 @@ export const Step1 = observer(function Step1Impl({
|
||||||
onPress={onPressOther}>
|
onPress={onPressOther}>
|
||||||
<View style={styles.otherForm}>
|
<View style={styles.otherForm}>
|
||||||
<Text nativeID="addressProvider" style={[pal.text, s.mb5]}>
|
<Text nativeID="addressProvider" style={[pal.text, s.mb5]}>
|
||||||
Enter the address of your provider:
|
<Trans>Enter the address of your provider:</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="customServerInput"
|
testID="customServerInput"
|
||||||
icon="globe"
|
icon="globe"
|
||||||
placeholder="Hosting provider address"
|
placeholder={_(msg`Hosting provider address`)}
|
||||||
value={model.serviceUrl}
|
value={model.serviceUrl}
|
||||||
editable
|
editable
|
||||||
onChange={onChangeServiceUrl}
|
onChange={onChangeServiceUrl}
|
||||||
accessibilityHint="Input hosting provider address"
|
accessibilityHint="Input hosting provider address"
|
||||||
accessibilityLabel="Hosting provider address"
|
accessibilityLabel={_(msg`Hosting provider address`)}
|
||||||
accessibilityLabelledBy="addressProvider"
|
accessibilityLabelledBy="addressProvider"
|
||||||
/>
|
/>
|
||||||
{LOGIN_INCLUDE_DEV_SERVERS && (
|
{LOGIN_INCLUDE_DEV_SERVERS && (
|
||||||
|
@ -100,13 +103,13 @@ export const Step1 = observer(function Step1Impl({
|
||||||
testID="stagingServerBtn"
|
testID="stagingServerBtn"
|
||||||
type="default"
|
type="default"
|
||||||
style={s.mr5}
|
style={s.mr5}
|
||||||
label="Staging"
|
label={_(msg`Staging`)}
|
||||||
onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
|
onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
testID="localDevServerBtn"
|
testID="localDevServerBtn"
|
||||||
type="default"
|
type="default"
|
||||||
label="Dev Server"
|
label={_(msg`Dev Server`)}
|
||||||
onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
|
onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -116,7 +119,7 @@ export const Step1 = observer(function Step1Impl({
|
||||||
{model.error ? (
|
{model.error ? (
|
||||||
<ErrorMessage message={model.error} style={styles.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>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {TextInput} from '../util/TextInput'
|
||||||
import {Policies} from './Policies'
|
import {Policies} from './Policies'
|
||||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
/** STEP 2: Your account
|
/** STEP 2: Your account
|
||||||
|
@ -28,6 +30,7 @@ export const Step2 = observer(function Step2Impl({
|
||||||
model: CreateAccountModel
|
model: CreateAccountModel
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
const onPressWaitlist = React.useCallback(() => {
|
const onPressWaitlist = React.useCallback(() => {
|
||||||
|
@ -36,7 +39,7 @@ export const Step2 = observer(function Step2Impl({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<StepHeader step="2" title="Your account" />
|
<StepHeader step="2" title={_(msg`Your account`)} />
|
||||||
|
|
||||||
{model.isInviteCodeRequired && (
|
{model.isInviteCodeRequired && (
|
||||||
<View style={s.pb20}>
|
<View style={s.pb20}>
|
||||||
|
@ -46,11 +49,11 @@ export const Step2 = observer(function Step2Impl({
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="inviteCodeInput"
|
testID="inviteCodeInput"
|
||||||
icon="ticket"
|
icon="ticket"
|
||||||
placeholder="Required for this provider"
|
placeholder={_(msg`Required for this provider`)}
|
||||||
value={model.inviteCode}
|
value={model.inviteCode}
|
||||||
editable
|
editable
|
||||||
onChange={model.setInviteCode}
|
onChange={model.setInviteCode}
|
||||||
accessibilityLabel="Invite code"
|
accessibilityLabel={_(msg`Invite code`)}
|
||||||
accessibilityHint="Input invite code to proceed"
|
accessibilityHint="Input invite code to proceed"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -61,10 +64,12 @@ export const Step2 = observer(function Step2Impl({
|
||||||
Don't have an invite code?{' '}
|
Don't have an invite code?{' '}
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={onPressWaitlist}
|
onPress={onPressWaitlist}
|
||||||
accessibilityLabel="Join the waitlist."
|
accessibilityLabel={_(msg`Join the waitlist.`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View style={styles.touchable}>
|
<View style={styles.touchable}>
|
||||||
<Text style={pal.link}>Join the waitlist.</Text>
|
<Text style={pal.link}>
|
||||||
|
<Trans>Join the waitlist.</Trans>
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -72,16 +77,16 @@ export const Step2 = observer(function Step2Impl({
|
||||||
<>
|
<>
|
||||||
<View style={s.pb20}>
|
<View style={s.pb20}>
|
||||||
<Text type="md-medium" style={[pal.text, s.mb2]} nativeID="email">
|
<Text type="md-medium" style={[pal.text, s.mb2]} nativeID="email">
|
||||||
Email address
|
<Trans>Email address</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="emailInput"
|
testID="emailInput"
|
||||||
icon="envelope"
|
icon="envelope"
|
||||||
placeholder="Enter your email address"
|
placeholder={_(msg`Enter your email address`)}
|
||||||
value={model.email}
|
value={model.email}
|
||||||
editable
|
editable
|
||||||
onChange={model.setEmail}
|
onChange={model.setEmail}
|
||||||
accessibilityLabel="Email"
|
accessibilityLabel={_(msg`Email`)}
|
||||||
accessibilityHint="Input email for Bluesky waitlist"
|
accessibilityHint="Input email for Bluesky waitlist"
|
||||||
accessibilityLabelledBy="email"
|
accessibilityLabelledBy="email"
|
||||||
/>
|
/>
|
||||||
|
@ -92,17 +97,17 @@ export const Step2 = observer(function Step2Impl({
|
||||||
type="md-medium"
|
type="md-medium"
|
||||||
style={[pal.text, s.mb2]}
|
style={[pal.text, s.mb2]}
|
||||||
nativeID="password">
|
nativeID="password">
|
||||||
Password
|
<Trans>Password</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="passwordInput"
|
testID="passwordInput"
|
||||||
icon="lock"
|
icon="lock"
|
||||||
placeholder="Choose your password"
|
placeholder={_(msg`Choose your password`)}
|
||||||
value={model.password}
|
value={model.password}
|
||||||
editable
|
editable
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
onChange={model.setPassword}
|
onChange={model.setPassword}
|
||||||
accessibilityLabel="Password"
|
accessibilityLabel={_(msg`Password`)}
|
||||||
accessibilityHint="Set password"
|
accessibilityHint="Set password"
|
||||||
accessibilityLabelledBy="password"
|
accessibilityLabelledBy="password"
|
||||||
/>
|
/>
|
||||||
|
@ -113,7 +118,7 @@ export const Step2 = observer(function Step2Impl({
|
||||||
type="md-medium"
|
type="md-medium"
|
||||||
style={[pal.text, s.mb2]}
|
style={[pal.text, s.mb2]}
|
||||||
nativeID="birthDate">
|
nativeID="birthDate">
|
||||||
Your birth date
|
<Trans>Your birth date</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<DateInput
|
<DateInput
|
||||||
testID="birthdayInput"
|
testID="birthdayInput"
|
||||||
|
@ -122,7 +127,7 @@ export const Step2 = observer(function Step2Impl({
|
||||||
buttonType="default-light"
|
buttonType="default-light"
|
||||||
buttonStyle={[pal.border, styles.dateInputButton]}
|
buttonStyle={[pal.border, styles.dateInputButton]}
|
||||||
buttonLabelType="lg"
|
buttonLabelType="lg"
|
||||||
accessibilityLabel="Birthday"
|
accessibilityLabel={_(msg`Birthday`)}
|
||||||
accessibilityHint="Enter your birth date"
|
accessibilityHint="Enter your birth date"
|
||||||
accessibilityLabelledBy="birthDate"
|
accessibilityLabelledBy="birthDate"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {TextInput} from '../util/TextInput'
|
||||||
import {createFullHandle} from 'lib/strings/handles'
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
/** STEP 3: Your user handle
|
/** STEP 3: Your user handle
|
||||||
* @field User handle
|
* @field User handle
|
||||||
|
@ -19,9 +21,10 @@ export const Step3 = observer(function Step3Impl({
|
||||||
model: CreateAccountModel
|
model: CreateAccountModel
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<StepHeader step="3" title="Your user handle" />
|
<StepHeader step="3" title={_(msg`Your user handle`)} />
|
||||||
<View style={s.pb10}>
|
<View style={s.pb10}>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="handleInput"
|
testID="handleInput"
|
||||||
|
@ -31,12 +34,12 @@ export const Step3 = observer(function Step3Impl({
|
||||||
editable
|
editable
|
||||||
onChange={model.setHandle}
|
onChange={model.setHandle}
|
||||||
// TODO: Add explicit text label
|
// TODO: Add explicit text label
|
||||||
accessibilityLabel="User handle"
|
accessibilityLabel={_(msg`User handle`)}
|
||||||
accessibilityHint="Input your user handle"
|
accessibilityHint="Input your user handle"
|
||||||
/>
|
/>
|
||||||
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
||||||
Your full handle will be{' '}
|
<Trans>Your full handle will be</Trans>
|
||||||
<Text type="lg-bold" style={pal.text}>
|
<Text type="lg-bold" style={[pal.text, s.ml5]}>
|
||||||
@{createFullHandle(model.handle, model.userDomain)}
|
@{createFullHandle(model.handle, model.userDomain)}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {UserAvatar} from '../../util/UserAvatar'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {AccountData} from 'state/models/session'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {styles} from './styles'
|
||||||
|
|
||||||
|
export const ChooseAccountForm = ({
|
||||||
|
store,
|
||||||
|
onSelectAccount,
|
||||||
|
onPressBack,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
onSelectAccount: (account?: AccountData) => void
|
||||||
|
onPressBack: () => void
|
||||||
|
}) => {
|
||||||
|
const {track, screen} = useAnalytics()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
screen('Choose Account')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
const onTryAccount = async (account: AccountData) => {
|
||||||
|
if (account.accessJwt && account.refreshJwt) {
|
||||||
|
setIsProcessing(true)
|
||||||
|
if (await store.session.resumeSession(account)) {
|
||||||
|
track('Sign In', {resumedSession: true})
|
||||||
|
setIsProcessing(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsProcessing(false)
|
||||||
|
}
|
||||||
|
onSelectAccount(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
|
||||||
|
<Text
|
||||||
|
type="2xl-medium"
|
||||||
|
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
|
||||||
|
<Trans>Sign in as...</Trans>
|
||||||
|
</Text>
|
||||||
|
{store.session.accounts.map(account => (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID={`chooseAccountBtn-${account.handle}`}
|
||||||
|
key={account.did}
|
||||||
|
style={[pal.view, pal.border, styles.account]}
|
||||||
|
onPress={() => onTryAccount(account)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
|
||||||
|
accessibilityHint="Double tap to sign in">
|
||||||
|
<View
|
||||||
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<View style={s.p10}>
|
||||||
|
<UserAvatar avatar={account.aviUrl} size={30} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.accountText}>
|
||||||
|
<Text type="lg-bold" style={pal.text}>
|
||||||
|
{account.displayName || account.handle}{' '}
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.textLight]}>
|
||||||
|
{account.handle}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="angle-right"
|
||||||
|
size={16}
|
||||||
|
style={[pal.text, s.mr10]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="chooseNewAccountBtn"
|
||||||
|
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
||||||
|
onPress={() => onSelectAccount(undefined)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Login to account that is not listed`)}
|
||||||
|
accessibilityHint="">
|
||||||
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<Text style={[styles.accountText, styles.accountTextOther]}>
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
<Trans>Other account</Trans>
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="angle-right"
|
||||||
|
size={16}
|
||||||
|
style={[pal.text, s.mr10]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{isProcessing && <ActivityIndicator />}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
import React, {useState, useEffect} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import * as EmailValidator from 'email-validator'
|
||||||
|
import {BskyAgent} from '@atproto/api'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {ServiceDescription} from 'state/models/session'
|
||||||
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
|
export const ForgotPasswordForm = ({
|
||||||
|
error,
|
||||||
|
serviceUrl,
|
||||||
|
serviceDescription,
|
||||||
|
setError,
|
||||||
|
setServiceUrl,
|
||||||
|
onPressBack,
|
||||||
|
onEmailSent,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
error: string
|
||||||
|
serviceUrl: string
|
||||||
|
serviceDescription: ServiceDescription | undefined
|
||||||
|
setError: (v: string) => void
|
||||||
|
setServiceUrl: (v: string) => void
|
||||||
|
onPressBack: () => void
|
||||||
|
onEmailSent: () => void
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const theme = useTheme()
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
|
const [email, setEmail] = useState<string>('')
|
||||||
|
const {screen} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screen('Signin:ForgotPassword')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
const onPressSelectService = () => {
|
||||||
|
openModal({
|
||||||
|
name: 'server-input',
|
||||||
|
initialService: serviceUrl,
|
||||||
|
onSelect: setServiceUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
if (!EmailValidator.validate(email)) {
|
||||||
|
return setError('Your email appears to be invalid.')
|
||||||
|
}
|
||||||
|
|
||||||
|
setError('')
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
await agent.com.atproto.server.requestPasswordReset({email})
|
||||||
|
onEmailSent()
|
||||||
|
} catch (e: any) {
|
||||||
|
const errMsg = e.toString()
|
||||||
|
logger.warn('Failed to request password reset', {error: e})
|
||||||
|
setIsProcessing(false)
|
||||||
|
if (isNetworkError(e)) {
|
||||||
|
setError(
|
||||||
|
'Unable to contact your service. Please check your Internet connection.',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError(cleanError(errMsg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
|
<Trans>Reset password</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text type="md" style={[pal.text, styles.instructions]}>
|
||||||
|
<Trans>
|
||||||
|
Enter the email you used to create your account. We'll send you a
|
||||||
|
"reset code" so you can set a new password.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
testID="forgotPasswordView"
|
||||||
|
style={[pal.borderDark, pal.view, styles.group]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="forgotPasswordSelectServiceButton"
|
||||||
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
||||||
|
onPress={onPressSelectService}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Hosting provider`)}
|
||||||
|
accessibilityHint="Sets hosting provider for password reset">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="globe"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
|
||||||
|
{toNiceDomain(serviceUrl)}
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="pen"
|
||||||
|
size={12}
|
||||||
|
style={pal.text as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="envelope"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="forgotPasswordEmail"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Email address"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoFocus
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel={_(msg`Email`)}
|
||||||
|
accessibilityHint="Sets email for password reset"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{error ? (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||||
|
</View>
|
||||||
|
<View style={s.flex1}>
|
||||||
|
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{!serviceDescription || isProcessing ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : !email ? (
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="newPasswordButton"
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Go to next`)}
|
||||||
|
accessibilityHint="Navigates to the next screen">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
{!serviceDescription || isProcessing ? (
|
||||||
|
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||||
|
<Trans>Processing...</Trans>
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,37 +1,19 @@
|
||||||
import React, {useState, useEffect, useRef} from 'react'
|
import React, {useState, useEffect} from 'react'
|
||||||
import {
|
import {KeyboardAvoidingView} from 'react-native'
|
||||||
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 {useAnalytics} from 'lib/analytics/analytics'
|
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 {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
|
||||||
import {s, colors} from 'lib/styles'
|
import {useStores, DEFAULT_SERVICE} from 'state/index'
|
||||||
import {createFullHandle} from 'lib/strings/handles'
|
|
||||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
|
||||||
import {useStores, RootStoreModel, DEFAULT_SERVICE} from 'state/index'
|
|
||||||
import {ServiceDescription} from 'state/models/session'
|
import {ServiceDescription} from 'state/models/session'
|
||||||
import {AccountData} from 'state/models/session'
|
import {AccountData} from 'state/models/session'
|
||||||
import {isNetworkError} from 'lib/strings/errors'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
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 {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 {
|
enum Forms {
|
||||||
Login,
|
Login,
|
||||||
|
@ -45,6 +27,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({})
|
const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({})
|
||||||
const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
|
const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
|
||||||
|
@ -87,14 +70,16 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
error: err,
|
error: err,
|
||||||
})
|
})
|
||||||
setError(
|
setError(
|
||||||
'Unable to contact your service. Please check your Internet connection.',
|
_(
|
||||||
|
msg`Unable to contact your service. Please check your Internet connection.`,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return () => {
|
return () => {
|
||||||
aborted = true
|
aborted = true
|
||||||
}
|
}
|
||||||
}, [store.session, serviceUrl, retryDescribeTrigger])
|
}, [store.session, serviceUrl, retryDescribeTrigger, _])
|
||||||
|
|
||||||
const onPressRetryConnect = () => setRetryDescribeTrigger({})
|
const onPressRetryConnect = () => setRetryDescribeTrigger({})
|
||||||
const onPressForgotPassword = () => {
|
const onPressForgotPassword = () => {
|
||||||
|
@ -107,8 +92,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
{currentForm === Forms.Login ? (
|
{currentForm === Forms.Login ? (
|
||||||
<LoggedOutLayout
|
<LoggedOutLayout
|
||||||
leadin=""
|
leadin=""
|
||||||
title="Sign in"
|
title={_(msg`Sign in`)}
|
||||||
description="Enter your username and password">
|
description={_(msg`Enter your username and password`)}>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
store={store}
|
store={store}
|
||||||
error={error}
|
error={error}
|
||||||
|
@ -126,8 +111,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
{currentForm === Forms.ChooseAccount ? (
|
{currentForm === Forms.ChooseAccount ? (
|
||||||
<LoggedOutLayout
|
<LoggedOutLayout
|
||||||
leadin=""
|
leadin=""
|
||||||
title="Sign in as..."
|
title={_(msg`Sign in as...`)}
|
||||||
description="Select from an existing account">
|
description={_(msg`Select from an existing account`)}>
|
||||||
<ChooseAccountForm
|
<ChooseAccountForm
|
||||||
store={store}
|
store={store}
|
||||||
onSelectAccount={onSelectAccount}
|
onSelectAccount={onSelectAccount}
|
||||||
|
@ -138,8 +123,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
{currentForm === Forms.ForgotPassword ? (
|
{currentForm === Forms.ForgotPassword ? (
|
||||||
<LoggedOutLayout
|
<LoggedOutLayout
|
||||||
leadin=""
|
leadin=""
|
||||||
title="Forgot Password"
|
title={_(msg`Forgot Password`)}
|
||||||
description="Let's get your password reset!">
|
description={_(msg`Let's get your password reset!`)}>
|
||||||
<ForgotPasswordForm
|
<ForgotPasswordForm
|
||||||
store={store}
|
store={store}
|
||||||
error={error}
|
error={error}
|
||||||
|
@ -155,8 +140,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
{currentForm === Forms.SetNewPassword ? (
|
{currentForm === Forms.SetNewPassword ? (
|
||||||
<LoggedOutLayout
|
<LoggedOutLayout
|
||||||
leadin=""
|
leadin=""
|
||||||
title="Forgot Password"
|
title={_(msg`Forgot Password`)}
|
||||||
description="Let's get your password reset!">
|
description={_(msg`Let's get your password reset!`)}>
|
||||||
<SetNewPasswordForm
|
<SetNewPasswordForm
|
||||||
store={store}
|
store={store}
|
||||||
error={error}
|
error={error}
|
||||||
|
@ -173,830 +158,3 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChooseAccountForm = ({
|
|
||||||
store,
|
|
||||||
onSelectAccount,
|
|
||||||
onPressBack,
|
|
||||||
}: {
|
|
||||||
store: RootStoreModel
|
|
||||||
onSelectAccount: (account?: AccountData) => void
|
|
||||||
onPressBack: () => void
|
|
||||||
}) => {
|
|
||||||
const {track, screen} = useAnalytics()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const [isProcessing, setIsProcessing] = React.useState(false)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
screen('Choose Account')
|
|
||||||
}, [screen])
|
|
||||||
|
|
||||||
const onTryAccount = async (account: AccountData) => {
|
|
||||||
if (account.accessJwt && account.refreshJwt) {
|
|
||||||
setIsProcessing(true)
|
|
||||||
if (await store.session.resumeSession(account)) {
|
|
||||||
track('Sign In', {resumedSession: true})
|
|
||||||
setIsProcessing(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
onSelectAccount(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
|
|
||||||
<Text
|
|
||||||
type="2xl-medium"
|
|
||||||
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
|
|
||||||
Sign in as...
|
|
||||||
</Text>
|
|
||||||
{store.session.accounts.map(account => (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID={`chooseAccountBtn-${account.handle}`}
|
|
||||||
key={account.did}
|
|
||||||
style={[pal.view, pal.border, styles.account]}
|
|
||||||
onPress={() => onTryAccount(account)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={`Sign in as ${account.handle}`}
|
|
||||||
accessibilityHint="Double tap to sign in">
|
|
||||||
<View
|
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
|
||||||
<View style={s.p10}>
|
|
||||||
<UserAvatar avatar={account.aviUrl} size={30} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.accountText}>
|
|
||||||
<Text type="lg-bold" style={pal.text}>
|
|
||||||
{account.displayName || account.handle}{' '}
|
|
||||||
</Text>
|
|
||||||
<Text type="lg" style={[pal.textLight]}>
|
|
||||||
{account.handle}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="angle-right"
|
|
||||||
size={16}
|
|
||||||
style={[pal.text, s.mr10]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="chooseNewAccountBtn"
|
|
||||||
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
|
||||||
onPress={() => onSelectAccount(undefined)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Login to account that is not listed"
|
|
||||||
accessibilityHint="">
|
|
||||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
|
||||||
<Text style={[styles.accountText, styles.accountTextOther]}>
|
|
||||||
<Text type="lg" style={pal.text}>
|
|
||||||
Other account
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="angle-right"
|
|
||||||
size={16}
|
|
||||||
style={[pal.text, s.mr10]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
|
||||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
|
||||||
Back
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{isProcessing && <ActivityIndicator />}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoginForm = ({
|
|
||||||
store,
|
|
||||||
error,
|
|
||||||
serviceUrl,
|
|
||||||
serviceDescription,
|
|
||||||
initialHandle,
|
|
||||||
setError,
|
|
||||||
setServiceUrl,
|
|
||||||
onPressRetryConnect,
|
|
||||||
onPressBack,
|
|
||||||
onPressForgotPassword,
|
|
||||||
}: {
|
|
||||||
store: RootStoreModel
|
|
||||||
error: string
|
|
||||||
serviceUrl: string
|
|
||||||
serviceDescription: ServiceDescription | undefined
|
|
||||||
initialHandle: string
|
|
||||||
setError: (v: string) => void
|
|
||||||
setServiceUrl: (v: string) => void
|
|
||||||
onPressRetryConnect: () => void
|
|
||||||
onPressBack: () => void
|
|
||||||
onPressForgotPassword: () => void
|
|
||||||
}) => {
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
|
||||||
const [identifier, setIdentifier] = useState<string>(initialHandle)
|
|
||||||
const [password, setPassword] = useState<string>('')
|
|
||||||
const passwordInputRef = useRef<TextInput>(null)
|
|
||||||
const {openModal} = useModalControls()
|
|
||||||
|
|
||||||
const onPressSelectService = () => {
|
|
||||||
openModal({
|
|
||||||
name: 'server-input',
|
|
||||||
initialService: serviceUrl,
|
|
||||||
onSelect: setServiceUrl,
|
|
||||||
})
|
|
||||||
Keyboard.dismiss()
|
|
||||||
track('Signin:PressedSelectService')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPressNext = async () => {
|
|
||||||
Keyboard.dismiss()
|
|
||||||
setError('')
|
|
||||||
setIsProcessing(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// try to guess the handle if the user just gave their own username
|
|
||||||
let fullIdent = identifier
|
|
||||||
if (
|
|
||||||
!identifier.includes('@') && // not an email
|
|
||||||
!identifier.includes('.') && // not a domain
|
|
||||||
serviceDescription &&
|
|
||||||
serviceDescription.availableUserDomains.length > 0
|
|
||||||
) {
|
|
||||||
let matched = false
|
|
||||||
for (const domain of serviceDescription.availableUserDomains) {
|
|
||||||
if (fullIdent.endsWith(domain)) {
|
|
||||||
matched = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!matched) {
|
|
||||||
fullIdent = createFullHandle(
|
|
||||||
identifier,
|
|
||||||
serviceDescription.availableUserDomains[0],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await store.session.login({
|
|
||||||
service: serviceUrl,
|
|
||||||
identifier: fullIdent,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
const errMsg = e.toString()
|
|
||||||
logger.warn('Failed to login', {error: e})
|
|
||||||
setIsProcessing(false)
|
|
||||||
if (errMsg.includes('Authentication Required')) {
|
|
||||||
setError('Invalid username or password')
|
|
||||||
} else if (isNetworkError(e)) {
|
|
||||||
setError(
|
|
||||||
'Unable to contact your service. Please check your Internet connection.',
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setError(cleanError(errMsg))
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
track('Sign In', {resumedSession: false})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isReady = !!serviceDescription && !!identifier && !!password
|
|
||||||
return (
|
|
||||||
<View testID="loginForm">
|
|
||||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
|
||||||
Sign into
|
|
||||||
</Text>
|
|
||||||
<View style={[pal.borderDark, styles.group]}>
|
|
||||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="globe"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="loginSelectServiceButton"
|
|
||||||
style={styles.textBtn}
|
|
||||||
onPress={onPressSelectService}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Select service"
|
|
||||||
accessibilityHint="Sets server for the Bluesky client">
|
|
||||||
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
|
||||||
{toNiceDomain(serviceUrl)}
|
|
||||||
</Text>
|
|
||||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="pen"
|
|
||||||
size={12}
|
|
||||||
style={pal.textLight as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
|
||||||
Account
|
|
||||||
</Text>
|
|
||||||
<View style={[pal.borderDark, styles.group]}>
|
|
||||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="at"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="loginUsernameInput"
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder="Username or email address"
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoFocus
|
|
||||||
autoCorrect={false}
|
|
||||||
autoComplete="username"
|
|
||||||
returnKeyType="next"
|
|
||||||
onSubmitEditing={() => {
|
|
||||||
passwordInputRef.current?.focus()
|
|
||||||
}}
|
|
||||||
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
value={identifier}
|
|
||||||
onChangeText={str =>
|
|
||||||
setIdentifier((str || '').toLowerCase().trim())
|
|
||||||
}
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessibilityLabel="Username or email address"
|
|
||||||
accessibilityHint="Input the username or email address you used at signup"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={[pal.borderDark, styles.groupContent]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="lock"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="loginPasswordInput"
|
|
||||||
ref={passwordInputRef}
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder="Password"
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
autoComplete="password"
|
|
||||||
returnKeyType="done"
|
|
||||||
enablesReturnKeyAutomatically={true}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
secureTextEntry={true}
|
|
||||||
textContentType="password"
|
|
||||||
clearButtonMode="while-editing"
|
|
||||||
value={password}
|
|
||||||
onChangeText={setPassword}
|
|
||||||
onSubmitEditing={onPressNext}
|
|
||||||
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessibilityLabel="Password"
|
|
||||||
accessibilityHint={
|
|
||||||
identifier === ''
|
|
||||||
? 'Input your password'
|
|
||||||
: `Input the password tied to ${identifier}`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="forgotPasswordButton"
|
|
||||||
style={styles.textInputInnerBtn}
|
|
||||||
onPress={onPressForgotPassword}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Forgot password"
|
|
||||||
accessibilityHint="Opens password reset form">
|
|
||||||
<Text style={pal.link}>Forgot</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{error ? (
|
|
||||||
<View style={styles.error}>
|
|
||||||
<View style={styles.errorIcon}>
|
|
||||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
|
||||||
</View>
|
|
||||||
<View style={s.flex1}>
|
|
||||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
|
||||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
|
||||||
Back
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{!serviceDescription && error ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="loginRetryButton"
|
|
||||||
onPress={onPressRetryConnect}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Retry"
|
|
||||||
accessibilityHint="Retries login">
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
Retry
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : !serviceDescription ? (
|
|
||||||
<>
|
|
||||||
<ActivityIndicator />
|
|
||||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
|
||||||
Connecting...
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : isProcessing ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : isReady ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="loginNextButton"
|
|
||||||
onPress={onPressNext}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Go to next"
|
|
||||||
accessibilityHint="Navigates to the next screen">
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
Next
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ForgotPasswordForm = ({
|
|
||||||
error,
|
|
||||||
serviceUrl,
|
|
||||||
serviceDescription,
|
|
||||||
setError,
|
|
||||||
setServiceUrl,
|
|
||||||
onPressBack,
|
|
||||||
onEmailSent,
|
|
||||||
}: {
|
|
||||||
store: RootStoreModel
|
|
||||||
error: string
|
|
||||||
serviceUrl: string
|
|
||||||
serviceDescription: ServiceDescription | undefined
|
|
||||||
setError: (v: string) => void
|
|
||||||
setServiceUrl: (v: string) => void
|
|
||||||
onPressBack: () => void
|
|
||||||
onEmailSent: () => void
|
|
||||||
}) => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
|
||||||
const [email, setEmail] = useState<string>('')
|
|
||||||
const {screen} = useAnalytics()
|
|
||||||
const {openModal} = useModalControls()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
screen('Signin:ForgotPassword')
|
|
||||||
}, [screen])
|
|
||||||
|
|
||||||
const onPressSelectService = () => {
|
|
||||||
openModal({
|
|
||||||
name: 'server-input',
|
|
||||||
initialService: serviceUrl,
|
|
||||||
onSelect: setServiceUrl,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPressNext = async () => {
|
|
||||||
if (!EmailValidator.validate(email)) {
|
|
||||||
return setError('Your email appears to be invalid.')
|
|
||||||
}
|
|
||||||
|
|
||||||
setError('')
|
|
||||||
setIsProcessing(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const agent = new BskyAgent({service: serviceUrl})
|
|
||||||
await agent.com.atproto.server.requestPasswordReset({email})
|
|
||||||
onEmailSent()
|
|
||||||
} catch (e: any) {
|
|
||||||
const errMsg = e.toString()
|
|
||||||
logger.warn('Failed to request password reset', {error: e})
|
|
||||||
setIsProcessing(false)
|
|
||||||
if (isNetworkError(e)) {
|
|
||||||
setError(
|
|
||||||
'Unable to contact your service. Please check your Internet connection.',
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setError(cleanError(errMsg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View>
|
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
|
||||||
Reset password
|
|
||||||
</Text>
|
|
||||||
<Text type="md" style={[pal.text, styles.instructions]}>
|
|
||||||
Enter the email you used to create your account. We'll send you a
|
|
||||||
"reset code" so you can set a new password.
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
testID="forgotPasswordView"
|
|
||||||
style={[pal.borderDark, pal.view, styles.group]}>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="forgotPasswordSelectServiceButton"
|
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
|
||||||
onPress={onPressSelectService}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Hosting provider"
|
|
||||||
accessibilityHint="Sets hosting provider for password reset">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="globe"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
|
|
||||||
{toNiceDomain(serviceUrl)}
|
|
||||||
</Text>
|
|
||||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="pen"
|
|
||||||
size={12}
|
|
||||||
style={pal.text as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={[pal.borderDark, styles.groupContent]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="envelope"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="forgotPasswordEmail"
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder="Email address"
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoFocus
|
|
||||||
autoCorrect={false}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
value={email}
|
|
||||||
onChangeText={setEmail}
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessibilityLabel="Email"
|
|
||||||
accessibilityHint="Sets email for password reset"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{error ? (
|
|
||||||
<View style={styles.error}>
|
|
||||||
<View style={styles.errorIcon}>
|
|
||||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
|
||||||
</View>
|
|
||||||
<View style={s.flex1}>
|
|
||||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
|
||||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
|
||||||
Back
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{!serviceDescription || isProcessing ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : !email ? (
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
|
||||||
Next
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="newPasswordButton"
|
|
||||||
onPress={onPressNext}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Go to next"
|
|
||||||
accessibilityHint="Navigates to the next screen">
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
Next
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{!serviceDescription || isProcessing ? (
|
|
||||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
|
||||||
Processing...
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SetNewPasswordForm = ({
|
|
||||||
error,
|
|
||||||
serviceUrl,
|
|
||||||
setError,
|
|
||||||
onPressBack,
|
|
||||||
onPasswordSet,
|
|
||||||
}: {
|
|
||||||
store: RootStoreModel
|
|
||||||
error: string
|
|
||||||
serviceUrl: string
|
|
||||||
setError: (v: string) => void
|
|
||||||
onPressBack: () => void
|
|
||||||
onPasswordSet: () => void
|
|
||||||
}) => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const {screen} = useAnalytics()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
screen('Signin:SetNewPasswordForm')
|
|
||||||
}, [screen])
|
|
||||||
|
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
|
||||||
const [resetCode, setResetCode] = useState<string>('')
|
|
||||||
const [password, setPassword] = useState<string>('')
|
|
||||||
|
|
||||||
const onPressNext = async () => {
|
|
||||||
setError('')
|
|
||||||
setIsProcessing(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const agent = new BskyAgent({service: serviceUrl})
|
|
||||||
const token = resetCode.replace(/\s/g, '')
|
|
||||||
await agent.com.atproto.server.resetPassword({
|
|
||||||
token,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
onPasswordSet()
|
|
||||||
} catch (e: any) {
|
|
||||||
const errMsg = e.toString()
|
|
||||||
logger.warn('Failed to set new password', {error: e})
|
|
||||||
setIsProcessing(false)
|
|
||||||
if (isNetworkError(e)) {
|
|
||||||
setError(
|
|
||||||
'Unable to contact your service. Please check your Internet connection.',
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setError(cleanError(errMsg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View>
|
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
|
||||||
Set new password
|
|
||||||
</Text>
|
|
||||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
|
||||||
You will receive an email with a "reset code." Enter that code here,
|
|
||||||
then enter your new password.
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
testID="newPasswordView"
|
|
||||||
style={[pal.view, pal.borderDark, styles.group]}>
|
|
||||||
<View
|
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="ticket"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="resetCodeInput"
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder="Reset code"
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
autoFocus
|
|
||||||
value={resetCode}
|
|
||||||
onChangeText={setResetCode}
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel="Reset code"
|
|
||||||
accessibilityHint="Input code sent to your email for password reset"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={[pal.borderDark, styles.groupContent]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="lock"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="newPasswordInput"
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder="New password"
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
secureTextEntry
|
|
||||||
value={password}
|
|
||||||
onChangeText={setPassword}
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel="Password"
|
|
||||||
accessibilityHint="Input new password"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{error ? (
|
|
||||||
<View style={styles.error}>
|
|
||||||
<View style={styles.errorIcon}>
|
|
||||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
|
||||||
</View>
|
|
||||||
<View style={s.flex1}>
|
|
||||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
|
||||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
|
||||||
Back
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{isProcessing ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : !resetCode || !password ? (
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
|
||||||
Next
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="setNewPasswordButton"
|
|
||||||
onPress={onPressNext}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Go to next"
|
|
||||||
accessibilityHint="Navigates to the next screen">
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
Next
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{isProcessing ? (
|
|
||||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
|
||||||
Updating...
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
|
|
||||||
const {screen} = useAnalytics()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
screen('Signin:PasswordUpdatedForm')
|
|
||||||
}, [screen])
|
|
||||||
|
|
||||||
const pal = usePalette('default')
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View>
|
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
|
||||||
Password updated!
|
|
||||||
</Text>
|
|
||||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
|
||||||
You can now sign in with your new password.
|
|
||||||
</Text>
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressNext}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Close alert"
|
|
||||||
accessibilityHint="Closes password update alert">
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
Okay
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
screenTitle: {
|
|
||||||
marginBottom: 10,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
instructions: {
|
|
||||||
marginBottom: 20,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
group: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 10,
|
|
||||||
marginBottom: 20,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
groupLabel: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingBottom: 5,
|
|
||||||
},
|
|
||||||
groupContent: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
noTopBorder: {
|
|
||||||
borderTopWidth: 0,
|
|
||||||
},
|
|
||||||
groupContentIcon: {
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
account: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingVertical: 4,
|
|
||||||
},
|
|
||||||
accountLast: {
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
marginBottom: 20,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
flex: 1,
|
|
||||||
width: '100%',
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
fontSize: 17,
|
|
||||||
letterSpacing: 0.25,
|
|
||||||
fontWeight: '400',
|
|
||||||
borderRadius: 10,
|
|
||||||
},
|
|
||||||
textInputInnerBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
marginHorizontal: 6,
|
|
||||||
},
|
|
||||||
textBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
textBtnLabel: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
},
|
|
||||||
textBtnFakeInnerBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
marginHorizontal: 6,
|
|
||||||
},
|
|
||||||
accountText: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
paddingVertical: 10,
|
|
||||||
},
|
|
||||||
accountTextOther: {
|
|
||||||
paddingLeft: 12,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
backgroundColor: colors.red4,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: -5,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
marginBottom: 15,
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
errorIcon: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.white,
|
|
||||||
color: colors.white,
|
|
||||||
borderRadius: 30,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: 5,
|
|
||||||
},
|
|
||||||
dimmed: {opacity: 0.5},
|
|
||||||
|
|
||||||
maxHeight: {
|
|
||||||
// @ts-ignore web only -prf
|
|
||||||
maxHeight: isWeb ? '100vh' : undefined,
|
|
||||||
height: !isWeb ? '100%' : undefined,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
import React, {useState, useRef} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Keyboard,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
|
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {ServiceDescription} from 'state/models/session'
|
||||||
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
|
export const LoginForm = ({
|
||||||
|
store,
|
||||||
|
error,
|
||||||
|
serviceUrl,
|
||||||
|
serviceDescription,
|
||||||
|
initialHandle,
|
||||||
|
setError,
|
||||||
|
setServiceUrl,
|
||||||
|
onPressRetryConnect,
|
||||||
|
onPressBack,
|
||||||
|
onPressForgotPassword,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
error: string
|
||||||
|
serviceUrl: string
|
||||||
|
serviceDescription: ServiceDescription | undefined
|
||||||
|
initialHandle: string
|
||||||
|
setError: (v: string) => void
|
||||||
|
setServiceUrl: (v: string) => void
|
||||||
|
onPressRetryConnect: () => void
|
||||||
|
onPressBack: () => void
|
||||||
|
onPressForgotPassword: () => void
|
||||||
|
}) => {
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const theme = useTheme()
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
|
const [identifier, setIdentifier] = useState<string>(initialHandle)
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
const passwordInputRef = useRef<TextInput>(null)
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
const onPressSelectService = () => {
|
||||||
|
openModal({
|
||||||
|
name: 'server-input',
|
||||||
|
initialService: serviceUrl,
|
||||||
|
onSelect: setServiceUrl,
|
||||||
|
})
|
||||||
|
Keyboard.dismiss()
|
||||||
|
track('Signin:PressedSelectService')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
setError('')
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// try to guess the handle if the user just gave their own username
|
||||||
|
let fullIdent = identifier
|
||||||
|
if (
|
||||||
|
!identifier.includes('@') && // not an email
|
||||||
|
!identifier.includes('.') && // not a domain
|
||||||
|
serviceDescription &&
|
||||||
|
serviceDescription.availableUserDomains.length > 0
|
||||||
|
) {
|
||||||
|
let matched = false
|
||||||
|
for (const domain of serviceDescription.availableUserDomains) {
|
||||||
|
if (fullIdent.endsWith(domain)) {
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
fullIdent = createFullHandle(
|
||||||
|
identifier,
|
||||||
|
serviceDescription.availableUserDomains[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.session.login({
|
||||||
|
service: serviceUrl,
|
||||||
|
identifier: fullIdent,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
const errMsg = e.toString()
|
||||||
|
logger.warn('Failed to login', {error: e})
|
||||||
|
setIsProcessing(false)
|
||||||
|
if (errMsg.includes('Authentication Required')) {
|
||||||
|
setError(_(msg`Invalid username or password`))
|
||||||
|
} else if (isNetworkError(e)) {
|
||||||
|
setError(
|
||||||
|
_(
|
||||||
|
msg`Unable to contact your service. Please check your Internet connection.`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError(cleanError(errMsg))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
track('Sign In', {resumedSession: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isReady = !!serviceDescription && !!identifier && !!password
|
||||||
|
return (
|
||||||
|
<View testID="loginForm">
|
||||||
|
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||||
|
<Trans>Sign into</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.borderDark, styles.group]}>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="globe"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="loginSelectServiceButton"
|
||||||
|
style={styles.textBtn}
|
||||||
|
onPress={onPressSelectService}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Select service`)}
|
||||||
|
accessibilityHint="Sets server for the Bluesky client">
|
||||||
|
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||||
|
{toNiceDomain(serviceUrl)}
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="pen"
|
||||||
|
size={12}
|
||||||
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||||
|
<Trans>Account</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.borderDark, styles.group]}>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="at"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="loginUsernameInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder={_(msg`Username or email address`)}
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoFocus
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="username"
|
||||||
|
returnKeyType="next"
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
passwordInputRef.current?.focus()
|
||||||
|
}}
|
||||||
|
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
value={identifier}
|
||||||
|
onChangeText={str =>
|
||||||
|
setIdentifier((str || '').toLowerCase().trim())
|
||||||
|
}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel={_(msg`Username or email address`)}
|
||||||
|
accessibilityHint="Input the username or email address you used at signup"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="lock"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="loginPasswordInput"
|
||||||
|
ref={passwordInputRef}
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Password"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="password"
|
||||||
|
returnKeyType="done"
|
||||||
|
enablesReturnKeyAutomatically={true}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
secureTextEntry={true}
|
||||||
|
textContentType="password"
|
||||||
|
clearButtonMode="while-editing"
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
onSubmitEditing={onPressNext}
|
||||||
|
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel={_(msg`Password`)}
|
||||||
|
accessibilityHint={
|
||||||
|
identifier === ''
|
||||||
|
? 'Input your password'
|
||||||
|
: `Input the password tied to ${identifier}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="forgotPasswordButton"
|
||||||
|
style={styles.textInputInnerBtn}
|
||||||
|
onPress={onPressForgotPassword}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Forgot password`)}
|
||||||
|
accessibilityHint="Opens password reset form">
|
||||||
|
<Text style={pal.link}>
|
||||||
|
<Trans>Forgot</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{error ? (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||||
|
</View>
|
||||||
|
<View style={s.flex1}>
|
||||||
|
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{!serviceDescription && error ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="loginRetryButton"
|
||||||
|
onPress={onPressRetryConnect}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Retry`)}
|
||||||
|
accessibilityHint="Retries login">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Retry</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : !serviceDescription ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator />
|
||||||
|
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||||
|
<Trans>Connecting...</Trans>
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : isProcessing ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : isReady ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="loginNextButton"
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Go to next`)}
|
||||||
|
accessibilityHint="Navigates to the next screen">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React, {useEffect} from 'react'
|
||||||
|
import {TouchableOpacity, View} from 'react-native'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
export const PasswordUpdatedForm = ({
|
||||||
|
onPressNext,
|
||||||
|
}: {
|
||||||
|
onPressNext: () => void
|
||||||
|
}) => {
|
||||||
|
const {screen} = useAnalytics()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screen('Signin:PasswordUpdatedForm')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
|
<Trans>Password updated!</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||||
|
<Trans>You can now sign in with your new password.</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Close alert`)}
|
||||||
|
accessibilityHint="Closes password update alert">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Okay</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
import React, {useState, useEffect} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {BskyAgent} from '@atproto/api'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
export const SetNewPasswordForm = ({
|
||||||
|
error,
|
||||||
|
serviceUrl,
|
||||||
|
setError,
|
||||||
|
onPressBack,
|
||||||
|
onPasswordSet,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
error: string
|
||||||
|
serviceUrl: string
|
||||||
|
setError: (v: string) => void
|
||||||
|
onPressBack: () => void
|
||||||
|
onPasswordSet: () => void
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const theme = useTheme()
|
||||||
|
const {screen} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screen('Signin:SetNewPasswordForm')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
|
const [resetCode, setResetCode] = useState<string>('')
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
setError('')
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
const token = resetCode.replace(/\s/g, '')
|
||||||
|
await agent.com.atproto.server.resetPassword({
|
||||||
|
token,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
onPasswordSet()
|
||||||
|
} catch (e: any) {
|
||||||
|
const errMsg = e.toString()
|
||||||
|
logger.warn('Failed to set new password', {error: e})
|
||||||
|
setIsProcessing(false)
|
||||||
|
if (isNetworkError(e)) {
|
||||||
|
setError(
|
||||||
|
'Unable to contact your service. Please check your Internet connection.',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError(cleanError(errMsg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
|
<Trans>Set new password</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||||
|
<Trans>
|
||||||
|
You will receive an email with a "reset code." Enter that code here,
|
||||||
|
then enter your new password.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
testID="newPasswordView"
|
||||||
|
style={[pal.view, pal.borderDark, styles.group]}>
|
||||||
|
<View
|
||||||
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="ticket"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="resetCodeInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Reset code"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
autoFocus
|
||||||
|
value={resetCode}
|
||||||
|
onChangeText={setResetCode}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={_(msg`Reset code`)}
|
||||||
|
accessibilityHint="Input code sent to your email for password reset"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="lock"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="newPasswordInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="New password"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
secureTextEntry
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={_(msg`Password`)}
|
||||||
|
accessibilityHint="Input new password"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{error ? (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||||
|
</View>
|
||||||
|
<View style={s.flex1}>
|
||||||
|
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{isProcessing ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : !resetCode || !password ? (
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="setNewPasswordButton"
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Go to next`)}
|
||||||
|
accessibilityHint="Navigates to the next screen">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
{isProcessing ? (
|
||||||
|
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||||
|
<Trans>Updating...</Trans>
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import {StyleSheet} from 'react-native'
|
||||||
|
import {colors} from 'lib/styles'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
|
||||||
|
export const styles = StyleSheet.create({
|
||||||
|
screenTitle: {
|
||||||
|
marginBottom: 10,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
instructions: {
|
||||||
|
marginBottom: 20,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
marginBottom: 20,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
groupLabel: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 5,
|
||||||
|
},
|
||||||
|
groupContent: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
noTopBorder: {
|
||||||
|
borderTopWidth: 0,
|
||||||
|
},
|
||||||
|
groupContentIcon: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 4,
|
||||||
|
},
|
||||||
|
accountLast: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
marginBottom: 20,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
fontSize: 17,
|
||||||
|
letterSpacing: 0.25,
|
||||||
|
fontWeight: '400',
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
textInputInnerBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
marginHorizontal: 6,
|
||||||
|
},
|
||||||
|
textBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
textBtnLabel: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
textBtnFakeInnerBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
marginHorizontal: 6,
|
||||||
|
},
|
||||||
|
accountText: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
accountTextOther: {
|
||||||
|
paddingLeft: 12,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: colors.red4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: -5,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
marginBottom: 15,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
errorIcon: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.white,
|
||||||
|
color: colors.white,
|
||||||
|
borderRadius: 30,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
dimmed: {opacity: 0.5},
|
||||||
|
|
||||||
|
maxHeight: {
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
maxHeight: isWeb ? '100vh' : undefined,
|
||||||
|
height: !isWeb ? '100%' : undefined,
|
||||||
|
},
|
||||||
|
})
|
|
@ -14,6 +14,7 @@ import {Text} from 'view/com/util/text/Text'
|
||||||
import Animated, {FadeInRight} from 'react-native-reanimated'
|
import Animated, {FadeInRight} from 'react-native-reanimated'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: SuggestedActor
|
item: SuggestedActor
|
||||||
|
@ -115,7 +116,9 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
||||||
{addingMoreSuggestions ? (
|
{addingMoreSuggestions ? (
|
||||||
<View style={styles.addingMoreContainer}>
|
<View style={styles.addingMoreContainer}>
|
||||||
<ActivityIndicator size="small" color={pal.colors.text} />
|
<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>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Button} from 'view/com/util/forms/Button'
|
import {Button} from 'view/com/util/forms/Button'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
@ -32,7 +33,9 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
style={[s.flexRow, s.alignCenter]}
|
style={[s.flexRow, s.alignCenter]}
|
||||||
onPress={skip}>
|
onPress={skip}>
|
||||||
<Text style={[pal.link]}>Skip</Text>
|
<Text style={[pal.link]}>
|
||||||
|
<Trans>Skip</Trans>
|
||||||
|
</Text>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={'chevron-right'}
|
icon={'chevron-right'}
|
||||||
size={14}
|
size={14}
|
||||||
|
@ -45,17 +48,21 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
||||||
<View>
|
<View>
|
||||||
<Text style={[pal.text, styles.title]}>
|
<Text style={[pal.text, styles.title]}>
|
||||||
Welcome to{' '}
|
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>
|
</Text>
|
||||||
<View style={styles.spacer} />
|
<View style={styles.spacer} />
|
||||||
<View style={[styles.row]}>
|
<View style={[styles.row]}>
|
||||||
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
||||||
<View style={[styles.rowText]}>
|
<View style={[styles.rowText]}>
|
||||||
<Text type="lg-bold" style={[pal.text]}>
|
<Text type="lg-bold" style={[pal.text]}>
|
||||||
Bluesky is public.
|
<Trans>Bluesky is public.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -63,10 +70,10 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
||||||
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
||||||
<View style={[styles.rowText]}>
|
<View style={[styles.rowText]}>
|
||||||
<Text type="lg-bold" style={[pal.text]}>
|
<Text type="lg-bold" style={[pal.text]}>
|
||||||
Bluesky is open.
|
<Trans>Bluesky is open.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -74,11 +81,13 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
||||||
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
||||||
<View style={[styles.rowText]}>
|
<View style={[styles.rowText]}>
|
||||||
<Text type="lg-bold" style={[pal.text]}>
|
<Text type="lg-bold" style={[pal.text]}>
|
||||||
Bluesky is flexible.
|
<Trans>Bluesky is flexible.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||||
Choose the algorithms that power your experience with custom
|
<Trans>
|
||||||
feeds.
|
Choose the algorithms that power your experience with custom
|
||||||
|
feeds.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -49,6 +49,8 @@ import {LabelsBtn} from './labels/LabelsBtn'
|
||||||
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
||||||
import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
|
import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
|
||||||
import {insertMentionAt} from 'lib/strings/mention-manip'
|
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 {useModals, useModalControls} from '#/state/modals'
|
||||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||||
import {
|
import {
|
||||||
|
@ -70,6 +72,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {isDesktop, isMobile} = useWebMediaQueries()
|
const {isDesktop, isMobile} = useWebMediaQueries()
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const requireAltTextEnabled = useRequireAltTextEnabled()
|
const requireAltTextEnabled = useRequireAltTextEnabled()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
|
@ -273,9 +276,11 @@ export const ComposePost = observer(function ComposePost({
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
onAccessibilityEscape={onPressCancel}
|
onAccessibilityEscape={onPressCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel={_(msg`Cancel`)}
|
||||||
accessibilityHint="Closes post composer and discards post draft">
|
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>
|
</TouchableOpacity>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
|
@ -316,7 +321,9 @@ export const ComposePost = observer(function ComposePost({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<View style={[styles.postBtn, pal.btn]}>
|
<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>
|
</View>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -332,7 +339,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[pal.text, s.flex1]}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
@ -388,7 +395,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
onSuggestedLinksChanged={setSuggestedLinks}
|
onSuggestedLinksChanged={setSuggestedLinks}
|
||||||
onError={setError}
|
onError={setError}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Write post"
|
accessibilityLabel={_(msg`Write post`)}
|
||||||
accessibilityHint={`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`}
|
accessibilityHint={`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -417,11 +424,11 @@ export const ComposePost = observer(function ComposePost({
|
||||||
style={[pal.borderDark, styles.addExtLinkBtn]}
|
style={[pal.borderDark, styles.addExtLinkBtn]}
|
||||||
onPress={() => onPressAddLinkCard(url)}
|
onPress={() => onPressAddLinkCard(url)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Add link card"
|
accessibilityLabel={_(msg`Add link card`)}
|
||||||
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
|
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
|
||||||
<Text style={pal.text}>
|
<Text style={pal.text}>
|
||||||
Add link card:{' '}
|
<Trans>Add link card:</Trans>
|
||||||
<Text style={pal.link}>{toShortUrl(url)}</Text>
|
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {Text} from '../util/text/Text'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {ExternalEmbedDraft} from 'lib/api/index'
|
import {ExternalEmbedDraft} from 'lib/api/index'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
export const ExternalEmbed = ({
|
export const ExternalEmbed = ({
|
||||||
link,
|
link,
|
||||||
|
@ -21,6 +23,7 @@ export const ExternalEmbed = ({
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palError = usePalette('error')
|
const palError = usePalette('error')
|
||||||
|
const {_} = useLingui()
|
||||||
if (!link) {
|
if (!link) {
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
@ -64,7 +67,7 @@ export const ExternalEmbed = ({
|
||||||
style={styles.removeBtn}
|
style={styles.removeBtn}
|
||||||
onPress={onRemove}
|
onPress={onRemove}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Remove image preview"
|
accessibilityLabel={_(msg`Remove image preview`)}
|
||||||
accessibilityHint={`Removes default thumbnail from ${link.uri}`}
|
accessibilityHint={`Removes default thumbnail from ${link.uri}`}
|
||||||
onAccessibilityEscape={onRemove}>
|
onAccessibilityEscape={onRemove}>
|
||||||
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />
|
<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 {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isDesktop} = useWebMediaQueries()
|
const {isDesktop} = useWebMediaQueries()
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -16,7 +19,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||||
style={[pal.view, pal.border, styles.prompt]}
|
style={[pal.view, pal.border, styles.prompt]}
|
||||||
onPress={() => onPressCompose()}
|
onPress={() => onPressCompose()}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Compose reply"
|
accessibilityLabel={_(msg`Compose reply`)}
|
||||||
accessibilityHint="Opens composer">
|
accessibilityHint="Opens composer">
|
||||||
<UserAvatar avatar={store.me.avatar} size={38} />
|
<UserAvatar avatar={store.me.avatar} size={38} />
|
||||||
<Text
|
<Text
|
||||||
|
@ -25,7 +28,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||||
pal.text,
|
pal.text,
|
||||||
isDesktop ? styles.labelDesktopWeb : styles.labelMobile,
|
isDesktop ? styles.labelDesktopWeb : styles.labelMobile,
|
||||||
]}>
|
]}>
|
||||||
Write your reply
|
<Trans>Write your reply</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {ShieldExclamation} from 'lib/icons'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const LabelsBtn = observer(function LabelsBtn({
|
export const LabelsBtn = observer(function LabelsBtn({
|
||||||
|
@ -19,6 +21,7 @@ export const LabelsBtn = observer(function LabelsBtn({
|
||||||
onChange: (v: string[]) => void
|
onChange: (v: string[]) => void
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -26,7 +29,7 @@ export const LabelsBtn = observer(function LabelsBtn({
|
||||||
type="default-light"
|
type="default-light"
|
||||||
testID="labelsBtn"
|
testID="labelsBtn"
|
||||||
style={[styles.button, !hasMedia && styles.dimmed]}
|
style={[styles.button, !hasMedia && styles.dimmed]}
|
||||||
accessibilityLabel="Content warnings"
|
accessibilityLabel={_(msg`Content warnings`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (isNative) {
|
if (isNative) {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {Text} from 'view/com/util/text/Text'
|
||||||
import {Dimensions} from 'lib/media/types'
|
import {Dimensions} from 'lib/media/types'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
|
|
||||||
|
@ -48,6 +50,7 @@ const GalleryInner = observer(function GalleryImpl({
|
||||||
containerInfo,
|
containerInfo,
|
||||||
}: GalleryInnerProps) {
|
}: GalleryInnerProps) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
@ -113,7 +116,7 @@ const GalleryInner = observer(function GalleryImpl({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="altTextButton"
|
testID="altTextButton"
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Add alt text"
|
accessibilityLabel={_(msg`Add alt text`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
|
@ -124,7 +127,7 @@ const GalleryInner = observer(function GalleryImpl({
|
||||||
}}
|
}}
|
||||||
style={[styles.altTextControl, altTextControlStyle]}>
|
style={[styles.altTextControl, altTextControlStyle]}>
|
||||||
<Text style={styles.altTextControlLabel} accessible={false}>
|
<Text style={styles.altTextControlLabel} accessible={false}>
|
||||||
ALT
|
<Trans>ALT</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{image.altText.length > 0 ? (
|
{image.altText.length > 0 ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -138,7 +141,7 @@ const GalleryInner = observer(function GalleryImpl({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="editPhotoButton"
|
testID="editPhotoButton"
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Edit image"
|
accessibilityLabel={_(msg`Edit image`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (isNative) {
|
if (isNative) {
|
||||||
|
@ -161,7 +164,7 @@ const GalleryInner = observer(function GalleryImpl({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="removePhotoButton"
|
testID="removePhotoButton"
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Remove image"
|
accessibilityLabel={_(msg`Remove image`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={() => gallery.remove(image)}
|
onPress={() => gallery.remove(image)}
|
||||||
style={styles.imageControl}>
|
style={styles.imageControl}>
|
||||||
|
@ -174,7 +177,7 @@ const GalleryInner = observer(function GalleryImpl({
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Add alt text"
|
accessibilityLabel={_(msg`Add alt text`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
|
@ -203,8 +206,10 @@ const GalleryInner = observer(function GalleryImpl({
|
||||||
<FontAwesomeIcon icon="info" size={12} color={pal.colors.text} />
|
<FontAwesomeIcon icon="info" size={12} color={pal.colors.text} />
|
||||||
</View>
|
</View>
|
||||||
<Text type="sm" style={[pal.textLight, s.flex1]}>
|
<Text type="sm" style={[pal.textLight, s.flex1]}>
|
||||||
Alt text describes images for blind and low-vision users, and helps
|
<Trans>
|
||||||
give context to everyone.
|
Alt text describes images for blind and low-vision users, and helps
|
||||||
|
give context to everyone.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants'
|
||||||
import {GalleryModel} from 'state/models/media/gallery'
|
import {GalleryModel} from 'state/models/media/gallery'
|
||||||
import {isMobileWeb, isNative} from 'platform/detection'
|
import {isMobileWeb, isNative} from 'platform/detection'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
gallery: GalleryModel
|
gallery: GalleryModel
|
||||||
|
@ -22,6 +24,7 @@ export function OpenCameraBtn({gallery}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
|
|
||||||
const onPressTakePicture = useCallback(async () => {
|
const onPressTakePicture = useCallback(async () => {
|
||||||
|
@ -56,7 +59,7 @@ export function OpenCameraBtn({gallery}: Props) {
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
hitSlop={HITSLOP_10}
|
hitSlop={HITSLOP_10}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Camera"
|
accessibilityLabel={_(msg`Camera`)}
|
||||||
accessibilityHint="Opens camera on device">
|
accessibilityHint="Opens camera on device">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="camera"
|
icon="camera"
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
|
||||||
import {GalleryModel} from 'state/models/media/gallery'
|
import {GalleryModel} from 'state/models/media/gallery'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
gallery: GalleryModel
|
gallery: GalleryModel
|
||||||
|
@ -18,6 +20,7 @@ type Props = {
|
||||||
export function SelectPhotoBtn({gallery}: Props) {
|
export function SelectPhotoBtn({gallery}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
|
||||||
const onPressSelectPhotos = useCallback(async () => {
|
const onPressSelectPhotos = useCallback(async () => {
|
||||||
|
@ -37,7 +40,7 @@ export function SelectPhotoBtn({gallery}: Props) {
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
hitSlop={HITSLOP_10}
|
hitSlop={HITSLOP_10}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Gallery"
|
accessibilityLabel={_(msg`Gallery`)}
|
||||||
accessibilityHint="Opens device photo gallery">
|
accessibilityHint="Opens device photo gallery">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={['far', 'image']}
|
icon={['far', 'image']}
|
||||||
|
|
|
@ -21,9 +21,12 @@ import {
|
||||||
toPostLanguages,
|
toPostLanguages,
|
||||||
hasPostLanguage,
|
hasPostLanguage,
|
||||||
} from '#/state/preferences/languages'
|
} from '#/state/preferences/languages'
|
||||||
|
import {t, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export const SelectLangBtn = observer(function SelectLangBtn() {
|
export const SelectLangBtn = observer(function SelectLangBtn() {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
|
@ -82,11 +85,11 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{heading: true, label: 'Post language'},
|
{heading: true, label: t`Post language`},
|
||||||
...arr.slice(0, 6),
|
...arr.slice(0, 6),
|
||||||
{sep: true},
|
{sep: true},
|
||||||
{
|
{
|
||||||
label: 'Other...',
|
label: t`Other...`,
|
||||||
onPress: onPressMore,
|
onPress: onPressMore,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -99,7 +102,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
|
||||||
items={items}
|
items={items}
|
||||||
openUpwards
|
openUpwards
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
accessibilityLabel="Language selection"
|
accessibilityLabel={_(msg`Language selection`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
{postLanguagesPref.length > 0 ? (
|
{postLanguagesPref.length > 0 ? (
|
||||||
<Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>
|
<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 {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
|
||||||
import useAppState from 'react-native-appstate-hook'
|
import useAppState from 'react-native-appstate-hook'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export const FeedPage = observer(function FeedPageImpl({
|
export const FeedPage = observer(function FeedPageImpl({
|
||||||
testID,
|
testID,
|
||||||
|
@ -37,6 +39,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isDesktop} = useWebMediaQueries()
|
const {isDesktop} = useWebMediaQueries()
|
||||||
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
|
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
|
||||||
const {screen, track} = useAnalytics()
|
const {screen, track} = useAnalytics()
|
||||||
|
@ -157,7 +160,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
||||||
type="title-lg"
|
type="title-lg"
|
||||||
href="/settings/home-feed"
|
href="/settings/home-feed"
|
||||||
style={{fontWeight: 'bold'}}
|
style={{fontWeight: 'bold'}}
|
||||||
accessibilityLabel="Feed Preferences"
|
accessibilityLabel={_(msg`Feed Preferences`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
text={
|
text={
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -170,7 +173,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <></>
|
return <></>
|
||||||
}, [isDesktop, pal, store, hasNew])
|
}, [isDesktop, pal.view, pal.text, pal.textLight, store, hasNew, _])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={s.h100pct}>
|
<View testID={testID} style={s.h100pct}>
|
||||||
|
@ -188,7 +191,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
||||||
{(isScrolledDown || hasNew) && (
|
{(isScrolledDown || hasNew) && (
|
||||||
<LoadLatestBtn
|
<LoadLatestBtn
|
||||||
onPress={onPressLoadLatest}
|
onPress={onPressLoadLatest}
|
||||||
label="Load new posts"
|
label={_(msg`Load new posts`)}
|
||||||
showIndicator={hasNew}
|
showIndicator={hasNew}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -197,7 +200,7 @@ export const FeedPage = observer(function FeedPageImpl({
|
||||||
onPress={onPressCompose}
|
onPress={onPressCompose}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="New post"
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {createHitslop} from 'lib/constants'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {createHitslop} from 'lib/constants'
|
||||||
import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
|
import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
|
||||||
|
import {t} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
|
@ -23,7 +23,7 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
|
||||||
onPress={onRequestClose}
|
onPress={onRequestClose}
|
||||||
hitSlop={HIT_SLOP}
|
hitSlop={HIT_SLOP}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Close image"
|
accessibilityLabel={t`Close image`}
|
||||||
accessibilityHint="Closes viewer for header image"
|
accessibilityHint="Closes viewer for header image"
|
||||||
onAccessibilityEscape={onRequestClose}>
|
onAccessibilityEscape={onRequestClose}>
|
||||||
<Text style={styles.closeText}>✕</Text>
|
<Text style={styles.closeText}>✕</Text>
|
||||||
|
|
|
@ -14,6 +14,8 @@ import * as models from 'state/models/ui/shell'
|
||||||
import {colors, s} from 'lib/styles'
|
import {colors, s} from 'lib/styles'
|
||||||
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
|
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
interface Img {
|
interface Img {
|
||||||
uri: string
|
uri: string
|
||||||
|
@ -62,6 +64,7 @@ function LightboxInner({
|
||||||
initialIndex: number
|
initialIndex: number
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
const [index, setIndex] = useState<number>(initialIndex)
|
const [index, setIndex] = useState<number>(initialIndex)
|
||||||
const [isAltExpanded, setAltExpanded] = useState(false)
|
const [isAltExpanded, setAltExpanded] = useState(false)
|
||||||
|
|
||||||
|
@ -101,7 +104,7 @@ function LightboxInner({
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Close image viewer"
|
accessibilityLabel={_(msg`Close image viewer`)}
|
||||||
accessibilityHint="Exits image view"
|
accessibilityHint="Exits image view"
|
||||||
onAccessibilityEscape={onClose}>
|
onAccessibilityEscape={onClose}>
|
||||||
<View style={styles.imageCenterer}>
|
<View style={styles.imageCenterer}>
|
||||||
|
@ -117,7 +120,7 @@ function LightboxInner({
|
||||||
onPress={onPressLeft}
|
onPress={onPressLeft}
|
||||||
style={[styles.btn, styles.leftBtn]}
|
style={[styles.btn, styles.leftBtn]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Previous image"
|
accessibilityLabel={_(msg`Previous image`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-left"
|
icon="angle-left"
|
||||||
|
@ -131,7 +134,7 @@ function LightboxInner({
|
||||||
onPress={onPressRight}
|
onPress={onPressRight}
|
||||||
style={[styles.btn, styles.rightBtn]}
|
style={[styles.btn, styles.rightBtn]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Next image"
|
accessibilityLabel={_(msg`Next image`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-right"
|
icon="angle-right"
|
||||||
|
@ -145,7 +148,7 @@ function LightboxInner({
|
||||||
{imgs[index].alt ? (
|
{imgs[index].alt ? (
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Expand alt text"
|
accessibilityLabel={_(msg`Expand alt text`)}
|
||||||
accessibilityHint="If alt text is long, toggles alt text expanded state"
|
accessibilityHint="If alt text is long, toggles alt text expanded state"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setAltExpanded(!isAltExpanded)
|
setAltExpanded(!isAltExpanded)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {FlatList} from '../util/Views'
|
import {FlatList} from '../util/Views'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
const LOADING = {_reactKey: '__loading__'}
|
const LOADING = {_reactKey: '__loading__'}
|
||||||
const EMPTY = {_reactKey: '__empty__'}
|
const EMPTY = {_reactKey: '__empty__'}
|
||||||
|
@ -107,7 +108,9 @@ export const ListsList = observer(function ListsListImpl({
|
||||||
<View
|
<View
|
||||||
testID="listsEmpty"
|
testID="listsEmpty"
|
||||||
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
|
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>
|
</View>
|
||||||
)
|
)
|
||||||
} else if (item === ERROR_ITEM) {
|
} else if (item === ERROR_ITEM) {
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['70%']
|
export const snapPoints = ['70%']
|
||||||
|
@ -55,6 +57,7 @@ const shadesOfBlue: string[] = [
|
||||||
export function Component({}: {}) {
|
export function Component({}: {}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const [name, setName] = useState(
|
const [name, setName] = useState(
|
||||||
shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
|
shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
|
||||||
|
@ -121,15 +124,19 @@ export function Component({}: {}) {
|
||||||
<View>
|
<View>
|
||||||
{!appPassword ? (
|
{!appPassword ? (
|
||||||
<Text type="lg" style={[pal.text]}>
|
<Text type="lg" style={[pal.text]}>
|
||||||
Please enter a unique name for this App Password or use our randomly
|
<Trans>
|
||||||
generated one.
|
Please enter a unique name for this App Password or use our
|
||||||
|
randomly generated one.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text type="lg" style={[pal.text]}>
|
<Text type="lg" style={[pal.text]}>
|
||||||
<Text type="lg-bold" style={[pal.text]}>
|
<Text type="lg-bold" style={[pal.text, s.mr5]}>
|
||||||
Here is your app password.
|
<Trans>Here is your app password.</Trans>
|
||||||
</Text>{' '}
|
</Text>
|
||||||
Use this to sign into the other app along with your handle.
|
<Trans>
|
||||||
|
Use this to sign into the other app along with your handle.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{!appPassword ? (
|
{!appPassword ? (
|
||||||
|
@ -154,7 +161,7 @@ export function Component({}: {}) {
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
onEndEditing={createAppPassword}
|
onEndEditing={createAppPassword}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Name"
|
accessibilityLabel={_(msg`Name`)}
|
||||||
accessibilityHint="Input name for app password"
|
accessibilityHint="Input name for app password"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -163,13 +170,15 @@ export function Component({}: {}) {
|
||||||
style={[pal.border, styles.passwordContainer, pal.btn]}
|
style={[pal.border, styles.passwordContainer, pal.btn]}
|
||||||
onPress={onCopy}
|
onPress={onCopy}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Copy"
|
accessibilityLabel={_(msg`Copy`)}
|
||||||
accessibilityHint="Copies app password">
|
accessibilityHint="Copies app password">
|
||||||
<Text type="2xl-bold" style={[pal.text]}>
|
<Text type="2xl-bold" style={[pal.text]}>
|
||||||
{appPassword}
|
{appPassword}
|
||||||
</Text>
|
</Text>
|
||||||
{wasCopied ? (
|
{wasCopied ? (
|
||||||
<Text style={[pal.textLight]}>Copied</Text>
|
<Text style={[pal.textLight]}>
|
||||||
|
<Trans>Copied</Trans>
|
||||||
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={['far', 'clone']}
|
icon={['far', 'clone']}
|
||||||
|
@ -182,14 +191,18 @@ export function Component({}: {}) {
|
||||||
</View>
|
</View>
|
||||||
{appPassword ? (
|
{appPassword ? (
|
||||||
<Text type="lg" style={[pal.textLight, s.mb10]}>
|
<Text type="lg" style={[pal.textLight, s.mb10]}>
|
||||||
For security reasons, you won't be able to view this again. If you
|
<Trans>
|
||||||
lose this password, you'll need to generate a new one.
|
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>
|
||||||
) : (
|
) : (
|
||||||
<Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
|
<Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
|
||||||
Can only contain letters, numbers, spaces, dashes, and underscores.
|
<Trans>
|
||||||
Must be at least 4 characters long, but no more than 32 characters
|
Can only contain letters, numbers, spaces, dashes, and underscores.
|
||||||
long.
|
Must be at least 4 characters long, but no more than 32 characters
|
||||||
|
long.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<View style={styles.btnContainer}>
|
<View style={styles.btnContainer}>
|
||||||
|
|
|
@ -19,6 +19,8 @@ import {Text} from '../util/text/Text'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {isAndroid, isWeb} from 'platform/detection'
|
import {isAndroid, isWeb} from 'platform/detection'
|
||||||
import {ImageModel} from 'state/models/media/image'
|
import {ImageModel} from 'state/models/media/image'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['fullscreen']
|
export const snapPoints = ['fullscreen']
|
||||||
|
@ -30,6 +32,7 @@ interface Props {
|
||||||
export function Component({image}: Props) {
|
export function Component({image}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const [altText, setAltText] = useState(image.altText)
|
const [altText, setAltText] = useState(image.altText)
|
||||||
const windim = useWindowDimensions()
|
const windim = useWindowDimensions()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
|
@ -90,7 +93,7 @@ export function Component({image}: Props) {
|
||||||
placeholderTextColor={pal.colors.textLight}
|
placeholderTextColor={pal.colors.textLight}
|
||||||
value={altText}
|
value={altText}
|
||||||
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
||||||
accessibilityLabel="Image alt text"
|
accessibilityLabel={_(msg`Image alt text`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityLabelledBy="imageAltText"
|
accessibilityLabelledBy="imageAltText"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
@ -99,7 +102,7 @@ export function Component({image}: Props) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="altTextImageSaveBtn"
|
testID="altTextImageSaveBtn"
|
||||||
onPress={onPressSave}
|
onPress={onPressSave}
|
||||||
accessibilityLabel="Save alt text"
|
accessibilityLabel={_(msg`Save alt text`)}
|
||||||
accessibilityHint={`Saves alt text, which reads: ${altText}`}
|
accessibilityHint={`Saves alt text, which reads: ${altText}`}
|
||||||
accessibilityRole="button">
|
accessibilityRole="button">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
|
@ -108,7 +111,7 @@ export function Component({image}: Props) {
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.button]}>
|
style={[styles.button]}>
|
||||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||||
Save
|
<Trans>Save</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -116,12 +119,12 @@ export function Component({image}: Props) {
|
||||||
testID="altTextImageCancelBtn"
|
testID="altTextImageCancelBtn"
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel add image alt text"
|
accessibilityLabel={_(msg`Cancel add image alt text`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={onPressCancel}>
|
onAccessibilityEscape={onPressCancel}>
|
||||||
<View style={[styles.button]}>
|
<View style={[styles.button]}>
|
||||||
<Text type="button-lg" style={[pal.textLight]}>
|
<Text type="button-lg" style={[pal.textLight]}>
|
||||||
Cancel
|
<Trans>Cancel</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['50%']
|
export const snapPoints = ['50%']
|
||||||
|
@ -22,6 +24,7 @@ export const snapPoints = ['50%']
|
||||||
export const Component = observer(function Component({}: {}) {
|
export const Component = observer(function Component({}: {}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const [date, setDate] = useState<Date>(
|
const [date, setDate] = useState<Date>(
|
||||||
store.preferences.birthDate || new Date(),
|
store.preferences.birthDate || new Date(),
|
||||||
|
@ -49,12 +52,12 @@ export const Component = observer(function Component({}: {}) {
|
||||||
style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
|
style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
|
||||||
<View style={styles.titleSection}>
|
<View style={styles.titleSection}>
|
||||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||||
My Birthday
|
<Trans>My Birthday</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
<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>
|
</Text>
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
|
@ -65,7 +68,7 @@ export const Component = observer(function Component({}: {}) {
|
||||||
buttonType="default-light"
|
buttonType="default-light"
|
||||||
buttonStyle={[pal.border, styles.dateInputButton]}
|
buttonStyle={[pal.border, styles.dateInputButton]}
|
||||||
buttonLabelType="lg"
|
buttonLabelType="lg"
|
||||||
accessibilityLabel="Birthday"
|
accessibilityLabel={_(msg`Birthday`)}
|
||||||
accessibilityHint="Enter your birth date"
|
accessibilityHint="Enter your birth date"
|
||||||
accessibilityLabelledBy="birthDate"
|
accessibilityLabelledBy="birthDate"
|
||||||
/>
|
/>
|
||||||
|
@ -86,9 +89,11 @@ export const Component = observer(function Component({}: {}) {
|
||||||
onPress={onSave}
|
onPress={onSave}
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Save"
|
accessibilityLabel={_(msg`Save`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text style={[s.white, s.bold, s.f18]}>Save</Text>
|
<Text style={[s.white, s.bold, s.f18]}>
|
||||||
|
<Trans>Save</Trans>
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
enum Stages {
|
enum Stages {
|
||||||
|
@ -25,6 +27,7 @@ export const snapPoints = ['90%']
|
||||||
export const Component = observer(function Component({}: {}) {
|
export const Component = observer(function Component({}: {}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
|
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
|
||||||
const [email, setEmail] = useState<string>(
|
const [email, setEmail] = useState<string>(
|
||||||
store.session.currentSession?.email || '',
|
store.session.currentSession?.email || '',
|
||||||
|
@ -62,7 +65,9 @@ export const Component = observer(function Component({}: {}) {
|
||||||
// you can remove this any time after Oct2023
|
// you can remove this any time after Oct2023
|
||||||
// -prf
|
// -prf
|
||||||
if (err === 'email must be confirmed (temporary)') {
|
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)
|
setError(err)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -103,26 +108,26 @@ export const Component = observer(function Component({}: {}) {
|
||||||
style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
|
style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
|
||||||
<View style={styles.titleSection}>
|
<View style={styles.titleSection}>
|
||||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||||
{stage === Stages.InputEmail ? 'Change Your Email' : ''}
|
{stage === Stages.InputEmail ? _(msg`Change Your Email`) : ''}
|
||||||
{stage === Stages.ConfirmCode ? 'Security Step Required' : ''}
|
{stage === Stages.ConfirmCode ? _(msg`Security Step Required`) : ''}
|
||||||
{stage === Stages.Done ? 'Email Updated' : ''}
|
{stage === Stages.Done ? _(msg`Email Updated`) : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
||||||
{stage === Stages.InputEmail ? (
|
{stage === Stages.InputEmail ? (
|
||||||
<>Enter your new email address below.</>
|
<Trans>Enter your new email address below.</Trans>
|
||||||
) : stage === Stages.ConfirmCode ? (
|
) : stage === Stages.ConfirmCode ? (
|
||||||
<>
|
<Trans>
|
||||||
An email has been sent to your previous address,{' '}
|
An email has been sent to your previous address,{' '}
|
||||||
{store.session.currentSession?.email || ''}. It includes a
|
{store.session.currentSession?.email || ''}. It includes a
|
||||||
confirmation code which you can enter below.
|
confirmation code which you can enter below.
|
||||||
</>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Trans>
|
||||||
Your email has been updated but not verified. As a next step,
|
Your email has been updated but not verified. As a next step,
|
||||||
please verify your new email.
|
please verify your new email.
|
||||||
</>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
@ -135,7 +140,7 @@ export const Component = observer(function Component({}: {}) {
|
||||||
value={email}
|
value={email}
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Email"
|
accessibilityLabel={_(msg`Email`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
|
@ -151,7 +156,7 @@ export const Component = observer(function Component({}: {}) {
|
||||||
value={confirmationCode}
|
value={confirmationCode}
|
||||||
onChangeText={setConfirmationCode}
|
onChangeText={setConfirmationCode}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Confirmation code"
|
accessibilityLabel={_(msg`Confirmation code`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
@ -175,9 +180,9 @@ export const Component = observer(function Component({}: {}) {
|
||||||
testID="requestChangeBtn"
|
testID="requestChangeBtn"
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={onRequestChange}
|
onPress={onRequestChange}
|
||||||
accessibilityLabel="Request Change"
|
accessibilityLabel={_(msg`Request Change`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Request Change"
|
label={_(msg`Request Change`)}
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
labelStyle={[s.f18]}
|
labelStyle={[s.f18]}
|
||||||
/>
|
/>
|
||||||
|
@ -187,9 +192,9 @@ export const Component = observer(function Component({}: {}) {
|
||||||
testID="confirmBtn"
|
testID="confirmBtn"
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={onConfirm}
|
onPress={onConfirm}
|
||||||
accessibilityLabel="Confirm Change"
|
accessibilityLabel={_(msg`Confirm Change`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Confirm Change"
|
label={_(msg`Confirm Change`)}
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
labelStyle={[s.f18]}
|
labelStyle={[s.f18]}
|
||||||
/>
|
/>
|
||||||
|
@ -199,9 +204,9 @@ export const Component = observer(function Component({}: {}) {
|
||||||
testID="verifyBtn"
|
testID="verifyBtn"
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={onVerify}
|
onPress={onVerify}
|
||||||
accessibilityLabel="Verify New Email"
|
accessibilityLabel={_(msg`Verify New Email`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Verify New Email"
|
label={_(msg`Verify New Email`)}
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
labelStyle={[s.f18]}
|
labelStyle={[s.f18]}
|
||||||
/>
|
/>
|
||||||
|
@ -210,9 +215,9 @@ export const Component = observer(function Component({}: {}) {
|
||||||
testID="cancelBtn"
|
testID="cancelBtn"
|
||||||
type="default"
|
type="default"
|
||||||
onPress={() => closeModal()}
|
onPress={() => closeModal()}
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel={_(msg`Cancel`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Cancel"
|
label={_(msg`Cancel`)}
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
labelStyle={[s.f18]}
|
labelStyle={[s.f18]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -22,6 +22,8 @@ import {useTheme} from 'lib/ThemeContext'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['100%']
|
export const snapPoints = ['100%']
|
||||||
|
@ -31,6 +33,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
|
|
||||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||||
|
@ -141,7 +144,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel change handle"
|
accessibilityLabel={_(msg`Cancel change handle`)}
|
||||||
accessibilityHint="Exits handle change process"
|
accessibilityHint="Exits handle change process"
|
||||||
onAccessibilityEscape={onPressCancel}>
|
onAccessibilityEscape={onPressCancel}>
|
||||||
<Text type="lg" style={pal.textLight}>
|
<Text type="lg" style={pal.textLight}>
|
||||||
|
@ -153,7 +156,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
type="2xl-bold"
|
type="2xl-bold"
|
||||||
style={[styles.titleMiddle, pal.text]}
|
style={[styles.titleMiddle, pal.text]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
Change Handle
|
<Trans>Change Handle</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.titleRight}>
|
<View style={styles.titleRight}>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
|
@ -163,7 +166,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
testID="retryConnectButton"
|
testID="retryConnectButton"
|
||||||
onPress={onPressRetryConnect}
|
onPress={onPressRetryConnect}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Retry change handle"
|
accessibilityLabel={_(msg`Retry change handle`)}
|
||||||
accessibilityHint={`Retries handle change to ${handle}`}>
|
accessibilityHint={`Retries handle change to ${handle}`}>
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Retry
|
Retry
|
||||||
|
@ -173,10 +176,10 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressSave}
|
onPress={onPressSave}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Save handle change"
|
accessibilityLabel={_(msg`Save handle change`)}
|
||||||
accessibilityHint={`Saves handle change to ${handle}`}>
|
accessibilityHint={`Saves handle change to ${handle}`}>
|
||||||
<Text type="2xl-medium" style={pal.link}>
|
<Text type="2xl-medium" style={pal.link}>
|
||||||
Save
|
<Trans>Save</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
@ -234,6 +237,7 @@ function ProvidedHandleForm({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
@ -266,12 +270,12 @@ function ProvidedHandleForm({
|
||||||
onChangeText={onChangeHandle}
|
onChangeText={onChangeHandle}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Handle"
|
accessibilityLabel={_(msg`Handle`)}
|
||||||
accessibilityHint="Sets Bluesky username"
|
accessibilityHint="Sets Bluesky username"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
|
<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}>
|
<Text type="md-bold" style={pal.textLight}>
|
||||||
@{createFullHandle(handle, userDomain)}
|
@{createFullHandle(handle, userDomain)}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -280,9 +284,9 @@ function ProvidedHandleForm({
|
||||||
onPress={onToggleCustom}
|
onPress={onToggleCustom}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityHint="Hosting provider"
|
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]}>
|
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
|
||||||
I have my own domain
|
<Trans>I have my own domain</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
|
@ -314,6 +318,7 @@ function CustomHandleForm({
|
||||||
const palSecondary = usePalette('secondary')
|
const palSecondary = usePalette('secondary')
|
||||||
const palError = usePalette('error')
|
const palError = usePalette('error')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const [isVerifying, setIsVerifying] = React.useState(false)
|
const [isVerifying, setIsVerifying] = React.useState(false)
|
||||||
const [error, setError] = React.useState<string>('')
|
const [error, setError] = React.useState<string>('')
|
||||||
const [isDNSForm, setDNSForm] = React.useState<boolean>(true)
|
const [isDNSForm, setDNSForm] = React.useState<boolean>(true)
|
||||||
|
@ -367,7 +372,7 @@ function CustomHandleForm({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain">
|
<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>
|
</Text>
|
||||||
<View style={[pal.btn, styles.textInputWrapper]}>
|
<View style={[pal.btn, styles.textInputWrapper]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -385,7 +390,7 @@ function CustomHandleForm({
|
||||||
onChangeText={onChangeHandle}
|
onChangeText={onChangeHandle}
|
||||||
editable={!isProcessing}
|
editable={!isProcessing}
|
||||||
accessibilityLabelledBy="customDomain"
|
accessibilityLabelledBy="customDomain"
|
||||||
accessibilityLabel="Custom domain"
|
accessibilityLabel={_(msg`Custom domain`)}
|
||||||
accessibilityHint="Input your preferred hosting provider"
|
accessibilityHint="Input your preferred hosting provider"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -413,7 +418,7 @@ function CustomHandleForm({
|
||||||
{isDNSForm ? (
|
{isDNSForm ? (
|
||||||
<>
|
<>
|
||||||
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
|
<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>
|
</Text>
|
||||||
<View style={[styles.dnsTable, pal.btn]}>
|
<View style={[styles.dnsTable, pal.btn]}>
|
||||||
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
|
<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]}>
|
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
|
||||||
Upload a text file to:
|
<Trans>Upload a text file to:</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[styles.valueContainer, pal.btn]}>
|
<View style={[styles.valueContainer, pal.btn]}>
|
||||||
<View style={[styles.dnsValue]}>
|
<View style={[styles.dnsValue]}>
|
||||||
|
@ -483,7 +488,7 @@ function CustomHandleForm({
|
||||||
{canSave === true && (
|
{canSave === true && (
|
||||||
<View style={[styles.message, palSecondary.view]}>
|
<View style={[styles.message, palSecondary.view]}>
|
||||||
<Text type="md-medium" style={palSecondary.text}>
|
<Text type="md-medium" style={palSecondary.text}>
|
||||||
Domain verified!
|
<Trans>Domain verified!</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
@ -511,7 +516,7 @@ function CustomHandleForm({
|
||||||
<View style={styles.spacer} />
|
<View style={styles.spacer} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onToggleCustom}
|
onPress={onToggleCustom}
|
||||||
accessibilityLabel="Use default provider"
|
accessibilityLabel={_(msg`Use default provider`)}
|
||||||
accessibilityHint="Use bsky.social as hosting provider">
|
accessibilityHint="Use bsky.social as hosting provider">
|
||||||
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
|
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
|
||||||
Nevermind, create a handle for me
|
Nevermind, create a handle for me
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import type {ConfirmModal} from '#/state/modals'
|
import type {ConfirmModal} from '#/state/modals'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
|
@ -26,6 +28,7 @@ export function Component({
|
||||||
cancelBtnText,
|
cancelBtnText,
|
||||||
}: ConfirmModal) {
|
}: ConfirmModal) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
|
@ -69,7 +72,7 @@ export function Component({
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[styles.btn, confirmBtnStyle]}
|
style={[styles.btn, confirmBtnStyle]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Confirm"
|
accessibilityLabel={_(msg`Confirm`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text style={[s.white, s.bold, s.f18]}>
|
<Text style={[s.white, s.bold, s.f18]}>
|
||||||
{confirmBtnText ?? 'Confirm'}
|
{confirmBtnText ?? 'Confirm'}
|
||||||
|
@ -82,7 +85,7 @@ export function Component({
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
style={[styles.btnCancel, s.mt10]}
|
style={[styles.btnCancel, s.mt10]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel={_(msg`Cancel`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
<Text type="button-lg" style={pal.textLight}>
|
||||||
{cancelBtnText ?? 'Cancel'}
|
{cancelBtnText ?? 'Cancel'}
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {isIOS} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['90%']
|
export const snapPoints = ['90%']
|
||||||
|
@ -25,6 +27,7 @@ export const Component = observer(
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -37,7 +40,9 @@ export const Component = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
|
<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}>
|
<ScrollView style={styles.scrollContainer}>
|
||||||
<AdultContentEnabledPref />
|
<AdultContentEnabledPref />
|
||||||
<ContentLabelPref
|
<ContentLabelPref
|
||||||
|
@ -71,14 +76,16 @@ export const Component = observer(
|
||||||
testID="sendReportBtn"
|
testID="sendReportBtn"
|
||||||
onPress={onPressDone}
|
onPress={onPressDone}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Done"
|
accessibilityLabel={_(msg`Done`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
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>
|
</LinearGradient>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -24,6 +24,8 @@ import {useTheme} from 'lib/ThemeContext'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
const MAX_NAME = 64 // todo
|
const MAX_NAME = 64 // todo
|
||||||
|
@ -47,6 +49,7 @@ export function Component({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
const activePurpose = useMemo(() => {
|
const activePurpose = useMemo(() => {
|
||||||
if (list?.data?.purpose) {
|
if (list?.data?.purpose) {
|
||||||
|
@ -164,14 +167,18 @@ export function Component({
|
||||||
]}
|
]}
|
||||||
testID="createOrEditListModal">
|
testID="createOrEditListModal">
|
||||||
<Text style={[styles.title, pal.text]}>
|
<Text style={[styles.title, pal.text]}>
|
||||||
{list ? 'Edit' : 'New'} {purposeLabel} List
|
<Trans>
|
||||||
|
{list ? 'Edit' : 'New'} {purposeLabel} List
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{error !== '' && (
|
{error !== '' && (
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<ErrorMessage message={error} />
|
<ErrorMessage message={error} />
|
||||||
</View>
|
</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}]}>
|
<View style={[styles.avi, {borderColor: pal.colors.background}]}>
|
||||||
<EditableUserAvatar
|
<EditableUserAvatar
|
||||||
type="list"
|
type="list"
|
||||||
|
@ -183,7 +190,7 @@ export function Component({
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.label, pal.text]} nativeID="list-name">
|
<Text style={[styles.label, pal.text]} nativeID="list-name">
|
||||||
List Name
|
<Trans>List Name</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="editNameInput"
|
testID="editNameInput"
|
||||||
|
@ -195,14 +202,14 @@ export function Component({
|
||||||
value={name}
|
value={name}
|
||||||
onChangeText={v => setName(enforceLen(v, MAX_NAME))}
|
onChangeText={v => setName(enforceLen(v, MAX_NAME))}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Name"
|
accessibilityLabel={_(msg`Name`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityLabelledBy="list-name"
|
accessibilityLabelledBy="list-name"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={s.pb10}>
|
<View style={s.pb10}>
|
||||||
<Text style={[styles.label, pal.text]} nativeID="list-description">
|
<Text style={[styles.label, pal.text]} nativeID="list-description">
|
||||||
Description
|
<Trans>Description</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="editDescriptionInput"
|
testID="editDescriptionInput"
|
||||||
|
@ -218,7 +225,7 @@ export function Component({
|
||||||
value={description}
|
value={description}
|
||||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Description"
|
accessibilityLabel={_(msg`Description`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityLabelledBy="list-description"
|
accessibilityLabelledBy="list-description"
|
||||||
/>
|
/>
|
||||||
|
@ -233,14 +240,16 @@ export function Component({
|
||||||
style={s.mt10}
|
style={s.mt10}
|
||||||
onPress={onPressSave}
|
onPress={onPressSave}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Save"
|
accessibilityLabel={_(msg`Save`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
style={[styles.btn]}>
|
||||||
<Text style={[s.white, s.bold]}>Save</Text>
|
<Text style={[s.white, s.bold]}>
|
||||||
|
<Trans>Save</Trans>
|
||||||
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
@ -249,11 +258,13 @@ export function Component({
|
||||||
style={s.mt5}
|
style={s.mt5}
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel={_(msg`Cancel`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={onPressCancel}>
|
onAccessibilityEscape={onPressCancel}>
|
||||||
<View style={[styles.btn]}>
|
<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>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {resetToTab} from '../../../Navigation'
|
import {resetToTab} from '../../../Navigation'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['60%']
|
export const snapPoints = ['60%']
|
||||||
|
@ -25,6 +27,7 @@ export function Component({}: {}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
||||||
|
@ -71,7 +74,7 @@ export function Component({}: {}) {
|
||||||
<View style={[styles.innerContainer, pal.view]}>
|
<View style={[styles.innerContainer, pal.view]}>
|
||||||
<View style={[styles.titleContainer, pal.view]}>
|
<View style={[styles.titleContainer, pal.view]}>
|
||||||
<Text type="title-xl" style={[s.textCenter, pal.text]}>
|
<Text type="title-xl" style={[s.textCenter, pal.text]}>
|
||||||
Delete Account
|
<Trans>Delete Account</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[pal.view, s.flexRow]}>
|
<View style={[pal.view, s.flexRow]}>
|
||||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
<Text type="title-xl" style={[pal.text, s.bold]}>
|
||||||
|
@ -95,8 +98,10 @@ export function Component({}: {}) {
|
||||||
{!isEmailSent ? (
|
{!isEmailSent ? (
|
||||||
<>
|
<>
|
||||||
<Text type="lg" style={[styles.description, pal.text]}>
|
<Text type="lg" style={[styles.description, pal.text]}>
|
||||||
For security reasons, we'll need to send a confirmation code to
|
<Trans>
|
||||||
your email address.
|
For security reasons, we'll need to send a confirmation code to
|
||||||
|
your email address.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={s.mt10}>
|
<View style={s.mt10}>
|
||||||
|
@ -113,7 +118,7 @@ export function Component({}: {}) {
|
||||||
style={styles.mt20}
|
style={styles.mt20}
|
||||||
onPress={onPressSendEmail}
|
onPress={onPressSendEmail}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Send email"
|
accessibilityLabel={_(msg`Send email`)}
|
||||||
accessibilityHint="Sends email with confirmation code for account deletion">
|
accessibilityHint="Sends email with confirmation code for account deletion">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[
|
colors={[
|
||||||
|
@ -124,7 +129,7 @@ export function Component({}: {}) {
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
style={[styles.btn]}>
|
||||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||||
Send Email
|
<Trans>Send Email</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -132,11 +137,11 @@ export function Component({}: {}) {
|
||||||
style={[styles.btn, s.mt10]}
|
style={[styles.btn, s.mt10]}
|
||||||
onPress={onCancel}
|
onPress={onCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel account deletion"
|
accessibilityLabel={_(msg`Cancel account deletion`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={onCancel}>
|
onAccessibilityEscape={onCancel}>
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
<Text type="button-lg" style={pal.textLight}>
|
||||||
Cancel
|
<Trans>Cancel</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
|
@ -149,8 +154,10 @@ export function Component({}: {}) {
|
||||||
type="lg"
|
type="lg"
|
||||||
style={styles.description}
|
style={styles.description}
|
||||||
nativeID="confirmationCode">
|
nativeID="confirmationCode">
|
||||||
Check your inbox for an email with the confirmation code to enter
|
<Trans>
|
||||||
below:
|
Check your inbox for an email with the confirmation code to
|
||||||
|
enter below:
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
|
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
|
||||||
|
@ -160,11 +167,11 @@ export function Component({}: {}) {
|
||||||
value={confirmCode}
|
value={confirmCode}
|
||||||
onChangeText={setConfirmCode}
|
onChangeText={setConfirmCode}
|
||||||
accessibilityLabelledBy="confirmationCode"
|
accessibilityLabelledBy="confirmationCode"
|
||||||
accessibilityLabel="Confirmation code"
|
accessibilityLabel={_(msg`Confirmation code`)}
|
||||||
accessibilityHint="Input confirmation code for account deletion"
|
accessibilityHint="Input confirmation code for account deletion"
|
||||||
/>
|
/>
|
||||||
<Text type="lg" style={styles.description} nativeID="password">
|
<Text type="lg" style={styles.description} nativeID="password">
|
||||||
Please enter your password as well:
|
<Trans>Please enter your password as well:</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, pal.borderDark, pal.text]}
|
style={[styles.textInput, pal.borderDark, pal.text]}
|
||||||
|
@ -175,7 +182,7 @@ export function Component({}: {}) {
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
accessibilityLabelledBy="password"
|
accessibilityLabelledBy="password"
|
||||||
accessibilityLabel="Password"
|
accessibilityLabel={_(msg`Password`)}
|
||||||
accessibilityHint="Input password for account deletion"
|
accessibilityHint="Input password for account deletion"
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
|
@ -193,21 +200,21 @@ export function Component({}: {}) {
|
||||||
style={[styles.btn, styles.evilBtn, styles.mt20]}
|
style={[styles.btn, styles.evilBtn, styles.mt20]}
|
||||||
onPress={onPressConfirmDelete}
|
onPress={onPressConfirmDelete}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Confirm delete account"
|
accessibilityLabel={_(msg`Confirm delete account`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||||
Delete my account
|
<Trans>Delete my account</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.btn, s.mt10]}
|
style={[styles.btn, s.mt10]}
|
||||||
onPress={onCancel}
|
onPress={onCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel account deletion"
|
accessibilityLabel={_(msg`Cancel account deletion`)}
|
||||||
accessibilityHint="Exits account deletion process"
|
accessibilityHint="Exits account deletion process"
|
||||||
onAccessibilityEscape={onCancel}>
|
onAccessibilityEscape={onCancel}>
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
<Text type="button-lg" style={pal.textLight}>
|
||||||
Cancel
|
<Trans>Cancel</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -18,6 +18,8 @@ import {Slider} from '@miblanchard/react-native-slider'
|
||||||
import {MaterialIcons} from '@expo/vector-icons'
|
import {MaterialIcons} from '@expo/vector-icons'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {getKeys} from 'lib/type-assertions'
|
import {getKeys} from 'lib/type-assertions'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['80%']
|
export const snapPoints = ['80%']
|
||||||
|
@ -52,6 +54,7 @@ export const Component = observer(function EditImageImpl({
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const windowDimensions = useWindowDimensions()
|
const windowDimensions = useWindowDimensions()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
|
@ -200,7 +203,9 @@ export const Component = observer(function EditImageImpl({
|
||||||
paddingHorizontal: isMobile ? 16 : undefined,
|
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 style={[styles.gap18, s.flexRow]}>
|
||||||
<View>
|
<View>
|
||||||
<View
|
<View
|
||||||
|
@ -228,7 +233,7 @@ export const Component = observer(function EditImageImpl({
|
||||||
<View>
|
<View>
|
||||||
{!isMobile ? (
|
{!isMobile ? (
|
||||||
<Text type="sm-bold" style={pal.text}>
|
<Text type="sm-bold" style={pal.text}>
|
||||||
Ratios
|
<Trans>Ratios</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={imgControlStyles}>
|
<View style={imgControlStyles}>
|
||||||
|
@ -263,7 +268,7 @@ export const Component = observer(function EditImageImpl({
|
||||||
</View>
|
</View>
|
||||||
{!isMobile ? (
|
{!isMobile ? (
|
||||||
<Text type="sm-bold" style={[pal.text, styles.subsection]}>
|
<Text type="sm-bold" style={[pal.text, styles.subsection]}>
|
||||||
Transformations
|
<Trans>Transformations</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={imgControlStyles}>
|
<View style={imgControlStyles}>
|
||||||
|
@ -291,7 +296,7 @@ export const Component = observer(function EditImageImpl({
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.gap18, styles.bottomSection, pal.border]}>
|
<View style={[styles.gap18, styles.bottomSection, pal.border]}>
|
||||||
<Text type="sm-bold" style={pal.text} nativeID="alt-text">
|
<Text type="sm-bold" style={pal.text} nativeID="alt-text">
|
||||||
Accessibility
|
<Trans>Accessibility</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="altTextImageInput"
|
testID="altTextImageInput"
|
||||||
|
@ -307,7 +312,7 @@ export const Component = observer(function EditImageImpl({
|
||||||
multiline
|
multiline
|
||||||
value={altText}
|
value={altText}
|
||||||
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
||||||
accessibilityLabel="Alt text"
|
accessibilityLabel={_(msg`Alt text`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityLabelledBy="alt-text"
|
accessibilityLabelledBy="alt-text"
|
||||||
/>
|
/>
|
||||||
|
@ -315,7 +320,7 @@ export const Component = observer(function EditImageImpl({
|
||||||
<View style={styles.btns}>
|
<View style={styles.btns}>
|
||||||
<Pressable onPress={onPressCancel} accessibilityRole="button">
|
<Pressable onPress={onPressCancel} accessibilityRole="button">
|
||||||
<Text type="xl" style={pal.link}>
|
<Text type="xl" style={pal.link}>
|
||||||
Cancel
|
<Trans>Cancel</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={onPressSave} accessibilityRole="button">
|
<Pressable onPress={onPressSave} accessibilityRole="button">
|
||||||
|
@ -325,7 +330,7 @@ export const Component = observer(function EditImageImpl({
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
style={[styles.btn]}>
|
||||||
<Text type="xl-medium" style={s.white}>
|
<Text type="xl-medium" style={s.white}>
|
||||||
Done
|
<Trans>Done</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
|
@ -26,6 +26,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
||||||
import Animated, {FadeOut} from 'react-native-reanimated'
|
import Animated, {FadeOut} from 'react-native-reanimated'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
const AnimatedTouchableOpacity =
|
const AnimatedTouchableOpacity =
|
||||||
|
@ -44,6 +46,7 @@ export function Component({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
|
|
||||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||||
|
@ -151,7 +154,9 @@ export function Component({
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView style={s.flex1} behavior="height">
|
<KeyboardAvoidingView style={s.flex1} behavior="height">
|
||||||
<ScrollView style={[pal.view]} testID="editProfileModal">
|
<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}>
|
<View style={styles.photos}>
|
||||||
<UserBanner
|
<UserBanner
|
||||||
banner={userBanner}
|
banner={userBanner}
|
||||||
|
@ -172,7 +177,9 @@ export function Component({
|
||||||
)}
|
)}
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.label, pal.text]}>Display Name</Text>
|
<Text style={[styles.label, pal.text]}>
|
||||||
|
<Trans>Display Name</Trans>
|
||||||
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="editProfileDisplayNameInput"
|
testID="editProfileDisplayNameInput"
|
||||||
style={[styles.textInput, pal.border, pal.text]}
|
style={[styles.textInput, pal.border, pal.text]}
|
||||||
|
@ -183,12 +190,14 @@ export function Component({
|
||||||
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
||||||
}
|
}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Display name"
|
accessibilityLabel={_(msg`Display name`)}
|
||||||
accessibilityHint="Edit your display name"
|
accessibilityHint="Edit your display name"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={s.pb10}>
|
<View style={s.pb10}>
|
||||||
<Text style={[styles.label, pal.text]}>Description</Text>
|
<Text style={[styles.label, pal.text]}>
|
||||||
|
<Trans>Description</Trans>
|
||||||
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="editProfileDescriptionInput"
|
testID="editProfileDescriptionInput"
|
||||||
style={[styles.textArea, pal.border, pal.text]}
|
style={[styles.textArea, pal.border, pal.text]}
|
||||||
|
@ -199,7 +208,7 @@ export function Component({
|
||||||
value={description}
|
value={description}
|
||||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Description"
|
accessibilityLabel={_(msg`Description`)}
|
||||||
accessibilityHint="Edit your profile description"
|
accessibilityHint="Edit your profile description"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -213,14 +222,16 @@ export function Component({
|
||||||
style={s.mt10}
|
style={s.mt10}
|
||||||
onPress={onPressSave}
|
onPress={onPressSave}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Save"
|
accessibilityLabel={_(msg`Save`)}
|
||||||
accessibilityHint="Saves any changes to your profile">
|
accessibilityHint="Saves any changes to your profile">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
style={[styles.btn]}>
|
||||||
<Text style={[s.white, s.bold]}>Save Changes</Text>
|
<Text style={[s.white, s.bold]}>
|
||||||
|
<Trans>Save Changes</Trans>
|
||||||
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
@ -231,11 +242,13 @@ export function Component({
|
||||||
style={s.mt5}
|
style={s.mt5}
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel profile editing"
|
accessibilityLabel={_(msg`Cancel profile editing`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={onPressCancel}>
|
onAccessibilityEscape={onPressCancel}>
|
||||||
<View style={[styles.btn]}>
|
<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>
|
</View>
|
||||||
</AnimatedTouchableOpacity>
|
</AnimatedTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {ScrollView} from './util'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useInvitesState, useInvitesAPI} from '#/state/invites'
|
import {useInvitesState, useInvitesAPI} from '#/state/invites'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
|
@ -38,8 +39,10 @@ export function Component({}: {}) {
|
||||||
<View style={[styles.container, pal.view]} testID="inviteCodesModal">
|
<View style={[styles.container, pal.view]} testID="inviteCodesModal">
|
||||||
<View style={[styles.empty, pal.viewLight]}>
|
<View style={[styles.empty, pal.viewLight]}>
|
||||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||||
You don't have any invite codes yet! We'll send you some when you've
|
<Trans>
|
||||||
been on Bluesky for a little longer.
|
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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.flex1} />
|
<View style={styles.flex1} />
|
||||||
|
@ -63,10 +66,12 @@ export function Component({}: {}) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, pal.view]} testID="inviteCodesModal">
|
<View style={[styles.container, pal.view]} testID="inviteCodesModal">
|
||||||
<Text type="title-xl" style={[styles.title, pal.text]}>
|
<Text type="title-xl" style={[styles.title, pal.text]}>
|
||||||
Invite a Friend
|
<Trans>Invite a Friend</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="lg" style={[styles.description, pal.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>
|
</Text>
|
||||||
<ScrollView style={[styles.scrollContainer, pal.border]}>
|
<ScrollView style={[styles.scrollContainer, pal.border]}>
|
||||||
{store.me.invites.map((invite, i) => (
|
{store.me.invites.map((invite, i) => (
|
||||||
|
@ -138,7 +143,9 @@ const InviteCode = observer(function InviteCodeImpl({
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.flex1} />
|
<View style={styles.flex1} />
|
||||||
{!used && invitesState.copiedInvites.includes(invite.code) && (
|
{!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 && (
|
{!used && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -154,7 +161,9 @@ const InviteCode = observer(function InviteCodeImpl({
|
||||||
gap: 8,
|
gap: 8,
|
||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
}}>
|
}}>
|
||||||
<Text style={pal.text}>Used by:</Text>
|
<Text style={pal.text}>
|
||||||
|
<Trans>Used by:</Trans>
|
||||||
|
</Text>
|
||||||
{invite.uses.map(use => (
|
{invite.uses.map(use => (
|
||||||
<Link
|
<Link
|
||||||
key={use.usedBy}
|
key={use.usedBy}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
|
import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['50%']
|
export const snapPoints = ['50%']
|
||||||
|
@ -24,6 +26,7 @@ export const Component = observer(function Component({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
const {_} = useLingui()
|
||||||
const potentiallyMisleading = isPossiblyAUrl(text)
|
const potentiallyMisleading = isPossiblyAUrl(text)
|
||||||
|
|
||||||
const onPressVisit = () => {
|
const onPressVisit = () => {
|
||||||
|
@ -45,26 +48,26 @@ export const Component = observer(function Component({
|
||||||
size={18}
|
size={18}
|
||||||
/>
|
/>
|
||||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||||
Potentially Misleading Link
|
<Trans>Potentially Misleading Link</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||||
Leaving Bluesky
|
<Trans>Leaving Bluesky</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{gap: 10}}>
|
<View style={{gap: 10}}>
|
||||||
<Text type="lg" style={pal.text}>
|
<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>
|
</Text>
|
||||||
|
|
||||||
<LinkBox href={href} />
|
<LinkBox href={href} />
|
||||||
|
|
||||||
{potentiallyMisleading && (
|
{potentiallyMisleading && (
|
||||||
<Text type="lg" style={pal.text}>
|
<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>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
@ -74,7 +77,7 @@ export const Component = observer(function Component({
|
||||||
testID="confirmBtn"
|
testID="confirmBtn"
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={onPressVisit}
|
onPress={onPressVisit}
|
||||||
accessibilityLabel="Visit Site"
|
accessibilityLabel={_(msg`Visit Site`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Visit Site"
|
label="Visit Site"
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
|
@ -84,7 +87,7 @@ export const Component = observer(function Component({
|
||||||
testID="cancelBtn"
|
testID="cancelBtn"
|
||||||
type="default"
|
type="default"
|
||||||
onPress={() => closeModal()}
|
onPress={() => closeModal()}
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel={_(msg`Cancel`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
|
|
|
@ -26,6 +26,8 @@ import {cleanError} from 'lib/strings/errors'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['90%']
|
export const snapPoints = ['90%']
|
||||||
|
@ -39,6 +41,7 @@ export const Component = observer(function Component({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
|
@ -85,7 +88,7 @@ export const Component = observer(function Component({
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={onChangeQuery}
|
onChangeText={onChangeQuery}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Search"
|
accessibilityLabel={_(msg`Search`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
autoFocus
|
autoFocus
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
@ -97,7 +100,7 @@ export const Component = observer(function Component({
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPressCancelSearch}
|
onPress={onPressCancelSearch}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel search"
|
accessibilityLabel={_(msg`Cancel search`)}
|
||||||
accessibilityHint="Exits inputting search query"
|
accessibilityHint="Exits inputting search query"
|
||||||
onAccessibilityEscape={onPressCancelSearch}
|
onAccessibilityEscape={onPressCancelSearch}
|
||||||
hitSlop={HITSLOP_20}>
|
hitSlop={HITSLOP_20}>
|
||||||
|
@ -136,7 +139,7 @@ export const Component = observer(function Component({
|
||||||
pal.textLight,
|
pal.textLight,
|
||||||
{paddingHorizontal: 12, paddingVertical: 16},
|
{paddingHorizontal: 12, paddingVertical: 16},
|
||||||
]}>
|
]}>
|
||||||
No results found for {autocompleteView.prefix}
|
<Trans>No results found for {autocompleteView.prefix}</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
@ -149,7 +152,7 @@ export const Component = observer(function Component({
|
||||||
testID="doneBtn"
|
testID="doneBtn"
|
||||||
type="default"
|
type="default"
|
||||||
onPress={() => closeModal()}
|
onPress={() => closeModal()}
|
||||||
accessibilityLabel="Done"
|
accessibilityLabel={_(msg`Done`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Done"
|
label="Done"
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {Text} from '../util/text/Text'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {RepostIcon} from 'lib/icons'
|
import {RepostIcon} from 'lib/icons'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = [250]
|
export const snapPoints = [250]
|
||||||
|
@ -21,6 +23,7 @@ export function Component({
|
||||||
// TODO: Add author into component
|
// TODO: Add author into component
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const onPress = async () => {
|
const onPress = async () => {
|
||||||
closeModal()
|
closeModal()
|
||||||
|
@ -38,7 +41,7 @@ export function Component({
|
||||||
accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
|
accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
|
||||||
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
|
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
|
||||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||||
{!isReposted ? 'Repost' : 'Undo repost'}
|
<Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -46,11 +49,11 @@ export function Component({
|
||||||
style={[styles.actionBtn]}
|
style={[styles.actionBtn]}
|
||||||
onPress={onQuote}
|
onPress={onQuote}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Quote post"
|
accessibilityLabel={_(msg`Quote post`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
|
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
|
||||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||||
Quote Post
|
<Trans>Quote Post</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
@ -58,7 +61,7 @@ export function Component({
|
||||||
testID="cancelBtn"
|
testID="cancelBtn"
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel quote post"
|
accessibilityLabel={_(msg`Cancel quote post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={onPress}>
|
onAccessibilityEscape={onPress}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
|
@ -66,7 +69,9 @@ export function Component({
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
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>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {isWeb} from 'platform/detection'
|
||||||
import {Button} from '../util/forms/Button'
|
import {Button} from '../util/forms/Button'
|
||||||
import {SelectableBtn} from '../util/forms/SelectableBtn'
|
import {SelectableBtn} from '../util/forms/SelectableBtn'
|
||||||
import {ScrollView} from 'view/com/modals/util'
|
import {ScrollView} from 'view/com/modals/util'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
|
const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
|
||||||
|
@ -28,6 +30,7 @@ export const Component = observer(function Component({
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const [selected, setSelected] = useState(labels)
|
const [selected, setSelected] = useState(labels)
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
const toggleAdultLabel = (label: string) => {
|
const toggleAdultLabel = (label: string) => {
|
||||||
const hadLabel = selected.includes(label)
|
const hadLabel = selected.includes(label)
|
||||||
|
@ -51,7 +54,7 @@ export const Component = observer(function Component({
|
||||||
<View testID="selfLabelModal" style={[pal.view, styles.container]}>
|
<View testID="selfLabelModal" style={[pal.view, styles.container]}>
|
||||||
<View style={styles.titleSection}>
|
<View style={styles.titleSection}>
|
||||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||||
Add a content warning
|
<Trans>Add a content warning</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
@ -70,7 +73,7 @@ export const Component = observer(function Component({
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
}}>
|
}}>
|
||||||
<Text type="title" style={pal.text}>
|
<Text type="title" style={pal.text}>
|
||||||
Adult Content
|
<Trans>Adult Content</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{hasAdultSelection ? (
|
{hasAdultSelection ? (
|
||||||
<Button
|
<Button
|
||||||
|
@ -78,7 +81,7 @@ export const Component = observer(function Component({
|
||||||
onPress={removeAdultLabel}
|
onPress={removeAdultLabel}
|
||||||
style={{paddingTop: 0, paddingBottom: 0, paddingRight: 0}}>
|
style={{paddingTop: 0, paddingBottom: 0, paddingRight: 0}}>
|
||||||
<Text type="md" style={pal.link}>
|
<Text type="md" style={pal.link}>
|
||||||
Remove
|
<Trans>Remove</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -116,23 +119,25 @@ export const Component = observer(function Component({
|
||||||
|
|
||||||
<Text style={[pal.text, styles.adultExplainer]}>
|
<Text style={[pal.text, styles.adultExplainer]}>
|
||||||
{selected.includes('sexual') ? (
|
{selected.includes('sexual') ? (
|
||||||
<>Pictures meant for adults.</>
|
<Trans>Pictures meant for adults.</Trans>
|
||||||
) : selected.includes('nudity') ? (
|
) : selected.includes('nudity') ? (
|
||||||
<>Artistic or non-erotic nudity.</>
|
<Trans>Artistic or non-erotic nudity.</Trans>
|
||||||
) : selected.includes('porn') ? (
|
) : 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>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<View>
|
<View>
|
||||||
<Text style={[pal.textLight]}>
|
<Text style={[pal.textLight]}>
|
||||||
<Text type="md-bold" style={[pal.textLight]}>
|
<Text type="md-bold" style={[pal.textLight, s.mr5]}>
|
||||||
Not Applicable
|
<Trans>Not Applicable.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
. This warning is only available for posts with media attached.
|
<Trans>
|
||||||
|
This warning is only available for posts with media attached.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
@ -147,9 +152,11 @@ export const Component = observer(function Component({
|
||||||
}}
|
}}
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Confirm"
|
accessibilityLabel={_(msg`Confirm`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
<Text style={[s.white, s.bold, s.f18]}>
|
||||||
|
<Trans>Done</Trans>
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
|
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
|
||||||
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
|
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'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['80%']
|
export const snapPoints = ['80%']
|
||||||
|
@ -19,6 +21,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [customUrl, setCustomUrl] = useState<string>('')
|
const [customUrl, setCustomUrl] = useState<string>('')
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
|
|
||||||
const doSelect = (url: string) => {
|
const doSelect = (url: string) => {
|
||||||
|
@ -32,7 +35,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
return (
|
return (
|
||||||
<View style={[pal.view, s.flex1]} testID="serverInputModal">
|
<View style={[pal.view, s.flex1]} testID="serverInputModal">
|
||||||
<Text type="2xl-bold" style={[pal.text, s.textCenter]}>
|
<Text type="2xl-bold" style={[pal.text, s.textCenter]}>
|
||||||
Choose Service
|
<Trans>Choose Service</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView style={styles.inner}>
|
<ScrollView style={styles.inner}>
|
||||||
<View style={styles.group}>
|
<View style={styles.group}>
|
||||||
|
@ -43,7 +46,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
|
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
|
||||||
accessibilityRole="button">
|
accessibilityRole="button">
|
||||||
<Text style={styles.btnText}>Local dev server</Text>
|
<Text style={styles.btnText}>
|
||||||
|
<Trans>Local dev server</Trans>
|
||||||
|
</Text>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrow-right"
|
icon="arrow-right"
|
||||||
style={s.white as FontAwesomeIconStyle}
|
style={s.white as FontAwesomeIconStyle}
|
||||||
|
@ -53,7 +58,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={() => doSelect(STAGING_SERVICE)}
|
onPress={() => doSelect(STAGING_SERVICE)}
|
||||||
accessibilityRole="button">
|
accessibilityRole="button">
|
||||||
<Text style={styles.btnText}>Staging</Text>
|
<Text style={styles.btnText}>
|
||||||
|
<Trans>Staging</Trans>
|
||||||
|
</Text>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrow-right"
|
icon="arrow-right"
|
||||||
style={s.white as FontAwesomeIconStyle}
|
style={s.white as FontAwesomeIconStyle}
|
||||||
|
@ -65,9 +72,11 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={() => doSelect(PROD_SERVICE)}
|
onPress={() => doSelect(PROD_SERVICE)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Select Bluesky Social"
|
accessibilityLabel={_(msg`Select Bluesky Social`)}
|
||||||
accessibilityHint="Sets Bluesky Social as your service provider">
|
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
|
<FontAwesomeIcon
|
||||||
icon="arrow-right"
|
icon="arrow-right"
|
||||||
style={s.white as FontAwesomeIconStyle}
|
style={s.white as FontAwesomeIconStyle}
|
||||||
|
@ -75,7 +84,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.group}>
|
<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}>
|
<View style={s.flexRow}>
|
||||||
<TextInput
|
<TextInput
|
||||||
testID="customServerTextInput"
|
testID="customServerTextInput"
|
||||||
|
@ -88,7 +99,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||||
keyboardAppearance={theme.colorScheme}
|
keyboardAppearance={theme.colorScheme}
|
||||||
value={customUrl}
|
value={customUrl}
|
||||||
onChangeText={setCustomUrl}
|
onChangeText={setCustomUrl}
|
||||||
accessibilityLabel="Custom domain"
|
accessibilityLabel={_(msg`Custom domain`)}
|
||||||
// TODO: Simplify this wording further to be understandable by everyone
|
// TODO: Simplify this wording further to be understandable by everyone
|
||||||
accessibilityHint="Use your domain as your Bluesky client service provider"
|
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 {makeProfileLink} from 'lib/routes/links'
|
||||||
import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
|
import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
|
||||||
import {Haptics} from 'lib/haptics'
|
import {Haptics} from 'lib/haptics'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export const snapPoints = ['40%', '90%']
|
export const snapPoints = ['40%', '90%']
|
||||||
|
|
||||||
export function Component({}: {}) {
|
export function Component({}: {}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const {_: _lingui} = useLingui()
|
||||||
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
|
const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
|
||||||
|
@ -41,7 +44,7 @@ export function Component({}: {}) {
|
||||||
style={[styles.container, pal.view]}
|
style={[styles.container, pal.view]}
|
||||||
contentContainerStyle={[styles.innerContainer, pal.view]}>
|
contentContainerStyle={[styles.innerContainer, pal.view]}>
|
||||||
<Text type="title-xl" style={[styles.title, pal.text]}>
|
<Text type="title-xl" style={[styles.title, pal.text]}>
|
||||||
Switch Account
|
<Trans>Switch Account</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{isSwitching ? (
|
{isSwitching ? (
|
||||||
<View style={[pal.view, styles.linkCard]}>
|
<View style={[pal.view, styles.linkCard]}>
|
||||||
|
@ -65,10 +68,10 @@ export function Component({}: {}) {
|
||||||
testID="signOutBtn"
|
testID="signOutBtn"
|
||||||
onPress={isSwitching ? undefined : onPressSignout}
|
onPress={isSwitching ? undefined : onPressSignout}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Sign out"
|
accessibilityLabel={_lingui(msg`Sign out`)}
|
||||||
accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
|
accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
|
||||||
<Text type="lg" style={pal.link}>
|
<Text type="lg" style={pal.link}>
|
||||||
Sign out
|
<Trans>Sign out</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid} from 'platform/detection'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['fullscreen']
|
export const snapPoints = ['fullscreen']
|
||||||
|
@ -39,6 +41,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const palPrimary = usePalette('primary')
|
const palPrimary = usePalette('primary')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
const [originalSelections, setOriginalSelections] = React.useState<string[]>(
|
const [originalSelections, setOriginalSelections] = React.useState<string[]>(
|
||||||
|
@ -181,7 +184,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
||||||
return (
|
return (
|
||||||
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
||||||
<Text style={[styles.title, pal.text]}>
|
<Text style={[styles.title, pal.text]}>
|
||||||
Update {displayName} in Lists
|
<Trans>Update {displayName} in Lists</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ListsList
|
<ListsList
|
||||||
listsList={listsList}
|
listsList={listsList}
|
||||||
|
@ -195,7 +198,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
||||||
type="default"
|
type="default"
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
style={styles.footerBtn}
|
style={styles.footerBtn}
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel={_(msg`Cancel`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={onPressCancel}
|
onAccessibilityEscape={onPressCancel}
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
|
@ -206,7 +209,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={onPressSave}
|
onPress={onPressSave}
|
||||||
style={styles.footerBtn}
|
style={styles.footerBtn}
|
||||||
accessibilityLabel="Save changes"
|
accessibilityLabel={_(msg`Save changes`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={onPressSave}
|
onAccessibilityEscape={onPressSave}
|
||||||
label="Save Changes"
|
label="Save Changes"
|
||||||
|
|
|
@ -20,6 +20,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['90%']
|
export const snapPoints = ['90%']
|
||||||
|
@ -37,6 +39,7 @@ export const Component = observer(function Component({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const [stage, setStage] = useState<Stages>(
|
const [stage, setStage] = useState<Stages>(
|
||||||
showReminder ? Stages.Reminder : Stages.Email,
|
showReminder ? Stages.Reminder : Stages.Email,
|
||||||
)
|
)
|
||||||
|
@ -98,21 +101,21 @@ export const Component = observer(function Component({
|
||||||
|
|
||||||
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
|
||||||
{stage === Stages.Reminder ? (
|
{stage === Stages.Reminder ? (
|
||||||
<>
|
<Trans>
|
||||||
Your email has not yet been verified. This is an important
|
Your email has not yet been verified. This is an important
|
||||||
security step which we recommend.
|
security step which we recommend.
|
||||||
</>
|
</Trans>
|
||||||
) : stage === Stages.Email ? (
|
) : stage === Stages.Email ? (
|
||||||
<>
|
<Trans>
|
||||||
This is important in case you ever need to change your email or
|
This is important in case you ever need to change your email or
|
||||||
reset your password.
|
reset your password.
|
||||||
</>
|
</Trans>
|
||||||
) : stage === Stages.ConfirmCode ? (
|
) : stage === Stages.ConfirmCode ? (
|
||||||
<>
|
<Trans>
|
||||||
An email has been sent to{' '}
|
An email has been sent to{' '}
|
||||||
{store.session.currentSession?.email || ''}. It includes a
|
{store.session.currentSession?.email || ''}. It includes a
|
||||||
confirmation code which you can enter below.
|
confirmation code which you can enter below.
|
||||||
</>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
|
@ -132,7 +135,7 @@ export const Component = observer(function Component({
|
||||||
</View>
|
</View>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
accessibilityLabel="Change my email"
|
accessibilityLabel={_(msg`Change my email`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={onEmailIncorrect}
|
onPress={onEmailIncorrect}
|
||||||
style={styles.changeEmailLink}>
|
style={styles.changeEmailLink}>
|
||||||
|
@ -150,7 +153,7 @@ export const Component = observer(function Component({
|
||||||
value={confirmationCode}
|
value={confirmationCode}
|
||||||
onChangeText={setConfirmationCode}
|
onChangeText={setConfirmationCode}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Confirmation code"
|
accessibilityLabel={_(msg`Confirmation code`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
@ -174,7 +177,7 @@ export const Component = observer(function Component({
|
||||||
testID="getStartedBtn"
|
testID="getStartedBtn"
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={() => setStage(Stages.Email)}
|
onPress={() => setStage(Stages.Email)}
|
||||||
accessibilityLabel="Get Started"
|
accessibilityLabel={_(msg`Get Started`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Get Started"
|
label="Get Started"
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
|
@ -187,7 +190,7 @@ export const Component = observer(function Component({
|
||||||
testID="sendEmailBtn"
|
testID="sendEmailBtn"
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={onSendEmail}
|
onPress={onSendEmail}
|
||||||
accessibilityLabel="Send Confirmation Email"
|
accessibilityLabel={_(msg`Send Confirmation Email`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Send Confirmation Email"
|
label="Send Confirmation Email"
|
||||||
labelContainerStyle={{
|
labelContainerStyle={{
|
||||||
|
@ -199,7 +202,7 @@ export const Component = observer(function Component({
|
||||||
<Button
|
<Button
|
||||||
testID="haveCodeBtn"
|
testID="haveCodeBtn"
|
||||||
type="default"
|
type="default"
|
||||||
accessibilityLabel="I have a code"
|
accessibilityLabel={_(msg`I have a code`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="I have a confirmation code"
|
label="I have a confirmation code"
|
||||||
labelContainerStyle={{
|
labelContainerStyle={{
|
||||||
|
@ -216,7 +219,7 @@ export const Component = observer(function Component({
|
||||||
testID="confirmBtn"
|
testID="confirmBtn"
|
||||||
type="primary"
|
type="primary"
|
||||||
onPress={onConfirm}
|
onPress={onConfirm}
|
||||||
accessibilityLabel="Confirm"
|
accessibilityLabel={_(msg`Confirm`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
label="Confirm"
|
label="Confirm"
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export const snapPoints = ['80%']
|
export const snapPoints = ['80%']
|
||||||
|
@ -24,6 +26,7 @@ export const snapPoints = ['80%']
|
||||||
export function Component({}: {}) {
|
export function Component({}: {}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const [email, setEmail] = React.useState<string>('')
|
const [email, setEmail] = React.useState<string>('')
|
||||||
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
||||||
|
@ -61,12 +64,14 @@ export function Component({}: {}) {
|
||||||
<View style={[styles.container, pal.view]}>
|
<View style={[styles.container, pal.view]}>
|
||||||
<View style={[styles.innerContainer, pal.view]}>
|
<View style={[styles.innerContainer, pal.view]}>
|
||||||
<Text type="title-xl" style={[styles.title, pal.text]}>
|
<Text type="title-xl" style={[styles.title, pal.text]}>
|
||||||
Join the waitlist
|
<Trans>Join the waitlist</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="lg" style={[styles.description, pal.text]}>
|
<Text type="lg" style={[styles.description, pal.text]}>
|
||||||
Bluesky uses invites to build a healthier community. If you don't know
|
<Trans>
|
||||||
anybody with an invite, you can sign up for the waitlist and we'll
|
Bluesky uses invites to build a healthier community. If you don't
|
||||||
send one soon.
|
know anybody with an invite, you can sign up for the waitlist and
|
||||||
|
we'll send one soon.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
|
style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
|
||||||
|
@ -80,7 +85,7 @@ export function Component({}: {}) {
|
||||||
onSubmitEditing={onPressSignup}
|
onSubmitEditing={onPressSignup}
|
||||||
enterKeyHint="done"
|
enterKeyHint="done"
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel="Email"
|
accessibilityLabel={_(msg`Email`)}
|
||||||
accessibilityHint="Input your email to get on the Bluesky waitlist"
|
accessibilityHint="Input your email to get on the Bluesky waitlist"
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
|
@ -99,7 +104,9 @@ export function Component({}: {}) {
|
||||||
style={pal.text as FontAwesomeIconStyle}
|
style={pal.text as FontAwesomeIconStyle}
|
||||||
/>
|
/>
|
||||||
<Text style={[s.ml10, pal.text]}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
@ -114,7 +121,7 @@ export function Component({}: {}) {
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
style={[styles.btn]}>
|
||||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||||
Join Waitlist
|
<Trans>Join Waitlist</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -122,11 +129,11 @@ export function Component({}: {}) {
|
||||||
style={[styles.btn, s.mt10]}
|
style={[styles.btn, s.mt10]}
|
||||||
onPress={onCancel}
|
onPress={onCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel waitlist signup"
|
accessibilityLabel={_(msg`Cancel waitlist signup`)}
|
||||||
accessibilityHint={`Exits signing up for waitlist with ${email}`}
|
accessibilityHint={`Exits signing up for waitlist with ${email}`}
|
||||||
onAccessibilityEscape={onCancel}>
|
onAccessibilityEscape={onCancel}>
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
<Text type="button-lg" style={pal.textLight}>
|
||||||
Cancel
|
<Trans>Cancel</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {s, gradients} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
|
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
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'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
enum AspectRatio {
|
enum AspectRatio {
|
||||||
|
@ -35,6 +37,7 @@ export function Component({
|
||||||
}) {
|
}) {
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
|
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
|
||||||
const [scale, setScale] = React.useState<number>(1)
|
const [scale, setScale] = React.useState<number>(1)
|
||||||
const editorRef = React.useRef<ImageEditor>(null)
|
const editorRef = React.useRef<ImageEditor>(null)
|
||||||
|
@ -96,7 +99,7 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={doSetAs(AspectRatio.Wide)}
|
onPress={doSetAs(AspectRatio.Wide)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Wide"
|
accessibilityLabel={_(msg`Wide`)}
|
||||||
accessibilityHint="Sets image aspect ratio to wide">
|
accessibilityHint="Sets image aspect ratio to wide">
|
||||||
<RectWideIcon
|
<RectWideIcon
|
||||||
size={24}
|
size={24}
|
||||||
|
@ -106,7 +109,7 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={doSetAs(AspectRatio.Tall)}
|
onPress={doSetAs(AspectRatio.Tall)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Tall"
|
accessibilityLabel={_(msg`Tall`)}
|
||||||
accessibilityHint="Sets image aspect ratio to tall">
|
accessibilityHint="Sets image aspect ratio to tall">
|
||||||
<RectTallIcon
|
<RectTallIcon
|
||||||
size={24}
|
size={24}
|
||||||
|
@ -116,7 +119,7 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={doSetAs(AspectRatio.Square)}
|
onPress={doSetAs(AspectRatio.Square)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Square"
|
accessibilityLabel={_(msg`Square`)}
|
||||||
accessibilityHint="Sets image aspect ratio to square">
|
accessibilityHint="Sets image aspect ratio to square">
|
||||||
<SquareIcon
|
<SquareIcon
|
||||||
size={24}
|
size={24}
|
||||||
|
@ -128,7 +131,7 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressCancel}
|
onPress={onPressCancel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel image crop"
|
accessibilityLabel={_(msg`Cancel image crop`)}
|
||||||
accessibilityHint="Exits image cropping process">
|
accessibilityHint="Exits image cropping process">
|
||||||
<Text type="xl" style={pal.link}>
|
<Text type="xl" style={pal.link}>
|
||||||
Cancel
|
Cancel
|
||||||
|
@ -138,7 +141,7 @@ export function Component({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressDone}
|
onPress={onPressDone}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Save image crop"
|
accessibilityLabel={_(msg`Save image crop`)}
|
||||||
accessibilityHint="Saves image crop settings">
|
accessibilityHint="Saves image crop settings">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
|
@ -146,7 +149,7 @@ export function Component({
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
style={[styles.btn]}>
|
||||||
<Text type="xl-medium" style={s.white}>
|
<Text type="xl-medium" style={s.white}>
|
||||||
Done
|
<Trans>Done</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
@ -4,6 +4,8 @@ import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {s, colors, gradients} from 'lib/styles'
|
import {s, colors, gradients} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export const ConfirmLanguagesButton = ({
|
export const ConfirmLanguagesButton = ({
|
||||||
onPress,
|
onPress,
|
||||||
|
@ -13,6 +15,7 @@ export const ConfirmLanguagesButton = ({
|
||||||
extraText?: string
|
extraText?: string
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
@ -28,14 +31,16 @@ export const ConfirmLanguagesButton = ({
|
||||||
testID="confirmContentLanguagesBtn"
|
testID="confirmContentLanguagesBtn"
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Confirm content language settings"
|
accessibilityLabel={_(msg`Confirm content language settings`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
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>
|
</LinearGradient>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {deviceLocales} from 'platform/detection'
|
||||||
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
|
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
|
||||||
import {LanguageToggle} from './LanguageToggle'
|
import {LanguageToggle} from './LanguageToggle'
|
||||||
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
|
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {
|
import {
|
||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
|
@ -69,12 +70,16 @@ export function Component({}: {}) {
|
||||||
maxHeight: '90vh',
|
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]}>
|
<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>
|
||||||
<Text style={[pal.textLight, styles.description]}>
|
<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>
|
</Text>
|
||||||
<ScrollView style={styles.scrollContainer}>
|
<ScrollView style={styles.scrollContainer}>
|
||||||
{languages.map(lang => (
|
{languages.map(lang => (
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {deviceLocales} from 'platform/detection'
|
||||||
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
|
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
|
||||||
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
|
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
|
||||||
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {
|
import {
|
||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
|
@ -71,9 +72,11 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
|
||||||
maxHeight: '90vh',
|
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]}>
|
<Text style={[pal.text, styles.description]}>
|
||||||
Which languages are used in this post?
|
<Trans>Which languages are used in this post?</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView style={styles.scrollContainer}>
|
<ScrollView style={styles.scrollContainer}>
|
||||||
{languages.map(lang => {
|
{languages.map(lang => {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {SendReportButton} from './SendReportButton'
|
import {SendReportButton} from './SendReportButton'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export function InputIssueDetails({
|
export function InputIssueDetails({
|
||||||
details,
|
details,
|
||||||
|
@ -23,6 +25,7 @@ export function InputIssueDetails({
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -35,14 +38,16 @@ export function InputIssueDetails({
|
||||||
style={[s.mb10, styles.backBtn]}
|
style={[s.mb10, styles.backBtn]}
|
||||||
onPress={goBack}
|
onPress={goBack}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Add details"
|
accessibilityLabel={_(msg`Add details`)}
|
||||||
accessibilityHint="Add more details to your report">
|
accessibilityHint="Add more details to your report">
|
||||||
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
|
<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>
|
</TouchableOpacity>
|
||||||
<View style={[pal.btn, styles.detailsInputContainer]}>
|
<View style={[pal.btn, styles.detailsInputContainer]}>
|
||||||
<TextInput
|
<TextInput
|
||||||
accessibilityLabel="Text input field"
|
accessibilityLabel={_(msg`Text input field`)}
|
||||||
accessibilityHint="Enter a reason for reporting this post."
|
accessibilityHint="Enter a reason for reporting this post."
|
||||||
placeholder="Enter a reason or any other details here."
|
placeholder="Enter a reason or any other details here."
|
||||||
placeholderTextColor={pal.textLight.color}
|
placeholderTextColor={pal.textLight.color}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {SendReportButton} from './SendReportButton'
|
||||||
import {InputIssueDetails} from './InputIssueDetails'
|
import {InputIssueDetails} from './InputIssueDetails'
|
||||||
import {ReportReasonOptions} from './ReasonOptions'
|
import {ReportReasonOptions} from './ReasonOptions'
|
||||||
import {CollectionId} from './types'
|
import {CollectionId} from './types'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
|
const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
|
||||||
|
@ -148,6 +150,7 @@ const SelectIssue = ({
|
||||||
atUri: AtUri | null
|
atUri: AtUri | null
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const collectionName = getCollectionNameForReport(atUri)
|
const collectionName = getCollectionNameForReport(atUri)
|
||||||
const onSelectIssue = (v: string) => setIssue(v)
|
const onSelectIssue = (v: string) => setIssue(v)
|
||||||
const goToDetails = () => {
|
const goToDetails = () => {
|
||||||
|
@ -160,9 +163,11 @@ const SelectIssue = ({
|
||||||
|
|
||||||
return (
|
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]}>
|
<Text style={[pal.textLight, styles.description]}>
|
||||||
What is the issue with this {collectionName}?
|
<Trans>What is the issue with this {collectionName}?</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{marginBottom: 10}}>
|
<View style={{marginBottom: 10}}>
|
||||||
<ReportReasonOptions
|
<ReportReasonOptions
|
||||||
|
@ -184,9 +189,11 @@ const SelectIssue = ({
|
||||||
style={styles.addDetailsBtn}
|
style={styles.addDetailsBtn}
|
||||||
onPress={goToDetails}
|
onPress={goToDetails}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Add details"
|
accessibilityLabel={_(msg`Add details`)}
|
||||||
accessibilityHint="Add more details to your report">
|
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>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {Text} from '../../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
import {s, gradients, colors} from 'lib/styles'
|
import {s, gradients, colors} from 'lib/styles'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export function SendReportButton({
|
export function SendReportButton({
|
||||||
onPress,
|
onPress,
|
||||||
|
@ -16,6 +18,7 @@ export function SendReportButton({
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
// loading state
|
// loading state
|
||||||
// =
|
// =
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
|
@ -31,14 +34,16 @@ export function SendReportButton({
|
||||||
style={s.mt10}
|
style={s.mt10}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Report post"
|
accessibilityLabel={_(msg`Report post`)}
|
||||||
accessibilityHint={`Reports post with reason and details`}>
|
accessibilityHint={`Reports post with reason and details`}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[styles.btn]}>
|
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>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
|
|
|
@ -40,6 +40,8 @@ import {formatCount} from '../util/numeric/format'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {TimeElapsed} from '../util/TimeElapsed'
|
import {TimeElapsed} from '../util/TimeElapsed'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
const MAX_AUTHORS = 5
|
const MAX_AUTHORS = 5
|
||||||
|
|
||||||
|
@ -232,7 +234,9 @@ export const FeedItem = observer(function FeedItemImpl({
|
||||||
/>
|
/>
|
||||||
{authors.length > 1 ? (
|
{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]}>
|
<Text style={[pal.text, s.bold]}>
|
||||||
{formatCount(authors.length - 1)}{' '}
|
{formatCount(authors.length - 1)}{' '}
|
||||||
{pluralize(authors.length - 1, 'other')}
|
{pluralize(authors.length - 1, 'other')}
|
||||||
|
@ -292,6 +296,8 @@ function CondensedAuthorsList({
|
||||||
onToggleAuthorsExpanded: () => void
|
onToggleAuthorsExpanded: () => void
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.avis}>
|
<View style={styles.avis}>
|
||||||
|
@ -299,7 +305,7 @@ function CondensedAuthorsList({
|
||||||
style={styles.expandedAuthorsCloseBtn}
|
style={styles.expandedAuthorsCloseBtn}
|
||||||
onPress={onToggleAuthorsExpanded}
|
onPress={onToggleAuthorsExpanded}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Hide user list"
|
accessibilityLabel={_(msg`Hide user list`)}
|
||||||
accessibilityHint="Collapses list of users for a given notification">
|
accessibilityHint="Collapses list of users for a given notification">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-up"
|
icon="angle-up"
|
||||||
|
@ -307,7 +313,7 @@ function CondensedAuthorsList({
|
||||||
style={[styles.expandedAuthorsCloseBtnIcon, pal.text]}
|
style={[styles.expandedAuthorsCloseBtnIcon, pal.text]}
|
||||||
/>
|
/>
|
||||||
<Text type="sm-medium" style={pal.text}>
|
<Text type="sm-medium" style={pal.text}>
|
||||||
Hide
|
<Trans>Hide</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
@ -328,7 +334,7 @@ function CondensedAuthorsList({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityLabel="Show users"
|
accessibilityLabel={_(msg`Show users`)}
|
||||||
accessibilityHint="Opens an expanded list of users in this notification"
|
accessibilityHint="Opens an expanded list of users in this notification"
|
||||||
onPress={onToggleAuthorsExpanded}>
|
onPress={onToggleAuthorsExpanded}>
|
||||||
<View style={styles.avis}>
|
<View style={styles.avis}>
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
import Animated from 'react-native-reanimated'
|
import Animated from 'react-native-reanimated'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||||
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
||||||
|
|
||||||
|
@ -22,6 +24,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
||||||
) {
|
) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const setDrawerOpen = useSetDrawerOpen()
|
const setDrawerOpen = useSetDrawerOpen()
|
||||||
const items = useHomeTabs(store.preferences.pinnedFeeds)
|
const items = useHomeTabs(store.preferences.pinnedFeeds)
|
||||||
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
|
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
|
||||||
|
@ -45,7 +48,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
||||||
testID="viewHeaderDrawerBtn"
|
testID="viewHeaderDrawerBtn"
|
||||||
onPress={onPressAvi}
|
onPress={onPressAvi}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Open navigation"
|
accessibilityLabel={_(msg`Open navigation`)}
|
||||||
accessibilityHint="Access profile and other navigation links"
|
accessibilityHint="Access profile and other navigation links"
|
||||||
hitSlop={HITSLOP_10}>
|
hitSlop={HITSLOP_10}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -64,7 +67,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
||||||
href="/settings/home-feed"
|
href="/settings/home-feed"
|
||||||
hitSlop={HITSLOP_10}
|
hitSlop={HITSLOP_10}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Home Feed Preferences"
|
accessibilityLabel={_(msg`Home Feed Preferences`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="sliders"
|
icon="sliders"
|
||||||
|
|
|
@ -31,6 +31,8 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2}
|
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2}
|
||||||
|
|
||||||
|
@ -79,6 +81,7 @@ export const PostThread = observer(function PostThread({
|
||||||
treeView: boolean
|
treeView: boolean
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isTablet, isDesktop} = useWebMediaQueries()
|
const {isTablet, isDesktop} = useWebMediaQueries()
|
||||||
const ref = useRef<FlatList>(null)
|
const ref = useRef<FlatList>(null)
|
||||||
const hasScrolledIntoView = useRef<boolean>(false)
|
const hasScrolledIntoView = useRef<boolean>(false)
|
||||||
|
@ -197,7 +200,7 @@ export const PostThread = observer(function PostThread({
|
||||||
return (
|
return (
|
||||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||||
<Text type="lg-bold" style={pal.textLight}>
|
<Text type="lg-bold" style={pal.textLight}>
|
||||||
Deleted post.
|
<Trans>Deleted post.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -205,7 +208,7 @@ export const PostThread = observer(function PostThread({
|
||||||
return (
|
return (
|
||||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||||
<Text type="lg-bold" style={pal.textLight}>
|
<Text type="lg-bold" style={pal.textLight}>
|
||||||
Blocked post.
|
<Trans>Blocked post.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -214,7 +217,7 @@ export const PostThread = observer(function PostThread({
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => setMaxVisible(n => n + 50)}
|
onPress={() => setMaxVisible(n => n + 50)}
|
||||||
style={[pal.border, pal.view, styles.itemContainer]}
|
style={[pal.border, pal.view, styles.itemContainer]}
|
||||||
accessibilityLabel="Load more posts"
|
accessibilityLabel={_(msg`Load more posts`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -222,7 +225,7 @@ export const PostThread = observer(function PostThread({
|
||||||
{paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
|
{paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
|
||||||
]}>
|
]}>
|
||||||
<Text type="lg-medium" style={pal.text}>
|
<Text type="lg-medium" style={pal.text}>
|
||||||
Load more posts
|
<Trans>Load more posts</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
@ -275,6 +278,7 @@ export const PostThread = observer(function PostThread({
|
||||||
posts,
|
posts,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
treeView,
|
treeView,
|
||||||
|
_,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -302,15 +306,15 @@ export const PostThread = observer(function PostThread({
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||||
<Text type="title-lg" style={[pal.text, s.mb5]}>
|
<Text type="title-lg" style={[pal.text, s.mb5]}>
|
||||||
Post not found
|
<Trans>Post not found</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="md" style={[pal.text, s.mb10]}>
|
<Text type="md" style={[pal.text, s.mb10]}>
|
||||||
The post may have been deleted.
|
<Trans>The post may have been deleted.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Back"
|
accessibilityLabel={_(msg`Back`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text type="2xl" style={pal.link}>
|
<Text type="2xl" style={pal.link}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -318,7 +322,7 @@ export const PostThread = observer(function PostThread({
|
||||||
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
|
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
|
||||||
size={14}
|
size={14}
|
||||||
/>
|
/>
|
||||||
Back
|
<Trans>Back</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
@ -336,15 +340,18 @@ export const PostThread = observer(function PostThread({
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||||
<Text type="title-lg" style={[pal.text, s.mb5]}>
|
<Text type="title-lg" style={[pal.text, s.mb5]}>
|
||||||
Post hidden
|
<Trans>Post hidden</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="md" style={[pal.text, s.mb10]}>
|
<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>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Back"
|
accessibilityLabel={_(msg`Back`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text type="2xl" style={pal.link}>
|
<Text type="2xl" style={pal.link}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -352,7 +359,7 @@ export const PostThread = observer(function PostThread({
|
||||||
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
|
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
|
||||||
size={14}
|
size={14}
|
||||||
/>
|
/>
|
||||||
Back
|
<Trans>Back</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {MAX_POST_LINES} from 'lib/constants'
|
import {MAX_POST_LINES} from 'lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
|
|
||||||
|
@ -176,7 +177,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
icon={['far', 'trash-can']}
|
icon={['far', 'trash-can']}
|
||||||
style={pal.icon as FontAwesomeIconStyle}
|
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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -650,9 +653,11 @@ function ExpandedPostDetails({
|
||||||
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
||||||
{needsTranslation && (
|
{needsTranslation && (
|
||||||
<>
|
<>
|
||||||
<Text style={pal.textLight}> • </Text>
|
<Text style={[pal.textLight, s.ml5, s.mr5]}>•</Text>
|
||||||
<Link href={translatorUrl} title="Translate">
|
<Link href={translatorUrl} title="Translate">
|
||||||
<Text style={pal.link}>Translate</Text>
|
<Text style={pal.link}>
|
||||||
|
<Trans>Translate</Trans>
|
||||||
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -40,6 +40,8 @@ import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -114,6 +116,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
@ -369,10 +372,10 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
onPress={onPressEditProfile}
|
onPress={onPressEditProfile}
|
||||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Edit profile"
|
accessibilityLabel={_(msg`Edit profile`)}
|
||||||
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
|
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
|
||||||
<Text type="button" style={pal.text}>
|
<Text type="button" style={pal.text}>
|
||||||
Edit Profile
|
<Trans>Edit Profile</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : view.viewer.blocking ? (
|
) : view.viewer.blocking ? (
|
||||||
|
@ -382,10 +385,10 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
onPress={onPressUnblockAccount}
|
onPress={onPressUnblockAccount}
|
||||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Unblock"
|
accessibilityLabel={_(msg`Unblock`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text type="button" style={[pal.text, s.bold]}>
|
<Text type="button" style={[pal.text, s.bold]}>
|
||||||
Unblock
|
<Trans>Unblock</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
|
@ -439,7 +442,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
size={14}
|
size={14}
|
||||||
/>
|
/>
|
||||||
<Text type="button" style={pal.text}>
|
<Text type="button" style={pal.text}>
|
||||||
Following
|
<Trans>Following</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
|
@ -455,7 +458,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
style={[palInverted.text, s.mr5]}
|
style={[palInverted.text, s.mr5]}
|
||||||
/>
|
/>
|
||||||
<Text type="button" style={[palInverted.text, s.bold]}>
|
<Text type="button" style={[palInverted.text, s.bold]}>
|
||||||
Follow
|
<Trans>Follow</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
@ -465,7 +468,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
testID="profileHeaderDropdownBtn"
|
testID="profileHeaderDropdownBtn"
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
accessibilityLabel="More options"
|
accessibilityLabel={_(msg`More options`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
||||||
<FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
|
<FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
|
||||||
|
@ -488,7 +491,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
{view.viewer.followedBy && !blockHide ? (
|
{view.viewer.followedBy && !blockHide ? (
|
||||||
<View style={[styles.pill, pal.btn, s.mr5]}>
|
<View style={[styles.pill, pal.btn, s.mr5]}>
|
||||||
<Text type="xs" style={[pal.text]}>
|
<Text type="xs" style={[pal.text]}>
|
||||||
Follows you
|
<Trans>Follows you</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
@ -533,7 +536,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
{following}{' '}
|
{following}{' '}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="md" style={[pal.textLight]}>
|
<Text type="md" style={[pal.textLight]}>
|
||||||
following
|
<Trans>following</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
<Text type="md" style={[s.bold, pal.text]}>
|
<Text type="md" style={[s.bold, pal.text]}>
|
||||||
|
@ -572,7 +575,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
hitSlop={BACK_HITSLOP}
|
hitSlop={BACK_HITSLOP}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Back"
|
accessibilityLabel={_(msg`Back`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View style={styles.backBtnWrapper}>
|
<View style={styles.backBtnWrapper}>
|
||||||
<BlurView style={styles.backBtn} blurType="dark">
|
<BlurView style={styles.backBtn} blurType="dark">
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {NavigationProp} from 'lib/routes/types'
|
||||||
import {BACK_HITSLOP} from 'lib/constants'
|
import {BACK_HITSLOP} from 'lib/constants'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
import {ImagesLightbox} from 'state/models/ui/shell'
|
import {ImagesLightbox} from 'state/models/ui/shell'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
|
|
||||||
export const ProfileSubpageHeader = observer(function HeaderImpl({
|
export const ProfileSubpageHeader = observer(function HeaderImpl({
|
||||||
|
@ -45,6 +47,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const setDrawerOpen = useSetDrawerOpen()
|
const setDrawerOpen = useSetDrawerOpen()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const {_} = useLingui()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const canGoBack = navigation.canGoBack()
|
const canGoBack = navigation.canGoBack()
|
||||||
|
@ -123,7 +126,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
|
||||||
testID="headerAviButton"
|
testID="headerAviButton"
|
||||||
onPress={onPressAvi}
|
onPress={onPressAvi}
|
||||||
accessibilityRole="image"
|
accessibilityRole="image"
|
||||||
accessibilityLabel="View the avatar"
|
accessibilityLabel={_(msg`View the avatar`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
style={{width: 58}}>
|
style={{width: 58}}>
|
||||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -36,6 +38,7 @@ export function HeaderWithInput({
|
||||||
const setDrawerOpen = useSetDrawerOpen()
|
const setDrawerOpen = useSetDrawerOpen()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const textInput = React.useRef<TextInput>(null)
|
const textInput = React.useRef<TextInput>(null)
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
@ -65,7 +68,7 @@ export function HeaderWithInput({
|
||||||
hitSlop={HITSLOP_10}
|
hitSlop={HITSLOP_10}
|
||||||
style={styles.headerMenuBtn}
|
style={styles.headerMenuBtn}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Menu"
|
accessibilityLabel={_(msg`Menu`)}
|
||||||
accessibilityHint="Access navigation links and settings">
|
accessibilityHint="Access navigation links and settings">
|
||||||
<FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} />
|
<FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -95,7 +98,7 @@ export function HeaderWithInput({
|
||||||
onSubmitEditing={onSubmitQuery}
|
onSubmitEditing={onSubmitQuery}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
accessibilityRole="search"
|
accessibilityRole="search"
|
||||||
accessibilityLabel="Search"
|
accessibilityLabel={_(msg`Search`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
@ -105,7 +108,7 @@ export function HeaderWithInput({
|
||||||
testID="searchTextInputClearBtn"
|
testID="searchTextInputClearBtn"
|
||||||
onPress={onPressClearQuery}
|
onPress={onPressClearQuery}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Clear search query"
|
accessibilityLabel={_(msg`Clear search query`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="xmark"
|
icon="xmark"
|
||||||
|
@ -120,7 +123,9 @@ export function HeaderWithInput({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressCancelSearchInner}
|
onPress={onPressCancelSearchInner}
|
||||||
accessibilityRole="button">
|
accessibilityRole="button">
|
||||||
<Text style={pal.text}>Cancel</Text>
|
<Text style={pal.text}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
|
@ -9,10 +9,14 @@ import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||||
import * as Toast from '../../com/util/Toast'
|
import * as Toast from '../../com/util/Toast'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
export function AccountDropdownBtn({handle}: {handle: string}) {
|
export function AccountDropdownBtn({handle}: {handle: string}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
const items: DropdownItem[] = [
|
const items: DropdownItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Remove account',
|
label: 'Remove account',
|
||||||
|
@ -34,7 +38,7 @@ export function AccountDropdownBtn({handle}: {handle: string}) {
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
testID="accountSettingsDropdownBtn"
|
testID="accountSettingsDropdownBtn"
|
||||||
items={items}
|
items={items}
|
||||||
accessibilityLabel="Account options"
|
accessibilityLabel={_(msg`Account options`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="ellipsis-h"
|
icon="ellipsis-h"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Animated, {
|
||||||
interpolate,
|
interpolate,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
import {t} from '@lingui/macro'
|
||||||
|
|
||||||
export function createCustomBackdrop(
|
export function createCustomBackdrop(
|
||||||
onClose?: (() => void) | undefined,
|
onClose?: (() => void) | undefined,
|
||||||
|
@ -29,7 +30,7 @@ export function createCustomBackdrop(
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
accessibilityLabel="Close bottom drawer"
|
accessibilityLabel={t`Close bottom drawer`}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onAccessibilityEscape={() => {
|
onAccessibilityEscape={() => {
|
||||||
if (onClose !== undefined) {
|
if (onClose !== undefined) {
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {isWeb, isAndroid} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {UserPreviewLink} from './UserPreviewLink'
|
import {UserPreviewLink} from './UserPreviewLink'
|
||||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
export type UserAvatarType = 'user' | 'algo' | 'list'
|
export type UserAvatarType = 'user' | 'algo' | 'list'
|
||||||
|
|
||||||
|
@ -184,6 +186,7 @@ export function EditableUserAvatar({
|
||||||
}: EditableUserAvatarProps) {
|
}: EditableUserAvatarProps) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
|
||||||
|
@ -294,7 +297,7 @@ export function EditableUserAvatar({
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
testID="changeAvatarBtn"
|
testID="changeAvatarBtn"
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
accessibilityLabel="Image options"
|
accessibilityLabel={_(msg`Image options`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
{avatar ? (
|
{avatar ? (
|
||||||
<HighPriorityImage
|
<HighPriorityImage
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
export function UserBanner({
|
export function UserBanner({
|
||||||
banner,
|
banner,
|
||||||
|
@ -26,6 +28,7 @@ export function UserBanner({
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
|
||||||
|
@ -112,7 +115,7 @@ export function UserBanner({
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
testID="changeBannerBtn"
|
testID="changeBannerBtn"
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
accessibilityLabel="Image options"
|
accessibilityLabel={_(msg`Image options`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
{banner ? (
|
{banner ? (
|
||||||
<Image
|
<Image
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
export function ErrorMessage({
|
export function ErrorMessage({
|
||||||
message,
|
message,
|
||||||
|
@ -27,6 +29,7 @@ export function ErrorMessage({
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('error')
|
const pal = usePalette('error')
|
||||||
|
const {_} = useLingui()
|
||||||
return (
|
return (
|
||||||
<View testID="errorMessageView" style={[styles.outer, pal.view, style]}>
|
<View testID="errorMessageView" style={[styles.outer, pal.view, style]}>
|
||||||
<View
|
<View
|
||||||
|
@ -49,7 +52,7 @@ export function ErrorMessage({
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={onPressTryAgain}
|
onPress={onPressTryAgain}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Retry"
|
accessibilityLabel={_(msg`Retry`)}
|
||||||
accessibilityHint="Retries the last action, which errored out">
|
accessibilityHint="Retries the last action, which errored out">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrows-rotate"
|
icon="arrows-rotate"
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {useTheme} from 'lib/ThemeContext'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {Button} from '../forms/Button'
|
import {Button} from '../forms/Button'
|
||||||
import {CenteredView} from '../Views'
|
import {CenteredView} from '../Views'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export function ErrorScreen({
|
export function ErrorScreen({
|
||||||
title,
|
title,
|
||||||
|
@ -25,6 +27,8 @@ export function ErrorScreen({
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
|
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
|
||||||
<View style={styles.errorIconContainer}>
|
<View style={styles.errorIconContainer}>
|
||||||
|
@ -58,7 +62,7 @@ export function ErrorScreen({
|
||||||
type="default"
|
type="default"
|
||||||
style={[styles.btn]}
|
style={[styles.btn]}
|
||||||
onPress={onPressTryAgain}
|
onPress={onPressTryAgain}
|
||||||
accessibilityLabel="Retry"
|
accessibilityLabel={_(msg`Retry`)}
|
||||||
accessibilityHint="Retries the last action, which errored out">
|
accessibilityHint="Retries the last action, which errored out">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrows-rotate"
|
icon="arrows-rotate"
|
||||||
|
@ -66,7 +70,7 @@ export function ErrorScreen({
|
||||||
size={16}
|
size={16}
|
||||||
/>
|
/>
|
||||||
<Text type="button" style={[styles.btnText, pal.link]}>
|
<Text type="button" style={[styles.btnText, pal.link]}>
|
||||||
Try again
|
<Trans>Try again</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {colors} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
const ESTIMATED_BTN_HEIGHT = 50
|
const ESTIMATED_BTN_HEIGHT = 50
|
||||||
const ESTIMATED_SEP_HEIGHT = 16
|
const ESTIMATED_SEP_HEIGHT = 16
|
||||||
|
@ -207,6 +209,7 @@ const DropdownItems = ({
|
||||||
}: DropDownItemProps) => {
|
}: DropDownItemProps) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const dropDownBackgroundColor =
|
const dropDownBackgroundColor =
|
||||||
theme.colorScheme === 'dark' ? pal.btn : pal.view
|
theme.colorScheme === 'dark' ? pal.btn : pal.view
|
||||||
const separatorColor =
|
const separatorColor =
|
||||||
|
@ -224,7 +227,7 @@ const DropdownItems = ({
|
||||||
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
|
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={onOuterPress}
|
onPress={onOuterPress}
|
||||||
accessibilityLabel="Toggle dropdown"
|
accessibilityLabel={_(msg`Toggle dropdown`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View style={[styles.bg]} />
|
<View style={[styles.bg]} />
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
DropdownItem as NativeDropdownItem,
|
DropdownItem as NativeDropdownItem,
|
||||||
} from './NativeDropdown'
|
} from './NativeDropdown'
|
||||||
import {EventStopper} from '../EventStopper'
|
import {EventStopper} from '../EventStopper'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export function PostDropdownBtn({
|
export function PostDropdownBtn({
|
||||||
|
@ -38,6 +40,7 @@ export function PostDropdownBtn({
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
@ -152,7 +155,7 @@ export function PostDropdownBtn({
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
testID={testID}
|
testID={testID}
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
accessibilityLabel="More post options"
|
accessibilityLabel={_(msg`More post options`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View style={style}>
|
<View style={style}>
|
||||||
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
|
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {
|
||||||
import {MagnifyingGlassIcon} from 'lib/icons'
|
import {MagnifyingGlassIcon} from 'lib/icons'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query: string
|
query: string
|
||||||
|
@ -33,6 +35,7 @@ export function SearchInput({
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const textInput = React.useRef<TextInput>(null)
|
const textInput = React.useRef<TextInput>(null)
|
||||||
|
|
||||||
const onPressCancelSearchInner = React.useCallback(() => {
|
const onPressCancelSearchInner = React.useCallback(() => {
|
||||||
|
@ -58,7 +61,7 @@ export function SearchInput({
|
||||||
onChangeText={onChangeQuery}
|
onChangeText={onChangeQuery}
|
||||||
onSubmitEditing={onSubmitQuery}
|
onSubmitEditing={onSubmitQuery}
|
||||||
accessibilityRole="search"
|
accessibilityRole="search"
|
||||||
accessibilityLabel="Search"
|
accessibilityLabel={_(msg`Search`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
@ -67,7 +70,7 @@ export function SearchInput({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressCancelSearchInner}
|
onPress={onPressCancelSearchInner}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Clear search query"
|
accessibilityLabel={_(msg`Clear search query`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="xmark"
|
icon="xmark"
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {ModerationUI} from '@atproto/api'
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
import {ShieldExclamation} from 'lib/icons'
|
import {ShieldExclamation} from 'lib/icons'
|
||||||
import {describeModerationCause} from 'lib/moderation'
|
import {describeModerationCause} from 'lib/moderation'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export function ContentHider({
|
export function ContentHider({
|
||||||
|
@ -23,6 +25,7 @@ export function ContentHider({
|
||||||
childContainerStyle?: StyleProp<ViewStyle>
|
childContainerStyle?: StyleProp<ViewStyle>
|
||||||
}>) {
|
}>) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const [override, setOverride] = React.useState(false)
|
const [override, setOverride] = React.useState(false)
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
@ -69,7 +72,7 @@ export function ContentHider({
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Learn more about this warning"
|
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<ShieldExclamation size={18} style={pal.text} />
|
<ShieldExclamation size={18} style={pal.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {Text} from '../text/Text'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {ShieldExclamation} from 'lib/icons'
|
import {ShieldExclamation} from 'lib/icons'
|
||||||
import {describeModerationCause} from 'lib/moderation'
|
import {describeModerationCause} from 'lib/moderation'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export function PostAlerts({
|
export function PostAlerts({
|
||||||
|
@ -16,6 +18,7 @@ export function PostAlerts({
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
const shouldAlert = !!moderation.cause && moderation.alert
|
const shouldAlert = !!moderation.cause && moderation.alert
|
||||||
|
@ -34,14 +37,14 @@ export function PostAlerts({
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Learn more about this warning"
|
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
style={[styles.container, pal.viewLight, style]}>
|
style={[styles.container, pal.viewLight, style]}>
|
||||||
<ShieldExclamation style={pal.text} size={16} />
|
<ShieldExclamation style={pal.text} size={16} />
|
||||||
<Text type="lg" style={[pal.text]}>
|
<Text type="lg" style={[pal.text]}>
|
||||||
{desc.name}{' '}
|
{desc.name}{' '}
|
||||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||||
Learn More
|
<Trans>Learn More</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {Text} from '../text/Text'
|
||||||
import {addStyle} from 'lib/styles'
|
import {addStyle} from 'lib/styles'
|
||||||
import {describeModerationCause} from 'lib/moderation'
|
import {describeModerationCause} from 'lib/moderation'
|
||||||
import {ShieldExclamation} from 'lib/icons'
|
import {ShieldExclamation} from 'lib/icons'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
interface Props extends ComponentProps<typeof Link> {
|
interface Props extends ComponentProps<typeof Link> {
|
||||||
|
@ -26,6 +28,7 @@ export function PostHider({
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const [override, setOverride] = React.useState(false)
|
const [override, setOverride] = React.useState(false)
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
@ -70,7 +73,7 @@ export function PostHider({
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Learn more about this warning"
|
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<ShieldExclamation size={18} style={pal.text} />
|
<ShieldExclamation size={18} style={pal.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
describeModerationCause,
|
describeModerationCause,
|
||||||
getProfileModerationCauses,
|
getProfileModerationCauses,
|
||||||
} from 'lib/moderation'
|
} from 'lib/moderation'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
export function ProfileHeaderAlerts({
|
export function ProfileHeaderAlerts({
|
||||||
|
@ -18,6 +20,7 @@ export function ProfileHeaderAlerts({
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
const causes = getProfileModerationCauses(moderation)
|
const causes = getProfileModerationCauses(moderation)
|
||||||
|
@ -41,7 +44,7 @@ export function ProfileHeaderAlerts({
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Learn more about this warning"
|
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
style={[styles.container, pal.viewLight, style]}>
|
style={[styles.container, pal.viewLight, style]}>
|
||||||
<ShieldExclamation style={pal.text} size={24} />
|
<ShieldExclamation style={pal.text} size={24} />
|
||||||
|
@ -49,7 +52,7 @@ export function ProfileHeaderAlerts({
|
||||||
{desc.name}
|
{desc.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||||
Learn More
|
<Trans>Learn More</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,7 +18,10 @@ import {NavigationProp} from 'lib/routes/types'
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
import {Button} from '../forms/Button'
|
import {Button} from '../forms/Button'
|
||||||
import {describeModerationCause} from 'lib/moderation'
|
import {describeModerationCause} from 'lib/moderation'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {s} from '#/lib/styles'
|
||||||
|
|
||||||
export function ScreenHider({
|
export function ScreenHider({
|
||||||
testID,
|
testID,
|
||||||
|
@ -36,6 +39,7 @@ export function ScreenHider({
|
||||||
}>) {
|
}>) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
|
const {_} = useLingui()
|
||||||
const [override, setOverride] = React.useState(false)
|
const [override, setOverride] = React.useState(false)
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
@ -62,14 +66,13 @@ export function ScreenHider({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text type="title-2xl" style={[styles.title, pal.text]}>
|
<Text type="title-2xl" style={[styles.title, pal.text]}>
|
||||||
Content Warning
|
<Trans>Content Warning</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="2xl" style={[styles.description, pal.textLight]}>
|
<Text type="2xl" style={[styles.description, pal.textLight]}>
|
||||||
This {screenDescription} has been flagged:{' '}
|
<Trans>This {screenDescription} has been flagged:</Trans>
|
||||||
<Text type="2xl-medium" style={pal.text}>
|
<Text type="2xl-medium" style={[pal.text, s.ml5]}>
|
||||||
{desc.name}
|
{desc.name}.
|
||||||
</Text>
|
</Text>
|
||||||
.{' '}
|
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
openModal({
|
openModal({
|
||||||
|
@ -79,10 +82,10 @@ export function ScreenHider({
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Learn more about this warning"
|
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text type="2xl" style={pal.link}>
|
<Text type="2xl" style={pal.link}>
|
||||||
Learn More
|
<Trans>Learn More</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -99,7 +102,7 @@ export function ScreenHider({
|
||||||
}}
|
}}
|
||||||
style={styles.btn}>
|
style={styles.btn}>
|
||||||
<Text type="button-lg" style={pal.textInverted}>
|
<Text type="button-lg" style={pal.textInverted}>
|
||||||
Go back
|
<Trans>Go back</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
{!moderation.noOverride && (
|
{!moderation.noOverride && (
|
||||||
|
@ -108,7 +111,7 @@ export function ScreenHider({
|
||||||
onPress={() => setOverride(v => !v)}
|
onPress={() => setOverride(v => !v)}
|
||||||
style={styles.btn}>
|
style={styles.btn}>
|
||||||
<Text type="button-lg" style={pal.text}>
|
<Text type="button-lg" style={pal.text}>
|
||||||
Show anyway
|
<Trans>Show anyway</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
DropdownItem as NativeDropdownItem,
|
DropdownItem as NativeDropdownItem,
|
||||||
} from '../forms/NativeDropdown'
|
} from '../forms/NativeDropdown'
|
||||||
import {EventStopper} from '../EventStopper'
|
import {EventStopper} from '../EventStopper'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isReposted: boolean
|
isReposted: boolean
|
||||||
|
@ -28,6 +30,7 @@ export const RepostButton = ({
|
||||||
onQuote,
|
onQuote,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
const defaultControlColor = React.useMemo(
|
const defaultControlColor = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -63,7 +66,7 @@ export const RepostButton = ({
|
||||||
<EventStopper>
|
<EventStopper>
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
accessibilityLabel="Repost or quote post"
|
accessibilityLabel={_(msg`Repost or quote post`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
|
@ -55,8 +57,10 @@ export const AppPasswords = withAuthRequired(
|
||||||
<AppPasswordsHeader />
|
<AppPasswordsHeader />
|
||||||
<View style={[styles.empty, pal.viewLight]}>
|
<View style={[styles.empty, pal.viewLight]}>
|
||||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||||
You have not created any app passwords yet. You can create one by
|
<Trans>
|
||||||
pressing the button below.
|
You have not created any app passwords yet. You can create one
|
||||||
|
by pressing the button below.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{!isTabletOrDesktop && <View style={styles.flex1} />}
|
{!isTabletOrDesktop && <View style={styles.flex1} />}
|
||||||
|
@ -146,8 +150,10 @@ function AppPasswordsHeader() {
|
||||||
pal.text,
|
pal.text,
|
||||||
isTabletOrDesktop && styles.descriptionDesktop,
|
isTabletOrDesktop && styles.descriptionDesktop,
|
||||||
]}>
|
]}>
|
||||||
Use app passwords to login to other Bluesky clients without giving full
|
<Trans>
|
||||||
access to your account or password.
|
Use app passwords to login to other Bluesky clients without giving
|
||||||
|
full access to your account or password.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -164,6 +170,7 @@ function AppPassword({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
const {contentLanguages} = useLanguagePrefs()
|
const {contentLanguages} = useLanguagePrefs()
|
||||||
|
|
||||||
|
@ -188,7 +195,7 @@ function AppPassword({
|
||||||
style={[styles.item, pal.border]}
|
style={[styles.item, pal.border]}
|
||||||
onPress={onDelete}
|
onPress={onDelete}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Delete app password"
|
accessibilityLabel={_(msg`Delete app password`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View>
|
<View>
|
||||||
<Text type="md-bold" style={pal.text}>
|
<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 {FlatList} from 'view/com/util/Views'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
|
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
|
||||||
|
@ -34,6 +36,7 @@ export const FeedsScreen = withAuthRequired(
|
||||||
observer<Props>(function FeedsScreenImpl({}: Props) {
|
observer<Props>(function FeedsScreenImpl({}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||||
const myFeeds = store.me.myFeeds
|
const myFeeds = store.me.myFeeds
|
||||||
|
@ -88,12 +91,12 @@ export const FeedsScreen = withAuthRequired(
|
||||||
href="/settings/saved-feeds"
|
href="/settings/saved-feeds"
|
||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Edit Saved Feeds"
|
accessibilityLabel={_(msg`Edit Saved Feeds`)}
|
||||||
accessibilityHint="Opens screen to edit Saved Feeds">
|
accessibilityHint="Opens screen to edit Saved Feeds">
|
||||||
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
|
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}, [pal])
|
}, [pal, _])
|
||||||
|
|
||||||
const onRefresh = React.useCallback(() => {
|
const onRefresh = React.useCallback(() => {
|
||||||
myFeeds.refresh()
|
myFeeds.refresh()
|
||||||
|
@ -124,11 +127,11 @@ export const FeedsScreen = withAuthRequired(
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Text type="title-lg" style={[pal.text, s.bold]}>
|
<Text type="title-lg" style={[pal.text, s.bold]}>
|
||||||
My Feeds
|
<Trans>My Feeds</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Link
|
<Link
|
||||||
href="/settings/saved-feeds"
|
href="/settings/saved-feeds"
|
||||||
accessibilityLabel="Edit My Feeds"
|
accessibilityLabel={_(msg`Edit My Feeds`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
|
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -139,7 +142,7 @@ export const FeedsScreen = withAuthRequired(
|
||||||
} else if (item.type === 'saved-feeds-loading') {
|
} else if (item.type === 'saved-feeds-loading') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Array.from(Array(item.numItems)).map((_, i) => (
|
{Array.from(Array(item.numItems)).map((_i, i) => (
|
||||||
<SavedFeedLoadingPlaceholder key={`placeholder-${i}`} />
|
<SavedFeedLoadingPlaceholder key={`placeholder-${i}`} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -161,7 +164,7 @@ export const FeedsScreen = withAuthRequired(
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Text type="title-lg" style={[pal.text, s.bold]}>
|
<Text type="title-lg" style={[pal.text, s.bold]}>
|
||||||
Discover new feeds
|
<Trans>Discover new feeds</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
@ -203,14 +206,22 @@ export const FeedsScreen = withAuthRequired(
|
||||||
paddingBottom: '150%',
|
paddingBottom: '150%',
|
||||||
}}>
|
}}>
|
||||||
<Text type="lg" style={pal.textLight}>
|
<Text type="lg" style={pal.textLight}>
|
||||||
No results found for "{query}"
|
<Trans>No results found for "{query}"</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
[isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery],
|
[
|
||||||
|
isMobile,
|
||||||
|
pal,
|
||||||
|
query,
|
||||||
|
onChangeQuery,
|
||||||
|
onPressCancelSearch,
|
||||||
|
onSubmitQuery,
|
||||||
|
_,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -249,7 +260,7 @@ export const FeedsScreen = withAuthRequired(
|
||||||
onPress={onPressCompose}
|
onPress={onPressCompose}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="New post"
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -289,7 +300,7 @@ function SavedFeed({feed}: {feed: FeedSourceModel}) {
|
||||||
{feed.error ? (
|
{feed.error ? (
|
||||||
<View style={[styles.offlineSlug, pal.borderDark]}>
|
<View style={[styles.offlineSlug, pal.borderDark]}>
|
||||||
<Text type="xs" style={pal.textLight}>
|
<Text type="xs" style={pal.textLight}>
|
||||||
Feed offline
|
<Trans>Feed offline</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {Text} from '../com/util/text/Text'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {getEntries} from '#/logger/logDump'
|
import {getEntries} from '#/logger/logDump'
|
||||||
import {ago} from 'lib/strings/time'
|
import {ago} from 'lib/strings/time'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
|
||||||
export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
||||||
|
@ -18,6 +20,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
||||||
'Log'
|
'Log'
|
||||||
>) {
|
>) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const [expanded, setExpanded] = React.useState<string[]>([])
|
const [expanded, setExpanded] = React.useState<string[]>([])
|
||||||
|
|
||||||
|
@ -47,7 +50,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.entry, pal.border, pal.view]}
|
style={[styles.entry, pal.border, pal.view]}
|
||||||
onPress={toggler(entry.id)}
|
onPress={toggler(entry.id)}
|
||||||
accessibilityLabel="View debug entry"
|
accessibilityLabel={_(msg`View debug entry`)}
|
||||||
accessibilityHint="Opens additional details for a debug entry">
|
accessibilityHint="Opens additional details for a debug entry">
|
||||||
{entry.level === 'debug' ? (
|
{entry.level === 'debug' ? (
|
||||||
<FontAwesomeIcon icon="info" />
|
<FontAwesomeIcon icon="info" />
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
function RepliesThresholdInput({enabled}: {enabled: boolean}) {
|
function RepliesThresholdInput({enabled}: {enabled: boolean}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
@ -66,6 +68,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -84,7 +87,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
|
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
|
||||||
]}>
|
]}>
|
||||||
<Text type="xl" style={[pal.textLight, styles.description]}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
@ -92,10 +95,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
<View style={styles.cardsContainer}>
|
<View style={styles.cardsContainer}>
|
||||||
<View style={[pal.viewLight, styles.card]}>
|
<View style={[pal.viewLight, styles.card]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
Show Replies
|
<Trans>Show Replies</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<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>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
testID="toggleRepliesBtn"
|
testID="toggleRepliesBtn"
|
||||||
|
@ -112,10 +117,13 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
store.preferences.homeFeed.hideReplies && styles.dimmed,
|
store.preferences.homeFeed.hideReplies && styles.dimmed,
|
||||||
]}>
|
]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
Reply Filters
|
<Trans>Reply Filters</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<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>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
type="default-light"
|
type="default-light"
|
||||||
|
@ -129,8 +137,10 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
style={[s.mb10]}
|
style={[s.mb10]}
|
||||||
/>
|
/>
|
||||||
<Text style={[pal.text]}>
|
<Text style={[pal.text]}>
|
||||||
Adjust the number of likes a reply must have to be shown in your
|
<Trans>
|
||||||
feed.
|
Adjust the number of likes a reply must have to be shown in your
|
||||||
|
feed.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<RepliesThresholdInput
|
<RepliesThresholdInput
|
||||||
enabled={!store.preferences.homeFeed.hideReplies}
|
enabled={!store.preferences.homeFeed.hideReplies}
|
||||||
|
@ -139,10 +149,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
|
|
||||||
<View style={[pal.viewLight, styles.card]}>
|
<View style={[pal.viewLight, styles.card]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
Show Reposts
|
<Trans>Show Reposts</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<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>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
type="default-light"
|
type="default-light"
|
||||||
|
@ -154,11 +166,13 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
|
|
||||||
<View style={[pal.viewLight, styles.card]}>
|
<View style={[pal.viewLight, styles.card]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
Show Quote Posts
|
<Trans>Show Quote Posts</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<Text style={[pal.text, s.pb10]}>
|
||||||
Set this setting to "No" to hide all quote posts from your feed.
|
<Trans>
|
||||||
Reposts will still be visible.
|
Set this setting to "No" to hide all quote posts from your feed.
|
||||||
|
Reposts will still be visible.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
type="default-light"
|
type="default-light"
|
||||||
|
@ -170,12 +184,14 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
|
|
||||||
<View style={[pal.viewLight, styles.card]}>
|
<View style={[pal.viewLight, styles.card]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Show
|
<FontAwesomeIcon icon="flask" color={pal.colors.text} />
|
||||||
Posts from My Feeds
|
<Trans>Show Posts from My Feeds</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<Text style={[pal.text, s.pb10]}>
|
||||||
Set this setting to "Yes" to show samples of your saved feeds in
|
<Trans>
|
||||||
your following feed. This is an experimental feature.
|
Set this setting to "Yes" to show samples of your saved feeds in
|
||||||
|
your following feed. This is an experimental feature.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
type="default-light"
|
type="default-light"
|
||||||
|
@ -204,9 +220,11 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
}}
|
}}
|
||||||
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
|
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Confirm"
|
accessibilityLabel={_(msg`Confirm`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
<Text style={[s.white, s.bold, s.f18]}>
|
||||||
|
<Trans>Done</Trans>
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {RadioGroup} from 'view/com/util/forms/RadioGroup'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
|
||||||
export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||||
|
@ -19,6 +21,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -37,7 +40,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||||
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
|
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
|
||||||
]}>
|
]}>
|
||||||
<Text type="xl" style={[pal.textLight, styles.description]}>
|
<Text type="xl" style={[pal.textLight, styles.description]}>
|
||||||
Fine-tune the discussion threads.
|
<Trans>Fine-tune the discussion threads.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
@ -45,10 +48,10 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||||
<View style={styles.cardsContainer}>
|
<View style={styles.cardsContainer}>
|
||||||
<View style={[pal.viewLight, styles.card]}>
|
<View style={[pal.viewLight, styles.card]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
Sort Replies
|
<Trans>Sort Replies</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<Text style={[pal.text, s.pb10]}>
|
||||||
Sort replies to the same post by:
|
<Trans>Sort replies to the same post by:</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
|
<View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
@ -67,10 +70,12 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||||
|
|
||||||
<View style={[pal.viewLight, styles.card]}>
|
<View style={[pal.viewLight, styles.card]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
Prioritize Your Follows
|
<Trans>Prioritize Your Follows</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<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>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
type="default-light"
|
type="default-light"
|
||||||
|
@ -84,12 +89,14 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||||
|
|
||||||
<View style={[pal.viewLight, styles.card]}>
|
<View style={[pal.viewLight, styles.card]}>
|
||||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Threaded
|
<FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
|
||||||
Mode
|
<Trans>Threaded Mode</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.text, s.pb10]}>
|
<Text style={[pal.text, s.pb10]}>
|
||||||
Set this setting to "Yes" to show replies in a threaded view. This
|
<Trans>
|
||||||
is an experimental feature.
|
Set this setting to "Yes" to show replies in a threaded view.
|
||||||
|
This is an experimental feature.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
type="default-light"
|
type="default-light"
|
||||||
|
@ -118,9 +125,11 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||||
}}
|
}}
|
||||||
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
|
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Confirm"
|
accessibilityLabel={_(msg`Confirm`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
<Text style={[s.white, s.bold, s.f18]}>
|
||||||
|
<Trans>Done</Trans>
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {FeedSourceModel} from 'state/models/content/feed-source'
|
||||||
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||||
import {combinedDisplayName} from 'lib/strings/display-names'
|
import {combinedDisplayName} from 'lib/strings/display-names'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
||||||
|
@ -38,6 +40,7 @@ export const ProfileScreen = withAuthRequired(
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {screen, track} = useAnalytics()
|
const {screen, track} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
|
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
|
||||||
const name = route.params.name === 'me' ? store.me.did : route.params.name
|
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
|
// if section is posts or posts & replies
|
||||||
} else {
|
} else {
|
||||||
if (item === ProfileUiModel.END_ITEM) {
|
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) {
|
} else if (item === ProfileUiModel.LOADING_ITEM) {
|
||||||
return <PostFeedLoadingPlaceholder />
|
return <PostFeedLoadingPlaceholder />
|
||||||
} else if (item._reactKey === '__error__') {
|
} else if (item._reactKey === '__error__') {
|
||||||
|
@ -296,7 +303,7 @@ export const ProfileScreen = withAuthRequired(
|
||||||
onPress={onPressCompose}
|
onPress={onPressCompose}
|
||||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="New post"
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
</ScreenHider>
|
</ScreenHider>
|
||||||
|
|
|
@ -47,6 +47,8 @@ import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {ComposeIcon2} from 'lib/icons'
|
import {ComposeIcon2} from 'lib/icons'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
const SECTION_TITLES = ['Posts', 'About']
|
const SECTION_TITLES = ['Posts', 'About']
|
||||||
|
@ -60,6 +62,7 @@ export const ProfileFeedScreen = withAuthRequired(
|
||||||
observer(function ProfileFeedScreenImpl(props: Props) {
|
observer(function ProfileFeedScreenImpl(props: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const {name: handleOrDid} = props.route.params
|
const {name: handleOrDid} = props.route.params
|
||||||
|
@ -98,7 +101,7 @@ export const ProfileFeedScreen = withAuthRequired(
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||||
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
||||||
Could not load feed
|
<Trans>Could not load feed</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="md" style={[pal.text, s.mb20]}>
|
<Text type="md" style={[pal.text, s.mb20]}>
|
||||||
{error}
|
{error}
|
||||||
|
@ -107,12 +110,12 @@ export const ProfileFeedScreen = withAuthRequired(
|
||||||
<View style={{flexDirection: 'row'}}>
|
<View style={{flexDirection: 'row'}}>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
accessibilityLabel="Go Back"
|
accessibilityLabel={_(msg`Go Back`)}
|
||||||
accessibilityHint="Return to previous page"
|
accessibilityHint="Return to previous page"
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
style={{flexShrink: 1}}>
|
style={{flexShrink: 1}}>
|
||||||
<Text type="button" style={pal.text}>
|
<Text type="button" style={pal.text}>
|
||||||
Go Back
|
<Trans>Go Back</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
@ -142,6 +145,7 @@ export const ProfileFeedScreenInner = observer(
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const feedSectionRef = React.useRef<SectionRef>(null)
|
const feedSectionRef = React.useRef<SectionRef>(null)
|
||||||
const {rkey, name: handleOrDid} = route.params
|
const {rkey, name: handleOrDid} = route.params
|
||||||
const uri = useMemo(
|
const uri = useMemo(
|
||||||
|
@ -313,7 +317,7 @@ export const ProfileFeedScreenInner = observer(
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
testID="headerDropdownBtn"
|
testID="headerDropdownBtn"
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
accessibilityLabel="More options"
|
accessibilityLabel={_(msg`More options`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View style={[pal.viewLight, styles.btn]}>
|
<View style={[pal.viewLight, styles.btn]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -334,6 +338,7 @@ export const ProfileFeedScreenInner = observer(
|
||||||
onTogglePinned,
|
onTogglePinned,
|
||||||
onToggleSaved,
|
onToggleSaved,
|
||||||
dropdownItems,
|
dropdownItems,
|
||||||
|
_,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -374,7 +379,7 @@ export const ProfileFeedScreenInner = observer(
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="New post"
|
accessibilityLabel={_(msg`New post`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -448,6 +453,7 @@ const AboutSection = observer(function AboutPageImpl({
|
||||||
onScroll: (e: NativeScrollEvent) => void
|
onScroll: (e: NativeScrollEvent) => void
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
const scrollHandler = useAnimatedScrollHandler({onScroll})
|
const scrollHandler = useAnimatedScrollHandler({onScroll})
|
||||||
|
|
||||||
if (!feedInfo) {
|
if (!feedInfo) {
|
||||||
|
@ -478,14 +484,14 @@ const AboutSection = observer(function AboutPageImpl({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
|
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
|
||||||
No description
|
<Trans>No description</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
|
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
testID="toggleLikeBtn"
|
testID="toggleLikeBtn"
|
||||||
accessibilityLabel="Like this feed"
|
accessibilityLabel={_(msg`Like this feed`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={onToggleLiked}
|
onPress={onToggleLiked}
|
||||||
style={{paddingHorizontal: 10}}>
|
style={{paddingHorizontal: 10}}>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue