diff options
Diffstat (limited to 'src')
81 files changed, 8361 insertions, 1997 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index 9de901767..41b78fc98 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -13,6 +13,8 @@ import { import 'view/icons' +import {ThemeProvider as Alf} from '#/alf' +import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {init as initPersistedState} from '#/state/persisted' import {listenSessionDropped} from './state/events' import {useColorMode} from 'state/shell' @@ -25,6 +27,7 @@ import {queryClient} from 'lib/react-query' import {TestCtrls} from 'view/com/testing/TestCtrls' import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ModalStateProvider} from 'state/modals' +import {Provider as DialogStateProvider} from 'state/dialogs' import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' @@ -39,6 +42,7 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' import {Splash} from '#/Splash' +import {Provider as PortalProvider} from '#/components/Portal' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -48,6 +52,7 @@ function InnerApp() { const colorMode = useColorMode() const {isInitialLoad, currentAccount} = useSession() const {resumeSession} = useSessionApi() + const theme = useColorModeTheme(colorMode) const {_} = useLingui() // init @@ -63,25 +68,27 @@ function InnerApp() { return ( <SafeAreaProvider initialMetrics={initialWindowMetrics}> - <Splash isReady={!isInitialLoad}> - <React.Fragment - // Resets the entire tree below when it changes: - key={currentAccount?.did}> - <LoggedOutViewProvider> - <UnreadNotifsProvider> - <ThemeProvider theme={colorMode}> - {/* All components should be within this provider */} - <RootSiblingParent> - <GestureHandlerRootView style={s.h100pct}> - <TestCtrls /> - <Shell /> - </GestureHandlerRootView> - </RootSiblingParent> - </ThemeProvider> - </UnreadNotifsProvider> - </LoggedOutViewProvider> - </React.Fragment> - </Splash> + <Alf theme={theme}> + <Splash isReady={!isInitialLoad}> + <React.Fragment + // Resets the entire tree below when it changes: + key={currentAccount?.did}> + <LoggedOutViewProvider> + <UnreadNotifsProvider> + <ThemeProvider theme={colorMode}> + {/* All components should be within this provider */} + <RootSiblingParent> + <GestureHandlerRootView style={s.h100pct}> + <TestCtrls /> + <Shell /> + </GestureHandlerRootView> + </RootSiblingParent> + </ThemeProvider> + </UnreadNotifsProvider> + </LoggedOutViewProvider> + </React.Fragment> + </Splash> + </Alf> </SafeAreaProvider> ) } @@ -109,11 +116,15 @@ function App() { <MutedThreadsProvider> <InvitesStateProvider> <ModalStateProvider> - <LightboxStateProvider> - <I18nProvider> - <InnerApp /> - </I18nProvider> - </LightboxStateProvider> + <DialogStateProvider> + <LightboxStateProvider> + <I18nProvider> + <PortalProvider> + <InnerApp /> + </PortalProvider> + </I18nProvider> + </LightboxStateProvider> + </DialogStateProvider> </ModalStateProvider> </InvitesStateProvider> </MutedThreadsProvider> diff --git a/src/App.web.tsx b/src/App.web.tsx index 1bdb3c208..1efa0567c 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -8,6 +8,7 @@ import {RootSiblingParent} from 'react-native-root-siblings' import 'view/icons' import {ThemeProvider as Alf} from '#/alf' +import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {init as initPersistedState} from '#/state/persisted' import {useColorMode} from 'state/shell' import {Shell} from 'view/shell/index' @@ -16,6 +17,7 @@ import {ThemeProvider} from 'lib/ThemeContext' import {queryClient} from 'lib/react-query' import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ModalStateProvider} from 'state/modals' +import {Provider as DialogStateProvider} from 'state/dialogs' import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' @@ -29,7 +31,7 @@ import { } from 'state/session' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' -import {useColorModeTheme} from '#/alf/util/useColorModeTheme' +import {Provider as PortalProvider} from '#/components/Portal' function InnerApp() { const {isInitialLoad, currentAccount} = useSession() @@ -92,11 +94,15 @@ function App() { <MutedThreadsProvider> <InvitesStateProvider> <ModalStateProvider> - <LightboxStateProvider> - <I18nProvider> - <InnerApp /> - </I18nProvider> - </LightboxStateProvider> + <DialogStateProvider> + <LightboxStateProvider> + <I18nProvider> + <PortalProvider> + <InnerApp /> + </PortalProvider> + </I18nProvider> + </LightboxStateProvider> + </DialogStateProvider> </ModalStateProvider> </InvitesStateProvider> </MutedThreadsProvider> diff --git a/src/Navigation.tsx b/src/Navigation.tsx index c68cb0580..08cdd1d89 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -61,7 +61,7 @@ import {ProfileListScreen} from './view/screens/ProfileList' import {PostThreadScreen} from './view/screens/PostThread' import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy' -import {DebugScreen} from './view/screens/DebugNew' +import {Storybook} from './view/screens/Storybook' import {LogScreen} from './view/screens/Log' import {SupportScreen} from './view/screens/Support' import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy' @@ -144,7 +144,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { name="Profile" getComponent={() => ProfileScreen} options={({route}) => ({ - title: title(msg`@${route.params.name}`), + title: bskyTitle(`@${route.params.name}`, unreadCountLabel), animation: 'none', })} /> @@ -200,8 +200,8 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { /> <Stack.Screen name="Debug" - getComponent={() => DebugScreen} - options={{title: title(msg`Debug`), requireAuth: true}} + getComponent={() => Storybook} + options={{title: title(msg`Storybook`), requireAuth: true}} /> <Stack.Screen name="Log" diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index c142f5f71..203c2f282 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -4,6 +4,9 @@ export const atoms = { /* * Positioning */ + fixed: { + position: 'fixed', + }, absolute: { position: 'absolute', }, @@ -32,6 +35,10 @@ export const atoms = { zIndex: 50, }, + overflow_hidden: { + overflow: 'hidden', + }, + /* * Width */ @@ -45,6 +52,12 @@ export const atoms = { /* * Border radius */ + rounded_2xs: { + borderRadius: tokens.borderRadius._2xs, + }, + rounded_xs: { + borderRadius: tokens.borderRadius.xs, + }, rounded_sm: { borderRadius: tokens.borderRadius.sm, }, @@ -58,8 +71,8 @@ export const atoms = { /* * Flex */ - gap_xxs: { - gap: tokens.space.xxs, + gap_2xs: { + gap: tokens.space._2xs, }, gap_xs: { gap: tokens.space.xs, @@ -76,8 +89,17 @@ export const atoms = { gap_xl: { gap: tokens.space.xl, }, - gap_xxl: { - gap: tokens.space.xxl, + gap_2xl: { + gap: tokens.space._2xl, + }, + gap_3xl: { + gap: tokens.space._3xl, + }, + gap_4xl: { + gap: tokens.space._4xl, + }, + gap_5xl: { + gap: tokens.space._5xl, }, flex: { display: 'flex', @@ -125,9 +147,9 @@ export const atoms = { text_right: { textAlign: 'right', }, - text_xxs: { - fontSize: tokens.fontSize.xxs, - lineHeight: tokens.fontSize.xxs, + text_2xs: { + fontSize: tokens.fontSize._2xs, + lineHeight: tokens.fontSize._2xs, }, text_xs: { fontSize: tokens.fontSize.xs, @@ -149,9 +171,21 @@ export const atoms = { fontSize: tokens.fontSize.xl, lineHeight: tokens.fontSize.xl, }, - text_xxl: { - fontSize: tokens.fontSize.xxl, - lineHeight: tokens.fontSize.xxl, + text_2xl: { + fontSize: tokens.fontSize._2xl, + lineHeight: tokens.fontSize._2xl, + }, + text_3xl: { + fontSize: tokens.fontSize._3xl, + lineHeight: tokens.fontSize._3xl, + }, + text_4xl: { + fontSize: tokens.fontSize._4xl, + lineHeight: tokens.fontSize._4xl, + }, + text_5xl: { + fontSize: tokens.fontSize._5xl, + lineHeight: tokens.fontSize._5xl, }, leading_tight: { lineHeight: 1.25, @@ -162,11 +196,8 @@ export const atoms = { font_normal: { fontWeight: tokens.fontWeight.normal, }, - font_semibold: { - fontWeight: tokens.fontWeight.semibold, - }, font_bold: { - fontWeight: tokens.fontWeight.bold, + fontWeight: tokens.fontWeight.semibold, }, /* @@ -183,10 +214,29 @@ export const atoms = { }, /* + * Shadow + */ + shadow_sm: { + shadowRadius: 8, + shadowOpacity: 0.1, + elevation: 8, + }, + shadow_md: { + shadowRadius: 16, + shadowOpacity: 0.1, + elevation: 16, + }, + shadow_lg: { + shadowRadius: 32, + shadowOpacity: 0.1, + elevation: 24, + }, + + /* * Padding */ - p_xxs: { - padding: tokens.space.xxs, + p_2xs: { + padding: tokens.space._2xs, }, p_xs: { padding: tokens.space.xs, @@ -203,12 +253,21 @@ export const atoms = { p_xl: { padding: tokens.space.xl, }, - p_xxl: { - padding: tokens.space.xxl, + p_2xl: { + padding: tokens.space._2xl, }, - px_xxs: { - paddingLeft: tokens.space.xxs, - paddingRight: tokens.space.xxs, + p_3xl: { + padding: tokens.space._3xl, + }, + p_4xl: { + padding: tokens.space._4xl, + }, + p_5xl: { + padding: tokens.space._5xl, + }, + px_2xs: { + paddingLeft: tokens.space._2xs, + paddingRight: tokens.space._2xs, }, px_xs: { paddingLeft: tokens.space.xs, @@ -230,13 +289,25 @@ export const atoms = { paddingLeft: tokens.space.xl, paddingRight: tokens.space.xl, }, - px_xxl: { - paddingLeft: tokens.space.xxl, - paddingRight: tokens.space.xxl, + px_2xl: { + paddingLeft: tokens.space._2xl, + paddingRight: tokens.space._2xl, + }, + px_3xl: { + paddingLeft: tokens.space._3xl, + paddingRight: tokens.space._3xl, }, - py_xxs: { - paddingTop: tokens.space.xxs, - paddingBottom: tokens.space.xxs, + px_4xl: { + paddingLeft: tokens.space._4xl, + paddingRight: tokens.space._4xl, + }, + px_5xl: { + paddingLeft: tokens.space._5xl, + paddingRight: tokens.space._5xl, + }, + py_2xs: { + paddingTop: tokens.space._2xs, + paddingBottom: tokens.space._2xs, }, py_xs: { paddingTop: tokens.space.xs, @@ -258,12 +329,24 @@ export const atoms = { paddingTop: tokens.space.xl, paddingBottom: tokens.space.xl, }, - py_xxl: { - paddingTop: tokens.space.xxl, - paddingBottom: tokens.space.xxl, + py_2xl: { + paddingTop: tokens.space._2xl, + paddingBottom: tokens.space._2xl, + }, + py_3xl: { + paddingTop: tokens.space._3xl, + paddingBottom: tokens.space._3xl, + }, + py_4xl: { + paddingTop: tokens.space._4xl, + paddingBottom: tokens.space._4xl, }, - pt_xxs: { - paddingTop: tokens.space.xxs, + py_5xl: { + paddingTop: tokens.space._5xl, + paddingBottom: tokens.space._5xl, + }, + pt_2xs: { + paddingTop: tokens.space._2xs, }, pt_xs: { paddingTop: tokens.space.xs, @@ -280,11 +363,20 @@ export const atoms = { pt_xl: { paddingTop: tokens.space.xl, }, - pt_xxl: { - paddingTop: tokens.space.xxl, + pt_2xl: { + paddingTop: tokens.space._2xl, + }, + pt_3xl: { + paddingTop: tokens.space._3xl, + }, + pt_4xl: { + paddingTop: tokens.space._4xl, }, - pb_xxs: { - paddingBottom: tokens.space.xxs, + pt_5xl: { + paddingTop: tokens.space._5xl, + }, + pb_2xs: { + paddingBottom: tokens.space._2xs, }, pb_xs: { paddingBottom: tokens.space.xs, @@ -301,11 +393,20 @@ export const atoms = { pb_xl: { paddingBottom: tokens.space.xl, }, - pb_xxl: { - paddingBottom: tokens.space.xxl, + pb_2xl: { + paddingBottom: tokens.space._2xl, + }, + pb_3xl: { + paddingBottom: tokens.space._3xl, + }, + pb_4xl: { + paddingBottom: tokens.space._4xl, + }, + pb_5xl: { + paddingBottom: tokens.space._5xl, }, - pl_xxs: { - paddingLeft: tokens.space.xxs, + pl_2xs: { + paddingLeft: tokens.space._2xs, }, pl_xs: { paddingLeft: tokens.space.xs, @@ -322,11 +423,20 @@ export const atoms = { pl_xl: { paddingLeft: tokens.space.xl, }, - pl_xxl: { - paddingLeft: tokens.space.xxl, + pl_2xl: { + paddingLeft: tokens.space._2xl, }, - pr_xxs: { - paddingRight: tokens.space.xxs, + pl_3xl: { + paddingLeft: tokens.space._3xl, + }, + pl_4xl: { + paddingLeft: tokens.space._4xl, + }, + pl_5xl: { + paddingLeft: tokens.space._5xl, + }, + pr_2xs: { + paddingRight: tokens.space._2xs, }, pr_xs: { paddingRight: tokens.space.xs, @@ -343,15 +453,24 @@ export const atoms = { pr_xl: { paddingRight: tokens.space.xl, }, - pr_xxl: { - paddingRight: tokens.space.xxl, + pr_2xl: { + paddingRight: tokens.space._2xl, + }, + pr_3xl: { + paddingRight: tokens.space._3xl, + }, + pr_4xl: { + paddingRight: tokens.space._4xl, + }, + pr_5xl: { + paddingRight: tokens.space._5xl, }, /* * Margin */ - m_xxs: { - margin: tokens.space.xxs, + m_2xs: { + margin: tokens.space._2xs, }, m_xs: { margin: tokens.space.xs, @@ -368,12 +487,21 @@ export const atoms = { m_xl: { margin: tokens.space.xl, }, - m_xxl: { - margin: tokens.space.xxl, + m_2xl: { + margin: tokens.space._2xl, + }, + m_3xl: { + margin: tokens.space._3xl, + }, + m_4xl: { + margin: tokens.space._4xl, }, - mx_xxs: { - marginLeft: tokens.space.xxs, - marginRight: tokens.space.xxs, + m_5xl: { + margin: tokens.space._5xl, + }, + mx_2xs: { + marginLeft: tokens.space._2xs, + marginRight: tokens.space._2xs, }, mx_xs: { marginLeft: tokens.space.xs, @@ -395,13 +523,25 @@ export const atoms = { marginLeft: tokens.space.xl, marginRight: tokens.space.xl, }, - mx_xxl: { - marginLeft: tokens.space.xxl, - marginRight: tokens.space.xxl, + mx_2xl: { + marginLeft: tokens.space._2xl, + marginRight: tokens.space._2xl, + }, + mx_3xl: { + marginLeft: tokens.space._3xl, + marginRight: tokens.space._3xl, + }, + mx_4xl: { + marginLeft: tokens.space._4xl, + marginRight: tokens.space._4xl, + }, + mx_5xl: { + marginLeft: tokens.space._5xl, + marginRight: tokens.space._5xl, }, - my_xxs: { - marginTop: tokens.space.xxs, - marginBottom: tokens.space.xxs, + my_2xs: { + marginTop: tokens.space._2xs, + marginBottom: tokens.space._2xs, }, my_xs: { marginTop: tokens.space.xs, @@ -423,12 +563,24 @@ export const atoms = { marginTop: tokens.space.xl, marginBottom: tokens.space.xl, }, - my_xxl: { - marginTop: tokens.space.xxl, - marginBottom: tokens.space.xxl, + my_2xl: { + marginTop: tokens.space._2xl, + marginBottom: tokens.space._2xl, }, - mt_xxs: { - marginTop: tokens.space.xxs, + my_3xl: { + marginTop: tokens.space._3xl, + marginBottom: tokens.space._3xl, + }, + my_4xl: { + marginTop: tokens.space._4xl, + marginBottom: tokens.space._4xl, + }, + my_5xl: { + marginTop: tokens.space._5xl, + marginBottom: tokens.space._5xl, + }, + mt_2xs: { + marginTop: tokens.space._2xs, }, mt_xs: { marginTop: tokens.space.xs, @@ -445,11 +597,20 @@ export const atoms = { mt_xl: { marginTop: tokens.space.xl, }, - mt_xxl: { - marginTop: tokens.space.xxl, + mt_2xl: { + marginTop: tokens.space._2xl, + }, + mt_3xl: { + marginTop: tokens.space._3xl, }, - mb_xxs: { - marginBottom: tokens.space.xxs, + mt_4xl: { + marginTop: tokens.space._4xl, + }, + mt_5xl: { + marginTop: tokens.space._5xl, + }, + mb_2xs: { + marginBottom: tokens.space._2xs, }, mb_xs: { marginBottom: tokens.space.xs, @@ -466,11 +627,20 @@ export const atoms = { mb_xl: { marginBottom: tokens.space.xl, }, - mb_xxl: { - marginBottom: tokens.space.xxl, + mb_2xl: { + marginBottom: tokens.space._2xl, + }, + mb_3xl: { + marginBottom: tokens.space._3xl, + }, + mb_4xl: { + marginBottom: tokens.space._4xl, }, - ml_xxs: { - marginLeft: tokens.space.xxs, + mb_5xl: { + marginBottom: tokens.space._5xl, + }, + ml_2xs: { + marginLeft: tokens.space._2xs, }, ml_xs: { marginLeft: tokens.space.xs, @@ -487,11 +657,20 @@ export const atoms = { ml_xl: { marginLeft: tokens.space.xl, }, - ml_xxl: { - marginLeft: tokens.space.xxl, + ml_2xl: { + marginLeft: tokens.space._2xl, + }, + ml_3xl: { + marginLeft: tokens.space._3xl, + }, + ml_4xl: { + marginLeft: tokens.space._4xl, + }, + ml_5xl: { + marginLeft: tokens.space._5xl, }, - mr_xxs: { - marginRight: tokens.space.xxs, + mr_2xs: { + marginRight: tokens.space._2xs, }, mr_xs: { marginRight: tokens.space.xs, @@ -508,7 +687,16 @@ export const atoms = { mr_xl: { marginRight: tokens.space.xl, }, - mr_xxl: { - marginRight: tokens.space.xxl, + mr_2xl: { + marginRight: tokens.space._2xl, + }, + mr_3xl: { + marginRight: tokens.space._3xl, + }, + mr_4xl: { + marginRight: tokens.space._4xl, + }, + mr_5xl: { + marginRight: tokens.space._5xl, }, } as const diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 1daa0bfed..69a879853 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -5,6 +5,7 @@ import * as themes from '#/alf/themes' export * as tokens from '#/alf/tokens' export {atoms} from '#/alf/atoms' export * from '#/alf/util/platform' +export * from '#/alf/util/flatten' type BreakpointName = keyof typeof breakpoints diff --git a/src/alf/themes.ts b/src/alf/themes.ts index aae5c5893..7c6b7dab4 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -1,108 +1,320 @@ import * as tokens from '#/alf/tokens' import type {Mutable} from '#/alf/types' +import {atoms} from '#/alf/atoms' -export type ThemeName = 'light' | 'dark' +export type ThemeName = 'light' | 'dim' | 'dark' export type ReadonlyTheme = typeof light export type Theme = Mutable<ReadonlyTheme> +export type ReadonlyPalette = typeof lightPalette +export type Palette = Mutable<ReadonlyPalette> -export type Palette = { - primary: string - positive: string - negative: string -} +export const lightPalette = { + white: tokens.color.gray_0, + black: tokens.color.gray_1000, + + contrast_25: tokens.color.gray_25, + contrast_50: tokens.color.gray_50, + contrast_100: tokens.color.gray_100, + contrast_200: tokens.color.gray_200, + contrast_300: tokens.color.gray_300, + contrast_400: tokens.color.gray_400, + contrast_500: tokens.color.gray_500, + contrast_600: tokens.color.gray_600, + contrast_700: tokens.color.gray_700, + contrast_800: tokens.color.gray_800, + contrast_900: tokens.color.gray_900, + contrast_950: tokens.color.gray_950, + contrast_975: tokens.color.gray_975, + + primary_25: tokens.color.blue_25, + primary_50: tokens.color.blue_50, + primary_100: tokens.color.blue_100, + primary_200: tokens.color.blue_200, + primary_300: tokens.color.blue_300, + primary_400: tokens.color.blue_400, + primary_500: tokens.color.blue_500, + primary_600: tokens.color.blue_600, + primary_700: tokens.color.blue_700, + primary_800: tokens.color.blue_800, + primary_900: tokens.color.blue_900, + primary_950: tokens.color.blue_950, + primary_975: tokens.color.blue_975, + + positive_25: tokens.color.green_25, + positive_50: tokens.color.green_50, + positive_100: tokens.color.green_100, + positive_200: tokens.color.green_200, + positive_300: tokens.color.green_300, + positive_400: tokens.color.green_400, + positive_500: tokens.color.green_500, + positive_600: tokens.color.green_600, + positive_700: tokens.color.green_700, + positive_800: tokens.color.green_800, + positive_900: tokens.color.green_900, + positive_950: tokens.color.green_950, + positive_975: tokens.color.green_975, -export const lightPalette: Palette = { - primary: tokens.color.blue_500, - positive: tokens.color.green_500, - negative: tokens.color.red_500, + negative_25: tokens.color.red_25, + negative_50: tokens.color.red_50, + negative_100: tokens.color.red_100, + negative_200: tokens.color.red_200, + negative_300: tokens.color.red_300, + negative_400: tokens.color.red_400, + negative_500: tokens.color.red_500, + negative_600: tokens.color.red_600, + negative_700: tokens.color.red_700, + negative_800: tokens.color.red_800, + negative_900: tokens.color.red_900, + negative_950: tokens.color.red_950, + negative_975: tokens.color.red_975, } as const export const darkPalette: Palette = { - primary: tokens.color.blue_500, - positive: tokens.color.green_400, - negative: tokens.color.red_400, + white: tokens.color.gray_0, + black: tokens.color.gray_1000, + + contrast_25: tokens.color.gray_975, + contrast_50: tokens.color.gray_950, + contrast_100: tokens.color.gray_900, + contrast_200: tokens.color.gray_800, + contrast_300: tokens.color.gray_700, + contrast_400: tokens.color.gray_600, + contrast_500: tokens.color.gray_500, + contrast_600: tokens.color.gray_400, + contrast_700: tokens.color.gray_300, + contrast_800: tokens.color.gray_200, + contrast_900: tokens.color.gray_100, + contrast_950: tokens.color.gray_50, + contrast_975: tokens.color.gray_25, + + primary_25: tokens.color.blue_25, + primary_50: tokens.color.blue_50, + primary_100: tokens.color.blue_100, + primary_200: tokens.color.blue_200, + primary_300: tokens.color.blue_300, + primary_400: tokens.color.blue_400, + primary_500: tokens.color.blue_500, + primary_600: tokens.color.blue_600, + primary_700: tokens.color.blue_700, + primary_800: tokens.color.blue_800, + primary_900: tokens.color.blue_900, + primary_950: tokens.color.blue_950, + primary_975: tokens.color.blue_975, + + positive_25: tokens.color.green_25, + positive_50: tokens.color.green_50, + positive_100: tokens.color.green_100, + positive_200: tokens.color.green_200, + positive_300: tokens.color.green_300, + positive_400: tokens.color.green_400, + positive_500: tokens.color.green_500, + positive_600: tokens.color.green_600, + positive_700: tokens.color.green_700, + positive_800: tokens.color.green_800, + positive_900: tokens.color.green_900, + positive_950: tokens.color.green_950, + positive_975: tokens.color.green_975, + + negative_25: tokens.color.red_25, + negative_50: tokens.color.red_50, + negative_100: tokens.color.red_100, + negative_200: tokens.color.red_200, + negative_300: tokens.color.red_300, + negative_400: tokens.color.red_400, + negative_500: tokens.color.red_500, + negative_600: tokens.color.red_600, + negative_700: tokens.color.red_700, + negative_800: tokens.color.red_800, + negative_900: tokens.color.red_900, + negative_950: tokens.color.red_950, + negative_975: tokens.color.red_975, } as const export const light = { + name: 'light', palette: lightPalette, atoms: { text: { - color: tokens.color.gray_1000, + color: lightPalette.black, }, text_contrast_700: { - color: tokens.color.gray_700, + color: lightPalette.contrast_700, + }, + text_contrast_600: { + color: lightPalette.contrast_600, }, text_contrast_500: { - color: tokens.color.gray_500, + color: lightPalette.contrast_500, + }, + text_contrast_400: { + color: lightPalette.contrast_400, }, text_inverted: { - color: tokens.color.white, + color: lightPalette.white, }, bg: { - backgroundColor: tokens.color.white, + backgroundColor: lightPalette.white, + }, + bg_contrast_25: { + backgroundColor: lightPalette.contrast_25, + }, + bg_contrast_50: { + backgroundColor: lightPalette.contrast_50, }, bg_contrast_100: { - backgroundColor: tokens.color.gray_100, + backgroundColor: lightPalette.contrast_100, }, bg_contrast_200: { - backgroundColor: tokens.color.gray_200, + backgroundColor: lightPalette.contrast_200, }, bg_contrast_300: { - backgroundColor: tokens.color.gray_300, + backgroundColor: lightPalette.contrast_300, + }, + border: { + borderColor: lightPalette.contrast_100, + }, + border_contrast: { + borderColor: lightPalette.contrast_400, + }, + shadow_sm: { + ...atoms.shadow_sm, + shadowColor: lightPalette.black, + }, + shadow_md: { + ...atoms.shadow_md, + shadowColor: lightPalette.black, }, - bg_positive: { - backgroundColor: tokens.color.green_500, + shadow_lg: { + ...atoms.shadow_lg, + shadowColor: lightPalette.black, + }, + }, +} + +export const dim: Theme = { + name: 'dim', + palette: darkPalette, + atoms: { + text: { + color: darkPalette.white, + }, + text_contrast_700: { + color: darkPalette.contrast_800, }, - bg_negative: { - backgroundColor: tokens.color.red_400, + text_contrast_600: { + color: darkPalette.contrast_700, + }, + text_contrast_500: { + color: darkPalette.contrast_600, + }, + text_contrast_400: { + color: darkPalette.contrast_500, + }, + text_inverted: { + color: darkPalette.black, + }, + bg: { + backgroundColor: darkPalette.contrast_50, + }, + bg_contrast_25: { + backgroundColor: darkPalette.contrast_100, + }, + bg_contrast_50: { + backgroundColor: darkPalette.contrast_200, + }, + bg_contrast_100: { + backgroundColor: darkPalette.contrast_300, + }, + bg_contrast_200: { + backgroundColor: darkPalette.contrast_400, + }, + bg_contrast_300: { + backgroundColor: darkPalette.contrast_500, }, border: { - borderColor: tokens.color.gray_200, + borderColor: darkPalette.contrast_200, + }, + border_contrast: { + borderColor: darkPalette.contrast_400, }, - border_contrast_500: { - borderColor: tokens.color.gray_500, + shadow_sm: { + ...atoms.shadow_sm, + shadowOpacity: 0.7, + shadowColor: tokens.color.trueBlack, + }, + shadow_md: { + ...atoms.shadow_md, + shadowOpacity: 0.7, + shadowColor: tokens.color.trueBlack, + }, + shadow_lg: { + ...atoms.shadow_lg, + shadowOpacity: 0.7, + shadowColor: tokens.color.trueBlack, }, }, } export const dark: Theme = { + name: 'dark', palette: darkPalette, atoms: { text: { - color: tokens.color.white, + color: darkPalette.white, }, text_contrast_700: { - color: tokens.color.gray_300, + color: darkPalette.contrast_700, + }, + text_contrast_600: { + color: darkPalette.contrast_600, }, text_contrast_500: { - color: tokens.color.gray_500, + color: darkPalette.contrast_500, + }, + text_contrast_400: { + color: darkPalette.contrast_400, }, text_inverted: { - color: tokens.color.gray_1000, + color: darkPalette.black, }, bg: { - backgroundColor: tokens.color.gray_1000, + backgroundColor: darkPalette.black, + }, + bg_contrast_25: { + backgroundColor: darkPalette.contrast_50, + }, + bg_contrast_50: { + backgroundColor: darkPalette.contrast_100, }, bg_contrast_100: { - backgroundColor: tokens.color.gray_900, + backgroundColor: darkPalette.contrast_200, }, bg_contrast_200: { - backgroundColor: tokens.color.gray_800, + backgroundColor: darkPalette.contrast_300, }, bg_contrast_300: { - backgroundColor: tokens.color.gray_700, + backgroundColor: darkPalette.contrast_400, }, - bg_positive: { - backgroundColor: tokens.color.green_400, + border: { + borderColor: darkPalette.contrast_100, }, - bg_negative: { - backgroundColor: tokens.color.red_400, + border_contrast: { + borderColor: darkPalette.contrast_300, }, - border: { - borderColor: tokens.color.gray_800, + shadow_sm: { + ...atoms.shadow_sm, + shadowOpacity: 0.7, + shadowColor: tokens.color.trueBlack, + }, + shadow_md: { + ...atoms.shadow_md, + shadowOpacity: 0.7, + shadowColor: tokens.color.trueBlack, }, - border_contrast_500: { - borderColor: tokens.color.gray_500, + shadow_lg: { + ...atoms.shadow_lg, + shadowOpacity: 0.7, + shadowColor: tokens.color.trueBlack, }, }, } diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 4034e0deb..0e370cdc1 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -1,79 +1,95 @@ const BLUE_HUE = 211 -const GRAYSCALE_SATURATION = 22 +const RED_HUE = 346 +const GREEN_HUE = 152 export const color = { - white: '#FFFFFF', + trueBlack: '#000000', - gray_0: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 100%)`, - gray_100: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 95%)`, - gray_200: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 85%)`, - gray_300: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 75%)`, - gray_400: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 65%)`, - gray_500: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 55%)`, - gray_600: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 45%)`, - gray_700: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 35%)`, - gray_800: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 25%)`, - gray_900: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 15%)`, - gray_1000: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 5%)`, + gray_0: `hsl(${BLUE_HUE}, 20%, 100%)`, + gray_25: `hsl(${BLUE_HUE}, 20%, 97%)`, + gray_50: `hsl(${BLUE_HUE}, 20%, 95%)`, + gray_100: `hsl(${BLUE_HUE}, 20%, 90%)`, + gray_200: `hsl(${BLUE_HUE}, 20%, 80%)`, + gray_300: `hsl(${BLUE_HUE}, 20%, 70%)`, + gray_400: `hsl(${BLUE_HUE}, 20%, 60%)`, + gray_500: `hsl(${BLUE_HUE}, 20%, 50%)`, + gray_600: `hsl(${BLUE_HUE}, 20%, 42%)`, + gray_700: `hsl(${BLUE_HUE}, 20%, 34%)`, + gray_800: `hsl(${BLUE_HUE}, 20%, 26%)`, + gray_900: `hsl(${BLUE_HUE}, 20%, 18%)`, + gray_950: `hsl(${BLUE_HUE}, 20%, 10%)`, + gray_975: `hsl(${BLUE_HUE}, 20%, 7%)`, + gray_1000: `hsl(${BLUE_HUE}, 20%, 4%)`, - blue_0: `hsl(${BLUE_HUE}, 99%, 100%)`, - blue_100: `hsl(${BLUE_HUE}, 99%, 93%)`, - blue_200: `hsl(${BLUE_HUE}, 99%, 83%)`, - blue_300: `hsl(${BLUE_HUE}, 99%, 73%)`, - blue_400: `hsl(${BLUE_HUE}, 99%, 63%)`, + blue_25: `hsl(${BLUE_HUE}, 99%, 97%)`, + blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`, + blue_100: `hsl(${BLUE_HUE}, 99%, 90%)`, + blue_200: `hsl(${BLUE_HUE}, 99%, 80%)`, + blue_300: `hsl(${BLUE_HUE}, 99%, 70%)`, + blue_400: `hsl(${BLUE_HUE}, 99%, 60%)`, blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`, - blue_600: `hsl(${BLUE_HUE}, 99%, 43%)`, - blue_700: `hsl(${BLUE_HUE}, 99%, 33%)`, - blue_800: `hsl(${BLUE_HUE}, 99%, 23%)`, - blue_900: `hsl(${BLUE_HUE}, 99%, 13%)`, - blue_1000: `hsl(${BLUE_HUE}, 99%, 8%)`, + blue_600: `hsl(${BLUE_HUE}, 99%, 42%)`, + blue_700: `hsl(${BLUE_HUE}, 99%, 34%)`, + blue_800: `hsl(${BLUE_HUE}, 99%, 26%)`, + blue_900: `hsl(${BLUE_HUE}, 99%, 18%)`, + blue_950: `hsl(${BLUE_HUE}, 99%, 10%)`, + blue_975: `hsl(${BLUE_HUE}, 99%, 7%)`, - green_0: `hsl(130, 60%, 100%)`, - green_100: `hsl(130, 60%, 95%)`, - green_200: `hsl(130, 60%, 85%)`, - green_300: `hsl(130, 60%, 75%)`, - green_400: `hsl(130, 60%, 65%)`, - green_500: `hsl(130, 60%, 55%)`, - green_600: `hsl(130, 60%, 45%)`, - green_700: `hsl(130, 60%, 35%)`, - green_800: `hsl(130, 60%, 25%)`, - green_900: `hsl(130, 60%, 15%)`, - green_1000: `hsl(130, 60%, 5%)`, + green_25: `hsl(${GREEN_HUE}, 82%, 97%)`, + green_50: `hsl(${GREEN_HUE}, 82%, 95%)`, + green_100: `hsl(${GREEN_HUE}, 82%, 90%)`, + green_200: `hsl(${GREEN_HUE}, 82%, 80%)`, + green_300: `hsl(${GREEN_HUE}, 82%, 70%)`, + green_400: `hsl(${GREEN_HUE}, 82%, 60%)`, + green_500: `hsl(${GREEN_HUE}, 82%, 50%)`, + green_600: `hsl(${GREEN_HUE}, 82%, 42%)`, + green_700: `hsl(${GREEN_HUE}, 82%, 34%)`, + green_800: `hsl(${GREEN_HUE}, 82%, 26%)`, + green_900: `hsl(${GREEN_HUE}, 82%, 18%)`, + green_950: `hsl(${GREEN_HUE}, 82%, 10%)`, + green_975: `hsl(${GREEN_HUE}, 82%, 7%)`, - red_0: `hsl(349, 96%, 100%)`, - red_100: `hsl(349, 96%, 95%)`, - red_200: `hsl(349, 96%, 85%)`, - red_300: `hsl(349, 96%, 75%)`, - red_400: `hsl(349, 96%, 65%)`, - red_500: `hsl(349, 96%, 55%)`, - red_600: `hsl(349, 96%, 45%)`, - red_700: `hsl(349, 96%, 35%)`, - red_800: `hsl(349, 96%, 25%)`, - red_900: `hsl(349, 96%, 15%)`, - red_1000: `hsl(349, 96%, 5%)`, + red_25: `hsl(${RED_HUE}, 91%, 97%)`, + red_50: `hsl(${RED_HUE}, 91%, 95%)`, + red_100: `hsl(${RED_HUE}, 91%, 90%)`, + red_200: `hsl(${RED_HUE}, 91%, 80%)`, + red_300: `hsl(${RED_HUE}, 91%, 70%)`, + red_400: `hsl(${RED_HUE}, 91%, 60%)`, + red_500: `hsl(${RED_HUE}, 91%, 50%)`, + red_600: `hsl(${RED_HUE}, 91%, 42%)`, + red_700: `hsl(${RED_HUE}, 91%, 34%)`, + red_800: `hsl(${RED_HUE}, 91%, 26%)`, + red_900: `hsl(${RED_HUE}, 91%, 18%)`, + red_950: `hsl(${RED_HUE}, 91%, 10%)`, + red_975: `hsl(${RED_HUE}, 91%, 7%)`, } as const export const space = { - xxs: 2, + _2xs: 2, xs: 4, sm: 8, md: 12, - lg: 18, - xl: 24, - xxl: 32, + lg: 16, + xl: 20, + _2xl: 24, + _3xl: 28, + _4xl: 32, + _5xl: 40, } as const export const fontSize = { - xxs: 10, + _2xs: 10, xs: 12, sm: 14, md: 16, lg: 18, - xl: 22, - xxl: 26, + xl: 20, + _2xl: 22, + _3xl: 26, + _4xl: 32, + _5xl: 40, } as const -// TODO test export const lineHeight = { none: 1, normal: 1.5, @@ -81,6 +97,8 @@ export const lineHeight = { } as const export const borderRadius = { + _2xs: 2, + xs: 4, sm: 8, md: 12, full: 999, @@ -92,6 +110,56 @@ export const fontWeight = { bold: '900', } as const +export const gradients = { + sky: { + values: [ + [0, '#0A7AFF'], + [1, '#59B9FF'], + ], + hover_value: '#0A7AFF', + }, + midnight: { + values: [ + [0, '#022C5E'], + [1, '#4079BC'], + ], + hover_value: '#022C5E', + }, + sunrise: { + values: [ + [0, '#4E90AE'], + [0.4, '#AEA3AB'], + [0.8, '#E6A98F'], + [1, '#F3A84C'], + ], + hover_value: '#AEA3AB', + }, + sunset: { + values: [ + [0, '#6772AF'], + [0.6, '#B88BB6'], + [1, '#FFA6AC'], + ], + hover_value: '#B88BB6', + }, + nordic: { + values: [ + [0, '#083367'], + [1, '#9EE8C1'], + ], + hover_value: '#3A7085', + }, + bonfire: { + values: [ + [0, '#203E4E'], + [0.4, '#755B62'], + [0.8, '#CD7765'], + [1, '#EF956E'], + ], + hover_value: '#755B62', + }, +} as const + export type Color = keyof typeof color export type Space = keyof typeof space export type FontSize = keyof typeof fontSize diff --git a/src/alf/util/flatten.ts b/src/alf/util/flatten.ts new file mode 100644 index 000000000..448716a08 --- /dev/null +++ b/src/alf/util/flatten.ts @@ -0,0 +1,3 @@ +import {StyleSheet} from 'react-native' + +export const flatten = StyleSheet.flatten diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 000000000..d2100f0b4 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,507 @@ +import React from 'react' +import { + Pressable, + Text, + PressableProps, + TextProps, + ViewStyle, + AccessibilityProps, + View, + TextStyle, + StyleSheet, +} from 'react-native' +import LinearGradient from 'react-native-linear-gradient' + +import {useTheme, atoms as a, tokens, web, native} from '#/alf' +import {Props as SVGIconProps} from '#/components/icons/common' + +export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' +export type ButtonColor = + | 'primary' + | 'secondary' + | 'negative' + | 'gradient_sky' + | 'gradient_midnight' + | 'gradient_sunrise' + | 'gradient_sunset' + | 'gradient_nordic' + | 'gradient_bonfire' +export type ButtonSize = 'small' | 'large' +export type VariantProps = { + /** + * The style variation of the button + */ + variant?: ButtonVariant + /** + * The color of the button + */ + color?: ButtonColor + /** + * The size of the button + */ + size?: ButtonSize +} + +export type ButtonProps = React.PropsWithChildren< + Pick<PressableProps, 'disabled' | 'onPress'> & + AccessibilityProps & + VariantProps & { + label: string + } +> +export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} + +const Context = React.createContext< + VariantProps & { + hovered: boolean + focused: boolean + pressed: boolean + disabled: boolean + } +>({ + hovered: false, + focused: false, + pressed: false, + disabled: false, +}) + +export function useButtonContext() { + return React.useContext(Context) +} + +export function Button({ + children, + variant, + color, + size, + label, + disabled = false, + ...rest +}: ButtonProps) { + const t = useTheme() + const [state, setState] = React.useState({ + pressed: false, + hovered: false, + focused: false, + }) + + const onPressIn = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: true, + })) + }, [setState]) + const onPressOut = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: false, + })) + }, [setState]) + const onHoverIn = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: true, + })) + }, [setState]) + const onHoverOut = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: false, + })) + }, [setState]) + const onFocus = React.useCallback(() => { + setState(s => ({ + ...s, + focused: true, + })) + }, [setState]) + const onBlur = React.useCallback(() => { + setState(s => ({ + ...s, + focused: false, + })) + }, [setState]) + + const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => { + const baseStyles: ViewStyle[] = [] + const hoverStyles: ViewStyle[] = [] + const light = t.name === 'light' + + if (color === 'primary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.primary_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.primary_600, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.primary_700, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: tokens.color.blue_500, + }) + hoverStyles.push(a.border, { + backgroundColor: light + ? t.palette.primary_50 + : t.palette.primary_950, + }) + } else { + baseStyles.push(a.border, { + borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? t.palette.primary_100 + : t.palette.primary_900, + }) + } + } + } else if (color === 'secondary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: light + ? tokens.color.gray_100 + : tokens.color.gray_900, + }) + hoverStyles.push({ + backgroundColor: light + ? tokens.color.gray_200 + : tokens.color.gray_950, + }) + } else { + baseStyles.push({ + backgroundColor: light + ? tokens.color.gray_300 + : tokens.color.gray_950, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500, + }) + hoverStyles.push(a.border, t.atoms.bg_contrast_50) + } else { + baseStyles.push(a.border, { + borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? tokens.color.gray_100 + : tokens.color.gray_900, + }) + } + } + } else if (color === 'negative') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.negative_400, + }) + hoverStyles.push({ + backgroundColor: t.palette.negative_500, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.negative_600, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.negative_400, + }) + hoverStyles.push(a.border, { + backgroundColor: light + ? t.palette.negative_50 + : t.palette.negative_975, + }) + } else { + baseStyles.push(a.border, { + borderColor: light + ? t.palette.negative_200 + : t.palette.negative_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? t.palette.negative_100 + : t.palette.negative_950, + }) + } + } + } + + if (size === 'large') { + baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm) + } else if (size === 'small') { + baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm) + } + + return { + baseStyles, + hoverStyles, + focusStyles: [ + ...hoverStyles, + { + outline: 0, + } as ViewStyle, + ], + } + }, [t, variant, color, size, disabled]) + + const {gradientColors, gradientHoverColors, gradientLocations} = + React.useMemo(() => { + const colors: string[] = [] + const hoverColors: string[] = [] + const locations: number[] = [] + const gradient = { + primary: tokens.gradients.sky, + secondary: tokens.gradients.sky, + negative: tokens.gradients.sky, + gradient_sky: tokens.gradients.sky, + gradient_midnight: tokens.gradients.midnight, + gradient_sunrise: tokens.gradients.sunrise, + gradient_sunset: tokens.gradients.sunset, + gradient_nordic: tokens.gradients.nordic, + gradient_bonfire: tokens.gradients.bonfire, + }[color || 'primary'] + + if (variant === 'gradient') { + colors.push(...gradient.values.map(([_, color]) => color)) + hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) + locations.push(...gradient.values.map(([location, _]) => location)) + } + + return { + gradientColors: colors, + gradientHoverColors: hoverColors, + gradientLocations: locations, + } + }, [variant, color]) + + const context = React.useMemo( + () => ({ + ...state, + variant, + color, + size, + disabled: disabled || false, + }), + [state, variant, color, size, disabled], + ) + + return ( + <Pressable + role="button" + accessibilityHint={undefined} // optional + {...rest} + aria-label={label} + aria-pressed={state.pressed} + accessibilityLabel={label} + disabled={disabled || false} + accessibilityState={{ + disabled: disabled || false, + }} + style={[ + a.flex_row, + a.align_center, + a.overflow_hidden, + ...baseStyles, + ...(state.hovered || state.pressed ? hoverStyles : []), + ...(state.focused ? focusStyles : []), + ]} + onPressIn={onPressIn} + onPressOut={onPressOut} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut} + onFocus={onFocus} + onBlur={onBlur}> + {variant === 'gradient' && ( + <LinearGradient + colors={ + state.hovered || state.pressed || state.focused + ? gradientHoverColors + : gradientColors + } + locations={gradientLocations} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[a.absolute, a.inset_0]} + /> + )} + <Context.Provider value={context}> + {typeof children === 'string' ? ( + <ButtonText>{children}</ButtonText> + ) : ( + children + )} + </Context.Provider> + </Pressable> + ) +} + +export function useSharedButtonTextStyles() { + const t = useTheme() + const {color, variant, disabled, size} = useButtonContext() + return React.useMemo(() => { + const baseStyles: TextStyle[] = [] + const light = t.name === 'light' + + if (color === 'primary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({color: t.palette.white}) + } else { + baseStyles.push({color: t.palette.white, opacity: 0.5}) + } + } else if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ + color: light ? t.palette.primary_600 : t.palette.primary_500, + }) + } else { + baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({color: t.palette.primary_600}) + } else { + baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) + } + } + } else if (color === 'secondary') { + if (variant === 'solid' || variant === 'gradient') { + if (!disabled) { + baseStyles.push({ + color: light ? tokens.color.gray_700 : tokens.color.gray_100, + }) + } else { + baseStyles.push({ + color: light ? tokens.color.gray_400 : tokens.color.gray_700, + }) + } + } else if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ + color: light ? tokens.color.gray_600 : tokens.color.gray_300, + }) + } else { + baseStyles.push({ + color: light ? tokens.color.gray_400 : tokens.color.gray_700, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({ + color: light ? tokens.color.gray_600 : tokens.color.gray_300, + }) + } else { + baseStyles.push({ + color: light ? tokens.color.gray_400 : tokens.color.gray_600, + }) + } + } + } else if (color === 'negative') { + if (variant === 'solid' || variant === 'gradient') { + if (!disabled) { + baseStyles.push({color: t.palette.white}) + } else { + baseStyles.push({color: t.palette.white, opacity: 0.5}) + } + } else if (variant === 'outline') { + if (!disabled) { + baseStyles.push({color: t.palette.negative_400}) + } else { + baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({color: t.palette.negative_400}) + } else { + baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) + } + } + } else { + if (!disabled) { + baseStyles.push({color: t.palette.white}) + } else { + baseStyles.push({color: t.palette.white, opacity: 0.5}) + } + } + + if (size === 'large') { + baseStyles.push( + a.text_md, + web({paddingBottom: 1}), + native({marginTop: 2}), + ) + } else { + baseStyles.push( + a.text_md, + web({paddingBottom: 1}), + native({marginTop: 2}), + ) + } + + return StyleSheet.flatten(baseStyles) + }, [t, variant, color, size, disabled]) +} + +export function ButtonText({children, style, ...rest}: ButtonTextProps) { + const textStyles = useSharedButtonTextStyles() + + return ( + <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}> + {children} + </Text> + ) +} + +export function ButtonIcon({ + icon: Comp, +}: { + icon: React.ComponentType<SVGIconProps> +}) { + const {size} = useButtonContext() + const textStyles = useSharedButtonTextStyles() + + return ( + <View style={[a.z_20]}> + <Comp + size={size === 'large' ? 'md' : 'sm'} + style={[{color: textStyles.color, pointerEvents: 'none'}]} + /> + </View> + ) +} diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts new file mode 100644 index 000000000..b28b9f5a2 --- /dev/null +++ b/src/components/Dialog/context.ts @@ -0,0 +1,35 @@ +import React from 'react' + +import {useDialogStateContext} from '#/state/dialogs' +import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types' + +export const Context = React.createContext<DialogContextProps>({ + close: () => {}, +}) + +export function useDialogContext() { + return React.useContext(Context) +} + +export function useDialogControl() { + const id = React.useId() + const control = React.useRef<DialogControlProps>({ + open: () => {}, + close: () => {}, + }) + const {activeDialogs} = useDialogStateContext() + + React.useEffect(() => { + activeDialogs.current.set(id, control) + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + activeDialogs.current.delete(id) + } + }, [id, activeDialogs]) + + return { + ref: control, + open: () => control.current.open(), + close: () => control.current.close(), + } +} diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx new file mode 100644 index 000000000..44e4dc8a7 --- /dev/null +++ b/src/components/Dialog/index.tsx @@ -0,0 +1,162 @@ +import React, {useImperativeHandle} from 'react' +import {View, Dimensions} from 'react-native' +import BottomSheet, { + BottomSheetBackdrop, + BottomSheetScrollView, + BottomSheetTextInput, + BottomSheetView, +} from '@gorhom/bottom-sheet' +import {useSafeAreaInsets} from 'react-native-safe-area-context' + +import {useTheme, atoms as a} from '#/alf' +import {Portal} from '#/components/Portal' +import {createInput} from '#/components/forms/TextField' + +import { + DialogOuterProps, + DialogControlProps, + DialogInnerProps, +} from '#/components/Dialog/types' +import {Context} from '#/components/Dialog/context' + +export {useDialogControl, useDialogContext} from '#/components/Dialog/context' +export * from '#/components/Dialog/types' +// @ts-ignore +export const Input = createInput(BottomSheetTextInput) + +export function Outer({ + children, + control, + onClose, + nativeOptions, +}: React.PropsWithChildren<DialogOuterProps>) { + const t = useTheme() + const sheet = React.useRef<BottomSheet>(null) + const sheetOptions = nativeOptions?.sheet || {} + const hasSnapPoints = !!sheetOptions.snapPoints + + const open = React.useCallback<DialogControlProps['open']>((i = 0) => { + sheet.current?.snapToIndex(i) + }, []) + + const close = React.useCallback(() => { + sheet.current?.close() + onClose?.() + }, [onClose]) + + useImperativeHandle( + control.ref, + () => ({ + open, + close, + }), + [open, close], + ) + + const context = React.useMemo(() => ({close}), [close]) + + return ( + <Portal> + <BottomSheet + enableDynamicSizing={!hasSnapPoints} + enablePanDownToClose + keyboardBehavior="interactive" + android_keyboardInputMode="adjustResize" + keyboardBlurBehavior="restore" + {...sheetOptions} + ref={sheet} + index={-1} + backgroundStyle={{backgroundColor: 'transparent'}} + backdropComponent={props => ( + <BottomSheetBackdrop + opacity={0.4} + appearsOnIndex={0} + disappearsOnIndex={-1} + {...props} + /> + )} + handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} + handleStyle={{display: 'none'}} + onClose={onClose}> + <Context.Provider value={context}> + <View + style={[ + a.absolute, + a.inset_0, + t.atoms.bg, + { + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + height: Dimensions.get('window').height * 2, + }, + ]} + /> + {children} + </Context.Provider> + </BottomSheet> + </Portal> + ) +} + +// TODO a11y props here, or is that handled by the sheet? +export function Inner(props: DialogInnerProps) { + const insets = useSafeAreaInsets() + return ( + <BottomSheetView + style={[ + a.p_lg, + a.pt_3xl, + { + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, + }, + ]}> + {props.children} + </BottomSheetView> + ) +} + +export function ScrollableInner(props: DialogInnerProps) { + const insets = useSafeAreaInsets() + return ( + <BottomSheetScrollView + style={[ + a.flex_1, // main diff is this + a.p_lg, + a.pt_3xl, + { + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + }, + ]}> + {props.children} + <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> + </BottomSheetScrollView> + ) +} + +export function Handle() { + const t = useTheme() + return ( + <View + style={[ + a.absolute, + a.rounded_sm, + a.z_10, + { + top: a.pt_lg.paddingTop, + width: 35, + height: 4, + alignSelf: 'center', + backgroundColor: t.palette.contrast_900, + opacity: 0.5, + }, + ]} + /> + ) +} + +export function Close() { + return null +} diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx new file mode 100644 index 000000000..305c00e97 --- /dev/null +++ b/src/components/Dialog/index.web.tsx @@ -0,0 +1,194 @@ +import React, {useImperativeHandle} from 'react' +import {View, TouchableWithoutFeedback} from 'react-native' +import {FocusScope} from '@tamagui/focus-scope' +import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' +import {Portal} from '#/components/Portal' + +import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' +import {Context} from '#/components/Dialog/context' + +export {useDialogControl, useDialogContext} from '#/components/Dialog/context' +export * from '#/components/Dialog/types' +export {Input} from '#/components/forms/TextField' + +const stopPropagation = (e: any) => e.stopPropagation() + +export function Outer({ + control, + onClose, + children, +}: React.PropsWithChildren<DialogOuterProps>) { + const {_} = useLingui() + const t = useTheme() + const {gtMobile} = useBreakpoints() + const [isOpen, setIsOpen] = React.useState(false) + const [isVisible, setIsVisible] = React.useState(true) + + const open = React.useCallback(() => { + setIsOpen(true) + }, [setIsOpen]) + + const close = React.useCallback(async () => { + setIsVisible(false) + await new Promise(resolve => setTimeout(resolve, 150)) + setIsOpen(false) + setIsVisible(true) + onClose?.() + }, [onClose, setIsOpen]) + + useImperativeHandle( + control.ref, + () => ({ + open, + close, + }), + [open, close], + ) + + React.useEffect(() => { + if (!isOpen) return + + function handler(e: KeyboardEvent) { + if (e.key === 'Escape') close() + } + + document.addEventListener('keydown', handler) + + return () => document.removeEventListener('keydown', handler) + }, [isOpen, close]) + + const context = React.useMemo( + () => ({ + close, + }), + [close], + ) + + return ( + <> + {isOpen && ( + <Portal> + <Context.Provider value={context}> + <TouchableWithoutFeedback + accessibilityHint={undefined} + accessibilityLabel={_(msg`Close active dialog`)} + onPress={close}> + <View + style={[ + web(a.fixed), + a.inset_0, + a.z_10, + a.align_center, + gtMobile ? a.p_lg : a.p_md, + {overflowY: 'auto'}, + ]}> + {isVisible && ( + <Animated.View + entering={FadeIn.duration(150)} + // exiting={FadeOut.duration(150)} + style={[ + web(a.fixed), + a.inset_0, + {opacity: 0.5, backgroundColor: t.palette.black}, + ]} + /> + )} + + <View + style={[ + a.w_full, + a.z_20, + a.justify_center, + a.align_center, + { + minHeight: web('calc(90vh - 36px)') || undefined, + }, + ]}> + {isVisible ? children : null} + </View> + </View> + </TouchableWithoutFeedback> + </Context.Provider> + </Portal> + )} + </> + ) +} + +export function Inner({ + children, + style, + label, + accessibilityLabelledBy, + accessibilityDescribedBy, +}: DialogInnerProps) { + const t = useTheme() + const {gtMobile} = useBreakpoints() + return ( + <FocusScope loop enabled trapped> + <Animated.View + role="dialog" + aria-role="dialog" + aria-label={label} + aria-labelledby={accessibilityLabelledBy} + aria-describedby={accessibilityDescribedBy} + // @ts-ignore web only -prf + onClick={stopPropagation} + onStartShouldSetResponder={_ => true} + onTouchEnd={stopPropagation} + entering={FadeInDown.duration(100)} + // exiting={FadeOut.duration(100)} + style={[ + a.relative, + a.rounded_md, + a.w_full, + a.border, + gtMobile ? a.p_xl : a.p_lg, + t.atoms.bg, + { + maxWidth: 600, + borderColor: t.palette.contrast_200, + shadowColor: t.palette.black, + shadowOpacity: t.name === 'light' ? 0.1 : 0.4, + shadowRadius: 30, + }, + ...(Array.isArray(style) ? style : [style || {}]), + ]}> + {children} + </Animated.View> + </FocusScope> + ) +} + +export const ScrollableInner = Inner + +export function Handle() { + return null +} + +/** + * TODO(eric) unused rn + */ +// export function Close() { +// const {_} = useLingui() +// const t = useTheme() +// const {close} = useDialogContext() +// return ( +// <View +// style={[ +// a.absolute, +// a.z_10, +// { +// top: a.pt_lg.paddingTop, +// right: a.pr_lg.paddingRight, +// }, +// ]}> +// <Button onPress={close} label={_(msg`Close active dialog`)}> +// </Button> +// </View> +// ) +// } diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts new file mode 100644 index 000000000..d36784183 --- /dev/null +++ b/src/components/Dialog/types.ts @@ -0,0 +1,43 @@ +import React from 'react' +import type {ViewStyle, AccessibilityProps} from 'react-native' +import {BottomSheetProps} from '@gorhom/bottom-sheet' + +type A11yProps = Required<AccessibilityProps> + +export type DialogContextProps = { + close: () => void +} + +export type DialogControlProps = { + open: (index?: number) => void + close: () => void +} + +export type DialogOuterProps = { + control: { + ref: React.RefObject<DialogControlProps> + open: (index?: number) => void + close: () => void + } + onClose?: () => void + nativeOptions?: { + sheet?: Omit<BottomSheetProps, 'children'> + } + webOptions?: {} +} + +type DialogInnerPropsBase<T> = React.PropsWithChildren<{ + style?: ViewStyle +}> & + T +export type DialogInnerProps = + | DialogInnerPropsBase<{ + label?: undefined + accessibilityLabelledBy: A11yProps['aria-labelledby'] + accessibilityDescribedBy: string + }> + | DialogInnerPropsBase<{ + label: string + accessibilityLabelledBy?: undefined + accessibilityDescribedBy?: undefined + }> diff --git a/src/components/Link.tsx b/src/components/Link.tsx new file mode 100644 index 000000000..8f686f3c4 --- /dev/null +++ b/src/components/Link.tsx @@ -0,0 +1,191 @@ +import React from 'react' +import { + Text, + TextStyle, + StyleProp, + GestureResponderEvent, + Linking, +} from 'react-native' +import { + useLinkProps, + useNavigation, + StackActions, +} from '@react-navigation/native' +import {sanitizeUrl} from '@braintree/sanitize-url' + +import {isWeb} from '#/platform/detection' +import {useTheme, web, flatten} from '#/alf' +import {Button, ButtonProps, useButtonContext} from '#/components/Button' +import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' +import { + convertBskyAppUrlIfNeeded, + isExternalUrl, + linkRequiresWarning, +} from '#/lib/strings/url-helpers' +import {useModalControls} from '#/state/modals' +import {router} from '#/routes' + +export type LinkProps = Omit< + ButtonProps, + 'style' | 'onPress' | 'disabled' | 'label' +> & { + /** + * `TextStyle` to apply to the anchor element itself. Does not apply to any children. + */ + style?: StyleProp<TextStyle> + /** + * The React Navigation `StackAction` to perform when the link is pressed. + */ + action?: 'push' | 'replace' | 'navigate' + /** + * If true, will warn the user if the link text does not match the href. Only + * works for Links with children that are strings i.e. text links. + */ + warnOnMismatchingTextChild?: boolean + label?: ButtonProps['label'] +} & Pick<Parameters<typeof useLinkProps<AllNavigatorParams>>[0], 'to'> + +/** + * A interactive element that renders as a `<a>` tag on the web. On mobile it + * will translate the `href` to navigator screens and params and dispatch a + * navigation action. + * + * Intended to behave as a web anchor tag. For more complex routing, use a + * `Button`. + */ +export function Link({ + children, + to, + action = 'push', + warnOnMismatchingTextChild, + style, + ...rest +}: LinkProps) { + const navigation = useNavigation<NavigationProp>() + const {href} = useLinkProps<AllNavigatorParams>({ + to: + typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to, + }) + const isExternal = isExternalUrl(href) + const {openModal, closeModal} = useModalControls() + const onPress = React.useCallback( + (e: GestureResponderEvent) => { + const stringChildren = typeof children === 'string' ? children : '' + const requiresWarning = Boolean( + warnOnMismatchingTextChild && + stringChildren && + isExternal && + linkRequiresWarning(href, stringChildren), + ) + + if (requiresWarning) { + e.preventDefault() + + openModal({ + name: 'link-warning', + text: stringChildren, + href: href, + }) + } else { + e.preventDefault() + + if (isExternal) { + Linking.openURL(href) + } else { + /** + * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch + * of @ts-ignore below. + */ + const event = e as any + const isMiddleClick = isWeb && event.button === 1 + const isMetaKey = + isWeb && + (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) + const shouldOpenInNewTab = isMetaKey || isMiddleClick + + if ( + shouldOpenInNewTab || + href.startsWith('http') || + href.startsWith('mailto') + ) { + Linking.openURL(href) + } else { + closeModal() // close any active modals + + if (action === 'push') { + navigation.dispatch(StackActions.push(...router.matchPath(href))) + } else if (action === 'replace') { + navigation.dispatch( + StackActions.replace(...router.matchPath(href)), + ) + } else if (action === 'navigate') { + // @ts-ignore + navigation.navigate(...router.matchPath(href)) + } else { + throw Error('Unsupported navigator action.') + } + } + } + } + }, + [ + href, + isExternal, + warnOnMismatchingTextChild, + navigation, + action, + children, + closeModal, + openModal, + ], + ) + + return ( + <Button + label={href} + {...rest} + role="link" + accessibilityRole="link" + href={href} + onPress={onPress} + {...web({ + hrefAttrs: { + target: isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + }, + dataSet: { + // default to no underline, apply this ourselves + noUnderline: '1', + }, + })}> + {typeof children === 'string' ? ( + <LinkText style={style}>{children}</LinkText> + ) : ( + children + )} + </Button> + ) +} + +function LinkText({ + children, + style, +}: React.PropsWithChildren<{ + style?: StyleProp<TextStyle> +}>) { + const t = useTheme() + const {hovered} = useButtonContext() + return ( + <Text + style={[ + {color: t.palette.primary_500}, + hovered && { + textDecorationLine: 'underline', + textDecorationColor: t.palette.primary_500, + }, + flatten(style), + ]}> + {children as string} + </Text> + ) +} diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx new file mode 100644 index 000000000..1813d9e05 --- /dev/null +++ b/src/components/Portal.tsx @@ -0,0 +1,56 @@ +import React from 'react' + +type Component = React.ReactElement + +type ContextType = { + outlet: Component | null + append(id: string, component: Component): void + remove(id: string): void +} + +type ComponentMap = { + [id: string]: Component +} + +export const Context = React.createContext<ContextType>({ + outlet: null, + append: () => {}, + remove: () => {}, +}) + +export function Provider(props: React.PropsWithChildren<{}>) { + const map = React.useRef<ComponentMap>({}) + const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null) + + const append = React.useCallback<ContextType['append']>((id, component) => { + if (map.current[id]) return + map.current[id] = <React.Fragment key={id}>{component}</React.Fragment> + setOutlet(<>{Object.values(map.current)}</>) + }, []) + + const remove = React.useCallback<ContextType['remove']>(id => { + delete map.current[id] + setOutlet(<>{Object.values(map.current)}</>) + }, []) + + return ( + <Context.Provider value={{outlet, append, remove}}> + {props.children} + </Context.Provider> + ) +} + +export function Outlet() { + const ctx = React.useContext(Context) + return ctx.outlet +} + +export function Portal({children}: React.PropsWithChildren<{}>) { + const {append, remove} = React.useContext(Context) + const id = React.useId() + React.useEffect(() => { + append(id, children as Component) + return () => remove(id) + }, [id, children, append, remove]) + return null +} diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx new file mode 100644 index 000000000..7115f6190 --- /dev/null +++ b/src/components/Prompt.tsx @@ -0,0 +1,119 @@ +import React from 'react' +import {View, PressableProps} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useTheme, atoms as a} from '#/alf' +import {H4, P} from '#/components/Typography' +import {Button} from '#/components/Button' + +import * as Dialog from '#/components/Dialog' + +export {useDialogControl as usePromptControl} from '#/components/Dialog' + +const Context = React.createContext<{ + titleId: string + descriptionId: string +}>({ + titleId: '', + descriptionId: '', +}) + +export function Outer({ + children, + control, +}: React.PropsWithChildren<{ + control: Dialog.DialogOuterProps['control'] +}>) { + const titleId = React.useId() + const descriptionId = React.useId() + + const context = React.useMemo( + () => ({titleId, descriptionId}), + [titleId, descriptionId], + ) + + return ( + <Dialog.Outer control={control}> + <Context.Provider value={context}> + <Dialog.Handle /> + + <Dialog.Inner + accessibilityLabelledBy={titleId} + accessibilityDescribedBy={descriptionId} + style={{width: 'auto', maxWidth: 400}}> + {children} + </Dialog.Inner> + </Context.Provider> + </Dialog.Outer> + ) +} + +export function Title({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const {titleId} = React.useContext(Context) + return ( + <H4 + nativeID={titleId} + style={[a.font_bold, t.atoms.text_contrast_700, a.pb_sm]}> + {children} + </H4> + ) +} + +export function Description({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const {descriptionId} = React.useContext(Context) + return ( + <P nativeID={descriptionId} style={[t.atoms.text, a.pb_lg]}> + {children} + </P> + ) +} + +export function Actions({children}: React.PropsWithChildren<{}>) { + return ( + <View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}> + {children} + </View> + ) +} + +export function Cancel({ + children, +}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { + const {_} = useLingui() + const {close} = Dialog.useDialogContext() + return ( + <Button + variant="solid" + color="secondary" + size="small" + label={_(msg`Cancel`)} + onPress={close}> + {children} + </Button> + ) +} + +export function Action({ + children, + onPress, +}: React.PropsWithChildren<{onPress?: () => void}>) { + const {_} = useLingui() + const {close} = Dialog.useDialogContext() + const handleOnPress = React.useCallback(() => { + close() + onPress?.() + }, [close, onPress]) + return ( + <Button + variant="solid" + color="primary" + size="small" + label={_(msg`Confirm`)} + onPress={handleOnPress}> + {children} + </Button> + ) +} diff --git a/src/view/com/Typography.tsx b/src/components/Typography.tsx index 6579c2e51..66cf0720d 100644 --- a/src/view/com/Typography.tsx +++ b/src/components/Typography.tsx @@ -1,6 +1,7 @@ import React from 'react' import {Text as RNText, TextProps} from 'react-native' -import {useTheme, atoms, web} from '#/alf' + +import {useTheme, atoms, web, flatten} from '#/alf' export function Text({style, ...rest}: TextProps) { const t = useTheme() @@ -18,7 +19,7 @@ export function H1({style, ...rest}: TextProps) { <RNText {...attr} {...rest} - style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]} + style={[atoms.text_5xl, atoms.font_bold, t.atoms.text, flatten(style)]} /> ) } @@ -34,7 +35,7 @@ export function H2({style, ...rest}: TextProps) { <RNText {...attr} {...rest} - style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]} + style={[atoms.text_4xl, atoms.font_bold, t.atoms.text, flatten(style)]} /> ) } @@ -50,7 +51,7 @@ export function H3({style, ...rest}: TextProps) { <RNText {...attr} {...rest} - style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]} + style={[atoms.text_3xl, atoms.font_bold, t.atoms.text, flatten(style)]} /> ) } @@ -66,7 +67,7 @@ export function H4({style, ...rest}: TextProps) { <RNText {...attr} {...rest} - style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]} + style={[atoms.text_2xl, atoms.font_bold, t.atoms.text, flatten(style)]} /> ) } @@ -82,7 +83,7 @@ export function H5({style, ...rest}: TextProps) { <RNText {...attr} {...rest} - style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]} + style={[atoms.text_xl, atoms.font_bold, t.atoms.text, flatten(style)]} /> ) } @@ -98,7 +99,26 @@ export function H6({style, ...rest}: TextProps) { <RNText {...attr} {...rest} - style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]} + style={[atoms.text_lg, atoms.font_bold, t.atoms.text, flatten(style)]} + /> + ) +} + +export function P({style, ...rest}: TextProps) { + const t = useTheme() + const attr = + web({ + role: 'paragraph', + }) || {} + const _style = flatten(style) + const lineHeight = + (_style?.lineHeight || atoms.text_md.lineHeight) * + atoms.leading_normal.lineHeight + return ( + <RNText + {...attr} + {...rest} + style={[atoms.text_md, t.atoms.text, _style, {lineHeight}]} /> ) } diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx new file mode 100644 index 000000000..83fa285f5 --- /dev/null +++ b/src/components/forms/DateField/index.android.tsx @@ -0,0 +1,108 @@ +import React from 'react' +import {View, Pressable} from 'react-native' +import DateTimePicker, { + BaseProps as DateTimePickerProps, +} from '@react-native-community/datetimepicker' + +import {useTheme, atoms} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import * as TextField from '#/components/forms/TextField' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' + +import {DateFieldProps} from '#/components/forms/DateField/types' +import { + localizeDate, + toSimpleDateString, +} from '#/components/forms/DateField/utils' + +export * as utils from '#/components/forms/DateField/utils' +export const Label = TextField.Label + +export function DateField({ + value, + onChangeDate, + label, + isInvalid, + testID, +}: DateFieldProps) { + const t = useTheme() + const [open, setOpen] = React.useState(false) + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const {chromeFocus, chromeError, chromeErrorHover} = + TextField.useSharedInputStyles() + + const onChangeInternal = React.useCallback< + Required<DateTimePickerProps>['onChange'] + >( + (_event, date) => { + setOpen(false) + + if (date) { + const formatted = toSimpleDateString(date) + onChangeDate(formatted) + } + }, + [onChangeDate, setOpen], + ) + + return ( + <View style={[atoms.relative, atoms.w_full]}> + <Pressable + aria-label={label} + accessibilityLabel={label} + accessibilityHint={undefined} + onPress={() => setOpen(true)} + onPressIn={onPressIn} + onPressOut={onPressOut} + onFocus={onFocus} + onBlur={onBlur} + style={[ + { + paddingTop: 16, + paddingBottom: 16, + borderColor: 'transparent', + borderWidth: 2, + }, + atoms.flex_row, + atoms.flex_1, + atoms.w_full, + atoms.px_lg, + atoms.rounded_sm, + t.atoms.bg_contrast_50, + focused || pressed ? chromeFocus : {}, + isInvalid ? chromeError : {}, + isInvalid && (focused || pressed) ? chromeErrorHover : {}, + ]}> + <TextField.Icon icon={CalendarDays} /> + + <Text + style={[atoms.text_md, atoms.pl_xs, t.atoms.text, {paddingTop: 3}]}> + {localizeDate(value)} + </Text> + </Pressable> + + {open && ( + <DateTimePicker + aria-label={label} + accessibilityLabel={label} + accessibilityHint={undefined} + testID={`${testID}-datepicker`} + mode="date" + timeZoneName={'Etc/UTC'} + display="spinner" + // @ts-ignore applies in iOS only -prf + themeVariant={t.name === 'dark' ? 'dark' : 'light'} + value={new Date(value)} + onChange={onChangeInternal} + /> + )} + </View> + ) +} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx new file mode 100644 index 000000000..c359a9d46 --- /dev/null +++ b/src/components/forms/DateField/index.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import {View} from 'react-native' +import DateTimePicker, { + DateTimePickerEvent, +} from '@react-native-community/datetimepicker' + +import {useTheme, atoms} from '#/alf' +import * as TextField from '#/components/forms/TextField' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import {DateFieldProps} from '#/components/forms/DateField/types' + +export * as utils from '#/components/forms/DateField/utils' +export const Label = TextField.Label + +/** + * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date + * changes in the same format. + * + * For dates of unknown format, convert with the + * `utils.toSimpleDateString(Date)` export of this file. + */ +export function DateField({ + value, + onChangeDate, + testID, + label, +}: DateFieldProps) { + const t = useTheme() + + const onChangeInternal = React.useCallback( + (event: DateTimePickerEvent, date: Date | undefined) => { + if (date) { + const formatted = toSimpleDateString(date) + onChangeDate(formatted) + } + }, + [onChangeDate], + ) + + return ( + <View style={[atoms.relative, atoms.w_full]}> + <DateTimePicker + aria-label={label} + accessibilityLabel={label} + accessibilityHint={undefined} + testID={`${testID}-datepicker`} + mode="date" + timeZoneName={'Etc/UTC'} + display="spinner" + themeVariant={t.name === 'dark' ? 'dark' : 'light'} + value={new Date(value)} + onChange={onChangeInternal} + /> + </View> + ) +} diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx new file mode 100644 index 000000000..32f38a5d1 --- /dev/null +++ b/src/components/forms/DateField/index.web.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import {TextInput, TextInputProps, StyleSheet} from 'react-native' +// @ts-ignore +import {unstable_createElement} from 'react-native-web' + +import * as TextField from '#/components/forms/TextField' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import {DateFieldProps} from '#/components/forms/DateField/types' + +export * as utils from '#/components/forms/DateField/utils' +export const Label = TextField.Label + +const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>( + ({style, ...props}, ref) => { + return unstable_createElement('input', { + ...props, + ref, + type: 'date', + style: [ + StyleSheet.flatten(style), + { + background: 'transparent', + border: 0, + }, + ], + }) + }, +) + +InputBase.displayName = 'InputBase' + +const Input = TextField.createInput(InputBase as unknown as typeof TextInput) + +export function DateField({ + value, + onChangeDate, + label, + isInvalid, + testID, +}: DateFieldProps) { + const handleOnChange = React.useCallback( + (e: any) => { + const date = e.target.valueAsDate || e.target.value + + if (date) { + const formatted = toSimpleDateString(date) + onChangeDate(formatted) + } + }, + [onChangeDate], + ) + + return ( + <TextField.Root isInvalid={isInvalid}> + <Input + value={value} + label={label} + onChange={handleOnChange} + onChangeText={() => {}} + testID={testID} + /> + </TextField.Root> + ) +} diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts new file mode 100644 index 000000000..129f5672d --- /dev/null +++ b/src/components/forms/DateField/types.ts @@ -0,0 +1,7 @@ +export type DateFieldProps = { + value: string + onChangeDate: (date: string) => void + label: string + isInvalid?: boolean + testID?: string +} diff --git a/src/components/forms/DateField/utils.ts b/src/components/forms/DateField/utils.ts new file mode 100644 index 000000000..c787272fe --- /dev/null +++ b/src/components/forms/DateField/utils.ts @@ -0,0 +1,16 @@ +import {getLocales} from 'expo-localization' + +const LOCALE = getLocales()[0] + +// we need the date in the form yyyy-MM-dd to pass to the input +export function toSimpleDateString(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return _date.toISOString().split('T')[0] +} + +export function localizeDate(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return new Intl.DateTimeFormat(LOCALE.languageTag, { + timeZone: 'UTC', + }).format(_date) +} diff --git a/src/components/forms/InputGroup.tsx b/src/components/forms/InputGroup.tsx new file mode 100644 index 000000000..6908d4df8 --- /dev/null +++ b/src/components/forms/InputGroup.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms, useTheme} from '#/alf' + +/** + * NOT FINISHED, just here as a reference + */ +export function InputGroup(props: React.PropsWithChildren<{}>) { + const t = useTheme() + const children = React.Children.toArray(props.children) + const total = children.length + return ( + <View style={[atoms.w_full]}> + {children.map((child, i) => { + return React.isValidElement(child) ? ( + <React.Fragment key={i}> + {i > 0 ? ( + <View + style={[atoms.border_b, {borderColor: t.palette.contrast_500}]} + /> + ) : null} + {React.cloneElement(child, { + // @ts-ignore + style: [ + ...(Array.isArray(child.props?.style) + ? child.props.style + : [child.props.style || {}]), + { + borderTopLeftRadius: i > 0 ? 0 : undefined, + borderTopRightRadius: i > 0 ? 0 : undefined, + borderBottomLeftRadius: i < total - 1 ? 0 : undefined, + borderBottomRightRadius: i < total - 1 ? 0 : undefined, + borderBottomWidth: i < total - 1 ? 0 : undefined, + }, + ], + })} + </React.Fragment> + ) : null + })} + </View> + ) +} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx new file mode 100644 index 000000000..1ee58303a --- /dev/null +++ b/src/components/forms/TextField.tsx @@ -0,0 +1,334 @@ +import React from 'react' +import { + View, + TextInput, + TextInputProps, + TextStyle, + ViewStyle, + Pressable, + StyleSheet, + AccessibilityProps, +} from 'react-native' + +import {HITSLOP_20} from 'lib/constants' +import {isWeb} from '#/platform/detection' +import {useTheme, atoms as a, web, tokens, android} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Props as SVGIconProps} from '#/components/icons/common' + +const Context = React.createContext<{ + inputRef: React.RefObject<TextInput> | null + isInvalid: boolean + hovered: boolean + onHoverIn: () => void + onHoverOut: () => void + focused: boolean + onFocus: () => void + onBlur: () => void +}>({ + inputRef: null, + isInvalid: false, + hovered: false, + onHoverIn: () => {}, + onHoverOut: () => {}, + focused: false, + onFocus: () => {}, + onBlur: () => {}, +}) + +export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}> + +export function Root({children, isInvalid = false}: RootProps) { + const inputRef = React.useRef<TextInput>(null) + const rootRef = React.useRef<View>(null) + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const context = React.useMemo( + () => ({ + inputRef, + hovered, + onHoverIn, + onHoverOut, + focused, + onFocus, + onBlur, + isInvalid, + }), + [ + inputRef, + hovered, + onHoverIn, + onHoverOut, + focused, + onFocus, + onBlur, + isInvalid, + ], + ) + + React.useLayoutEffect(() => { + const root = rootRef.current + if (!root || !isWeb) return + // @ts-ignore web only + root.tabIndex = -1 + }, []) + + return ( + <Context.Provider value={context}> + <Pressable + accessibilityRole="button" + ref={rootRef} + role="none" + style={[ + a.flex_row, + a.align_center, + a.relative, + a.w_full, + a.px_md, + { + paddingVertical: 14, + }, + ]} + // onPressIn/out don't work on android web + onPress={() => inputRef.current?.focus()} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut}> + {children} + </Pressable> + </Context.Provider> + ) +} + +export function useSharedInputStyles() { + const t = useTheme() + return React.useMemo(() => { + const hover: ViewStyle[] = [ + { + borderColor: t.palette.contrast_100, + }, + ] + const focus: ViewStyle[] = [ + { + backgroundColor: t.palette.contrast_50, + borderColor: t.palette.primary_500, + }, + ] + const error: ViewStyle[] = [ + { + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: + t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, + }, + ] + const errorHover: ViewStyle[] = [ + { + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: tokens.color.red_500, + }, + ] + + return { + chromeHover: StyleSheet.flatten(hover), + chromeFocus: StyleSheet.flatten(focus), + chromeError: StyleSheet.flatten(error), + chromeErrorHover: StyleSheet.flatten(errorHover), + } + }, [t]) +} + +export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & { + label: string + value: string + onChangeText: (value: string) => void + isInvalid?: boolean +} + +export function createInput(Component: typeof TextInput) { + return function Input({ + label, + placeholder, + value, + onChangeText, + isInvalid, + ...rest + }: InputProps) { + const t = useTheme() + const ctx = React.useContext(Context) + const withinRoot = Boolean(ctx.inputRef) + + const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = + useSharedInputStyles() + + if (!withinRoot) { + return ( + <Root isInvalid={isInvalid}> + <Input + label={label} + placeholder={placeholder} + value={value} + onChangeText={onChangeText} + isInvalid={isInvalid} + {...rest} + /> + </Root> + ) + } + + return ( + <> + <Component + accessibilityHint={undefined} + {...rest} + aria-label={label} + accessibilityLabel={label} + ref={ctx.inputRef} + value={value} + onChangeText={onChangeText} + onFocus={ctx.onFocus} + onBlur={ctx.onBlur} + placeholder={placeholder || label} + placeholderTextColor={t.palette.contrast_500} + hitSlop={HITSLOP_20} + style={[ + a.relative, + a.z_20, + a.flex_1, + a.text_md, + t.atoms.text, + a.px_xs, + android({ + paddingBottom: 2, + }), + { + lineHeight: a.text_md.lineHeight * 1.1875, + textAlignVertical: rest.multiline ? 'top' : undefined, + minHeight: rest.multiline ? 60 : undefined, + }, + ]} + /> + + <View + style={[ + a.z_10, + a.absolute, + a.inset_0, + a.rounded_sm, + t.atoms.bg_contrast_25, + {borderColor: 'transparent', borderWidth: 2}, + ctx.hovered ? chromeHover : {}, + ctx.focused ? chromeFocus : {}, + ctx.isInvalid || isInvalid ? chromeError : {}, + (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) + ? chromeErrorHover + : {}, + ]} + /> + </> + ) + } +} + +export const Input = createInput(TextInput) + +export function Label({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + return ( + <Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_600, a.mb_sm]}> + {children} + </Text> + ) +} + +export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { + const t = useTheme() + const ctx = React.useContext(Context) + const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { + const hover: TextStyle[] = [ + { + color: t.palette.contrast_800, + }, + ] + const focus: TextStyle[] = [ + { + color: t.palette.primary_500, + }, + ] + const errorHover: TextStyle[] = [ + { + color: t.palette.negative_500, + }, + ] + const errorFocus: TextStyle[] = [ + { + color: t.palette.negative_500, + }, + ] + + return { + hover, + focus, + errorHover, + errorFocus, + } + }, [t]) + + return ( + <View style={[a.z_20, a.pr_xs]}> + <Comp + size="md" + style={[ + {color: t.palette.contrast_500, pointerEvents: 'none'}, + ctx.hovered ? hover : {}, + ctx.focused ? focus : {}, + ctx.isInvalid && ctx.hovered ? errorHover : {}, + ctx.isInvalid && ctx.focused ? errorFocus : {}, + ]} + /> + </View> + ) +} + +export function Suffix({ + children, + label, + accessibilityHint, +}: React.PropsWithChildren<{ + label: string + accessibilityHint?: AccessibilityProps['accessibilityHint'] +}>) { + const t = useTheme() + const ctx = React.useContext(Context) + return ( + <Text + aria-label={label} + accessibilityLabel={label} + accessibilityHint={accessibilityHint} + style={[ + a.z_20, + a.pr_sm, + a.text_md, + t.atoms.text_contrast_400, + { + pointerEvents: 'none', + }, + web({ + marginTop: -2, + }), + ctx.hovered || ctx.focused + ? { + color: t.palette.contrast_800, + } + : {}, + ]}> + {children} + </Text> + ) +} diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx new file mode 100644 index 000000000..ad82bdff5 --- /dev/null +++ b/src/components/forms/Toggle.tsx @@ -0,0 +1,473 @@ +import React from 'react' +import {Pressable, View, ViewStyle} from 'react-native' + +import {HITSLOP_10} from 'lib/constants' +import {useTheme, atoms as a, web, native} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' + +export type ItemState = { + name: string + selected: boolean + disabled: boolean + isInvalid: boolean + hovered: boolean + pressed: boolean + focused: boolean +} + +const ItemContext = React.createContext<ItemState>({ + name: '', + selected: false, + disabled: false, + isInvalid: false, + hovered: false, + pressed: false, + focused: false, +}) + +const GroupContext = React.createContext<{ + values: string[] + disabled: boolean + type: 'radio' | 'checkbox' + maxSelectionsReached: boolean + setFieldValue: (props: {name: string; value: boolean}) => void +}>({ + type: 'checkbox', + values: [], + disabled: false, + maxSelectionsReached: false, + setFieldValue: () => {}, +}) + +export type GroupProps = React.PropsWithChildren<{ + type?: 'radio' | 'checkbox' + values: string[] + maxSelections?: number + disabled?: boolean + onChange: (value: string[]) => void + label: string +}> + +export type ItemProps = { + type?: 'radio' | 'checkbox' + name: string + label: string + value?: boolean + disabled?: boolean + onChange?: (selected: boolean) => void + isInvalid?: boolean + style?: (state: ItemState) => ViewStyle + children: ((props: ItemState) => React.ReactNode) | React.ReactNode +} + +export function useItemContext() { + return React.useContext(ItemContext) +} + +export function Group({ + children, + values: providedValues, + onChange, + disabled = false, + type = 'checkbox', + maxSelections, + label, +}: GroupProps) { + const groupRole = type === 'radio' ? 'radiogroup' : undefined + const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues + const [maxReached, setMaxReached] = React.useState(false) + + const setFieldValue = React.useCallback< + (props: {name: string; value: boolean}) => void + >( + ({name, value}) => { + if (type === 'checkbox') { + const pruned = values.filter(v => v !== name) + const next = value ? pruned.concat(name) : pruned + onChange(next) + } else { + onChange([name]) + } + }, + [type, onChange, values], + ) + + React.useEffect(() => { + if (type === 'checkbox') { + if ( + maxSelections && + values.length >= maxSelections && + maxReached === false + ) { + setMaxReached(true) + } else if ( + maxSelections && + values.length < maxSelections && + maxReached === true + ) { + setMaxReached(false) + } + } + }, [type, values.length, maxSelections, maxReached, setMaxReached]) + + const context = React.useMemo( + () => ({ + values, + type, + disabled, + maxSelectionsReached: maxReached, + setFieldValue, + }), + [values, disabled, type, maxReached, setFieldValue], + ) + + return ( + <GroupContext.Provider value={context}> + <View + role={groupRole} + {...(groupRole === 'radiogroup' + ? { + 'aria-label': label, + accessibilityLabel: label, + accessibilityRole: groupRole, + } + : {})}> + {children} + </View> + </GroupContext.Provider> + ) +} + +export function Item({ + children, + name, + value = false, + disabled: itemDisabled = false, + onChange, + isInvalid, + style, + type = 'checkbox', + label, + ...rest +}: ItemProps) { + const { + values: selectedValues, + type: groupType, + disabled: groupDisabled, + setFieldValue, + maxSelectionsReached, + } = React.useContext(GroupContext) + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const role = groupType === 'radio' ? 'radio' : type + const selected = selectedValues.includes(name) || !!value + const disabled = + groupDisabled || itemDisabled || (!selected && maxSelectionsReached) + + const onPress = React.useCallback(() => { + const next = !selected + setFieldValue({name, value: next}) + onChange?.(next) + }, [name, selected, onChange, setFieldValue]) + + const state = React.useMemo( + () => ({ + name, + selected, + disabled: disabled ?? false, + isInvalid: isInvalid ?? false, + hovered, + pressed, + focused, + }), + [name, selected, disabled, hovered, pressed, focused, isInvalid], + ) + + return ( + <ItemContext.Provider value={state}> + <Pressable + accessibilityHint={undefined} // optional + hitSlop={HITSLOP_10} + {...rest} + disabled={disabled} + aria-disabled={disabled ?? false} + aria-checked={selected} + aria-invalid={isInvalid} + aria-label={label} + role={role} + accessibilityRole={role} + accessibilityState={{ + disabled: disabled ?? false, + selected: selected, + }} + accessibilityLabel={label} + onPress={onPress} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut} + onPressIn={onPressIn} + onPressOut={onPressOut} + onFocus={onFocus} + onBlur={onBlur} + style={[ + a.flex_row, + a.align_center, + a.gap_sm, + focused ? web({outline: 'none'}) : {}, + style?.(state), + ]}> + {typeof children === 'function' ? children(state) : children} + </Pressable> + </ItemContext.Provider> + ) +} + +export function Label({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const {disabled} = useItemContext() + return ( + <Text + style={[ + a.font_bold, + { + userSelect: 'none', + color: disabled ? t.palette.contrast_400 : t.palette.contrast_600, + }, + native({ + paddingTop: 3, + }), + ]}> + {children} + </Text> + ) +} + +// TODO(eric) refactor to memoize styles without knowledge of state +export function createSharedToggleStyles({ + theme: t, + hovered, + focused, + selected, + disabled, + isInvalid, +}: { + theme: ReturnType<typeof useTheme> + selected: boolean + hovered: boolean + focused: boolean + disabled: boolean + isInvalid: boolean +}) { + const base: ViewStyle[] = [] + const baseHover: ViewStyle[] = [] + const indicator: ViewStyle[] = [] + + if (selected) { + base.push({ + backgroundColor: + t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, + borderColor: t.palette.primary_500, + }) + + if (hovered || focused) { + baseHover.push({ + backgroundColor: + t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800, + borderColor: + t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400, + }) + } + } else { + if (hovered || focused) { + baseHover.push({ + backgroundColor: + t.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100, + borderColor: t.palette.contrast_500, + }) + } + } + + if (isInvalid) { + base.push({ + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: + t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, + }) + + if (hovered || focused) { + baseHover.push({ + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: t.palette.negative_500, + }) + } + } + + if (disabled) { + base.push({ + backgroundColor: t.palette.contrast_100, + borderColor: t.palette.contrast_400, + }) + } + + return { + baseStyles: base, + baseHoverStyles: disabled ? [] : baseHover, + indicatorStyles: indicator, + } +} + +export function Checkbox() { + const t = useTheme() + const {selected, hovered, focused, disabled, isInvalid} = useItemContext() + const {baseStyles, baseHoverStyles, indicatorStyles} = + createSharedToggleStyles({ + theme: t, + hovered, + focused, + selected, + disabled, + isInvalid, + }) + return ( + <View + style={[ + a.justify_center, + a.align_center, + a.border, + a.rounded_xs, + t.atoms.border_contrast, + { + height: 20, + width: 20, + }, + baseStyles, + hovered || focused ? baseHoverStyles : {}, + ]}> + {selected ? ( + <View + style={[ + a.absolute, + a.rounded_2xs, + {height: 12, width: 12}, + selected + ? { + backgroundColor: t.palette.primary_500, + } + : {}, + indicatorStyles, + ]} + /> + ) : null} + </View> + ) +} + +export function Switch() { + const t = useTheme() + const {selected, hovered, focused, disabled, isInvalid} = useItemContext() + const {baseStyles, baseHoverStyles, indicatorStyles} = + createSharedToggleStyles({ + theme: t, + hovered, + focused, + selected, + disabled, + isInvalid, + }) + return ( + <View + style={[ + a.relative, + a.border, + a.rounded_full, + t.atoms.bg, + t.atoms.border_contrast, + { + height: 20, + width: 30, + }, + baseStyles, + hovered || focused ? baseHoverStyles : {}, + ]}> + <View + style={[ + a.absolute, + a.rounded_full, + { + height: 12, + width: 12, + top: 3, + left: 3, + backgroundColor: t.palette.contrast_400, + }, + selected + ? { + backgroundColor: t.palette.primary_500, + left: 13, + } + : {}, + indicatorStyles, + ]} + /> + </View> + ) +} + +export function Radio() { + const t = useTheme() + const {selected, hovered, focused, disabled, isInvalid} = + React.useContext(ItemContext) + const {baseStyles, baseHoverStyles, indicatorStyles} = + createSharedToggleStyles({ + theme: t, + hovered, + focused, + selected, + disabled, + isInvalid, + }) + return ( + <View + style={[ + a.justify_center, + a.align_center, + a.border, + a.rounded_full, + t.atoms.border_contrast, + { + height: 20, + width: 20, + }, + baseStyles, + hovered || focused ? baseHoverStyles : {}, + ]}> + {selected ? ( + <View + style={[ + a.absolute, + a.rounded_full, + {height: 12, width: 12}, + selected + ? { + backgroundColor: t.palette.primary_500, + } + : {}, + indicatorStyles, + ]} + /> + ) : null} + </View> + ) +} diff --git a/src/components/forms/ToggleButton.tsx b/src/components/forms/ToggleButton.tsx new file mode 100644 index 000000000..615fedae8 --- /dev/null +++ b/src/components/forms/ToggleButton.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native' + +import {atoms as a, useTheme, native} from '#/alf' +import {Text} from '#/components/Typography' + +import * as Toggle from '#/components/forms/Toggle' + +export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> & + AccessibilityProps & + React.PropsWithChildren<{}> + +export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & { + multiple?: boolean +} + +export function Group({children, multiple, ...props}: GroupProps) { + const t = useTheme() + return ( + <Toggle.Group type={multiple ? 'checkbox' : 'radio'} {...props}> + <View + style={[ + a.flex_row, + a.border, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border, + ]}> + {children} + </View> + </Toggle.Group> + ) +} + +export function Button({children, ...props}: ItemProps) { + return ( + <Toggle.Item {...props}> + <ButtonInner>{children}</ButtonInner> + </Toggle.Item> + ) +} + +function ButtonInner({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const state = Toggle.useItemContext() + + const {baseStyles, hoverStyles, activeStyles, textStyles} = + React.useMemo(() => { + const base: ViewStyle[] = [] + const hover: ViewStyle[] = [] + const active: ViewStyle[] = [] + const text: TextStyle[] = [] + + hover.push( + t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25, + ) + + if (state.selected) { + active.push({ + backgroundColor: t.palette.contrast_800, + }) + text.push(t.atoms.text_inverted) + hover.push({ + backgroundColor: t.palette.contrast_800, + }) + + if (state.disabled) { + active.push({ + backgroundColor: t.palette.contrast_500, + }) + } + } + + if (state.disabled) { + base.push({ + backgroundColor: t.palette.contrast_100, + }) + text.push({ + opacity: 0.5, + }) + } + + return { + baseStyles: base, + hoverStyles: hover, + activeStyles: active, + textStyles: text, + } + }, [t, state]) + + return ( + <View + style={[ + { + borderLeftWidth: 1, + marginLeft: -1, + }, + a.px_lg, + a.py_md, + native({ + paddingTop: 14, + }), + t.atoms.bg, + t.atoms.border, + baseStyles, + activeStyles, + (state.hovered || state.focused || state.pressed) && hoverStyles, + ]}> + {typeof children === 'string' ? ( + <Text + style={[ + a.text_center, + a.font_bold, + t.atoms.text_contrast_500, + textStyles, + ]}> + {children} + </Text> + ) : ( + children + )} + </View> + ) +} diff --git a/src/components/hooks/useInteractionState.ts b/src/components/hooks/useInteractionState.ts new file mode 100644 index 000000000..653b1c10e --- /dev/null +++ b/src/components/hooks/useInteractionState.ts @@ -0,0 +1,21 @@ +import React from 'react' + +export function useInteractionState() { + const [state, setState] = React.useState(false) + + const onIn = React.useCallback(() => { + setState(true) + }, [setState]) + const onOut = React.useCallback(() => { + setState(false) + }, [setState]) + + return React.useMemo( + () => ({ + state, + onIn, + onOut, + }), + [state, onIn, onOut], + ) +} diff --git a/src/components/icons/ArrowTopRight.tsx b/src/components/icons/ArrowTopRight.tsx new file mode 100644 index 000000000..92ad30a12 --- /dev/null +++ b/src/components/icons/ArrowTopRight.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z', +}) diff --git a/src/components/icons/CalendarDays.tsx b/src/components/icons/CalendarDays.tsx new file mode 100644 index 000000000..72cc48e26 --- /dev/null +++ b/src/components/icons/CalendarDays.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CalendarDays_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V9h14v10H5ZM5 7h14V5H5v2Zm3 10.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM17.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM9.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 17.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z', +}) diff --git a/src/components/icons/ColorPalette.tsx b/src/components/icons/ColorPalette.tsx new file mode 100644 index 000000000..157fa7fa1 --- /dev/null +++ b/src/components/icons/ColorPalette.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const ColorPalette_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 12c0-4.09 3.527-7.5 8-7.5s8 3.41 8 7.5c0 1.579-.419 2.056-.708 2.236-.388.241-1.031.286-2.058.153-.33-.043-.652-.096-.991-.152a65.905 65.905 0 0 0-.531-.087c-.52-.081-1.077-.156-1.61-.164-1.065-.016-2.336.245-2.996 1.567-.418.834-.295 1.67-.078 2.314.18.534.47 1.055.683 1.437v.001l.097.175.01.018C7.432 19.407 4 16.033 4 12Zm8-9.5C6.532 2.5 2 6.7 2 12s4.532 9.5 10 9.5c.401 0 .812-.04 1.166-.193.41-.176.761-.517.866-1.028.085-.416-.03-.796-.118-1.029a5.981 5.981 0 0 0-.351-.73l-.12-.215c-.215-.392-.403-.73-.52-1.078-.13-.387-.111-.614-.029-.78.146-.291.404-.473 1.178-.461.385.005.825.06 1.329.14.15.023.308.05.47.077.36.059.742.122 1.105.17 1.021.132 2.325.213 3.373-.439C21.496 15.22 22 13.874 22 12c0-5.3-4.532-9.5-10-9.5Zm3.5 8.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM9 12.25a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm1.5-2.75a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', +}) diff --git a/src/components/icons/Globe.tsx b/src/components/icons/Globe.tsx new file mode 100644 index 000000000..f81b3ff7a --- /dev/null +++ b/src/components/icons/Globe.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Globe_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z', +}) diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx new file mode 100644 index 000000000..9fc147037 --- /dev/null +++ b/src/components/icons/TEMPLATE.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +import {useCommonSVGProps, Props} from '#/components/icons/common' + +export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( + function LogoImpl(props: Props, ref) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + <Svg + fill="none" + {...rest} + // @ts-ignore it's fiiiiine + ref={ref} + viewBox="0 0 24 24" + width={size} + height={size} + style={[style]}> + <Path + fill={fill} + fillRule="evenodd" + clipRule="evenodd" + d="M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z" + /> + </Svg> + ) + }, +) + +export function createSinglePathSVG({path}: {path: string}) { + return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + <Svg + fill="none" + {...rest} + ref={ref} + viewBox="0 0 24 24" + width={size} + height={size} + style={[style]}> + <Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} /> + </Svg> + ) + }) +} diff --git a/src/components/icons/common.ts b/src/components/icons/common.ts new file mode 100644 index 000000000..9e9f15c4d --- /dev/null +++ b/src/components/icons/common.ts @@ -0,0 +1,32 @@ +import {StyleSheet, TextProps} from 'react-native' +import type {SvgProps, PathProps} from 'react-native-svg' + +import {tokens} from '#/alf' + +export type Props = { + fill?: PathProps['fill'] + style?: TextProps['style'] + size?: keyof typeof sizes +} & Omit<SvgProps, 'style' | 'size'> + +export const sizes = { + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, +} + +export function useCommonSVGProps(props: Props) { + const {fill, size, ...rest} = props + const style = StyleSheet.flatten(rest.style) + const _fill = fill || style?.color || tokens.color.blue_500 + const _size = Number(size ? sizes[size] : rest.width || sizes.md) + + return { + fill: _fill, + size: _size, + style, + ...rest, + } +} diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts index 91634927a..436a66d07 100644 --- a/src/lib/api/feed/home.ts +++ b/src/lib/api/feed/home.ts @@ -68,7 +68,8 @@ export class HomeFeedAPI implements FeedAPI { const res = await this.following.fetch({cursor, limit}) returnCursor = res.cursor posts = posts.concat(res.feed) - if (res.feed.length === 0 || !cursor) { + if (!returnCursor) { + cursor = '' posts.push(FALLBACK_MARKER_POST) this.usingDiscover = true } diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 0f97eb080..3270b6f07 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -68,11 +68,12 @@ export function parseEmbedPlayerFromUrl( // youtube if (urlp.hostname === 'youtu.be') { const videoId = urlp.pathname.split('/')[1] + const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0) if (videoId) { return { type: 'youtube_video', source: 'youtube', - playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`, + playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1&start=${seek}`, } } } @@ -84,13 +85,14 @@ export function parseEmbedPlayerFromUrl( const [_, page, shortVideoId] = urlp.pathname.split('/') const videoId = page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string) + const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0) if (videoId) { return { type: page === 'shorts' ? 'youtube_short' : 'youtube_video', source: page === 'shorts' ? 'youtubeShorts' : 'youtube', hideDetails: page === 'shorts' ? true : undefined, - playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`, + playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1&start=${seek}`, } } } diff --git a/src/lib/themes.ts b/src/lib/themes.ts index ad7574db6..2d4515c77 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -2,30 +2,32 @@ import {Platform} from 'react-native' import type {Theme} from './ThemeContext' import {colors} from './styles' +import {darkPalette, lightPalette} from '#/alf/themes' + export const defaultTheme: Theme = { colorScheme: 'light', palette: { default: { - background: colors.white, - backgroundLight: colors.gray1, - text: colors.black, - textLight: colors.gray5, - textInverted: colors.white, - link: colors.blue3, - border: '#f0e9e9', - borderDark: '#e0d9d9', - icon: colors.gray4, + background: lightPalette.white, + backgroundLight: lightPalette.contrast_50, + text: lightPalette.black, + textLight: lightPalette.contrast_700, + textInverted: lightPalette.white, + link: lightPalette.primary_500, + border: lightPalette.contrast_100, + borderDark: lightPalette.contrast_200, + icon: lightPalette.contrast_500, // non-standard - textVeryLight: colors.gray4, - replyLine: colors.gray2, - replyLineDot: colors.gray3, - unreadNotifBg: '#ebf6ff', - unreadNotifBorder: colors.blue1, - postCtrl: '#71768A', - brandText: '#0066FF', - emptyStateIcon: '#B6B6C9', - borderLinkHover: '#cac1c1', + textVeryLight: lightPalette.contrast_400, + replyLine: lightPalette.contrast_100, + replyLineDot: lightPalette.contrast_200, + unreadNotifBg: lightPalette.primary_25, + unreadNotifBorder: lightPalette.primary_100, + postCtrl: lightPalette.contrast_500, + brandText: lightPalette.primary_500, + emptyStateIcon: lightPalette.contrast_300, + borderLinkHover: lightPalette.contrast_300, }, primary: { background: colors.blue3, @@ -50,15 +52,15 @@ export const defaultTheme: Theme = { icon: colors.green4, }, inverted: { - background: colors.black, - backgroundLight: colors.gray6, - text: colors.white, - textLight: colors.gray3, - textInverted: colors.black, - link: colors.blue2, - border: colors.gray3, - borderDark: colors.gray2, - icon: colors.gray5, + background: darkPalette.black, + backgroundLight: darkPalette.contrast_50, + text: darkPalette.white, + textLight: darkPalette.contrast_700, + textInverted: darkPalette.black, + link: darkPalette.primary_500, + border: darkPalette.contrast_100, + borderDark: darkPalette.contrast_200, + icon: darkPalette.contrast_500, }, error: { background: colors.red3, @@ -292,26 +294,26 @@ export const darkTheme: Theme = { palette: { ...defaultTheme.palette, default: { - background: colors.black, - backgroundLight: colors.gray7, - text: colors.white, - textLight: colors.gray3, - textInverted: colors.black, - link: colors.blue3, - border: colors.gray7, - borderDark: colors.gray6, - icon: colors.gray4, + background: darkPalette.black, + backgroundLight: darkPalette.contrast_50, + text: darkPalette.white, + textLight: darkPalette.contrast_700, + textInverted: darkPalette.black, + link: darkPalette.primary_500, + border: darkPalette.contrast_100, + borderDark: darkPalette.contrast_200, + icon: darkPalette.contrast_500, // non-standard - textVeryLight: colors.gray4, - replyLine: colors.gray5, - replyLineDot: colors.gray6, - unreadNotifBg: colors.blue7, - unreadNotifBorder: colors.blue6, - postCtrl: '#707489', - brandText: '#0085ff', - emptyStateIcon: colors.gray4, - borderLinkHover: colors.gray5, + textVeryLight: darkPalette.contrast_400, + replyLine: darkPalette.contrast_100, + replyLineDot: darkPalette.contrast_200, + unreadNotifBg: darkPalette.primary_975, + unreadNotifBorder: darkPalette.primary_900, + postCtrl: darkPalette.contrast_500, + brandText: darkPalette.primary_500, + emptyStateIcon: darkPalette.contrast_300, + borderLinkHover: darkPalette.contrast_300, }, primary: { ...defaultTheme.palette.primary, @@ -322,15 +324,15 @@ export const darkTheme: Theme = { textInverted: colors.green2, }, inverted: { - background: colors.white, - backgroundLight: colors.gray2, - text: colors.black, - textLight: colors.gray5, - textInverted: colors.white, - link: colors.blue3, - border: colors.gray3, - borderDark: colors.gray4, - icon: colors.gray1, + background: lightPalette.white, + backgroundLight: lightPalette.contrast_50, + text: lightPalette.black, + textLight: lightPalette.contrast_700, + textInverted: lightPalette.white, + link: lightPalette.primary_500, + border: lightPalette.contrast_100, + borderDark: lightPalette.contrast_200, + icon: lightPalette.contrast_500, }, }, } diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index 8b3bf5f3d..dddd6855c 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -137,6 +137,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { return AppLanguage.pt_BR case 'uk': return AppLanguage.uk + case 'ca': + return AppLanguage.ca default: continue } diff --git a/src/locale/i18n.ts b/src/locale/i18n.ts index 88ae10b62..d0bc828cf 100644 --- a/src/locale/i18n.ts +++ b/src/locale/i18n.ts @@ -13,6 +13,7 @@ import {messages as messagesJa} from '#/locale/locales/ja/messages' import {messages as messagesKo} from '#/locale/locales/ko/messages' import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages' import {messages as messagesUk} from '#/locale/locales/uk/messages' +import {messages as messagesCa} from '#/locale/locales/ca/messages' import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {AppLanguage} from '#/locale/languages' @@ -59,6 +60,10 @@ export async function dynamicActivate(locale: AppLanguage) { i18n.loadAndActivate({locale, messages: messagesUk}) break } + case AppLanguage.ca: { + i18n.loadAndActivate({locale, messages: messagesCa}) + break + } default: { i18n.loadAndActivate({locale, messages: messagesEn}) break diff --git a/src/locale/i18n.web.ts b/src/locale/i18n.web.ts index a6f0e158d..de5e5aa78 100644 --- a/src/locale/i18n.web.ts +++ b/src/locale/i18n.web.ts @@ -49,6 +49,10 @@ export async function dynamicActivate(locale: AppLanguage) { mod = await import(`./locales/uk/messages`) break } + case AppLanguage.ca: { + mod = await import(`./locales/ca/messages`) + break + } default: { mod = await import(`./locales/en/messages`) break diff --git a/src/locale/languages.ts b/src/locale/languages.ts index c6799816c..7b19fbe2f 100644 --- a/src/locale/languages.ts +++ b/src/locale/languages.ts @@ -16,6 +16,7 @@ export enum AppLanguage { ko = 'ko', pt_BR = 'pt-BR', uk = 'uk', + ca = 'ca', } interface AppLanguageConfig { @@ -35,6 +36,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [ {code2: AppLanguage.ko, name: '한국어'}, {code2: AppLanguage.pt_BR, name: 'Português (BR)'}, {code2: AppLanguage.uk, name: 'Українська'}, + {code2: AppLanguage.ca, name: 'Catalan'}, ] export const LANGUAGES: Language[] = [ diff --git a/src/locale/locales/ca/messages.po b/src/locale/locales/ca/messages.po new file mode 100644 index 000000000..106ca8137 --- /dev/null +++ b/src/locale/locales/ca/messages.po @@ -0,0 +1,2489 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2024-01-05 11:44+0530\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: ca\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: Ivan Beà\n" +"Language-Team: \n" +"X-Poedit-Language: Catalan\n" +"X-Poedit-Country: SPAIN\n" +"X-Poedit-SourceCharset: utf-8\n" + +#: src/view/shell/desktop/RightNav.tsx:168 +msgid "{0, plural, one {# invite code available} other {# invite codes available}}" +msgstr "{0, plural, one {# codi d'invitació disponible} other {# codis d'invitació disponibles}}" + +#: src/view/com/modals/Repost.tsx:44 +msgid "{0}" +msgstr "{0}" + +#: src/view/com/modals/CreateOrEditList.tsx:176 +msgid "{0} {purposeLabel} List" +msgstr "Llista {purposeLabel} {0}" + +#: src/view/shell/desktop/RightNav.tsx:151 +msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}" +msgstr "{invitesAvailable, plural, one {Codis d'invitació: # available} other {Codi d'invitació: # available}}" + +#: src/view/screens/Settings.tsx:407 +#: src/view/shell/Drawer.tsx:659 +msgid "{invitesAvailable} invite code available" +msgstr "{invitesAvailable} codi d'invitació disponible" + +#: src/view/screens/Settings.tsx:409 +#: src/view/shell/Drawer.tsx:661 +msgid "{invitesAvailable} invite codes available" +msgstr "{invitesAvailable} codis d'invitació disponibles" + +#: src/view/screens/Search/Search.tsx:87 +msgid "{message}" +msgstr "{message}" + +#: src/view/com/threadgate/WhoCanReply.tsx:158 +msgid "<0/> members" +msgstr "<0/> membres" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 +msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>" +msgstr "<0>Tria els teus</0><1>canals</1><2>recomanats</2>" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:37 +msgid "<0>Follow some</0><1>Recommended</1><2>Users</2>" +msgstr "<0>Segueix alguns</0><1>usuaris</1><2>recomanats</2>" + +#: src/view/com/util/moderation/LabelInfo.tsx:45 +msgid "A content warning has been applied to this {0}." +msgstr "S'ha aplicat una advertència de contingut a {0}" + +#: src/lib/hooks/useOTAUpdate.ts:16 +msgid "A new version of the app is available. Please update to continue using the app." +msgstr "Hi ha una nova versió d'aquesta aplicació. Actualitza-la per continuar." + +#: src/view/com/modals/EditImage.tsx:299 +#: src/view/screens/Settings.tsx:417 +msgid "Accessibility" +msgstr "Accessibilitat" + +#: src/view/com/auth/login/LoginForm.tsx:159 +#: src/view/screens/Settings.tsx:286 +msgid "Account" +msgstr "Compte" + +#: src/view/com/util/AccountDropdownBtn.tsx:41 +msgid "Account options" +msgstr "Opcions del compte" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:264 +#: src/view/com/modals/UserAddRemoveLists.tsx:193 +#: src/view/screens/ProfileList.tsx:763 +msgid "Add" +msgstr "Afegeix" + +#: src/view/com/modals/SelfLabel.tsx:56 +msgid "Add a content warning" +msgstr "Afegeix una advertència de contingut" + +#: src/view/screens/ProfileList.tsx:753 +msgid "Add a user to this list" +msgstr "Afegeix un usuari a aquesta llista" + +#: src/view/screens/Settings.tsx:355 +#: src/view/screens/Settings.tsx:364 +msgid "Add account" +msgstr "Afegeix un compte" + +#: src/view/com/composer/photos/Gallery.tsx:119 +#: src/view/com/composer/photos/Gallery.tsx:180 +msgid "Add alt text" +msgstr "Afegeix text alternatiu" + +#: src/view/com/modals/report/InputIssueDetails.tsx:41 +#: src/view/com/modals/report/Modal.tsx:191 +msgid "Add details" +msgstr "Afegeix detalls" + +#: src/view/com/modals/report/Modal.tsx:194 +msgid "Add details to report" +msgstr "Afegeix detalls a l'informe" + +#: src/view/com/composer/Composer.tsx:442 +msgid "Add link card" +msgstr "Afegeix una targeta a l'enllaç" + +#: src/view/com/composer/Composer.tsx:445 +msgid "Add link card:" +msgstr "Afegeix una targeta a l'enllaç:" + +#: src/view/com/modals/ChangeHandle.tsx:415 +msgid "Add the following DNS record to your domain:" +msgstr "Afegeix el següent registre DNS al teu domini:" + +#: src/view/com/profile/ProfileHeader.tsx:353 +msgid "Add to Lists" +msgstr "Afegeix a les llistes" + +#: src/view/screens/ProfileFeed.tsx:271 +msgid "Add to my feeds" +msgstr "Afegeix als meus canals" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:191 +#: src/view/com/modals/UserAddRemoveLists.tsx:128 +msgid "Added to list" +msgstr "Afegit a la llista" + +#: src/view/screens/PreferencesHomeFeed.tsx:164 +msgid "Adjust the number of likes a reply must have to be shown in your feed." +msgstr "Ajusta el nombre de m'agrades que hagi de tenir una resposta per aparèixer al teu canal." + +#: src/view/com/modals/SelfLabel.tsx:75 +msgid "Adult Content" +msgstr "Contingut per a adults" + +#: src/view/screens/Settings.tsx:569 +msgid "Advanced" +msgstr "Avançat" + +#: src/view/com/composer/photos/Gallery.tsx:130 +msgid "ALT" +msgstr "ALT" + +#: src/view/com/modals/EditImage.tsx:315 +msgid "Alt text" +msgstr "Text alternatiu" + +#: src/view/com/composer/photos/Gallery.tsx:209 +msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone." +msgstr "El text alternatiu descriu les imatges per a les persones cegues o amb problemes de visió, i ajuda a donar context a tothom." + +#: src/view/com/modals/VerifyEmail.tsx:118 +msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." +msgstr "S'ha enviat un correu a {0}. Inclou un codi de confirmació que has d'entrar aquí sota." + +#: src/view/com/modals/ChangeEmail.tsx:119 +msgid "An email has been sent to your previous address, {0}. It includes a confirmation code which you can enter below." +msgstr "S'ha enviat un correu a la teva adreça prèvia, {0}. Inclou un codi de confirmació que has d'entrar aquí sota." + +#: src/view/com/notifications/FeedItem.tsx:237 +#: src/view/com/threadgate/WhoCanReply.tsx:178 +msgid "and" +msgstr "i" + +#: src/view/screens/LanguageSettings.tsx:95 +msgid "App Language" +msgstr "Idioma de l'aplicació" + +#: src/view/screens/Settings.tsx:589 +msgid "App passwords" +msgstr "Contrasenyes de l'aplicació" + +#: src/view/screens/AppPasswords.tsx:186 +msgid "App Passwords" +msgstr "Contrasenyes de l'aplicació" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:236 +msgid "Appeal content warning" +msgstr "Afegeix una advertència de contingut" + +#: src/view/com/modals/AppealLabel.tsx:65 +msgid "Appeal Content Warning" +msgstr "Afegeix una advertència de contingut" +Apel·lació d'advertència sobre el contingut +#: src/view/com/util/moderation/LabelInfo.tsx:52 +msgid "Appeal this decision" +msgstr "Apel·la aquesta decisió" + +#: src/view/com/util/moderation/LabelInfo.tsx:56 +msgid "Appeal this decision." +msgstr "Apel·la aquesta decisió." + +#: src/view/screens/Settings.tsx:432 +msgid "Appearance" +msgstr "Aparença" + +#: src/view/screens/AppPasswords.tsx:223 +msgid "Are you sure you want to delete the app password \"{name}\"?" +msgstr "Confirmes que vols eliminar la contrasenya de l'aplicació \"{name}\"?" + +#: src/view/com/composer/Composer.tsx:142 +msgid "Are you sure you'd like to discard this draft?" +msgstr "Confirmes que vols descartar aquest esborrany?" + +#: src/view/screens/ProfileList.tsx:353 +msgid "Are you sure?" +msgstr "Ho confirmes?" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:219 +msgid "Are you sure? This cannot be undone." +msgstr "Ho confirmes? Aquesta acció no es pot desfer." + +#: src/view/com/modals/SelfLabel.tsx:123 +msgid "Artistic or non-erotic nudity." +msgstr "Nuesa artística o no eròtica." + +#: src/view/com/auth/create/CreateAccount.tsx:141 +#: src/view/com/auth/login/ChooseAccountForm.tsx:151 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:166 +#: src/view/com/auth/login/LoginForm.tsx:250 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:148 +#: src/view/com/modals/report/InputIssueDetails.tsx:46 +#: src/view/com/post-thread/PostThread.tsx:388 +#: src/view/com/post-thread/PostThread.tsx:438 +#: src/view/com/post-thread/PostThread.tsx:446 +#: src/view/com/profile/ProfileHeader.tsx:672 +msgid "Back" +msgstr "Endarrere" + +#: src/view/screens/Settings.tsx:461 +msgid "Basics" +msgstr "Conceptes bàsics" + +#: src/view/com/auth/create/Step2.tsx:156 +#: src/view/com/modals/BirthDateSettings.tsx:73 +msgid "Birthday" +msgstr "Aniversari" + +#: src/view/screens/Settings.tsx:312 +msgid "Birthday:" +msgstr "Aniversari:" + +#: src/view/com/profile/ProfileHeader.tsx:282 +#: src/view/com/profile/ProfileHeader.tsx:389 +msgid "Block Account" +msgstr "Bloqueja el compte" + +#: src/view/screens/ProfileList.tsx:523 +msgid "Block accounts" +msgstr "Bloqueja comptes" + +#: src/view/screens/ProfileList.tsx:473 +msgid "Block list" +msgstr "Bloqueja una llista" + +#: src/view/screens/ProfileList.tsx:308 +msgid "Block these accounts?" +msgstr "Vols bloquejar aquests comptes?" + +#: src/view/screens/Moderation.tsx:123 +msgid "Blocked accounts" +msgstr "Comptes bloquejats" + +#: src/view/screens/ModerationBlockedAccounts.tsx:107 +msgid "Blocked Accounts" +msgstr "Comptes bloquejats" + +#: src/view/com/profile/ProfileHeader.tsx:284 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "Els comptes bloquejats no poden respondre cap fil teu, ni anomenar-te ni interactuar amb tu de cap manera." + +#: src/view/screens/ModerationBlockedAccounts.tsx:115 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours." +msgstr "Els comptes bloquejats no poden respondre a cap fil teu, ni anomenar-te ni interactuar amb tu de cap manera. No veuràs mai el seu contingut ni ells el teu." + +#: src/view/com/post-thread/PostThread.tsx:250 +msgid "Blocked post." +msgstr "Publicació bloquejada." + +#: src/view/screens/ProfileList.tsx:310 +msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "El bloqueig és públic. Els comptes bloquejats no poden respondre els teus fils, ni mencionar-te ni interactuar amb tu de cap manera." + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:93 +msgid "Blog" +msgstr "Blog" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:31 +msgid "Bluesky" +msgstr "Bluesky" + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:80 +msgid "Bluesky is flexible." +msgstr "Bluesky és flexible." + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:69 +msgid "Bluesky is open." +msgstr "Bluesky és obert." + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:56 +msgid "Bluesky is public." +msgstr "Bluesky és públic." + +#: src/view/com/modals/Waitlist.tsx:70 +msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon." +msgstr "Bluesky utilitza les invitacions per construir una comunitat saludable. Si no coneixes ningú amb invitacions, pots apuntar-te a la llista d'espera i te n'enviarem una aviat." + +#: src/view/screens/Moderation.tsx:225 +msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." +msgstr "Bluesky no mostrarà el teu perfil ni les publicacions als usuaris que no estiguin registrats. Altres aplicacions poden no seguir aquesta demanda. Això no fa que el teu compte sigui privat." + +#: src/view/com/modals/ServerInput.tsx:78 +msgid "Bluesky.Social" +msgstr "Bluesky.Social" + +#: src/view/screens/Settings.tsx:718 +msgid "Build version {0} {1}" +msgstr "Versió {0} {1}" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:87 +msgid "Business" +msgstr "Negocis" + +#: src/view/com/composer/photos/OpenCameraBtn.tsx:60 +#: src/view/com/util/UserAvatar.tsx:221 +#: src/view/com/util/UserBanner.tsx:38 +msgid "Camera" +msgstr "Càmera" + +#: src/view/com/modals/AddAppPasswords.tsx:214 +msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." +msgstr "Només pot tenir lletres, números, espais, guions i guions baixos. Ha de tenir almenys 4 caràcters i no més de 32." + +#: src/view/com/composer/Composer.tsx:289 +#: src/view/com/composer/Composer.tsx:292 +#: src/view/com/modals/AltImage.tsx:128 +#: src/view/com/modals/ChangeEmail.tsx:218 +#: src/view/com/modals/ChangeEmail.tsx:220 +#: src/view/com/modals/Confirm.tsx:88 +#: src/view/com/modals/CreateOrEditList.tsx:267 +#: src/view/com/modals/CreateOrEditList.tsx:272 +#: src/view/com/modals/DeleteAccount.tsx:150 +#: src/view/com/modals/DeleteAccount.tsx:223 +#: src/view/com/modals/EditImage.tsx:323 +#: src/view/com/modals/EditProfile.tsx:248 +#: src/view/com/modals/LinkWarning.tsx:85 +#: src/view/com/modals/Repost.tsx:73 +#: src/view/com/modals/Waitlist.tsx:136 +#: src/view/screens/Search/Search.tsx:601 +#: src/view/shell/desktop/Search.tsx:182 +msgid "Cancel" +msgstr "Cancel·la" + +#: src/view/com/modals/DeleteAccount.tsx:146 +#: src/view/com/modals/DeleteAccount.tsx:219 +msgid "Cancel account deletion" +msgstr "Cancel·la la supressió del compte" + +#: src/view/com/modals/AltImage.tsx:123 +msgid "Cancel add image alt text" +msgstr "Cancel·la afegir text a la imatge" + +#: src/view/com/modals/ChangeHandle.tsx:149 +msgid "Cancel change handle" +msgstr "Cancel·la el canvi d'identificador" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:134 +msgid "Cancel image crop" +msgstr "Cancel·la la retallada de la imatge" + +#: src/view/com/modals/EditProfile.tsx:243 +msgid "Cancel profile editing" +msgstr "Cancel·la l'edició del perfil" + +#: src/view/com/modals/Repost.tsx:64 +msgid "Cancel quote post" +msgstr "Cancel·la la citació de la publicació" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:87 +#: src/view/shell/desktop/Search.tsx:178 +msgid "Cancel search" +msgstr "Cancel·la la cerca" + +#: src/view/com/modals/Waitlist.tsx:132 +msgid "Cancel waitlist signup" +msgstr "Cancel·la la inscripció a la llista d'espera" + +#: src/view/screens/Settings.tsx:306 +msgid "Change" +msgstr "Canvia" + +#: src/view/screens/Settings.tsx:601 +#: src/view/screens/Settings.tsx:610 +msgid "Change handle" +msgstr "Canvia l'identificador" + +#: src/view/com/modals/ChangeHandle.tsx:161 +msgid "Change Handle" +msgstr "Canvia l'identificador" + +#: src/view/com/modals/VerifyEmail.tsx:141 +msgid "Change my email" +msgstr "Canvia el meu correu" + +#: src/view/com/modals/ChangeEmail.tsx:109 +msgid "Change Your Email" +msgstr "Canvia el teu correu" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121 +msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds." +msgstr "Mira alguns canals recomanats. Prem + per afegir-los als teus canals fixats." + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:185 +msgid "Check out some recommended users. Follow them to see similar users." +msgstr "Mira alguns usuaris recomanats. Segueix-los per veure altres usuaris similars." + +#: src/view/com/modals/DeleteAccount.tsx:163 +msgid "Check your inbox for an email with the confirmation code to enter below:" +msgstr "Comprova el teu correu per rebre el codi de confirmació i entra'l aquí sota:" + +#: src/view/com/modals/Threadgate.tsx:72 +msgid "Choose \"Everybody\" or \"Nobody\"" +msgstr "Tria \"Tothom\" or \"Ningú\"" + +#: src/view/com/modals/ServerInput.tsx:38 +msgid "Choose Service" +msgstr "Tria un servei" + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:83 +msgid "Choose the algorithms that power your experience with custom feeds." +msgstr "Tria els algoritmes que potenciaran la teva experiència amb els canals personalitzats." + +#: src/view/com/auth/create/Step2.tsx:127 +msgid "Choose your password" +msgstr "Tria la teva contrasenya." + +#: src/view/screens/Settings.tsx:694 +msgid "Clear all legacy storage data" +msgstr "Esborra totes les dades antigues emmagatzemades" + +#: src/view/screens/Settings.tsx:696 +msgid "Clear all legacy storage data (restart after this)" +msgstr "Esborra totes les dades antigues emmagatzemades (i després reinicia)" + +#: src/view/screens/Settings.tsx:706 +msgid "Clear all storage data" +msgstr "Esborra totes les dades emmagatzemades" + +#: src/view/screens/Settings.tsx:708 +msgid "Clear all storage data (restart after this)" +msgstr "Esborra totes les dades emmagatzemades (i després reinicia)" + +#: src/view/com/util/forms/SearchInput.tsx:74 +#: src/view/screens/Search/Search.tsx:582 +msgid "Clear search query" +msgstr "Esborra la cerca" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:38 +msgid "Close alert" +msgstr "Tanca l'advertència" + +#: src/view/com/util/BottomSheetCustomBackdrop.tsx:33 +msgid "Close bottom drawer" +msgstr "Tanca el calaix inferior" + +#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:26 +msgid "Close image" +msgstr "Tanca la imatge" + +#: src/view/com/lightbox/Lightbox.web.tsx:112 +msgid "Close image viewer" +msgstr "Tanca el visor d'imatges" + +#: src/view/shell/index.web.tsx:49 +msgid "Close navigation footer" +msgstr "Tanca el peu de la navegació" + +#: src/view/screens/CommunityGuidelines.tsx:32 +msgid "Community Guidelines" +msgstr "Directrius de la comunitat" + +#: src/view/com/composer/Prompt.tsx:24 +msgid "Compose reply" +msgstr "Redacta una resposta" + +#: src/view/com/modals/AppealLabel.tsx:98 +#: src/view/com/modals/Confirm.tsx:75 +#: src/view/com/modals/SelfLabel.tsx:154 +#: src/view/com/modals/VerifyEmail.tsx:225 +#: src/view/screens/PreferencesHomeFeed.tsx:299 +#: src/view/screens/PreferencesThreads.tsx:153 +msgid "Confirm" +msgstr "Confirma" + +#: src/view/com/modals/ChangeEmail.tsx:193 +#: src/view/com/modals/ChangeEmail.tsx:195 +msgid "Confirm Change" +msgstr "Confirma el canvi" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:34 +msgid "Confirm content language settings" +msgstr "Confirma la configuració de l'idioma del contingut" + +#: src/view/com/modals/DeleteAccount.tsx:209 +msgid "Confirm delete account" +msgstr "Confirma l'eliminació del compte" + +#: src/view/com/modals/ChangeEmail.tsx:157 +#: src/view/com/modals/DeleteAccount.tsx:176 +#: src/view/com/modals/VerifyEmail.tsx:159 +msgid "Confirmation code" +msgstr "Codi de confirmació" + +#: src/view/com/auth/create/CreateAccount.tsx:174 +#: src/view/com/auth/login/LoginForm.tsx:269 +msgid "Connecting..." +msgstr "Connectant…" + +#: src/view/screens/Moderation.tsx:81 +msgid "Content filtering" +msgstr "Filtre de contingut" + +#: src/view/com/modals/ContentFilteringSettings.tsx:44 +msgid "Content Filtering" +msgstr "Filtre de contingut" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 +#: src/view/screens/LanguageSettings.tsx:278 +msgid "Content Languages" +msgstr "Idiomes del contingut" + +#: src/view/com/util/moderation/ScreenHider.tsx:78 +msgid "Content Warning" +msgstr "Advertència del contingut" + +#: src/view/com/composer/labels/LabelsBtn.tsx:31 +msgid "Content warnings" +msgstr "Advertències del contingut" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:148 +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:209 +msgid "Continue" +msgstr "Continua" + +#: src/view/com/modals/AddAppPasswords.tsx:193 +#: src/view/com/modals/InviteCodes.tsx:179 +msgid "Copied" +msgstr "Copiat" + +#: src/view/com/modals/AddAppPasswords.tsx:186 +msgid "Copy" +msgstr "Copia" + +#: src/view/screens/ProfileList.tsx:385 +msgid "Copy link to list" +msgstr "Copia l'enllaç a la llista" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:139 +msgid "Copy link to post" +msgstr "Copia l'enllaç a la publicació" + +#: src/view/com/profile/ProfileHeader.tsx:338 +msgid "Copy link to profile" +msgstr "Copia l'enllaç al perfil" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:125 +msgid "Copy post text" +msgstr "Copia el text de la publicació" + +#: src/view/screens/CopyrightPolicy.tsx:29 +msgid "Copyright Policy" +msgstr "Política de drets d'autor" + +#: src/view/screens/ProfileFeed.tsx:95 +msgid "Could not load feed" +msgstr "No es pot carregar el canal" + +#: src/view/screens/ProfileList.tsx:839 +msgid "Could not load list" +msgstr "No es pot carregar la llista" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:62 +#: src/view/com/auth/SplashScreen.tsx:46 +msgid "Create a new account" +msgstr "Crea un nou compte" + +#: src/view/com/auth/create/CreateAccount.tsx:121 +msgid "Create Account" +msgstr "Crea un compte" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:54 +#: src/view/com/auth/SplashScreen.tsx:43 +msgid "Create new account" +msgstr "Crea un nou compte" + +#: src/view/screens/AppPasswords.tsx:248 +msgid "Created {0}" +msgstr "Creat {0}" + +#: src/view/com/modals/ChangeHandle.tsx:387 +#: src/view/com/modals/ServerInput.tsx:102 +msgid "Custom domain" +msgstr "Domini personalitzat" + +#: src/view/screens/Settings.tsx:615 +msgid "Danger Zone" +msgstr "Zona de perill" + +#: src/view/screens/Settings.tsx:622 +msgid "Delete account" +msgstr "Elimina el compte" + +#: src/view/com/modals/DeleteAccount.tsx:83 +msgid "Delete Account" +msgstr "Elimina el compte" + +#: src/view/screens/AppPasswords.tsx:221 +#: src/view/screens/AppPasswords.tsx:241 +msgid "Delete app password" +msgstr "Elimina la contrasenya d'aplicació" + +#: src/view/screens/ProfileList.tsx:352 +#: src/view/screens/ProfileList.tsx:412 +msgid "Delete List" +msgstr "Elimina la llista" + +#: src/view/com/modals/DeleteAccount.tsx:212 +msgid "Delete my account" +msgstr "Elimina el meu compte" + +#: src/view/screens/Settings.tsx:632 +msgid "Delete my account…" +msgstr "Elimina el meu compte…" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:214 +msgid "Delete post" +msgstr "Elimina la publicació" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:218 +msgid "Delete this post?" +msgstr "Vols eliminar aquesta publicació?" + +#: src/view/com/post-thread/PostThread.tsx:242 +msgid "Deleted post." +msgstr "Publicació eliminada." + +#: src/view/com/modals/CreateOrEditList.tsx:218 +#: src/view/com/modals/CreateOrEditList.tsx:234 +#: src/view/com/modals/EditProfile.tsx:197 +#: src/view/com/modals/EditProfile.tsx:209 +msgid "Description" +msgstr "Descripció" + +#: src/view/com/auth/create/Step1.tsx:96 +msgid "Dev Server" +msgstr "Servidor de desenvolupament" + +#: src/view/screens/Settings.tsx:637 +msgid "Developer Tools" +msgstr "Eines de desenvolupador" + +#: src/view/com/composer/Composer.tsx:210 +msgid "Did you want to say anything?" +msgstr "Vols dir alguna cosa?" + +#: src/view/com/composer/Composer.tsx:143 +msgid "Discard" +msgstr "Descarta" + +#: src/view/com/composer/Composer.tsx:137 +msgid "Discard draft" +msgstr "Descarta l'esborrany" + +#: src/view/screens/Moderation.tsx:207 +msgid "Discourage apps from showing my account to logged-out users" +msgstr "Evita que les aplicacions mostrin el meu compte als usuaris no connectats" + +#: src/view/screens/Feeds.tsx:405 +msgid "Discover new feeds" +msgstr "Descobreix nous canals" + +#: src/view/com/modals/EditProfile.tsx:191 +msgid "Display name" +msgstr "Nom mostrat" + +#: src/view/com/modals/EditProfile.tsx:179 +msgid "Display Name" +msgstr "Nom mostrat" + +#: src/view/com/modals/ChangeHandle.tsx:485 +msgid "Domain verified!" +msgstr "Domini verificat!" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:86 +#: src/view/com/modals/ContentFilteringSettings.tsx:88 +#: src/view/com/modals/ContentFilteringSettings.tsx:96 +#: src/view/com/modals/crop-image/CropImage.web.tsx:152 +#: src/view/com/modals/EditImage.tsx:333 +#: src/view/com/modals/ListAddRemoveUsers.tsx:142 +#: src/view/com/modals/SelfLabel.tsx:157 +#: src/view/com/modals/Threadgate.tsx:129 +#: src/view/com/modals/Threadgate.tsx:132 +#: src/view/com/modals/UserAddRemoveLists.tsx:79 +#: src/view/screens/PreferencesHomeFeed.tsx:302 +#: src/view/screens/PreferencesThreads.tsx:156 +msgid "Done" +msgstr "Fet" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:42 +msgid "Done{extraText}" +msgstr "Fet{extraText}" + +#: src/view/com/modals/InviteCodes.tsx:94 +msgid "Each code works once. You'll receive more invite codes periodically." +msgstr "Cada codi funciona un cop. Rebràs més codis d'invitació periòdicament." + +#: src/view/com/composer/photos/Gallery.tsx:144 +#: src/view/com/modals/EditImage.tsx:207 +msgid "Edit image" +msgstr "Edita la imatge" + +#: src/view/screens/ProfileList.tsx:400 +msgid "Edit list details" +msgstr "Edita els detalls de la llista" + +#: src/view/screens/Feeds.tsx:367 +#: src/view/screens/SavedFeeds.tsx:84 +msgid "Edit My Feeds" +msgstr "Edita els meus canals" + +#: src/view/com/modals/EditProfile.tsx:151 +msgid "Edit my profile" +msgstr "Edita el meu perfil" + +#: src/view/com/profile/ProfileHeader.tsx:453 +msgid "Edit profile" +msgstr "Edita el perfil" + +#: src/view/com/profile/ProfileHeader.tsx:456 +msgid "Edit Profile" +msgstr "Edita el perfil" + +#: src/view/screens/Feeds.tsx:330 +msgid "Edit Saved Feeds" +msgstr "Edita els meus canals guardats" + +#: src/view/com/auth/create/Step2.tsx:108 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:148 +#: src/view/com/modals/ChangeEmail.tsx:141 +#: src/view/com/modals/Waitlist.tsx:88 +msgid "Email" +msgstr "Correu" + +#: src/view/com/auth/create/Step2.tsx:99 +msgid "Email address" +msgstr "Adreça de correu" + +#: src/view/com/modals/ChangeEmail.tsx:111 +msgid "Email Updated" +msgstr "Correu actualitzat" + +#: src/view/screens/Settings.tsx:290 +msgid "Email:" +msgstr "Correu:" + +#: src/view/screens/PreferencesHomeFeed.tsx:138 +msgid "Enable this setting to only see replies between people you follow." +msgstr "Activa aquesta opció per veure només les respostes entre els comptes que segueixes" + +#: src/view/screens/Profile.tsx:426 +msgid "End of feed" +msgstr "Fi del canal" + +#: src/view/com/auth/create/Step1.tsx:71 +msgid "Enter the address of your provider:" +msgstr "Introdueix l'adreça del teu proveïdor:" + +#: src/view/com/modals/ChangeHandle.tsx:369 +msgid "Enter the domain you want to use" +msgstr "Introdueix el domini que vols utilitzar" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:101 +msgid "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." +msgstr "Introdueix el correu que vas fer servir per crear el teu compte. T'enviarem un \"codi de restabliment\" perquè puguis posar una nova contrasenya." + +#: src/view/com/auth/create/Step2.tsx:104 +msgid "Enter your email address" +msgstr "Introdueix el teu correu" + +#: src/view/com/modals/ChangeEmail.tsx:117 +msgid "Enter your new email address below." +msgstr "Introdueix el teu nou correu a continuació." + +#: src/view/com/auth/login/Login.tsx:99 +msgid "Enter your username and password" +msgstr "Introdueix el teu usuari i contrasenya" + +#: src/view/screens/Search/Search.tsx:105 +msgid "Error:" +msgstr "Error:" + +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "Tothom" + +#: src/view/com/lightbox/Lightbox.web.tsx:156 +msgid "Expand alt text" +msgstr "Expandeix el text alternatiu" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:109 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:141 +msgid "Failed to load recommended feeds" +msgstr "Error en carregar els canals recomanats" + +#: src/view/screens/Feeds.tsx:556 +msgid "Feed offline" +msgstr "Canal fora de línia" + +#: src/view/com/feeds/FeedPage.tsx:143 +msgid "Feed Preferences" +msgstr "Preferències del canal" + +#: src/view/shell/desktop/RightNav.tsx:73 +#: src/view/shell/Drawer.tsx:311 +msgid "Feedback" +msgstr "Comentaris" + +#: src/view/screens/Feeds.tsx:475 +#: src/view/screens/Profile.tsx:165 +#: src/view/shell/bottom-bar/BottomBar.tsx:181 +#: src/view/shell/desktop/LeftNav.tsx:342 +#: src/view/shell/Drawer.tsx:474 +#: src/view/shell/Drawer.tsx:475 +msgid "Feeds" +msgstr "Canals" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57 +msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." +msgstr "Els canals són creats pels usuaris per curar contingut. Tria els canals que trobis interessants." + +#: src/view/screens/SavedFeeds.tsx:156 +msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." +msgstr "Els canals són algoritmes personalitzats creats per usuaris que coneixen una mica de codi. <0/> per a més informació." + +#: src/view/screens/Search/Search.tsx:427 +msgid "Find users on Bluesky" +msgstr "Troba usuaris a Bluesky" + +#: src/view/screens/Search/Search.tsx:425 +msgid "Find users with the search tool on the right" +msgstr "Troba usuaris amb l'eina de cerca de la dreta" + +#: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:150 +msgid "Finding similar accounts..." +msgstr "Troba comptes similars…" + +#: src/view/screens/PreferencesHomeFeed.tsx:102 +msgid "Fine-tune the content you see on your home screen." +msgstr "Ajusta el contingut que es veu a la teva pantalla d'inici." + +#: src/view/screens/PreferencesThreads.tsx:60 +msgid "Fine-tune the discussion threads." +msgstr "Ajusta els fils de discussió." + +#: src/view/com/profile/ProfileHeader.tsx:538 +msgid "Follow" +msgstr "Segueix" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:64 +msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." +msgstr "Segueix a alguns usuaris per començar. Te'n podem recomanar més basant-nos en els que trobes interessants." + +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "Usuaris seguits" + +#: src/view/screens/PreferencesHomeFeed.tsx:145 +msgid "Followed users only" +msgstr "Només els usuaris seguits" + +#: src/view/screens/ProfileFollowers.tsx:25 +msgid "Followers" +msgstr "Seguidors" + +#: src/view/com/profile/ProfileHeader.tsx:624 +msgid "following" +msgstr "seguint" + +#: src/view/com/profile/ProfileHeader.tsx:522 +#: src/view/screens/ProfileFollows.tsx:25 +msgid "Following" +msgstr "Seguint" + +#: src/view/com/profile/ProfileHeader.tsx:571 +msgid "Follows you" +msgstr "Et segueix" + +#: src/view/com/modals/DeleteAccount.tsx:107 +msgid "For security reasons, we'll need to send a confirmation code to your email address." +msgstr "Per motius de seguretat necessitem enviar-te un codi de confirmació al teu correu." + +#: src/view/com/modals/AddAppPasswords.tsx:207 +msgid "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." +msgstr "Per motius de seguretat no podràs tornar-la a veure. Si perds aquesta contrasenya necessitaràs generar-ne una de nova." + +#: src/view/com/auth/login/LoginForm.tsx:232 +msgid "Forgot" +msgstr "L'he oblidat" + +#: src/view/com/auth/login/LoginForm.tsx:229 +msgid "Forgot password" +msgstr "He oblidat la contrasenya" + +#: src/view/com/auth/login/Login.tsx:127 +#: src/view/com/auth/login/Login.tsx:143 +msgid "Forgot Password" +msgstr "He oblidat la contrasenya" + +#: src/view/com/composer/photos/SelectPhotoBtn.tsx:43 +msgid "Gallery" +msgstr "Galeria" + +#: src/view/com/modals/VerifyEmail.tsx:183 +msgid "Get Started" +msgstr "Comença" + +#: src/view/com/auth/LoggedOut.tsx:81 +#: src/view/com/auth/LoggedOut.tsx:82 +#: src/view/com/util/moderation/ScreenHider.tsx:123 +#: src/view/shell/desktop/LeftNav.tsx:104 +msgid "Go back" +msgstr "Ves enrere" + +#: src/view/screens/ProfileFeed.tsx:104 +#: src/view/screens/ProfileFeed.tsx:109 +#: src/view/screens/ProfileList.tsx:848 +#: src/view/screens/ProfileList.tsx:853 +msgid "Go Back" +msgstr "Ves enrere" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:181 +#: src/view/com/auth/login/LoginForm.tsx:279 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:163 +msgid "Go to next" +msgstr "Ves al següent" + +#: src/view/com/modals/ChangeHandle.tsx:265 +msgid "Handle" +msgstr "Identificador" + +#: src/view/shell/desktop/RightNav.tsx:102 +#: src/view/shell/Drawer.tsx:321 +msgid "Help" +msgstr "Ajuda" + +#: src/view/com/modals/AddAppPasswords.tsx:148 +msgid "Here is your app password." +msgstr "Aquí tens la teva contrasenya d'aplicació." + +#: src/view/com/notifications/FeedItem.tsx:324 +#: src/view/com/util/moderation/ContentHider.tsx:103 +msgid "Hide" +msgstr "Amaga" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:173 +msgid "Hide post" +msgstr "Amaga l'entrada" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:177 +msgid "Hide this post?" +msgstr "Vols amagar aquesta entrada?" + +#: src/view/com/notifications/FeedItem.tsx:316 +msgid "Hide user list" +msgstr "Amaga la llista d'usuaris" + +#: src/view/com/posts/FeedErrorMessage.tsx:110 +msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue." +msgstr "S'ha produït algun error quan s'intentava connectar amb el servidor del canal. Avisa al propietari del canal d'aquest problema." + +#: src/view/com/posts/FeedErrorMessage.tsx:98 +msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue." +msgstr "Sembla que el servidor del canal està mal configurat. Avisa al propietari del canal d'aquest problema." + +#: src/view/com/posts/FeedErrorMessage.tsx:104 +msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue." +msgstr "Sembla que el servidor del canal està sense connexió. Avisa al propietari del canal d'aquest problema." + +#: src/view/com/posts/FeedErrorMessage.tsx:101 +msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue." +msgstr "El servidor del canal ha donat una resposta incorrecta. Avisa al propietari del canal d'aquest problema." + +#: src/view/com/posts/FeedErrorMessage.tsx:95 +msgid "Hmm, we're having trouble finding this feed. It may have been deleted." +msgstr "Tenim problemes per trobar aquest canal. Potser ha estat eliminat." + +#: src/view/shell/bottom-bar/BottomBar.tsx:137 +#: src/view/shell/desktop/LeftNav.tsx:306 +#: src/view/shell/Drawer.tsx:398 +#: src/view/shell/Drawer.tsx:399 +msgid "Home" +msgstr "Inici" + +#: src/view/com/pager/FeedsTabBarMobile.tsx:96 +#: src/view/screens/PreferencesHomeFeed.tsx:95 +#: src/view/screens/Settings.tsx:481 +msgid "Home Feed Preferences" +msgstr "Preferències dels canals a l'inici" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:114 +msgid "Hosting provider" +msgstr "Proveïdor d'allotjament" + +#: src/view/com/auth/create/Step1.tsx:76 +#: src/view/com/auth/create/Step1.tsx:81 +msgid "Hosting provider address" +msgstr "Adreça del proveïdor d'allotjament" + +#: src/view/com/modals/VerifyEmail.tsx:208 +msgid "I have a code" +msgstr "Tinc un codi" + +#: src/view/com/modals/ChangeHandle.tsx:281 +msgid "I have my own domain" +msgstr "Tinc el meu propi domini" + +#: src/view/com/modals/SelfLabel.tsx:127 +msgid "If none are selected, suitable for all ages." +msgstr "Si no en selecciones cap, és apropiat per a totes les edats." + +#: src/view/com/modals/AltImage.tsx:97 +msgid "Image alt text" +msgstr "Text alternatiu de la imatge" + +#: src/view/com/util/UserAvatar.tsx:308 +#: src/view/com/util/UserBanner.tsx:116 +msgid "Image options" +msgstr "Opcions de la imatge" + +#: src/view/com/auth/login/LoginForm.tsx:113 +msgid "Invalid username or password" +msgstr "Nom d'usuari o contrasenya incorrectes" + +#: src/view/screens/Settings.tsx:383 +msgid "Invite" +msgstr "Convida" + +#: src/view/com/modals/InviteCodes.tsx:91 +#: src/view/screens/Settings.tsx:371 +msgid "Invite a Friend" +msgstr "Convida un amic" + +#: src/view/com/auth/create/Step2.tsx:72 +msgid "Invite code" +msgstr "Codi d'invitació" + +#: src/view/com/auth/create/state.ts:136 +msgid "Invite code not accepted. Check that you input it correctly and try again." +msgstr "Codi d'invitació rebutjat. Comprova que l'has entrat correctament i torna-ho a provar." + +#: src/view/shell/Drawer.tsx:640 +msgid "Invite codes: {invitesAvailable} available" +msgstr "Codis d'invitació: {invitesAvailable} disponibles" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:99 +msgid "Jobs" +msgstr "Feines" + +#: src/view/com/modals/Waitlist.tsx:67 +msgid "Join the waitlist" +msgstr "Uneix-te a la llista d'espera" + +#: src/view/com/auth/create/Step2.tsx:86 +#: src/view/com/auth/create/Step2.tsx:90 +msgid "Join the waitlist." +msgstr "Uneix-te a la llista d'espera." + +#: src/view/com/modals/Waitlist.tsx:124 +msgid "Join Waitlist" +msgstr "Uneix-te a la llista d'espera" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:104 +msgid "Language selection" +msgstr "Tria l'idioma" + +#: src/view/screens/LanguageSettings.tsx:89 +msgid "Language Settings" +msgstr "Configuració d'idioma" + +#: src/view/screens/Settings.tsx:541 +msgid "Languages" +msgstr "Idiomes" + +#: src/view/com/util/moderation/ContentHider.tsx:101 +msgid "Learn more" +msgstr "Més informació" + +#: src/view/com/util/moderation/PostAlerts.tsx:47 +#: src/view/com/util/moderation/ProfileHeaderAlerts.tsx:65 +#: src/view/com/util/moderation/ScreenHider.tsx:104 +msgid "Learn More" +msgstr "Més informació" + +#: src/view/com/util/moderation/ContentHider.tsx:83 +#: src/view/com/util/moderation/PostAlerts.tsx:40 +#: src/view/com/util/moderation/PostHider.tsx:76 +#: src/view/com/util/moderation/ProfileHeaderAlerts.tsx:49 +#: src/view/com/util/moderation/ScreenHider.tsx:101 +msgid "Learn more about this warning" +msgstr "Més informació d'aquesta advertència" + +#: src/view/screens/Moderation.tsx:242 +msgid "Learn more about what is public on Bluesky." +msgstr "Més informació sobre què és públic a Bluesky." + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 +msgid "Leave them all unchecked to see any language." +msgstr "Deixa'ls tots sense marcar per veure tots els idiomes" + +#: src/view/com/modals/LinkWarning.tsx:49 +msgid "Leaving Bluesky" +msgstr "Sortint de Bluesky" + +#: src/view/com/auth/login/Login.tsx:128 +#: src/view/com/auth/login/Login.tsx:144 +msgid "Let's get your password reset!" +msgstr "Restablirem la teva contrasenya!" + +#: src/view/com/util/UserAvatar.tsx:245 +#: src/view/com/util/UserBanner.tsx:60 +msgid "Library" +msgstr "Biblioteca" + +#: src/view/screens/ProfileFeed.tsx:586 +msgid "Like this feed" +msgstr "Fes m'agrada a aquest canal" + +#: src/view/screens/PostLikedBy.tsx:27 +#: src/view/screens/ProfileFeedLikedBy.tsx:27 +msgid "Liked by" +msgstr "Li ha agradat a" + +#: src/view/screens/Profile.tsx:164 +msgid "Likes" +msgstr "M'agrades" + +#: src/view/com/modals/CreateOrEditList.tsx:186 +msgid "List Avatar" +msgstr "Avatar de la llista" + +#: src/view/com/modals/CreateOrEditList.tsx:199 +msgid "List Name" +msgstr "Nom de la llista" + +#: src/view/screens/Profile.tsx:166 +#: src/view/shell/desktop/LeftNav.tsx:379 +#: src/view/shell/Drawer.tsx:490 +#: src/view/shell/Drawer.tsx:491 +msgid "Lists" +msgstr "Llistes" + +#: src/view/com/post-thread/PostThread.tsx:259 +#: src/view/com/post-thread/PostThread.tsx:267 +msgid "Load more posts" +msgstr "Carrega més publicacions" + +#: src/view/screens/Notifications.tsx:148 +msgid "Load new notifications" +msgstr "Carrega noves notificacions" + +#: src/view/com/feeds/FeedPage.tsx:189 +msgid "Load new posts" +msgstr "Carrega noves publicacions" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:95 +msgid "Loading..." +msgstr "Carregant…" + +#: src/view/com/modals/ServerInput.tsx:50 +msgid "Local dev server" +msgstr "Servidor de desenvolupament local" + +#: src/view/screens/Moderation.tsx:136 +msgid "Logged-out visibility" +msgstr "Visibilitat pels usuaris no connectats" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:133 +msgid "Login to account that is not listed" +msgstr "Accedeix a un compte que no està llistat" + +#: src/view/com/modals/LinkWarning.tsx:63 +msgid "Make sure this is where you intend to go!" +msgstr "Assegura't que és aquí on vols anar!" + +#: src/view/screens/Profile.tsx:163 +msgid "Media" +msgstr "Contingut" + +#: src/view/com/threadgate/WhoCanReply.tsx:139 +msgid "mentioned users" +msgstr "usuaris mencionats" + +#: src/view/com/modals/Threadgate.tsx:93 +msgid "Mentioned users" +msgstr "Usuaris mencionats" + +#: src/view/screens/Search/Search.tsx:537 +msgid "Menu" +msgstr "Menú" + +#: src/view/com/posts/FeedErrorMessage.tsx:194 +msgid "Message from server" +msgstr "Missatge del servidor" + +#: src/view/screens/Moderation.tsx:64 +#: src/view/screens/Settings.tsx:563 +#: src/view/shell/desktop/LeftNav.tsx:397 +#: src/view/shell/Drawer.tsx:509 +#: src/view/shell/Drawer.tsx:510 +msgid "Moderation" +msgstr "Moderació" + +#: src/view/screens/Moderation.tsx:95 +msgid "Moderation lists" +msgstr "Llistes de moderació" + +#: src/view/screens/ModerationModlists.tsx:58 +msgid "Moderation Lists" +msgstr "Llistes de moderació" + +#: src/view/shell/desktop/Feeds.tsx:53 +msgid "More feeds" +msgstr "Més canals" + +#: src/view/com/profile/ProfileHeader.tsx:548 +#: src/view/screens/ProfileFeed.tsx:361 +#: src/view/screens/ProfileList.tsx:584 +msgid "More options" +msgstr "Més opcions" + +#: src/view/com/profile/ProfileHeader.tsx:370 +msgid "Mute Account" +msgstr "Silenciar el compte" + +#: src/view/screens/ProfileList.tsx:511 +msgid "Mute accounts" +msgstr "Silencia els comptes" + +#: src/view/screens/ProfileList.tsx:458 +msgid "Mute list" +msgstr "Silencia la llista" + +#: src/view/screens/ProfileList.tsx:271 +msgid "Mute these accounts?" +msgstr "Vols silenciar aquests comptes?" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:157 +msgid "Mute thread" +msgstr "Silencia el fil de discussió" + +#: src/view/screens/Moderation.tsx:109 +msgid "Muted accounts" +msgstr "Comptes silenciats" + +#: src/view/screens/ModerationMutedAccounts.tsx:107 +msgid "Muted Accounts" +msgstr "Comptes silenciats" + +#: src/view/screens/ModerationMutedAccounts.tsx:115 +msgid "Muted accounts have their posts removed from your feed and from your notifications. Mutes are completely private." +msgstr "Les publicacions dels comptes silenciats seran eliminats del teu canal i de les teves notificacions. Silenciar comptes és completament privat." + +#: src/view/screens/ProfileList.tsx:273 +msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." +msgstr "Silenciar és privat. Els comptes silenciats poden interactuar amb tu, però tu no veuràs les seves publicacions ni rebràs notificacions seves." + +#: src/view/com/modals/BirthDateSettings.tsx:56 +msgid "My Birthday" +msgstr "El meu aniversari" + +#: src/view/screens/Feeds.tsx:363 +msgid "My Feeds" +msgstr "Els meus canals" + +#: src/view/shell/desktop/LeftNav.tsx:65 +msgid "My Profile" +msgstr "El meu perfil" + +#: src/view/screens/Settings.tsx:520 +msgid "My Saved Feeds" +msgstr "Els meus canals desats" + +#: src/view/com/modals/AddAppPasswords.tsx:177 +#: src/view/com/modals/CreateOrEditList.tsx:211 +msgid "Name" +msgstr "Nom" + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:72 +msgid "Never lose access to your followers and data." +msgstr "No perdis mai accés als teus seguidors ni a les teves dades." + +#: src/view/screens/Lists.tsx:76 +#: src/view/screens/ModerationModlists.tsx:78 +msgid "New" +msgstr "Nou" + +#: src/view/com/feeds/FeedPage.tsx:200 +#: src/view/screens/Feeds.tsx:507 +#: src/view/screens/Profile.tsx:354 +#: src/view/screens/ProfileFeed.tsx:431 +#: src/view/screens/ProfileList.tsx:194 +#: src/view/screens/ProfileList.tsx:222 +#: src/view/shell/desktop/LeftNav.tsx:248 +msgid "New post" +msgstr "Nova publicació" + +#: src/view/shell/desktop/LeftNav.tsx:258 +msgid "New Post" +msgstr "Nova publicació" + +#: src/view/com/auth/create/CreateAccount.tsx:154 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:174 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:184 +#: src/view/com/auth/login/LoginForm.tsx:282 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:156 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:166 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:79 +msgid "Next" +msgstr "Següent" + +#: src/view/com/lightbox/Lightbox.web.tsx:142 +msgid "Next image" +msgstr "Següent imatge" + +#: src/view/screens/PreferencesHomeFeed.tsx:120 +#: src/view/screens/PreferencesHomeFeed.tsx:191 +#: src/view/screens/PreferencesHomeFeed.tsx:226 +#: src/view/screens/PreferencesHomeFeed.tsx:263 +msgid "No" +msgstr "No" + +#: src/view/screens/ProfileFeed.tsx:579 +#: src/view/screens/ProfileList.tsx:720 +msgid "No description" +msgstr "Cap descripció" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:97 +msgid "No result" +msgstr "Cap resultat" + +#: src/view/screens/Feeds.tsx:452 +msgid "No results found for \"{query}\"" +msgstr "No s'han trobat resultats per \"{query}\"" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:127 +#: src/view/screens/Search/Search.tsx:270 +#: src/view/screens/Search/Search.tsx:298 +#: src/view/screens/Search/Search.tsx:629 +#: src/view/shell/desktop/Search.tsx:210 +msgid "No results found for {query}" +msgstr "No s'han trobat resultats per {query}" + +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "Ningú" + +#: src/view/com/modals/SelfLabel.tsx:135 +msgid "Not Applicable." +msgstr "No aplicable." + +#: src/view/screens/Moderation.tsx:232 +msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." +msgstr "Nota: Bluesky és una xarxa oberta i pública. Aquesta configuració tan sols limita el teu contingut a l'aplicació de Bluesky i a la web, altres aplicacions poden no respectar-ho. El teu contingut pot ser mostrat a usuaris no connectats per altres aplicacions i webs." + +#: src/view/screens/Notifications.tsx:113 +#: src/view/screens/Notifications.tsx:137 +#: src/view/shell/bottom-bar/BottomBar.tsx:205 +#: src/view/shell/desktop/LeftNav.tsx:361 +#: src/view/shell/Drawer.tsx:435 +#: src/view/shell/Drawer.tsx:436 +msgid "Notifications" +msgstr "Notificacions" + +#: src/view/com/util/ErrorBoundary.tsx:34 +msgid "Oh no!" +msgstr "Ostres!" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 +msgid "Okay" +msgstr "D'acord" + +#: src/view/com/composer/Composer.tsx:358 +msgid "One or more images is missing alt text." +msgstr "Falta el text alternatiu a una o més imatges." + +#: src/view/com/threadgate/WhoCanReply.tsx:100 +msgid "Only {0} can reply." +msgstr "Només {0} pot respondre." + +#: src/view/com/pager/FeedsTabBarMobile.tsx:76 +msgid "Open navigation" +msgstr "Obre la navegació" + +#: src/view/screens/Settings.tsx:533 +msgid "Opens configurable language settings" +msgstr "Obre les configuracions d'idioma" + +#: src/view/shell/desktop/RightNav.tsx:156 +#: src/view/shell/Drawer.tsx:641 +msgid "Opens list of invite codes" +msgstr "Obre la llista de codis d'invitació" + +#: src/view/com/modals/ChangeHandle.tsx:279 +msgid "Opens modal for using custom domain" +msgstr "Obre el modal per a utilitzar un domini personalitzat" + +#: src/view/screens/Settings.tsx:558 +msgid "Opens moderation settings" +msgstr "Obre la configuració de la moderació" + +#: src/view/screens/Settings.tsx:514 +msgid "Opens screen with all saved feeds" +msgstr "Obre la pantalla amb tots els canals desats" + +#: src/view/screens/Settings.tsx:581 +msgid "Opens the app password settings page" +msgstr "Obre la pàgina de configuració de les contrasenyes d'aplicació" + +#: src/view/screens/Settings.tsx:473 +msgid "Opens the home feed preferences" +msgstr "Obre les preferències de canals de l'inici" + +#: src/view/screens/Settings.tsx:664 +msgid "Opens the storybook page" +msgstr "Obre la pàgina de l'historial" + +#: src/view/screens/Settings.tsx:644 +msgid "Opens the system log page" +msgstr "Obre la pàgina de registres del sistema" + +#: src/view/screens/Settings.tsx:494 +msgid "Opens the threads preferences" +msgstr "Obre les preferències dels fils de discussió" + +#: src/view/com/modals/Threadgate.tsx:89 +msgid "Or combine these options:" +msgstr "O combina aquestes opcions:" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:138 +msgid "Other account" +msgstr "Un altre compte" + +#: src/view/com/modals/ServerInput.tsx:88 +msgid "Other service" +msgstr "Un altre servei" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:91 +msgid "Other..." +msgstr "Un altre…" + +#: src/view/screens/NotFound.tsx:42 +#: src/view/screens/NotFound.tsx:45 +msgid "Page not found" +msgstr "Pàgina no trobada" + +#: src/view/com/auth/create/Step2.tsx:122 +#: src/view/com/auth/create/Step2.tsx:132 +#: src/view/com/auth/login/LoginForm.tsx:217 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:130 +#: src/view/com/modals/DeleteAccount.tsx:191 +msgid "Password" +msgstr "Contrasenya" + +#: src/view/com/auth/login/Login.tsx:157 +msgid "Password updated" +msgstr "Contrasenya actualitzada" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:28 +msgid "Password updated!" +msgstr "Contrasenya actualitzada!" + +#: src/view/com/modals/SelfLabel.tsx:121 +msgid "Pictures meant for adults." +msgstr "Imatges destinades a adults." + +#: src/view/screens/SavedFeeds.tsx:88 +msgid "Pinned Feeds" +msgstr "Canals de notícies ancorats" + +#: src/view/com/auth/create/state.ts:116 +msgid "Please choose your handle." +msgstr "Tria el teu identificador." + +#: src/view/com/auth/create/state.ts:109 +msgid "Please choose your password." +msgstr "Tria la teva contrasenya." + +#: src/view/com/modals/ChangeEmail.tsx:67 +msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed." +msgstr "Confirma el teu correu abans de canviar-lo. Aquest és un requisit temporal mentre no s'afegeixin eines per actualitzar el correu. Aviat no serà necessari," + +#: src/view/com/modals/AddAppPasswords.tsx:140 +msgid "Please enter a unique name for this App Password or use our randomly generated one." +msgstr "Introdueix un nom únic per aquesta contrasenya d'aplicació o fes servir un nom generat aleatòriament." + +#: src/view/com/auth/create/state.ts:95 +msgid "Please enter your email." +msgstr "Introdueix el teu correu." + +#: src/view/com/modals/DeleteAccount.tsx:180 +msgid "Please enter your password as well:" +msgstr "Introdueix la teva contrasenya també:" + +#: src/view/com/modals/AppealLabel.tsx:72 +#: src/view/com/modals/AppealLabel.tsx:75 +msgid "Please tell us why you think this content warning was incorrectly applied!" +msgstr "Digues-nos per què creus que s'ha aplicat incorrectament l'advertència de contingut." + +#: src/view/com/composer/Composer.tsx:214 +msgid "Please wait for your link card to finish loading" +msgstr "Espera que es generi la targeta de l'enllaç" + +#: src/view/com/composer/Composer.tsx:341 +#: src/view/com/post-thread/PostThread.tsx:225 +#: src/view/screens/PostThread.tsx:80 +msgid "Post" +msgstr "Publicació" + +#: src/view/com/post-thread/PostThread.tsx:378 +msgid "Post hidden" +msgstr "Publicació oculta" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:87 +msgid "Post language" +msgstr "Idioma de la publicació" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:75 +msgid "Post Languages" +msgstr "Idiomes de les publicacions" + +#: src/view/com/post-thread/PostThread.tsx:430 +msgid "Post not found" +msgstr "Publicació no trobada" + +#: src/view/screens/Profile.tsx:161 +msgid "Posts" +msgstr "Publicacions" + +#: src/view/com/modals/LinkWarning.tsx:44 +msgid "Potentially Misleading Link" +msgstr "Enllaç potencialment enganyós" + +#: src/view/com/lightbox/Lightbox.web.tsx:128 +msgid "Previous image" +msgstr "Imatge anterior" + +#: src/view/screens/LanguageSettings.tsx:187 +msgid "Primary Language" +msgstr "Idioma principal" + +#: src/view/screens/PreferencesThreads.tsx:91 +msgid "Prioritize Your Follows" +msgstr "Prioritza els usuaris que segueixes" + +#: src/view/shell/desktop/RightNav.tsx:84 +msgid "Privacy" +msgstr "Privacitat" + +#: src/view/screens/PrivacyPolicy.tsx:29 +#: src/view/screens/Settings.tsx:750 +#: src/view/shell/Drawer.tsx:262 +msgid "Privacy Policy" +msgstr "Política de privacitat" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:190 +msgid "Processing..." +msgstr "Processant…" + +#: src/view/shell/bottom-bar/BottomBar.tsx:247 +#: src/view/shell/desktop/LeftNav.tsx:415 +#: src/view/shell/Drawer.tsx:70 +#: src/view/shell/Drawer.tsx:544 +#: src/view/shell/Drawer.tsx:545 +msgid "Profile" +msgstr "Perfil" + +#: src/view/screens/Settings.tsx:808 +msgid "Protect your account by verifying your email." +msgstr "Protegeix el teu compte verificant el teu correu." + +#: src/view/screens/ModerationModlists.tsx:61 +msgid "Public, shareable lists of users to mute or block in bulk." +msgstr "Llistes d'usuaris per silenciar o bloquejar en massa, públiques i per compartir." + +#: src/view/screens/Lists.tsx:61 +msgid "Public, shareable lists which can drive feeds." +msgstr "Llistes que poden nodrir canals, públiques i per compartir." + +#: src/view/com/modals/Repost.tsx:52 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:58 +msgid "Quote post" +msgstr "Cita una publicació" + +#: src/view/com/modals/Repost.tsx:56 +msgid "Quote Post" +msgstr "Cita una publicació" + +#: src/view/com/modals/EditImage.tsx:236 +msgid "Ratios" +msgstr "Proporcions" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 +msgid "Recommended Feeds" +msgstr "Canals recomanats" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:180 +msgid "Recommended Users" +msgstr "Usuaris recomanats" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:264 +#: src/view/com/modals/SelfLabel.tsx:83 +#: src/view/com/modals/UserAddRemoveLists.tsx:193 +#: src/view/com/util/UserAvatar.tsx:282 +#: src/view/com/util/UserBanner.tsx:89 +msgid "Remove" +msgstr "Elimina" + +#: src/view/com/feeds/FeedSourceCard.tsx:106 +msgid "Remove {0} from my feeds?" +msgstr "Vols eliminar {0} dels teus canals?" + +#: src/view/com/util/AccountDropdownBtn.tsx:22 +msgid "Remove account" +msgstr "Elimina el compte" + +#: src/view/com/posts/FeedErrorMessage.tsx:130 +msgid "Remove feed" +msgstr "Elimina el canal" + +#: src/view/com/feeds/FeedSourceCard.tsx:105 +#: src/view/com/feeds/FeedSourceCard.tsx:172 +#: src/view/screens/ProfileFeed.tsx:271 +msgid "Remove from my feeds" +msgstr "Elimina dels meus canals" + +#: src/view/com/composer/photos/Gallery.tsx:167 +msgid "Remove image" +msgstr "Elimina la imatge" + +#: src/view/com/composer/ExternalEmbed.tsx:70 +msgid "Remove image preview" +msgstr "Elimina la visualització prèvia de la imatge" + +#: src/view/com/feeds/FeedSourceCard.tsx:173 +msgid "Remove this feed from my feeds?" +msgstr "Vols eliminar aquest canal dels meus canals?" + +#: src/view/com/posts/FeedErrorMessage.tsx:131 +msgid "Remove this feed from your saved feeds?" +msgstr "Vols eliminar aquest canal dels teus canals desats?" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:199 +#: src/view/com/modals/UserAddRemoveLists.tsx:136 +msgid "Removed from list" +msgstr "Elimina de la llista" + +#: src/view/screens/Profile.tsx:162 +msgid "Replies" +msgstr "Respostes" + +#: src/view/com/threadgate/WhoCanReply.tsx:98 +msgid "Replies to this thread are disabled" +msgstr "Les respostes a aquest fil de discussió estan deshabilitades" + +#: src/view/screens/PreferencesHomeFeed.tsx:135 +msgid "Reply Filters" +msgstr "Filtres de resposta" + +#: src/view/com/modals/report/Modal.tsx:166 +msgid "Report {collectionName}" +msgstr "Informa de {collectionName}" + +#: src/view/com/profile/ProfileHeader.tsx:404 +msgid "Report Account" +msgstr "Informa del compte" + +#: src/view/screens/ProfileFeed.tsx:291 +msgid "Report feed" +msgstr "Informa del canal" + +#: src/view/screens/ProfileList.tsx:426 +msgid "Report List" +msgstr "Informa de la llista" + +#: src/view/com/modals/report/SendReportButton.tsx:37 +#: src/view/com/util/forms/PostDropdownBtn.tsx:196 +msgid "Report post" +msgstr "Informa de la publicació" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Repost" +msgstr "Republica" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:94 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:105 +msgid "Repost or quote post" +msgstr "Republica o cita la publicació" + +#: src/view/screens/PostRepostedBy.tsx:27 +msgid "Reposted by" +msgstr "Republicada per" + +#: src/view/com/modals/ChangeEmail.tsx:181 +#: src/view/com/modals/ChangeEmail.tsx:183 +msgid "Request Change" +msgstr "Demana un canvi" + +#: src/view/com/auth/create/Step2.tsx:68 +msgid "Required for this provider" +msgstr "Requerit per aquest proveïdor" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:108 +msgid "Reset code" +msgstr "Codi de restabliment" + +#: src/view/screens/Settings.tsx:686 +msgid "Reset onboarding state" +msgstr "Restableix l'estat de la incorporació" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:98 +msgid "Reset password" +msgstr "Restableix la contrasenya" + +#: src/view/screens/Settings.tsx:676 +msgid "Reset preferences state" +msgstr "Restableix l'estat de les preferències" + +#: src/view/screens/Settings.tsx:684 +msgid "Resets the onboarding state" +msgstr "Restableix l'estat de la incorporació" + +#: src/view/screens/Settings.tsx:674 +msgid "Resets the preferences state" +msgstr "Restableix l'estat de les preferències" + +#: src/view/com/auth/create/CreateAccount.tsx:163 +#: src/view/com/auth/create/CreateAccount.tsx:167 +#: src/view/com/auth/login/LoginForm.tsx:259 +#: src/view/com/auth/login/LoginForm.tsx:262 +#: src/view/com/util/error/ErrorMessage.tsx:55 +#: src/view/com/util/error/ErrorScreen.tsx:65 +msgid "Retry" +msgstr "Torna-ho a provar" + +#: src/view/com/modals/AltImage.tsx:115 +#: src/view/com/modals/BirthDateSettings.tsx:94 +#: src/view/com/modals/BirthDateSettings.tsx:97 +#: src/view/com/modals/ChangeHandle.tsx:173 +#: src/view/com/modals/CreateOrEditList.tsx:249 +#: src/view/com/modals/CreateOrEditList.tsx:257 +#: src/view/com/modals/EditProfile.tsx:223 +msgid "Save" +msgstr "Desa" + +#: src/view/com/modals/AltImage.tsx:106 +msgid "Save alt text" +msgstr "Desa el text alternatiu" + +#: src/view/com/modals/EditProfile.tsx:231 +msgid "Save Changes" +msgstr "Desa els canvis" + +#: src/view/com/modals/ChangeHandle.tsx:170 +msgid "Save handle change" +msgstr "Desa el canvi d'identificador" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:144 +msgid "Save image crop" +msgstr "Desa la imatge retallada" + +#: src/view/screens/SavedFeeds.tsx:122 +msgid "Saved Feeds" +msgstr "Canals desats" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:75 +#: src/view/com/util/forms/SearchInput.tsx:65 +#: src/view/screens/Search/Search.tsx:406 +#: src/view/screens/Search/Search.tsx:572 +#: src/view/shell/bottom-bar/BottomBar.tsx:159 +#: src/view/shell/desktop/LeftNav.tsx:324 +#: src/view/shell/desktop/Search.tsx:161 +#: src/view/shell/desktop/Search.tsx:170 +#: src/view/shell/Drawer.tsx:362 +#: src/view/shell/Drawer.tsx:363 +msgid "Search" +msgstr "Cerca" + +#: src/view/com/auth/LoggedOut.tsx:104 +#: src/view/com/auth/LoggedOut.tsx:105 +msgid "Search for users" +msgstr "Cerca usuaris" + +#: src/view/com/modals/ChangeEmail.tsx:110 +msgid "Security Step Required" +msgstr "Es requereix un pas de seguretat" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:39 +msgid "See what's next" +msgstr "Què més hi ha" + +#: src/view/com/modals/ServerInput.tsx:75 +msgid "Select Bluesky Social" +msgstr "Selecciona Bluesky Social" + +#: src/view/com/auth/login/Login.tsx:117 +msgid "Select from an existing account" +msgstr "Selecciona d'un compte existent" + +#: src/view/com/auth/login/LoginForm.tsx:143 +msgid "Select service" +msgstr "Selecciona el servei" + +#: src/view/screens/LanguageSettings.tsx:281 +msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." +msgstr "Selecciona quins idiomes vols que incloguin els canals a què estàs subscrit. Si no en selecciones cap, es mostraran tots." + +#: src/view/screens/LanguageSettings.tsx:98 +msgid "Select your app language for the default text to display in the app" +msgstr "Seleccioneu l'idioma de l'aplicació perquè el text predeterminat es mostri en aquesta" + +#: src/view/screens/LanguageSettings.tsx:190 +msgid "Select your preferred language for translations in your feed." +msgstr "Selecciona el teu idioma preferit per a les traduccions al teu canal." + +#: src/view/com/modals/VerifyEmail.tsx:196 +msgid "Send Confirmation Email" +msgstr "Envia correu de confirmació" + +#: src/view/com/modals/DeleteAccount.tsx:127 +msgid "Send email" +msgstr "Envia correu" + +#: src/view/com/modals/DeleteAccount.tsx:138 +msgid "Send Email" +msgstr "Envia correu" + +#: src/view/shell/Drawer.tsx:295 +#: src/view/shell/Drawer.tsx:316 +msgid "Send feedback" +msgstr "Envia comentari" + +#: src/view/com/modals/report/SendReportButton.tsx:45 +msgid "Send Report" +msgstr "Envia informe" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:78 +msgid "Set new password" +msgstr "Estableix una nova contrasenya" + +#: src/view/screens/PreferencesHomeFeed.tsx:216 +msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible." +msgstr "Posa \"No\" a aquesta opció per amagar totes les publicacions citades del teu canal. Les republicacions encara seran visibles." + +#: src/view/screens/PreferencesHomeFeed.tsx:113 +msgid "Set this setting to \"No\" to hide all replies from your feed." +msgstr "Posa \"No\" a aquesta opció per amagar totes les respostes del teu canal." + +#: src/view/screens/PreferencesHomeFeed.tsx:182 +msgid "Set this setting to \"No\" to hide all reposts from your feed." +msgstr "Posa \"No\" a aquesta opció per amagar totes les republicacions del teu canal." + +#: src/view/screens/PreferencesThreads.tsx:116 +msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature." +msgstr "Posa \"Sí\" a aquesta opció per mostrar les respostes en vista de fil de discussió. Aquesta és una opció experimental." + +#: src/view/screens/PreferencesHomeFeed.tsx:252 +msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature." +msgstr "Posa \"Sí\" a aquesta opció per mostrar algunes publicacions dels teus canals en el teu canal de seguits. Aquesta és una opció experimental." + +#: src/view/screens/Settings.tsx:277 +#: src/view/shell/desktop/LeftNav.tsx:433 +#: src/view/shell/Drawer.tsx:565 +#: src/view/shell/Drawer.tsx:566 +msgid "Settings" +msgstr "Configuracions" + +#: src/view/com/modals/SelfLabel.tsx:125 +msgid "Sexual activity or erotic nudity." +msgstr "Activitat sexual o nu eròtic." + +#: src/view/com/profile/ProfileHeader.tsx:338 +#: src/view/com/util/forms/PostDropdownBtn.tsx:139 +#: src/view/screens/ProfileList.tsx:385 +msgid "Share" +msgstr "Comparteix" + +#: src/view/screens/ProfileFeed.tsx:303 +msgid "Share feed" +msgstr "Comparteix el canal" + +#: src/view/com/util/moderation/ContentHider.tsx:105 +#: src/view/screens/Settings.tsx:316 +msgid "Show" +msgstr "Mostra" + +#: src/view/com/util/moderation/ScreenHider.tsx:132 +msgid "Show anyway" +msgstr "Mostra igualment" + +#: src/view/screens/PreferencesHomeFeed.tsx:249 +msgid "Show Posts from My Feeds" +msgstr "Mostra les publicacions dels meus canals" + +#: src/view/screens/PreferencesHomeFeed.tsx:213 +msgid "Show Quote Posts" +msgstr "Mostra les publicacions citades" + +#: src/view/screens/PreferencesHomeFeed.tsx:110 +msgid "Show Replies" +msgstr "Mostra les respostes" + +#: src/view/screens/PreferencesThreads.tsx:94 +msgid "Show replies by people you follow before all other replies." +msgstr "Mostra les respostes dels comptes que segueixes abans que les altres." + +#: src/view/screens/PreferencesHomeFeed.tsx:179 +msgid "Show Reposts" +msgstr "Mostra republicacions" + +#: src/view/com/notifications/FeedItem.tsx:345 +msgid "Show users" +msgstr "Mostra usuaris" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:70 +#: src/view/com/auth/login/Login.tsx:98 +#: src/view/com/auth/SplashScreen.tsx:54 +#: src/view/shell/bottom-bar/BottomBar.tsx:285 +#: src/view/shell/bottom-bar/BottomBar.tsx:286 +#: src/view/shell/bottom-bar/BottomBar.tsx:288 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:177 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:178 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:180 +#: src/view/shell/NavSignupCard.tsx:58 +#: src/view/shell/NavSignupCard.tsx:59 +msgid "Sign in" +msgstr "Inicia sessió" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:78 +#: src/view/com/auth/SplashScreen.tsx:57 +#: src/view/com/auth/SplashScreen.web.tsx:87 +msgid "Sign In" +msgstr "Inicia sessió" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:44 +msgid "Sign in as {0}" +msgstr "Inicia sessió com a {0}" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:118 +#: src/view/com/auth/login/Login.tsx:116 +msgid "Sign in as..." +msgstr "Inicia sessió com a …" + +#: src/view/com/auth/login/LoginForm.tsx:130 +msgid "Sign into" +msgstr "Inicia sessió en" + +#: src/view/com/modals/SwitchAccount.tsx:64 +#: src/view/com/modals/SwitchAccount.tsx:67 +msgid "Sign out" +msgstr "Tanca sessió" + +#: src/view/shell/bottom-bar/BottomBar.tsx:275 +#: src/view/shell/bottom-bar/BottomBar.tsx:276 +#: src/view/shell/bottom-bar/BottomBar.tsx:278 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:167 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:168 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:170 +#: src/view/shell/NavSignupCard.tsx:49 +#: src/view/shell/NavSignupCard.tsx:50 +#: src/view/shell/NavSignupCard.tsx:52 +msgid "Sign up" +msgstr "Registra't" + +#: src/view/shell/NavSignupCard.tsx:42 +msgid "Sign up or sign in to join the conversation" +msgstr "Registra't o inicia sessió per unir-te a la conversa" + +#: src/view/com/util/moderation/ScreenHider.tsx:76 +msgid "Sign-in Required" +msgstr "Es requereix iniciar sessió" + +#: src/view/screens/Settings.tsx:327 +msgid "Signed in as" +msgstr "S'ha iniciat sessió com a" + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:33 +msgid "Skip" +msgstr "Salta aquest pas" + +#: src/view/screens/PreferencesThreads.tsx:69 +msgid "Sort Replies" +msgstr "Ordena les respostes" + +#: src/view/screens/PreferencesThreads.tsx:72 +msgid "Sort replies to the same post by:" +msgstr "Ordena les respostes a la mateixa publicació per:" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:122 +msgid "Square" +msgstr "Quadrat" + +#: src/view/com/auth/create/Step1.tsx:90 +#: src/view/com/modals/ServerInput.tsx:62 +msgid "Staging" +msgstr "Posada en escena" + +#: src/view/screens/Settings.tsx:730 +msgid "Status page" +msgstr "Pàgina d'estat" + +#: src/view/screens/Settings.tsx:666 +msgid "Storybook" +msgstr "Historial" + +#: src/view/com/modals/AppealLabel.tsx:101 +msgid "Submit" +msgstr "Envia" + +#: src/view/screens/ProfileList.tsx:575 +msgid "Subscribe" +msgstr "Subscriure's" + +#: src/view/screens/ProfileList.tsx:571 +msgid "Subscribe to this list" +msgstr "Subscriure's a la llista" + +#: src/view/screens/Search/Search.tsx:362 +msgid "Suggested Follows" +msgstr "Usuaris suggerits per seguir" + +#: src/view/screens/Support.tsx:30 +#: src/view/screens/Support.tsx:33 +msgid "Support" +msgstr "Suport" + +#: src/view/com/modals/SwitchAccount.tsx:115 +msgid "Switch Account" +msgstr "Canvia el compte" + +#: src/view/screens/Settings.tsx:646 +msgid "System log" +msgstr "Registres del sistema" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:112 +msgid "Tall" +msgstr "Alt" + +#: src/view/shell/desktop/RightNav.tsx:93 +msgid "Terms" +msgstr "Condicions" + +#: src/view/screens/Settings.tsx:744 +#: src/view/screens/TermsOfService.tsx:29 +#: src/view/shell/Drawer.tsx:256 +msgid "Terms of Service" +msgstr "Condicions del servei" + +#: src/view/com/modals/AppealLabel.tsx:70 +#: src/view/com/modals/report/InputIssueDetails.tsx:51 +msgid "Text input field" +msgstr "Camp d'introducció de text" + +#: src/view/com/profile/ProfileHeader.tsx:306 +msgid "The account will be able to interact with you after unblocking." +msgstr "El compte podrà interactuar amb tu després del desbloqueig." + +#: src/view/screens/CommunityGuidelines.tsx:36 +msgid "The Community Guidelines have been moved to <0/>" +msgstr "Les directrius de la comunitat han estat traslladades a <0/>" + +#: src/view/screens/CopyrightPolicy.tsx:33 +msgid "The Copyright Policy has been moved to <0/>" +msgstr "La política de drets d'autoria ha estat traslladada a <0/>" + +#: src/view/com/post-thread/PostThread.tsx:433 +msgid "The post may have been deleted." +msgstr "És possible que la publicació s'hagi esborrat." + +#: src/view/screens/PrivacyPolicy.tsx:33 +msgid "The Privacy Policy has been moved to <0/>" +msgstr "La política de privacitat ha estat traslladada a <0/>" + +#: src/view/screens/Support.tsx:36 +msgid "The support form has been moved. If you need help, please<0/> or visit {HELP_DESK_URL} to get in touch with us." +msgstr "El formulari de suport ha estat traslladat. Si necessites ajuda, <0/> o visita {HELP_DESK_URL} per contactar amb nosaltres." + +#: src/view/screens/TermsOfService.tsx:33 +msgid "The Terms of Service have been moved to" +msgstr "Les condicions del servei han estat traslladades a " + +#: src/view/com/util/ErrorBoundary.tsx:35 +msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" +msgstr "S'ha produït un problema inesperat a l'aplicació. Fes-nos saber si això t'ha passat a tu!" + +#: src/view/com/util/moderation/ScreenHider.tsx:88 +msgid "This {screenDescription} has been flagged:" +msgstr "Aquesta {screenDescription} ha estat etiquetada:" + +#: src/view/com/util/moderation/ScreenHider.tsx:83 +msgid "This account has requested that users sign in to view their profile." +msgstr "Aquest compte ha sol·licitat que els usuaris estiguin registrats per veure el seu perfil." + +#: src/view/com/posts/FeedErrorMessage.tsx:107 +msgid "This content is not viewable without a Bluesky account." +msgstr "Aquest contingut no es pot veure sense un compte de Bluesky." + +#: src/view/com/posts/FeedErrorMessage.tsx:113 +msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." +msgstr "Aquest canal està rebent moltes visites actualment i està temporalment inactiu. Prova-ho més tard." + +#: src/view/com/modals/BirthDateSettings.tsx:61 +msgid "This information is not shared with other users." +msgstr "Aquesta informació no es comparteix amb altres usuaris." + +#: src/view/com/modals/VerifyEmail.tsx:113 +msgid "This is important in case you ever need to change your email or reset your password." +msgstr "Això és important si mai necessites canviar el teu correu o restablir la contrasenya." + +#: src/view/com/auth/create/Step1.tsx:55 +msgid "This is the service that keeps you online." +msgstr "Aquest és el servei que et manté connectat." + +#: src/view/com/modals/LinkWarning.tsx:56 +msgid "This link is taking you to the following website:" +msgstr "Aquest enllaç et porta a la web:" + +#: src/view/com/post-thread/PostThreadItem.tsx:123 +msgid "This post has been deleted." +msgstr "Aquesta publicació ha estat esborrada." + +#: src/view/com/modals/SelfLabel.tsx:137 +msgid "This warning is only available for posts with media attached." +msgstr "Aquesta advertència només està disponible per publicacions amb contingut adjuntat." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:178 +msgid "This will hide this post from your feeds." +msgstr "Això amagarà aquesta publicació dels teus canals" + +#: src/view/screens/PreferencesThreads.tsx:53 +#: src/view/screens/Settings.tsx:503 +msgid "Thread Preferences" +msgstr "Preferències dels fils de discussió" + +#: src/view/screens/PreferencesThreads.tsx:113 +msgid "Threaded Mode" +msgstr "Mode fils de discussió" + +#: src/view/com/util/forms/DropdownButton.tsx:230 +msgid "Toggle dropdown" +msgstr "Commuta el menú desplegable" + +#: src/view/com/modals/EditImage.tsx:271 +msgid "Transformations" +msgstr "Transformacions" + +#: src/view/com/post-thread/PostThreadItem.tsx:705 +#: src/view/com/post-thread/PostThreadItem.tsx:707 +#: src/view/com/util/forms/PostDropdownBtn.tsx:111 +msgid "Translate" +msgstr "Tradueix" + +#: src/view/com/util/error/ErrorScreen.tsx:73 +msgid "Try again" +msgstr "Torna-ho a provar" + +#: src/view/screens/ProfileList.tsx:473 +msgid "Un-block list" +msgstr "Desbloqueja la llista" + +#: src/view/screens/ProfileList.tsx:458 +msgid "Un-mute list" +msgstr "Deixa de silenciar la llista" + +#: src/view/com/auth/create/CreateAccount.tsx:65 +#: src/view/com/auth/login/Login.tsx:76 +#: src/view/com/auth/login/LoginForm.tsx:117 +msgid "Unable to contact your service. Please check your Internet connection." +msgstr "No es pot contactar amb el teu servei. Comprova la teva connexió a internet." + +#: src/view/com/profile/ProfileHeader.tsx:466 +#: src/view/com/profile/ProfileHeader.tsx:469 +msgid "Unblock" +msgstr "Desbloqueja" + +#: src/view/com/profile/ProfileHeader.tsx:304 +#: src/view/com/profile/ProfileHeader.tsx:388 +msgid "Unblock Account" +msgstr "Desbloqueja el compte" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Undo repost" +msgstr "Desfés la republicació" + +#: src/view/com/auth/create/state.ts:210 +msgid "Unfortunately, you do not meet the requirements to create an account." +msgstr "No compleixes les condicions per crear un compte." + +#: src/view/com/profile/ProfileHeader.tsx:369 +msgid "Unmute Account" +msgstr "Deixa de silenciar el compte" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:157 +msgid "Unmute thread" +msgstr "Deixa de silenciar el fil de discussió" + +#: src/view/screens/ProfileList.tsx:441 +msgid "Unpin moderation list" +msgstr "Desancora la llista de moderació" + +#: src/view/com/modals/UserAddRemoveLists.tsx:54 +msgid "Update {displayName} in Lists" +msgstr "Actualitza {displayName} a les Llistes" + +#: src/lib/hooks/useOTAUpdate.ts:15 +msgid "Update Available" +msgstr "Actualització disponible" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:172 +msgid "Updating..." +msgstr "Actualitzant…" + +#: src/view/com/modals/ChangeHandle.tsx:453 +msgid "Upload a text file to:" +msgstr "Puja un fitxer de text a:" + +#: src/view/screens/AppPasswords.tsx:194 +msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." +msgstr "Utilitza les contrasenyes d'aplicació per iniciar sessió en altres clients de Bluesky, sense haver de donar accés total al teu compte o contrasenya." + +#: src/view/com/modals/ChangeHandle.tsx:513 +msgid "Use default provider" +msgstr "Utilitza el proveïdor predeterminat" + +#: src/view/com/modals/AddAppPasswords.tsx:150 +msgid "Use this to sign into the other app along with your handle." +msgstr "Utilitza-ho per iniciar sessió a l'altra aplicació, juntament amb el teu identificador." + +#: src/view/com/modals/InviteCodes.tsx:197 +msgid "Used by:" +msgstr "Utilitzat per:" + +#: src/view/com/auth/create/Step3.tsx:38 +msgid "User handle" +msgstr "Identificador d'usuari" + +#: src/view/screens/Lists.tsx:58 +msgid "User Lists" +msgstr "Llistes d'usuaris" + +#: src/view/com/auth/login/LoginForm.tsx:170 +#: src/view/com/auth/login/LoginForm.tsx:188 +msgid "Username or email address" +msgstr "Nom d'usuari o correu" + +#: src/view/screens/ProfileList.tsx:747 +msgid "Users" +msgstr "Usuaris" + +#: src/view/com/threadgate/WhoCanReply.tsx:143 +msgid "users followed by <0/>" +msgstr "usuaris seguits per <0/>" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "Usuaris a «{0}»" + +#: src/view/screens/Settings.tsx:769 +msgid "Verify email" +msgstr "Verifica el correu" + +#: src/view/screens/Settings.tsx:794 +msgid "Verify my email" +msgstr "Verifica el meu correu" + +#: src/view/screens/Settings.tsx:803 +msgid "Verify My Email" +msgstr "Verifica el meu correu" + +#: src/view/com/modals/ChangeEmail.tsx:205 +#: src/view/com/modals/ChangeEmail.tsx:207 +msgid "Verify New Email" +msgstr "Verifica el correu nou" + +#: src/view/screens/Log.tsx:52 +msgid "View debug entry" +msgstr "Veure el registre de depuració" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:128 +msgid "View the avatar" +msgstr "Veure l'avatar" + +#: src/view/com/modals/LinkWarning.tsx:73 +msgid "Visit Site" +msgstr "Visita el lloc web" + +#: src/view/com/auth/create/CreateAccount.tsx:122 +msgid "We're so excited to have you join us!" +msgstr "Ens fa molta il·lusió que t'uneixis a nosaltres!" + +#: src/view/screens/Search/Search.tsx:243 +msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." +msgstr "Ens sap greu, però la teva cerca no s'ha pogut fer. Prova-ho d'aquí una estona." + +#: src/view/screens/NotFound.tsx:48 +msgid "We're sorry! We can't find the page you were looking for." +msgstr "Ens sap greu! No podem trobar la pàgina que estàs cercant." + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:46 +msgid "Welcome to <0>Bluesky</0>" +msgstr "Benvingut a <0>Bluesky</0>" + +#: src/view/com/modals/report/Modal.tsx:169 +msgid "What is the issue with this {collectionName}?" +msgstr "Quin problema hi ha amb {collectionName}?" + +#: src/view/com/auth/SplashScreen.tsx:34 +msgid "What's up?" +msgstr "Què hi ha de nou" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:78 +msgid "Which languages are used in this post?" +msgstr "En quins idiomes està aquesta publicació?" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:77 +msgid "Which languages would you like to see in your algorithmic feeds?" +msgstr "Quins idiomes t'agradaria veure en els teus canals algorítmics?" + +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:47 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "Qui hi pot respondre" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:102 +msgid "Wide" +msgstr "Amplada" + +#: src/view/com/composer/Composer.tsx:413 +msgid "Write post" +msgstr "Escriu una publicació" + +#: src/view/com/composer/Prompt.tsx:33 +msgid "Write your reply" +msgstr "Escriu la teva resposta" + +#: src/view/screens/PreferencesHomeFeed.tsx:120 +#: src/view/screens/PreferencesHomeFeed.tsx:192 +#: src/view/screens/PreferencesHomeFeed.tsx:227 +#: src/view/screens/PreferencesHomeFeed.tsx:262 +msgid "Yes" +msgstr "Sí" + +#: src/view/com/auth/create/Step1.tsx:106 +msgid "You can change hosting providers at any time." +msgstr "Pots canviar el teu proveïdor d'allotjament quan vulguis." + +#: src/view/com/auth/login/Login.tsx:158 +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:31 +msgid "You can now sign in with your new password." +msgstr "Ara pots iniciar sessió amb la nova contrasenya." + +#: src/view/com/modals/InviteCodes.tsx:64 +msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." +msgstr "Encara no tens codis d'invitació! Te n'enviarem quan portis una mica més de temps a Bluesky." + +#: src/view/screens/SavedFeeds.tsx:102 +msgid "You don't have any pinned feeds." +msgstr "No tens cap canal fixat." + +#: src/view/screens/Feeds.tsx:383 +msgid "You don't have any saved feeds!" +msgstr "No tens cap canal desat!" + +#: src/view/screens/SavedFeeds.tsx:135 +msgid "You don't have any saved feeds." +msgstr "No tens cap canal desat." + +#: src/view/com/post-thread/PostThread.tsx:381 +msgid "You have blocked the author or you have been blocked by the author." +msgstr "Has bloquejat l'autor o has estat bloquejat per ell." + +#: src/view/com/feeds/ProfileFeedgens.tsx:134 +msgid "You have no feeds." +msgstr "No tens canals." + +#: src/view/com/lists/MyLists.tsx:89 +#: src/view/com/lists/ProfileLists.tsx:138 +msgid "You have no lists." +msgstr "No tens llistes." + +#: src/view/screens/ModerationBlockedAccounts.tsx:132 +msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." +msgstr "Encara no has bloquejat cap compte. Per fer-ho, vés al seu perfil i selecciona \"Bloqueja el compte\" en el menú del seu compte." + +#: src/view/screens/AppPasswords.tsx:86 +msgid "You have not created any app passwords yet. You can create one by pressing the button below." +msgstr "Encara no has creat cap contrasenya d'aplicació. Pots fer-ho amb el botó d'aquí sota." + +#: src/view/screens/ModerationMutedAccounts.tsx:131 +msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." +msgstr "Encara no has silenciat cap compte. Per fer-ho, vés al seu perfil i selecciona \"Silencia compte\" en el menú del seu compte." + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:81 +msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." +msgstr "Rebràs un correu amb un \"codi de restabliment\". Introdueix aquí el codi i després la teva contrasenya nova." + +#: src/view/com/auth/create/Step2.tsx:58 +msgid "Your account" +msgstr "El teu compte" + +#: src/view/com/auth/create/Step2.tsx:146 +msgid "Your birth date" +msgstr "La teva data de naixement" + +#: src/view/com/auth/create/state.ts:102 +msgid "Your email appears to be invalid." +msgstr "El teu correu no sembla vàlid." + +#: src/view/com/modals/Waitlist.tsx:107 +msgid "Your email has been saved! We'll be in touch soon." +msgstr "Hem desat el teu correu! Aviat ens posarem en contacte amb tu." + +#: src/view/com/modals/ChangeEmail.tsx:125 +msgid "Your email has been updated but not verified. As a next step, please verify your new email." +msgstr "El teu correu s'ha actualitzat, però no ha estat verificat. En el pas següent cal que verifiquis el teu correu." + +#: src/view/com/modals/VerifyEmail.tsx:108 +msgid "Your email has not yet been verified. This is an important security step which we recommend." +msgstr "El teu correu encara no s'ha verificat. Et recomanem fer-ho per seguretat." + +#: src/view/com/auth/create/Step3.tsx:42 +#: src/view/com/modals/ChangeHandle.tsx:270 +msgid "Your full handle will be" +msgstr "El teu identificador complet serà" + +#: src/view/com/auth/create/Step1.tsx:53 +msgid "Your hosting provider" +msgstr "El teu proveïdor d'allotjament" + +#: src/view/screens/Settings.tsx:402 +#: src/view/shell/desktop/RightNav.tsx:137 +#: src/view/shell/Drawer.tsx:655 +msgid "Your invite codes are hidden when logged in using an App Password" +msgstr "Els teus codis d'invitació no es mostren quan has iniciat sessió amb una contrasenya d'aplicació" + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59 +msgid "Your posts, likes, and blocks are public. Mutes are private." +msgstr "Les teves publicacions, m'agrades i bloquejos són públics. Els comptes silenciats són privats." + +#: src/view/com/modals/SwitchAccount.tsx:82 +msgid "Your profile" +msgstr "El teu perfil" + +#: src/view/com/auth/create/Step3.tsx:28 +msgid "Your user handle" +msgstr "El teu identificador d'usuari" + +#~ msgid "Appeal Decision" +#~ msgstr "Decisión de apelación" +#~ msgid "Looks like this feed is only available to users with a Bluesky account. Please sign up or sign in to view this feed!" +#~ msgstr "Parece que este canal de noticias sólo está disponible para usuarios con una cuenta Bluesky. Por favor, ¡regístrate o inicia sesión para ver este canal!" +#~ msgid "Please tell us why you think this decision was incorrect." +#~ msgstr "Por favor, dinos por qué crees que esta decisión fue incorrecta." +#~ msgid "This {0} has been labeled." +#~ msgstr "Este {0} ha sido etiquetado." +#~ msgid "What's next?" +#~ msgstr "¿Qué sigue?" + diff --git a/src/locale/locales/fr/messages.po b/src/locale/locales/fr/messages.po index dea80a1a9..305336c0e 100644 --- a/src/locale/locales/fr/messages.po +++ b/src/locale/locales/fr/messages.po @@ -9,42 +9,38 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "PO-Revision-Date: \n" -"Last-Translator: \n" +"Last-Translator: Stanislas Signoud (@signez.fr)\n" "Language-Team: \n" "Plural-Forms: \n" #: src/view/com/modals/VerifyEmail.tsx:142 msgid "(no email)" -msgstr "" +msgstr "(pas d’e-mail)" #: src/view/shell/desktop/RightNav.tsx:168 msgid "{0, plural, one {# invite code available} other {# invite codes available}}" -msgstr "{0, plural, one {# invite code available} other {# invite codes available}}" +msgstr "{0, plural, one {# code d’invitation disponible} other {# codes d’invitations disponibles}}" #: src/view/com/modals/CreateOrEditList.tsx:185 #: src/view/screens/Settings.tsx:294 msgid "{0}" msgstr "{0}" -#: src/view/com/modals/CreateOrEditList.tsx:176 -#~ msgid "{0} {purposeLabel} List" -#~ msgstr "{0} {purposeLabel} Liste" - #: src/view/com/profile/ProfileHeader.tsx:632 msgid "{following} following" -msgstr "" +msgstr "{following} abonnements" #: src/view/shell/desktop/RightNav.tsx:151 msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}" -msgstr "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}" +msgstr "{invitesAvailable, plural, one {Code d’invitation : # disponible} other {Codes d’invitation : # disponibles}}" #: src/view/screens/Settings.tsx:435 -#: src/view/shell/Drawer.tsx:661 +#: src/view/shell/Drawer.tsx:664 msgid "{invitesAvailable} invite code available" msgstr "{invitesAvailable} code d’invitation disponible" #: src/view/screens/Settings.tsx:437 -#: src/view/shell/Drawer.tsx:663 +#: src/view/shell/Drawer.tsx:666 msgid "{invitesAvailable} invite codes available" msgstr "{invitesAvailable} codes d’invitation disponibles" @@ -52,13 +48,13 @@ msgstr "{invitesAvailable} codes d’invitation disponibles" msgid "{message}" msgstr "{message}" -#: src/view/shell/Drawer.tsx:440 +#: src/view/shell/Drawer.tsx:443 msgid "{numUnreadNotifications} unread" -msgstr "" +msgstr "{numUnreadNotifications} non lus" #: src/Navigation.tsx:147 msgid "@{0}" -msgstr "" +msgstr "@{0}" #: src/view/com/threadgate/WhoCanReply.tsx:158 msgid "<0/> members" @@ -66,7 +62,7 @@ msgstr "<0/> membres" #: src/view/com/profile/ProfileHeader.tsx:634 msgid "<0>{following} </0><1>following</1>" -msgstr "" +msgstr "<0>{following} </0><1>abonnements</1>" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>" @@ -74,15 +70,15 @@ msgstr "<0>Choisissez vos</0><1>fils d’actualité</1><2>recommandés</2>" #: src/view/com/auth/onboarding/RecommendedFollows.tsx:37 msgid "<0>Follow some</0><1>Recommended</1><2>Users</2>" -msgstr "<0>Suivre certains</0><1>utilisateurs</1><2>recommandés</2>" +msgstr "<0>Suivre certains</0><1>comptes</1><2>recommandés</2>" #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:21 msgid "<0>Welcome to</0><1>Bluesky</1>" -msgstr "" +msgstr "<0>Bienvenue sur</0><1>Bluesky</1>" #: src/view/com/profile/ProfileHeader.tsx:597 msgid "⚠Invalid Handle" -msgstr "" +msgstr "⚠Pseudo invalide" #: src/view/com/util/moderation/LabelInfo.tsx:45 msgid "A content warning has been applied to this {0}." @@ -95,11 +91,11 @@ msgstr "Une nouvelle version de l’application est disponible. Veuillez faire l #: src/view/com/util/ViewHeader.tsx:83 #: src/view/screens/Search/Search.tsx:538 msgid "Access navigation links and settings" -msgstr "" +msgstr "Accède aux liens de navigation et aux paramètres" -#: src/view/com/pager/FeedsTabBarMobile.tsx:78 +#: src/view/com/pager/FeedsTabBarMobile.tsx:83 msgid "Access profile and other navigation links" -msgstr "" +msgstr "Accède au profil et aux autres liens de navigation" #: src/view/com/modals/EditImage.tsx:299 #: src/view/screens/Settings.tsx:445 @@ -113,19 +109,19 @@ msgstr "Compte" #: src/view/com/profile/ProfileHeader.tsx:293 msgid "Account blocked" -msgstr "" +msgstr "Compte bloqué" #: src/view/com/profile/ProfileHeader.tsx:260 msgid "Account muted" -msgstr "" +msgstr "Compte masqué" #: src/view/com/modals/ModerationDetails.tsx:86 msgid "Account Muted" -msgstr "" +msgstr "Compte masqué" #: src/view/com/modals/ModerationDetails.tsx:72 msgid "Account Muted by List" -msgstr "" +msgstr "Compte masqué par liste" #: src/view/com/util/AccountDropdownBtn.tsx:41 msgid "Account options" @@ -133,15 +129,15 @@ msgstr "Options de compte" #: src/view/com/util/AccountDropdownBtn.tsx:25 msgid "Account removed from quick access" -msgstr "" +msgstr "Compte supprimé de l’accès rapide" #: src/view/com/profile/ProfileHeader.tsx:315 msgid "Account unblocked" -msgstr "" +msgstr "Compte débloqué" #: src/view/com/profile/ProfileHeader.tsx:273 msgid "Account unmuted" -msgstr "" +msgstr "Compte démasqué" #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:150 #: src/view/com/modals/ListAddRemoveUsers.tsx:264 @@ -156,7 +152,7 @@ msgstr "Ajouter un avertissement sur le contenu" #: src/view/screens/ProfileList.tsx:781 msgid "Add a user to this list" -msgstr "Ajouter un utilisateur à cette liste" +msgstr "Ajouter un compte à cette liste" #: src/view/screens/Settings.tsx:383 #: src/view/screens/Settings.tsx:392 @@ -173,7 +169,7 @@ msgstr "Ajouter un texte alt" #: src/view/screens/AppPasswords.tsx:143 #: src/view/screens/AppPasswords.tsx:156 msgid "Add App Password" -msgstr "" +msgstr "Ajouter un mot de passe d’application" #: src/view/com/modals/report/InputIssueDetails.tsx:41 #: src/view/com/modals/report/Modal.tsx:191 @@ -190,11 +186,11 @@ msgstr "Ajouter une carte de lien" #: src/view/com/composer/Composer.tsx:451 msgid "Add link card:" -msgstr "Ajouter une carte de lien :" +msgstr "Ajouter une carte de lien :" #: src/view/com/modals/ChangeHandle.tsx:417 msgid "Add the following DNS record to your domain:" -msgstr "Ajoutez l’enregistrement DNS suivant à votre domaine :" +msgstr "Ajoutez l’enregistrement DNS suivant à votre domaine :" #: src/view/com/profile/ProfileHeader.tsx:357 msgid "Add to Lists" @@ -207,7 +203,7 @@ msgstr "Ajouter à mes fils d’actu" #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:139 msgid "Added" -msgstr "" +msgstr "Ajouté" #: src/view/com/modals/ListAddRemoveUsers.tsx:191 #: src/view/com/modals/UserAddRemoveLists.tsx:128 @@ -216,7 +212,7 @@ msgstr "Ajouté à la liste" #: src/view/com/feeds/FeedSourceCard.tsx:125 msgid "Added to my feeds" -msgstr "" +msgstr "Ajouté à mes fils d’actu" #: src/view/screens/PreferencesHomeFeed.tsx:173 msgid "Adjust the number of likes a reply must have to be shown in your feed." @@ -228,7 +224,7 @@ msgstr "Contenu pour adultes" #: src/view/com/modals/ContentFilteringSettings.tsx:137 msgid "Adult content can only be enabled via the Web at <0/>." -msgstr "" +msgstr "Le contenu pour adultes ne peut être activé que via le Web à <0/>." #: src/view/screens/Settings.tsx:630 msgid "Advanced" @@ -236,7 +232,7 @@ msgstr "Avancé" #: src/view/com/auth/login/ChooseAccountForm.tsx:98 msgid "Already signed in as @{0}" -msgstr "" +msgstr "Déjà connecté·e en tant que @{0}" #: src/view/com/composer/photos/Gallery.tsx:130 msgid "ALT" @@ -248,7 +244,7 @@ msgstr "Texte Alt" #: src/view/com/composer/photos/Gallery.tsx:209 msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone." -msgstr "Le texte Alt décrit les images pour les utilisateurs aveugles et malvoyants, et aide à donner un contexte à tout le monde." +msgstr "Le texte Alt décrit les images pour les personnes aveugles et malvoyantes, et aide à donner un contexte à tout le monde." #: src/view/com/modals/VerifyEmail.tsx:124 msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." @@ -261,7 +257,7 @@ msgstr "Un courriel a été envoyé à votre ancienne adresse, {0}. Il comprend #: src/view/com/profile/FollowButton.tsx:30 #: src/view/com/profile/FollowButton.tsx:40 msgid "An issue occurred, please try again." -msgstr "" +msgstr "Un problème est survenu, veuillez réessayer." #: src/view/com/notifications/FeedItem.tsx:240 #: src/view/com/threadgate/WhoCanReply.tsx:178 @@ -274,19 +270,19 @@ msgstr "Langue de l’application" #: src/view/screens/AppPasswords.tsx:228 msgid "App password deleted" -msgstr "" +msgstr "Mot de passe d’application supprimé" #: src/view/com/modals/AddAppPasswords.tsx:133 msgid "App Password names can only contain letters, numbers, spaces, dashes, and underscores." -msgstr "" +msgstr "Les noms de mots de passe d’application ne peuvent contenir que des lettres, des chiffres, des espaces, des tirets et des tirets bas." #: src/view/com/modals/AddAppPasswords.tsx:98 msgid "App Password names must be at least 4 characters long." -msgstr "" +msgstr "Les noms de mots de passe d’application doivent comporter au moins 4 caractères." #: src/view/screens/Settings.tsx:641 msgid "App password settings" -msgstr "" +msgstr "Paramètres de mot de passe d’application" #: src/view/screens/Settings.tsx:650 msgid "App passwords" @@ -305,9 +301,6 @@ msgstr "Faire appel de l’avertissement sur le contenu" msgid "Appeal Content Warning" msgstr "Appel de l’avertissement sur le contenu" -#~ msgid "Appeal Decision" -#~ msgstr "Appel de la décision" - #: src/view/com/util/moderation/LabelInfo.tsx:52 msgid "Appeal this decision" msgstr "Faire appel de cette décision" @@ -322,19 +315,19 @@ msgstr "Affichage" #: src/view/screens/AppPasswords.tsx:224 msgid "Are you sure you want to delete the app password \"{name}\"?" -msgstr "Êtes-vous sûr de vouloir supprimer le mot de passe de l’application « {name} » ?" +msgstr "Êtes-vous sûr de vouloir supprimer le mot de passe de l’application « {name} » ?" #: src/view/com/composer/Composer.tsx:143 msgid "Are you sure you'd like to discard this draft?" -msgstr "Êtes-vous sûr de vouloir rejeter ce brouillon ?" +msgstr "Êtes-vous sûr de vouloir rejeter ce brouillon ?" #: src/view/screens/ProfileList.tsx:364 msgid "Are you sure?" -msgstr "Vous confirmez ?" +msgstr "Vous confirmez ?" #: src/view/com/util/forms/PostDropdownBtn.tsx:231 msgid "Are you sure? This cannot be undone." -msgstr "Vous confirmez ? Cela ne pourra pas être annulé." +msgstr "Vous confirmez ? Cela ne pourra pas être annulé." #: src/view/com/composer/select-language/SuggestedLanguage.tsx:65 msgid "Are you writing in <0>{0}</0>?" @@ -344,7 +337,12 @@ msgstr "" msgid "Artistic or non-erotic nudity." msgstr "Nudité artistique ou non érotique." -#: src/view/com/auth/create/CreateAccount.tsx:141 +#: src/view/com/post-thread/PostThread.tsx:400 +msgctxt "action" +msgid "Back" +msgstr "Retour" + +#: src/view/com/auth/create/CreateAccount.tsx:142 #: src/view/com/auth/login/ChooseAccountForm.tsx:151 #: src/view/com/auth/login/ForgotPasswordForm.tsx:170 #: src/view/com/auth/login/LoginForm.tsx:256 @@ -358,23 +356,18 @@ msgstr "Nudité artistique ou non érotique." msgid "Back" msgstr "Arrière" -#: src/view/com/post-thread/PostThread.tsx:400 -msgctxt "action" -msgid "Back" -msgstr "" - #: src/view/screens/Settings.tsx:489 msgid "Basics" msgstr "Principes de base" -#: src/view/com/auth/create/Step2.tsx:156 +#: src/view/com/auth/create/Step1.tsx:194 #: src/view/com/modals/BirthDateSettings.tsx:73 msgid "Birthday" msgstr "Date de naissance" #: src/view/screens/Settings.tsx:340 msgid "Birthday:" -msgstr "Date de naissance :" +msgstr "Date de naissance :" #: src/view/com/profile/ProfileHeader.tsx:286 #: src/view/com/profile/ProfileHeader.tsx:393 @@ -391,16 +384,16 @@ msgstr "Liste de blocage" #: src/view/screens/ProfileList.tsx:315 msgid "Block these accounts?" -msgstr "Bloquer ces comptes ?" +msgstr "Bloquer ces comptes ?" #: src/view/screens/ProfileList.tsx:319 msgid "Block this List" -msgstr "" +msgstr "Bloquer cette liste" #: src/view/com/lists/ListCard.tsx:109 #: src/view/com/util/post-embeds/QuoteEmbed.tsx:57 msgid "Blocked" -msgstr "" +msgstr "Bloqué" #: src/view/screens/Moderation.tsx:123 msgid "Blocked accounts" @@ -456,7 +449,7 @@ msgstr "Bluesky distribue des invitations pour construire une communauté plus s #: src/view/screens/Moderation.tsx:225 msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." -msgstr "Bluesky n’affichera pas votre profil et vos messages à des utilisateurs non connectés. Il est possible que d’autres applications n’honorent pas cette demande. Cela ne privatise pas votre compte." +msgstr "Bluesky n’affichera pas votre profil et vos messages à des personnes non connectées. Il est possible que d’autres applications n’honorent pas cette demande. Cela ne privatise pas votre compte." #: src/view/com/modals/ServerInput.tsx:78 msgid "Bluesky.Social" @@ -472,23 +465,23 @@ msgstr "Affaires" #: src/view/com/modals/ServerInput.tsx:115 msgid "Button disabled. Input custom domain to proceed." -msgstr "" +msgstr "Bouton désactivé. Saisissez un domaine personnalisé pour continuer." #: src/view/com/profile/ProfileSubpageHeader.tsx:157 msgid "by —" -msgstr "" +msgstr "par —" #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:100 msgid "by {0}" -msgstr "" +msgstr "par {0}" #: src/view/com/profile/ProfileSubpageHeader.tsx:161 msgid "by <0/>" -msgstr "" +msgstr "par <0/>" #: src/view/com/profile/ProfileSubpageHeader.tsx:159 msgid "by you" -msgstr "" +msgstr "par vous" #: src/view/com/composer/photos/OpenCameraBtn.tsx:60 #: src/view/com/util/UserAvatar.tsx:221 @@ -500,6 +493,16 @@ msgstr "Caméra" msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." msgstr "Ne peut contenir que des lettres, des chiffres, des espaces, des tirets et des tirets bas. La longueur doit être d’au moins 4 caractères, mais pas plus de 32." +#: src/view/com/modals/Confirm.tsx:88 +#: src/view/com/modals/Confirm.tsx:91 +#: src/view/com/modals/CreateOrEditList.tsx:293 +#: src/view/com/modals/DeleteAccount.tsx:152 +#: src/view/com/modals/DeleteAccount.tsx:230 +msgctxt "action" +msgid "Cancel" +msgstr "Annuler" + +#: src/components/Prompt.tsx:92 #: src/view/com/composer/Composer.tsx:300 #: src/view/com/composer/Composer.tsx:305 #: src/view/com/modals/ChangeEmail.tsx:218 @@ -518,24 +521,11 @@ msgstr "Ne peut contenir que des lettres, des chiffres, des espaces, des tirets msgid "Cancel" msgstr "Annuler" -#: src/view/com/modals/Confirm.tsx:88 -#: src/view/com/modals/Confirm.tsx:91 -#: src/view/com/modals/CreateOrEditList.tsx:293 -#: src/view/com/modals/DeleteAccount.tsx:152 -#: src/view/com/modals/DeleteAccount.tsx:230 -msgctxt "action" -msgid "Cancel" -msgstr "" - #: src/view/com/modals/DeleteAccount.tsx:148 #: src/view/com/modals/DeleteAccount.tsx:226 msgid "Cancel account deletion" msgstr "Annuler la suppression de compte" -#: src/view/com/modals/AltImage.tsx:123 -#~ msgid "Cancel add image alt text" -#~ msgstr "Annuler l’ajout d’un texte alt à l’image" - #: src/view/com/modals/ChangeHandle.tsx:149 msgid "Cancel change handle" msgstr "Annuler le changement de pseudo" @@ -564,20 +554,16 @@ msgstr "Annuler l’inscription sur la liste d’attente" #: src/view/screens/Settings.tsx:334 msgctxt "action" msgid "Change" -msgstr "" - -#: src/view/screens/Settings.tsx:306 -#~ msgid "Change" -#~ msgstr "Changer" +msgstr "Modifier" #: src/view/screens/Settings.tsx:662 #: src/view/screens/Settings.tsx:671 msgid "Change handle" -msgstr "Changer le pseudo" +msgstr "Modifier le pseudo" #: src/view/com/modals/ChangeHandle.tsx:161 msgid "Change Handle" -msgstr "Changer le pseudo" +msgstr "Modifier le pseudo" #: src/view/com/modals/VerifyEmail.tsx:147 msgid "Change my email" @@ -589,7 +575,7 @@ msgstr "" #: src/view/com/modals/ChangeEmail.tsx:109 msgid "Change Your Email" -msgstr "Modifiez votre e-mail" +msgstr "Modifier votre e-mail" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121 msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds." @@ -597,19 +583,19 @@ msgstr "Consultez quelques fils d’actu recommandés. Appuyez sur + pour les aj #: src/view/com/auth/onboarding/RecommendedFollows.tsx:185 msgid "Check out some recommended users. Follow them to see similar users." -msgstr "Consultez quelques utilisateurs recommandés. Suivez-les pour voir des utilisateurs similaires." +msgstr "Consultez quelques comptes recommandés. Suivez-les pour voir des personnes similaires." #: src/view/com/modals/DeleteAccount.tsx:165 msgid "Check your inbox for an email with the confirmation code to enter below:" -msgstr "Consultez votre boîte de réception, vous avez du recevoir un e-mail contenant un code de confirmation à saisir ci-dessous :" +msgstr "Consultez votre boîte de réception, vous avez du recevoir un e-mail contenant un code de confirmation à saisir ci-dessous :" #: src/view/com/modals/Threadgate.tsx:72 msgid "Choose \"Everybody\" or \"Nobody\"" -msgstr "Choisir « Tout le monde » ou « Personne »" +msgstr "Choisir « Tout le monde » ou « Personne »" #: src/view/screens/Settings.tsx:663 msgid "Choose a new Bluesky username or create" -msgstr "" +msgstr "Choisir un nouveau pseudo Bluesky ou en créer un" #: src/view/com/modals/ServerInput.tsx:38 msgid "Choose Service" @@ -620,7 +606,7 @@ msgstr "Choisir un service" msgid "Choose the algorithms that power your experience with custom feeds." msgstr "Choisissez les algorithmes qui alimentent votre expérience avec des fils d’actualité personnalisés." -#: src/view/com/auth/create/Step2.tsx:127 +#: src/view/com/auth/create/Step1.tsx:163 msgid "Choose your password" msgstr "Choisissez votre mot de passe" @@ -649,6 +635,10 @@ msgstr "Effacer la recherche" #: src/view/screens/Support.tsx:40 msgid "click here" +msgstr "cliquez ici" + +#: src/components/Dialog/index.web.tsx:78 +msgid "Close active dialog" msgstr "" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:38 @@ -667,29 +657,29 @@ msgstr "Fermer l’image" msgid "Close image viewer" msgstr "Fermer la visionneuse d’images" -#: src/view/shell/index.web.tsx:49 +#: src/view/shell/index.web.tsx:51 msgid "Close navigation footer" msgstr "Fermer le pied de page de navigation" -#: src/view/shell/index.web.tsx:50 +#: src/view/shell/index.web.tsx:52 msgid "Closes bottom navigation bar" -msgstr "" +msgstr "Ferme la barre de navigation du bas" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:39 msgid "Closes password update alert" -msgstr "" +msgstr "Ferme la notification de mise à jour du mot de passe" #: src/view/com/composer/Composer.tsx:302 msgid "Closes post composer and discards post draft" -msgstr "" +msgstr "Ferme la fenêtre de rédaction et supprime le brouillon" #: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:27 msgid "Closes viewer for header image" -msgstr "" +msgstr "Ferme la visionneuse pour l’image d’en-tête" #: src/view/com/notifications/FeedItem.tsx:321 msgid "Collapses list of users for a given notification" -msgstr "" +msgstr "Réduit la liste des comptes pour une notification donnée" #: src/Navigation.tsx:229 #: src/view/screens/CommunityGuidelines.tsx:32 @@ -698,12 +688,19 @@ msgstr "Directives communautaires" #: src/view/com/composer/Composer.tsx:417 msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" -msgstr "" +msgstr "Permet d’écrire des messages de {MAX_GRAPHEME_LENGTH} caractères maximum" #: src/view/com/composer/Prompt.tsx:24 msgid "Compose reply" msgstr "Rédiger une réponse" +#: src/view/com/modals/Confirm.tsx:75 +#: src/view/com/modals/Confirm.tsx:78 +msgctxt "action" +msgid "Confirm" +msgstr "Confirmer" + +#: src/components/Prompt.tsx:114 #: src/view/com/modals/AppealLabel.tsx:98 #: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/VerifyEmail.tsx:231 @@ -713,12 +710,6 @@ msgstr "Rédiger une réponse" msgid "Confirm" msgstr "Confirmer" -#: src/view/com/modals/Confirm.tsx:75 -#: src/view/com/modals/Confirm.tsx:78 -msgctxt "action" -msgid "Confirm" -msgstr "" - #: src/view/com/modals/ChangeEmail.tsx:193 #: src/view/com/modals/ChangeEmail.tsx:195 msgid "Confirm Change" @@ -734,7 +725,7 @@ msgstr "Confirmer la suppression du compte" #: src/view/com/modals/ContentFilteringSettings.tsx:151 msgid "Confirm your age to enable adult content." -msgstr "" +msgstr "Confirmez votre âge pour activer le contenu pour adultes." #: src/view/com/modals/ChangeEmail.tsx:157 #: src/view/com/modals/DeleteAccount.tsx:178 @@ -744,13 +735,17 @@ msgstr "Code de confirmation" #: src/view/com/modals/Waitlist.tsx:120 msgid "Confirms signing up {email} to the waitlist" -msgstr "" +msgstr "Confirme l’inscription de {email} sur la liste d’attente" -#: src/view/com/auth/create/CreateAccount.tsx:174 +#: src/view/com/auth/create/CreateAccount.tsx:175 #: src/view/com/auth/login/LoginForm.tsx:275 msgid "Connecting..." msgstr "Connexion…" +#: src/view/com/auth/create/CreateAccount.tsx:195 +msgid "Contact support" +msgstr "" + #: src/view/screens/Moderation.tsx:81 msgid "Content filtering" msgstr "Filtrage du contenu" @@ -766,7 +761,7 @@ msgstr "Langues du contenu" #: src/view/com/modals/ModerationDetails.tsx:65 msgid "Content Not Available" -msgstr "" +msgstr "Contenu non disponible" #: src/view/com/modals/ModerationDetails.tsx:33 #: src/view/com/util/moderation/ScreenHider.tsx:78 @@ -789,17 +784,17 @@ msgstr "Copié" #: src/view/screens/Settings.tsx:243 msgid "Copied build version to clipboard" -msgstr "" +msgstr "Version de build copiée dans le presse-papier" #: src/view/com/modals/AddAppPasswords.tsx:75 #: src/view/com/modals/InviteCodes.tsx:152 #: src/view/com/util/forms/PostDropdownBtn.tsx:110 msgid "Copied to clipboard" -msgstr "" +msgstr "Copié dans le presse-papier" #: src/view/com/modals/AddAppPasswords.tsx:191 msgid "Copies app password" -msgstr "" +msgstr "Copie le mot de passe d’application" #: src/view/com/modals/AddAppPasswords.tsx:190 msgid "Copy" @@ -807,7 +802,7 @@ msgstr "Copie" #: src/view/screens/ProfileList.tsx:396 msgid "Copy link to list" -msgstr "Copier le lien dans la liste" +msgstr "Copier le lien vers la liste" #: src/view/com/util/forms/PostDropdownBtn.tsx:151 msgid "Copy link to post" @@ -815,7 +810,7 @@ msgstr "Copier le lien vers le post" #: src/view/com/profile/ProfileHeader.tsx:342 msgid "Copy link to profile" -msgstr "Copier le lien sur le profil" +msgstr "Copier le lien vers le profil" #: src/view/com/util/forms/PostDropdownBtn.tsx:137 msgid "Copy post text" @@ -842,15 +837,15 @@ msgstr "Créer un nouveau compte" #: src/view/screens/Settings.tsx:384 msgid "Create a new Bluesky account" -msgstr "" +msgstr "Créer un compte Bluesky" -#: src/view/com/auth/create/CreateAccount.tsx:121 +#: src/view/com/auth/create/CreateAccount.tsx:122 msgid "Create Account" msgstr "Créer un compte" #: src/view/com/modals/AddAppPasswords.tsx:228 msgid "Create App Password" -msgstr "" +msgstr "Créer un mot de passe d’application" #: src/view/com/auth/HomeLoggedOutCTA.tsx:54 #: src/view/com/auth/SplashScreen.tsx:43 @@ -863,15 +858,15 @@ msgstr "{0} créé" #: src/view/screens/ProfileFeed.tsx:625 msgid "Created by <0/>" -msgstr "" +msgstr "Créée par <0/>" #: src/view/screens/ProfileFeed.tsx:623 msgid "Created by you" -msgstr "" +msgstr "Créée par vous" #: src/view/com/composer/Composer.tsx:448 msgid "Creates a card with a thumbnail. The card links to {url}" -msgstr "" +msgstr "Crée une carte avec une miniature. La carte pointe vers {url}" #: src/view/com/modals/ChangeHandle.tsx:389 #: src/view/com/modals/ServerInput.tsx:102 @@ -880,7 +875,7 @@ msgstr "Domaine personnalisé" #: src/view/screens/PreferencesExternalEmbeds.tsx:55 msgid "Customize media from external sites." -msgstr "" +msgstr "Personnaliser les médias provenant de sites externes." #: src/view/screens/Settings.tsx:687 msgid "Danger Zone" @@ -888,19 +883,19 @@ msgstr "Zone de danger" #: src/view/screens/Settings.tsx:479 msgid "Dark" -msgstr "" +msgstr "Sombre" #: src/view/screens/Debug.tsx:63 msgid "Dark mode" -msgstr "" +msgstr "Mode sombre" #: src/Navigation.tsx:204 -msgid "Debug" -msgstr "" +#~ msgid "Debug" +#~ msgstr "Débug" #: src/view/screens/Debug.tsx:83 msgid "Debug panel" -msgstr "" +msgstr "Panneau de débug" #: src/view/screens/Settings.tsx:694 msgid "Delete account" @@ -934,11 +929,11 @@ msgstr "Supprimer le post" #: src/view/com/util/forms/PostDropdownBtn.tsx:230 msgid "Delete this post?" -msgstr "Supprimer ce post ?" +msgstr "Supprimer ce post ?" #: src/view/com/util/post-embeds/QuoteEmbed.tsx:66 msgid "Deleted" -msgstr "" +msgstr "Supprimé" #: src/view/com/post-thread/PostThread.tsx:246 msgid "Deleted post." @@ -952,8 +947,8 @@ msgid "Description" msgstr "Description" #: src/view/com/auth/create/Step1.tsx:96 -msgid "Dev Server" -msgstr "Serveur de dév" +#~ msgid "Dev Server" +#~ msgstr "Serveur de dév" #: src/view/screens/Settings.tsx:711 msgid "Developer Tools" @@ -961,7 +956,7 @@ msgstr "Outils de dév" #: src/view/com/composer/Composer.tsx:211 msgid "Did you want to say anything?" -msgstr "Vous vouliez dire quelque chose ?" +msgstr "Vous vouliez dire quelque chose ?" #: src/view/com/composer/Composer.tsx:144 msgid "Discard" @@ -973,12 +968,12 @@ msgstr "Ignorer le brouillon" #: src/view/screens/Moderation.tsx:207 msgid "Discourage apps from showing my account to logged-out users" -msgstr "Empêcher les applis de montrer mon compte aux utilisateurs déconnectés" +msgstr "Empêcher les applis de montrer mon compte aux personnes non connectées" #: src/view/com/posts/FollowingEmptyState.tsx:74 #: src/view/com/posts/FollowingEndOfFeed.tsx:75 msgid "Discover new custom feeds" -msgstr "" +msgstr "Découvrir des fils d’actu personnalisés" #: src/view/screens/Feeds.tsx:409 msgid "Discover new feeds" @@ -994,11 +989,11 @@ msgstr "Afficher le nom" #: src/view/com/modals/ChangeHandle.tsx:487 msgid "Domain verified!" -msgstr "Domaine vérifié !" +msgstr "Domaine vérifié !" -#: src/view/com/auth/create/Step2.tsx:83 +#: src/view/com/auth/create/Step1.tsx:114 msgid "Don't have an invite code?" -msgstr "" +msgstr "Pas de code d’invitation ?" #: src/view/com/auth/onboarding/RecommendedFollows.tsx:86 #: src/view/com/modals/EditImage.tsx:333 @@ -1011,7 +1006,7 @@ msgstr "" #: src/view/screens/PreferencesThreads.tsx:162 msgctxt "action" msgid "Done" -msgstr "" +msgstr "Terminer" #: src/view/com/modals/AddAppPasswords.tsx:228 #: src/view/com/modals/AltImage.tsx:115 @@ -1031,31 +1026,31 @@ msgstr "Terminé{extraText}" #: src/view/com/auth/login/ChooseAccountForm.tsx:45 msgid "Double tap to sign in" -msgstr "" +msgstr "Tapotez deux fois pour vous connecter" #: src/view/com/modals/EditProfile.tsx:185 msgid "e.g. Alice Roberts" -msgstr "" +msgstr "ex. Alice Dupont" #: src/view/com/modals/EditProfile.tsx:203 msgid "e.g. Artist, dog-lover, and avid reader." -msgstr "" +msgstr "ex. Artiste, amoureuse des chiens et lectrice passionnée." #: src/view/com/modals/CreateOrEditList.tsx:225 msgid "e.g. Great Posters" -msgstr "" +msgstr "ex. Les meilleurs comptes" #: src/view/com/modals/CreateOrEditList.tsx:226 msgid "e.g. Spammers" -msgstr "" +msgstr "ex. Spammeurs" #: src/view/com/modals/CreateOrEditList.tsx:246 msgid "e.g. The posters who never miss." -msgstr "" +msgstr "ex. Ces comptes qui ne ratent jamais leur coup." #: src/view/com/modals/CreateOrEditList.tsx:247 msgid "e.g. Users that repeatedly reply with ads." -msgstr "" +msgstr "ex. Les comptes qui répondent toujours avec des pubs." #: src/view/com/modals/InviteCodes.tsx:96 msgid "Each code works once. You'll receive more invite codes periodically." @@ -1064,7 +1059,7 @@ msgstr "Chaque code ne fonctionne qu’une seule fois. Vous recevrez régulière #: src/view/com/lists/ListMembers.tsx:149 msgctxt "action" msgid "Edit" -msgstr "" +msgstr "Modifier" #: src/view/com/composer/photos/Gallery.tsx:144 #: src/view/com/modals/EditImage.tsx:207 @@ -1077,7 +1072,7 @@ msgstr "Modifier les infos de la liste" #: src/view/com/modals/CreateOrEditList.tsx:193 msgid "Edit Moderation List" -msgstr "" +msgstr "Modifier la liste de modération" #: src/Navigation.tsx:244 #: src/view/screens/Feeds.tsx:371 @@ -1103,24 +1098,26 @@ msgstr "Modifier les fils d’actu enregistrés" #: src/view/com/modals/CreateOrEditList.tsx:188 msgid "Edit User List" -msgstr "" +msgstr "Modifier la liste de comptes" #: src/view/com/modals/EditProfile.tsx:193 msgid "Edit your display name" -msgstr "" +msgstr "Modifier votre nom d’affichage" #: src/view/com/modals/EditProfile.tsx:211 msgid "Edit your profile description" -msgstr "" +msgstr "Modifier votre description de profil" -#: src/view/com/auth/create/Step2.tsx:108 +#: src/view/com/auth/create/Step1.tsx:143 +#: src/view/com/auth/create/Step2.tsx:90 +#: src/view/com/auth/create/Step2.tsx:164 #: src/view/com/auth/login/ForgotPasswordForm.tsx:152 #: src/view/com/modals/ChangeEmail.tsx:141 #: src/view/com/modals/Waitlist.tsx:88 msgid "Email" msgstr "E-mail" -#: src/view/com/auth/create/Step2.tsx:99 +#: src/view/com/auth/create/Step1.tsx:134 #: src/view/com/auth/login/ForgotPasswordForm.tsx:143 msgid "Email address" msgstr "Adresse e-mail" @@ -1128,7 +1125,7 @@ msgstr "Adresse e-mail" #: src/view/com/modals/ChangeEmail.tsx:56 #: src/view/com/modals/ChangeEmail.tsx:88 msgid "Email updated" -msgstr "" +msgstr "Adresse e-mail mise à jour" #: src/view/com/modals/ChangeEmail.tsx:111 msgid "Email Updated" @@ -1136,27 +1133,27 @@ msgstr "E-mail mis à jour" #: src/view/com/modals/VerifyEmail.tsx:78 msgid "Email verified" -msgstr "" +msgstr "Adresse e-mail vérifiée" #: src/view/screens/Settings.tsx:312 msgid "Email:" -msgstr "E-mail :" +msgstr "E-mail :" #: src/view/com/modals/EmbedConsent.tsx:113 msgid "Enable {0} only" -msgstr "" +msgstr "Activer {0} uniquement" #: src/view/com/modals/ContentFilteringSettings.tsx:162 msgid "Enable Adult Content" -msgstr "" +msgstr "Activer le contenu pour adultes" #: src/view/com/modals/EmbedConsent.tsx:97 msgid "Enable External Media" -msgstr "" +msgstr "Activer les médias externes" #: src/view/screens/PreferencesExternalEmbeds.tsx:75 msgid "Enable media players for" -msgstr "" +msgstr "Activer les lecteurs médias pour" #: src/view/screens/PreferencesHomeFeed.tsx:147 msgid "Enable this setting to only see replies between people you follow." @@ -1168,15 +1165,15 @@ msgstr "Fin du fil d’actu" #: src/view/com/modals/AddAppPasswords.tsx:165 msgid "Enter a name for this App Password" -msgstr "" +msgstr "Entrer un nom pour ce mot de passe d’application" #: src/view/com/modals/VerifyEmail.tsx:105 msgid "Enter Confirmation Code" -msgstr "" +msgstr "Entrer un code de confirmation" #: src/view/com/auth/create/Step1.tsx:71 -msgid "Enter the address of your provider:" -msgstr "Saisissez l’adresse de votre fournisseur :" +#~ msgid "Enter the address of your provider:" +#~ msgstr "Saisissez l’adresse de votre hébergeur :" #: src/view/com/modals/ChangeHandle.tsx:371 msgid "Enter the domain you want to use" @@ -1184,36 +1181,40 @@ msgstr "Entrez le domaine que vous voulez utiliser" #: src/view/com/auth/login/ForgotPasswordForm.tsx:103 msgid "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." -msgstr "Saisissez l’e-mail que vous avez utilisé pour créer votre compte. Nous vous enverrons un « code de réinitialisation » afin changer votre mot de passe." +msgstr "Saisissez l’e-mail que vous avez utilisé pour créer votre compte. Nous vous enverrons un « code de réinitialisation » afin changer votre mot de passe." -#: src/view/com/auth/create/Step2.tsx:157 +#: src/view/com/auth/create/Step1.tsx:195 #: src/view/com/modals/BirthDateSettings.tsx:74 msgid "Enter your birth date" -msgstr "" +msgstr "Saisissez votre date de naissance" #: src/view/com/modals/Waitlist.tsx:78 msgid "Enter your email" -msgstr "" +msgstr "Entrez votre e-mail" -#: src/view/com/auth/create/Step2.tsx:104 +#: src/view/com/auth/create/Step1.tsx:139 msgid "Enter your email address" msgstr "Entrez votre e-mail" #: src/view/com/modals/ChangeEmail.tsx:41 msgid "Enter your new email above" -msgstr "" +msgstr "Entrez votre nouvel e-mail ci-dessus" #: src/view/com/modals/ChangeEmail.tsx:117 msgid "Enter your new email address below." msgstr "Entrez votre nouvelle e-mail ci-dessous." +#: src/view/com/auth/create/Step2.tsx:84 +msgid "Enter your phone number" +msgstr "" + #: src/view/com/auth/login/Login.tsx:99 msgid "Enter your username and password" -msgstr "Entrez votre nom d’utilisateur et votre mot de passe" +msgstr "Entrez votre pseudo et votre mot de passe" #: src/view/screens/Search/Search.tsx:105 msgid "Error:" -msgstr "Erreur :" +msgstr "Erreur :" #: src/view/com/modals/Threadgate.tsx:76 msgid "Everybody" @@ -1221,20 +1222,20 @@ msgstr "Tout le monde" #: src/view/com/modals/ChangeHandle.tsx:150 msgid "Exits handle change process" -msgstr "" +msgstr "Sort du processus de changement de pseudo" #: src/view/com/lightbox/Lightbox.web.tsx:113 msgid "Exits image view" -msgstr "" +msgstr "Sort de la vue de l’image" #: src/view/com/modals/ListAddRemoveUsers.tsx:88 #: src/view/shell/desktop/Search.tsx:182 msgid "Exits inputting search query" -msgstr "" +msgstr "Sort de la saisie de la recherche" #: src/view/com/modals/Waitlist.tsx:138 msgid "Exits signing up for waitlist with {email}" -msgstr "" +msgstr "Sort de l’inscription sur la liste d’attente avec {email}" #: src/view/com/lightbox/Lightbox.web.tsx:156 msgid "Expand alt text" @@ -1243,39 +1244,39 @@ msgstr "Développer le texte alt" #: src/view/com/composer/ComposerReplyTo.tsx:81 #: src/view/com/composer/ComposerReplyTo.tsx:84 msgid "Expand or collapse the full post you are replying to" -msgstr "" +msgstr "Développe ou réduit le post complet auquel vous répondez" #: src/view/com/modals/EmbedConsent.tsx:64 msgid "External Media" -msgstr "" +msgstr "Média externe" #: src/view/com/modals/EmbedConsent.tsx:75 #: src/view/screens/PreferencesExternalEmbeds.tsx:66 msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." -msgstr "" +msgstr "Les médias externes peuvent permettre à des sites web de collecter des informations sur vous et votre appareil. Aucune information n’est envoyée ou demandée tant que vous n’appuyez pas sur le bouton de lecture." #: src/Navigation.tsx:260 #: src/view/screens/PreferencesExternalEmbeds.tsx:52 #: src/view/screens/Settings.tsx:623 msgid "External Media Preferences" -msgstr "" +msgstr "Préférences sur les médias externes" #: src/view/screens/Settings.tsx:614 msgid "External media settings" -msgstr "" +msgstr "Préférences sur les médias externes" #: src/view/com/modals/AddAppPasswords.tsx:114 #: src/view/com/modals/AddAppPasswords.tsx:118 msgid "Failed to create app password." -msgstr "" +msgstr "Échec de la création du mot de passe d’application." #: src/view/com/modals/CreateOrEditList.tsx:148 msgid "Failed to create the list. Check your internet connection and try again." -msgstr "" +msgstr "Échec de la création de la liste. Vérifiez votre connexion Internet et réessayez." #: src/view/com/util/forms/PostDropdownBtn.tsx:86 msgid "Failed to delete post, please try again" -msgstr "" +msgstr "Échec de la suppression du post, veuillez réessayer" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:109 #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:141 @@ -1284,11 +1285,11 @@ msgstr "Échec du chargement des fils d’actu recommandés" #: src/Navigation.tsx:194 msgid "Feed" -msgstr "" +msgstr "Fil d’actu" #: src/view/com/feeds/FeedSourceCard.tsx:229 msgid "Feed by {0}" -msgstr "" +msgstr "Fil d’actu par {0}" #: src/view/screens/Feeds.tsx:560 msgid "Feed offline" @@ -1299,7 +1300,7 @@ msgid "Feed Preferences" msgstr "Préférences en matière de fil d’actu" #: src/view/shell/desktop/RightNav.tsx:73 -#: src/view/shell/Drawer.tsx:311 +#: src/view/shell/Drawer.tsx:314 msgid "Feedback" msgstr "Feedback" @@ -1308,32 +1309,32 @@ msgstr "Feedback" #: src/view/screens/Profile.tsx:165 #: src/view/shell/bottom-bar/BottomBar.tsx:181 #: src/view/shell/desktop/LeftNav.tsx:342 -#: src/view/shell/Drawer.tsx:476 -#: src/view/shell/Drawer.tsx:477 +#: src/view/shell/Drawer.tsx:479 +#: src/view/shell/Drawer.tsx:480 msgid "Feeds" msgstr "Fil d’actu" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57 msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." -msgstr "Les fils d’actu sont créés par les utilisateurs pour rassembler du contenu. Choisissez des fils d’actu qui vous intéressent." +msgstr "Les fils d’actu sont créés par d’autres personnes pour rassembler du contenu. Choisissez des fils d’actu qui vous intéressent." #: src/view/screens/SavedFeeds.tsx:156 msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." -msgstr "Les fils d’actu sont des algorithmes personnalisés que les utilisateurs construisent avec un peu d’expertise en codage. <0/> pour obtenir de plus amples informations." +msgstr "Les fils d’actu sont des algorithmes personnalisés qui se construisent avec un peu d’expertise en programmation. <0/> pour plus d’informations." #: src/view/com/posts/CustomFeedEmptyState.tsx:47 #: src/view/com/posts/FollowingEmptyState.tsx:57 #: src/view/com/posts/FollowingEndOfFeed.tsx:58 msgid "Find accounts to follow" -msgstr "" +msgstr "Trouver des comptes à suivre" #: src/view/screens/Search/Search.tsx:427 msgid "Find users on Bluesky" -msgstr "Trouver des utilisateurs sur Bluesky" +msgstr "Trouver des comptes sur Bluesky" #: src/view/screens/Search/Search.tsx:425 msgid "Find users with the search tool on the right" -msgstr "Trouvez des utilisateurs à l’aide de l’outil de recherche, à droite" +msgstr "Trouvez des comptes à l’aide de l’outil de recherche, à droite" #: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:150 msgid "Finding similar accounts..." @@ -1341,25 +1342,25 @@ msgstr "Recherche de comptes similaires…" #: src/view/screens/PreferencesHomeFeed.tsx:111 msgid "Fine-tune the content you see on your home screen." -msgstr "Affinez le contenu de votre écran d’accueil." +msgstr "Affine le contenu affiché sur votre écran d’accueil." #: src/view/screens/PreferencesThreads.tsx:60 msgid "Fine-tune the discussion threads." -msgstr "Affiner les fils de discussion." +msgstr "Affine les fils de discussion." #: src/view/com/modals/EditImage.tsx:115 msgid "Flip horizontal" -msgstr "" +msgstr "Miroir horizontal" #: src/view/com/modals/EditImage.tsx:120 #: src/view/com/modals/EditImage.tsx:287 msgid "Flip vertically" -msgstr "" +msgstr "Miroir vertical" #: src/view/com/profile/FollowButton.tsx:64 msgctxt "action" msgid "Follow" -msgstr "" +msgstr "Suivre" #: src/view/com/profile/ProfileHeader.tsx:552 msgid "Follow" @@ -1367,35 +1368,31 @@ msgstr "Suivre" #: src/view/com/profile/ProfileHeader.tsx:543 msgid "Follow {0}" -msgstr "" +msgstr "Suivre {0}" #: src/view/com/auth/onboarding/RecommendedFollows.tsx:64 msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." -msgstr "Suivez quelques utilisateurs pour commencer. Nous pouvons vous recommander d’autres utilisateurs en fonction des personnes qui vous intéressent." +msgstr "Suivez quelques comptes pour commencer. Nous pouvons vous recommander d’autres comptes en fonction des personnes qui vous intéressent." #: src/view/com/profile/ProfileCard.tsx:194 msgid "Followed by {0}" -msgstr "" +msgstr "Suivi par {0}" #: src/view/com/modals/Threadgate.tsx:98 msgid "Followed users" -msgstr "Utilisateurs suivis" +msgstr "Comptes suivis" #: src/view/screens/PreferencesHomeFeed.tsx:154 msgid "Followed users only" -msgstr "Utilisateurs suivis uniquement" +msgstr "Comptes suivis uniquement" #: src/view/com/notifications/FeedItem.tsx:166 msgid "followed you" -msgstr "" +msgstr "vous suit" #: src/view/screens/ProfileFollowers.tsx:25 msgid "Followers" -msgstr "Followers" - -#: src/view/com/profile/ProfileHeader.tsx:624 -#~ msgid "following" -#~ msgstr "suivi" +msgstr "Abonné·e·s" #: src/view/com/profile/ProfileHeader.tsx:534 #: src/view/screens/ProfileFollows.tsx:25 @@ -1404,7 +1401,7 @@ msgstr "Suivi" #: src/view/com/profile/ProfileHeader.tsx:196 msgid "Following {0}" -msgstr "" +msgstr "Suit {0}" #: src/view/com/profile/ProfileHeader.tsx:585 msgid "Follows you" @@ -1412,7 +1409,7 @@ msgstr "Vous suit" #: src/view/com/profile/ProfileCard.tsx:141 msgid "Follows You" -msgstr "" +msgstr "Vous suit" #: src/view/com/modals/DeleteAccount.tsx:107 msgid "For security reasons, we'll need to send a confirmation code to your email address." @@ -1438,7 +1435,7 @@ msgstr "Mot de passe oublié" #: src/view/com/posts/FeedItem.tsx:188 msgctxt "from-feed" msgid "From <0/>" -msgstr "" +msgstr "Tiré de <0/>" #: src/view/com/composer/photos/SelectPhotoBtn.tsx:43 msgid "Gallery" @@ -1473,8 +1470,12 @@ msgstr "Aller à la suite" msgid "Handle" msgstr "Pseudo" +#: src/view/com/auth/create/CreateAccount.tsx:190 +msgid "Having trouble?" +msgstr "" + #: src/view/shell/desktop/RightNav.tsx:102 -#: src/view/shell/Drawer.tsx:321 +#: src/view/shell/Drawer.tsx:324 msgid "Help" msgstr "Aide" @@ -1486,7 +1487,7 @@ msgstr "Voici le mot de passe de votre appli." #: src/view/com/notifications/FeedItem.tsx:329 msgctxt "action" msgid "Hide" -msgstr "" +msgstr "Cacher" #: src/view/com/modals/ContentFilteringSettings.tsx:246 #: src/view/com/util/moderation/ContentHider.tsx:105 @@ -1501,50 +1502,50 @@ msgstr "Cacher ce post" #: src/view/com/util/moderation/ContentHider.tsx:67 #: src/view/com/util/moderation/PostHider.tsx:61 msgid "Hide the content" -msgstr "" +msgstr "Cacher ce contenu" #: src/view/com/util/forms/PostDropdownBtn.tsx:189 msgid "Hide this post?" -msgstr "Cacher ce post ?" +msgstr "Cacher ce post ?" #: src/view/com/notifications/FeedItem.tsx:319 msgid "Hide user list" -msgstr "Cacher la liste des utilisateurs" +msgstr "Cacher la liste des comptes" #: src/view/com/profile/ProfileHeader.tsx:526 msgid "Hides posts from {0} in your feed" -msgstr "" +msgstr "Masque les posts de {0} dans votre fil d’actu" #: src/view/com/posts/FeedErrorMessage.tsx:111 msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue." -msgstr "Hmm, un problème s’est produit avec le serveur de fils d’actu. Veuillez informer le propriétaire du fil d’actu de ce problème." +msgstr "Hmm, un problème s’est produit avec le serveur de fils d’actu. Veuillez informer la personne propriétaire du fil d’actu de ce problème." #: src/view/com/posts/FeedErrorMessage.tsx:99 msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue." -msgstr "Hmm, le serveur du fils d’actu semble être mal configuré. Veuillez informer le propriétaire du fil d’actu de ce problème." +msgstr "Hmm, le serveur du fils d’actu semble être mal configuré. Veuillez informer la personne propriétaire du fil d’actu de ce problème." #: src/view/com/posts/FeedErrorMessage.tsx:105 msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue." -msgstr "Mmm… le serveur de fils d’actu semble être hors ligne. Veuillez informer le propriétaire du fil d’actu de ce problème." +msgstr "Mmm… le serveur de fils d’actu semble être hors ligne. Veuillez informer la personne propriétaire du fil d’actu de ce problème." #: src/view/com/posts/FeedErrorMessage.tsx:102 msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue." -msgstr "Mmm… le serveur de fils d’actu ne répond pas. Veuillez informer le propriétaire du fil d’actu de ce problème." +msgstr "Mmm… le serveur de fils d’actu ne répond pas. Veuillez informer la personne propriétaire du fil d’actu de ce problème." #: src/view/com/posts/FeedErrorMessage.tsx:96 msgid "Hmm, we're having trouble finding this feed. It may have been deleted." -msgstr "Hmm, nous n’arrivons pas à trouver ce fils d’actu. Il a peut-être été supprimé." +msgstr "Hmm, nous n’arrivons pas à trouver ce fil d’actu. Il a peut-être été supprimé." #: src/Navigation.tsx:432 #: src/view/shell/bottom-bar/BottomBar.tsx:137 #: src/view/shell/desktop/LeftNav.tsx:306 -#: src/view/shell/Drawer.tsx:398 -#: src/view/shell/Drawer.tsx:399 +#: src/view/shell/Drawer.tsx:401 +#: src/view/shell/Drawer.tsx:402 msgid "Home" msgstr "Accueil" #: src/Navigation.tsx:249 -#: src/view/com/pager/FeedsTabBarMobile.tsx:98 +#: src/view/com/pager/FeedsTabBarMobile.tsx:117 #: src/view/screens/PreferencesHomeFeed.tsx:104 #: src/view/screens/Settings.tsx:509 msgid "Home Feed Preferences" @@ -1552,12 +1553,12 @@ msgstr "Préférences de fils d’actu de l’accueil" #: src/view/com/auth/login/ForgotPasswordForm.tsx:116 msgid "Hosting provider" -msgstr "Fournisseur d’hébergement" +msgstr "Hébergeur" #: src/view/com/auth/create/Step1.tsx:76 #: src/view/com/auth/create/Step1.tsx:81 -msgid "Hosting provider address" -msgstr "Adresse de l’hébergeur" +#~ msgid "Hosting provider address" +#~ msgstr "Adresse de l’hébergeur" #: src/view/com/modals/InAppBrowserConsent.tsx:44 msgid "How should we open this link?" @@ -1569,7 +1570,7 @@ msgstr "J’ai un code" #: src/view/com/modals/VerifyEmail.tsx:216 msgid "I have a confirmation code" -msgstr "" +msgstr "J’ai un code de confirmation" #: src/view/com/modals/ChangeHandle.tsx:283 msgid "I have my own domain" @@ -1577,7 +1578,7 @@ msgstr "J’ai mon propre domaine" #: src/view/com/lightbox/Lightbox.web.tsx:158 msgid "If alt text is long, toggles alt text expanded state" -msgstr "" +msgstr "Si le texte alternatif est trop long, change son mode d’affichage" #: src/view/com/modals/SelfLabel.tsx:127 msgid "If none are selected, suitable for all ages." @@ -1585,7 +1586,7 @@ msgstr "Si rien n’est sélectionné, il n’y a pas de restriction d’âge." #: src/view/com/util/images/Gallery.tsx:37 msgid "Image" -msgstr "" +msgstr "Image" #: src/view/com/modals/AltImage.tsx:97 msgid "Image alt text" @@ -1598,63 +1599,75 @@ msgstr "Options d’images" #: src/view/com/auth/login/SetNewPasswordForm.tsx:110 msgid "Input code sent to your email for password reset" -msgstr "" +msgstr "Entrez le code envoyé à votre e-mail pour réinitialiser le mot de passe" #: src/view/com/modals/DeleteAccount.tsx:180 msgid "Input confirmation code for account deletion" +msgstr "Entrez le code de confirmation pour supprimer le compte" + +#: src/view/com/auth/create/Step1.tsx:144 +msgid "Input email for Bluesky account" msgstr "" #: src/view/com/auth/create/Step2.tsx:109 -msgid "Input email for Bluesky waitlist" -msgstr "" +#~ msgid "Input email for Bluesky waitlist" +#~ msgstr "Entrez l’e-mail pour la liste d’attente de Bluesky" #: src/view/com/auth/create/Step1.tsx:80 -msgid "Input hosting provider address" -msgstr "" +#~ msgid "Input hosting provider address" +#~ msgstr "Entrez l’adresse de l’hébergeur" -#: src/view/com/auth/create/Step2.tsx:73 +#: src/view/com/auth/create/Step1.tsx:102 msgid "Input invite code to proceed" -msgstr "" +msgstr "Entrez le code d’invitation pour continuer" #: src/view/com/modals/AddAppPasswords.tsx:182 msgid "Input name for app password" -msgstr "" +msgstr "Entrez le nom du mot de passe de l’appli" #: src/view/com/auth/login/SetNewPasswordForm.tsx:133 msgid "Input new password" -msgstr "" +msgstr "Entrez le nouveau mot de passe" #: src/view/com/modals/DeleteAccount.tsx:199 msgid "Input password for account deletion" +msgstr "Entrez le mot de passe pour la suppression du compte" + +#: src/view/com/auth/create/Step2.tsx:92 +msgid "Input phone number for SMS verification" msgstr "" #: src/view/com/auth/login/LoginForm.tsx:227 msgid "Input the password tied to {identifier}" -msgstr "" +msgstr "Entrez le mot de passe associé à {identifier}" #: src/view/com/auth/login/LoginForm.tsx:194 msgid "Input the username or email address you used at signup" +msgstr "Entrez le pseudo ou l’adresse e-mail que vous avez utilisé lors de l’inscription" + +#: src/view/com/auth/create/Step2.tsx:166 +msgid "Input the verification code we have texted to you" msgstr "" #: src/view/com/modals/Waitlist.tsx:90 msgid "Input your email to get on the Bluesky waitlist" -msgstr "" +msgstr "Entrez votre e-mail pour vous inscrire sur la liste d’attente de Bluesky" #: src/view/com/auth/login/LoginForm.tsx:226 msgid "Input your password" -msgstr "" +msgstr "Entrez votre mot de passe" #: src/view/com/auth/create/Step3.tsx:39 msgid "Input your user handle" -msgstr "" +msgstr "Entrez votre pseudo" #: src/view/com/post-thread/PostThreadItem.tsx:229 msgid "Invalid or unsupported post record" -msgstr "" +msgstr "Enregistrement de post invalide ou non pris en charge" #: src/view/com/auth/login/LoginForm.tsx:115 msgid "Invalid username or password" -msgstr "Nom d’utilisateur ou mot de passe incorrect" +msgstr "Pseudo ou mot de passe incorrect" #: src/view/screens/Settings.tsx:411 msgid "Invite" @@ -1665,26 +1678,26 @@ msgstr "Inviter" msgid "Invite a Friend" msgstr "Inviter un ami" -#: src/view/com/auth/create/Step2.tsx:63 -#: src/view/com/auth/create/Step2.tsx:72 +#: src/view/com/auth/create/Step1.tsx:92 +#: src/view/com/auth/create/Step1.tsx:101 msgid "Invite code" msgstr "Code d’invitation" -#: src/view/com/auth/create/state.ts:136 +#: src/view/com/auth/create/state.ts:193 msgid "Invite code not accepted. Check that you input it correctly and try again." msgstr "Code d’invitation refusé. Vérifiez que vous l’avez saisi correctement et réessayez." #: src/view/com/modals/InviteCodes.tsx:170 msgid "Invite codes: {0} available" -msgstr "" +msgstr "Code d’invitation : {0} disponible" -#: src/view/shell/Drawer.tsx:642 +#: src/view/shell/Drawer.tsx:645 msgid "Invite codes: {invitesAvailable} available" -msgstr "Codes d’invitation : {invitesAvailable} disponible" +msgstr "Invitations : {invitesAvailable} codes dispo" #: src/view/com/modals/InviteCodes.tsx:169 msgid "Invite codes: 1 available" -msgstr "" +msgstr "Invitations : 1 code dispo" #: src/view/com/auth/HomeLoggedOutCTA.tsx:99 msgid "Jobs" @@ -1694,8 +1707,8 @@ msgstr "Emplois" msgid "Join the waitlist" msgstr "S’inscrire sur la liste d’attente" -#: src/view/com/auth/create/Step2.tsx:86 -#: src/view/com/auth/create/Step2.tsx:90 +#: src/view/com/auth/create/Step1.tsx:118 +#: src/view/com/auth/create/Step1.tsx:122 msgid "Join the waitlist." msgstr "S’inscrire sur la liste d’attente." @@ -1709,7 +1722,7 @@ msgstr "Sélection de la langue" #: src/view/screens/Settings.tsx:560 msgid "Language settings" -msgstr "" +msgstr "Préférences de langue" #: src/Navigation.tsx:141 #: src/view/screens/LanguageSettings.tsx:89 @@ -1720,9 +1733,9 @@ msgstr "Paramètres linguistiques" msgid "Languages" msgstr "Langues" -#: src/view/com/auth/create/StepHeader.tsx:13 +#: src/view/com/auth/create/StepHeader.tsx:20 msgid "Last step!" -msgstr "" +msgstr "Dernière étape !" #: src/view/com/util/moderation/ContentHider.tsx:103 msgid "Learn more" @@ -1756,12 +1769,12 @@ msgstr "Quitter Bluesky" #: src/view/screens/Settings.tsx:280 msgid "Legacy storage cleared, you need to restart the app now." -msgstr "" +msgstr "Stockage ancien effacé, vous devez redémarrer l’application maintenant." #: src/view/com/auth/login/Login.tsx:128 #: src/view/com/auth/login/Login.tsx:144 msgid "Let's get your password reset!" -msgstr "Réinitialisez votre mot de passe !" +msgstr "Réinitialisez votre mot de passe !" #: src/view/com/util/UserAvatar.tsx:245 #: src/view/com/util/UserBanner.tsx:60 @@ -1770,11 +1783,11 @@ msgstr "Bibliothèque" #: src/view/screens/Settings.tsx:473 msgid "Light" -msgstr "" +msgstr "Clair" #: src/view/com/util/post-ctrls/PostCtrls.tsx:189 msgid "Like" -msgstr "" +msgstr "Liker" #: src/view/screens/ProfileFeed.tsx:600 msgid "Like this feed" @@ -1788,19 +1801,19 @@ msgstr "Liké par" #: src/view/com/feeds/FeedSourceCard.tsx:277 msgid "Liked by {0} {1}" -msgstr "" +msgstr "Liké par {0} {1}" #: src/view/screens/ProfileFeed.tsx:615 msgid "Liked by {likeCount} {0}" -msgstr "" +msgstr "Liké par {likeCount} {0}" #: src/view/com/notifications/FeedItem.tsx:171 msgid "liked your custom feed{0}" -msgstr "" +msgstr "liké votre fil d’actu personnalisé{0}" #: src/view/com/notifications/FeedItem.tsx:155 msgid "liked your post" -msgstr "" +msgstr "liké votre post" #: src/view/screens/Profile.tsx:164 msgid "Likes" @@ -1808,11 +1821,11 @@ msgstr "Likes" #: src/view/com/post-thread/PostThreadItem.tsx:184 msgid "Likes on this post" -msgstr "" +msgstr "Likes sur ce post" #: src/Navigation.tsx:168 msgid "List" -msgstr "" +msgstr "Liste" #: src/view/com/modals/CreateOrEditList.tsx:205 msgid "List Avatar" @@ -1820,19 +1833,19 @@ msgstr "Liste des avatars" #: src/view/screens/ProfileList.tsx:323 msgid "List blocked" -msgstr "" +msgstr "Liste bloquée" #: src/view/com/feeds/FeedSourceCard.tsx:231 msgid "List by {0}" -msgstr "" +msgstr "Liste par {0}" #: src/view/screens/ProfileList.tsx:367 msgid "List deleted" -msgstr "" +msgstr "Liste supprimée" #: src/view/screens/ProfileList.tsx:282 msgid "List muted" -msgstr "" +msgstr "Liste masquée" #: src/view/com/modals/CreateOrEditList.tsx:218 msgid "List Name" @@ -1840,17 +1853,17 @@ msgstr "Nom de liste" #: src/view/screens/ProfileList.tsx:342 msgid "List unblocked" -msgstr "" +msgstr "Liste débloquée" #: src/view/screens/ProfileList.tsx:301 msgid "List unmuted" -msgstr "" +msgstr "Liste démasquée" #: src/Navigation.tsx:111 #: src/view/screens/Profile.tsx:166 #: src/view/shell/desktop/LeftNav.tsx:379 -#: src/view/shell/Drawer.tsx:492 -#: src/view/shell/Drawer.tsx:493 +#: src/view/shell/Drawer.tsx:495 +#: src/view/shell/Drawer.tsx:496 msgid "Lists" msgstr "Listes" @@ -1880,7 +1893,7 @@ msgstr "Serveur de dév local" #: src/Navigation.tsx:209 msgid "Log" -msgstr "" +msgstr "Journaux" #: src/view/screens/Moderation.tsx:136 msgid "Logged-out visibility" @@ -1892,7 +1905,7 @@ msgstr "Se connecter à un compte qui n’est pas listé" #: src/view/com/modals/LinkWarning.tsx:65 msgid "Make sure this is where you intend to go!" -msgstr "Assurez-vous que c’est bien là que vous avez l’intention d’aller !" +msgstr "Assurez-vous que c’est bien là que vous avez l’intention d’aller !" #: src/view/screens/Profile.tsx:163 msgid "Media" @@ -1900,56 +1913,52 @@ msgstr "Média" #: src/view/com/threadgate/WhoCanReply.tsx:139 msgid "mentioned users" -msgstr "utilisateurs mentionnés" +msgstr "comptes mentionnés" #: src/view/com/modals/Threadgate.tsx:93 msgid "Mentioned users" -msgstr "Utilisateurs mentionnés" +msgstr "Comptes mentionnés" #: src/view/com/util/ViewHeader.tsx:81 #: src/view/screens/Search/Search.tsx:537 msgid "Menu" msgstr "Menu" -#: src/view/com/posts/FeedErrorMessage.tsx:194 -#~ msgid "Message from server" -#~ msgstr "Message du serveur" - #: src/view/com/posts/FeedErrorMessage.tsx:197 msgid "Message from server: {0}" -msgstr "" +msgstr "Message du serveur : {0}" #: src/Navigation.tsx:116 #: src/view/screens/Moderation.tsx:64 #: src/view/screens/Settings.tsx:591 #: src/view/shell/desktop/LeftNav.tsx:397 -#: src/view/shell/Drawer.tsx:511 -#: src/view/shell/Drawer.tsx:512 +#: src/view/shell/Drawer.tsx:514 +#: src/view/shell/Drawer.tsx:515 msgid "Moderation" msgstr "Modération" #: src/view/com/lists/ListCard.tsx:92 #: src/view/com/modals/UserAddRemoveLists.tsx:190 msgid "Moderation list by {0}" -msgstr "" +msgstr "Liste de modération par {0}" #: src/view/screens/ProfileList.tsx:753 msgid "Moderation list by <0/>" -msgstr "" +msgstr "Liste de modération par <0/>" #: src/view/com/lists/ListCard.tsx:90 #: src/view/com/modals/UserAddRemoveLists.tsx:188 #: src/view/screens/ProfileList.tsx:751 msgid "Moderation list by you" -msgstr "" +msgstr "Liste de modération par vous" #: src/view/com/modals/CreateOrEditList.tsx:139 msgid "Moderation list created" -msgstr "" +msgstr "Liste de modération créée" #: src/view/com/modals/CreateOrEditList.tsx:126 msgid "Moderation list updated" -msgstr "" +msgstr "Liste de modération mise à jour" #: src/view/screens/Moderation.tsx:95 msgid "Moderation lists" @@ -1962,11 +1971,11 @@ msgstr "Listes de modération" #: src/view/screens/Settings.tsx:585 msgid "Moderation settings" -msgstr "" +msgstr "Paramètres de modération" #: src/view/com/modals/ModerationDetails.tsx:35 msgid "Moderator has chosen to set a general warning on the content." -msgstr "" +msgstr "La modération a choisi d’ajouter un avertissement général sur le contenu." #: src/view/shell/desktop/Feeds.tsx:53 msgid "More feeds" @@ -1980,7 +1989,7 @@ msgstr "Plus d’options" #: src/view/com/util/forms/PostDropdownBtn.tsx:268 msgid "More post options" -msgstr "" +msgstr "Plus d’options de post" #: src/view/screens/PreferencesThreads.tsx:82 msgid "Most-liked replies first" @@ -2000,11 +2009,11 @@ msgstr "Masquer la liste" #: src/view/screens/ProfileList.tsx:274 msgid "Mute these accounts?" -msgstr "Masquer ces comptes ?" +msgstr "Masquer ces comptes ?" #: src/view/screens/ProfileList.tsx:278 msgid "Mute this List" -msgstr "" +msgstr "Masquer cette liste" #: src/view/com/util/forms/PostDropdownBtn.tsx:169 msgid "Mute thread" @@ -2054,22 +2063,22 @@ msgstr "Nom" #: src/view/com/modals/CreateOrEditList.tsx:108 msgid "Name is required" -msgstr "" +msgstr "Le nom est requis" #: src/view/com/auth/login/ForgotPasswordForm.tsx:186 #: src/view/com/auth/login/LoginForm.tsx:286 #: src/view/com/auth/login/SetNewPasswordForm.tsx:166 msgid "Navigates to the next screen" -msgstr "" +msgstr "Navigue vers le prochain écran" -#: src/view/shell/Drawer.tsx:71 +#: src/view/shell/Drawer.tsx:73 msgid "Navigates to your profile" -msgstr "" +msgstr "Navigue vers votre profil" #: src/view/com/modals/EmbedConsent.tsx:107 #: src/view/com/modals/EmbedConsent.tsx:123 msgid "Never load embeds from {0}" -msgstr "" +msgstr "Ne jamais charger les contenus intégrés de {0}" #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:72 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:72 @@ -2079,7 +2088,7 @@ msgstr "Ne perdez jamais l’accès à vos followers et à vos données." #: src/view/screens/Lists.tsx:76 msgctxt "action" msgid "New" -msgstr "" +msgstr "Nouveau" #: src/view/screens/ModerationModlists.tsx:78 msgid "New" @@ -2087,16 +2096,16 @@ msgstr "Nouveau" #: src/view/com/modals/CreateOrEditList.tsx:195 msgid "New Moderation List" -msgstr "" +msgstr "Nouvelle liste de modération" #: src/view/com/auth/login/SetNewPasswordForm.tsx:122 msgid "New password" -msgstr "" +msgstr "Nouveau mot de passe" #: src/view/com/feeds/FeedPage.tsx:201 msgctxt "action" msgid "New post" -msgstr "" +msgstr "Nouveau post" #: src/view/screens/Feeds.tsx:511 #: src/view/screens/Profile.tsx:354 @@ -2110,21 +2119,22 @@ msgstr "Nouveau post" #: src/view/shell/desktop/LeftNav.tsx:258 msgctxt "action" msgid "New Post" -msgstr "" - -#: src/view/shell/desktop/LeftNav.tsx:258 -#~ msgid "New Post" -#~ msgstr "Nouveau post" +msgstr "Nouveau post" #: src/view/com/modals/CreateOrEditList.tsx:190 msgid "New User List" -msgstr "" +msgstr "Nouvelle liste de comptes" #: src/view/screens/PreferencesThreads.tsx:79 msgid "Newest replies first" -msgstr "" +msgstr "Réponses les plus récentes en premier" -#: src/view/com/auth/create/CreateAccount.tsx:154 +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103 +msgctxt "action" +msgid "Next" +msgstr "Suivant" + +#: src/view/com/auth/create/CreateAccount.tsx:155 #: src/view/com/auth/login/ForgotPasswordForm.tsx:178 #: src/view/com/auth/login/ForgotPasswordForm.tsx:188 #: src/view/com/auth/login/LoginForm.tsx:288 @@ -2134,11 +2144,6 @@ msgstr "" msgid "Next" msgstr "Suivant" -#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103 -msgctxt "action" -msgid "Next" -msgstr "" - #: src/view/com/lightbox/Lightbox.web.tsx:142 msgid "Next image" msgstr "Image suivante" @@ -2159,11 +2164,11 @@ msgstr "Aucune description" #: src/view/com/profile/ProfileHeader.tsx:217 msgid "No longer following {0}" -msgstr "" +msgstr "Ne suit plus {0}" #: src/view/com/notifications/Feed.tsx:107 msgid "No notifications yet!" -msgstr "" +msgstr "Pas encore de notifications !" #: src/view/com/composer/text-input/mobile/Autocomplete.tsx:97 #: src/view/com/composer/text-input/web/Autocomplete.tsx:191 @@ -2172,7 +2177,7 @@ msgstr "Aucun résultat" #: src/view/screens/Feeds.tsx:456 msgid "No results found for \"{query}\"" -msgstr "Aucun résultat trouvé pour « {query} »" +msgstr "Aucun résultat trouvé pour « {query} »" #: src/view/com/modals/ListAddRemoveUsers.tsx:127 #: src/view/screens/Search/Search.tsx:270 @@ -2184,7 +2189,7 @@ msgstr "Aucun résultat trouvé pour {query}" #: src/view/com/modals/EmbedConsent.tsx:129 msgid "No thanks" -msgstr "" +msgstr "Non merci" #: src/view/com/modals/Threadgate.tsx:82 msgid "Nobody" @@ -2196,34 +2201,34 @@ msgstr "Sans objet." #: src/Navigation.tsx:106 msgid "Not Found" -msgstr "" +msgstr "Introuvable" #: src/view/com/modals/VerifyEmail.tsx:246 #: src/view/com/modals/VerifyEmail.tsx:252 msgid "Not right now" -msgstr "" +msgstr "Pas maintenant" #: src/view/screens/Moderation.tsx:232 msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." -msgstr "Remarque : Bluesky est un réseau ouvert et public. Ce paramètre limite uniquement la visibilité de votre contenu sur l’application et le site Web de Bluesky, et d’autres applications peuvent ne pas respecter ce paramètre. Votre contenu peut toujours être montré aux utilisateurs déconnectés par d’autres applications et sites Web." +msgstr "Remarque : Bluesky est un réseau ouvert et public. Ce paramètre limite uniquement la visibilité de votre contenu sur l’application et le site Web de Bluesky, et d’autres applications peuvent ne pas respecter ce paramètre. Votre contenu peut toujours être montré aux personnes non connectées par d’autres applications et sites Web." #: src/Navigation.tsx:447 #: src/view/screens/Notifications.tsx:113 #: src/view/screens/Notifications.tsx:137 #: src/view/shell/bottom-bar/BottomBar.tsx:205 #: src/view/shell/desktop/LeftNav.tsx:361 -#: src/view/shell/Drawer.tsx:435 -#: src/view/shell/Drawer.tsx:436 +#: src/view/shell/Drawer.tsx:438 +#: src/view/shell/Drawer.tsx:439 msgid "Notifications" msgstr "Notifications" #: src/view/com/modals/SelfLabel.tsx:103 msgid "Nudity" -msgstr "" +msgstr "Nudité" #: src/view/com/util/ErrorBoundary.tsx:34 msgid "Oh no!" -msgstr "Oh non !" +msgstr "Oh non !" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 msgid "Okay" @@ -2235,7 +2240,7 @@ msgstr "Plus anciennes réponses en premier" #: src/view/screens/Settings.tsx:236 msgid "Onboarding reset" -msgstr "" +msgstr "Réinitialiser le didacticiel" #: src/view/com/composer/Composer.tsx:375 msgid "One or more images is missing alt text." @@ -2249,7 +2254,7 @@ msgstr "Seul {0} peut répondre." #: src/view/com/modals/ProfilePreview.tsx:61 #: src/view/screens/AppPasswords.tsx:65 msgid "Oops!" -msgstr "" +msgstr "Oups !" #: src/view/com/composer/Composer.tsx:470 #: src/view/com/composer/Composer.tsx:471 @@ -2260,33 +2265,33 @@ msgstr "Ouvrir le sélecteur d’emoji" msgid "Open links with in-app browser" msgstr "" -#: src/view/com/pager/FeedsTabBarMobile.tsx:76 +#: src/view/com/pager/FeedsTabBarMobile.tsx:81 msgid "Open navigation" msgstr "Navigation ouverte" #: src/view/screens/Settings.tsx:737 msgid "Open storybook page" -msgstr "" +msgstr "Ouvrir la page Storybook" #: src/view/com/util/forms/DropdownButton.tsx:147 msgid "Opens {numItems} options" -msgstr "" +msgstr "Ouvre {numItems} options" #: src/view/screens/Log.tsx:54 msgid "Opens additional details for a debug entry" -msgstr "" +msgstr "Ouvre des détails supplémentaires pour une entrée de débug" #: src/view/com/notifications/FeedItem.tsx:352 msgid "Opens an expanded list of users in this notification" -msgstr "" +msgstr "Ouvre une liste étendue des comptes dans cette notification" #: src/view/com/composer/photos/OpenCameraBtn.tsx:61 msgid "Opens camera on device" -msgstr "" +msgstr "Ouvre l’appareil photo de l’appareil" #: src/view/com/composer/Prompt.tsx:25 msgid "Opens composer" -msgstr "" +msgstr "Ouvre le rédacteur" #: src/view/screens/Settings.tsx:561 msgid "Opens configurable language settings" @@ -2294,37 +2299,37 @@ msgstr "Ouvre les paramètres linguistiques configurables" #: src/view/com/composer/photos/SelectPhotoBtn.tsx:44 msgid "Opens device photo gallery" -msgstr "" +msgstr "Ouvre la galerie de photos de l’appareil" #: src/view/com/profile/ProfileHeader.tsx:459 msgid "Opens editor for profile display name, avatar, background image, and description" -msgstr "" +msgstr "Ouvre l’éditeur pour le nom d’affichage du profil, l’avatar, l’image d’arrière-plan et la description" #: src/view/screens/Settings.tsx:615 msgid "Opens external embeds settings" -msgstr "" +msgstr "Ouvre les paramètres d’intégration externe" #: src/view/com/profile/ProfileHeader.tsx:614 msgid "Opens followers list" -msgstr "" +msgstr "Ouvre la liste des comptes abonnés" #: src/view/com/profile/ProfileHeader.tsx:633 msgid "Opens following list" -msgstr "" +msgstr "Ouvre la liste des abonnements" #: src/view/screens/Settings.tsx:412 msgid "Opens invite code list" -msgstr "" +msgstr "Ouvre la liste des codes d’invitation" #: src/view/com/modals/InviteCodes.tsx:172 #: src/view/shell/desktop/RightNav.tsx:156 -#: src/view/shell/Drawer.tsx:643 +#: src/view/shell/Drawer.tsx:646 msgid "Opens list of invite codes" msgstr "Ouvre la liste des codes d’invitation" #: src/view/screens/Settings.tsx:696 msgid "Opens modal for account deletion confirmation. Requires email code." -msgstr "" +msgstr "Ouvre la fenêtre modale pour confirmer la suppression du compte. Requiert un code e-mail." #: src/view/com/modals/ChangeHandle.tsx:281 msgid "Opens modal for using custom domain" @@ -2336,11 +2341,11 @@ msgstr "Ouvre les paramètres de modération" #: src/view/com/auth/login/LoginForm.tsx:236 msgid "Opens password reset form" -msgstr "" +msgstr "Ouvre le formulaire de réinitialisation du mot de passe" #: src/view/screens/Feeds.tsx:335 msgid "Opens screen to edit Saved Feeds" -msgstr "" +msgstr "Ouvre l’écran pour modifier les fils d’actu enregistrés" #: src/view/screens/Settings.tsx:542 msgid "Opens screen with all saved feeds" @@ -2368,11 +2373,11 @@ msgstr "Ouvre les préférences relatives aux fils de discussion" #: src/view/com/util/forms/DropdownButton.tsx:254 msgid "Option {0} of {numItems}" -msgstr "" +msgstr "Option {0} sur {numItems}" #: src/view/com/modals/Threadgate.tsx:89 msgid "Or combine these options:" -msgstr "Ou une combinaison de ces options :" +msgstr "Ou une combinaison de ces options :" #: src/view/com/auth/login/ChooseAccountForm.tsx:138 msgid "Other account" @@ -2391,8 +2396,8 @@ msgstr "Autre…" msgid "Page not found" msgstr "Page introuvable" -#: src/view/com/auth/create/Step2.tsx:122 -#: src/view/com/auth/create/Step2.tsx:132 +#: src/view/com/auth/create/Step1.tsx:158 +#: src/view/com/auth/create/Step1.tsx:168 #: src/view/com/auth/login/LoginForm.tsx:223 #: src/view/com/auth/login/SetNewPasswordForm.tsx:132 #: src/view/com/modals/DeleteAccount.tsx:198 @@ -2405,22 +2410,26 @@ msgstr "Mise à jour du mot de passe" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:28 msgid "Password updated!" -msgstr "Mot de passe mis à jour !" +msgstr "Mot de passe mis à jour !" #: src/Navigation.tsx:162 msgid "People followed by @{0}" -msgstr "" +msgstr "Personnes suivies par @{0}" #: src/Navigation.tsx:155 msgid "People following @{0}" -msgstr "" +msgstr "Personnes qui suivent @{0}" #: src/view/com/lightbox/Lightbox.tsx:66 msgid "Permission to access camera roll is required." -msgstr "" +msgstr "Permission d’accès à la pellicule requise." #: src/view/com/lightbox/Lightbox.tsx:72 msgid "Permission to access camera roll was denied. Please enable it in your system settings." +msgstr "Permission d’accès à la pellicule refusée. Veuillez l’activer dans les paramètres de votre système." + +#: src/view/com/auth/create/Step2.tsx:79 +msgid "Phone number" msgstr "" #: src/view/com/modals/SelfLabel.tsx:121 @@ -2430,7 +2439,7 @@ msgstr "Images destinées aux adultes." #: src/view/screens/ProfileFeed.tsx:362 #: src/view/screens/ProfileList.tsx:559 msgid "Pin to home" -msgstr "" +msgstr "Ajouter à l’accueil" #: src/view/screens/SavedFeeds.tsx:88 msgid "Pinned Feeds" @@ -2438,22 +2447,22 @@ msgstr "Fils épinglés" #: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:111 msgid "Play {0}" -msgstr "" +msgstr "Lire {0}" #: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:54 #: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:55 msgid "Play Video" -msgstr "" +msgstr "Lire la vidéo" #: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:110 msgid "Plays the GIF" -msgstr "" +msgstr "Lit le GIF" -#: src/view/com/auth/create/state.ts:116 +#: src/view/com/auth/create/state.ts:171 msgid "Please choose your handle." msgstr "Veuillez choisir votre pseudo." -#: src/view/com/auth/create/state.ts:109 +#: src/view/com/auth/create/state.ts:154 msgid "Please choose your password." msgstr "Veuillez choisir votre mot de passe." @@ -2463,31 +2472,40 @@ msgstr "Veuillez confirmer votre e-mail avant de le modifier. Ceci est temporair #: src/view/com/modals/AddAppPasswords.tsx:89 msgid "Please enter a name for your app password. All spaces is not allowed." +msgstr "Veuillez entrer un nom pour votre mot de passe d’application. Les espaces ne sont pas autorisés." + +#: src/view/com/auth/create/Step2.tsx:102 +msgid "Please enter a phone number that can receive SMS text messages." msgstr "" #: src/view/com/modals/AddAppPasswords.tsx:144 msgid "Please enter a unique name for this App Password or use our randomly generated one." msgstr "Veuillez saisir un nom unique pour le mot de passe de l’application ou utiliser celui que nous avons généré de manière aléatoire." -#: src/view/com/auth/create/state.ts:95 +#: src/view/com/auth/create/state.ts:164 +msgid "Please enter the code you received by SMS." +msgstr "" + +#: src/view/com/auth/create/Step2.tsx:177 +msgid "Please enter the verification code sent to" +msgstr "" + +#: src/view/com/auth/create/state.ts:140 msgid "Please enter your email." msgstr "Veuillez entrer votre e-mail." #: src/view/com/modals/DeleteAccount.tsx:187 msgid "Please enter your password as well:" -msgstr "Veuillez également entrer votre mot de passe :" +msgstr "Veuillez également entrer votre mot de passe :" #: src/view/com/modals/AppealLabel.tsx:72 #: src/view/com/modals/AppealLabel.tsx:75 msgid "Please tell us why you think this content warning was incorrectly applied!" -msgstr "Dites-nous donc pourquoi vous pensez que cet avertissement de contenu a été appliqué à tort !" - -#~ msgid "Please tell us why you think this decision was incorrect." -#~ msgstr "Dites-nous pourquoi vous pensez que cette décision était incorrecte." +msgstr "Dites-nous donc pourquoi vous pensez que cet avertissement de contenu a été appliqué à tort !" #: src/view/com/modals/VerifyEmail.tsx:101 msgid "Please Verify Your Email" -msgstr "" +msgstr "Veuillez vérifier votre e-mail" #: src/view/com/composer/Composer.tsx:215 msgid "Please wait for your link card to finish loading" @@ -2495,43 +2513,37 @@ msgstr "Veuillez patienter le temps que votre carte de lien soit chargée" #: src/view/com/modals/SelfLabel.tsx:111 msgid "Porn" -msgstr "" +msgstr "Porno" #: src/view/com/composer/Composer.tsx:350 #: src/view/com/composer/Composer.tsx:358 msgctxt "action" msgid "Post" -msgstr "" +msgstr "Poster" #: src/view/com/post-thread/PostThread.tsx:227 #: src/view/screens/PostThread.tsx:82 msgctxt "description" msgid "Post" -msgstr "" - -#: src/view/com/composer/Composer.tsx:346 -#: src/view/com/post-thread/PostThread.tsx:225 -#: src/view/screens/PostThread.tsx:80 -#~ msgid "Post" -#~ msgstr "Post" +msgstr "Post" #: src/view/com/post-thread/PostThreadItem.tsx:176 msgid "Post by {0}" -msgstr "" +msgstr "Post de {0}" #: src/Navigation.tsx:174 #: src/Navigation.tsx:181 #: src/Navigation.tsx:188 msgid "Post by @{0}" -msgstr "" +msgstr "Post de @{0}" #: src/view/com/util/forms/PostDropdownBtn.tsx:82 msgid "Post deleted" -msgstr "" +msgstr "Post supprimé" #: src/view/com/post-thread/PostThread.tsx:382 msgid "Post hidden" -msgstr "Posts cachés" +msgstr "Post caché" #: src/view/com/composer/select-language/SelectLangBtn.tsx:87 msgid "Post language" @@ -2551,7 +2563,7 @@ msgstr "Posts" #: src/view/com/posts/FeedErrorMessage.tsx:64 msgid "Posts hidden" -msgstr "" +msgstr "Posts cachés" #: src/view/com/modals/LinkWarning.tsx:46 msgid "Potentially Misleading Link" @@ -2577,7 +2589,7 @@ msgstr "Vie privée" #: src/Navigation.tsx:219 #: src/view/screens/PrivacyPolicy.tsx:29 #: src/view/screens/Settings.tsx:824 -#: src/view/shell/Drawer.tsx:262 +#: src/view/shell/Drawer.tsx:265 msgid "Privacy Policy" msgstr "Charte de confidentialité" @@ -2587,15 +2599,15 @@ msgstr "Traitement…" #: src/view/shell/bottom-bar/BottomBar.tsx:247 #: src/view/shell/desktop/LeftNav.tsx:415 -#: src/view/shell/Drawer.tsx:70 -#: src/view/shell/Drawer.tsx:546 -#: src/view/shell/Drawer.tsx:547 +#: src/view/shell/Drawer.tsx:72 +#: src/view/shell/Drawer.tsx:549 +#: src/view/shell/Drawer.tsx:550 msgid "Profile" msgstr "Profil" #: src/view/com/modals/EditProfile.tsx:128 msgid "Profile updated" -msgstr "" +msgstr "Profil mis à jour" #: src/view/screens/Settings.tsx:882 msgid "Protect your account by verifying your email." @@ -2603,7 +2615,7 @@ msgstr "Protégez votre compte en vérifiant votre e-mail." #: src/view/screens/ModerationModlists.tsx:61 msgid "Public, shareable lists of users to mute or block in bulk." -msgstr "Listes publiques et partageables d’utilisateurs à masquer ou à bloquer." +msgstr "Listes publiques et partageables de comptes à masquer ou à bloquer." #: src/view/screens/Lists.tsx:61 msgid "Public, shareable lists which can drive feeds." @@ -2611,16 +2623,16 @@ msgstr "Les listes publiques et partageables qui peuvent alimenter les fils d’ #: src/view/com/composer/Composer.tsx:335 msgid "Publish post" -msgstr "" +msgstr "Publier le post" #: src/view/com/composer/Composer.tsx:335 msgid "Publish reply" -msgstr "" +msgstr "Publier la réponse" #: src/view/com/modals/Repost.tsx:65 msgctxt "action" msgid "Quote post" -msgstr "" +msgstr "Citer le post" #: src/view/com/util/post-ctrls/RepostButton.web.tsx:58 msgid "Quote post" @@ -2629,15 +2641,11 @@ msgstr "Citer le post" #: src/view/com/modals/Repost.tsx:70 msgctxt "action" msgid "Quote Post" -msgstr "" - -#: src/view/com/modals/Repost.tsx:56 -#~ msgid "Quote Post" -#~ msgstr "Citer un post" +msgstr "Citer le post" #: src/view/screens/PreferencesThreads.tsx:86 msgid "Random (aka \"Poster's Roulette\")" -msgstr "" +msgstr "Aléatoire" #: src/view/com/modals/EditImage.tsx:236 msgid "Ratios" @@ -2649,7 +2657,7 @@ msgstr "Fils d’actu recommandés" #: src/view/com/auth/onboarding/RecommendedFollows.tsx:180 msgid "Recommended Users" -msgstr "Utilisateurs recommandés" +msgstr "Comptes recommandés" #: src/view/com/modals/ListAddRemoveUsers.tsx:264 #: src/view/com/modals/SelfLabel.tsx:83 @@ -2661,7 +2669,7 @@ msgstr "Supprimer" #: src/view/com/feeds/FeedSourceCard.tsx:106 msgid "Remove {0} from my feeds?" -msgstr "Supprimer {0} de mes fils d’actu ?" +msgstr "Supprimer {0} de mes fils d’actu ?" #: src/view/com/util/AccountDropdownBtn.tsx:22 msgid "Remove account" @@ -2690,15 +2698,15 @@ msgstr "Supprimer l’aperçu d’image" #: src/view/com/modals/Repost.tsx:47 msgid "Remove repost" -msgstr "" +msgstr "Supprimer le repost" #: src/view/com/feeds/FeedSourceCard.tsx:173 msgid "Remove this feed from my feeds?" -msgstr "Supprimer ce fil d’actu ?" +msgstr "Supprimer ce fil d’actu ?" #: src/view/com/posts/FeedErrorMessage.tsx:132 msgid "Remove this feed from your saved feeds?" -msgstr "Supprimer ce fil d’actu de vos fils d’actu enregistrés ?" +msgstr "Supprimer ce fil d’actu de vos fils d’actu enregistrés ?" #: src/view/com/modals/ListAddRemoveUsers.tsx:199 #: src/view/com/modals/UserAddRemoveLists.tsx:136 @@ -2708,11 +2716,11 @@ msgstr "Supprimé de la liste" #: src/view/com/feeds/FeedSourceCard.tsx:111 #: src/view/com/feeds/FeedSourceCard.tsx:178 msgid "Removed from my feeds" -msgstr "" +msgstr "Supprimé de mes fils d’actu" #: src/view/com/composer/ExternalEmbed.tsx:71 msgid "Removes default thumbnail from {0}" -msgstr "" +msgstr "Supprime la miniature par défaut de {0}" #: src/view/screens/Profile.tsx:162 msgid "Replies" @@ -2725,7 +2733,7 @@ msgstr "Les réponses à ce fil de discussion sont désactivées" #: src/view/com/composer/Composer.tsx:348 msgctxt "action" msgid "Reply" -msgstr "" +msgstr "Répondre" #: src/view/screens/PreferencesHomeFeed.tsx:144 msgid "Reply Filters" @@ -2735,7 +2743,7 @@ msgstr "Filtres de réponse" #: src/view/com/posts/FeedItem.tsx:286 msgctxt "description" msgid "Reply to <0/>" -msgstr "" +msgstr "Réponse à <0/>" #: src/view/com/modals/report/Modal.tsx:166 msgid "Report {collectionName}" @@ -2764,7 +2772,7 @@ msgstr "Signaler le post" #: src/view/com/util/post-ctrls/RepostButton.tsx:61 msgctxt "action" msgid "Repost" -msgstr "" +msgstr "Republier" #: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 msgid "Repost" @@ -2781,32 +2789,36 @@ msgstr "Republié par" #: src/view/com/posts/FeedItem.tsx:206 msgid "Reposted by {0})" -msgstr "" +msgstr "Republié par {0})" #: src/view/com/posts/FeedItem.tsx:223 msgid "Reposted by <0/>" -msgstr "" +msgstr "Republié par <0/>" #: src/view/com/notifications/FeedItem.tsx:162 msgid "reposted your post" -msgstr "" +msgstr "a republié votre post" #: src/view/com/post-thread/PostThreadItem.tsx:189 msgid "Reposts of this post" -msgstr "" +msgstr "Reposts de ce post" #: src/view/com/modals/ChangeEmail.tsx:181 #: src/view/com/modals/ChangeEmail.tsx:183 msgid "Request Change" msgstr "Demande de modification" +#: src/view/com/auth/create/Step2.tsx:115 +msgid "Request code" +msgstr "" + #: src/view/screens/Settings.tsx:450 msgid "Require alt text before posting" msgstr "Nécessiter un texte alt avant de publier" -#: src/view/com/auth/create/Step2.tsx:68 +#: src/view/com/auth/create/Step1.tsx:97 msgid "Required for this provider" -msgstr "Obligatoire pour ce fournisseur" +msgstr "Obligatoire pour cet hébergeur" #: src/view/com/auth/login/SetNewPasswordForm.tsx:98 #: src/view/com/auth/login/SetNewPasswordForm.tsx:108 @@ -2815,11 +2827,11 @@ msgstr "Réinitialiser le code" #: src/view/screens/Settings.tsx:757 msgid "Reset onboarding" -msgstr "" +msgstr "Réinitialiser le didacticiel" #: src/view/screens/Settings.tsx:760 msgid "Reset onboarding state" -msgstr "Réinitialisation de l’état d’accueil" +msgstr "Réinitialisation du didacticiel" #: src/view/com/auth/login/ForgotPasswordForm.tsx:100 msgid "Reset password" @@ -2827,7 +2839,7 @@ msgstr "Réinitialiser mot de passe" #: src/view/screens/Settings.tsx:747 msgid "Reset preferences" -msgstr "" +msgstr "Réinitialiser les préférences" #: src/view/screens/Settings.tsx:750 msgid "Reset preferences state" @@ -2843,15 +2855,16 @@ msgstr "Réinitialise l’état des préférences" #: src/view/com/auth/login/LoginForm.tsx:266 msgid "Retries login" -msgstr "" +msgstr "Réessaye la connection" #: src/view/com/util/error/ErrorMessage.tsx:57 #: src/view/com/util/error/ErrorScreen.tsx:67 msgid "Retries the last action, which errored out" -msgstr "" +msgstr "Réessaye la dernière action, qui a échoué" -#: src/view/com/auth/create/CreateAccount.tsx:163 -#: src/view/com/auth/create/CreateAccount.tsx:167 +#: src/view/com/auth/create/CreateAccount.tsx:164 +#: src/view/com/auth/create/CreateAccount.tsx:168 +#: src/view/com/auth/create/Step2.tsx:150 #: src/view/com/auth/login/LoginForm.tsx:265 #: src/view/com/auth/login/LoginForm.tsx:268 #: src/view/com/util/error/ErrorMessage.tsx:55 @@ -2859,19 +2872,23 @@ msgstr "" msgid "Retry" msgstr "Réessayer" +#: src/view/com/auth/create/Step2.tsx:143 +msgid "Retry." +msgstr "" + #: src/view/screens/ProfileList.tsx:877 msgid "Return to previous page" -msgstr "" +msgstr "Retourne à la page précédente" #: src/view/shell/desktop/RightNav.tsx:59 msgid "SANDBOX. Posts and accounts are not permanent." -msgstr "" +msgstr "SANDBOX. Les posts et les comptes ne sont pas permanents." #: src/view/com/lightbox/Lightbox.tsx:125 #: src/view/com/modals/CreateOrEditList.tsx:278 msgctxt "action" msgid "Save" -msgstr "" +msgstr "Enregistrer" #: src/view/com/modals/BirthDateSettings.tsx:94 #: src/view/com/modals/BirthDateSettings.tsx:97 @@ -2904,15 +2921,15 @@ msgstr "Fils d’actu enregistrés" #: src/view/com/modals/EditProfile.tsx:225 msgid "Saves any changes to your profile" -msgstr "" +msgstr "Enregistre toutes les modifications apportées à votre profil" #: src/view/com/modals/ChangeHandle.tsx:171 msgid "Saves handle change to {handle}" -msgstr "" +msgstr "Enregistre le changement de pseudo en {handle}" #: src/view/screens/ProfileList.tsx:833 msgid "Scroll to top" -msgstr "" +msgstr "Remonter en haut" #: src/Navigation.tsx:437 #: src/view/com/auth/LoggedOut.tsx:122 @@ -2926,8 +2943,8 @@ msgstr "" #: src/view/shell/desktop/LeftNav.tsx:324 #: src/view/shell/desktop/Search.tsx:161 #: src/view/shell/desktop/Search.tsx:170 -#: src/view/shell/Drawer.tsx:362 -#: src/view/shell/Drawer.tsx:363 +#: src/view/shell/Drawer.tsx:365 +#: src/view/shell/Drawer.tsx:366 msgid "Search" msgstr "Recherche" @@ -2935,7 +2952,7 @@ msgstr "Recherche" #: src/view/com/auth/LoggedOut.tsx:105 #: src/view/com/modals/ListAddRemoveUsers.tsx:70 msgid "Search for users" -msgstr "Rechercher des utilisateurs" +msgstr "Rechercher des comptes" #: src/view/com/modals/ChangeEmail.tsx:110 msgid "Security Step Required" @@ -2943,7 +2960,7 @@ msgstr "Étape de sécurité requise" #: src/view/screens/SavedFeeds.tsx:163 msgid "See this guide" -msgstr "" +msgstr "Voir ce guide" #: src/view/com/auth/HomeLoggedOutCTA.tsx:39 msgid "See what's next" @@ -2951,7 +2968,7 @@ msgstr "Voir la suite" #: src/view/com/util/Selector.tsx:106 msgid "Select {item}" -msgstr "" +msgstr "Sélectionner {item}" #: src/view/com/modals/ServerInput.tsx:75 msgid "Select Bluesky Social" @@ -2963,15 +2980,16 @@ msgstr "Sélectionner un compte existant" #: src/view/com/util/Selector.tsx:107 msgid "Select option {i} of {numItems}" -msgstr "" +msgstr "Sélectionne l’option {i} sur {numItems}" +#: src/view/com/auth/create/Step1.tsx:77 #: src/view/com/auth/login/LoginForm.tsx:147 msgid "Select service" msgstr "Sélectionner un service" #: src/view/screens/LanguageSettings.tsx:281 msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." -msgstr "Sélectionnez les langues que vous souhaitez voir figurer dans les fils d’actu auxquels vous êtes abonné. Si aucune langue n’est sélectionnée, toutes les langues seront affichées." +msgstr "Sélectionnez les langues que vous souhaitez voir figurer dans les fils d’actu que vous suivez. Si aucune langue n’est sélectionnée, toutes les langues seront affichées." #: src/view/screens/LanguageSettings.tsx:98 msgid "Select your app language for the default text to display in the app" @@ -2993,14 +3011,10 @@ msgstr "Envoyer e-mail" #: src/view/com/modals/DeleteAccount.tsx:140 msgctxt "action" msgid "Send Email" -msgstr "" - -#: src/view/com/modals/DeleteAccount.tsx:138 -#~ msgid "Send Email" -#~ msgstr "Envoyer e-mail" +msgstr "Envoyer l’e-mail" -#: src/view/shell/Drawer.tsx:295 -#: src/view/shell/Drawer.tsx:316 +#: src/view/shell/Drawer.tsx:298 +#: src/view/shell/Drawer.tsx:319 msgid "Send feedback" msgstr "Envoyer des commentaires" @@ -3010,83 +3024,84 @@ msgstr "Envoyer le rapport" #: src/view/com/modals/DeleteAccount.tsx:129 msgid "Sends email with confirmation code for account deletion" -msgstr "" +msgstr "Envoie un e-mail avec le code de confirmation pour la suppression du compte" #: src/view/com/modals/ContentFilteringSettings.tsx:306 msgid "Set {value} for {labelGroup} content moderation policy" -msgstr "" +msgstr "Choisis {value} pour la politique de modération de contenu {labelGroup}" #: src/view/com/modals/ContentFilteringSettings.tsx:155 #: src/view/com/modals/ContentFilteringSettings.tsx:174 msgctxt "action" msgid "Set Age" -msgstr "" +msgstr "Enregistrer l’âge" #: src/view/screens/Settings.tsx:482 msgid "Set color theme to dark" -msgstr "" +msgstr "Change le thème de couleur en sombre" #: src/view/screens/Settings.tsx:475 msgid "Set color theme to light" -msgstr "" +msgstr "Change le thème de couleur en clair" #: src/view/screens/Settings.tsx:469 msgid "Set color theme to system setting" -msgstr "" +msgstr "Change le thème de couleur en fonction du paramètre système" #: src/view/com/auth/login/SetNewPasswordForm.tsx:78 msgid "Set new password" msgstr "Définir un nouveau mot de passe" -#: src/view/com/auth/create/Step2.tsx:133 +#: src/view/com/auth/create/Step1.tsx:169 msgid "Set password" -msgstr "" +msgstr "Définit le mot de passe" #: src/view/screens/PreferencesHomeFeed.tsx:225 msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible." -msgstr "Choisissez « Non » pour cacher toutes les citations sur votre fils d’actu. Les reposts seront toujours visibles." +msgstr "Choisissez « Non » pour cacher toutes les citations sur votre fils d’actu. Les reposts seront toujours visibles." #: src/view/screens/PreferencesHomeFeed.tsx:122 msgid "Set this setting to \"No\" to hide all replies from your feed." -msgstr "Choisissez « Non » pour cacher toutes les réponses dans votre fils d’actu." +msgstr "Choisissez « Non » pour cacher toutes les réponses dans votre fils d’actu." #: src/view/screens/PreferencesHomeFeed.tsx:191 msgid "Set this setting to \"No\" to hide all reposts from your feed." -msgstr "Choisissez « Non » pour cacher toutes les reposts de votre fils d’actu." +msgstr "Choisissez « Non » pour cacher toutes les reposts de votre fils d’actu." #: src/view/screens/PreferencesThreads.tsx:122 msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature." -msgstr "Choisissez « Oui » pour afficher les réponses dans un fil de discussion. C’est une fonctionnalité expérimentale." +msgstr "Choisissez « Oui » pour afficher les réponses dans un fil de discussion. C’est une fonctionnalité expérimentale." #: src/view/screens/PreferencesHomeFeed.tsx:261 msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature." -msgstr "Choisissez « Oui » pour afficher des échantillons de vos fils d’actu enregistrés dans votre fils d’actu suivant. C’est une fonctionnalité expérimentale." +msgstr "Choisissez « Oui » pour afficher des échantillons de vos fils d’actu enregistrés dans votre fils d’actu suivant. C’est une fonctionnalité expérimentale." #: src/view/com/modals/ChangeHandle.tsx:266 msgid "Sets Bluesky username" -msgstr "" +msgstr "Définit le pseudo Bluesky" #: src/view/com/auth/login/ForgotPasswordForm.tsx:153 msgid "Sets email for password reset" -msgstr "" +msgstr "Définit l’e-mail pour la réinitialisation du mot de passe" #: src/view/com/auth/login/ForgotPasswordForm.tsx:118 msgid "Sets hosting provider for password reset" -msgstr "" +msgstr "Définit l’hébergeur pour la réinitialisation du mot de passe" #: src/view/com/auth/create/Step1.tsx:143 -msgid "Sets hosting provider to {label}" -msgstr "" +#~ msgid "Sets hosting provider to {label}" +#~ msgstr "Définit l’hébergeur à {label}" +#: src/view/com/auth/create/Step1.tsx:78 #: src/view/com/auth/login/LoginForm.tsx:148 msgid "Sets server for the Bluesky client" -msgstr "" +msgstr "Définit le serveur pour le client Bluesky" #: src/Navigation.tsx:136 #: src/view/screens/Settings.tsx:294 #: src/view/shell/desktop/LeftNav.tsx:433 -#: src/view/shell/Drawer.tsx:567 -#: src/view/shell/Drawer.tsx:568 +#: src/view/shell/Drawer.tsx:570 +#: src/view/shell/Drawer.tsx:571 msgid "Settings" msgstr "Paramètres" @@ -3097,7 +3112,7 @@ msgstr "Activité sexuelle ou nudité érotique." #: src/view/com/lightbox/Lightbox.tsx:134 msgctxt "action" msgid "Share" -msgstr "" +msgstr "Partager" #: src/view/com/profile/ProfileHeader.tsx:342 #: src/view/com/util/forms/PostDropdownBtn.tsx:151 @@ -3118,7 +3133,7 @@ msgstr "Afficher" #: src/view/screens/PreferencesHomeFeed.tsx:68 msgid "Show all replies" -msgstr "" +msgstr "Afficher toutes les réponses" #: src/view/com/util/moderation/ScreenHider.tsx:132 msgid "Show anyway" @@ -3126,17 +3141,17 @@ msgstr "Afficher quand même" #: src/view/com/modals/EmbedConsent.tsx:87 msgid "Show embeds from {0}" -msgstr "" +msgstr "Afficher les intégrations de {0}" #: src/view/com/profile/ProfileHeader.tsx:498 msgid "Show follows similar to {0}" -msgstr "" +msgstr "Afficher les suivis similaires à {0}" #: src/view/com/post-thread/PostThreadItem.tsx:569 #: src/view/com/post/Post.tsx:196 #: src/view/com/posts/FeedItem.tsx:362 msgid "Show More" -msgstr "" +msgstr "Voir plus" #: src/view/screens/PreferencesHomeFeed.tsx:258 msgid "Show Posts from My Feeds" @@ -3144,7 +3159,7 @@ msgstr "Afficher les posts de mes fils d’actu" #: src/view/screens/PreferencesHomeFeed.tsx:222 msgid "Show Quote Posts" -msgstr "Afficher les posts de citation" +msgstr "Afficher les citations" #: src/view/screens/PreferencesHomeFeed.tsx:119 msgid "Show Replies" @@ -3156,7 +3171,7 @@ msgstr "Afficher les réponses des personnes que vous suivez avant toutes les au #: src/view/screens/PreferencesHomeFeed.tsx:70 msgid "Show replies with at least {value} {0}" -msgstr "" +msgstr "Afficher les réponses avec au moins {value} {0}" #: src/view/screens/PreferencesHomeFeed.tsx:188 msgid "Show Reposts" @@ -3165,19 +3180,19 @@ msgstr "Afficher les reposts" #: src/view/com/util/moderation/ContentHider.tsx:67 #: src/view/com/util/moderation/PostHider.tsx:61 msgid "Show the content" -msgstr "" +msgstr "Afficher le contenu" #: src/view/com/notifications/FeedItem.tsx:350 msgid "Show users" -msgstr "Afficher les utilisateurs" +msgstr "Afficher les comptes" #: src/view/com/profile/ProfileHeader.tsx:501 msgid "Shows a list of users similar to this user." -msgstr "" +msgstr "Affiche une liste de comptes similaires à ce compte." #: src/view/com/profile/ProfileHeader.tsx:545 msgid "Shows posts from {0} in your feed" -msgstr "" +msgstr "Affiche les posts de {0} dans votre fil d’actu" #: src/view/com/auth/HomeLoggedOutCTA.tsx:70 #: src/view/com/auth/login/Login.tsx:98 @@ -3245,27 +3260,31 @@ msgstr "Connecté en tant que" #: src/view/com/auth/login/ChooseAccountForm.tsx:103 msgid "Signed in as @{0}" -msgstr "" +msgstr "Connecté en tant que @{0}" #: src/view/com/modals/SwitchAccount.tsx:66 msgid "Signs {0} out of Bluesky" -msgstr "" +msgstr "Déconnecte {0} de Bluesky" #: src/view/com/auth/onboarding/WelcomeMobile.tsx:33 msgid "Skip" msgstr "Ignorer" +#: src/view/com/auth/create/Step2.tsx:70 +msgid "SMS verification" +msgstr "" + #: src/view/com/modals/ProfilePreview.tsx:62 msgid "Something went wrong and we're not sure what." -msgstr "" +msgstr "Quelque chose n’a pas marché, mais on ne sait pas trop quoi." #: src/view/com/modals/Waitlist.tsx:51 msgid "Something went wrong. Check your email and try again." -msgstr "" +msgstr "Quelque chose n’a pas marché. Vérifiez vos e-mails et réessayez." -#: src/App.native.tsx:57 +#: src/App.native.tsx:62 msgid "Sorry! Your session expired. Please log in again." -msgstr "" +msgstr "Désolé ! Votre session a expiré. Essayez de vous reconnecter." #: src/view/screens/PreferencesThreads.tsx:69 msgid "Sort Replies" @@ -3273,29 +3292,33 @@ msgstr "Trier les réponses" #: src/view/screens/PreferencesThreads.tsx:72 msgid "Sort replies to the same post by:" -msgstr "Trier les réponses au même post par :" +msgstr "Trier les réponses au même post par :" #: src/view/com/modals/crop-image/CropImage.web.tsx:122 msgid "Square" msgstr "Carré" -#: src/view/com/auth/create/Step1.tsx:90 #: src/view/com/modals/ServerInput.tsx:62 msgid "Staging" -msgstr "Mise en scène" +msgstr "Serveur de test" #: src/view/screens/Settings.tsx:804 msgid "Status page" -msgstr "Page de statut" +msgstr "État du service" -#: src/view/com/auth/create/StepHeader.tsx:15 -msgid "Step {step} of 3" +#: src/view/com/auth/create/StepHeader.tsx:22 +msgid "Step {0} of {numSteps}" msgstr "" +#: src/view/com/auth/create/StepHeader.tsx:15 +#~ msgid "Step {step} of 3" +#~ msgstr "Étape {step} sur 3" + #: src/view/screens/Settings.tsx:276 msgid "Storage cleared, you need to restart the app now." -msgstr "" +msgstr "Stockage effacé, vous devez redémarrer l’application maintenant." +#: src/Navigation.tsx:204 #: src/view/screens/Settings.tsx:740 msgid "Storybook" msgstr "Historique" @@ -3314,7 +3337,7 @@ msgstr "S’abonner à cette liste" #: src/view/com/lists/ListCard.tsx:101 #~ msgid "Subscribed" -#~ msgstr "" +#~ msgstr "Abonné·e" #: src/view/screens/Search/Search.tsx:362 msgid "Suggested Follows" @@ -3322,11 +3345,11 @@ msgstr "Suivis suggérés" #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:64 msgid "Suggested for you" -msgstr "" +msgstr "Suggérés pour vous" #: src/view/com/modals/SelfLabel.tsx:95 msgid "Suggestive" -msgstr "" +msgstr "Suggestif" #: src/Navigation.tsx:214 #: src/view/screens/Support.tsx:30 @@ -3336,7 +3359,7 @@ msgstr "Soutien" #: src/view/com/modals/ProfilePreview.tsx:110 msgid "Swipe up to see more" -msgstr "" +msgstr "Glisser vers le haut pour en voir plus" #: src/view/com/modals/SwitchAccount.tsx:117 msgid "Switch Account" @@ -3345,16 +3368,16 @@ msgstr "Changer de compte" #: src/view/com/modals/SwitchAccount.tsx:97 #: src/view/screens/Settings.tsx:137 msgid "Switch to {0}" -msgstr "" +msgstr "Basculer sur {0}" #: src/view/com/modals/SwitchAccount.tsx:98 #: src/view/screens/Settings.tsx:138 msgid "Switches the account you are logged in to" -msgstr "" +msgstr "Bascule le compte auquel vous êtes connectés vers" #: src/view/screens/Settings.tsx:466 msgid "System" -msgstr "" +msgstr "Système" #: src/view/screens/Settings.tsx:720 msgid "System log" @@ -3366,7 +3389,7 @@ msgstr "Grand" #: src/view/com/util/images/AutoSizedImage.tsx:70 msgid "Tap to view fully" -msgstr "" +msgstr "Tapper pour voir en entier" #: src/view/shell/desktop/RightNav.tsx:93 msgid "Terms" @@ -3375,7 +3398,7 @@ msgstr "Conditions générales" #: src/Navigation.tsx:224 #: src/view/screens/Settings.tsx:818 #: src/view/screens/TermsOfService.tsx:29 -#: src/view/shell/Drawer.tsx:256 +#: src/view/shell/Drawer.tsx:259 msgid "Terms of Service" msgstr "Conditions d’utilisation" @@ -3406,11 +3429,7 @@ msgstr "Notre politique de confidentialité a été déplacée vers <0/>" #: src/view/screens/Support.tsx:36 msgid "The support form has been moved. If you need help, please <0/> or visit {HELP_DESK_URL} to get in touch with us." -msgstr "" - -#: src/view/screens/Support.tsx:36 -#~ msgid "The support form has been moved. If you need help, please<0/> or visit {HELP_DESK_URL} to get in touch with us." -#~ msgstr "Le formulaire d’assistance a été déplacé. Si vous avez besoin d’aide, veuillez <0/> ou rendez-vous sur {HELP_DESK_URL} pour nous contacter." +msgstr "Le formulaire d’assistance a été déplacé. Si vous avez besoin d’aide, veuillez <0/> ou rendez-vous sur {HELP_DESK_URL} pour nous contacter." #: src/view/screens/TermsOfService.tsx:33 msgid "The Terms of Service have been moved to" @@ -3418,15 +3437,15 @@ msgstr "Nos conditions d’utilisation ont été déplacées vers" #: src/view/screens/ProfileFeed.tsx:558 msgid "There was an an issue contacting the server, please check your internet connection and try again." -msgstr "" +msgstr "Il y a eu un problème de connexion au serveur, veuillez vérifier votre connexion Internet et réessayez." #: src/view/com/posts/FeedErrorMessage.tsx:139 msgid "There was an an issue removing this feed. Please check your internet connection and try again." -msgstr "" +msgstr "Il y a eu un problème lors de la suppression du fil, veuillez vérifier votre connexion Internet et réessayez." #: src/view/screens/ProfileFeed.tsx:218 msgid "There was an an issue updating your feeds, please check your internet connection and try again." -msgstr "" +msgstr "Il y a eu un problème lors de la mise à jour de vos fils d’actu, veuillez vérifier votre connexion Internet et réessayez." #: src/view/screens/ProfileFeed.tsx:245 #: src/view/screens/ProfileList.tsx:266 @@ -3434,7 +3453,7 @@ msgstr "" #: src/view/screens/SavedFeeds.tsx:231 #: src/view/screens/SavedFeeds.tsx:252 msgid "There was an issue contacting the server" -msgstr "" +msgstr "Il y a eu un problème de connexion au serveur" #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:57 #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:66 @@ -3442,32 +3461,32 @@ msgstr "" #: src/view/com/feeds/FeedSourceCard.tsx:127 #: src/view/com/feeds/FeedSourceCard.tsx:181 msgid "There was an issue contacting your server" -msgstr "" +msgstr "Il y a eu un problème de connexion à votre serveur" #: src/view/com/notifications/Feed.tsx:115 msgid "There was an issue fetching notifications. Tap here to try again." -msgstr "" +msgstr "Il y a eu un problème lors de la récupération des notifications. Appuyez ici pour réessayer." #: src/view/com/posts/Feed.tsx:263 msgid "There was an issue fetching posts. Tap here to try again." -msgstr "" +msgstr "Il y a eu un problème lors de la récupération des posts. Appuyez ici pour réessayer." #: src/view/com/lists/ListMembers.tsx:172 msgid "There was an issue fetching the list. Tap here to try again." -msgstr "" +msgstr "Il y a eu un problème lors de la récupération de la liste. Appuyez ici pour réessayer." #: src/view/com/feeds/ProfileFeedgens.tsx:148 #: src/view/com/lists/ProfileLists.tsx:155 msgid "There was an issue fetching your lists. Tap here to try again." -msgstr "" +msgstr "Il y a eu un problème lors de la récupération de vos listes. Appuyez ici pour réessayer." #: src/view/com/modals/ContentFilteringSettings.tsx:126 msgid "There was an issue syncing your preferences with the server" -msgstr "" +msgstr "Il y a eu un problème de synchronisation de vos préférences avec le serveur" #: src/view/screens/AppPasswords.tsx:66 msgid "There was an issue with fetching your app passwords" -msgstr "" +msgstr "Il y a eu un problème lors de la récupération de vos mots de passe d’application" #: src/view/com/profile/ProfileHeader.tsx:204 #: src/view/com/profile/ProfileHeader.tsx:225 @@ -3476,37 +3495,38 @@ msgstr "" #: src/view/com/profile/ProfileHeader.tsx:297 #: src/view/com/profile/ProfileHeader.tsx:319 msgid "There was an issue! {0}" -msgstr "" +msgstr "Il y a eu un problème ! {0}" #: src/view/screens/ProfileList.tsx:287 #: src/view/screens/ProfileList.tsx:306 #: src/view/screens/ProfileList.tsx:328 #: src/view/screens/ProfileList.tsx:347 msgid "There was an issue. Please check your internet connection and try again." -msgstr "" +msgstr "Il y a eu un problème. Veuillez vérifier votre connexion Internet et réessayez." #: src/view/com/util/ErrorBoundary.tsx:35 msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" -msgstr "Un problème inattendu s’est produit dans l’application. N’hésitez pas à nous faire savoir si cela vous est arrivé !" +msgstr "Un problème inattendu s’est produit dans l’application. N’hésitez pas à nous faire savoir si cela vous est arrivé !" -#~ msgid "This {0} has been labeled." -#~ msgstr "Ce {0} a été classé." +#: src/view/com/auth/create/Step2.tsx:47 +msgid "There's something wrong with this number. Please include your country and/or area code!" +msgstr "" #: src/view/com/util/moderation/ScreenHider.tsx:88 msgid "This {screenDescription} has been flagged:" -msgstr "Ce {screenDescription} a été signalé :" +msgstr "Ce {screenDescription} a été signalé :" #: src/view/com/util/moderation/ScreenHider.tsx:83 msgid "This account has requested that users sign in to view their profile." -msgstr "Ce compte a demandé aux utilisateurs de s’identifier pour voir son profil." +msgstr "Ce compte a demandé aux personnes de se connecter pour voir son profil." #: src/view/com/modals/EmbedConsent.tsx:68 msgid "This content is hosted by {0}. Do you want to enable external media?" -msgstr "" +msgstr "Ce contenu est hébergé par {0}. Voulez-vous activer les médias externes ?" #: src/view/com/modals/ModerationDetails.tsx:67 msgid "This content is not available because one of the users involved has blocked the other." -msgstr "" +msgstr "Ce contenu n’est pas disponible car l’un des comptes impliqués a bloqué l’autre." #: src/view/com/posts/FeedErrorMessage.tsx:108 msgid "This content is not viewable without a Bluesky account." @@ -3520,35 +3540,35 @@ msgstr "Ce fil d’actu reçoit actuellement un trafic important, il est tempora #: src/view/screens/ProfileFeed.tsx:484 #: src/view/screens/ProfileList.tsx:639 msgid "This feed is empty!" -msgstr "" +msgstr "Ce fil d’actu est vide !" #: src/view/com/posts/CustomFeedEmptyState.tsx:37 msgid "This feed is empty! You may need to follow more users or tune your language settings." -msgstr "" +msgstr "Ce fil d’actu est vide ! Vous devriez peut-être suivre plus de comptes ou ajuster vos paramètres de langue." #: src/view/com/modals/BirthDateSettings.tsx:61 msgid "This information is not shared with other users." -msgstr "Ces informations ne sont pas partagées avec d’autres utilisateurs." +msgstr "Ces informations ne sont pas partagées avec d’autres personnes." #: src/view/com/modals/VerifyEmail.tsx:119 msgid "This is important in case you ever need to change your email or reset your password." msgstr "Ceci est important au cas où vous auriez besoin de changer d’e-mail ou de réinitialiser votre mot de passe." #: src/view/com/auth/create/Step1.tsx:55 -msgid "This is the service that keeps you online." -msgstr "C’est le service qui vous permet de rester en ligne." +#~ msgid "This is the service that keeps you online." +#~ msgstr "C’est le service qui vous permet de rester en ligne." #: src/view/com/modals/LinkWarning.tsx:58 msgid "This link is taking you to the following website:" -msgstr "Ce lien vous conduit au site Web suivant :" +msgstr "Ce lien vous conduit au site Web suivant :" #: src/view/screens/ProfileList.tsx:813 msgid "This list is empty!" -msgstr "" +msgstr "Cette liste est vide !" #: src/view/com/modals/AddAppPasswords.tsx:105 msgid "This name is already in use" -msgstr "" +msgstr "Ce nom est déjà utilisé" #: src/view/com/post-thread/PostThreadItem.tsx:123 msgid "This post has been deleted." @@ -3556,15 +3576,15 @@ msgstr "Ce post a été supprimé." #: src/view/com/modals/ModerationDetails.tsx:62 msgid "This user has blocked you. You cannot view their content." -msgstr "" +msgstr "Ce compte vous a bloqué. Vous ne pouvez pas voir son contenu." #: src/view/com/modals/ModerationDetails.tsx:42 msgid "This user is included in the <0/> list which you have blocked." -msgstr "" +msgstr "Ce compte est inclus dans la liste <0/> que vous avez bloquée." #: src/view/com/modals/ModerationDetails.tsx:74 msgid "This user is included the <0/> list which you have muted." -msgstr "" +msgstr "Ce compte est inclus dans la liste <0/> que vous avez masquée." #: src/view/com/modals/SelfLabel.tsx:137 msgid "This warning is only available for posts with media attached." @@ -3585,7 +3605,7 @@ msgstr "Mode arborescent" #: src/Navigation.tsx:254 msgid "Threads Preferences" -msgstr "" +msgstr "Préférences de fils de discussion" #: src/view/com/util/forms/DropdownButton.tsx:234 msgid "Toggle dropdown" @@ -3604,11 +3624,7 @@ msgstr "Traduire" #: src/view/com/util/error/ErrorScreen.tsx:75 msgctxt "action" msgid "Try again" -msgstr "" - -#: src/view/com/util/error/ErrorScreen.tsx:73 -#~ msgid "Try again" -#~ msgstr "Réessayer" +msgstr "Réessayer" #: src/view/screens/ProfileList.tsx:484 msgid "Un-block list" @@ -3618,22 +3634,22 @@ msgstr "Débloquer la liste" msgid "Un-mute list" msgstr "Réafficher cette liste" -#: src/view/com/auth/create/CreateAccount.tsx:65 +#: src/view/com/auth/create/CreateAccount.tsx:66 #: src/view/com/auth/login/ForgotPasswordForm.tsx:87 #: src/view/com/auth/login/Login.tsx:76 #: src/view/com/auth/login/LoginForm.tsx:120 msgid "Unable to contact your service. Please check your Internet connection." msgstr "Impossible de contacter votre service. Veuillez vérifier votre connexion Internet." -#: src/view/com/profile/ProfileHeader.tsx:472 -#: src/view/screens/ProfileList.tsx:568 +#: src/view/com/profile/ProfileHeader.tsx:475 +msgctxt "action" msgid "Unblock" msgstr "Débloquer" -#: src/view/com/profile/ProfileHeader.tsx:475 -msgctxt "action" +#: src/view/com/profile/ProfileHeader.tsx:472 +#: src/view/screens/ProfileList.tsx:568 msgid "Unblock" -msgstr "" +msgstr "Débloquer" #: src/view/com/profile/ProfileHeader.tsx:308 #: src/view/com/profile/ProfileHeader.tsx:392 @@ -3650,23 +3666,23 @@ msgstr "Annuler le repost" #: src/view/com/profile/FollowButton.tsx:55 msgctxt "action" msgid "Unfollow" -msgstr "" +msgstr "Se désabonner" #: src/view/com/profile/ProfileHeader.tsx:524 msgid "Unfollow {0}" -msgstr "" +msgstr "Se désabonner de {0}" -#: src/view/com/auth/create/state.ts:216 +#: src/view/com/auth/create/state.ts:289 msgid "Unfortunately, you do not meet the requirements to create an account." msgstr "Malheureusement, vous ne remplissez pas les conditions requises pour créer un compte." #: src/view/com/util/post-ctrls/PostCtrls.tsx:189 msgid "Unlike" -msgstr "" +msgstr "Déliker" #: src/view/screens/ProfileList.tsx:575 msgid "Unmute" -msgstr "" +msgstr "Réafficher" #: src/view/com/profile/ProfileHeader.tsx:373 msgid "Unmute Account" @@ -3679,7 +3695,7 @@ msgstr "Réafficher ce fil de discussion" #: src/view/screens/ProfileFeed.tsx:362 #: src/view/screens/ProfileList.tsx:559 msgid "Unpin" -msgstr "" +msgstr "Désépingler" #: src/view/screens/ProfileList.tsx:452 msgid "Unpin moderation list" @@ -3687,7 +3703,7 @@ msgstr "Supprimer la liste de modération" #: src/view/screens/ProfileFeed.tsx:354 msgid "Unsave" -msgstr "" +msgstr "Supprimer" #: src/view/com/modals/UserAddRemoveLists.tsx:54 msgid "Update {displayName} in Lists" @@ -3703,11 +3719,11 @@ msgstr "Mise à jour…" #: src/view/com/modals/ChangeHandle.tsx:455 msgid "Upload a text file to:" -msgstr "Télécharger un fichier texte vers :" +msgstr "Envoyer un fichier texte vers :" #: src/view/screens/AppPasswords.tsx:195 msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." -msgstr "Utiliser les mots de passe de l’appli pour se connecter à d’autres clients Bluesky sans donner un accès complet à votre compte ou à votre mot de passe." +msgstr "Utilisez les mots de passe de l’appli pour se connecter à d’autres clients Bluesky sans donner un accès complet à votre compte ou à votre mot de passe." #: src/view/com/modals/ChangeHandle.tsx:515 msgid "Use default provider" @@ -3729,71 +3745,75 @@ msgstr "Utilisez-le pour vous connecter à l’autre application avec votre iden #: src/view/com/modals/ServerInput.tsx:105 msgid "Use your domain as your Bluesky client service provider" -msgstr "" +msgstr "Utilise votre domaine comme votre fournisseur de client Bluesky" #: src/view/com/modals/InviteCodes.tsx:200 msgid "Used by:" -msgstr "Utilisé par :" +msgstr "Utilisé par :" #: src/view/com/modals/ModerationDetails.tsx:54 msgid "User Blocked" -msgstr "" +msgstr "Compte bloqué" #: src/view/com/modals/ModerationDetails.tsx:40 msgid "User Blocked by List" -msgstr "" +msgstr "Compte bloqué par liste" #: src/view/com/modals/ModerationDetails.tsx:60 msgid "User Blocks You" -msgstr "" +msgstr "Compte qui vous bloque" #: src/view/com/auth/create/Step3.tsx:38 msgid "User handle" -msgstr "Pseudo utilisateur" +msgstr "Pseudo" #: src/view/com/lists/ListCard.tsx:84 #: src/view/com/modals/UserAddRemoveLists.tsx:182 msgid "User list by {0}" -msgstr "" +msgstr "Liste de compte de {0}" #: src/view/screens/ProfileList.tsx:741 msgid "User list by <0/>" -msgstr "" +msgstr "Liste de compte par <0/>" #: src/view/com/lists/ListCard.tsx:82 #: src/view/com/modals/UserAddRemoveLists.tsx:180 #: src/view/screens/ProfileList.tsx:739 msgid "User list by you" -msgstr "" +msgstr "Liste de compte par vous" #: src/view/com/modals/CreateOrEditList.tsx:138 msgid "User list created" -msgstr "" +msgstr "Liste de compte créée" #: src/view/com/modals/CreateOrEditList.tsx:125 msgid "User list updated" -msgstr "" +msgstr "Liste de compte mise à jour" #: src/view/screens/Lists.tsx:58 msgid "User Lists" -msgstr "Listes d’utilisateurs" +msgstr "Listes de comptes" #: src/view/com/auth/login/LoginForm.tsx:174 #: src/view/com/auth/login/LoginForm.tsx:192 msgid "Username or email address" -msgstr "Nom d’utilisateur ou e-mail" +msgstr "Pseudo ou e-mail" #: src/view/screens/ProfileList.tsx:775 msgid "Users" -msgstr "Utilisateurs" +msgstr "Comptes" #: src/view/com/threadgate/WhoCanReply.tsx:143 msgid "users followed by <0/>" -msgstr "utilisateurs suivis par <0/>" +msgstr "comptes suivis par <0/>" #: src/view/com/modals/Threadgate.tsx:106 msgid "Users in \"{0}\"" -msgstr "Utilisateurs dans « {0} »" +msgstr "Comptes dans « {0} »" + +#: src/view/com/auth/create/Step2.tsx:139 +msgid "Verification code" +msgstr "" #: src/view/screens/Settings.tsx:843 msgid "Verify email" @@ -3814,11 +3834,11 @@ msgstr "Confirmer le nouvel e-mail" #: src/view/com/modals/VerifyEmail.tsx:103 msgid "Verify Your Email" -msgstr "" +msgstr "Vérifiez votre e-mail" #: src/view/com/profile/ProfileHeader.tsx:701 msgid "View {0}'s avatar" -msgstr "" +msgstr "Voir l’avatar de {0}" #: src/view/screens/Log.tsx:52 msgid "View debug entry" @@ -3826,11 +3846,11 @@ msgstr "Afficher l’entrée de débogage" #: src/view/com/posts/FeedSlice.tsx:103 msgid "View full thread" -msgstr "" +msgstr "Voir le fil de discussion entier" #: src/view/com/posts/FeedErrorMessage.tsx:172 msgid "View profile" -msgstr "" +msgstr "Voir le profil" #: src/view/com/profile/ProfileSubpageHeader.tsx:128 msgid "View the avatar" @@ -3842,23 +3862,23 @@ msgstr "Visiter le site" #: src/view/com/modals/ContentFilteringSettings.tsx:254 msgid "Warn" -msgstr "" +msgstr "Avertir" #: src/view/com/posts/DiscoverFallbackHeader.tsx:29 -msgid "We ran out of posts from your follows. Here's the latest from" +msgid "We ran out of posts from your follows. Here's the latest from <0/>." msgstr "" #: src/view/com/modals/AppealLabel.tsx:48 msgid "We'll look into your appeal promptly." -msgstr "" +msgstr "Nous examinerons votre appel rapidement." -#: src/view/com/auth/create/CreateAccount.tsx:122 +#: src/view/com/auth/create/CreateAccount.tsx:123 msgid "We're so excited to have you join us!" -msgstr "Nous sommes ravis de vous accueillir !" +msgstr "Nous sommes ravis de vous accueillir !" #: src/view/screens/ProfileList.tsx:83 msgid "We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @{handleOrDid}." -msgstr "" +msgstr "Nous sommes désolés, mais nous n’avons pas pu charger cette liste. Si cela persiste, veuillez contacter l’origine de la liste, @{handleOrDid}." #: src/view/screens/Search/Search.tsx:243 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." @@ -3866,7 +3886,7 @@ msgstr "Nous sommes désolés, mais votre recherche a été annulée. Veuillez r #: src/view/screens/NotFound.tsx:48 msgid "We're sorry! We can't find the page you were looking for." -msgstr "Nous sommes désolés ! La page que vous recherchez est introuvable." +msgstr "Nous sommes désolés ! La page que vous recherchez est introuvable." #: src/view/com/auth/onboarding/WelcomeMobile.tsx:46 msgid "Welcome to <0>Bluesky</0>" @@ -3874,25 +3894,25 @@ msgstr "Bienvenue sur <0>Bluesky</0>" #: src/view/com/modals/report/Modal.tsx:169 msgid "What is the issue with this {collectionName}?" -msgstr "Quel est le problème avec cette {collectionName} ?" +msgstr "Quel est le problème avec cette {collectionName} ?" #: src/view/com/auth/SplashScreen.tsx:34 #: src/view/com/composer/Composer.tsx:279 msgid "What's up?" -msgstr "Quoi de neuf ?" +msgstr "Quoi de neuf ?" #: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:78 msgid "Which languages are used in this post?" -msgstr "Quelles sont les langues utilisées dans ce post ?" +msgstr "Quelles sont les langues utilisées dans ce post ?" #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:77 msgid "Which languages would you like to see in your algorithmic feeds?" -msgstr "Quelles langues aimeriez-vous voir apparaître dans vos flux algorithmiques ?" +msgstr "Quelles langues aimeriez-vous voir apparaître dans vos flux algorithmiques ?" #: src/view/com/composer/threadgate/ThreadgateBtn.tsx:47 #: src/view/com/modals/Threadgate.tsx:66 msgid "Who can reply" -msgstr "Qui peut répondre ?" +msgstr "Qui peut répondre ?" #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" @@ -3907,6 +3927,10 @@ msgstr "Rédiger un post" msgid "Write your reply" msgstr "Rédigez votre réponse" +#: src/view/com/auth/create/Step2.tsx:158 +msgid "XXXXXX" +msgstr "" + #: src/view/com/composer/select-language/SuggestedLanguage.tsx:82 #: src/view/screens/PreferencesHomeFeed.tsx:129 #: src/view/screens/PreferencesHomeFeed.tsx:201 @@ -3920,11 +3944,11 @@ msgstr "Oui" #: src/view/com/posts/FollowingEmptyState.tsx:67 #: src/view/com/posts/FollowingEndOfFeed.tsx:68 msgid "You can also discover new Custom Feeds to follow." -msgstr "" +msgstr "Vous pouvez aussi découvrir de nouveaux fils d’actu personnalisés à suivre." #: src/view/com/auth/create/Step1.tsx:106 -msgid "You can change hosting providers at any time." -msgstr "Vous pouvez changer d’hébergeur à tout moment." +#~ msgid "You can change hosting providers at any time." +#~ msgstr "Vous pouvez changer d’hébergeur à tout moment." #: src/view/com/auth/login/Login.tsx:158 #: src/view/com/auth/login/PasswordUpdatedForm.tsx:31 @@ -3933,7 +3957,7 @@ msgstr "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe." #: src/view/com/modals/InviteCodes.tsx:66 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." -msgstr "Vous n’avez encore aucun code d’invitation ! Nous vous en enverrons lorsque vous serez sur Bluesky depuis un peu plus longtemps." +msgstr "Vous n’avez encore aucun code d’invitation ! Nous vous en enverrons lorsque vous serez sur Bluesky depuis un peu plus longtemps." #: src/view/screens/SavedFeeds.tsx:102 msgid "You don't have any pinned feeds." @@ -3941,7 +3965,7 @@ msgstr "Vous n’avez encore aucun fil épinglé." #: src/view/screens/Feeds.tsx:387 msgid "You don't have any saved feeds!" -msgstr "Vous n’avez encore aucun fil enregistré !" +msgstr "Vous n’avez encore aucun fil enregistré !" #: src/view/screens/SavedFeeds.tsx:135 msgid "You don't have any saved feeds." @@ -3953,11 +3977,11 @@ msgstr "Vous avez bloqué cet auteur ou vous avez été bloqué par celui-ci." #: src/view/com/modals/ModerationDetails.tsx:56 msgid "You have blocked this user. You cannot view their content." -msgstr "" +msgstr "Vous avez bloqué ce compte. Vous ne pouvez pas voir son contenu." #: src/view/com/modals/ModerationDetails.tsx:87 msgid "You have muted this user." -msgstr "" +msgstr "Vous avez masqué ce compte." #: src/view/com/feeds/ProfileFeedgens.tsx:136 msgid "You have no feeds." @@ -3970,7 +3994,7 @@ msgstr "Vous n’avez aucune liste." #: src/view/screens/ModerationBlockedAccounts.tsx:132 msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." -msgstr "Vous n’avez pas encore bloqué de comptes. Pour bloquer un compte, accédez à son profil et sélectionnez « Bloquer le compte » dans le menu de son compte." +msgstr "Vous n’avez pas encore bloqué de comptes. Pour bloquer un compte, accédez à son profil et sélectionnez « Bloquer le compte » dans le menu de son compte." #: src/view/screens/AppPasswords.tsx:87 msgid "You have not created any app passwords yet. You can create one by pressing the button below." @@ -3978,37 +4002,37 @@ msgstr "Vous n’avez encore créé aucun mot de passe pour l’appli. Vous pouv #: src/view/screens/ModerationMutedAccounts.tsx:131 msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." -msgstr "Vous n’avez encore masqué aucun compte. Pour désactiver un compte, allez sur son profil et sélectionnez « Masquer le compte » dans le menu de son compte." +msgstr "Vous n’avez encore masqué aucun compte. Pour désactiver un compte, allez sur son profil et sélectionnez « Masquer le compte » dans le menu de son compte." #: src/view/com/modals/ContentFilteringSettings.tsx:170 msgid "You must be 18 or older to enable adult content." -msgstr "" +msgstr "Vous devez avoir 18 ans ou plus pour activer le contenu pour adultes." #: src/view/com/util/forms/PostDropdownBtn.tsx:96 msgid "You will no longer receive notifications for this thread" -msgstr "" +msgstr "Vous ne recevrez plus de notifications pour ce fil de discussion" #: src/view/com/util/forms/PostDropdownBtn.tsx:99 msgid "You will now receive notifications for this thread" -msgstr "" +msgstr "Vous recevrez désormais des notifications pour ce fil de discussion" #: src/view/com/auth/login/SetNewPasswordForm.tsx:81 msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." -msgstr "Vous recevrez un e-mail contenant un « code de réinitialisation » Saisissez ce code ici, puis votre nouveau mot de passe." +msgstr "Vous recevrez un e-mail contenant un « code de réinitialisation » Saisissez ce code ici, puis votre nouveau mot de passe." #: src/view/com/posts/FollowingEndOfFeed.tsx:48 msgid "You've reached the end of your feed! Find some more accounts to follow." -msgstr "" +msgstr "Vous avez atteint la fin de votre fil d’actu ! Trouvez d’autres comptes à suivre." -#: src/view/com/auth/create/Step2.tsx:58 +#: src/view/com/auth/create/Step1.tsx:67 msgid "Your account" msgstr "Votre compte" #: src/view/com/modals/DeleteAccount.tsx:65 msgid "Your account has been deleted" -msgstr "" +msgstr "Votre compte a été supprimé" -#: src/view/com/auth/create/Step2.tsx:146 +#: src/view/com/auth/create/Step1.tsx:182 msgid "Your birth date" msgstr "Votre date de naissance" @@ -4016,14 +4040,14 @@ msgstr "Votre date de naissance" msgid "Your choice will be saved, but can be changed later in settings." msgstr "" -#: src/view/com/auth/create/state.ts:102 +#: src/view/com/auth/create/state.ts:147 #: src/view/com/auth/login/ForgotPasswordForm.tsx:70 msgid "Your email appears to be invalid." msgstr "Votre e-mail semble être invalide." #: src/view/com/modals/Waitlist.tsx:109 msgid "Your email has been saved! We'll be in touch soon." -msgstr "Votre e-mail a été enregistré ! Nous vous contacterons bientôt." +msgstr "Votre e-mail a été enregistré ! Nous vous contacterons bientôt." #: src/view/com/modals/ChangeEmail.tsx:125 msgid "Your email has been updated but not verified. As a next step, please verify your new email." @@ -4035,7 +4059,7 @@ msgstr "Votre e-mail n’a pas encore été vérifié. Il s’agit d’une mesur #: src/view/com/posts/FollowingEmptyState.tsx:47 msgid "Your following feed is empty! Follow more users to see what's happening." -msgstr "" +msgstr "Votre fil d’actu des comptes suivis est vide ! Suivez plus de comptes pour voir ce qui se passe." #: src/view/com/auth/create/Step3.tsx:42 msgid "Your full handle will be" @@ -4043,26 +4067,26 @@ msgstr "Votre nom complet sera" #: src/view/com/modals/ChangeHandle.tsx:270 msgid "Your full handle will be <0>@{0}</0>" -msgstr "" +msgstr "Votre pseudo complet sera <0>@{0}</0>" #: src/view/com/auth/create/Step1.tsx:53 -msgid "Your hosting provider" -msgstr "Votre fournisseur d’hébergement" +#~ msgid "Your hosting provider" +#~ msgstr "Votre hébergeur" #: src/view/screens/Settings.tsx:430 #: src/view/shell/desktop/RightNav.tsx:137 -#: src/view/shell/Drawer.tsx:657 +#: src/view/shell/Drawer.tsx:660 msgid "Your invite codes are hidden when logged in using an App Password" msgstr "Vos codes d’invitation sont cachés lorsque vous êtes connecté à l’aide d’un mot de passe d’application." #: src/view/com/composer/Composer.tsx:267 msgid "Your post has been published" -msgstr "" +msgstr "Votre post a été publié" #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:59 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:59 msgid "Your posts, likes, and blocks are public. Mutes are private." -msgstr "Vos posts, les mentions « J’aime » et les blocages sont publics. Les silences (comptes masqués) sont privés." +msgstr "Vos posts, les likes et les blocages sont publics. Les silences (comptes masqués) sont privés." #: src/view/com/modals/SwitchAccount.tsx:84 #: src/view/screens/Settings.tsx:125 @@ -4071,7 +4095,7 @@ msgstr "Votre profil" #: src/view/com/composer/Composer.tsx:266 msgid "Your reply has been published" -msgstr "" +msgstr "Votre réponse a été publiée" #: src/view/com/auth/create/Step3.tsx:28 msgid "Your user handle" diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx new file mode 100644 index 000000000..4cafaa086 --- /dev/null +++ b/src/state/dialogs/index.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import {DialogControlProps} from '#/components/Dialog' + +const DialogContext = React.createContext<{ + activeDialogs: React.MutableRefObject< + Map<string, React.MutableRefObject<DialogControlProps>> + > +}>({ + activeDialogs: { + current: new Map(), + }, +}) + +const DialogControlContext = React.createContext<{ + closeAllDialogs(): void +}>({ + closeAllDialogs: () => {}, +}) + +export function useDialogStateContext() { + return React.useContext(DialogContext) +} + +export function useDialogStateControlContext() { + return React.useContext(DialogControlContext) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const activeDialogs = React.useRef< + Map<string, React.MutableRefObject<DialogControlProps>> + >(new Map()) + const closeAllDialogs = React.useCallback(() => { + activeDialogs.current.forEach(dialog => dialog.current.close()) + }, []) + const context = React.useMemo(() => ({activeDialogs}), []) + const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs]) + return ( + <DialogContext.Provider value={context}> + <DialogControlContext.Provider value={controls}> + {children} + </DialogControlContext.Provider> + </DialogContext.Provider> + ) +} diff --git a/src/state/persisted/__tests__/migrate.test.ts b/src/state/persisted/__tests__/migrate.test.ts index d42580efd..2435ed24f 100644 --- a/src/state/persisted/__tests__/migrate.test.ts +++ b/src/state/persisted/__tests__/migrate.test.ts @@ -26,7 +26,7 @@ test('migrate: fresh install', async () => { expect(AsyncStorage.getItem).toHaveBeenCalledWith('root') expect(read).toHaveBeenCalledTimes(1) - expect(logger.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( 'persisted state: no migration needed', ) }) @@ -38,7 +38,7 @@ test('migrate: fresh install, existing new storage', async () => { expect(AsyncStorage.getItem).toHaveBeenCalledWith('root') expect(read).toHaveBeenCalledTimes(1) - expect(logger.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( 'persisted state: no migration needed', ) }) @@ -68,7 +68,7 @@ test('migrate: has legacy data', async () => { await migrate() expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP)) - expect(logger.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( 'persisted state: migrated legacy storage', ) }) diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts index 334ef1d92..097d6bc5c 100644 --- a/src/state/persisted/legacy.ts +++ b/src/state/persisted/legacy.ts @@ -164,14 +164,14 @@ export async function migrate() { if (validate.success) { await write(newData) - logger.log('persisted state: migrated legacy storage') + logger.info('persisted state: migrated legacy storage') } else { logger.error('persisted state: legacy data failed validation', { error: validate.error, }) } } else { - logger.log('persisted state: no migration needed') + logger.info('persisted state: no migration needed') } } catch (e: any) { logger.error(e, { diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 7a55b4e18..4acc7179a 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -272,7 +272,8 @@ export function usePinnedFeedsInfos(): { }, }) } catch (e) { - logger.warn(`usePinnedFeedsInfos: failed to fetch ${uri}`, { + // expected failure + logger.info(`usePinnedFeedsInfos: failed to fetch ${uri}`, { error: e, }) } diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index 49bb5a29d..a96b56225 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -167,7 +167,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } broadcast.postMessage({event: unreadCountStr}) } catch (e) { - logger.error('Failed to check unread notifications', {error: e}) + logger.warn('Failed to check unread notifications', {error: e}) } }, diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 0a565c975..e49bc2b39 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -44,6 +44,8 @@ export type ApiContext = { password: string handle: string inviteCode?: string + verificationPhone?: string + verificationCode?: string }) => Promise<void> login: (props: { service: string @@ -203,7 +205,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, [setStateAndPersist, queryClient]) const createAccount = React.useCallback<ApiContext['createAccount']>( - async ({service, email, password, handle, inviteCode}: any) => { + async ({ + service, + email, + password, + handle, + inviteCode, + verificationPhone, + verificationCode, + }: any) => { logger.info(`session: creating account`, { service, handle, @@ -217,6 +227,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { password, email, inviteCode, + verificationPhone, + verificationCode, }) if (!agent.session) { diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx deleted file mode 100644 index d1f70d4ae..000000000 --- a/src/view/com/Button.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import React from 'react' -import {Pressable, Text, PressableProps, TextProps} from 'react-native' -import * as tokens from '#/alf/tokens' -import {atoms} from '#/alf' - -export type ButtonType = - | 'primary' - | 'secondary' - | 'tertiary' - | 'positive' - | 'negative' -export type ButtonSize = 'small' | 'large' - -export type VariantProps = { - type?: ButtonType - size?: ButtonSize -} -type ButtonState = { - pressed: boolean - hovered: boolean - focused: boolean -} -export type ButtonProps = Omit<PressableProps, 'children'> & - VariantProps & { - children: - | ((props: { - state: ButtonState - type?: ButtonType - size?: ButtonSize - }) => React.ReactNode) - | React.ReactNode - | string - } -export type ButtonTextProps = TextProps & VariantProps - -export function Button({children, style, type, size, ...rest}: ButtonProps) { - const {baseStyles, hoverStyles} = React.useMemo(() => { - const baseStyles = [] - const hoverStyles = [] - - switch (type) { - case 'primary': - baseStyles.push({ - backgroundColor: tokens.color.blue_500, - }) - break - case 'secondary': - baseStyles.push({ - backgroundColor: tokens.color.gray_200, - }) - hoverStyles.push({ - backgroundColor: tokens.color.gray_100, - }) - break - default: - } - - switch (size) { - case 'large': - baseStyles.push( - atoms.py_md, - atoms.px_xl, - atoms.rounded_md, - atoms.gap_sm, - ) - break - case 'small': - baseStyles.push( - atoms.py_sm, - atoms.px_md, - atoms.rounded_sm, - atoms.gap_xs, - ) - break - default: - } - - return { - baseStyles, - hoverStyles, - } - }, [type, size]) - - const [state, setState] = React.useState({ - pressed: false, - hovered: false, - focused: false, - }) - - const onPressIn = React.useCallback(() => { - setState(s => ({ - ...s, - pressed: true, - })) - }, [setState]) - const onPressOut = React.useCallback(() => { - setState(s => ({ - ...s, - pressed: false, - })) - }, [setState]) - const onHoverIn = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: true, - })) - }, [setState]) - const onHoverOut = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: false, - })) - }, [setState]) - const onFocus = React.useCallback(() => { - setState(s => ({ - ...s, - focused: true, - })) - }, [setState]) - const onBlur = React.useCallback(() => { - setState(s => ({ - ...s, - focused: false, - })) - }, [setState]) - - return ( - <Pressable - {...rest} - style={state => [ - atoms.flex_row, - atoms.align_center, - ...baseStyles, - ...(state.hovered ? hoverStyles : []), - typeof style === 'function' ? style(state) : style, - ]} - onPressIn={onPressIn} - onPressOut={onPressOut} - onHoverIn={onHoverIn} - onHoverOut={onHoverOut} - onFocus={onFocus} - onBlur={onBlur}> - {typeof children === 'string' ? ( - <ButtonText type={type} size={size}> - {children} - </ButtonText> - ) : typeof children === 'function' ? ( - children({state, type, size}) - ) : ( - children - )} - </Pressable> - ) -} - -export function ButtonText({ - children, - style, - type, - size, - ...rest -}: ButtonTextProps) { - const textStyles = React.useMemo(() => { - const base = [] - - switch (type) { - case 'primary': - base.push({color: tokens.color.white}) - break - case 'secondary': - base.push({ - color: tokens.color.gray_700, - }) - break - default: - } - - switch (size) { - case 'small': - base.push(atoms.text_sm, {paddingBottom: 1}) - break - case 'large': - base.push(atoms.text_md, {paddingBottom: 1}) - break - default: - } - - return base - }, [type, size]) - - return ( - <Text - {...rest} - style={[ - atoms.flex_1, - atoms.font_semibold, - atoms.text_center, - ...textStyles, - style, - ]}> - {children} - </Text> - ) -} diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx index 74307a631..449afb0d3 100644 --- a/src/view/com/auth/create/CreateAccount.tsx +++ b/src/view/com/auth/create/CreateAccount.tsx @@ -22,12 +22,13 @@ import { useSetSaveFeedsMutation, DEFAULT_PROD_FEEDS, } from '#/state/queries/preferences' -import {IS_PROD} from '#/lib/constants' +import {FEEDBACK_FORM_URL, IS_PROD} from '#/lib/constants' import {Step1} from './Step1' import {Step2} from './Step2' import {Step3} from './Step3' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {TextLink} from '../../util/Link' export function CreateAccount({onPressBack}: {onPressBack: () => void}) { const {screen} = useAnalytics() @@ -117,7 +118,7 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) { return ( <LoggedOutLayout - leadin={`Step ${uiState.step}`} + leadin="" title={_(msg`Create Account`)} description={_(msg`We're so excited to have you join us!`)}> <ScrollView testID="createAccount" style={pal.view}> @@ -176,6 +177,27 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) { </> ) : undefined} </View> + + <View style={styles.stepContainer}> + <View + style={[ + s.flexRow, + s.alignCenter, + pal.viewLight, + {borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12}, + ]}> + <Text type="md" style={pal.textLight}> + <Trans>Having trouble?</Trans>{' '} + </Text> + <TextLink + type="md" + style={pal.link} + text={_(msg`Contact support`)} + href={FEEDBACK_FORM_URL({email: uiState.email})} + /> + </View> + </View> + <View style={{height: isTabletOrDesktop ? 50 : 400}} /> </ScrollView> </LoggedOutLayout> diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index 0f8581c0b..2ce77cf53 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -1,25 +1,38 @@ import React from 'react' -import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' +import { + ActivityIndicator, + Keyboard, + StyleSheet, + TouchableWithoutFeedback, + View, +} from 'react-native' +import {CreateAccountState, CreateAccountDispatch, is18} from './state' import {Text} from 'view/com/util/text/Text' +import {DateInput} from 'view/com/util/forms/DateInput' import {StepHeader} from './StepHeader' -import {CreateAccountState, CreateAccountDispatch} from './state' -import {useTheme} from 'lib/ThemeContext' -import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' -import {HelpTip} from '../util/HelpTip' +import {usePalette} from 'lib/hooks/usePalette' import {TextInput} from '../util/TextInput' -import {Button} from 'view/com/util/forms/Button' +import {Button} from '../../util/forms/Button' +import {Policies} from './Policies' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {msg, Trans} from '@lingui/macro' +import {isWeb} from 'platform/detection' +import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useModalControls} from '#/state/modals' +import {logger} from '#/logger' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants' -import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' +function sanitizeDate(date: Date): Date { + if (!date || date.toString() === 'Invalid Date') { + logger.error(`Create account: handled invalid date for birthDate`, { + hasDate: !!date, + }) + return new Date() + } + return date +} -/** STEP 1: Your hosting provider - * @field Bluesky (default) - * @field Other (staging, local dev, your own PDS, etc.) - */ export function Step1({ uiState, uiDispatch, @@ -28,136 +41,175 @@ export function Step1({ uiDispatch: CreateAccountDispatch }) { const pal = usePalette('default') - const [isDefaultSelected, setIsDefaultSelected] = React.useState(true) const {_} = useLingui() + const {openModal} = useModalControls() - const onPressDefault = React.useCallback(() => { - setIsDefaultSelected(true) - uiDispatch({type: 'set-service-url', value: PROD_SERVICE}) - }, [setIsDefaultSelected, uiDispatch]) + const onPressSelectService = React.useCallback(() => { + openModal({ + name: 'server-input', + initialService: uiState.serviceUrl, + onSelect: (url: string) => + uiDispatch({type: 'set-service-url', value: url}), + }) + Keyboard.dismiss() + }, [uiDispatch, uiState.serviceUrl, openModal]) - const onPressOther = React.useCallback(() => { - setIsDefaultSelected(false) - uiDispatch({type: 'set-service-url', value: 'https://'}) - }, [setIsDefaultSelected, uiDispatch]) + const onPressWaitlist = React.useCallback(() => { + openModal({name: 'waitlist'}) + }, [openModal]) - const onChangeServiceUrl = React.useCallback( - (v: string) => { - uiDispatch({type: 'set-service-url', value: v}) - }, - [uiDispatch], - ) + const birthDate = React.useMemo(() => { + return sanitizeDate(uiState.birthDate) + }, [uiState.birthDate]) return ( <View> - <StepHeader step="1" title={_(msg`Your hosting provider`)} /> - <Text style={[pal.text, s.mb10]}> - <Trans>This is the service that keeps you online.</Trans> - </Text> - <Option - testID="blueskyServerBtn" - isSelected={isDefaultSelected} - label="Bluesky" - help=" (default)" - onPress={onPressDefault} - /> - <Option - testID="otherServerBtn" - isSelected={!isDefaultSelected} - label="Other" - onPress={onPressOther}> - <View style={styles.otherForm}> - <Text nativeID="addressProvider" style={[pal.text, s.mb5]}> - <Trans>Enter the address of your provider:</Trans> - </Text> - <TextInput - testID="customServerInput" - icon="globe" - placeholder={_(msg`Hosting provider address`)} - value={uiState.serviceUrl} - editable - onChange={onChangeServiceUrl} - accessibilityHint={_(msg`Input hosting provider address`)} - accessibilityLabel={_(msg`Hosting provider address`)} - accessibilityLabelledBy="addressProvider" - /> - {LOGIN_INCLUDE_DEV_SERVERS && ( - <View style={[s.flexRow, s.mt10]}> - <Button - testID="stagingServerBtn" - type="default" - style={s.mr5} - label={_(msg`Staging`)} - onPress={() => onChangeServiceUrl(STAGING_SERVICE)} - /> - <Button - testID="localDevServerBtn" - type="default" - label={_(msg`Dev Server`)} - onPress={() => onChangeServiceUrl(LOCAL_DEV_SERVICE)} + <StepHeader uiState={uiState} title={_(msg`Your account`)}> + <View> + <Button + testID="selectServiceButton" + type="default" + style={{ + aspectRatio: 1, + justifyContent: 'center', + alignItems: 'center', + }} + accessibilityLabel={_(msg`Select service`)} + accessibilityHint={_(msg`Sets server for the Bluesky client`)} + onPress={onPressSelectService}> + <FontAwesomeIcon icon="server" size={21} /> + </Button> + </View> + </StepHeader> + + {!uiState.serviceDescription ? ( + <ActivityIndicator /> + ) : ( + <> + {uiState.isInviteCodeRequired && ( + <View style={s.pb20}> + <Text type="md-medium" style={[pal.text, s.mb2]}> + <Trans>Invite code</Trans> + </Text> + <TextInput + testID="inviteCodeInput" + icon="ticket" + placeholder={_(msg`Required for this provider`)} + value={uiState.inviteCode} + editable + onChange={value => uiDispatch({type: 'set-invite-code', value})} + accessibilityLabel={_(msg`Invite code`)} + accessibilityHint={_(msg`Input invite code to proceed`)} + autoCapitalize="none" + autoComplete="off" + autoCorrect={false} + autoFocus={true} /> </View> )} - </View> - </Option> - {uiState.error ? ( - <ErrorMessage message={uiState.error} style={styles.error} /> - ) : ( - <HelpTip text={_(msg`You can change hosting providers at any time.`)} /> - )} - </View> - ) -} -function Option({ - children, - isSelected, - label, - help, - onPress, - testID, -}: React.PropsWithChildren<{ - isSelected: boolean - label: string - help?: string - onPress: () => void - testID?: string -}>) { - const theme = useTheme() - const pal = usePalette('default') - const {_} = useLingui() - const circleFillStyle = React.useMemo( - () => ({ - backgroundColor: theme.palette.primary.background, - }), - [theme], - ) - - return ( - <View style={[styles.option, pal.border]}> - <TouchableWithoutFeedback - onPress={onPress} - testID={testID} - accessibilityRole="button" - accessibilityLabel={label} - accessibilityHint={_(msg`Sets hosting provider to ${label}`)}> - <View style={styles.optionHeading}> - <View style={[styles.circle, pal.border]}> - {isSelected ? ( - <View style={[circleFillStyle, styles.circleFill]} /> - ) : undefined} - </View> - <Text type="xl" style={pal.text}> - {label} - {help ? ( - <Text type="xl" style={pal.textLight}> - {help} + {!uiState.inviteCode && uiState.isInviteCodeRequired ? ( + <View style={[s.flexRow, s.alignCenter]}> + <Text style={pal.text}> + <Trans>Don't have an invite code?</Trans>{' '} </Text> - ) : undefined} - </Text> - </View> - </TouchableWithoutFeedback> - {isSelected && children} + <TouchableWithoutFeedback + onPress={onPressWaitlist} + accessibilityLabel={_(msg`Join the waitlist.`)} + accessibilityHint=""> + <View style={styles.touchable}> + <Text style={pal.link}> + <Trans>Join the waitlist.</Trans> + </Text> + </View> + </TouchableWithoutFeedback> + </View> + ) : ( + <> + <View style={s.pb20}> + <Text + type="md-medium" + style={[pal.text, s.mb2]} + nativeID="email"> + <Trans>Email address</Trans> + </Text> + <TextInput + testID="emailInput" + icon="envelope" + placeholder={_(msg`Enter your email address`)} + value={uiState.email} + editable + onChange={value => uiDispatch({type: 'set-email', value})} + accessibilityLabel={_(msg`Email`)} + accessibilityHint={_(msg`Input email for Bluesky account`)} + accessibilityLabelledBy="email" + autoCapitalize="none" + autoComplete="off" + autoCorrect={false} + autoFocus={!uiState.isInviteCodeRequired} + /> + </View> + + <View style={s.pb20}> + <Text + type="md-medium" + style={[pal.text, s.mb2]} + nativeID="password"> + <Trans>Password</Trans> + </Text> + <TextInput + testID="passwordInput" + icon="lock" + placeholder={_(msg`Choose your password`)} + value={uiState.password} + editable + secureTextEntry + onChange={value => uiDispatch({type: 'set-password', value})} + accessibilityLabel={_(msg`Password`)} + accessibilityHint={_(msg`Set password`)} + accessibilityLabelledBy="password" + autoCapitalize="none" + autoComplete="off" + autoCorrect={false} + /> + </View> + + <View style={s.pb20}> + <Text + type="md-medium" + style={[pal.text, s.mb2]} + nativeID="birthDate"> + <Trans>Your birth date</Trans> + </Text> + <DateInput + handleAsUTC + testID="birthdayInput" + value={birthDate} + onChange={value => + uiDispatch({type: 'set-birth-date', value}) + } + buttonType="default-light" + buttonStyle={[pal.border, styles.dateInputButton]} + buttonLabelType="lg" + accessibilityLabel={_(msg`Birthday`)} + accessibilityHint={_(msg`Enter your birth date`)} + accessibilityLabelledBy="birthDate" + /> + </View> + + {uiState.serviceDescription && ( + <Policies + serviceDescription={uiState.serviceDescription} + needsGuardian={!is18(uiState)} + /> + )} + </> + )} + </> + )} + {uiState.error ? ( + <ErrorMessage message={uiState.error} style={styles.error} /> + ) : undefined} </View> ) } @@ -165,34 +217,15 @@ function Option({ const styles = StyleSheet.create({ error: { borderRadius: 6, + marginTop: 10, }, - - option: { + dateInputButton: { borderWidth: 1, borderRadius: 6, - marginBottom: 10, - }, - optionHeading: { - flexDirection: 'row', - alignItems: 'center', - padding: 10, + paddingVertical: 14, }, - circle: { - width: 26, - height: 26, - borderRadius: 15, - padding: 4, - borderWidth: 1, - marginRight: 10, - }, - circleFill: { - width: 16, - height: 16, - borderRadius: 10, - }, - - otherForm: { - paddingBottom: 10, - paddingHorizontal: 12, + // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. + touchable: { + ...(isWeb && {cursor: 'pointer'}), }, }) diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index 53e1e02c9..f938bb9ce 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -1,39 +1,28 @@ import React from 'react' -import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' -import {CreateAccountState, CreateAccountDispatch, is18} from './state' +import { + ActivityIndicator, + StyleSheet, + TouchableWithoutFeedback, + View, +} from 'react-native' +import { + CreateAccountState, + CreateAccountDispatch, + requestVerificationCode, +} from './state' import {Text} from 'view/com/util/text/Text' -import {DateInput} from 'view/com/util/forms/DateInput' import {StepHeader} from './StepHeader' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {TextInput} from '../util/TextInput' -import {Policies} from './Policies' +import {Button} from '../../util/forms/Button' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {isWeb} from 'platform/detection' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' -import {logger} from '#/logger' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import parsePhoneNumber from 'libphonenumber-js' -function sanitizeDate(date: Date): Date { - if (!date || date.toString() === 'Invalid Date') { - logger.error(`Create account: handled invalid date for birthDate`, { - hasDate: !!date, - }) - return new Date() - } - return date -} - -/** STEP 2: Your account - * @field Invite code or waitlist - * @field Email address - * @field Email address - * @field Email address - * @field Password - * @field Birth date - * @readonly Terms of service & privacy policy - */ export function Step2({ uiState, uiDispatch, @@ -43,130 +32,155 @@ export function Step2({ }) { const pal = usePalette('default') const {_} = useLingui() - const {openModal} = useModalControls() + const {isMobile} = useWebMediaQueries() - const onPressWaitlist = React.useCallback(() => { - openModal({name: 'waitlist'}) - }, [openModal]) + const onPressRequest = React.useCallback(() => { + if ( + uiState.verificationPhone.length >= 9 && + parsePhoneNumber(uiState.verificationPhone, 'US') + ) { + requestVerificationCode({uiState, uiDispatch, _}) + } else { + uiDispatch({ + type: 'set-error', + value: _( + msg`There's something wrong with this number. Please include your country and/or area code!`, + ), + }) + } + }, [uiState, uiDispatch, _]) - const birthDate = React.useMemo(() => { - return sanitizeDate(uiState.birthDate) - }, [uiState.birthDate]) + const onPressRetry = React.useCallback(() => { + uiDispatch({type: 'set-has-requested-verification-code', value: false}) + }, [uiDispatch]) + + const phoneNumberFormatted = React.useMemo( + () => + uiState.hasRequestedVerificationCode + ? parsePhoneNumber( + uiState.verificationPhone, + 'US', + )?.formatInternational() + : '', + [uiState.hasRequestedVerificationCode, uiState.verificationPhone], + ) return ( <View> - <StepHeader step="2" title={_(msg`Your account`)} /> + <StepHeader uiState={uiState} title={_(msg`SMS verification`)} /> - {uiState.isInviteCodeRequired && ( - <View style={s.pb20}> - <Text type="md-medium" style={[pal.text, s.mb2]}> - <Trans>Invite code</Trans> - </Text> - <TextInput - testID="inviteCodeInput" - icon="ticket" - placeholder={_(msg`Required for this provider`)} - value={uiState.inviteCode} - editable - onChange={value => uiDispatch({type: 'set-invite-code', value})} - accessibilityLabel={_(msg`Invite code`)} - accessibilityHint={_(msg`Input invite code to proceed`)} - autoCapitalize="none" - autoComplete="off" - autoCorrect={false} - /> - </View> - )} - - {!uiState.inviteCode && uiState.isInviteCodeRequired ? ( - <Text style={[s.alignBaseline, pal.text]}> - <Trans>Don't have an invite code?</Trans>{' '} - <TouchableWithoutFeedback - onPress={onPressWaitlist} - accessibilityLabel={_(msg`Join the waitlist.`)} - accessibilityHint=""> - <View style={styles.touchable}> - <Text style={pal.link}> - <Trans>Join the waitlist.</Trans> - </Text> - </View> - </TouchableWithoutFeedback> - </Text> - ) : ( + {!uiState.hasRequestedVerificationCode ? ( <> <View style={s.pb20}> - <Text type="md-medium" style={[pal.text, s.mb2]} nativeID="email"> - <Trans>Email address</Trans> + <Text + type="md-medium" + style={[pal.text, s.mb2]} + nativeID="phoneNumber"> + <Trans>Phone number</Trans> </Text> <TextInput - testID="emailInput" - icon="envelope" - placeholder={_(msg`Enter your email address`)} - value={uiState.email} + testID="phoneInput" + icon="phone" + placeholder={_(msg`Enter your phone number`)} + value={uiState.verificationPhone} editable - onChange={value => uiDispatch({type: 'set-email', value})} + onChange={value => + uiDispatch({type: 'set-verification-phone', value}) + } accessibilityLabel={_(msg`Email`)} - accessibilityHint={_(msg`Input email for Bluesky waitlist`)} - accessibilityLabelledBy="email" + accessibilityHint={_( + msg`Input phone number for SMS verification`, + )} + accessibilityLabelledBy="phoneNumber" + keyboardType="phone-pad" autoCapitalize="none" - autoComplete="off" + autoComplete="tel" autoCorrect={false} + autoFocus={true} /> + <Text type="sm" style={[pal.textLight, s.mt5]}> + <Trans> + Please enter a phone number that can receive SMS text messages. + </Trans> + </Text> </View> + <View style={isMobile ? {} : {flexDirection: 'row'}}> + {uiState.isProcessing ? ( + <ActivityIndicator /> + ) : ( + <Button + testID="requestCodeBtn" + type="primary" + label={_(msg`Request code`)} + labelStyle={isMobile ? [s.flex1, s.textCenter, s.f17] : []} + style={ + isMobile ? {paddingVertical: 12, paddingHorizontal: 20} : {} + } + onPress={onPressRequest} + /> + )} + </View> + </> + ) : ( + <> <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="password"> - <Trans>Password</Trans> - </Text> + <View + style={[ + s.flexRow, + s.mb5, + s.alignCenter, + {justifyContent: 'space-between'}, + ]}> + <Text + type="md-medium" + style={pal.text} + nativeID="verificationCode"> + <Trans>Verification code</Trans>{' '} + </Text> + <TouchableWithoutFeedback + onPress={onPressRetry} + accessibilityLabel={_(msg`Retry.`)} + accessibilityHint=""> + <View style={styles.touchable}> + <Text + type="md-medium" + style={pal.link} + nativeID="verificationCode"> + <Trans>Retry</Trans> + </Text> + </View> + </TouchableWithoutFeedback> + </View> <TextInput - testID="passwordInput" - icon="lock" - placeholder={_(msg`Choose your password`)} - value={uiState.password} + testID="codeInput" + icon="hashtag" + placeholder={_(msg`XXXXXX`)} + value={uiState.verificationCode} editable - secureTextEntry - onChange={value => uiDispatch({type: 'set-password', value})} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={_(msg`Set password`)} - accessibilityLabelledBy="password" + onChange={value => + uiDispatch({type: 'set-verification-code', value}) + } + accessibilityLabel={_(msg`Email`)} + accessibilityHint={_( + msg`Input the verification code we have texted to you`, + )} + accessibilityLabelledBy="verificationCode" + keyboardType="phone-pad" autoCapitalize="none" - autoComplete="off" + autoComplete="one-time-code" + textContentType="oneTimeCode" autoCorrect={false} + autoFocus={true} /> - </View> - - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="birthDate"> - <Trans>Your birth date</Trans> + <Text type="sm" style={[pal.textLight, s.mt5]}> + <Trans>Please enter the verification code sent to</Trans>{' '} + {phoneNumberFormatted}. </Text> - <DateInput - handleAsUTC - testID="birthdayInput" - value={birthDate} - onChange={value => uiDispatch({type: 'set-birth-date', value})} - buttonType="default-light" - buttonStyle={[pal.border, styles.dateInputButton]} - buttonLabelType="lg" - accessibilityLabel={_(msg`Birthday`)} - accessibilityHint={_(msg`Enter your birth date`)} - accessibilityLabelledBy="birthDate" - /> </View> - - {uiState.serviceDescription && ( - <Policies - serviceDescription={uiState.serviceDescription} - needsGuardian={!is18(uiState)} - /> - )} </> )} + {uiState.error ? ( <ErrorMessage message={uiState.error} style={styles.error} /> ) : undefined} @@ -179,11 +193,6 @@ const styles = StyleSheet.create({ borderRadius: 6, marginTop: 10, }, - dateInputButton: { - borderWidth: 1, - borderRadius: 6, - paddingVertical: 14, - }, // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. touchable: { ...(isWeb && {cursor: 'pointer'}), diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx index 2b2b9f7fe..bc7956da4 100644 --- a/src/view/com/auth/create/Step3.tsx +++ b/src/view/com/auth/create/Step3.tsx @@ -25,7 +25,7 @@ export function Step3({ const {_} = useLingui() return ( <View> - <StepHeader step="3" title={_(msg`Your user handle`)} /> + <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> <View style={s.pb10}> <TextInput testID="handleInput" diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx index 41f912051..af6bf5478 100644 --- a/src/view/com/auth/create/StepHeader.tsx +++ b/src/view/com/auth/create/StepHeader.tsx @@ -3,27 +3,42 @@ import {StyleSheet, View} from 'react-native' import {Text} from 'view/com/util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {Trans} from '@lingui/macro' +import {CreateAccountState} from './state' -export function StepHeader({step, title}: {step: string; title: string}) { +export function StepHeader({ + uiState, + title, + children, +}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) { const pal = usePalette('default') + const numSteps = uiState.isPhoneVerificationRequired ? 3 : 2 return ( <View style={styles.container}> - <Text type="lg" style={[pal.textLight]}> - {step === '3' ? ( - <Trans>Last step!</Trans> - ) : ( - <Trans>Step {step} of 3</Trans> - )} - </Text> - <Text style={[pal.text]} type="title-xl"> - {title} - </Text> + <View> + <Text type="lg" style={[pal.textLight]}> + {uiState.step === 3 ? ( + <Trans>Last step!</Trans> + ) : ( + <Trans> + Step {uiState.step} of {numSteps} + </Trans> + )} + </Text> + + <Text style={[pal.text]} type="title-xl"> + {title} + </Text> + </View> + {children} </View> ) } const styles = StyleSheet.create({ container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', marginBottom: 20, }, }) diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts index 62a8495b3..7e0310bb3 100644 --- a/src/view/com/auth/create/state.ts +++ b/src/view/com/auth/create/state.ts @@ -2,6 +2,7 @@ import {useReducer} from 'react' import { ComAtprotoServerDescribeServer, ComAtprotoServerCreateAccount, + BskyAgent, } from '@atproto/api' import {I18nContext, useLingui} from '@lingui/react' import {msg} from '@lingui/macro' @@ -13,6 +14,7 @@ import {cleanError} from '#/lib/strings/errors' import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding' import {ApiContext as SessionApiContext} from '#/state/session' import {DEFAULT_SERVICE} from '#/lib/constants' +import parsePhoneNumber from 'libphonenumber-js' export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago @@ -27,6 +29,9 @@ export type CreateAccountAction = | {type: 'set-invite-code'; value: string} | {type: 'set-email'; value: string} | {type: 'set-password'; value: string} + | {type: 'set-verification-phone'; value: string} + | {type: 'set-verification-code'; value: string} + | {type: 'set-has-requested-verification-code'; value: boolean} | {type: 'set-handle'; value: string} | {type: 'set-birth-date'; value: Date} | {type: 'next'} @@ -43,6 +48,9 @@ export interface CreateAccountState { inviteCode: string email: string password: string + verificationPhone: string + verificationCode: string + hasRequestedVerificationCode: boolean handle: string birthDate: Date @@ -50,6 +58,7 @@ export interface CreateAccountState { canBack: boolean canNext: boolean isInviteCodeRequired: boolean + isPhoneVerificationRequired: boolean } export type CreateAccountDispatch = (action: CreateAccountAction) => void @@ -66,15 +75,51 @@ export function useCreateAccount() { inviteCode: '', email: '', password: '', + verificationPhone: '', + verificationCode: '', + hasRequestedVerificationCode: false, handle: '', birthDate: DEFAULT_DATE, canBack: false, canNext: false, isInviteCodeRequired: false, + isPhoneVerificationRequired: false, }) } +export async function requestVerificationCode({ + uiState, + uiDispatch, + _, +}: { + uiState: CreateAccountState + uiDispatch: CreateAccountDispatch + _: I18nContext['_'] +}) { + const phoneNumber = parsePhoneNumber(uiState.verificationPhone, 'US')?.number + if (!phoneNumber) { + return + } + uiDispatch({type: 'set-error', value: ''}) + uiDispatch({type: 'set-processing', value: true}) + uiDispatch({type: 'set-verification-phone', value: phoneNumber}) + try { + const agent = new BskyAgent({service: uiState.serviceUrl}) + await agent.com.atproto.temp.requestPhoneVerification({ + phoneNumber, + }) + uiDispatch({type: 'set-has-requested-verification-code', value: true}) + } catch (e: any) { + logger.error( + `Failed to request sms verification code (${e.status} status)`, + {error: e}, + ) + uiDispatch({type: 'set-error', value: cleanError(e.toString())}) + } + uiDispatch({type: 'set-processing', value: false}) +} + export async function submit({ createAccount, onboardingDispatch, @@ -89,26 +134,36 @@ export async function submit({ _: I18nContext['_'] }) { if (!uiState.email) { - uiDispatch({type: 'set-step', value: 2}) + uiDispatch({type: 'set-step', value: 1}) return uiDispatch({ type: 'set-error', value: _(msg`Please enter your email.`), }) } if (!EmailValidator.validate(uiState.email)) { - uiDispatch({type: 'set-step', value: 2}) + uiDispatch({type: 'set-step', value: 1}) return uiDispatch({ type: 'set-error', value: _(msg`Your email appears to be invalid.`), }) } if (!uiState.password) { - uiDispatch({type: 'set-step', value: 2}) + uiDispatch({type: 'set-step', value: 1}) return uiDispatch({ type: 'set-error', value: _(msg`Please choose your password.`), }) } + if ( + uiState.isPhoneVerificationRequired && + (!uiState.verificationPhone || !uiState.verificationCode) + ) { + uiDispatch({type: 'set-step', value: 2}) + return uiDispatch({ + type: 'set-error', + value: _(msg`Please enter the code you received by SMS.`), + }) + } if (!uiState.handle) { uiDispatch({type: 'set-step', value: 3}) return uiDispatch({ @@ -127,6 +182,8 @@ export async function submit({ handle: createFullHandle(uiState.handle, uiState.userDomain), password: uiState.password, inviteCode: uiState.inviteCode.trim(), + verificationPhone: uiState.verificationPhone.trim(), + verificationCode: uiState.verificationCode.trim(), }) } catch (e: any) { onboardingDispatch({type: 'skip'}) // undo starting the onboard @@ -135,6 +192,9 @@ export async function submit({ errMsg = _( msg`Invite code not accepted. Check that you input it correctly and try again.`, ) + uiDispatch({type: 'set-step', value: 1}) + } else if (e.error === 'InvalidPhoneVerification') { + uiDispatch({type: 'set-step', value: 2}) } if ([400, 429].includes(e.status)) { @@ -201,6 +261,19 @@ function createReducer({_}: {_: I18nContext['_']}) { case 'set-password': { return compute({...state, password: action.value}) } + case 'set-verification-phone': { + return compute({ + ...state, + verificationPhone: action.value, + hasRequestedVerificationCode: false, + }) + } + case 'set-verification-code': { + return compute({...state, verificationCode: action.value.trim()}) + } + case 'set-has-requested-verification-code': { + return compute({...state, hasRequestedVerificationCode: action.value}) + } case 'set-handle': { return compute({...state, handle: action.value}) } @@ -208,7 +281,7 @@ function createReducer({_}: {_: I18nContext['_']}) { return compute({...state, birthDate: action.value}) } case 'next': { - if (state.step === 2) { + if (state.step === 1) { if (!is13(state)) { return compute({ ...state, @@ -218,10 +291,18 @@ function createReducer({_}: {_: I18nContext['_']}) { }) } } - return compute({...state, error: '', step: state.step + 1}) + let increment = 1 + if (state.step === 1 && !state.isPhoneVerificationRequired) { + increment = 2 + } + return compute({...state, error: '', step: state.step + increment}) } case 'back': { - return compute({...state, error: '', step: state.step - 1}) + let decrement = 1 + if (state.step === 3 && !state.isPhoneVerificationRequired) { + decrement = 2 + } + return compute({...state, error: '', step: state.step - decrement}) } } } @@ -230,12 +311,16 @@ function createReducer({_}: {_: I18nContext['_']}) { function compute(state: CreateAccountState): CreateAccountState { let canNext = true if (state.step === 1) { - canNext = !!state.serviceDescription - } else if (state.step === 2) { canNext = + !!state.serviceDescription && (!state.isInviteCodeRequired || !!state.inviteCode) && !!state.email && !!state.password + } else if (state.step === 2) { + canNext = + !state.isPhoneVerificationRequired || + (!!state.verificationPhone && + isValidVerificationCode(state.verificationCode)) } else if (state.step === 3) { canNext = !!state.handle } @@ -244,5 +329,11 @@ function compute(state: CreateAccountState): CreateAccountState { canBack: state.step > 1, canNext, isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired, + isPhoneVerificationRequired: + !!state.serviceDescription?.phoneVerificationRequired, } } + +function isValidVerificationCode(str: string): boolean { + return /[0-9]{6}/.test(str) +} diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 2271bb9fb..ee096b0d2 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Pressable, StyleSheet, View} from 'react-native' +import {StyleSheet, View, Pressable} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import ImageView from './ImageViewing' import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip' @@ -107,12 +107,16 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) { {altText ? ( <Pressable onPress={() => setAltExpanded(!isAltExpanded)} + onLongPress={() => {}} accessibilityRole="button"> - <Text - style={[s.gray3, styles.footerText]} - numberOfLines={isAltExpanded ? undefined : 3}> - {altText} - </Text> + <View> + <Text + selectable + style={[s.gray3, styles.footerText]} + numberOfLines={isAltExpanded ? undefined : 3}> + {altText} + </Text> + </View> </Pressable> ) : null} <View style={styles.footerBtns}> diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx index bd1eb3393..77a1debec 100644 --- a/src/view/com/modals/CreateOrEditList.tsx +++ b/src/view/com/modals/CreateOrEditList.tsx @@ -182,19 +182,17 @@ export function Component({ ]} testID="createOrEditListModal"> <Text style={[styles.title, pal.text]}> - <Trans> - {isCurateList ? ( - list ? ( - <Trans>Edit User List</Trans> - ) : ( - <Trans>New User List</Trans> - ) - ) : list ? ( - <Trans>Edit Moderation List</Trans> + {isCurateList ? ( + list ? ( + <Trans>Edit User List</Trans> ) : ( - <Trans>New Moderation List</Trans> - )} - </Trans> + <Trans>New User List</Trans> + ) + ) : list ? ( + <Trans>Edit Moderation List</Trans> + ) : ( + <Trans>New Moderation List</Trans> + )} </Text> {error !== '' && ( <View style={styles.errorContainer}> diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 2c5ba5dfb..9c562f67d 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -20,6 +20,11 @@ import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {Logo} from '#/view/icons/Logo' +import {IS_DEV} from '#/env' +import {atoms} from '#/alf' +import {Link as Link2} from '#/components/Link' +import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' + export function FeedsTabBar( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { @@ -68,7 +73,7 @@ export function FeedsTabBar( headerHeight.value = e.nativeEvent.layout.height }}> <View style={[pal.view, styles.topBar]}> - <View style={[pal.view]}> + <View style={[pal.view, {width: 100}]}> <TouchableOpacity testID="viewHeaderDrawerBtn" onPress={onPressAvi} @@ -88,7 +93,21 @@ export function FeedsTabBar( <View> <Logo width={30} /> </View> - <View style={[pal.view, {width: 18}]}> + <View + style={[ + atoms.flex_row, + atoms.justify_end, + atoms.align_center, + atoms.gap_md, + pal.view, + {width: 100}, + ]}> + {IS_DEV && ( + <Link2 to="/sys/debug"> + <ColorPalette size="md" /> + </Link2> + )} + {hasSession && ( <Link testID="viewHeaderHomeFeedPrefsBtn" diff --git a/src/view/com/posts/DiscoverFallbackHeader.tsx b/src/view/com/posts/DiscoverFallbackHeader.tsx index dcfa3b012..ffde89997 100644 --- a/src/view/com/posts/DiscoverFallbackHeader.tsx +++ b/src/view/com/posts/DiscoverFallbackHeader.tsx @@ -27,15 +27,15 @@ export function DiscoverFallbackHeader() { <View style={{flex: 1}}> <Text type="md" style={pal.text}> <Trans> - We ran out of posts from your follows. Here's the latest from - </Trans>{' '} - <TextLink - type="md-medium" - href="/profile/bsky.app/feed/whats-hot" - text="Discover" - style={pal.link} - /> - . + We ran out of posts from your follows. Here's the latest from{' '} + <TextLink + type="md-medium" + href="/profile/bsky.app/feed/whats-hot" + text="Discover" + style={pal.link} + /> + . + </Trans> </Text> </View> </View> diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 4f898767d..db26258d6 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -306,6 +306,8 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ ) }) +const EXEMPT_PATHS = ['/robots.txt', '/security.txt', '/.well-known/'] + // NOTE // we can't use the onPress given by useLinkProps because it will // match most paths to the HomeTab routes while we actually want to @@ -350,7 +352,12 @@ function onPressInner( if (shouldHandle) { href = convertBskyAppUrlIfNeeded(href) - if (newTab || href.startsWith('http') || href.startsWith('mailto')) { + if ( + newTab || + href.startsWith('http') || + href.startsWith('mailto') || + EXEMPT_PATHS.some(path => href.startsWith(path)) + ) { openLink(href) } else { closeModal() // close any active modals diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 00a102e7b..6f168a293 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -22,7 +22,6 @@ import {Link} from '../Link' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed' import {AutoSizedImage} from '../images/AutoSizedImage' @@ -51,7 +50,6 @@ export function PostEmbeds({ }) { const pal = usePalette('default') const {openLightbox} = useLightboxControls() - const {isMobile} = useWebMediaQueries() // quote post with media // = @@ -129,10 +127,7 @@ export function PostEmbeds({ dimensionsHint={aspectRatio} onPress={() => _openLightbox(0)} onPressIn={() => onPressIn(0)} - style={[ - styles.singleImage, - isMobile && styles.singleImageMobile, - ]}> + style={[styles.singleImage]}> {alt === '' ? null : ( <View style={styles.altContainer}> <Text style={styles.alt} accessible={false}> @@ -151,11 +146,7 @@ export function PostEmbeds({ images={embed.images} onPress={_openLightbox} onPressIn={onPressIn} - style={ - embed.images.length === 1 - ? [styles.singleImage, isMobile && styles.singleImageMobile] - : undefined - } + style={embed.images.length === 1 ? [styles.singleImage] : undefined} /> </View> ) @@ -188,10 +179,6 @@ const styles = StyleSheet.create({ }, singleImage: { borderRadius: 8, - maxHeight: 1000, - }, - singleImageMobile: { - maxHeight: 500, }, extOuter: { borderWidth: 1, diff --git a/src/view/icons/Logo.tsx b/src/view/icons/Logo.tsx index 15ab5a11c..9212381a9 100644 --- a/src/view/icons/Logo.tsx +++ b/src/view/icons/Logo.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {StyleSheet, TextProps} from 'react-native' import Svg, { Path, Defs, @@ -14,12 +15,14 @@ const ratio = 57 / 64 type Props = { fill?: PathProps['fill'] -} & SvgProps + style?: TextProps['style'] +} & Omit<SvgProps, 'style'> export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) { const {fill, ...rest} = props const gradient = fill === 'sky' - const _fill = gradient ? 'url(#sky)' : fill || colors.blue3 + const styles = StyleSheet.flatten(props.style) + const _fill = gradient ? 'url(#sky)' : fill || styles?.color || colors.blue3 // @ts-ignore it's fiiiiine const size = parseInt(rest.width || 32) return ( @@ -29,7 +32,7 @@ export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) { ref={ref} viewBox="0 0 64 57" {...rest} - style={{width: size, height: size * ratio}}> + style={[{width: size, height: size * ratio}, styles]}> {gradient && ( <Defs> <LinearGradient id="sky" x1="0" y1="0" x2="0" y2="1"> diff --git a/src/view/icons/index.tsx b/src/view/icons/index.tsx index 221b9702c..be139d2f2 100644 --- a/src/view/icons/index.tsx +++ b/src/view/icons/index.tsx @@ -52,6 +52,7 @@ import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' import {faHand} from '@fortawesome/free-solid-svg-icons/faHand' import {faHand as farHand} from '@fortawesome/free-regular-svg-icons/faHand' +import {faHashtag} from '@fortawesome/free-solid-svg-icons/faHashtag' import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse' @@ -71,6 +72,7 @@ import {faPaste} from '@fortawesome/free-regular-svg-icons/faPaste' import {faPen} from '@fortawesome/free-solid-svg-icons/faPen' import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' +import {faPhone} from '@fortawesome/free-solid-svg-icons/faPhone' import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft' @@ -78,6 +80,7 @@ import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' import {faSatelliteDish} from '@fortawesome/free-solid-svg-icons/faSatelliteDish' +import {faServer} from '@fortawesome/free-solid-svg-icons/faServer' import {faShare} from '@fortawesome/free-solid-svg-icons/faShare' import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' @@ -153,6 +156,7 @@ library.add( faGlobe, faHand, farHand, + faHashtag, faHeart, fasHeart, faHouse, @@ -172,6 +176,7 @@ library.add( faPen, faPenNib, faPenToSquare, + faPhone, faPlay, faPlus, faQuoteLeft, @@ -179,6 +184,7 @@ library.add( faRetweet, faRss, faSatelliteDish, + faServer, faShare, faShareFromSquare, faShield, diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx deleted file mode 100644 index 0b7c5f03b..000000000 --- a/src/view/screens/DebugNew.tsx +++ /dev/null @@ -1,541 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {CenteredView, ScrollView} from '#/view/com/util/Views' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' - -import {useSetColorMode} from '#/state/shell' -import * as tokens from '#/alf/tokens' -import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf' -import {Button, ButtonText} from '#/view/com/Button' -import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' - -function ThemeSelector() { - const setColorMode = useSetColorMode() - - return ( - <View style={[a.flex_row, a.gap_md]}> - <Button - type="secondary" - size="small" - onPress={() => setColorMode('system')}> - System - </Button> - <Button - type="secondary" - size="small" - onPress={() => setColorMode('light')}> - Light - </Button> - <Button - type="secondary" - size="small" - onPress={() => setColorMode('dark')}> - Dark - </Button> - </View> - ) -} - -function BreakpointDebugger() { - const t = useTheme() - const breakpoints = useBreakpoints() - - return ( - <View> - <H3 style={[a.pb_md]}>Breakpoint Debugger</H3> - <Text style={[a.pb_md]}> - Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>} - {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>} - {breakpoints.gtTablet && <Text>desktop</Text>} - </Text> - <Text - style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}> - {JSON.stringify(breakpoints, null, 2)} - </Text> - </View> - ) -} - -function ThemedSection() { - const t = useTheme() - - return ( - <View style={[t.atoms.bg, a.gap_md, a.p_xl]}> - <H3 style={[a.font_bold]}>theme.atoms.text</H3> - <View style={[a.flex_1, t.atoms.border, a.border_t]} /> - <H3 style={[a.font_bold, t.atoms.text_contrast_700]}> - theme.atoms.text_contrast_700 - </H3> - <View style={[a.flex_1, t.atoms.border, a.border_t]} /> - <H3 style={[a.font_bold, t.atoms.text_contrast_500]}> - theme.atoms.text_contrast_500 - </H3> - <View style={[a.flex_1, t.atoms.border_contrast_500, a.border_t]} /> - - <View style={[a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_1, - t.atoms.bg, - a.align_center, - a.justify_center, - {height: 60}, - ]}> - <Text>theme.bg</Text> - </View> - <View - style={[ - a.flex_1, - t.atoms.bg_contrast_100, - a.align_center, - a.justify_center, - {height: 60}, - ]}> - <Text>theme.bg_contrast_100</Text> - </View> - </View> - <View style={[a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_1, - t.atoms.bg_contrast_200, - a.align_center, - a.justify_center, - {height: 60}, - ]}> - <Text>theme.bg_contrast_200</Text> - </View> - <View - style={[ - a.flex_1, - t.atoms.bg_contrast_300, - a.align_center, - a.justify_center, - {height: 60}, - ]}> - <Text>theme.bg_contrast_300</Text> - </View> - </View> - <View style={[a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_1, - t.atoms.bg_positive, - a.align_center, - a.justify_center, - {height: 60}, - ]}> - <Text>theme.bg_positive</Text> - </View> - <View - style={[ - a.flex_1, - t.atoms.bg_negative, - a.align_center, - a.justify_center, - {height: 60}, - ]}> - <Text>theme.bg_negative</Text> - </View> - </View> - </View> - ) -} - -export function DebugScreen() { - const t = useTheme() - - return ( - <ScrollView> - <CenteredView style={[t.atoms.bg]}> - <View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}> - <ThemeSelector /> - - <Alf theme="light"> - <ThemedSection /> - </Alf> - <Alf theme="dark"> - <ThemedSection /> - </Alf> - - <H1>Heading 1</H1> - <H2>Heading 2</H2> - <H3>Heading 3</H3> - <H4>Heading 4</H4> - <H5>Heading 5</H5> - <H6>Heading 6</H6> - - <Text style={[a.text_xxl]}>atoms.text_xxl</Text> - <Text style={[a.text_xl]}>atoms.text_xl</Text> - <Text style={[a.text_lg]}>atoms.text_lg</Text> - <Text style={[a.text_md]}>atoms.text_md</Text> - <Text style={[a.text_sm]}>atoms.text_sm</Text> - <Text style={[a.text_xs]}>atoms.text_xs</Text> - <Text style={[a.text_xxs]}>atoms.text_xxs</Text> - - <View style={[a.gap_md, a.align_start]}> - <Button> - {({state}) => ( - <View style={[a.p_md, a.rounded_full, t.atoms.bg_contrast_300]}> - <Text>Unstyled button, state: {JSON.stringify(state)}</Text> - </View> - )} - </Button> - - <Button type="primary" size="small"> - Button - </Button> - <Button type="secondary" size="small"> - Button - </Button> - - <Button type="primary" size="large"> - Button - </Button> - <Button type="secondary" size="large"> - Button - </Button> - - <Button type="secondary" size="small"> - {({type, size}) => ( - <> - <FontAwesomeIcon icon={['fas', 'plus']} size={12} /> - <ButtonText type={type} size={size}> - With an icon - </ButtonText> - </> - )} - </Button> - <Button type="primary" size="large"> - {({state: _state, ...rest}) => ( - <> - <FontAwesomeIcon icon={['fas', 'plus']} /> - <ButtonText {...rest}>With an icon</ButtonText> - </> - )} - </Button> - </View> - - <View style={[a.gap_md]}> - <View style={[a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_0}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_100}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_200}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_300}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_400}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_500}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_600}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_700}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_800}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_900}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.gray_1000}, - ]} - /> - </View> - - <View style={[a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_0}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_100}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_200}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_300}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_400}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_500}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_600}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_700}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_800}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_900}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.blue_1000}, - ]} - /> - </View> - <View style={[a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_0}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_100}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_200}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_300}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_400}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_500}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_600}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_700}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_800}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_900}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.green_1000}, - ]} - /> - </View> - <View style={[a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_0}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_100}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_200}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_300}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_400}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_500}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_600}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_700}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_800}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_900}, - ]} - /> - <View - style={[ - a.flex_1, - {height: 60, backgroundColor: tokens.color.red_1000}, - ]} - /> - </View> - </View> - - <View> - <H3 style={[a.pb_md, a.font_bold]}>Spacing</H3> - - <View style={[a.gap_md]}> - <View style={[a.flex_row, a.align_center]}> - <Text style={{width: 80}}>xxs (2px)</Text> - <View style={[a.flex_1, a.pt_xxs, t.atoms.bg_contrast_300]} /> - </View> - - <View style={[a.flex_row, a.align_center]}> - <Text style={{width: 80}}>xs (4px)</Text> - <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} /> - </View> - - <View style={[a.flex_row, a.align_center]}> - <Text style={{width: 80}}>sm (8px)</Text> - <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} /> - </View> - - <View style={[a.flex_row, a.align_center]}> - <Text style={{width: 80}}>md (12px)</Text> - <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} /> - </View> - - <View style={[a.flex_row, a.align_center]}> - <Text style={{width: 80}}>lg (18px)</Text> - <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} /> - </View> - - <View style={[a.flex_row, a.align_center]}> - <Text style={{width: 80}}>xl (24px)</Text> - <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} /> - </View> - - <View style={[a.flex_row, a.align_center]}> - <Text style={{width: 80}}>xxl (32px)</Text> - <View style={[a.flex_1, a.pt_xxl, t.atoms.bg_contrast_300]} /> - </View> - </View> - </View> - - <BreakpointDebugger /> - </View> - </CenteredView> - </ScrollView> - ) -} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 02a631fa0..612eb5cc1 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -87,9 +87,7 @@ function EmptyState({message, error}: {message: string; error?: string}) { }, ]}> <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> - <Text style={[pal.text]}> - <Trans>{message}</Trans> - </Text> + <Text style={[pal.text]}>{message}</Text> {error && ( <> diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 1f117b45b..3b50c5449 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -291,7 +291,7 @@ export function SettingsScreen({}: Props) { ]}> <View style={{flex: 1}}> <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> - <Trans>{_(msg`Settings`)}</Trans> + <Trans>Settings</Trans> </Text> </View> </SimpleViewHeader> diff --git a/src/view/screens/Storybook/Breakpoints.tsx b/src/view/screens/Storybook/Breakpoints.tsx new file mode 100644 index 000000000..1b846d517 --- /dev/null +++ b/src/view/screens/Storybook/Breakpoints.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme, useBreakpoints} from '#/alf' +import {Text, H3} from '#/components/Typography' + +export function Breakpoints() { + const t = useTheme() + const breakpoints = useBreakpoints() + + return ( + <View> + <H3 style={[a.pb_md]}>Breakpoint Debugger</H3> + <Text style={[a.pb_md]}> + Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>} + {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>} + {breakpoints.gtTablet && <Text>desktop</Text>} + </Text> + <Text + style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}> + {JSON.stringify(breakpoints, null, 2)} + </Text> + </View> + ) +} diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx new file mode 100644 index 000000000..fbdc84eb4 --- /dev/null +++ b/src/view/screens/Storybook/Buttons.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import { + Button, + ButtonVariant, + ButtonColor, + ButtonIcon, + ButtonText, +} from '#/components/Button' +import {H1} from '#/components/Typography' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' + +export function Buttons() { + return ( + <View style={[a.gap_md]}> + <H1>Buttons</H1> + + <View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}> + {['primary', 'secondary', 'negative'].map(color => ( + <View key={color} style={[a.gap_md, a.align_start]}> + {['solid', 'outline', 'ghost'].map(variant => ( + <React.Fragment key={variant}> + <Button + variant={variant as ButtonVariant} + color={color as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + <Button + disabled + variant={variant as ButtonVariant} + color={color as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + </React.Fragment> + ))} + </View> + ))} + + <View style={[a.flex_row, a.gap_md, a.align_start]}> + <View style={[a.gap_md, a.align_start]}> + {['gradient_sky', 'gradient_midnight', 'gradient_sunrise'].map( + name => ( + <React.Fragment key={name}> + <Button + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + <Button + disabled + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + </React.Fragment> + ), + )} + </View> + <View style={[a.gap_md, a.align_start]}> + {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map( + name => ( + <React.Fragment key={name}> + <Button + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + <Button + disabled + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + </React.Fragment> + ), + )} + </View> + </View> + + <Button + variant="gradient" + color="gradient_sky" + size="large" + label="Link out"> + <ButtonText>Link out</ButtonText> + <ButtonIcon icon={ArrowTopRight} /> + </Button> + + <Button + variant="gradient" + color="gradient_sky" + size="small" + label="Link out"> + <ButtonText>Link out</ButtonText> + <ButtonIcon icon={ArrowTopRight} /> + </Button> + + <Button + variant="gradient" + color="gradient_sky" + size="small" + label="Link out"> + <ButtonIcon icon={Globe} /> + <ButtonText>See the world</ButtonText> + </Button> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx new file mode 100644 index 000000000..db568c6bd --- /dev/null +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -0,0 +1,90 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Button} from '#/components/Button' +import {H3, P} from '#/components/Typography' +import * as Dialog from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' +import {useDialogStateControlContext} from '#/state/dialogs' + +export function Dialogs() { + const control = Dialog.useDialogControl() + const prompt = Prompt.usePromptControl() + const {closeAllDialogs} = useDialogStateControlContext() + + return ( + <View style={[a.gap_md]}> + <Button + variant="outline" + color="secondary" + size="small" + onPress={() => { + control.open() + prompt.open() + }} + label="Open basic dialog"> + Open basic dialog + </Button> + + <Button + variant="solid" + color="primary" + size="small" + onPress={() => prompt.open()} + label="Open prompt"> + Open prompt + </Button> + + <Prompt.Outer control={prompt}> + <Prompt.Title>This is a prompt</Prompt.Title> + <Prompt.Description> + This is a generic prompt component. It accepts a title and a + description, as well as two actions. + </Prompt.Description> + <Prompt.Actions> + <Prompt.Cancel>Cancel</Prompt.Cancel> + <Prompt.Action>Confirm</Prompt.Action> + </Prompt.Actions> + </Prompt.Outer> + + <Dialog.Outer + control={control} + nativeOptions={{sheet: {snapPoints: ['90%']}}}> + <Dialog.Handle /> + + <Dialog.ScrollableInner + accessibilityDescribedBy="dialog-description" + accessibilityLabelledBy="dialog-title"> + <View style={[a.relative, a.gap_md, a.w_full]}> + <H3 nativeID="dialog-title">Dialog</H3> + <P nativeID="dialog-description"> + A scrollable dialog with an input within it. + </P> + <Dialog.Input value="" onChangeText={() => {}} label="Type here" /> + + <Button + variant="outline" + color="secondary" + size="small" + onPress={closeAllDialogs} + label="Close all dialogs"> + Close all dialogs + </Button> + <View style={{height: 1000}} /> + <View style={[a.flex_row, a.justify_end]}> + <Button + variant="outline" + color="primary" + size="small" + onPress={() => control.close()} + label="Open basic dialog"> + Close basic dialog + </Button> + </View> + </View> + </Dialog.ScrollableInner> + </Dialog.Outer> + </View> + ) +} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx new file mode 100644 index 000000000..9396cca67 --- /dev/null +++ b/src/view/screens/Storybook/Forms.tsx @@ -0,0 +1,215 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {H1, H3} from '#/components/Typography' +import * as TextField from '#/components/forms/TextField' +import {DateField, Label} from '#/components/forms/DateField' +import * as Toggle from '#/components/forms/Toggle' +import * as ToggleButton from '#/components/forms/ToggleButton' +import {Button} from '#/components/Button' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' + +export function Forms() { + const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) + const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b']) + const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b']) + const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn']) + + const [value, setValue] = React.useState('') + const [date, setDate] = React.useState('2001-01-01') + + return ( + <View style={[a.gap_4xl, a.align_start]}> + <H1>Forms</H1> + + <View style={[a.gap_md, a.align_start, a.w_full]}> + <H3>InputText</H3> + + <TextField.Input + value={value} + onChangeText={setValue} + label="Text field" + /> + + <TextField.Root> + <TextField.Icon icon={Globe} /> + <TextField.Input + value={value} + onChangeText={setValue} + label="Text field" + /> + </TextField.Root> + + <View style={[a.w_full]}> + <TextField.Label>Text field</TextField.Label> + <TextField.Root> + <TextField.Icon icon={Globe} /> + <TextField.Input + value={value} + onChangeText={setValue} + label="Text field" + /> + <TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix> + </TextField.Root> + </View> + + <View style={[a.w_full]}> + <TextField.Label>Textarea</TextField.Label> + <TextField.Input + multiline + numberOfLines={4} + value={value} + onChangeText={setValue} + label="Text field" + /> + </View> + + <H3>DateField</H3> + + <View style={[a.w_full]}> + <Label>Date</Label> + <DateField + testID="date" + value={date} + onChangeDate={date => { + console.log(date) + setDate(date) + }} + label="Input" + /> + </View> + </View> + + <View style={[a.gap_md, a.align_start, a.w_full]}> + <H3>Toggles</H3> + + <Toggle.Item name="a" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Uncontrolled toggle</Toggle.Label> + </Toggle.Item> + + <Toggle.Group + label="Toggle" + type="checkbox" + maxSelections={2} + values={toggleGroupAValues} + onChange={setToggleGroupAValues}> + <View style={[a.gap_md]}> + <Toggle.Item name="a" label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="b" label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="c" label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="d" disabled label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="e" isInvalid label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + </View> + </Toggle.Group> + + <Toggle.Group + label="Toggle" + type="checkbox" + maxSelections={2} + values={toggleGroupBValues} + onChange={setToggleGroupBValues}> + <View style={[a.gap_md]}> + <Toggle.Item name="a" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="b" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="c" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="d" disabled label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="e" isInvalid label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + </View> + </Toggle.Group> + + <Toggle.Group + label="Toggle" + type="radio" + values={toggleGroupCValues} + onChange={setToggleGroupCValues}> + <View style={[a.gap_md]}> + <Toggle.Item name="a" label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="b" label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="c" label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="d" disabled label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="e" isInvalid label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + </View> + </Toggle.Group> + </View> + + <Button + variant="gradient" + color="gradient_nordic" + size="small" + label="Reset all toggles" + onPress={() => { + setToggleGroupAValues(['a']) + setToggleGroupBValues(['a', 'b']) + setToggleGroupCValues(['a']) + }}> + Reset all toggles + </Button> + + <View style={[a.gap_md, a.align_start, a.w_full]}> + <H3>ToggleButton</H3> + + <ToggleButton.Group + label="Preferences" + values={toggleGroupDValues} + onChange={setToggleGroupDValues}> + <ToggleButton.Button name="hide" label="Hide"> + Hide + </ToggleButton.Button> + <ToggleButton.Button name="warn" label="Warn"> + Warn + </ToggleButton.Button> + <ToggleButton.Button name="show" label="Show"> + Show + </ToggleButton.Button> + </ToggleButton.Group> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx new file mode 100644 index 000000000..73466e077 --- /dev/null +++ b/src/view/screens/Storybook/Icons.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {H1} from '#/components/Typography' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' + +export function Icons() { + const t = useTheme() + return ( + <View style={[a.gap_md]}> + <H1>Icons</H1> + + <View style={[a.flex_row, a.gap_xl]}> + <Globe size="xs" fill={t.atoms.text.color} /> + <Globe size="sm" fill={t.atoms.text.color} /> + <Globe size="md" fill={t.atoms.text.color} /> + <Globe size="lg" fill={t.atoms.text.color} /> + <Globe size="xl" fill={t.atoms.text.color} /> + </View> + + <View style={[a.flex_row, a.gap_xl]}> + <ArrowTopRight size="xs" fill={t.atoms.text.color} /> + <ArrowTopRight size="sm" fill={t.atoms.text.color} /> + <ArrowTopRight size="md" fill={t.atoms.text.color} /> + <ArrowTopRight size="lg" fill={t.atoms.text.color} /> + <ArrowTopRight size="xl" fill={t.atoms.text.color} /> + </View> + + <View style={[a.flex_row, a.gap_xl]}> + <CalendarDays size="xs" fill={t.atoms.text.color} /> + <CalendarDays size="sm" fill={t.atoms.text.color} /> + <CalendarDays size="md" fill={t.atoms.text.color} /> + <CalendarDays size="lg" fill={t.atoms.text.color} /> + <CalendarDays size="xl" fill={t.atoms.text.color} /> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx new file mode 100644 index 000000000..c3b1c0e0f --- /dev/null +++ b/src/view/screens/Storybook/Links.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {ButtonText} from '#/components/Button' +import {Link} from '#/components/Link' +import {H1, H3} from '#/components/Typography' + +export function Links() { + return ( + <View style={[a.gap_md, a.align_start]}> + <H1>Links</H1> + + <View style={[a.gap_md, a.align_start]}> + <Link + to="https://blueskyweb.xyz" + warnOnMismatchingTextChild + style={[a.text_md]}> + External + </Link> + <Link to="https://blueskyweb.xyz" style={[a.text_md]}> + <H3>External with custom children</H3> + </Link> + <Link + to="https://blueskyweb.xyz" + warnOnMismatchingTextChild + style={[a.text_lg]}> + https://blueskyweb.xyz + </Link> + <Link + to="https://bsky.app/profile/bsky.app" + warnOnMismatchingTextChild + style={[a.text_md]}> + Internal + </Link> + + <Link + variant="solid" + color="primary" + size="large" + label="View @bsky.app's profile" + to="https://bsky.app/profile/bsky.app"> + <ButtonText>Link as a button</ButtonText> + </Link> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Palette.tsx b/src/view/screens/Storybook/Palette.tsx new file mode 100644 index 000000000..b521fe860 --- /dev/null +++ b/src/view/screens/Storybook/Palette.tsx @@ -0,0 +1,336 @@ +import React from 'react' +import {View} from 'react-native' + +import * as tokens from '#/alf/tokens' +import {atoms as a} from '#/alf' + +export function Palette() { + return ( + <View style={[a.gap_md]}> + <View style={[a.flex_row, a.gap_md]}> + <View + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.gray_0}]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_25}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_50}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_975}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_1000}, + ]} + /> + </View> + + <View style={[a.flex_row, a.gap_md]}> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_25}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_50}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_975}, + ]} + /> + </View> + <View style={[a.flex_row, a.gap_md]}> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_25}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_50}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_975}, + ]} + /> + </View> + <View style={[a.flex_row, a.gap_md]}> + <View + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]} + /> + <View + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_975}, + ]} + /> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Shadows.tsx b/src/view/screens/Storybook/Shadows.tsx new file mode 100644 index 000000000..f92112395 --- /dev/null +++ b/src/view/screens/Storybook/Shadows.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {H1, Text} from '#/components/Typography' + +export function Shadows() { + const t = useTheme() + + return ( + <View style={[a.gap_md]}> + <H1>Shadows</H1> + + <View style={[a.flex_row, a.gap_5xl]}> + <View + style={[ + a.flex_1, + a.justify_center, + a.px_lg, + a.py_2xl, + t.atoms.bg, + t.atoms.shadow_sm, + ]}> + <Text>shadow_sm</Text> + </View> + + <View + style={[ + a.flex_1, + a.justify_center, + a.px_lg, + a.py_2xl, + t.atoms.bg, + t.atoms.shadow_md, + ]}> + <Text>shadow_md</Text> + </View> + + <View + style={[ + a.flex_1, + a.justify_center, + a.px_lg, + a.py_2xl, + t.atoms.bg, + t.atoms.shadow_lg, + ]}> + <Text>shadow_lg</Text> + </View> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Spacing.tsx b/src/view/screens/Storybook/Spacing.tsx new file mode 100644 index 000000000..d7faf93a8 --- /dev/null +++ b/src/view/screens/Storybook/Spacing.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Text, H1} from '#/components/Typography' + +export function Spacing() { + const t = useTheme() + return ( + <View style={[a.gap_md]}> + <H1>Spacing</H1> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>2xs (2px)</Text> + <View style={[a.flex_1, a.pt_2xs, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>xs (4px)</Text> + <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>sm (8px)</Text> + <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>md (12px)</Text> + <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>lg (16px)</Text> + <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>xl (20px)</Text> + <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>2xl (24px)</Text> + <View style={[a.flex_1, a.pt_2xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>3xl (28px)</Text> + <View style={[a.flex_1, a.pt_3xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>4xl (32px)</Text> + <View style={[a.flex_1, a.pt_4xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>5xl (40px)</Text> + <View style={[a.flex_1, a.pt_5xl, t.atoms.bg_contrast_300]} /> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Theming.tsx b/src/view/screens/Storybook/Theming.tsx new file mode 100644 index 000000000..a05443473 --- /dev/null +++ b/src/view/screens/Storybook/Theming.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import {Palette} from './Palette' + +export function Theming() { + const t = useTheme() + + return ( + <View style={[t.atoms.bg, a.gap_lg, a.p_xl]}> + <Palette /> + + <Text style={[a.font_bold, a.pt_xl, a.px_md]}>theme.atoms.text</Text> + + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> + <Text style={[a.font_bold, t.atoms.text_contrast_600, a.px_md]}> + theme.atoms.text_contrast_600 + </Text> + + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> + <Text style={[a.font_bold, t.atoms.text_contrast_500, a.px_md]}> + theme.atoms.text_contrast_500 + </Text> + + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> + <Text style={[a.font_bold, t.atoms.text_contrast_400, a.px_md]}> + theme.atoms.text_contrast_400 + </Text> + + <View style={[a.flex_1, t.atoms.border_contrast, a.border_t]} /> + + <View style={[a.w_full, a.gap_md]}> + <View style={[t.atoms.bg, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg</Text> + </View> + <View style={[t.atoms.bg_contrast_25, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_25</Text> + </View> + <View style={[t.atoms.bg_contrast_50, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_50</Text> + </View> + <View style={[t.atoms.bg_contrast_100, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_100</Text> + </View> + <View style={[t.atoms.bg_contrast_200, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_200</Text> + </View> + <View style={[t.atoms.bg_contrast_300, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_300</Text> + </View> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx new file mode 100644 index 000000000..2e1f04a66 --- /dev/null +++ b/src/view/screens/Storybook/Typography.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography' + +export function Typography() { + return ( + <View style={[a.gap_md]}> + <H1>H1 Heading</H1> + <H2>H2 Heading</H2> + <H3>H3 Heading</H3> + <H4>H4 Heading</H4> + <H5>H5 Heading</H5> + <H6>H6 Heading</H6> + <P>P Paragraph</P> + + <Text style={[a.text_5xl]}>atoms.text_5xl</Text> + <Text style={[a.text_4xl]}>atoms.text_4xl</Text> + <Text style={[a.text_3xl]}>atoms.text_3xl</Text> + <Text style={[a.text_2xl]}>atoms.text_2xl</Text> + <Text style={[a.text_xl]}>atoms.text_xl</Text> + <Text style={[a.text_lg]}>atoms.text_lg</Text> + <Text style={[a.text_md]}>atoms.text_md</Text> + <Text style={[a.text_sm]}>atoms.text_sm</Text> + <Text style={[a.text_xs]}>atoms.text_xs</Text> + <Text style={[a.text_2xs]}>atoms.text_2xs</Text> + </View> + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx new file mode 100644 index 000000000..d8898f20e --- /dev/null +++ b/src/view/screens/Storybook/index.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import {View} from 'react-native' +import {CenteredView, ScrollView} from '#/view/com/util/Views' + +import {atoms as a, useTheme, ThemeProvider} from '#/alf' +import {useSetColorMode} from '#/state/shell' +import {Button} from '#/components/Button' + +import {Theming} from './Theming' +import {Typography} from './Typography' +import {Spacing} from './Spacing' +import {Buttons} from './Buttons' +import {Links} from './Links' +import {Forms} from './Forms' +import {Dialogs} from './Dialogs' +import {Breakpoints} from './Breakpoints' +import {Shadows} from './Shadows' +import {Icons} from './Icons' + +export function Storybook() { + const t = useTheme() + const setColorMode = useSetColorMode() + + return ( + <ScrollView> + <CenteredView style={[t.atoms.bg]}> + <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}> + <View style={[a.flex_row, a.align_start, a.gap_md]}> + <Button + variant="outline" + color="primary" + size="small" + label='Set theme to "system"' + onPress={() => setColorMode('system')}> + System + </Button> + <Button + variant="solid" + color="secondary" + size="small" + label='Set theme to "system"' + onPress={() => setColorMode('light')}> + Light + </Button> + <Button + variant="solid" + color="secondary" + size="small" + label='Set theme to "system"' + onPress={() => setColorMode('dark')}> + Dark + </Button> + </View> + + <ThemeProvider theme="light"> + <Theming /> + </ThemeProvider> + <ThemeProvider theme="dim"> + <Theming /> + </ThemeProvider> + <ThemeProvider theme="dark"> + <Theming /> + </ThemeProvider> + + <Typography /> + <Spacing /> + <Shadows /> + <Buttons /> + <Icons /> + <Links /> + <Forms /> + <Dialogs /> + <Breakpoints /> + </View> + </CenteredView> + </ScrollView> + ) +} diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 6f748755a..c30874c2f 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -53,6 +53,8 @@ import {useInviteCodesQuery} from '#/state/queries/invites' import {NavSignupCard} from '#/view/shell/NavSignupCard' import {TextLink} from '../com/util/Link' +import {useTheme as useAlfTheme} from '#/alf' + let DrawerProfileCard = ({ account, onPressProfile, @@ -106,6 +108,7 @@ export {DrawerProfileCard} let DrawerContent = ({}: {}): React.ReactNode => { const theme = useTheme() + const t = useAlfTheme() const pal = usePalette('default') const {_} = useLingui() const setDrawerOpen = useSetDrawerOpen() @@ -208,7 +211,7 @@ let DrawerContent = ({}: {}): React.ReactNode => { testID="drawer" style={[ styles.view, - theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode, + theme.colorScheme === 'light' ? pal.view : t.atoms.bg_contrast_25, ]}> <SafeAreaView style={s.flex1}> <ScrollView style={styles.main}> diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 51c03ae3d..5320aebfc 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -28,6 +28,7 @@ import {isAndroid} from 'platform/detection' import {useSession} from '#/state/session' import {useCloseAnyActiveElement} from '#/state/util' import * as notifications from 'lib/notifications/notifications' +import {Outlet as PortalOutlet} from '#/components/Portal' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -94,6 +95,7 @@ function ShellInner() { </View> <Composer winHeight={winDim.height} /> <ModalsContainer /> + <PortalOutlet /> <Lightbox /> </> ) diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 20bc0dff1..1ada883c9 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -15,6 +15,7 @@ import {useAuxClick} from 'lib/hooks/useAuxClick' import {t} from '@lingui/macro' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useCloseAllActiveElements} from '#/state/util' +import {Outlet as PortalOutlet} from '#/components/Portal' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -41,6 +42,7 @@ function ShellInner() { </View> <Composer winHeight={0} /> <ModalsContainer /> + <PortalOutlet /> <Lightbox /> {!isDesktop && isDrawerOpen && ( <TouchableOpacity |