Merge branch 'main' into ja-translation-17

This commit is contained in:
Takayuki KUSANO
2024-09-21 13:31:51 +09:00
140 changed files with 1685 additions and 833 deletions
+1
View File
@@ -33,6 +33,7 @@ module.exports = {
],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error',
'bsky-internal/use-prefixed-imports': 'warn',
'simple-import-sort/imports': [
'warn',
{
+6
View File
@@ -158,6 +158,12 @@ export function Embed({
return <Info>The quoted post is blocked.</Info>
}
// Case 3.8: Detached quote post
if (AppBskyEmbedRecord.isViewDetached(record)) {
// Just don't show anything
return null
}
// Unknown embed type
return null
}
+12
View File
@@ -15,16 +15,22 @@
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Regular.1f5ed03b6dd9fd1f9982.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Italic.95778eb0c75dc956257e.otf">
<!--
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Medium.296aa2d65964269836b3.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-MediumItalic.0e57e17a6311368e2114.otf">
-->
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-SemiBold.2277990330981b8409bb.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-SemiBoldItalic.f62fea3df3a521d6c8a7.otf">
<!--
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Bold.8d330503e1d034ad68de.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BoldItalic.bb17e63f9baa0d861a20.otf">
-->
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-ExtraBoldItalic.0e50b40728d24d40fdf4.otf">
<!--
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-Black.66e9a87f1c921e844ed4.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BlackItalic.27b9f0ad06fd13a7b9da.otf">
-->
<style>
@font-face {
@@ -41,6 +47,7 @@
font-style: italic;
font-display: swap;
}
/*
@font-face {
font-family: "Inter-Medium";
src: local("Inter-Medium"), url(/static/media/Inter-Medium.296aa2d65964269836b3.otf) format("font/otf");
@@ -55,6 +62,7 @@
font-style: italic;
font-display: swap;
}
*/
@font-face {
font-family: "Inter-SemiBold";
src: local("Inter-SemiBold"), url(/static/media/Inter-SemiBold.2277990330981b8409bb.otf) format("font/otf");
@@ -69,6 +77,7 @@
font-style: italic;
font-display: swap;
}
/*
@font-face {
font-family: "Inter-Bold";
src: local("Inter-Bold"), url(/static/media/Inter-Bold.8d330503e1d034ad68de.otf) format("font/otf");
@@ -83,6 +92,7 @@
font-style: italic;
font-display: swap;
}
*/
@font-face {
font-family: "Inter-ExtraBold";
src: local("Inter-ExtraBold"), url(/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf) format("font/otf");
@@ -97,6 +107,7 @@
font-style: italic;
font-display: swap;
}
/*
@font-face {
font-family: "Inter-Black";
src: local("Inter-Black"), url(/static/media/Inter-Black.66e9a87f1c921e844ed4.otf) format("font/otf");
@@ -111,6 +122,7 @@
font-style: italic;
font-display: swap;
}
*/
/**
* Extend the react-native-web reset:
+1
View File
@@ -5,5 +5,6 @@ module.exports = {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
'use-exact-imports': require('./use-exact-imports'),
'use-typed-gates': require('./use-typed-gates'),
'use-prefixed-imports': require('./use-prefixed-imports'),
},
}
+4 -4
View File
@@ -1,4 +1,3 @@
/* eslint-disable bsky-internal/use-exact-imports */
const BANNED_IMPORTS = [
'@fortawesome/free-regular-svg-icons',
'@fortawesome/free-solid-svg-icons',
@@ -6,11 +5,12 @@ const BANNED_IMPORTS = [
exports.create = function create(context) {
return {
Literal(node) {
if (typeof node.value !== 'string') {
ImportDeclaration(node) {
const source = node.source
if (typeof source.value !== 'string') {
return
}
if (BANNED_IMPORTS.includes(node.value)) {
if (BANNED_IMPORTS.includes(source.value)) {
context.report({
node,
message:
+39
View File
@@ -0,0 +1,39 @@
const BANNED_IMPORT_PREFIXES = [
'alf/',
'components/',
'lib/',
'locale/',
'logger/',
'platform/',
'state/',
'storage/',
'view/',
]
module.exports = {
meta: {
type: 'suggestion',
fixable: 'code',
},
create(context) {
return {
ImportDeclaration(node) {
const source = node.source
if (typeof source.value !== 'string') {
return
}
if (
BANNED_IMPORT_PREFIXES.some(banned => source.value.startsWith(banned))
) {
context.report({
node: source,
message: `Use '#/${source.value}'`,
fix(fixer) {
return fixer.replaceText(source, `'#/${source.value}'`)
},
})
}
},
}
},
}
+2
View File
@@ -110,6 +110,7 @@
"await-lock": "^2.2.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"base64-js": "^1.5.1",
"bcp-47": "^2.1.0",
"bcp-47-match": "^2.0.3",
"date-fns": "^2.30.0",
"deprecated-react-native-prop-types": "^5.0.0",
@@ -204,6 +205,7 @@
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"tldts": "^6.1.46",
"zeego": "^1.6.2",
"zod": "^3.20.2"
},
+14 -2
View File
@@ -4,11 +4,23 @@ index bb74e80..0aa0202 100644
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
@@ -90,8 +90,8 @@ public class NativeModulesProxy extends ReactContextBaseJavaModule {
mModuleRegistry.ensureIsInitialized();
KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
- kotlinModuleRegistry.emitOnCreate();
kotlinModuleRegistry.installJSIInterop();
+ kotlinModuleRegistry.emitOnCreate();
Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
index 109d3fe..c7fce9e 100644
--- a/node_modules/expo-modules-core/build/uuid/uuid.js
+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
@@ -1,5 +1,7 @@
import bytesToUuid from './lib/bytesToUuid';
import { Uuidv5Namespace } from './uuid.types';
+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
+ensureNativeModulesAreInstalled();
const nativeUuidv4 = globalThis?.expo?.uuidv4;
const nativeUuidv5 = globalThis?.expo?.uuidv5;
function uuidv4() {
+45 -31
View File
@@ -29,6 +29,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -66,6 +71,11 @@ import {BackgroundNotificationPreferencesProvider} from '../modules/expo-backgro
SplashScreen.preventAutoHideAsync()
/**
* Begin geolocation ASAP
*/
beginResolveGeolocation()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
@@ -158,7 +168,9 @@ function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, [])
if (!isReady) {
@@ -170,36 +182,38 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
<GeolocationProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
</GeolocationProvider>
)
}
+40 -26
View File
@@ -18,6 +18,11 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -54,6 +59,11 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
import {Provider as PortalProvider} from '#/components/Portal'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
/**
* Begin geolocation ASAP
*/
beginResolveGeolocation()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
@@ -148,7 +158,9 @@ function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, [])
if (!isReady) {
@@ -160,31 +172,33 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<A11yProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</A11yProvider>
<GeolocationProvider>
<A11yProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</A11yProvider>
</GeolocationProvider>
)
}
+3 -6
View File
@@ -276,16 +276,13 @@ export const atoms = {
letterSpacing: tokens.TRACKING,
},
font_normal: {
fontWeight: tokens.fontWeight.normal,
},
font_semibold: {
fontWeight: tokens.fontWeight.semibold,
fontWeight: tokens.fontWeight.regular,
},
font_bold: {
fontWeight: tokens.fontWeight.bold,
fontWeight: tokens.fontWeight.semibold,
},
font_heavy: {
fontWeight: tokens.fontWeight.heavy,
fontWeight: tokens.fontWeight.extrabold,
},
italic: {
fontStyle: 'italic',
+6 -6
View File
@@ -54,16 +54,16 @@ export function DO_NOT_USE() {
// 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'),
'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'),
'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'),
'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
// 'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
// 'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'),
'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
// 'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
// 'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'),
'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
// 'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
// 'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
})
}
+9 -4
View File
@@ -47,11 +47,16 @@ export const borderRadius = {
full: 999,
} as const
/**
* These correspond to Inter font files we actually load.
*/
export const fontWeight = {
normal: '400',
semibold: '500',
bold: '600',
heavy: '700',
regular: '400',
// medium: '500',
semibold: '600',
// bold: '700',
extrabold: '800',
// black: '900',
} as const
export const gradients = {
-2
View File
@@ -24,8 +24,6 @@ export function AppLanguageDropdown() {
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
setLangPrefs.setPrimaryLanguage(value)
setLangPrefs.setContentLanguage(value)
// reset feeds to refetch content
resetPostsFeedQueries(queryClient)
@@ -27,8 +27,6 @@ export function AppLanguageDropdown() {
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
setLangPrefs.setPrimaryLanguage(value)
setLangPrefs.setContentLanguage(value)
// reset feeds to refetch content
resetPostsFeedQueries(queryClient)
+111 -37
View File
@@ -14,7 +14,7 @@ import {
} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
import {atoms as a, flatten, select, tokens, useTheme, web} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
@@ -30,7 +30,7 @@ export type ButtonColor =
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large'
export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
@@ -343,39 +343,46 @@ export const Button = React.forwardRef<View, ButtonProps>(
if (shape === 'default') {
if (size === 'large') {
baseStyles.push(
{paddingVertical: 15},
a.px_2xl,
a.rounded_sm,
a.gap_md,
)
} else if (size === 'medium') {
baseStyles.push(
{paddingVertical: 12},
a.px_2xl,
a.rounded_sm,
a.gap_md,
)
baseStyles.push({
paddingVertical: 13,
paddingHorizontal: 20,
borderRadius: 8,
gap: 8,
})
} else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
} else if (size === 'xsmall') {
baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm)
baseStyles.push({
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 6,
gap: 6,
})
} else if (size === 'tiny') {
baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
baseStyles.push({
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 4,
gap: 4,
})
}
} else if (shape === 'round' || shape === 'square') {
if (size === 'large') {
if (shape === 'round') {
baseStyles.push({height: 54, width: 54})
baseStyles.push({height: 46, width: 46})
} else {
baseStyles.push({height: 50, width: 50})
baseStyles.push({height: 44, width: 44})
}
} else if (size === 'small') {
baseStyles.push({height: 34, width: 34})
} else if (size === 'xsmall') {
baseStyles.push({height: 28, width: 28})
if (shape === 'round') {
baseStyles.push({height: 36, width: 36})
} else {
baseStyles.push({height: 34, width: 34})
}
} else if (size === 'tiny') {
baseStyles.push({height: 20, width: 20})
if (shape === 'round') {
baseStyles.push({height: 22, width: 22})
} else {
baseStyles.push({height: 21, width: 21})
}
}
if (shape === 'round') {
@@ -619,11 +626,11 @@ export function useSharedButtonTextStyles() {
}
if (size === 'large') {
baseStyles.push(a.text_md, android({paddingBottom: 1}))
baseStyles.push(a.text_md, a.leading_tight, web({paddingTop: 1}))
} else if (size === 'small') {
baseStyles.push(a.text_sm, a.leading_tight, web({paddingTop: 1}))
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, android({paddingBottom: 1}))
} else {
baseStyles.push(a.text_sm, android({paddingBottom: 1}))
baseStyles.push(a.text_xs, a.leading_tight)
}
return StyleSheet.flatten(baseStyles)
@@ -643,31 +650,98 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
export function ButtonIcon({
icon: Comp,
position,
size: iconSize,
size,
}: {
icon: React.ComponentType<SVGIconProps>
position?: 'left' | 'right'
size?: SVGIconProps['size']
}) {
const {size, disabled} = useButtonContext()
const {size: buttonSize, disabled} = useButtonContext()
const textStyles = useSharedButtonTextStyles()
const {iconSize, iconContainerSize} = React.useMemo(() => {
/**
* Pre-set icon sizes for different button sizes
*/
const iconSizeShorthand =
size ??
(({
large: 'sm',
small: 'xs',
tiny: 'xs',
}[buttonSize || 'small'] || 'sm') as Exclude<
SVGIconProps['size'],
undefined
>)
/*
* Copied here from icons/common.tsx so we can tweak if we need to, but
* also so that we can calculate transforms.
*/
const iconSize = {
xs: 12,
sm: 16,
md: 20,
lg: 24,
xl: 28,
'2xl': 32,
}[iconSizeShorthand]
/*
* Goal here is to match rendered text size so that different size icons
* don't increase button size
*/
const iconContainerSize = {
large: 18,
small: 16,
tiny: 13,
}[buttonSize || 'small']
return {
iconSize,
iconContainerSize,
}
}, [buttonSize, size])
return (
<View
style={[
a.z_20,
{
width: iconContainerSize,
height: iconContainerSize,
opacity: disabled ? 0.7 : 1,
marginLeft: position === 'left' ? -2 : 0,
marginRight: position === 'right' ? -2 : 0,
},
]}>
<Comp
size={
iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm')
}
style={[{color: textStyles.color, pointerEvents: 'none'}]}
/>
<View
style={[
a.absolute,
{
width: iconSize,
height: iconSize,
top: '50%',
left: '50%',
transform: [
{
translateX: (iconSize / 2) * -1,
},
{
translateY: (iconSize / 2) * -1,
},
],
},
]}>
<Comp
width={iconSize}
style={[
{
color: textStyles.color,
pointerEvents: 'none',
},
]}
/>
</View>
</View>
)
}
+25 -5
View File
@@ -9,6 +9,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
@@ -43,21 +44,40 @@ export function Avatar({avatar}: {avatar?: string}) {
}
export function Title({value}: {value: string}) {
return <Text style={[a.text_md, a.font_bold]}>{value}</Text>
return <Text style={[a.text_md, a.font_bold, a.leading_tight]}>{value}</Text>
}
export function Description({value, handle}: {value?: string; handle: string}) {
return value ? (
<Text numberOfLines={2}>
<RichText value={value} style={[]} />
<RichText value={value} style={[a.leading_snug]} />
</Text>
) : (
<Text>
<Text style={[a.leading_snug]}>
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
</Text>
)
}
export function RegionalNotice() {
const t = useTheme()
return (
<View
style={[
a.flex_row,
a.align_center,
a.gap_xs,
a.pt_2xs,
{marginLeft: -2},
]}>
<Flag fill={t.atoms.text_contrast_low.color} size="sm" />
<Text style={[a.italic, a.leading_snug]}>
<Trans>Required in your region</Trans>
</Text>
</View>
)
}
export function LikeCount({count}: {count: number}) {
const t = useTheme()
return (
@@ -66,7 +86,7 @@ export function LikeCount({count}: {count: number}) {
a.mt_sm,
a.text_sm,
t.atoms.text_contrast_medium,
{fontWeight: '500'},
{fontWeight: '600'},
]}>
<Plural value={count} one="Liked by # user" other="Liked by # users" />
</Text>
@@ -85,7 +105,7 @@ export function Content({children}: React.PropsWithChildren<{}>) {
a.align_center,
a.justify_between,
]}>
<View style={[a.gap_xs, a.flex_1]}>{children}</View>
<View style={[a.gap_2xs, a.flex_1]}>{children}</View>
<ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
</View>
+1 -1
View File
@@ -24,7 +24,7 @@ export function MediaInsetBorder({
return (
<Fill
style={[
a.rounded_sm,
a.rounded_md,
a.border,
opaque
? [t.atoms.border_contrast_low]
+1 -1
View File
@@ -170,6 +170,6 @@ const styles = StyleSheet.create({
alt: {
color: 'white',
fontSize: 7,
fontWeight: 'bold',
fontWeight: '600',
},
})
+2
View File
@@ -179,8 +179,10 @@ export function Outer({
style={[
a.rounded_sm,
a.p_xs,
a.border,
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
t.atoms.shadow_md,
t.atoms.border_contrast_low,
style,
]}>
{children}
+1 -1
View File
@@ -132,7 +132,7 @@ export function Label({
<Text
style={[
text,
a.font_semibold,
a.font_bold,
a.leading_tight,
t.atoms.text_contrast_medium,
{paddingRight: 3},
@@ -411,6 +411,7 @@ function Inner({
() => currentAccount?.did === profile.did,
[currentAccount, profile],
)
const isLabeler = profile.associated?.labeler
return (
<View>
@@ -419,11 +420,13 @@ function Inner({
<UserAvatar
size={64}
avatar={profile.avatar}
type={isLabeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')}
/>
</Link>
{!isMe &&
!isLabeler &&
(isBlockedUser ? (
<Link
to={profileURL}
+1 -1
View File
@@ -26,7 +26,7 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
<Text
style={[
t.atoms.text_contrast_medium,
a.font_semibold,
a.font_bold,
a.text_sm,
{textTransform: 'uppercase'},
]}>
+1 -3
View File
@@ -35,9 +35,7 @@ export function ProgressGuideTask({
)}
<View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
<Text style={[a.text_sm, a.font_semibold, a.leading_tight]}>
{title}
</Text>
<Text style={[a.text_sm, a.font_bold, a.leading_tight]}>{title}</Text>
{subtitle && (
<Text
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}>
+1 -1
View File
@@ -154,7 +154,7 @@ export const ProgressGuideToast = React.forwardRef<
ref={animatedCheckRef}
/>
<View>
<Text style={[a.text_md, a.font_semibold]}>{title}</Text>
<Text style={[a.text_md, a.font_bold]}>{title}</Text>
{subtitle && (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{subtitle}
+2 -2
View File
@@ -120,7 +120,7 @@ export function Cancel({
<Button
variant="solid"
color="secondary"
size={gtMobile ? 'small' : 'medium'}
size={gtMobile ? 'small' : 'large'}
label={cta || _(msg`Cancel`)}
onPress={onPress}>
<ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
@@ -163,7 +163,7 @@ export function Action({
<Button
variant="solid"
color={color}
size={gtMobile ? 'small' : 'medium'}
size={gtMobile ? 'small' : 'large'}
label={cta || _(msg`Confirm`)}
onPress={handleOnPress}
testID={testID}>
@@ -77,8 +77,7 @@ function LabelerButton({
handle: labeler.creator.handle,
})}
/>
<Text
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}>
<Text style={[t.atoms.text_contrast_medium, a.text_sm, a.font_bold]}>
@{labeler.creator.handle}
</Text>
</LabelingServiceCard.Content>
@@ -125,7 +125,7 @@ export function WizardEditListDialog({
label={_(msg`Close`)}
variant="ghost"
color="primary"
size="xsmall"
size="small"
onPress={() => control.close()}>
<ButtonText>
<Trans>Close</Trans>
@@ -101,7 +101,7 @@ function WizardListCard({
label={_(msg`Remove`)}
variant="solid"
color="secondary"
size="xsmall"
size="small"
style={[a.self_center, {marginLeft: 'auto'}]}
onPress={onPress}>
<ButtonText>
+1 -1
View File
@@ -117,7 +117,7 @@ function BirthdayInner({
<View style={isWeb && [a.flex_row, a.justify_end]}>
<Button
label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)}
size="medium"
size="large"
onPress={onSave}
variant="solid"
color="primary">
+1 -1
View File
@@ -120,7 +120,7 @@ function EmbedDialogInner({
label={_(msg`Copy code`)}
color="primary"
variant="solid"
size="medium"
size="large"
onPress={() => {
ref.current?.focus()
ref.current?.setSelection(0, snippet.length)
+3 -3
View File
@@ -83,7 +83,7 @@ export function EmbedConsentDialog({
onPress={onShowAllPress}
onAccessibilityEscape={control.close}
color="primary"
size="medium"
size="large"
variant="solid">
<ButtonText>
<Trans>Enable external media</Trans>
@@ -95,7 +95,7 @@ export function EmbedConsentDialog({
onPress={onShowPress}
onAccessibilityEscape={control.close}
color="secondary"
size="medium"
size="large"
variant="solid">
<ButtonText>
<Trans>Enable {externalEmbedLabels[source]} only</Trans>
@@ -106,7 +106,7 @@ export function EmbedConsentDialog({
onAccessibilityEscape={control.close}
onPress={onHidePress}
color="secondary"
size="medium"
size="large"
variant="ghost">
<ButtonText>
<Trans>No thanks</Trans>
+1 -1
View File
@@ -244,7 +244,7 @@ function ModalError({details, close}: {details?: string; close: () => void}) {
label={_(msg`Close dialog`)}
onPress={close}
color="primary"
size="medium"
size="large"
variant="solid">
<ButtonText>
<Trans>Close</Trans>
+1 -1
View File
@@ -264,7 +264,7 @@ function DialogError({details}: {details?: string}) {
label={_(msg`Close dialog`)}
onPress={() => control.close()}
color="primary"
size="medium"
size="large"
variant="solid">
<ButtonText>
<Trans>Close</Trans>
+1 -1
View File
@@ -319,7 +319,7 @@ function MutedWordsInner() {
<Button
disabled={isPending || !field}
label={_(msg`Add mute word for configured settings`)}
size="medium"
size="large"
color="primary"
variant="solid"
style={[]}
@@ -439,7 +439,7 @@ export function PostInteractionSettingsForm({
onPress={onSave}
onAccessibilityEscape={control.close}
color="primary"
size="medium"
size="large"
variant="solid"
style={a.mt_xl}>
<ButtonText>{_(msg`Save`)}</ButtonText>
@@ -491,9 +491,7 @@ function Selectable({
},
style,
]}>
<Text style={[a.text_sm, isSelected && a.font_semibold]}>
{label}
</Text>
<Text style={[a.text_sm, isSelected && a.font_bold]}>{label}</Text>
{isSelected ? (
<Check size="sm" fill={t.palette.primary_500} />
) : (
@@ -48,7 +48,7 @@ export function NeueTypography() {
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
<View style={[a.gap_xl]}>
<View style={[a.gap_md]}>
<Text style={[a.text_3xl, {fontWeight: '900'}]}>
<Text style={[a.text_3xl, a.font_heavy]}>
<Trans>New font settings </Trans>
</Text>
<Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}>
@@ -19,10 +19,10 @@ import {isIOS, isNative} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from 'state/shell'
import {useComposerControls} from '#/state/shell'
import {formatCount} from '#/view/com/util/numeric/format'
import * as Toast from '#/view/com/util/Toast'
import {Logomark} from '#/view/icons/Logomark'
import * as Toast from 'view/com/util/Toast'
import {
atoms as a,
ThemeProvider,
@@ -441,10 +441,10 @@ export function TenMillionInner({
allowFontScaling={false}
style={[
a.absolute,
a.font_heavy,
{
color: t.palette.primary_500,
fontSize: 32,
fontWeight: '900',
width: 32,
top: isNative ? -10 : 0,
left: 0,
@@ -462,11 +462,11 @@ export function TenMillionInner({
style={[
a.relative,
a.text_center,
a.font_heavy,
{
fontStyle: 'italic',
fontSize: getFontSize(userNumber),
lineHeight: getFontSize(userNumber),
fontWeight: '900',
letterSpacing: -2,
},
]}>
@@ -536,7 +536,7 @@ export function TenMillionInner({
style={[
a.flex_1,
a.text_sm,
a.font_semibold,
a.font_bold,
a.leading_snug,
lightTheme.atoms.text_contrast_medium,
]}>
@@ -551,7 +551,7 @@ export function TenMillionInner({
style={[
a.flex_1,
a.text_sm,
a.font_semibold,
a.font_bold,
a.leading_snug,
a.text_right,
lightTheme.atoms.text_contrast_low,
@@ -643,14 +643,7 @@ export function TenMillionInner({
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
<Text
allowFontScaling={false}
style={[
a.text_5xl,
a.leading_tight,
a.pb_lg,
{
fontWeight: '900',
},
]}>
style={[a.text_5xl, a.leading_tight, a.pb_lg, a.font_heavy]}>
<Trans>Thanks for being one of our first 10 million users.</Trans>
</Text>
+1 -1
View File
@@ -160,7 +160,7 @@ function DialogInner({
<Button
label={_(msg`Start chatting`)}
accessibilityHint={_(msg`Close modal`)}
size="medium"
size="large"
color="primary"
variant="solid"
onPress={() => control.close()}>
+1 -1
View File
@@ -76,7 +76,7 @@ export function DateField({
<Button
label={_(msg`Done`)}
onPress={() => control.close()}
size="medium"
size="large"
color="primary"
variant="solid">
<ButtonText>
@@ -112,7 +112,7 @@ function Inner({control}: {control: DialogControlProps}) {
onPress={() => control.close()}
variant="solid"
color={status === 'failure' ? 'secondary' : 'primary'}
size="medium"
size="large"
style={{marginLeft: 'auto'}}>
<ButtonText>
<Trans>Close</Trans>
@@ -124,7 +124,7 @@ function Inner({control}: {control: DialogControlProps}) {
onPress={onPressResendEmail}
variant="solid"
color="primary"
size="medium"
size="large"
disabled={sending}>
<ButtonText>
<Trans>Resend Email</Trans>
+2 -2
View File
@@ -94,7 +94,7 @@ export function ContentHider({
a.text_left,
a.font_bold,
a.leading_snug,
gtMobile && [a.font_semibold],
gtMobile && [a.font_bold],
t.atoms.text_contrast_medium,
web({
marginBottom: 1,
@@ -107,7 +107,7 @@ export function ContentHider({
style={[
a.font_bold,
a.leading_snug,
gtMobile && [a.font_semibold],
gtMobile && [a.font_bold],
t.atoms.text_contrast_high,
web({
marginBottom: 1,
@@ -236,8 +236,7 @@ export function LabelerLabelPreference({
<View style={[a.flex_row, a.gap_xs, a.align_center, a.mt_xs]}>
<CircleInfo size="sm" fill={t.atoms.text_contrast_high.color} />
<Text
style={[t.atoms.text_contrast_medium, a.font_semibold, a.italic]}>
<Text style={[t.atoms.text_contrast_medium, a.font_bold, a.italic]}>
{adultDisabled ? (
<Trans>Adult content is disabled.</Trans>
) : isGlobalLabel ? (
@@ -279,7 +279,7 @@ function AppealForm({
testID="backBtn"
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onPressBack}
label={_(msg`Back`)}>
<ButtonText>{_(msg`Back`)}</ButtonText>
@@ -288,7 +288,7 @@ function AppealForm({
testID="submitBtn"
variant="solid"
color="primary"
size="medium"
size="large"
onPress={onSubmit}
label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText>
+4 -10
View File
@@ -10,9 +10,9 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {NavigationProp} from '#/lib/routes/types'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -86,13 +86,7 @@ export function ScreenHider({
</View>
</View>
<Text
style={[
a.text_4xl,
a.font_semibold,
a.text_center,
a.mb_md,
t.atoms.text,
]}>
style={[a.text_4xl, a.font_bold, a.text_center, a.mb_md, t.atoms.text]}>
{isNoPwi ? (
<Trans>Sign-in Required</Trans>
) : (
@@ -118,7 +112,7 @@ export function ScreenHider({
<Text
style={[
a.text_lg,
a.font_semibold,
a.font_bold,
a.leading_snug,
t.atoms.text,
a.ml_xs,
+14
View File
@@ -33,6 +33,20 @@ export function isJustAMute(modui: ModerationUI): boolean {
return modui.filters.length === 1 && modui.filters[0].type === 'muted'
}
export function moduiContainsHideableOffense(modui: ModerationUI): boolean {
const label = modui.filters.at(0)
if (label && label.type === 'label') {
return labelIsHideableOffense(label.label)
}
return false
}
export function labelIsHideableOffense(
label: ComAtprotoLabelDefs.Label,
): boolean {
return ['!hide', '!takedown'].includes(label.val)
}
export function getLabelingServiceTitle({
displayName,
handle,
+82
View File
@@ -0,0 +1,82 @@
import {describe, expect, it} from '@jest/globals'
import tldts from 'tldts'
import {isEmailMaybeInvalid} from '#/lib/strings/email'
describe('emailTypoChecker', () => {
const invalidCases = [
'gnail.com',
'gnail.co',
'gmaill.com',
'gmaill.co',
'gmai.com',
'gmai.co',
'gmal.com',
'gmal.co',
'gmail.co',
'iclod.com',
'iclod.co',
'outllok.com',
'outllok.co',
'outlook.co',
'yaoo.com',
'yaoo.co',
'yaho.com',
'yaho.co',
'yahooo.com',
'yahooo.co',
'yahoo.co',
'hithere.jul',
'agpowj.notshop',
'thisisnot.avalid.tld.nope',
// old tld for czechoslovakia
'czechoslovakia.cs',
// tlds that cbs was registering in 2024 but cancelled
'liveon.cbs',
'its.showtime',
]
const validCases = [
'gmail.com',
// subdomains (tests end of string)
'gnail.com.test.com',
'outlook.com',
'yahoo.com',
'icloud.com',
'firefox.com',
'firefox.co',
'hello.world.com',
'buy.me.a.coffee.shop',
'mayotte.yt',
'aland.ax',
'bouvet.bv',
'uk.gb',
'chad.td',
'somalia.so',
'plane.aero',
'cute.cat',
'together.coop',
'findme.jobs',
'nightatthe.museum',
'industrial.mil',
'czechrepublic.cz',
'lovakia.sk',
// new gtlds in 2024
'whatsinyour.locker',
'letsmakea.deal',
'skeet.now',
'everyone.みんな',
'bourgeois.lifestyle',
'california.living',
'skeet.ing',
'listeningto.music',
'createa.meme',
]
it.each(invalidCases)(`should be invalid: abcde@%s`, domain => {
expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(true)
})
it.each(validCases)(`should be valid: abcde@%s`, domain => {
expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(false)
})
})
+9
View File
@@ -0,0 +1,9 @@
import type tldts from 'tldts'
const COMMON_ERROR_PATTERN =
/([a-zA-Z0-9._%+-]+)@(gnail\.(co|com)|gmaill\.(co|com)|gmai\.(co|com)|gmail\.co|gmal\.(co|com)|iclod\.(co|com)|icloud\.co|outllok\.(co|com)|outlok\.(co|com)|outlook\.co|yaoo\.(co|com)|yaho\.(co|com)|yahoo\.co|yahooo\.(co|com))$/
export function isEmailMaybeInvalid(email: string, dynamicTldts: typeof tldts) {
const isIcann = dynamicTldts.parse(email).isIcann
return !isIcann || COMMON_ERROR_PATTERN.test(email)
}
+4 -5
View File
@@ -1,6 +1,6 @@
import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native'
import {isWeb} from 'platform/detection'
import {isWeb} from '#/platform/detection'
import {Theme, TypographyVariant} from './ThemeContext'
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
@@ -79,14 +79,13 @@ export const s = StyleSheet.create({
// font weights
fw600: {fontWeight: '600'},
bold: {fontWeight: '700'},
fw500: {fontWeight: '500'},
semiBold: {fontWeight: '500'},
bold: {fontWeight: '600'},
fw500: {fontWeight: '600'},
semiBold: {fontWeight: '600'},
fw400: {fontWeight: '400'},
normal: {fontWeight: '400'},
fw300: {fontWeight: '400'},
light: {fontWeight: '400'},
fw200: {fontWeight: '200'},
// text decoration
underline: {textDecorationLine: 'underline'},
+19 -19
View File
@@ -100,12 +100,12 @@ export const defaultTheme: Theme = {
'2xl-medium': {
fontSize: 18,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'2xl-bold': {
fontSize: 18,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
fontWeight: '600',
},
'2xl-heavy': {
fontSize: 18,
@@ -125,12 +125,12 @@ export const defaultTheme: Theme = {
'xl-medium': {
fontSize: 17,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'xl-bold': {
fontSize: 17,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
fontWeight: '600',
},
'xl-heavy': {
fontSize: 17,
@@ -150,12 +150,12 @@ export const defaultTheme: Theme = {
'lg-medium': {
fontSize: 16,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'lg-bold': {
fontSize: 16,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
fontWeight: '600',
},
'lg-heavy': {
fontSize: 16,
@@ -175,12 +175,12 @@ export const defaultTheme: Theme = {
'md-medium': {
fontSize: 15,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'md-bold': {
fontSize: 15,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
fontWeight: '600',
},
'md-heavy': {
fontSize: 15,
@@ -200,12 +200,12 @@ export const defaultTheme: Theme = {
'sm-medium': {
fontSize: 14,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'sm-bold': {
fontSize: 14,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
fontWeight: '600',
},
'sm-heavy': {
fontSize: 14,
@@ -225,12 +225,12 @@ export const defaultTheme: Theme = {
'xs-medium': {
fontSize: 13,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'xs-bold': {
fontSize: 13,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
fontWeight: '600',
},
'xs-heavy': {
fontSize: 13,
@@ -241,24 +241,24 @@ export const defaultTheme: Theme = {
'title-2xl': {
fontSize: 34,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'title-xl': {
fontSize: 28,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
fontWeight: '600',
},
'title-lg': {
fontSize: 22,
fontWeight: '500',
fontWeight: '600',
},
title: {
fontWeight: '500',
fontWeight: '600',
fontSize: 20,
letterSpacing: tokens.TRACKING,
},
'title-sm': {
fontWeight: 'bold',
fontWeight: '600',
fontSize: 17,
letterSpacing: tokens.TRACKING,
},
@@ -273,12 +273,12 @@ export const defaultTheme: Theme = {
fontWeight: '400',
},
'button-lg': {
fontWeight: '500',
fontWeight: '600',
fontSize: 18,
letterSpacing: tokens.TRACKING,
},
button: {
fontWeight: '500',
fontWeight: '600',
fontSize: 14,
letterSpacing: tokens.TRACKING,
},
+53
View File
@@ -0,0 +1,53 @@
import {getLocales as defaultGetLocales, Locale} from 'expo-localization'
import {dedupArray} from '#/lib/functions'
type LocalWithLanguageCode = Locale & {
languageCode: string
}
/**
* Normalized locales
*
* Handles legacy migration for Java devices.
*
* {@link https://github.com/bluesky-social/social-app/pull/4461}
* {@link https://xml.coverpages.org/iso639a.html}
*/
export function getLocales() {
const locales = defaultGetLocales?.() ?? []
const output: LocalWithLanguageCode[] = []
for (const locale of locales) {
if (typeof locale.languageCode === 'string') {
if (locale.languageCode === 'in') {
// indonesian
locale.languageCode = 'id'
}
if (locale.languageCode === 'iw') {
// hebrew
locale.languageCode = 'he'
}
if (locale.languageCode === 'ji') {
// yiddish
locale.languageCode = 'yi'
}
// @ts-ignore checked above
output.push(locale)
}
}
return output
}
export const deviceLocales = getLocales()
/**
* BCP-47 language tag without region e.g. array of 2-char lang codes
*
* {@link https://docs.expo.dev/versions/latest/sdk/localization/#locale}
*/
export const deviceLanguageCodes = dedupArray(
deviceLocales.map(l => l.languageCode),
)
+23 -1
View File
@@ -160,8 +160,13 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.en
}
/**
* Handles legacy migration for Java devices.
*
* {@link https://github.com/bluesky-social/social-app/pull/4461}
* {@link https://xml.coverpages.org/iso639a.html}
*/
export function fixLegacyLanguageCode(code: string | null): string | null {
// handle some legacy code conversions, see https://xml.coverpages.org/iso639a.html
if (code === 'in') {
// indonesian
return 'id'
@@ -176,3 +181,20 @@ export function fixLegacyLanguageCode(code: string | null): string | null {
}
return code
}
/**
* Find the first language supported by our translation infra. Values should be
* in order of preference, and match the values of {@link AppLanguage}.
*
* If no match, returns `en`.
*/
export function findSupportedAppLanguage(languageTags: (string | undefined)[]) {
const supported = new Set(Object.values(AppLanguage))
for (const tag of languageTags) {
if (!tag) continue
if (supported.has(tag as AppLanguage)) {
return tag
}
}
return AppLanguage.en
}
-10
View File
@@ -1,8 +1,4 @@
import {Platform} from 'react-native'
import {getLocales} from 'expo-localization'
import {fixLegacyLanguageCode} from '#/locale/helpers'
import {dedupArray} from 'lib/functions'
export const isIOS = Platform.OS === 'ios'
export const isAndroid = Platform.OS === 'android'
@@ -15,9 +11,3 @@ export const isMobileWeb =
// @ts-ignore we know window exists -prf
global.window.matchMedia(isMobileWebMediaQuery)?.matches
export const isIPhoneWeb = isWeb && /iPhone/.test(navigator.userAgent)
export const deviceLocales = dedupArray(
getLocales?.()
.map?.(locale => fixLegacyLanguageCode(locale.languageCode))
.filter(code => typeof code === 'string'),
) as string[]
+3 -3
View File
@@ -142,7 +142,7 @@ export function Deactivated() {
<View style={[a.gap_sm]}>
<Button
label={_(msg`Reactivate your account`)}
size="medium"
size="large"
variant="solid"
color="primary"
onPress={handleActivate}>
@@ -153,7 +153,7 @@ export function Deactivated() {
</Button>
<Button
label={_(msg`Cancel reactivation and log out`)}
size="medium"
size="large"
variant="solid"
color="secondary"
onPress={onPressLogout}>
@@ -212,7 +212,7 @@ export function Deactivated() {
</Text>
<Button
label={_(msg`Log in or sign up`)}
size="medium"
size="large"
variant="solid"
color="secondary"
onPress={() => setShowLoggedOut(true)}>
@@ -23,7 +23,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
size="small"
onPress={async () => {
SharedPrefs.removeValue('testerString')
SharedPrefs.setValue('testerString', 'Hello')
@@ -39,7 +39,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
size="small"
onPress={async () => {
SharedPrefs.removeValue('testerString')
const str = SharedPrefs.getString('testerString')
@@ -53,7 +53,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
size="small"
onPress={async () => {
SharedPrefs.removeValue('testerBool')
SharedPrefs.setValue('testerBool', true)
@@ -68,7 +68,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
size="small"
onPress={async () => {
SharedPrefs.removeValue('testerNumber')
SharedPrefs.setValue('testerNumber', 123)
@@ -83,7 +83,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
size="small"
onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!')
SharedPrefs.addToSet('testerSet', 'Hello!')
@@ -98,7 +98,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
size="xsmall"
size="small"
onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!')
const contains = SharedPrefs.setContains('testerSet', 'Hello!')
+2 -2
View File
@@ -91,7 +91,7 @@ export function NoFeedsPinned({
<Button
disabled={isPending}
label={_(msg`Apply default recommended feeds`)}
size="medium"
size="large"
variant="solid"
color="primary"
onPress={addRecommendedFeeds}>
@@ -102,7 +102,7 @@ export function NoFeedsPinned({
<Link
label={_(msg`Browse other feeds`)}
to="/feeds"
size="medium"
size="large"
variant="solid"
color="secondary">
<ButtonIcon icon={ListSparkle} position="left" />
+4 -4
View File
@@ -152,7 +152,7 @@ export function ListHiddenScreen({
<Button
variant="solid"
color="secondary"
size="medium"
size="large"
label={_(msg`Remove from saved feeds`)}
onPress={onRemoveList}
disabled={isProcessing}>
@@ -168,7 +168,7 @@ export function ListHiddenScreen({
<Button
variant="solid"
color="secondary"
size="medium"
size="large"
label={_(msg`Show list anyway`)}
onPress={() => setIsContentVisible(true)}
disabled={isProcessing}>
@@ -180,7 +180,7 @@ export function ListHiddenScreen({
<Button
variant="solid"
color="secondary"
size="medium"
size="large"
label={_(msg`Unsubscribe from list`)}
onPress={() => {
if (isModList) {
@@ -204,7 +204,7 @@ export function ListHiddenScreen({
color="primary"
label={_(msg`Return to previous page`)}
onPress={goBack}
size="medium"
size="large"
disabled={isProcessing}>
<ButtonText>
<Trans>Go Back</Trans>
+1 -1
View File
@@ -98,7 +98,7 @@ export const ChooseAccountForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onPressBack}>
<ButtonText>{_(msg`Back`)}</ButtonText>
</Button>
+3 -3
View File
@@ -129,7 +129,7 @@ export const ForgotPasswordForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onPressBack}>
<ButtonText>
<Trans>Back</Trans>
@@ -143,7 +143,7 @@ export const ForgotPasswordForm = ({
label={_(msg`Next`)}
variant="solid"
color={'primary'}
size="medium"
size="large"
onPress={onPressNext}>
<ButtonText>
<Trans>Next</Trans>
@@ -170,7 +170,7 @@ export const ForgotPasswordForm = ({
onPress={onEmailSent}
label={_(msg`Go to next`)}
accessibilityHint={_(msg`Navigates to the next screen`)}
size="medium"
size="large"
variant="ghost"
color="secondary">
<ButtonText>
+3 -3
View File
@@ -285,7 +285,7 @@ export const LoginForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onPressBack}>
<ButtonText>
<Trans>Back</Trans>
@@ -299,7 +299,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Retries login`)}
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onPressRetryConnect}>
<ButtonText>
<Trans>Retry</Trans>
@@ -319,7 +319,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Navigates to the next screen`)}
variant="solid"
color="primary"
size="medium"
size="large"
onPress={onPressNext}>
<ButtonText>
<Trans>Next</Trans>
+1 -1
View File
@@ -39,7 +39,7 @@ export const PasswordUpdatedForm = ({
accessibilityHint={_(msg`Closes password update alert`)}
variant="solid"
color="primary"
size="medium">
size="large">
<ButtonText>
<Trans>Okay</Trans>
</ButtonText>
+2 -2
View File
@@ -160,7 +160,7 @@ export const SetNewPasswordForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onPressBack}>
<ButtonText>
<Trans>Back</Trans>
@@ -174,7 +174,7 @@ export const SetNewPasswordForm = ({
label={_(msg`Next`)}
variant="solid"
color="primary"
size="medium"
size="large"
onPress={onPressNext}>
<ButtonText>
<Trans>Next</Trans>
@@ -128,7 +128,7 @@ function DialogInner() {
testID="backBtn"
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onBack}
label={_(msg`Back`)}>
<ButtonText>{_(msg`Back`)}</ButtonText>
@@ -137,7 +137,7 @@ function DialogInner() {
testID="submitBtn"
variant="solid"
color="primary"
size="medium"
size="large"
onPress={onSubmit}
label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText>
+1 -1
View File
@@ -198,7 +198,7 @@ export function MessagesScreen({navigation, route}: Props) {
<Button
label={_(msg`Reload conversations`)}
size="medium"
size="large"
color="secondary"
variant="solid"
onPress={() => refetch()}>
+6 -2
View File
@@ -7,6 +7,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger'
@@ -22,8 +23,8 @@ import {
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useSetMinimalShellMode} from '#/state/shell'
import {useAnalytics} from 'lib/analytics/analytics'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views'
@@ -338,7 +339,7 @@ export function ModerationScreenInner({
a.justify_between,
disabledOnIOS && {opacity: 0.5},
]}>
<Text style={[a.font_semibold, t.atoms.text_contrast_high]}>
<Text style={[a.font_bold, t.atoms.text_contrast_high]}>
<Trans>Enable adult content</Trans>
</Text>
<Toggle.Item
@@ -455,6 +456,9 @@ export function ModerationScreenInner({
value={labeler.creator.description}
handle={labeler.creator.handle}
/>
{isNonConfigurableModerationAuthority(
labeler.creator.did,
) && <LabelingService.RegionalNotice />}
</LabelingService.Content>
</LabelingService.Outer>
)}
+3 -3
View File
@@ -2,9 +2,9 @@ import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {Shadow} from '#/state/cache/types'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
@@ -20,7 +20,7 @@ export function ProfileHeaderDisplayName({
<View pointerEvents="none">
<Text
testID="profileHeaderDisplayName"
style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '500'}]}>
style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '600'}]}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
@@ -102,7 +102,7 @@ function DeactivateAccountDialogInner({
<Button
variant="solid"
color="negative"
size={gtMobile ? 'small' : 'medium'}
size={gtMobile ? 'small' : 'large'}
label={_(msg`Yes, deactivate`)}
onPress={handleDeactivate}>
<ButtonText>{_(msg`Yes, deactivate`)}</ButtonText>
+6 -4
View File
@@ -15,6 +15,7 @@ export interface BackNextButtonsProps {
onBackPress: () => void
onNextPress?: () => void
onRetryPress?: () => void
overrideNextText?: string
}
export function BackNextButtons({
@@ -25,6 +26,7 @@ export function BackNextButtons({
onBackPress,
onNextPress,
onRetryPress,
overrideNextText,
}: BackNextButtonsProps) {
const {_} = useLingui()
@@ -34,7 +36,7 @@ export function BackNextButtons({
label={_(msg`Go back to previous step`)}
variant="solid"
color="secondary"
size="medium"
size="large"
onPress={onBackPress}>
<ButtonText>
<Trans>Back</Trans>
@@ -46,7 +48,7 @@ export function BackNextButtons({
label={_(msg`Press to retry`)}
variant="solid"
color="primary"
size="medium"
size="large"
onPress={onRetryPress}>
<ButtonText>
<Trans>Retry</Trans>
@@ -59,11 +61,11 @@ export function BackNextButtons({
label={_(msg`Continue to next step`)}
variant="solid"
color="primary"
size="medium"
size="large"
disabled={isLoading || isNextDisabled}
onPress={onNextPress}>
<ButtonText>
<Trans>Next</Trans>
{overrideNextText ? overrideNextText : <Trans>Next</Trans>}
</ButtonText>
{isLoading && <ButtonIcon icon={Loader} />}
</Button>
+36 -8
View File
@@ -3,9 +3,11 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator'
import type tldts from 'tldts'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isEmailMaybeInvalid} from 'lib/strings/email'
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {is13, is18, useSignupContext} from '#/screens/Signup/state'
import {Policies} from '#/screens/Signup/StepInfo/Policies'
@@ -46,13 +48,41 @@ export function StepInfo({
const inviteCodeValueRef = useRef<string>(state.inviteCode)
const emailValueRef = useRef<string>(state.email)
const prevEmailValueRef = useRef<string>(state.email)
const passwordValueRef = useRef<string>(state.password)
const onNextPress = React.useCallback(async () => {
const [hasWarnedEmail, setHasWarnedEmail] = React.useState<boolean>(false)
const tldtsRef = React.useRef<typeof tldts>()
React.useEffect(() => {
// @ts-expect-error - valid path
import('tldts/dist/index.cjs.min.js').then(tldts => {
tldtsRef.current = tldts
})
}, [])
const onNextPress = () => {
const inviteCode = inviteCodeValueRef.current
const email = emailValueRef.current
const emailChanged = prevEmailValueRef.current !== email
const password = passwordValueRef.current
if (emailChanged && tldtsRef.current) {
if (isEmailMaybeInvalid(email, tldtsRef.current)) {
prevEmailValueRef.current = email
setHasWarnedEmail(true)
return dispatch({
type: 'setError',
value: _(
msg`It looks like you may have entered your email address incorrectly. Are you sure it's right?`,
),
})
}
} else if (hasWarnedEmail) {
setHasWarnedEmail(false)
}
prevEmailValueRef.current = email
if (!is13(state.dateOfBirth)) {
return
}
@@ -89,13 +119,7 @@ export function StepInfo({
logEvent('signup:nextPressed', {
activeStep: state.activeStep,
})
}, [
_,
dispatch,
state.activeStep,
state.dateOfBirth,
state.serviceDescription?.inviteCodeRequired,
])
}
return (
<ScreenTransition>
@@ -148,6 +172,9 @@ export function StepInfo({
testID="emailInput"
onChangeText={value => {
emailValueRef.current = value.trim()
if (hasWarnedEmail) {
setHasWarnedEmail(false)
}
}}
label={_(msg`Enter your email address`)}
defaultValue={state.email}
@@ -208,6 +235,7 @@ export function StepInfo({
onBackPress={onPressBack}
onNextPress={onNextPress}
onRetryPress={refetchServer}
overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined}
/>
</ScreenTransition>
)
+3 -3
View File
@@ -8,8 +8,8 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {FEEDBACK_FORM_URL} from '#/lib/constants'
import {useServiceQuery} from '#/state/queries/service'
import {useStarterPackQuery} from 'state/queries/starter-packs'
import {useActiveStarterPack} from 'state/shell/starter-pack'
import {useStarterPackQuery} from '#/state/queries/starter-packs'
import {useActiveStarterPack} from '#/state/shell/starter-pack'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
import {
initialState,
@@ -132,7 +132,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
!gtMobile && {paddingBottom: 100},
]}>
<View style={[a.gap_sm, a.pb_3xl]}>
<Text style={[a.font_semibold, t.atoms.text_contrast_medium]}>
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
<Trans>
Step {state.activeStep + 1} of{' '}
{state.serviceDescription &&
@@ -11,22 +11,22 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isAndroidWeb} from '#/lib/browser'
import {JOINED_THIS_WEEK} from '#/lib/constants'
import {isAndroidWeb} from 'lib/browser'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {logEvent} from 'lib/statsig/statsig'
import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack'
import {isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts'
import {useStarterPackQuery} from 'state/queries/starter-packs'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {logEvent} from '#/lib/statsig/statsig'
import {createStarterPackGooglePlayUri} from '#/lib/strings/starter-pack'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useStarterPackQuery} from '#/state/queries/starter-packs'
import {
useActiveStarterPack,
useSetActiveStarterPack,
} from 'state/shell/starter-pack'
} from '#/state/shell/starter-pack'
import {LoggedOutScreenState} from '#/view/com/auth/LoggedOut'
import {formatCount} from '#/view/com/util/numeric/format'
import {LoggedOutScreenState} from 'view/com/auth/LoggedOut'
import {CenteredView} from 'view/com/util/Views'
import {Logo} from 'view/icons/Logo'
import {CenteredView} from '#/view/com/util/Views'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
@@ -188,12 +188,7 @@ function LandingScreenLoaded({
{record.name}
</Text>
<Text
style={[
a.text_center,
a.font_semibold,
a.text_md,
{color: 'white'},
]}>
style={[a.text_center, a.font_bold, a.text_md, {color: 'white'}]}>
Starter pack by {`@${creator.handle}`}
</Text>
</LinearGradientBackground>
@@ -219,11 +214,7 @@ function LandingScreenLoaded({
color={t.atoms.text_contrast_medium.color}
/>
<Text
style={[
a.font_semibold,
a.text_sm,
t.atoms.text_contrast_medium,
]}
style={[a.font_bold, a.text_sm, t.atoms.text_contrast_medium]}
numberOfLines={1}>
<Trans>
{formatCount(i18n, JOINED_THIS_WEEK)} joined this week
@@ -308,7 +299,7 @@ function LandingScreenLoaded({
label={_(msg`Signup without a starter pack`)}
variant="solid"
color="secondary"
size="medium"
size="large"
style={[a.py_lg]}
onPress={onJoinWithoutPress}>
<ButtonText>
@@ -449,7 +449,7 @@ function Header({
}}
variant="solid"
color="primary"
size="medium">
size="large">
<ButtonText style={[a.text_lg]}>
<Trans>Join Bluesky</Trans>
</ButtonText>
@@ -645,7 +645,7 @@ function OverflowMenu({
<Button
variant="solid"
color="negative"
size={gtMobile ? 'small' : 'medium'}
size={gtMobile ? 'small' : 'large'}
label={_(msg`Yes, delete this starter pack`)}
onPress={onDeleteStarterPack}>
<ButtonText>
+1 -1
View File
@@ -358,7 +358,7 @@ function Container({children}: {children: React.ReactNode}) {
label={_(msg`Next`)}
variant="solid"
color="primary"
size="medium"
size="large"
style={[a.mx_xl, a.mb_lg, {marginTop: 35}]}
onPress={() => dispatch({type: 'Next'})}>
<ButtonText>
+169
View File
@@ -0,0 +1,169 @@
import React from 'react'
import EventEmitter from 'eventemitter3'
import {networkRetry} from '#/lib/async/retry'
import {logger} from '#/logger'
import {IS_DEV} from '#/env'
import {Device, device} from '#/storage'
const events = new EventEmitter()
const EVENT = 'geolocation-updated'
const emitGeolocationUpdate = (geolocation: Device['geolocation']) => {
events.emit(EVENT, geolocation)
}
const onGeolocationUpdate = (
listener: (geolocation: Device['geolocation']) => void,
) => {
events.on(EVENT, listener)
return () => {
events.off(EVENT, listener)
}
}
/**
* Default geolocation value. IF undefined, we fail closed and apply all
* additional mod authorities.
*/
export const DEFAULT_GEOLOCATION: Device['geolocation'] = {
countryCode: undefined,
}
async function getGeolocation(): Promise<Device['geolocation']> {
const res = await fetch(`https://bsky.app/ipcc`)
if (!res.ok) {
throw new Error(`geolocation: lookup failed ${res.status}`)
}
const json = await res.json()
if (json.countryCode) {
return {
countryCode: json.countryCode,
}
} else {
return undefined
}
}
/**
* Local promise used within this file only.
*/
let geolocationResolution: Promise<void> | undefined
/**
* Begin the process of resolving geolocation. This should be called once at
* app start.
*
* THIS METHOD SHOULD NEVER THROW.
*
* This method is otherwise not used for any purpose. To ensure geolocation is
* resolved, use {@link ensureGeolocationResolved}
*/
export function beginResolveGeolocation() {
/**
* In dev, IP server is unavailable, so we just set the default geolocation
* and fail closed.
*/
if (IS_DEV) {
geolocationResolution = new Promise(y => y())
device.set(['geolocation'], DEFAULT_GEOLOCATION)
return
}
geolocationResolution = new Promise(async resolve => {
try {
// Try once, fail fast
const geolocation = await getGeolocation()
if (geolocation) {
device.set(['geolocation'], geolocation)
emitGeolocationUpdate(geolocation)
logger.debug(`geolocation: success`, {geolocation})
} else {
// endpoint should throw on all failures, this is insurance
throw new Error(`geolocation: nothing returned from initial request`)
}
} catch (e: any) {
logger.error(`geolocation: failed initial request`, {
safeMessage: e.message,
})
// set to default
device.set(['geolocation'], DEFAULT_GEOLOCATION)
// retry 3 times, but don't await, proceed with default
networkRetry(3, getGeolocation)
.then(geolocation => {
if (geolocation) {
device.set(['geolocation'], geolocation)
emitGeolocationUpdate(geolocation)
logger.debug(`geolocation: success`, {geolocation})
} else {
// endpoint should throw on all failures, this is insurance
throw new Error(`geolocation: nothing returned from retries`)
}
})
.catch((e: any) => {
// complete fail closed
logger.error(`geolocation: failed retries`, {safeMessage: e.message})
})
} finally {
resolve(undefined)
}
})
}
/**
* Ensure that geolocation has been resolved, or at the very least attempted
* once. Subsequent retries will not be captured by this `await`. Those will be
* reported via {@link events}.
*/
export async function ensureGeolocationResolved() {
if (!geolocationResolution) {
throw new Error(`geolocation: beginResolveGeolocation not called yet`)
}
const cached = device.get(['geolocation'])
if (cached) {
logger.debug(`geolocation: using cache`, {cached})
} else {
logger.debug(`geolocation: no cache`)
await geolocationResolution
logger.debug(`geolocation: resolved`, {
resolved: device.get(['geolocation']),
})
}
}
type Context = {
geolocation: Device['geolocation']
}
const context = React.createContext<Context>({
geolocation: DEFAULT_GEOLOCATION,
})
export function Provider({children}: {children: React.ReactNode}) {
const [geolocation, setGeolocation] = React.useState(() => {
const initial = device.get(['geolocation']) || DEFAULT_GEOLOCATION
return initial
})
React.useEffect(() => {
return onGeolocationUpdate(geolocation => {
setGeolocation(geolocation!)
})
}, [])
const ctx = React.useMemo(() => {
return {
geolocation,
}
}, [geolocation])
return <context.Provider value={ctx}>{children}</context.Provider>
}
export function useGeolocation() {
return React.useContext(context)
}
+7 -3
View File
@@ -8,6 +8,7 @@ import {
tryStringify,
} from '#/state/persisted/schema'
import {PersistedApi} from './types'
import {normalizeData} from './util'
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema'
@@ -33,10 +34,10 @@ export async function write<K extends keyof Schema>(
key: K,
value: Schema[K],
): Promise<void> {
_state = {
_state = normalizeData({
..._state,
[key]: value,
}
})
await writeToStorage(_state)
}
write satisfies PersistedApi['write']
@@ -81,6 +82,9 @@ async function readFromStorage(): Promise<Schema | undefined> {
})
}
if (rawData) {
return tryParse(rawData)
const parsed = tryParse(rawData)
if (parsed) {
return normalizeData(parsed)
}
}
}
+8 -5
View File
@@ -9,6 +9,7 @@ import {
tryStringify,
} from '#/state/persisted/schema'
import {PersistedApi} from './types'
import {normalizeData} from './util'
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema'
@@ -56,10 +57,10 @@ export async function write<K extends keyof Schema>(
} catch (e) {
// Ignore and go through the normal path.
}
_state = {
_state = normalizeData({
..._state,
[key]: value,
}
})
writeToStorage(_state)
broadcast.postMessage({event: {type: UPDATE_EVENT, key}})
broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading
@@ -140,9 +141,11 @@ function readFromStorage(): Schema | undefined {
return lastResult
} else {
const result = tryParse(rawData)
lastRawData = rawData
lastResult = result
return result
if (result) {
lastRawData = rawData
lastResult = normalizeData(result)
return lastResult
}
}
}
}
+43 -9
View File
@@ -1,7 +1,8 @@
import {z} from 'zod'
import {deviceLanguageCodes, deviceLocales} from '#/locale/deviceLocales'
import {findSupportedAppLanguage} from '#/locale/helpers'
import {logger} from '#/logger'
import {deviceLocales} from '#/platform/detection'
import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army'
const externalEmbedOptions = ['show', 'hide'] as const
@@ -55,10 +56,39 @@ const schema = z.object({
lastEmailConfirm: z.string().optional(),
}),
languagePrefs: z.object({
primaryLanguage: z.string(), // should move to server
contentLanguages: z.array(z.string()), // should move to server
postLanguage: z.string(), // should move to server
/**
* The target language for translating posts.
*
* BCP-47 2-letter language code without region.
*/
primaryLanguage: z.string(),
/**
* The languages the user can read, passed to feeds.
*
* BCP-47 2-letter language codes without region.
*/
contentLanguages: z.array(z.string()),
/**
* The language(s) the user is currently posting in, configured within the
* composer. Multiple languages are psearate by commas.
*
* BCP-47 2-letter language code without region.
*/
postLanguage: z.string(),
/**
* The user's post language history, used to pre-populate the post language
* selector in the composer. Within each value, multiple languages are
* separated by values.
*
* BCP-47 2-letter language codes without region.
*/
postLanguageHistory: z.array(z.string()),
/**
* The language for UI translations in the app.
*
* BCP-47 2-letter language code with or without region,
* to match with {@link AppLanguage}.
*/
appLanguage: z.string(),
}),
requireAltTextEnabled: z.boolean(), // should move to server
@@ -108,13 +138,17 @@ export const defaults: Schema = {
lastEmailConfirm: undefined,
},
languagePrefs: {
primaryLanguage: deviceLocales[0] || 'en',
contentLanguages: deviceLocales || [],
postLanguage: deviceLocales[0] || 'en',
postLanguageHistory: (deviceLocales || [])
primaryLanguage: deviceLanguageCodes[0] || 'en',
contentLanguages: deviceLanguageCodes || [],
postLanguage: deviceLanguageCodes[0] || 'en',
postLanguageHistory: (deviceLanguageCodes || [])
.concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6),
appLanguage: deviceLocales[0] || 'en',
// try full language tag first, then fallback to language code
appLanguage: findSupportedAppLanguage([
deviceLocales.at(0)?.languageTag,
deviceLanguageCodes[0],
]),
},
requireAltTextEnabled: false,
largeAltBadgeEnabled: false,
+51
View File
@@ -0,0 +1,51 @@
import {parse} from 'bcp-47'
import {dedupArray} from '#/lib/functions'
import {logger} from '#/logger'
import {Schema} from '#/state/persisted/schema'
export function normalizeData(data: Schema) {
const next = {...data}
/**
* Normalize language prefs to ensure that these values only contain 2-letter
* country codes without region.
*/
try {
const langPrefs = {...next.languagePrefs}
langPrefs.primaryLanguage = normalizeLanguageTagToTwoLetterCode(
langPrefs.primaryLanguage,
)
langPrefs.contentLanguages = dedupArray(
langPrefs.contentLanguages.map(lang =>
normalizeLanguageTagToTwoLetterCode(lang),
),
)
langPrefs.postLanguage = langPrefs.postLanguage
.split(',')
.map(lang => normalizeLanguageTagToTwoLetterCode(lang))
.filter(Boolean)
.join(',')
langPrefs.postLanguageHistory = dedupArray(
langPrefs.postLanguageHistory.map(postLanguage => {
return postLanguage
.split(',')
.map(lang => normalizeLanguageTagToTwoLetterCode(lang))
.filter(Boolean)
.join(',')
}),
)
next.languagePrefs = langPrefs
} catch (e: any) {
logger.error(`persisted state: failed to normalize language prefs`, {
safeMessage: e.message,
})
}
return next
}
export function normalizeLanguageTagToTwoLetterCode(lang: string) {
const result = parse(lang).language
return result ?? lang
}
+6 -2
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute} from '#/lib/moderation'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
@@ -113,6 +113,10 @@ function computeSuggestions({
return items.filter(profile => {
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
const isExactMatch = q && profile.handle.toLowerCase() === q
return isExactMatch || !modui.filter || isJustAMute(modui)
return (
(isExactMatch && !moduiContainsHideableOffense(modui)) ||
!modui.filter ||
isJustAMute(modui)
)
})
}
+5
View File
@@ -13,6 +13,7 @@ import {
import {QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation'
import {precacheProfile} from '../profile'
import {FeedNotification, FeedPage, NotificationType} from './types'
@@ -104,6 +105,10 @@ export function shouldFilterNotif(
notif: AppBskyNotificationListNotifications.Notification,
moderationOpts: ModerationOpts | undefined,
): boolean {
const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense)
if (containsImperative) {
return true
}
if (!moderationOpts) {
return false
}
@@ -10,6 +10,10 @@ jest.mock('jwt-decode', () => ({
},
}))
jest.mock('expo-localization', () => ({
getLocales: () => [],
}))
describe('session', () => {
it('can log in and out', () => {
let state = getInitialState([])
@@ -0,0 +1,41 @@
import {BskyAgent} from '@atproto/api'
import {logger} from '#/logger'
import {device} from '#/storage'
export const BR_LABELER = 'did:plc:ekitcvx7uwnauoqy5oest3hm'
export const ADDITIONAL_LABELERS_MAP: {
[countryCode: string]: string[]
} = {
BR: [BR_LABELER],
}
export const ALL_ADDITIONAL_LABELERS = Object.values(
ADDITIONAL_LABELERS_MAP,
).flat()
export const NON_CONFIGURABLE_LABELERS = [BR_LABELER]
export function isNonConfigurableModerationAuthority(did: string) {
return NON_CONFIGURABLE_LABELERS.includes(did)
}
export function configureAdditionalModerationAuthorities() {
const geolocation = device.get(['geolocation'])
let additionalLabelers: string[] = ALL_ADDITIONAL_LABELERS
if (geolocation?.countryCode) {
additionalLabelers = ADDITIONAL_LABELERS_MAP[geolocation.countryCode] ?? []
} else {
logger.info(`no geolocation, cannot apply mod authorities`)
}
const appLabelers = Array.from(
new Set([...BskyAgent.appLabelers, ...additionalLabelers]),
)
logger.info(`applying mod authorities`, {
additionalLabelers,
appLabelers,
})
BskyAgent.configure({appLabelers})
}
+4
View File
@@ -1,6 +1,7 @@
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {IS_TEST_USER} from '#/lib/constants'
import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
import {readLabelers} from './agent-config'
import {SessionAccount} from './types'
@@ -8,6 +9,7 @@ export function configureModerationForGuest() {
// This global mutation is *only* OK because this code is only relevant for testing.
// Don't add any other global behavior here!
switchToBskyAppLabeler()
configureAdditionalModerationAuthorities()
}
export async function configureModerationForAccount(
@@ -31,6 +33,8 @@ export async function configureModerationForAccount(
// If there are no headers in the storage, we'll not send them on the initial requests.
// If we wanted to fix this, we could block on the preferences query here.
}
configureAdditionalModerationAuthorities()
}
function switchToBskyAppLabeler() {
+9 -1
View File
@@ -1,5 +1,6 @@
import {MMKV} from 'react-native-mmkv'
import {IS_DEV} from '#/env'
import {Device} from '#/storage/schema'
export * from '#/storage/schema'
@@ -71,4 +72,11 @@ export class Storage<Scopes extends unknown[], Schema> {
*
* `device.set([key], true)`
*/
export const device = new Storage<[], Device>({id: 'device'})
export const device = new Storage<[], Device>({id: 'bsky_device'})
if (IS_DEV && typeof window !== 'undefined') {
// @ts-ignore
window.bsky_storage = {
device,
}
}
+3
View File
@@ -5,4 +5,7 @@ export type Device = {
fontScale: '-2' | '-1' | '0' | '1' | '2'
fontFamily: 'system' | 'theme'
lastNuxDialog: string | undefined
geolocation?: {
countryCode: string | undefined
}
}
+2 -3
View File
@@ -4,9 +4,9 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button'
@@ -35,8 +35,7 @@ export const SplashScreen = ({
<Logotype width={161} fill={t.atoms.text.color} />
</View>
<Text
style={[a.text_md, a.font_semibold, t.atoms.text_contrast_medium]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
<Trans>What's up?</Trans>
</Text>
</View>
+3 -7
View File
@@ -4,11 +4,11 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useKawaiiMode} from '#/state/preferences/kawaii'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button'
@@ -78,11 +78,7 @@ export const SplashScreen = ({
)}
<Text
style={[
a.text_md,
a.font_semibold,
t.atoms.text_contrast_medium,
]}>
style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
<Trans>What's up?</Trans>
</Text>
</View>
+67 -90
View File
@@ -16,7 +16,7 @@ import {ComposerOptsPostRef} from 'state/shell/composer'
import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
import {Text} from 'view/com/util/text/Text'
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
import {useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const t = useTheme()
@@ -122,94 +122,87 @@ function ComposerReplyToImages({
showFull: boolean
}) {
return (
<View
style={{
width: 65,
flexDirection: 'column',
alignItems: 'center',
}}>
<View style={styles.imagesContainer}>
{(images.length === 1 && (
<Image
source={{uri: images[0].thumb}}
style={styles.singleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<View style={[styles.imagesContainer, a.mx_xs]}>
{(images.length === 1 && (
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
)) ||
(images.length === 2 && (
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
)) ||
(images.length === 2 && (
<View style={[styles.imagesInner, styles.imagesRow]}>
(images.length === 3 && (
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[2].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
)) ||
(images.length === 4 && (
<View style={[a.flex_1, a.gap_2xs]}>
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImageTall}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImageTall}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
)) ||
(images.length === 3 && (
<View style={[styles.imagesInner, styles.imagesRow]}>
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImageTall}
source={{uri: images[2].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[3].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<View style={styles.imagesInner}>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[2].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
)) ||
(images.length === 4 && (
<View style={styles.imagesInner}>
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[2].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[3].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
))}
</View>
</View>
))}
</View>
)
}
@@ -240,23 +233,7 @@ const styles = StyleSheet.create({
borderRadius: 6,
overflow: 'hidden',
marginTop: 2,
},
imagesInner: {
gap: 2,
},
imagesRow: {
flexDirection: 'row',
},
singleImage: {
width: 65,
height: 65,
},
doubleImageTall: {
width: 32.5,
height: 65,
},
doubleImage: {
width: 32.5,
height: 32.5,
height: 64,
width: 64,
},
})
+1 -1
View File
@@ -160,7 +160,7 @@ function AltTextInner({
</View>
<Button
label={_(msg`Save`)}
size="medium"
size="large"
color="primary"
variant="solid"
onPress={onPressSubmit}>
+7 -7
View File
@@ -7,13 +7,13 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {observer} from 'mobx-react-lite'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {Dimensions} from '#/lib/media/types'
import {colors, s} from '#/lib/styles'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Dimensions} from 'lib/media/types'
import {colors, s} from 'lib/styles'
import {isNative} from 'platform/detection'
import {GalleryModel} from 'state/models/media/gallery'
import {Text} from 'view/com/util/text/Text'
import {GalleryModel} from '#/state/models/media/gallery'
import {Text} from '#/view/com/util/text/Text'
import {useTheme} from '#/alf'
const IMAGE_GAP = 8
@@ -263,7 +263,7 @@ const styles = StyleSheet.create({
altTextControlLabel: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
fontWeight: '600',
letterSpacing: 1,
},
altTextHiddenRegion: {
@@ -60,7 +60,7 @@ export function ThreadgateBtn({
<Button
variant="solid"
color="secondary"
size="xsmall"
size="small"
testID="openReplyGateButton"
onPress={onPress}
label={label}
@@ -44,7 +44,7 @@ export function SubtitleDialogBtn(props: Props) {
? _('Opens captions and alt text dialog')
: _('Opens alt text dialog')
}
size="xsmall"
size="small"
color="secondary"
variant="ghost"
onPress={() => {
@@ -169,7 +169,7 @@ function SubtitleDialogInner({
<View style={web([a.flex_row, a.justify_end])}>
<Button
label={_(msg`Done`)}
size={isWeb ? 'small' : 'medium'}
size={isWeb ? 'small' : 'large'}
color="primary"
variant="solid"
onPress={() => {
@@ -57,7 +57,7 @@ export function SubtitleFilePicker({
<Button
onPress={handleClick}
label={_('Select subtitle file (.vtt)')}
size="medium"
size="large"
color="primary"
variant="solid"
disabled={disabled}>
+10 -10
View File
@@ -14,22 +14,22 @@ import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {compressIfNeeded} from '#/lib/media/manip'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {enforceLen} from '#/lib/strings/helpers'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {colors, gradients, s} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext'
import {useModalControls} from '#/state/modals'
import {
useListCreateMutation,
useListMetadataMutation,
} from '#/state/queries/list'
import {useAgent} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {compressIfNeeded} from 'lib/media/manip'
import {cleanError, isNetworkError} from 'lib/strings/errors'
import {enforceLen} from 'lib/strings/helpers'
import {colors, gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
@@ -359,7 +359,7 @@ export function Component({
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: 'bold',
fontWeight: '600',
fontSize: 24,
marginBottom: 18,
},
@@ -373,7 +373,7 @@ const styles = StyleSheet.create({
marginTop: 20,
},
label: {
fontWeight: 'bold',
fontWeight: '600',
},
form: {
paddingHorizontal: 6,
+10 -10
View File
@@ -8,16 +8,16 @@ import {Slider} from '@miblanchard/react-native-slider'
import {observer} from 'mobx-react-lite'
import ImageEditor, {Position} from 'react-avatar-editor'
import {MAX_ALT_TEXT} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {enforceLen} from '#/lib/strings/helpers'
import {gradients, s} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext'
import {getKeys} from '#/lib/type-assertions'
import {useModalControls} from '#/state/modals'
import {MAX_ALT_TEXT} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {enforceLen} from 'lib/strings/helpers'
import {gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {getKeys} from 'lib/type-assertions'
import {GalleryModel} from 'state/models/media/gallery'
import {ImageModel} from 'state/models/media/image'
import {GalleryModel} from '#/state/models/media/gallery'
import {ImageModel} from '#/state/models/media/image'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {
@@ -333,7 +333,7 @@ const styles = StyleSheet.create({
subsection: {marginTop: 12},
gap18: {gap: 18},
title: {
fontWeight: 'bold',
fontWeight: '600',
fontSize: 24,
},
btns: {
+11 -11
View File
@@ -15,18 +15,18 @@ import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
import {compressIfNeeded} from '#/lib/media/manip'
import {cleanError} from '#/lib/strings/errors'
import {enforceLen} from '#/lib/strings/helpers'
import {colors, gradients, s} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useProfileUpdateMutation} from '#/state/queries/profile'
import {useAnalytics} from 'lib/analytics/analytics'
import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette'
import {compressIfNeeded} from 'lib/media/manip'
import {cleanError} from 'lib/strings/errors'
import {enforceLen} from 'lib/strings/helpers'
import {colors, gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
@@ -261,12 +261,12 @@ export function Component({
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: 'bold',
fontWeight: '600',
fontSize: 24,
marginBottom: 18,
},
label: {
fontWeight: 'bold',
fontWeight: '600',
paddingHorizontal: 4,
paddingBottom: 4,
marginTop: 20,
+7 -8
View File
@@ -1,19 +1,18 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {s} from 'lib/styles'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ScrollView} from './util'
import {usePalette} from 'lib/hooks/usePalette'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {s} from '#/lib/styles'
import {useModalControls} from '#/state/modals'
import {
useOpenLink,
useSetInAppBrowser,
} from '#/state/preferences/in-app-browser'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import {ScrollView} from './util'
export const snapPoints = [350]
@@ -89,7 +88,7 @@ export function Component({href}: {href: string}) {
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: 'bold',
fontWeight: '600',
fontSize: 24,
marginBottom: 12,
},
+6 -6
View File
@@ -9,7 +9,12 @@ import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {s} from '#/lib/styles'
import {isAndroid, isMobileWeb, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {
getMembership,
@@ -19,11 +24,6 @@ import {
useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships'
import {useSession} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {s} from 'lib/styles'
import {isAndroid, isMobileWeb, isWeb} from 'platform/detection'
import {MyLists} from '../lists/MyLists'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
@@ -71,7 +71,7 @@ export function Component({
style={[
{
textAlign: 'center',
fontWeight: 'bold',
fontWeight: '600',
fontSize: 20,
marginBottom: 12,
paddingHorizontal: 12,
@@ -1,19 +1,20 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ScrollView} from '../util'
import {Text} from '../../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {LanguageToggle} from './LanguageToggle'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {Trans} from '@lingui/macro'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {deviceLanguageCodes} from '#/locale/deviceLocales'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
useLanguagePrefsApi,
} from '#/state/preferences/languages'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {Text} from '../../util/text/Text'
import {ScrollView} from '../util'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {LanguageToggle} from './LanguageToggle'
export const snapPoints = ['100%']
@@ -37,10 +38,10 @@ export function Component({}: {}) {
langs.sort((a, b) => {
const hasA =
langPrefs.contentLanguages.includes(a.code2) ||
deviceLocales.includes(a.code2)
deviceLanguageCodes.includes(a.code2)
const hasB =
langPrefs.contentLanguages.includes(b.code2) ||
deviceLocales.includes(b.code2)
deviceLanguageCodes.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
@@ -110,7 +111,7 @@ const styles = StyleSheet.create({
},
title: {
textAlign: 'center',
fontWeight: 'bold',
fontWeight: '600',
fontSize: 24,
marginBottom: 12,
},
@@ -1,20 +1,21 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ScrollView} from '../util'
import {Text} from '../../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {Trans} from '@lingui/macro'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {deviceLanguageCodes} from '#/locale/deviceLocales'
import {useModalControls} from '#/state/modals'
import {
hasPostLanguage,
useLanguagePrefs,
useLanguagePrefsApi,
hasPostLanguage,
} from '#/state/preferences/languages'
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {Text} from '../../util/text/Text'
import {ScrollView} from '../util'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
export const snapPoints = ['100%']
@@ -38,10 +39,10 @@ export function Component() {
langs.sort((a, b) => {
const hasA =
hasPostLanguage(langPrefs.postLanguage, a.code2) ||
deviceLocales.includes(a.code2)
deviceLanguageCodes.includes(a.code2)
const hasB =
hasPostLanguage(langPrefs.postLanguage, b.code2) ||
deviceLocales.includes(b.code2)
deviceLanguageCodes.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
@@ -118,7 +119,7 @@ const styles = StyleSheet.create({
},
title: {
textAlign: 'center',
fontWeight: 'bold',
fontWeight: '600',
fontSize: 24,
marginBottom: 12,
},

Some files were not shown because too many files have changed in this diff Show More