Internationalization & localization (#1822)
* install and setup lingui * setup dynamic locale activation and async loading * first pass of automated replacement of text messages * add some more documentaton * fix nits * add `es` and `hi`locales for testing purposes * make accessibilityLabel localized * compile and extract new messages * fix merge conflicts * fix eslint warning * change instructions from sending email to opening PR * fix comments
This commit is contained in:
parent
82059b7ee1
commit
4c7850f8c4
108 changed files with 10334 additions and 1365 deletions
|
@ -45,6 +45,7 @@ module.exports = function (api) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'macros',
|
||||||
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
|
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
|
|
113
docs/internationalization.md
Normal file
113
docs/internationalization.md
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
# Internationalization
|
||||||
|
|
||||||
|
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please open a PR or issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app)
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
We are using Lingui to manage translations. You can find the documentation [here](https://lingui.dev/).
|
||||||
|
|
||||||
|
### Adding new strings
|
||||||
|
When adding a new string, do it as follows:
|
||||||
|
```jsx
|
||||||
|
// Before
|
||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
|
<Text>Hello World</Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// After
|
||||||
|
import { Text } from "react-native";
|
||||||
|
import { Trans } from "@lingui/macro";
|
||||||
|
|
||||||
|
<Text><Trans>Hello World</Trans></Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<Trans>` macro will extract the string and add it to the catalog. It is not really a component, but a macro. Further reading [here](https://lingui.dev/ref/macro.html)
|
||||||
|
|
||||||
|
However sometimes you will run into this case:
|
||||||
|
```jsx
|
||||||
|
// Before
|
||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
|
const text = "Hello World";
|
||||||
|
<Text accessibilityLabel="Label is here">{text}</Text>
|
||||||
|
```
|
||||||
|
In this case, you cannot use the `useLingui()` hook:
|
||||||
|
```jsx
|
||||||
|
import { msg } from "@lingui/macro";
|
||||||
|
import { useLingui } from "@lingui/react";
|
||||||
|
|
||||||
|
const { _ } = useLingui();
|
||||||
|
return <Text accessibilityLabel={_(msg`Label is here`)}>{text}</Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to do this outside of a React component, you can use the `t` macro instead (note: this won't react to changes if the locale is switched dynamically within the app):
|
||||||
|
```jsx
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
const text = t`Hello World`;
|
||||||
|
```
|
||||||
|
|
||||||
|
We can then run `yarn intl:extract` to update the catalog in `src/locale/locales/{locale}/messages.po`. This will add the new string to the catalog.
|
||||||
|
We can then run `yarn intl:compile` to update the translation files in `src/locale/locales/{locale}/messages.js`. This will add the new string to the translation files.
|
||||||
|
The configuration for translations is defined in `lingui.config.js`
|
||||||
|
|
||||||
|
So the workflow is as follows:
|
||||||
|
1. Wrap messages in Trans macro
|
||||||
|
2. Run `yarn intl:extract` command to generate message catalogs
|
||||||
|
3. Translate message catalogs (send them to translators usually)
|
||||||
|
4. Run `yarn intl:compile` to create runtime catalogs
|
||||||
|
5. Load runtime catalog
|
||||||
|
6. Enjoy translated app!
|
||||||
|
|
||||||
|
### Common pitfalls
|
||||||
|
These pitfalls are memoization pitfalls that will cause the components to not re-render when the locale is changed -- causing stale translations to be shown.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { msg } from "@lingui/macro";
|
||||||
|
import { i18n } from "@lingui/core";
|
||||||
|
|
||||||
|
const welcomeMessage = msg`Welcome!`;
|
||||||
|
|
||||||
|
// ❌ Bad! This code won't work
|
||||||
|
export function Welcome() {
|
||||||
|
const buggyWelcome = useMemo(() => {
|
||||||
|
return i18n._(welcomeMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div>{buggyWelcome}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Bad! This code won't work either because the reference to i18n does not change
|
||||||
|
export function Welcome() {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
|
const buggyWelcome = useMemo(() => {
|
||||||
|
return i18n._(welcomeMessage);
|
||||||
|
}, [i18n]);
|
||||||
|
|
||||||
|
return <div>{buggyWelcome}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Good! `useMemo` has i18n context in the dependency
|
||||||
|
export function Welcome() {
|
||||||
|
const linguiCtx = useLingui();
|
||||||
|
|
||||||
|
const welcome = useMemo(() => {
|
||||||
|
return linguiCtx.i18n._(welcomeMessage);
|
||||||
|
}, [linguiCtx]);
|
||||||
|
|
||||||
|
return <div>{welcome}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context
|
||||||
|
export function Welcome() {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const welcome = useMemo(() => {
|
||||||
|
return _(welcomeMessage);
|
||||||
|
}, [_]);
|
||||||
|
|
||||||
|
return <div>{welcome}</div>;
|
||||||
|
}
|
||||||
|
```
|
11
lingui.config.js
Normal file
11
lingui.config.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/** @type {import('@lingui/conf').LinguiConfig} */
|
||||||
|
module.exports = {
|
||||||
|
locales: ['en', 'cs', 'fr', 'hi', 'es'],
|
||||||
|
catalogs: [
|
||||||
|
{
|
||||||
|
path: '<rootDir>/src/locale/locales/{locale}/messages',
|
||||||
|
include: ['src'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
format: 'po',
|
||||||
|
}
|
10
package.json
10
package.json
|
@ -28,7 +28,9 @@
|
||||||
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
|
"perf:test: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}>
|
||||||
|
<I18nProvider i18n={i18n}>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<Shell />
|
<Shell />
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
|
</I18nProvider>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</RootStoreProvider>
|
</RootStoreProvider>
|
||||||
</analytics.Provider>
|
</analytics.Provider>
|
||||||
|
|
20
src/locale/i18n.ts
Normal file
20
src/locale/i18n.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {i18n} from '@lingui/core'
|
||||||
|
|
||||||
|
export const locales = {
|
||||||
|
en: 'English',
|
||||||
|
cs: 'Česky',
|
||||||
|
fr: 'Français',
|
||||||
|
hi: 'हिंदी',
|
||||||
|
es: 'Español',
|
||||||
|
}
|
||||||
|
export const defaultLocale = 'en'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We do a dynamic import of just the catalog that we need
|
||||||
|
* @param locale any locale string
|
||||||
|
*/
|
||||||
|
export async function dynamicActivate(locale: string) {
|
||||||
|
const {messages} = await import(`./locales/${locale}/messages`)
|
||||||
|
i18n.load(locale, messages)
|
||||||
|
i18n.activate(locale)
|
||||||
|
}
|
1
src/locale/locales/cs/messages.js
Normal file
1
src/locale/locales/cs/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/cs/messages.po
Normal file
1544
src/locale/locales/cs/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/en/messages.js
Normal file
1
src/locale/locales/en/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/en/messages.po
Normal file
1544
src/locale/locales/en/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/es/messages.js
Normal file
1
src/locale/locales/es/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/es/messages.po
Normal file
1544
src/locale/locales/es/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/fr/messages.js
Normal file
1
src/locale/locales/fr/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/fr/messages.po
Normal file
1544
src/locale/locales/fr/messages.po
Normal file
File diff suppressed because it is too large
Load diff
1
src/locale/locales/hi/messages.js
Normal file
1
src/locale/locales/hi/messages.js
Normal file
File diff suppressed because one or more lines are too long
1544
src/locale/locales/hi/messages.po
Normal file
1544
src/locale/locales/hi/messages.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,8 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
import {s, colors} from 'lib/styles'
|
import {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>
|
||||||
|
|
119
src/view/com/auth/login/ChooseAccountForm.tsx
Normal file
119
src/view/com/auth/login/ChooseAccountForm.tsx
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {UserAvatar} from '../../util/UserAvatar'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {AccountData} from 'state/models/session'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {styles} from './styles'
|
||||||
|
|
||||||
|
export const ChooseAccountForm = ({
|
||||||
|
store,
|
||||||
|
onSelectAccount,
|
||||||
|
onPressBack,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
onSelectAccount: (account?: AccountData) => void
|
||||||
|
onPressBack: () => void
|
||||||
|
}) => {
|
||||||
|
const {track, screen} = useAnalytics()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
screen('Choose Account')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
const onTryAccount = async (account: AccountData) => {
|
||||||
|
if (account.accessJwt && account.refreshJwt) {
|
||||||
|
setIsProcessing(true)
|
||||||
|
if (await store.session.resumeSession(account)) {
|
||||||
|
track('Sign In', {resumedSession: true})
|
||||||
|
setIsProcessing(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsProcessing(false)
|
||||||
|
}
|
||||||
|
onSelectAccount(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
|
||||||
|
<Text
|
||||||
|
type="2xl-medium"
|
||||||
|
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
|
||||||
|
<Trans>Sign in as...</Trans>
|
||||||
|
</Text>
|
||||||
|
{store.session.accounts.map(account => (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID={`chooseAccountBtn-${account.handle}`}
|
||||||
|
key={account.did}
|
||||||
|
style={[pal.view, pal.border, styles.account]}
|
||||||
|
onPress={() => onTryAccount(account)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
|
||||||
|
accessibilityHint="Double tap to sign in">
|
||||||
|
<View
|
||||||
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<View style={s.p10}>
|
||||||
|
<UserAvatar avatar={account.aviUrl} size={30} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.accountText}>
|
||||||
|
<Text type="lg-bold" style={pal.text}>
|
||||||
|
{account.displayName || account.handle}{' '}
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.textLight]}>
|
||||||
|
{account.handle}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="angle-right"
|
||||||
|
size={16}
|
||||||
|
style={[pal.text, s.mr10]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="chooseNewAccountBtn"
|
||||||
|
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
||||||
|
onPress={() => onSelectAccount(undefined)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Login to account that is not listed`)}
|
||||||
|
accessibilityHint="">
|
||||||
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<Text style={[styles.accountText, styles.accountTextOther]}>
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
<Trans>Other account</Trans>
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="angle-right"
|
||||||
|
size={16}
|
||||||
|
style={[pal.text, s.mr10]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{isProcessing && <ActivityIndicator />}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
197
src/view/com/auth/login/ForgotPasswordForm.tsx
Normal file
197
src/view/com/auth/login/ForgotPasswordForm.tsx
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import React, {useState, useEffect} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import * as EmailValidator from 'email-validator'
|
||||||
|
import {BskyAgent} from '@atproto/api'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {ServiceDescription} from 'state/models/session'
|
||||||
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
|
export const ForgotPasswordForm = ({
|
||||||
|
error,
|
||||||
|
serviceUrl,
|
||||||
|
serviceDescription,
|
||||||
|
setError,
|
||||||
|
setServiceUrl,
|
||||||
|
onPressBack,
|
||||||
|
onEmailSent,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
error: string
|
||||||
|
serviceUrl: string
|
||||||
|
serviceDescription: ServiceDescription | undefined
|
||||||
|
setError: (v: string) => void
|
||||||
|
setServiceUrl: (v: string) => void
|
||||||
|
onPressBack: () => void
|
||||||
|
onEmailSent: () => void
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const theme = useTheme()
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
|
const [email, setEmail] = useState<string>('')
|
||||||
|
const {screen} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screen('Signin:ForgotPassword')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
const onPressSelectService = () => {
|
||||||
|
openModal({
|
||||||
|
name: 'server-input',
|
||||||
|
initialService: serviceUrl,
|
||||||
|
onSelect: setServiceUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
if (!EmailValidator.validate(email)) {
|
||||||
|
return setError('Your email appears to be invalid.')
|
||||||
|
}
|
||||||
|
|
||||||
|
setError('')
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
await agent.com.atproto.server.requestPasswordReset({email})
|
||||||
|
onEmailSent()
|
||||||
|
} catch (e: any) {
|
||||||
|
const errMsg = e.toString()
|
||||||
|
logger.warn('Failed to request password reset', {error: e})
|
||||||
|
setIsProcessing(false)
|
||||||
|
if (isNetworkError(e)) {
|
||||||
|
setError(
|
||||||
|
'Unable to contact your service. Please check your Internet connection.',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError(cleanError(errMsg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
|
<Trans>Reset password</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text type="md" style={[pal.text, styles.instructions]}>
|
||||||
|
<Trans>
|
||||||
|
Enter the email you used to create your account. We'll send you a
|
||||||
|
"reset code" so you can set a new password.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
testID="forgotPasswordView"
|
||||||
|
style={[pal.borderDark, pal.view, styles.group]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="forgotPasswordSelectServiceButton"
|
||||||
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
||||||
|
onPress={onPressSelectService}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Hosting provider`)}
|
||||||
|
accessibilityHint="Sets hosting provider for password reset">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="globe"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
|
||||||
|
{toNiceDomain(serviceUrl)}
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="pen"
|
||||||
|
size={12}
|
||||||
|
style={pal.text as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="envelope"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="forgotPasswordEmail"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Email address"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoFocus
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel={_(msg`Email`)}
|
||||||
|
accessibilityHint="Sets email for password reset"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{error ? (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||||
|
</View>
|
||||||
|
<View style={s.flex1}>
|
||||||
|
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{!serviceDescription || isProcessing ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : !email ? (
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="newPasswordButton"
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Go to next`)}
|
||||||
|
accessibilityHint="Navigates to the next screen">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
{!serviceDescription || isProcessing ? (
|
||||||
|
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||||
|
<Trans>Processing...</Trans>
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,37 +1,19 @@
|
||||||
import React, {useState, useEffect, useRef} from 'react'
|
import 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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
288
src/view/com/auth/login/LoginForm.tsx
Normal file
288
src/view/com/auth/login/LoginForm.tsx
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
import React, {useState, useRef} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Keyboard,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
|
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {ServiceDescription} from 'state/models/session'
|
||||||
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
|
export const LoginForm = ({
|
||||||
|
store,
|
||||||
|
error,
|
||||||
|
serviceUrl,
|
||||||
|
serviceDescription,
|
||||||
|
initialHandle,
|
||||||
|
setError,
|
||||||
|
setServiceUrl,
|
||||||
|
onPressRetryConnect,
|
||||||
|
onPressBack,
|
||||||
|
onPressForgotPassword,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
error: string
|
||||||
|
serviceUrl: string
|
||||||
|
serviceDescription: ServiceDescription | undefined
|
||||||
|
initialHandle: string
|
||||||
|
setError: (v: string) => void
|
||||||
|
setServiceUrl: (v: string) => void
|
||||||
|
onPressRetryConnect: () => void
|
||||||
|
onPressBack: () => void
|
||||||
|
onPressForgotPassword: () => void
|
||||||
|
}) => {
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const theme = useTheme()
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
|
const [identifier, setIdentifier] = useState<string>(initialHandle)
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
const passwordInputRef = useRef<TextInput>(null)
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
const onPressSelectService = () => {
|
||||||
|
openModal({
|
||||||
|
name: 'server-input',
|
||||||
|
initialService: serviceUrl,
|
||||||
|
onSelect: setServiceUrl,
|
||||||
|
})
|
||||||
|
Keyboard.dismiss()
|
||||||
|
track('Signin:PressedSelectService')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
setError('')
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// try to guess the handle if the user just gave their own username
|
||||||
|
let fullIdent = identifier
|
||||||
|
if (
|
||||||
|
!identifier.includes('@') && // not an email
|
||||||
|
!identifier.includes('.') && // not a domain
|
||||||
|
serviceDescription &&
|
||||||
|
serviceDescription.availableUserDomains.length > 0
|
||||||
|
) {
|
||||||
|
let matched = false
|
||||||
|
for (const domain of serviceDescription.availableUserDomains) {
|
||||||
|
if (fullIdent.endsWith(domain)) {
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
fullIdent = createFullHandle(
|
||||||
|
identifier,
|
||||||
|
serviceDescription.availableUserDomains[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.session.login({
|
||||||
|
service: serviceUrl,
|
||||||
|
identifier: fullIdent,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
const errMsg = e.toString()
|
||||||
|
logger.warn('Failed to login', {error: e})
|
||||||
|
setIsProcessing(false)
|
||||||
|
if (errMsg.includes('Authentication Required')) {
|
||||||
|
setError(_(msg`Invalid username or password`))
|
||||||
|
} else if (isNetworkError(e)) {
|
||||||
|
setError(
|
||||||
|
_(
|
||||||
|
msg`Unable to contact your service. Please check your Internet connection.`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError(cleanError(errMsg))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
track('Sign In', {resumedSession: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isReady = !!serviceDescription && !!identifier && !!password
|
||||||
|
return (
|
||||||
|
<View testID="loginForm">
|
||||||
|
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||||
|
<Trans>Sign into</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.borderDark, styles.group]}>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="globe"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="loginSelectServiceButton"
|
||||||
|
style={styles.textBtn}
|
||||||
|
onPress={onPressSelectService}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Select service`)}
|
||||||
|
accessibilityHint="Sets server for the Bluesky client">
|
||||||
|
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||||
|
{toNiceDomain(serviceUrl)}
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="pen"
|
||||||
|
size={12}
|
||||||
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||||
|
<Trans>Account</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[pal.borderDark, styles.group]}>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="at"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="loginUsernameInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder={_(msg`Username or email address`)}
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoFocus
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="username"
|
||||||
|
returnKeyType="next"
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
passwordInputRef.current?.focus()
|
||||||
|
}}
|
||||||
|
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
value={identifier}
|
||||||
|
onChangeText={str =>
|
||||||
|
setIdentifier((str || '').toLowerCase().trim())
|
||||||
|
}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel={_(msg`Username or email address`)}
|
||||||
|
accessibilityHint="Input the username or email address you used at signup"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="lock"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="loginPasswordInput"
|
||||||
|
ref={passwordInputRef}
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Password"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="password"
|
||||||
|
returnKeyType="done"
|
||||||
|
enablesReturnKeyAutomatically={true}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
secureTextEntry={true}
|
||||||
|
textContentType="password"
|
||||||
|
clearButtonMode="while-editing"
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
onSubmitEditing={onPressNext}
|
||||||
|
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessibilityLabel={_(msg`Password`)}
|
||||||
|
accessibilityHint={
|
||||||
|
identifier === ''
|
||||||
|
? 'Input your password'
|
||||||
|
: `Input the password tied to ${identifier}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="forgotPasswordButton"
|
||||||
|
style={styles.textInputInnerBtn}
|
||||||
|
onPress={onPressForgotPassword}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Forgot password`)}
|
||||||
|
accessibilityHint="Opens password reset form">
|
||||||
|
<Text style={pal.link}>
|
||||||
|
<Trans>Forgot</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{error ? (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||||
|
</View>
|
||||||
|
<View style={s.flex1}>
|
||||||
|
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{!serviceDescription && error ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="loginRetryButton"
|
||||||
|
onPress={onPressRetryConnect}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Retry`)}
|
||||||
|
accessibilityHint="Retries login">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Retry</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : !serviceDescription ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator />
|
||||||
|
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||||
|
<Trans>Connecting...</Trans>
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : isProcessing ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : isReady ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="loginNextButton"
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Go to next`)}
|
||||||
|
accessibilityHint="Navigates to the next screen">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
48
src/view/com/auth/login/PasswordUpdatedForm.tsx
Normal file
48
src/view/com/auth/login/PasswordUpdatedForm.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React, {useEffect} from 'react'
|
||||||
|
import {TouchableOpacity, View} from 'react-native'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
export const PasswordUpdatedForm = ({
|
||||||
|
onPressNext,
|
||||||
|
}: {
|
||||||
|
onPressNext: () => void
|
||||||
|
}) => {
|
||||||
|
const {screen} = useAnalytics()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screen('Signin:PasswordUpdatedForm')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
|
<Trans>Password updated!</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||||
|
<Trans>You can now sign in with your new password.</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Close alert`)}
|
||||||
|
accessibilityHint="Closes password update alert">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Okay</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
181
src/view/com/auth/login/SetNewPasswordForm.tsx
Normal file
181
src/view/com/auth/login/SetNewPasswordForm.tsx
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import React, {useState, useEffect} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {BskyAgent} from '@atproto/api'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {styles} from './styles'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
export const SetNewPasswordForm = ({
|
||||||
|
error,
|
||||||
|
serviceUrl,
|
||||||
|
setError,
|
||||||
|
onPressBack,
|
||||||
|
onPasswordSet,
|
||||||
|
}: {
|
||||||
|
store: RootStoreModel
|
||||||
|
error: string
|
||||||
|
serviceUrl: string
|
||||||
|
setError: (v: string) => void
|
||||||
|
onPressBack: () => void
|
||||||
|
onPasswordSet: () => void
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const theme = useTheme()
|
||||||
|
const {screen} = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screen('Signin:SetNewPasswordForm')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
|
const [resetCode, setResetCode] = useState<string>('')
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
setError('')
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
const token = resetCode.replace(/\s/g, '')
|
||||||
|
await agent.com.atproto.server.resetPassword({
|
||||||
|
token,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
onPasswordSet()
|
||||||
|
} catch (e: any) {
|
||||||
|
const errMsg = e.toString()
|
||||||
|
logger.warn('Failed to set new password', {error: e})
|
||||||
|
setIsProcessing(false)
|
||||||
|
if (isNetworkError(e)) {
|
||||||
|
setError(
|
||||||
|
'Unable to contact your service. Please check your Internet connection.',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError(cleanError(errMsg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View>
|
||||||
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
|
<Trans>Set new password</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||||
|
<Trans>
|
||||||
|
You will receive an email with a "reset code." Enter that code here,
|
||||||
|
then enter your new password.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
testID="newPasswordView"
|
||||||
|
style={[pal.view, pal.borderDark, styles.group]}>
|
||||||
|
<View
|
||||||
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="ticket"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="resetCodeInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Reset code"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
autoFocus
|
||||||
|
value={resetCode}
|
||||||
|
onChangeText={setResetCode}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={_(msg`Reset code`)}
|
||||||
|
accessibilityHint="Input code sent to your email for password reset"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[pal.borderDark, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="lock"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="newPasswordInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="New password"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
secureTextEntry
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
editable={!isProcessing}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={_(msg`Password`)}
|
||||||
|
accessibilityHint="Input new password"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{error ? (
|
||||||
|
<View style={styles.error}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||||
|
</View>
|
||||||
|
<View style={s.flex1}>
|
||||||
|
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
||||||
|
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{isProcessing ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : !resetCode || !password ? (
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="setNewPasswordButton"
|
||||||
|
onPress={onPressNext}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Go to next`)}
|
||||||
|
accessibilityHint="Navigates to the next screen">
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
{isProcessing ? (
|
||||||
|
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||||
|
<Trans>Updating...</Trans>
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
118
src/view/com/auth/login/styles.ts
Normal file
118
src/view/com/auth/login/styles.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import {StyleSheet} from 'react-native'
|
||||||
|
import {colors} from 'lib/styles'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
|
||||||
|
export const styles = StyleSheet.create({
|
||||||
|
screenTitle: {
|
||||||
|
marginBottom: 10,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
instructions: {
|
||||||
|
marginBottom: 20,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
marginBottom: 20,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
groupLabel: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 5,
|
||||||
|
},
|
||||||
|
groupContent: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
noTopBorder: {
|
||||||
|
borderTopWidth: 0,
|
||||||
|
},
|
||||||
|
groupContentIcon: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 4,
|
||||||
|
},
|
||||||
|
accountLast: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
marginBottom: 20,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
fontSize: 17,
|
||||||
|
letterSpacing: 0.25,
|
||||||
|
fontWeight: '400',
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
textInputInnerBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
marginHorizontal: 6,
|
||||||
|
},
|
||||||
|
textBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
textBtnLabel: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
textBtnFakeInnerBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
marginHorizontal: 6,
|
||||||
|
},
|
||||||
|
accountText: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
accountTextOther: {
|
||||||
|
paddingLeft: 12,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: colors.red4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: -5,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
marginBottom: 15,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
errorIcon: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.white,
|
||||||
|
color: colors.white,
|
||||||
|
borderRadius: 30,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
dimmed: {opacity: 0.5},
|
||||||
|
|
||||||
|
maxHeight: {
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
maxHeight: isWeb ? '100vh' : undefined,
|
||||||
|
height: !isWeb ? '100%' : undefined,
|
||||||
|
},
|
||||||
|
})
|
|
@ -14,6 +14,7 @@ import {Text} from 'view/com/util/text/Text'
|
||||||
import Animated, {FadeInRight} from 'react-native-reanimated'
|
import 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]}>
|
||||||
|
<Trans>
|
||||||
Your posts, likes, and blocks are public. Mutes are private.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Choose the algorithms that power your experience with custom
|
Choose the algorithms that power your experience with custom
|
||||||
feeds.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Alt text describes images for blind and low-vision users, and helps
|
Alt text describes images for blind and low-vision users, and helps
|
||||||
give context to everyone.
|
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>
|
||||||
|
<Trans>
|
||||||
Use this to sign into the other app along with your handle.
|
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]}>
|
||||||
|
<Trans>
|
||||||
For security reasons, you won't be able to view this again. If you
|
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.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Can only contain letters, numbers, spaces, dashes, and underscores.
|
Can only contain letters, numbers, spaces, dashes, and underscores.
|
||||||
Must be at least 4 characters long, but no more than 32 characters
|
Must be at least 4 characters long, but no more than 32 characters
|
||||||
long.
|
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]}>
|
||||||
|
<Trans>
|
||||||
{list ? 'Edit' : 'New'} {purposeLabel} List
|
{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]}>
|
||||||
|
<Trans>
|
||||||
For security reasons, we'll need to send a confirmation code to
|
For security reasons, we'll need to send a confirmation code to
|
||||||
your email address.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Each code works once. You'll receive more invite codes periodically.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Your email has been saved! We'll be in touch soon.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Which languages would you like to see in your algorithmic feeds?
|
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]}>
|
||||||
|
<Trans>
|
||||||
Set this setting to "No" to hide all replies from your feed.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Adjust the number of likes a reply must have to be shown in your
|
Adjust the number of likes a reply must have to be shown in your
|
||||||
feed.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Set this setting to "No" to hide all reposts from your feed.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Set this setting to "No" to hide all quote posts from your feed.
|
Set this setting to "No" to hide all quote posts from your feed.
|
||||||
Reposts will still be visible.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Set this setting to "Yes" to show samples of your saved feeds in
|
Set this setting to "Yes" to show samples of your saved feeds in
|
||||||
your following feed. This is an experimental feature.
|
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]}>
|
||||||
|
<Trans>
|
||||||
Show replies by people you follow before all other replies.
|
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…
Add table
Add a link
Reference in a new issue