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-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error', 'bsky-internal/use-typed-gates': 'error',
'bsky-internal/use-prefixed-imports': 'warn',
'simple-import-sort/imports': [ 'simple-import-sort/imports': [
'warn', 'warn',
{ {
+6
View File
@@ -158,6 +158,12 @@ export function Embed({
return <Info>The quoted post is blocked.</Info> 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 // Unknown embed type
return null 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-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-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-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-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-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-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-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-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-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-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-Black.66e9a87f1c921e844ed4.otf">
<link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BlackItalic.27b9f0ad06fd13a7b9da.otf"> <link rel="preload" as="font" type="font/otf" href="/static/media/Inter-BlackItalic.27b9f0ad06fd13a7b9da.otf">
-->
<style> <style>
@font-face { @font-face {
@@ -41,6 +47,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
/*
@font-face { @font-face {
font-family: "Inter-Medium"; font-family: "Inter-Medium";
src: local("Inter-Medium"), url(/static/media/Inter-Medium.296aa2d65964269836b3.otf) format("font/otf"); src: local("Inter-Medium"), url(/static/media/Inter-Medium.296aa2d65964269836b3.otf) format("font/otf");
@@ -55,6 +62,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
*/
@font-face { @font-face {
font-family: "Inter-SemiBold"; font-family: "Inter-SemiBold";
src: local("Inter-SemiBold"), url(/static/media/Inter-SemiBold.2277990330981b8409bb.otf) format("font/otf"); src: local("Inter-SemiBold"), url(/static/media/Inter-SemiBold.2277990330981b8409bb.otf) format("font/otf");
@@ -69,6 +77,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
/*
@font-face { @font-face {
font-family: "Inter-Bold"; font-family: "Inter-Bold";
src: local("Inter-Bold"), url(/static/media/Inter-Bold.8d330503e1d034ad68de.otf) format("font/otf"); src: local("Inter-Bold"), url(/static/media/Inter-Bold.8d330503e1d034ad68de.otf) format("font/otf");
@@ -83,6 +92,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
*/
@font-face { @font-face {
font-family: "Inter-ExtraBold"; font-family: "Inter-ExtraBold";
src: local("Inter-ExtraBold"), url(/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf) format("font/otf"); src: local("Inter-ExtraBold"), url(/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf) format("font/otf");
@@ -97,6 +107,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
/*
@font-face { @font-face {
font-family: "Inter-Black"; font-family: "Inter-Black";
src: local("Inter-Black"), url(/static/media/Inter-Black.66e9a87f1c921e844ed4.otf) format("font/otf"); src: local("Inter-Black"), url(/static/media/Inter-Black.66e9a87f1c921e844ed4.otf) format("font/otf");
@@ -111,6 +122,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
*/
/** /**
* Extend the react-native-web reset: * Extend the react-native-web reset:
+1
View File
@@ -5,5 +5,6 @@ module.exports = {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'), 'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
'use-exact-imports': require('./use-exact-imports'), 'use-exact-imports': require('./use-exact-imports'),
'use-typed-gates': require('./use-typed-gates'), '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 = [ const BANNED_IMPORTS = [
'@fortawesome/free-regular-svg-icons', '@fortawesome/free-regular-svg-icons',
'@fortawesome/free-solid-svg-icons', '@fortawesome/free-solid-svg-icons',
@@ -6,11 +5,12 @@ const BANNED_IMPORTS = [
exports.create = function create(context) { exports.create = function create(context) {
return { return {
Literal(node) { ImportDeclaration(node) {
if (typeof node.value !== 'string') { const source = node.source
if (typeof source.value !== 'string') {
return return
} }
if (BANNED_IMPORTS.includes(node.value)) { if (BANNED_IMPORTS.includes(source.value)) {
context.report({ context.report({
node, node,
message: 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", "await-lock": "^2.2.2",
"babel-plugin-transform-remove-console": "^6.9.4", "babel-plugin-transform-remove-console": "^6.9.4",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"bcp-47": "^2.1.0",
"bcp-47-match": "^2.0.3", "bcp-47-match": "^2.0.3",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"deprecated-react-native-prop-types": "^5.0.0", "deprecated-react-native-prop-types": "^5.0.0",
@@ -204,6 +205,7 @@
"statsig-react-native-expo": "^4.6.1", "statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tlds": "^1.234.0", "tlds": "^1.234.0",
"tldts": "^6.1.46",
"zeego": "^1.6.2", "zeego": "^1.6.2",
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
+12
View File
@@ -12,3 +12,15 @@ index bb74e80..0aa0202 100644
Map<String, Object> constants = new HashMap<>(3); Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>()); 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() {
+15 -1
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 MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events' import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' import {MessagesProvider} from '#/state/messages'
@@ -66,6 +71,11 @@ import {BackgroundNotificationPreferencesProvider} from '../modules/expo-backgro
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
/**
* Begin geolocation ASAP
*/
beginResolveGeolocation()
function InnerApp() { function InnerApp() {
const [isReady, setIsReady] = React.useState(false) const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -158,7 +168,9 @@ function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
React.useEffect(() => { React.useEffect(() => {
initPersistedState().then(() => setReady(true)) Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, []) }, [])
if (!isReady) { if (!isReady) {
@@ -170,6 +182,7 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<GeolocationProvider>
<A11yProvider> <A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}> <KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider> <SessionProvider>
@@ -200,6 +213,7 @@ function App() {
</SessionProvider> </SessionProvider>
</KeyboardProvider> </KeyboardProvider>
</A11yProvider> </A11yProvider>
</GeolocationProvider>
) )
} }
+15 -1
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 MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events' import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' 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 {Provider as PortalProvider} from '#/components/Portal'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
/**
* Begin geolocation ASAP
*/
beginResolveGeolocation()
function InnerApp() { function InnerApp() {
const [isReady, setIsReady] = React.useState(false) const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -148,7 +158,9 @@ function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
React.useEffect(() => { React.useEffect(() => {
initPersistedState().then(() => setReady(true)) Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
setReady(true),
)
}, []) }, [])
if (!isReady) { if (!isReady) {
@@ -160,6 +172,7 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<GeolocationProvider>
<A11yProvider> <A11yProvider>
<SessionProvider> <SessionProvider>
<PrefsStateProvider> <PrefsStateProvider>
@@ -185,6 +198,7 @@ function App() {
</PrefsStateProvider> </PrefsStateProvider>
</SessionProvider> </SessionProvider>
</A11yProvider> </A11yProvider>
</GeolocationProvider>
) )
} }
+3 -6
View File
@@ -276,16 +276,13 @@ export const atoms = {
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
}, },
font_normal: { font_normal: {
fontWeight: tokens.fontWeight.normal, fontWeight: tokens.fontWeight.regular,
},
font_semibold: {
fontWeight: tokens.fontWeight.semibold,
}, },
font_bold: { font_bold: {
fontWeight: tokens.fontWeight.bold, fontWeight: tokens.fontWeight.semibold,
}, },
font_heavy: { font_heavy: {
fontWeight: tokens.fontWeight.heavy, fontWeight: tokens.fontWeight.extrabold,
}, },
italic: { italic: {
fontStyle: '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-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'),
'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'), 'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'),
'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'), 'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'),
'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'), // 'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'), // 'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'), 'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'),
'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'), 'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'), // 'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'), // 'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'), 'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'),
'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'), 'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'), // 'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'), // 'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
}) })
} }
+9 -4
View File
@@ -47,11 +47,16 @@ export const borderRadius = {
full: 999, full: 999,
} as const } as const
/**
* These correspond to Inter font files we actually load.
*/
export const fontWeight = { export const fontWeight = {
normal: '400', regular: '400',
semibold: '500', // medium: '500',
bold: '600', semibold: '600',
heavy: '700', // bold: '700',
extrabold: '800',
// black: '900',
} as const } as const
export const gradients = { export const gradients = {
-2
View File
@@ -24,8 +24,6 @@ export function AppLanguageDropdown() {
if (sanitizedLang !== value) { if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
} }
setLangPrefs.setPrimaryLanguage(value)
setLangPrefs.setContentLanguage(value)
// reset feeds to refetch content // reset feeds to refetch content
resetPostsFeedQueries(queryClient) resetPostsFeedQueries(queryClient)
@@ -27,8 +27,6 @@ export function AppLanguageDropdown() {
if (sanitizedLang !== value) { if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
} }
setLangPrefs.setPrimaryLanguage(value)
setLangPrefs.setContentLanguage(value)
// reset feeds to refetch content // reset feeds to refetch content
resetPostsFeedQueries(queryClient) resetPostsFeedQueries(queryClient)
+108 -34
View File
@@ -14,7 +14,7 @@ import {
} from 'react-native' } from 'react-native'
import {LinearGradient} from 'expo-linear-gradient' 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 {Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -30,7 +30,7 @@ export type ButtonColor =
| 'gradient_sunset' | 'gradient_sunset'
| 'gradient_nordic' | 'gradient_nordic'
| 'gradient_bonfire' | 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large' export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default' export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = { export type VariantProps = {
/** /**
@@ -343,39 +343,46 @@ export const Button = React.forwardRef<View, ButtonProps>(
if (shape === 'default') { if (shape === 'default') {
if (size === 'large') { if (size === 'large') {
baseStyles.push( baseStyles.push({
{paddingVertical: 15}, paddingVertical: 13,
a.px_2xl, paddingHorizontal: 20,
a.rounded_sm, borderRadius: 8,
a.gap_md, gap: 8,
) })
} else if (size === 'medium') {
baseStyles.push(
{paddingVertical: 12},
a.px_2xl,
a.rounded_sm,
a.gap_md,
)
} else if (size === 'small') { } else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) baseStyles.push({
} else if (size === 'xsmall') { paddingVertical: 8,
baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm) paddingHorizontal: 12,
borderRadius: 6,
gap: 6,
})
} else if (size === 'tiny') { } 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') { } else if (shape === 'round' || shape === 'square') {
if (size === 'large') { if (size === 'large') {
if (shape === 'round') { if (shape === 'round') {
baseStyles.push({height: 54, width: 54}) baseStyles.push({height: 46, width: 46})
} else { } else {
baseStyles.push({height: 50, width: 50}) baseStyles.push({height: 44, width: 44})
} }
} else if (size === 'small') { } else if (size === 'small') {
if (shape === 'round') {
baseStyles.push({height: 36, width: 36})
} else {
baseStyles.push({height: 34, width: 34}) baseStyles.push({height: 34, width: 34})
} else if (size === 'xsmall') { }
baseStyles.push({height: 28, width: 28})
} else if (size === 'tiny') { } 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') { if (shape === 'round') {
@@ -619,11 +626,11 @@ export function useSharedButtonTextStyles() {
} }
if (size === 'large') { 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') { } else if (size === 'tiny') {
baseStyles.push(a.text_xs, android({paddingBottom: 1})) baseStyles.push(a.text_xs, a.leading_tight)
} else {
baseStyles.push(a.text_sm, android({paddingBottom: 1}))
} }
return StyleSheet.flatten(baseStyles) return StyleSheet.flatten(baseStyles)
@@ -643,31 +650,98 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
export function ButtonIcon({ export function ButtonIcon({
icon: Comp, icon: Comp,
position, position,
size: iconSize, size,
}: { }: {
icon: React.ComponentType<SVGIconProps> icon: React.ComponentType<SVGIconProps>
position?: 'left' | 'right' position?: 'left' | 'right'
size?: SVGIconProps['size'] size?: SVGIconProps['size']
}) { }) {
const {size, disabled} = useButtonContext() const {size: buttonSize, disabled} = useButtonContext()
const textStyles = useSharedButtonTextStyles() 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 ( return (
<View <View
style={[ style={[
a.z_20, a.z_20,
{ {
width: iconContainerSize,
height: iconContainerSize,
opacity: disabled ? 0.7 : 1, opacity: disabled ? 0.7 : 1,
marginLeft: position === 'left' ? -2 : 0, marginLeft: position === 'left' ? -2 : 0,
marginRight: position === 'right' ? -2 : 0, marginRight: position === 'right' ? -2 : 0,
}, },
]}> ]}>
<View
style={[
a.absolute,
{
width: iconSize,
height: iconSize,
top: '50%',
left: '50%',
transform: [
{
translateX: (iconSize / 2) * -1,
},
{
translateY: (iconSize / 2) * -1,
},
],
},
]}>
<Comp <Comp
size={ width={iconSize}
iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm') style={[
} {
style={[{color: textStyles.color, pointerEvents: 'none'}]} color: textStyles.color,
pointerEvents: 'none',
},
]}
/> />
</View> </View>
</View>
) )
} }
+25 -5
View File
@@ -9,6 +9,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {useLabelerInfoQuery} from '#/state/queries/labeler' import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf' 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 {Link as InternalLink, LinkProps} from '#/components/Link'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -43,21 +44,40 @@ export function Avatar({avatar}: {avatar?: string}) {
} }
export function Title({value}: {value: 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}) { export function Description({value, handle}: {value?: string; handle: string}) {
return value ? ( return value ? (
<Text numberOfLines={2}> <Text numberOfLines={2}>
<RichText value={value} style={[]} /> <RichText value={value} style={[a.leading_snug]} />
</Text> </Text>
) : ( ) : (
<Text> <Text style={[a.leading_snug]}>
<Trans>By {sanitizeHandle(handle, '@')}</Trans> <Trans>By {sanitizeHandle(handle, '@')}</Trans>
</Text> </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}) { export function LikeCount({count}: {count: number}) {
const t = useTheme() const t = useTheme()
return ( return (
@@ -66,7 +86,7 @@ export function LikeCount({count}: {count: number}) {
a.mt_sm, a.mt_sm,
a.text_sm, a.text_sm,
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
{fontWeight: '500'}, {fontWeight: '600'},
]}> ]}>
<Plural value={count} one="Liked by # user" other="Liked by # users" /> <Plural value={count} one="Liked by # user" other="Liked by # users" />
</Text> </Text>
@@ -85,7 +105,7 @@ export function Content({children}: React.PropsWithChildren<{}>) {
a.align_center, a.align_center,
a.justify_between, 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]} /> <ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
</View> </View>
+1 -1
View File
@@ -24,7 +24,7 @@ export function MediaInsetBorder({
return ( return (
<Fill <Fill
style={[ style={[
a.rounded_sm, a.rounded_md,
a.border, a.border,
opaque opaque
? [t.atoms.border_contrast_low] ? [t.atoms.border_contrast_low]
+1 -1
View File
@@ -170,6 +170,6 @@ const styles = StyleSheet.create({
alt: { alt: {
color: 'white', color: 'white',
fontSize: 7, fontSize: 7,
fontWeight: 'bold', fontWeight: '600',
}, },
}) })
+2
View File
@@ -179,8 +179,10 @@ export function Outer({
style={[ style={[
a.rounded_sm, a.rounded_sm,
a.p_xs, a.p_xs,
a.border,
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25, t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
t.atoms.shadow_md, t.atoms.shadow_md,
t.atoms.border_contrast_low,
style, style,
]}> ]}>
{children} {children}
+1 -1
View File
@@ -132,7 +132,7 @@ export function Label({
<Text <Text
style={[ style={[
text, text,
a.font_semibold, a.font_bold,
a.leading_tight, a.leading_tight,
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
{paddingRight: 3}, {paddingRight: 3},
@@ -411,6 +411,7 @@ function Inner({
() => currentAccount?.did === profile.did, () => currentAccount?.did === profile.did,
[currentAccount, profile], [currentAccount, profile],
) )
const isLabeler = profile.associated?.labeler
return ( return (
<View> <View>
@@ -419,11 +420,13 @@ function Inner({
<UserAvatar <UserAvatar
size={64} size={64}
avatar={profile.avatar} avatar={profile.avatar}
type={isLabeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
/> />
</Link> </Link>
{!isMe && {!isMe &&
!isLabeler &&
(isBlockedUser ? ( (isBlockedUser ? (
<Link <Link
to={profileURL} to={profileURL}
+1 -1
View File
@@ -26,7 +26,7 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
<Text <Text
style={[ style={[
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
a.font_semibold, a.font_bold,
a.text_sm, a.text_sm,
{textTransform: 'uppercase'}, {textTransform: 'uppercase'},
]}> ]}>
+1 -3
View File
@@ -35,9 +35,7 @@ export function ProgressGuideTask({
)} )}
<View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}> <View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
<Text style={[a.text_sm, a.font_semibold, a.leading_tight]}> <Text style={[a.text_sm, a.font_bold, a.leading_tight]}>{title}</Text>
{title}
</Text>
{subtitle && ( {subtitle && (
<Text <Text
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}> 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} ref={animatedCheckRef}
/> />
<View> <View>
<Text style={[a.text_md, a.font_semibold]}>{title}</Text> <Text style={[a.text_md, a.font_bold]}>{title}</Text>
{subtitle && ( {subtitle && (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{subtitle} {subtitle}
+2 -2
View File
@@ -120,7 +120,7 @@ export function Cancel({
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size={gtMobile ? 'small' : 'medium'} size={gtMobile ? 'small' : 'large'}
label={cta || _(msg`Cancel`)} label={cta || _(msg`Cancel`)}
onPress={onPress}> onPress={onPress}>
<ButtonText>{cta || _(msg`Cancel`)}</ButtonText> <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
@@ -163,7 +163,7 @@ export function Action({
<Button <Button
variant="solid" variant="solid"
color={color} color={color}
size={gtMobile ? 'small' : 'medium'} size={gtMobile ? 'small' : 'large'}
label={cta || _(msg`Confirm`)} label={cta || _(msg`Confirm`)}
onPress={handleOnPress} onPress={handleOnPress}
testID={testID}> testID={testID}>
@@ -77,8 +77,7 @@ function LabelerButton({
handle: labeler.creator.handle, handle: labeler.creator.handle,
})} })}
/> />
<Text <Text style={[t.atoms.text_contrast_medium, a.text_sm, a.font_bold]}>
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}>
@{labeler.creator.handle} @{labeler.creator.handle}
</Text> </Text>
</LabelingServiceCard.Content> </LabelingServiceCard.Content>
@@ -125,7 +125,7 @@ export function WizardEditListDialog({
label={_(msg`Close`)} label={_(msg`Close`)}
variant="ghost" variant="ghost"
color="primary" color="primary"
size="xsmall" size="small"
onPress={() => control.close()}> onPress={() => control.close()}>
<ButtonText> <ButtonText>
<Trans>Close</Trans> <Trans>Close</Trans>
@@ -101,7 +101,7 @@ function WizardListCard({
label={_(msg`Remove`)} label={_(msg`Remove`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="xsmall" size="small"
style={[a.self_center, {marginLeft: 'auto'}]} style={[a.self_center, {marginLeft: 'auto'}]}
onPress={onPress}> onPress={onPress}>
<ButtonText> <ButtonText>
+1 -1
View File
@@ -117,7 +117,7 @@ function BirthdayInner({
<View style={isWeb && [a.flex_row, a.justify_end]}> <View style={isWeb && [a.flex_row, a.justify_end]}>
<Button <Button
label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)} label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)}
size="medium" size="large"
onPress={onSave} onPress={onSave}
variant="solid" variant="solid"
color="primary"> color="primary">
+1 -1
View File
@@ -120,7 +120,7 @@ function EmbedDialogInner({
label={_(msg`Copy code`)} label={_(msg`Copy code`)}
color="primary" color="primary"
variant="solid" variant="solid"
size="medium" size="large"
onPress={() => { onPress={() => {
ref.current?.focus() ref.current?.focus()
ref.current?.setSelection(0, snippet.length) ref.current?.setSelection(0, snippet.length)
+3 -3
View File
@@ -83,7 +83,7 @@ export function EmbedConsentDialog({
onPress={onShowAllPress} onPress={onShowAllPress}
onAccessibilityEscape={control.close} onAccessibilityEscape={control.close}
color="primary" color="primary"
size="medium" size="large"
variant="solid"> variant="solid">
<ButtonText> <ButtonText>
<Trans>Enable external media</Trans> <Trans>Enable external media</Trans>
@@ -95,7 +95,7 @@ export function EmbedConsentDialog({
onPress={onShowPress} onPress={onShowPress}
onAccessibilityEscape={control.close} onAccessibilityEscape={control.close}
color="secondary" color="secondary"
size="medium" size="large"
variant="solid"> variant="solid">
<ButtonText> <ButtonText>
<Trans>Enable {externalEmbedLabels[source]} only</Trans> <Trans>Enable {externalEmbedLabels[source]} only</Trans>
@@ -106,7 +106,7 @@ export function EmbedConsentDialog({
onAccessibilityEscape={control.close} onAccessibilityEscape={control.close}
onPress={onHidePress} onPress={onHidePress}
color="secondary" color="secondary"
size="medium" size="large"
variant="ghost"> variant="ghost">
<ButtonText> <ButtonText>
<Trans>No thanks</Trans> <Trans>No thanks</Trans>
+1 -1
View File
@@ -244,7 +244,7 @@ function ModalError({details, close}: {details?: string; close: () => void}) {
label={_(msg`Close dialog`)} label={_(msg`Close dialog`)}
onPress={close} onPress={close}
color="primary" color="primary"
size="medium" size="large"
variant="solid"> variant="solid">
<ButtonText> <ButtonText>
<Trans>Close</Trans> <Trans>Close</Trans>
+1 -1
View File
@@ -264,7 +264,7 @@ function DialogError({details}: {details?: string}) {
label={_(msg`Close dialog`)} label={_(msg`Close dialog`)}
onPress={() => control.close()} onPress={() => control.close()}
color="primary" color="primary"
size="medium" size="large"
variant="solid"> variant="solid">
<ButtonText> <ButtonText>
<Trans>Close</Trans> <Trans>Close</Trans>
+1 -1
View File
@@ -319,7 +319,7 @@ function MutedWordsInner() {
<Button <Button
disabled={isPending || !field} disabled={isPending || !field}
label={_(msg`Add mute word for configured settings`)} label={_(msg`Add mute word for configured settings`)}
size="medium" size="large"
color="primary" color="primary"
variant="solid" variant="solid"
style={[]} style={[]}
@@ -439,7 +439,7 @@ export function PostInteractionSettingsForm({
onPress={onSave} onPress={onSave}
onAccessibilityEscape={control.close} onAccessibilityEscape={control.close}
color="primary" color="primary"
size="medium" size="large"
variant="solid" variant="solid"
style={a.mt_xl}> style={a.mt_xl}>
<ButtonText>{_(msg`Save`)}</ButtonText> <ButtonText>{_(msg`Save`)}</ButtonText>
@@ -491,9 +491,7 @@ function Selectable({
}, },
style, style,
]}> ]}>
<Text style={[a.text_sm, isSelected && a.font_semibold]}> <Text style={[a.text_sm, isSelected && a.font_bold]}>{label}</Text>
{label}
</Text>
{isSelected ? ( {isSelected ? (
<Check size="sm" fill={t.palette.primary_500} /> <Check size="sm" fill={t.palette.primary_500} />
) : ( ) : (
@@ -48,7 +48,7 @@ export function NeueTypography() {
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}> <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
<View style={[a.gap_xl]}> <View style={[a.gap_xl]}>
<View style={[a.gap_md]}> <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> <Trans>New font settings ✨</Trans>
</Text> </Text>
<Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}> <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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session' 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 {formatCount} from '#/view/com/util/numeric/format'
import * as Toast from '#/view/com/util/Toast'
import {Logomark} from '#/view/icons/Logomark' import {Logomark} from '#/view/icons/Logomark'
import * as Toast from 'view/com/util/Toast'
import { import {
atoms as a, atoms as a,
ThemeProvider, ThemeProvider,
@@ -441,10 +441,10 @@ export function TenMillionInner({
allowFontScaling={false} allowFontScaling={false}
style={[ style={[
a.absolute, a.absolute,
a.font_heavy,
{ {
color: t.palette.primary_500, color: t.palette.primary_500,
fontSize: 32, fontSize: 32,
fontWeight: '900',
width: 32, width: 32,
top: isNative ? -10 : 0, top: isNative ? -10 : 0,
left: 0, left: 0,
@@ -462,11 +462,11 @@ export function TenMillionInner({
style={[ style={[
a.relative, a.relative,
a.text_center, a.text_center,
a.font_heavy,
{ {
fontStyle: 'italic', fontStyle: 'italic',
fontSize: getFontSize(userNumber), fontSize: getFontSize(userNumber),
lineHeight: getFontSize(userNumber), lineHeight: getFontSize(userNumber),
fontWeight: '900',
letterSpacing: -2, letterSpacing: -2,
}, },
]}> ]}>
@@ -536,7 +536,7 @@ export function TenMillionInner({
style={[ style={[
a.flex_1, a.flex_1,
a.text_sm, a.text_sm,
a.font_semibold, a.font_bold,
a.leading_snug, a.leading_snug,
lightTheme.atoms.text_contrast_medium, lightTheme.atoms.text_contrast_medium,
]}> ]}>
@@ -551,7 +551,7 @@ export function TenMillionInner({
style={[ style={[
a.flex_1, a.flex_1,
a.text_sm, a.text_sm,
a.font_semibold, a.font_bold,
a.leading_snug, a.leading_snug,
a.text_right, a.text_right,
lightTheme.atoms.text_contrast_low, lightTheme.atoms.text_contrast_low,
@@ -643,14 +643,7 @@ export function TenMillionInner({
<View style={[gtMobile ? a.p_2xl : a.p_xl]}> <View style={[gtMobile ? a.p_2xl : a.p_xl]}>
<Text <Text
allowFontScaling={false} allowFontScaling={false}
style={[ style={[a.text_5xl, a.leading_tight, a.pb_lg, a.font_heavy]}>
a.text_5xl,
a.leading_tight,
a.pb_lg,
{
fontWeight: '900',
},
]}>
<Trans>Thanks for being one of our first 10 million users.</Trans> <Trans>Thanks for being one of our first 10 million users.</Trans>
</Text> </Text>
+1 -1
View File
@@ -160,7 +160,7 @@ function DialogInner({
<Button <Button
label={_(msg`Start chatting`)} label={_(msg`Start chatting`)}
accessibilityHint={_(msg`Close modal`)} accessibilityHint={_(msg`Close modal`)}
size="medium" size="large"
color="primary" color="primary"
variant="solid" variant="solid"
onPress={() => control.close()}> onPress={() => control.close()}>
+1 -1
View File
@@ -76,7 +76,7 @@ export function DateField({
<Button <Button
label={_(msg`Done`)} label={_(msg`Done`)}
onPress={() => control.close()} onPress={() => control.close()}
size="medium" size="large"
color="primary" color="primary"
variant="solid"> variant="solid">
<ButtonText> <ButtonText>
@@ -112,7 +112,7 @@ function Inner({control}: {control: DialogControlProps}) {
onPress={() => control.close()} onPress={() => control.close()}
variant="solid" variant="solid"
color={status === 'failure' ? 'secondary' : 'primary'} color={status === 'failure' ? 'secondary' : 'primary'}
size="medium" size="large"
style={{marginLeft: 'auto'}}> style={{marginLeft: 'auto'}}>
<ButtonText> <ButtonText>
<Trans>Close</Trans> <Trans>Close</Trans>
@@ -124,7 +124,7 @@ function Inner({control}: {control: DialogControlProps}) {
onPress={onPressResendEmail} onPress={onPressResendEmail}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
disabled={sending}> disabled={sending}>
<ButtonText> <ButtonText>
<Trans>Resend Email</Trans> <Trans>Resend Email</Trans>
+2 -2
View File
@@ -94,7 +94,7 @@ export function ContentHider({
a.text_left, a.text_left,
a.font_bold, a.font_bold,
a.leading_snug, a.leading_snug,
gtMobile && [a.font_semibold], gtMobile && [a.font_bold],
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
web({ web({
marginBottom: 1, marginBottom: 1,
@@ -107,7 +107,7 @@ export function ContentHider({
style={[ style={[
a.font_bold, a.font_bold,
a.leading_snug, a.leading_snug,
gtMobile && [a.font_semibold], gtMobile && [a.font_bold],
t.atoms.text_contrast_high, t.atoms.text_contrast_high,
web({ web({
marginBottom: 1, marginBottom: 1,
@@ -236,8 +236,7 @@ export function LabelerLabelPreference({
<View style={[a.flex_row, a.gap_xs, a.align_center, a.mt_xs]}> <View style={[a.flex_row, a.gap_xs, a.align_center, a.mt_xs]}>
<CircleInfo size="sm" fill={t.atoms.text_contrast_high.color} /> <CircleInfo size="sm" fill={t.atoms.text_contrast_high.color} />
<Text <Text style={[t.atoms.text_contrast_medium, a.font_bold, a.italic]}>
style={[t.atoms.text_contrast_medium, a.font_semibold, a.italic]}>
{adultDisabled ? ( {adultDisabled ? (
<Trans>Adult content is disabled.</Trans> <Trans>Adult content is disabled.</Trans>
) : isGlobalLabel ? ( ) : isGlobalLabel ? (
@@ -279,7 +279,7 @@ function AppealForm({
testID="backBtn" testID="backBtn"
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onPressBack} onPress={onPressBack}
label={_(msg`Back`)}> label={_(msg`Back`)}>
<ButtonText>{_(msg`Back`)}</ButtonText> <ButtonText>{_(msg`Back`)}</ButtonText>
@@ -288,7 +288,7 @@ function AppealForm({
testID="submitBtn" testID="submitBtn"
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
onPress={onSubmit} onPress={onSubmit}
label={_(msg`Submit`)}> label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText> <ButtonText>{_(msg`Submit`)}</ButtonText>
+4 -10
View File
@@ -10,9 +10,9 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 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 {CenteredView} from '#/view/com/util/Views'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -86,13 +86,7 @@ export function ScreenHider({
</View> </View>
</View> </View>
<Text <Text
style={[ style={[a.text_4xl, a.font_bold, a.text_center, a.mb_md, t.atoms.text]}>
a.text_4xl,
a.font_semibold,
a.text_center,
a.mb_md,
t.atoms.text,
]}>
{isNoPwi ? ( {isNoPwi ? (
<Trans>Sign-in Required</Trans> <Trans>Sign-in Required</Trans>
) : ( ) : (
@@ -118,7 +112,7 @@ export function ScreenHider({
<Text <Text
style={[ style={[
a.text_lg, a.text_lg,
a.font_semibold, a.font_bold,
a.leading_snug, a.leading_snug,
t.atoms.text, t.atoms.text,
a.ml_xs, 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' 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({ export function getLabelingServiceTitle({
displayName, displayName,
handle, 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 {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native'
import {isWeb} from 'platform/detection' import {isWeb} from '#/platform/detection'
import {Theme, TypographyVariant} from './ThemeContext' import {Theme, TypographyVariant} from './ThemeContext'
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest // 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 // font weights
fw600: {fontWeight: '600'}, fw600: {fontWeight: '600'},
bold: {fontWeight: '700'}, bold: {fontWeight: '600'},
fw500: {fontWeight: '500'}, fw500: {fontWeight: '600'},
semiBold: {fontWeight: '500'}, semiBold: {fontWeight: '600'},
fw400: {fontWeight: '400'}, fw400: {fontWeight: '400'},
normal: {fontWeight: '400'}, normal: {fontWeight: '400'},
fw300: {fontWeight: '400'}, fw300: {fontWeight: '400'},
light: {fontWeight: '400'}, light: {fontWeight: '400'},
fw200: {fontWeight: '200'},
// text decoration // text decoration
underline: {textDecorationLine: 'underline'}, underline: {textDecorationLine: 'underline'},
+19 -19
View File
@@ -100,12 +100,12 @@ export const defaultTheme: Theme = {
'2xl-medium': { '2xl-medium': {
fontSize: 18, fontSize: 18,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'2xl-bold': { '2xl-bold': {
fontSize: 18, fontSize: 18,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '600',
}, },
'2xl-heavy': { '2xl-heavy': {
fontSize: 18, fontSize: 18,
@@ -125,12 +125,12 @@ export const defaultTheme: Theme = {
'xl-medium': { 'xl-medium': {
fontSize: 17, fontSize: 17,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'xl-bold': { 'xl-bold': {
fontSize: 17, fontSize: 17,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '600',
}, },
'xl-heavy': { 'xl-heavy': {
fontSize: 17, fontSize: 17,
@@ -150,12 +150,12 @@ export const defaultTheme: Theme = {
'lg-medium': { 'lg-medium': {
fontSize: 16, fontSize: 16,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'lg-bold': { 'lg-bold': {
fontSize: 16, fontSize: 16,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '600',
}, },
'lg-heavy': { 'lg-heavy': {
fontSize: 16, fontSize: 16,
@@ -175,12 +175,12 @@ export const defaultTheme: Theme = {
'md-medium': { 'md-medium': {
fontSize: 15, fontSize: 15,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'md-bold': { 'md-bold': {
fontSize: 15, fontSize: 15,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '600',
}, },
'md-heavy': { 'md-heavy': {
fontSize: 15, fontSize: 15,
@@ -200,12 +200,12 @@ export const defaultTheme: Theme = {
'sm-medium': { 'sm-medium': {
fontSize: 14, fontSize: 14,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'sm-bold': { 'sm-bold': {
fontSize: 14, fontSize: 14,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '600',
}, },
'sm-heavy': { 'sm-heavy': {
fontSize: 14, fontSize: 14,
@@ -225,12 +225,12 @@ export const defaultTheme: Theme = {
'xs-medium': { 'xs-medium': {
fontSize: 13, fontSize: 13,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'xs-bold': { 'xs-bold': {
fontSize: 13, fontSize: 13,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '600',
}, },
'xs-heavy': { 'xs-heavy': {
fontSize: 13, fontSize: 13,
@@ -241,24 +241,24 @@ export const defaultTheme: Theme = {
'title-2xl': { 'title-2xl': {
fontSize: 34, fontSize: 34,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'title-xl': { 'title-xl': {
fontSize: 28, fontSize: 28,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '600',
}, },
'title-lg': { 'title-lg': {
fontSize: 22, fontSize: 22,
fontWeight: '500', fontWeight: '600',
}, },
title: { title: {
fontWeight: '500', fontWeight: '600',
fontSize: 20, fontSize: 20,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
}, },
'title-sm': { 'title-sm': {
fontWeight: 'bold', fontWeight: '600',
fontSize: 17, fontSize: 17,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
}, },
@@ -273,12 +273,12 @@ export const defaultTheme: Theme = {
fontWeight: '400', fontWeight: '400',
}, },
'button-lg': { 'button-lg': {
fontWeight: '500', fontWeight: '600',
fontSize: 18, fontSize: 18,
letterSpacing: tokens.TRACKING, letterSpacing: tokens.TRACKING,
}, },
button: { button: {
fontWeight: '500', fontWeight: '600',
fontSize: 14, fontSize: 14,
letterSpacing: tokens.TRACKING, 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 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 { export function fixLegacyLanguageCode(code: string | null): string | null {
// handle some legacy code conversions, see https://xml.coverpages.org/iso639a.html
if (code === 'in') { if (code === 'in') {
// indonesian // indonesian
return 'id' return 'id'
@@ -176,3 +181,20 @@ export function fixLegacyLanguageCode(code: string | null): string | null {
} }
return code 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 {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 isIOS = Platform.OS === 'ios'
export const isAndroid = Platform.OS === 'android' export const isAndroid = Platform.OS === 'android'
@@ -15,9 +11,3 @@ export const isMobileWeb =
// @ts-ignore we know window exists -prf // @ts-ignore we know window exists -prf
global.window.matchMedia(isMobileWebMediaQuery)?.matches global.window.matchMedia(isMobileWebMediaQuery)?.matches
export const isIPhoneWeb = isWeb && /iPhone/.test(navigator.userAgent) 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]}> <View style={[a.gap_sm]}>
<Button <Button
label={_(msg`Reactivate your account`)} label={_(msg`Reactivate your account`)}
size="medium" size="large"
variant="solid" variant="solid"
color="primary" color="primary"
onPress={handleActivate}> onPress={handleActivate}>
@@ -153,7 +153,7 @@ export function Deactivated() {
</Button> </Button>
<Button <Button
label={_(msg`Cancel reactivation and log out`)} label={_(msg`Cancel reactivation and log out`)}
size="medium" size="large"
variant="solid" variant="solid"
color="secondary" color="secondary"
onPress={onPressLogout}> onPress={onPressLogout}>
@@ -212,7 +212,7 @@ export function Deactivated() {
</Text> </Text>
<Button <Button
label={_(msg`Log in or sign up`)} label={_(msg`Log in or sign up`)}
size="medium" size="large"
variant="solid" variant="solid"
color="secondary" color="secondary"
onPress={() => setShowLoggedOut(true)}> onPress={() => setShowLoggedOut(true)}>
@@ -23,7 +23,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]} style={[a.self_center]}
variant="solid" variant="solid"
color="primary" color="primary"
size="xsmall" size="small"
onPress={async () => { onPress={async () => {
SharedPrefs.removeValue('testerString') SharedPrefs.removeValue('testerString')
SharedPrefs.setValue('testerString', 'Hello') SharedPrefs.setValue('testerString', 'Hello')
@@ -39,7 +39,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]} style={[a.self_center]}
variant="solid" variant="solid"
color="primary" color="primary"
size="xsmall" size="small"
onPress={async () => { onPress={async () => {
SharedPrefs.removeValue('testerString') SharedPrefs.removeValue('testerString')
const str = SharedPrefs.getString('testerString') const str = SharedPrefs.getString('testerString')
@@ -53,7 +53,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]} style={[a.self_center]}
variant="solid" variant="solid"
color="primary" color="primary"
size="xsmall" size="small"
onPress={async () => { onPress={async () => {
SharedPrefs.removeValue('testerBool') SharedPrefs.removeValue('testerBool')
SharedPrefs.setValue('testerBool', true) SharedPrefs.setValue('testerBool', true)
@@ -68,7 +68,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]} style={[a.self_center]}
variant="solid" variant="solid"
color="primary" color="primary"
size="xsmall" size="small"
onPress={async () => { onPress={async () => {
SharedPrefs.removeValue('testerNumber') SharedPrefs.removeValue('testerNumber')
SharedPrefs.setValue('testerNumber', 123) SharedPrefs.setValue('testerNumber', 123)
@@ -83,7 +83,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]} style={[a.self_center]}
variant="solid" variant="solid"
color="primary" color="primary"
size="xsmall" size="small"
onPress={async () => { onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!') SharedPrefs.removeFromSet('testerSet', 'Hello!')
SharedPrefs.addToSet('testerSet', 'Hello!') SharedPrefs.addToSet('testerSet', 'Hello!')
@@ -98,7 +98,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]} style={[a.self_center]}
variant="solid" variant="solid"
color="primary" color="primary"
size="xsmall" size="small"
onPress={async () => { onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!') SharedPrefs.removeFromSet('testerSet', 'Hello!')
const contains = SharedPrefs.setContains('testerSet', 'Hello!') const contains = SharedPrefs.setContains('testerSet', 'Hello!')
+2 -2
View File
@@ -91,7 +91,7 @@ export function NoFeedsPinned({
<Button <Button
disabled={isPending} disabled={isPending}
label={_(msg`Apply default recommended feeds`)} label={_(msg`Apply default recommended feeds`)}
size="medium" size="large"
variant="solid" variant="solid"
color="primary" color="primary"
onPress={addRecommendedFeeds}> onPress={addRecommendedFeeds}>
@@ -102,7 +102,7 @@ export function NoFeedsPinned({
<Link <Link
label={_(msg`Browse other feeds`)} label={_(msg`Browse other feeds`)}
to="/feeds" to="/feeds"
size="medium" size="large"
variant="solid" variant="solid"
color="secondary"> color="secondary">
<ButtonIcon icon={ListSparkle} position="left" /> <ButtonIcon icon={ListSparkle} position="left" />
+4 -4
View File
@@ -152,7 +152,7 @@ export function ListHiddenScreen({
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
label={_(msg`Remove from saved feeds`)} label={_(msg`Remove from saved feeds`)}
onPress={onRemoveList} onPress={onRemoveList}
disabled={isProcessing}> disabled={isProcessing}>
@@ -168,7 +168,7 @@ export function ListHiddenScreen({
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
label={_(msg`Show list anyway`)} label={_(msg`Show list anyway`)}
onPress={() => setIsContentVisible(true)} onPress={() => setIsContentVisible(true)}
disabled={isProcessing}> disabled={isProcessing}>
@@ -180,7 +180,7 @@ export function ListHiddenScreen({
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
label={_(msg`Unsubscribe from list`)} label={_(msg`Unsubscribe from list`)}
onPress={() => { onPress={() => {
if (isModList) { if (isModList) {
@@ -204,7 +204,7 @@ export function ListHiddenScreen({
color="primary" color="primary"
label={_(msg`Return to previous page`)} label={_(msg`Return to previous page`)}
onPress={goBack} onPress={goBack}
size="medium" size="large"
disabled={isProcessing}> disabled={isProcessing}>
<ButtonText> <ButtonText>
<Trans>Go Back</Trans> <Trans>Go Back</Trans>
+1 -1
View File
@@ -98,7 +98,7 @@ export const ChooseAccountForm = ({
label={_(msg`Back`)} label={_(msg`Back`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onPressBack}> onPress={onPressBack}>
<ButtonText>{_(msg`Back`)}</ButtonText> <ButtonText>{_(msg`Back`)}</ButtonText>
</Button> </Button>
+3 -3
View File
@@ -129,7 +129,7 @@ export const ForgotPasswordForm = ({
label={_(msg`Back`)} label={_(msg`Back`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onPressBack}> onPress={onPressBack}>
<ButtonText> <ButtonText>
<Trans>Back</Trans> <Trans>Back</Trans>
@@ -143,7 +143,7 @@ export const ForgotPasswordForm = ({
label={_(msg`Next`)} label={_(msg`Next`)}
variant="solid" variant="solid"
color={'primary'} color={'primary'}
size="medium" size="large"
onPress={onPressNext}> onPress={onPressNext}>
<ButtonText> <ButtonText>
<Trans>Next</Trans> <Trans>Next</Trans>
@@ -170,7 +170,7 @@ export const ForgotPasswordForm = ({
onPress={onEmailSent} onPress={onEmailSent}
label={_(msg`Go to next`)} label={_(msg`Go to next`)}
accessibilityHint={_(msg`Navigates to the next screen`)} accessibilityHint={_(msg`Navigates to the next screen`)}
size="medium" size="large"
variant="ghost" variant="ghost"
color="secondary"> color="secondary">
<ButtonText> <ButtonText>
+3 -3
View File
@@ -285,7 +285,7 @@ export const LoginForm = ({
label={_(msg`Back`)} label={_(msg`Back`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onPressBack}> onPress={onPressBack}>
<ButtonText> <ButtonText>
<Trans>Back</Trans> <Trans>Back</Trans>
@@ -299,7 +299,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Retries login`)} accessibilityHint={_(msg`Retries login`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onPressRetryConnect}> onPress={onPressRetryConnect}>
<ButtonText> <ButtonText>
<Trans>Retry</Trans> <Trans>Retry</Trans>
@@ -319,7 +319,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Navigates to the next screen`)} accessibilityHint={_(msg`Navigates to the next screen`)}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
onPress={onPressNext}> onPress={onPressNext}>
<ButtonText> <ButtonText>
<Trans>Next</Trans> <Trans>Next</Trans>
+1 -1
View File
@@ -39,7 +39,7 @@ export const PasswordUpdatedForm = ({
accessibilityHint={_(msg`Closes password update alert`)} accessibilityHint={_(msg`Closes password update alert`)}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium"> size="large">
<ButtonText> <ButtonText>
<Trans>Okay</Trans> <Trans>Okay</Trans>
</ButtonText> </ButtonText>
+2 -2
View File
@@ -160,7 +160,7 @@ export const SetNewPasswordForm = ({
label={_(msg`Back`)} label={_(msg`Back`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onPressBack}> onPress={onPressBack}>
<ButtonText> <ButtonText>
<Trans>Back</Trans> <Trans>Back</Trans>
@@ -174,7 +174,7 @@ export const SetNewPasswordForm = ({
label={_(msg`Next`)} label={_(msg`Next`)}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
onPress={onPressNext}> onPress={onPressNext}>
<ButtonText> <ButtonText>
<Trans>Next</Trans> <Trans>Next</Trans>
@@ -128,7 +128,7 @@ function DialogInner() {
testID="backBtn" testID="backBtn"
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onBack} onPress={onBack}
label={_(msg`Back`)}> label={_(msg`Back`)}>
<ButtonText>{_(msg`Back`)}</ButtonText> <ButtonText>{_(msg`Back`)}</ButtonText>
@@ -137,7 +137,7 @@ function DialogInner() {
testID="submitBtn" testID="submitBtn"
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
onPress={onSubmit} onPress={onSubmit}
label={_(msg`Submit`)}> label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText> <ButtonText>{_(msg`Submit`)}</ButtonText>
+1 -1
View File
@@ -198,7 +198,7 @@ export function MessagesScreen({navigation, route}: Props) {
<Button <Button
label={_(msg`Reload conversations`)} label={_(msg`Reload conversations`)}
size="medium" size="large"
color="secondary" color="secondary"
variant="solid" variant="solid"
onPress={() => refetch()}> onPress={() => refetch()}>
+6 -2
View File
@@ -7,6 +7,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {getLabelingServiceTitle} from '#/lib/moderation' import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -22,8 +23,8 @@ import {
useProfileUpdateMutation, useProfileUpdateMutation,
} from '#/state/queries/profile' } from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useAnalytics} from 'lib/analytics/analytics'
import {ViewHeader} from '#/view/com/util/ViewHeader' import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views' import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
@@ -338,7 +339,7 @@ export function ModerationScreenInner({
a.justify_between, a.justify_between,
disabledOnIOS && {opacity: 0.5}, 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> <Trans>Enable adult content</Trans>
</Text> </Text>
<Toggle.Item <Toggle.Item
@@ -455,6 +456,9 @@ export function ModerationScreenInner({
value={labeler.creator.description} value={labeler.creator.description}
handle={labeler.creator.handle} handle={labeler.creator.handle}
/> />
{isNonConfigurableModerationAuthority(
labeler.creator.did,
) && <LabelingService.RegionalNotice />}
</LabelingService.Content> </LabelingService.Content>
</LabelingService.Outer> </LabelingService.Outer>
)} )}
+3 -3
View File
@@ -2,9 +2,9 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' 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 {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 {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -20,7 +20,7 @@ export function ProfileHeaderDisplayName({
<View pointerEvents="none"> <View pointerEvents="none">
<Text <Text
testID="profileHeaderDisplayName" 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( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'), moderation.ui('displayName'),
@@ -102,7 +102,7 @@ function DeactivateAccountDialogInner({
<Button <Button
variant="solid" variant="solid"
color="negative" color="negative"
size={gtMobile ? 'small' : 'medium'} size={gtMobile ? 'small' : 'large'}
label={_(msg`Yes, deactivate`)} label={_(msg`Yes, deactivate`)}
onPress={handleDeactivate}> onPress={handleDeactivate}>
<ButtonText>{_(msg`Yes, deactivate`)}</ButtonText> <ButtonText>{_(msg`Yes, deactivate`)}</ButtonText>
+6 -4
View File
@@ -15,6 +15,7 @@ export interface BackNextButtonsProps {
onBackPress: () => void onBackPress: () => void
onNextPress?: () => void onNextPress?: () => void
onRetryPress?: () => void onRetryPress?: () => void
overrideNextText?: string
} }
export function BackNextButtons({ export function BackNextButtons({
@@ -25,6 +26,7 @@ export function BackNextButtons({
onBackPress, onBackPress,
onNextPress, onNextPress,
onRetryPress, onRetryPress,
overrideNextText,
}: BackNextButtonsProps) { }: BackNextButtonsProps) {
const {_} = useLingui() const {_} = useLingui()
@@ -34,7 +36,7 @@ export function BackNextButtons({
label={_(msg`Go back to previous step`)} label={_(msg`Go back to previous step`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
onPress={onBackPress}> onPress={onBackPress}>
<ButtonText> <ButtonText>
<Trans>Back</Trans> <Trans>Back</Trans>
@@ -46,7 +48,7 @@ export function BackNextButtons({
label={_(msg`Press to retry`)} label={_(msg`Press to retry`)}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
onPress={onRetryPress}> onPress={onRetryPress}>
<ButtonText> <ButtonText>
<Trans>Retry</Trans> <Trans>Retry</Trans>
@@ -59,11 +61,11 @@ export function BackNextButtons({
label={_(msg`Continue to next step`)} label={_(msg`Continue to next step`)}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
disabled={isLoading || isNextDisabled} disabled={isLoading || isNextDisabled}
onPress={onNextPress}> onPress={onNextPress}>
<ButtonText> <ButtonText>
<Trans>Next</Trans> {overrideNextText ? overrideNextText : <Trans>Next</Trans>}
</ButtonText> </ButtonText>
{isLoading && <ButtonIcon icon={Loader} />} {isLoading && <ButtonIcon icon={Loader} />}
</Button> </Button>
+36 -8
View File
@@ -3,9 +3,11 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator' import * as EmailValidator from 'email-validator'
import type tldts from 'tldts'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isEmailMaybeInvalid} from 'lib/strings/email'
import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {is13, is18, useSignupContext} from '#/screens/Signup/state' import {is13, is18, useSignupContext} from '#/screens/Signup/state'
import {Policies} from '#/screens/Signup/StepInfo/Policies' import {Policies} from '#/screens/Signup/StepInfo/Policies'
@@ -46,13 +48,41 @@ export function StepInfo({
const inviteCodeValueRef = useRef<string>(state.inviteCode) const inviteCodeValueRef = useRef<string>(state.inviteCode)
const emailValueRef = useRef<string>(state.email) const emailValueRef = useRef<string>(state.email)
const prevEmailValueRef = useRef<string>(state.email)
const passwordValueRef = useRef<string>(state.password) 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 inviteCode = inviteCodeValueRef.current
const email = emailValueRef.current const email = emailValueRef.current
const emailChanged = prevEmailValueRef.current !== email
const password = passwordValueRef.current 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)) { if (!is13(state.dateOfBirth)) {
return return
} }
@@ -89,13 +119,7 @@ export function StepInfo({
logEvent('signup:nextPressed', { logEvent('signup:nextPressed', {
activeStep: state.activeStep, activeStep: state.activeStep,
}) })
}, [ }
_,
dispatch,
state.activeStep,
state.dateOfBirth,
state.serviceDescription?.inviteCodeRequired,
])
return ( return (
<ScreenTransition> <ScreenTransition>
@@ -148,6 +172,9 @@ export function StepInfo({
testID="emailInput" testID="emailInput"
onChangeText={value => { onChangeText={value => {
emailValueRef.current = value.trim() emailValueRef.current = value.trim()
if (hasWarnedEmail) {
setHasWarnedEmail(false)
}
}} }}
label={_(msg`Enter your email address`)} label={_(msg`Enter your email address`)}
defaultValue={state.email} defaultValue={state.email}
@@ -208,6 +235,7 @@ export function StepInfo({
onBackPress={onPressBack} onBackPress={onPressBack}
onNextPress={onNextPress} onNextPress={onNextPress}
onRetryPress={refetchServer} onRetryPress={refetchServer}
overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined}
/> />
</ScreenTransition> </ScreenTransition>
) )
+3 -3
View File
@@ -8,8 +8,8 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {FEEDBACK_FORM_URL} from '#/lib/constants' import {FEEDBACK_FORM_URL} from '#/lib/constants'
import {useServiceQuery} from '#/state/queries/service' import {useServiceQuery} from '#/state/queries/service'
import {useStarterPackQuery} from 'state/queries/starter-packs' import {useStarterPackQuery} from '#/state/queries/starter-packs'
import {useActiveStarterPack} from 'state/shell/starter-pack' import {useActiveStarterPack} from '#/state/shell/starter-pack'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
import { import {
initialState, initialState,
@@ -132,7 +132,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
!gtMobile && {paddingBottom: 100}, !gtMobile && {paddingBottom: 100},
]}> ]}>
<View style={[a.gap_sm, a.pb_3xl]}> <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> <Trans>
Step {state.activeStep + 1} of{' '} Step {state.activeStep + 1} of{' '}
{state.serviceDescription && {state.serviceDescription &&
@@ -11,22 +11,22 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isAndroidWeb} from '#/lib/browser'
import {JOINED_THIS_WEEK} from '#/lib/constants' import {JOINED_THIS_WEEK} from '#/lib/constants'
import {isAndroidWeb} from 'lib/browser' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {logEvent} from '#/lib/statsig/statsig'
import {logEvent} from 'lib/statsig/statsig' import {createStarterPackGooglePlayUri} from '#/lib/strings/starter-pack'
import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack' import {isWeb} from '#/platform/detection'
import {isWeb} from 'platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useModerationOpts} from 'state/preferences/moderation-opts' import {useStarterPackQuery} from '#/state/queries/starter-packs'
import {useStarterPackQuery} from 'state/queries/starter-packs'
import { import {
useActiveStarterPack, useActiveStarterPack,
useSetActiveStarterPack, 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 {formatCount} from '#/view/com/util/numeric/format'
import {LoggedOutScreenState} from 'view/com/auth/LoggedOut' import {CenteredView} from '#/view/com/util/Views'
import {CenteredView} from 'view/com/util/Views' import {Logo} from '#/view/icons/Logo'
import {Logo} from 'view/icons/Logo'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
@@ -188,12 +188,7 @@ function LandingScreenLoaded({
{record.name} {record.name}
</Text> </Text>
<Text <Text
style={[ style={[a.text_center, a.font_bold, a.text_md, {color: 'white'}]}>
a.text_center,
a.font_semibold,
a.text_md,
{color: 'white'},
]}>
Starter pack by {`@${creator.handle}`} Starter pack by {`@${creator.handle}`}
</Text> </Text>
</LinearGradientBackground> </LinearGradientBackground>
@@ -219,11 +214,7 @@ function LandingScreenLoaded({
color={t.atoms.text_contrast_medium.color} color={t.atoms.text_contrast_medium.color}
/> />
<Text <Text
style={[ style={[a.font_bold, a.text_sm, t.atoms.text_contrast_medium]}
a.font_semibold,
a.text_sm,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}> numberOfLines={1}>
<Trans> <Trans>
{formatCount(i18n, JOINED_THIS_WEEK)} joined this week {formatCount(i18n, JOINED_THIS_WEEK)} joined this week
@@ -308,7 +299,7 @@ function LandingScreenLoaded({
label={_(msg`Signup without a starter pack`)} label={_(msg`Signup without a starter pack`)}
variant="solid" variant="solid"
color="secondary" color="secondary"
size="medium" size="large"
style={[a.py_lg]} style={[a.py_lg]}
onPress={onJoinWithoutPress}> onPress={onJoinWithoutPress}>
<ButtonText> <ButtonText>
@@ -449,7 +449,7 @@ function Header({
}} }}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium"> size="large">
<ButtonText style={[a.text_lg]}> <ButtonText style={[a.text_lg]}>
<Trans>Join Bluesky</Trans> <Trans>Join Bluesky</Trans>
</ButtonText> </ButtonText>
@@ -645,7 +645,7 @@ function OverflowMenu({
<Button <Button
variant="solid" variant="solid"
color="negative" color="negative"
size={gtMobile ? 'small' : 'medium'} size={gtMobile ? 'small' : 'large'}
label={_(msg`Yes, delete this starter pack`)} label={_(msg`Yes, delete this starter pack`)}
onPress={onDeleteStarterPack}> onPress={onDeleteStarterPack}>
<ButtonText> <ButtonText>
+1 -1
View File
@@ -358,7 +358,7 @@ function Container({children}: {children: React.ReactNode}) {
label={_(msg`Next`)} label={_(msg`Next`)}
variant="solid" variant="solid"
color="primary" color="primary"
size="medium" size="large"
style={[a.mx_xl, a.mb_lg, {marginTop: 35}]} style={[a.mx_xl, a.mb_lg, {marginTop: 35}]}
onPress={() => dispatch({type: 'Next'})}> onPress={() => dispatch({type: 'Next'})}>
<ButtonText> <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, tryStringify,
} from '#/state/persisted/schema' } from '#/state/persisted/schema'
import {PersistedApi} from './types' import {PersistedApi} from './types'
import {normalizeData} from './util'
export type {PersistedAccount, Schema} from '#/state/persisted/schema' export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema' export {defaults} from '#/state/persisted/schema'
@@ -33,10 +34,10 @@ export async function write<K extends keyof Schema>(
key: K, key: K,
value: Schema[K], value: Schema[K],
): Promise<void> { ): Promise<void> {
_state = { _state = normalizeData({
..._state, ..._state,
[key]: value, [key]: value,
} })
await writeToStorage(_state) await writeToStorage(_state)
} }
write satisfies PersistedApi['write'] write satisfies PersistedApi['write']
@@ -81,6 +82,9 @@ async function readFromStorage(): Promise<Schema | undefined> {
}) })
} }
if (rawData) { if (rawData) {
return tryParse(rawData) const parsed = tryParse(rawData)
if (parsed) {
return normalizeData(parsed)
}
} }
} }
+7 -4
View File
@@ -9,6 +9,7 @@ import {
tryStringify, tryStringify,
} from '#/state/persisted/schema' } from '#/state/persisted/schema'
import {PersistedApi} from './types' import {PersistedApi} from './types'
import {normalizeData} from './util'
export type {PersistedAccount, Schema} from '#/state/persisted/schema' export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema' export {defaults} from '#/state/persisted/schema'
@@ -56,10 +57,10 @@ export async function write<K extends keyof Schema>(
} catch (e) { } catch (e) {
// Ignore and go through the normal path. // Ignore and go through the normal path.
} }
_state = { _state = normalizeData({
..._state, ..._state,
[key]: value, [key]: value,
} })
writeToStorage(_state) writeToStorage(_state)
broadcast.postMessage({event: {type: UPDATE_EVENT, key}}) broadcast.postMessage({event: {type: UPDATE_EVENT, key}})
broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading
@@ -140,9 +141,11 @@ function readFromStorage(): Schema | undefined {
return lastResult return lastResult
} else { } else {
const result = tryParse(rawData) const result = tryParse(rawData)
if (result) {
lastRawData = rawData lastRawData = rawData
lastResult = result lastResult = normalizeData(result)
return result return lastResult
}
} }
} }
} }
+43 -9
View File
@@ -1,7 +1,8 @@
import {z} from 'zod' import {z} from 'zod'
import {deviceLanguageCodes, deviceLocales} from '#/locale/deviceLocales'
import {findSupportedAppLanguage} from '#/locale/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {deviceLocales} from '#/platform/detection'
import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army' import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army'
const externalEmbedOptions = ['show', 'hide'] as const const externalEmbedOptions = ['show', 'hide'] as const
@@ -55,10 +56,39 @@ const schema = z.object({
lastEmailConfirm: z.string().optional(), lastEmailConfirm: z.string().optional(),
}), }),
languagePrefs: z.object({ languagePrefs: z.object({
primaryLanguage: z.string(), // should move to server /**
contentLanguages: z.array(z.string()), // should move to server * The target language for translating posts.
postLanguage: z.string(), // should move to server *
* 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()), 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(), appLanguage: z.string(),
}), }),
requireAltTextEnabled: z.boolean(), // should move to server requireAltTextEnabled: z.boolean(), // should move to server
@@ -108,13 +138,17 @@ export const defaults: Schema = {
lastEmailConfirm: undefined, lastEmailConfirm: undefined,
}, },
languagePrefs: { languagePrefs: {
primaryLanguage: deviceLocales[0] || 'en', primaryLanguage: deviceLanguageCodes[0] || 'en',
contentLanguages: deviceLocales || [], contentLanguages: deviceLanguageCodes || [],
postLanguage: deviceLocales[0] || 'en', postLanguage: deviceLanguageCodes[0] || 'en',
postLanguageHistory: (deviceLocales || []) postLanguageHistory: (deviceLanguageCodes || [])
.concat(['en', 'ja', 'pt', 'de']) .concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6), .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, requireAltTextEnabled: false,
largeAltBadgeEnabled: 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 {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query' import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute} from '#/lib/moderation' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
@@ -113,6 +113,10 @@ function computeSuggestions({
return items.filter(profile => { return items.filter(profile => {
const modui = moderateProfile(profile, moderationOpts).ui('profileList') const modui = moderateProfile(profile, moderationOpts).ui('profileList')
const isExactMatch = q && profile.handle.toLowerCase() === q 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 {QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk' import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation'
import {precacheProfile} from '../profile' import {precacheProfile} from '../profile'
import {FeedNotification, FeedPage, NotificationType} from './types' import {FeedNotification, FeedPage, NotificationType} from './types'
@@ -104,6 +105,10 @@ export function shouldFilterNotif(
notif: AppBskyNotificationListNotifications.Notification, notif: AppBskyNotificationListNotifications.Notification,
moderationOpts: ModerationOpts | undefined, moderationOpts: ModerationOpts | undefined,
): boolean { ): boolean {
const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense)
if (containsImperative) {
return true
}
if (!moderationOpts) { if (!moderationOpts) {
return false return false
} }
@@ -10,6 +10,10 @@ jest.mock('jwt-decode', () => ({
}, },
})) }))
jest.mock('expo-localization', () => ({
getLocales: () => [],
}))
describe('session', () => { describe('session', () => {
it('can log in and out', () => { it('can log in and out', () => {
let state = getInitialState([]) 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 {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {IS_TEST_USER} from '#/lib/constants' import {IS_TEST_USER} from '#/lib/constants'
import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
import {readLabelers} from './agent-config' import {readLabelers} from './agent-config'
import {SessionAccount} from './types' 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. // This global mutation is *only* OK because this code is only relevant for testing.
// Don't add any other global behavior here! // Don't add any other global behavior here!
switchToBskyAppLabeler() switchToBskyAppLabeler()
configureAdditionalModerationAuthorities()
} }
export async function configureModerationForAccount( 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 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. // If we wanted to fix this, we could block on the preferences query here.
} }
configureAdditionalModerationAuthorities()
} }
function switchToBskyAppLabeler() { function switchToBskyAppLabeler() {
+9 -1
View File
@@ -1,5 +1,6 @@
import {MMKV} from 'react-native-mmkv' import {MMKV} from 'react-native-mmkv'
import {IS_DEV} from '#/env'
import {Device} from '#/storage/schema' import {Device} from '#/storage/schema'
export * from '#/storage/schema' export * from '#/storage/schema'
@@ -71,4 +72,11 @@ export class Storage<Scopes extends unknown[], Schema> {
* *
* `device.set([key], true)` * `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' fontScale: '-2' | '-1' | '0' | '1' | '2'
fontFamily: 'system' | 'theme' fontFamily: 'system' | 'theme'
lastNuxDialog: string | undefined 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 {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype' import {Logotype} from '#/view/icons/Logotype'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -35,8 +35,7 @@ export const SplashScreen = ({
<Logotype width={161} fill={t.atoms.text.color} /> <Logotype width={161} fill={t.atoms.text.color} />
</View> </View>
<Text <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
style={[a.text_md, a.font_semibold, t.atoms.text_contrast_medium]}>
<Trans>What's up?</Trans> <Trans>What's up?</Trans>
</Text> </Text>
</View> </View>
+3 -7
View File
@@ -4,11 +4,11 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useKawaiiMode} from '#/state/preferences/kawaii' 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 {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype' import {Logotype} from '#/view/icons/Logotype'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -78,11 +78,7 @@ export const SplashScreen = ({
)} )}
<Text <Text
style={[ style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
a.text_md,
a.font_semibold,
t.atoms.text_contrast_medium,
]}>
<Trans>What's up?</Trans> <Trans>What's up?</Trans>
</Text> </Text>
</View> </View>
+20 -43
View File
@@ -16,7 +16,7 @@ import {ComposerOptsPostRef} from 'state/shell/composer'
import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed' import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
import {useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const t = useTheme() const t = useTheme()
@@ -122,55 +122,49 @@ function ComposerReplyToImages({
showFull: boolean showFull: boolean
}) { }) {
return ( return (
<View <View style={[styles.imagesContainer, a.mx_xs]}>
style={{
width: 65,
flexDirection: 'column',
alignItems: 'center',
}}>
<View style={styles.imagesContainer}>
{(images.length === 1 && ( {(images.length === 1 && (
<Image <Image
source={{uri: images[0].thumb}} source={{uri: images[0].thumb}}
style={styles.singleImage} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
)) || )) ||
(images.length === 2 && ( (images.length === 2 && (
<View style={[styles.imagesInner, styles.imagesRow]}> <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image <Image
source={{uri: images[0].thumb}} source={{uri: images[0].thumb}}
style={styles.doubleImageTall} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
<Image <Image
source={{uri: images[1].thumb}} source={{uri: images[1].thumb}}
style={styles.doubleImageTall} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
</View> </View>
)) || )) ||
(images.length === 3 && ( (images.length === 3 && (
<View style={[styles.imagesInner, styles.imagesRow]}> <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image <Image
source={{uri: images[0].thumb}} source={{uri: images[0].thumb}}
style={styles.doubleImageTall} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
<View style={styles.imagesInner}> <View style={[a.flex_1, a.gap_2xs]}>
<Image <Image
source={{uri: images[1].thumb}} source={{uri: images[1].thumb}}
style={styles.doubleImage} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
<Image <Image
source={{uri: images[2].thumb}} source={{uri: images[2].thumb}}
style={styles.doubleImage} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
@@ -178,31 +172,31 @@ function ComposerReplyToImages({
</View> </View>
)) || )) ||
(images.length === 4 && ( (images.length === 4 && (
<View style={styles.imagesInner}> <View style={[a.flex_1, a.gap_2xs]}>
<View style={[styles.imagesInner, styles.imagesRow]}> <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image <Image
source={{uri: images[0].thumb}} source={{uri: images[0].thumb}}
style={styles.doubleImage} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
<Image <Image
source={{uri: images[1].thumb}} source={{uri: images[1].thumb}}
style={styles.doubleImage} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
</View> </View>
<View style={[styles.imagesInner, styles.imagesRow]}> <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image <Image
source={{uri: images[2].thumb}} source={{uri: images[2].thumb}}
style={styles.doubleImage} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
<Image <Image
source={{uri: images[3].thumb}} source={{uri: images[3].thumb}}
style={styles.doubleImage} style={[a.flex_1]}
cachePolicy="memory-disk" cachePolicy="memory-disk"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
@@ -210,7 +204,6 @@ function ComposerReplyToImages({
</View> </View>
))} ))}
</View> </View>
</View>
) )
} }
@@ -240,23 +233,7 @@ const styles = StyleSheet.create({
borderRadius: 6, borderRadius: 6,
overflow: 'hidden', overflow: 'hidden',
marginTop: 2, marginTop: 2,
}, height: 64,
imagesInner: { width: 64,
gap: 2,
},
imagesRow: {
flexDirection: 'row',
},
singleImage: {
width: 65,
height: 65,
},
doubleImageTall: {
width: 32.5,
height: 65,
},
doubleImage: {
width: 32.5,
height: 32.5,
}, },
}) })
+1 -1
View File
@@ -160,7 +160,7 @@ function AltTextInner({
</View> </View>
<Button <Button
label={_(msg`Save`)} label={_(msg`Save`)}
size="medium" size="large"
color="primary" color="primary"
variant="solid" variant="solid"
onPress={onPressSubmit}> onPress={onPressSubmit}>
+7 -7
View File
@@ -7,13 +7,13 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {observer} from 'mobx-react-lite' 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 {useModalControls} from '#/state/modals'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {GalleryModel} from '#/state/models/media/gallery'
import {Dimensions} from 'lib/media/types' import {Text} from '#/view/com/util/text/Text'
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 {useTheme} from '#/alf' import {useTheme} from '#/alf'
const IMAGE_GAP = 8 const IMAGE_GAP = 8
@@ -263,7 +263,7 @@ const styles = StyleSheet.create({
altTextControlLabel: { altTextControlLabel: {
color: 'white', color: 'white',
fontSize: 12, fontSize: 12,
fontWeight: 'bold', fontWeight: '600',
letterSpacing: 1, letterSpacing: 1,
}, },
altTextHiddenRegion: { altTextHiddenRegion: {
@@ -60,7 +60,7 @@ export function ThreadgateBtn({
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size="xsmall" size="small"
testID="openReplyGateButton" testID="openReplyGateButton"
onPress={onPress} onPress={onPress}
label={label} label={label}
@@ -44,7 +44,7 @@ export function SubtitleDialogBtn(props: Props) {
? _('Opens captions and alt text dialog') ? _('Opens captions and alt text dialog')
: _('Opens alt text dialog') : _('Opens alt text dialog')
} }
size="xsmall" size="small"
color="secondary" color="secondary"
variant="ghost" variant="ghost"
onPress={() => { onPress={() => {
@@ -169,7 +169,7 @@ function SubtitleDialogInner({
<View style={web([a.flex_row, a.justify_end])}> <View style={web([a.flex_row, a.justify_end])}>
<Button <Button
label={_(msg`Done`)} label={_(msg`Done`)}
size={isWeb ? 'small' : 'medium'} size={isWeb ? 'small' : 'large'}
color="primary" color="primary"
variant="solid" variant="solid"
onPress={() => { onPress={() => {
@@ -57,7 +57,7 @@ export function SubtitleFilePicker({
<Button <Button
onPress={handleClick} onPress={handleClick}
label={_('Select subtitle file (.vtt)')} label={_('Select subtitle file (.vtt)')}
size="medium" size="large"
color="primary" color="primary"
variant="solid" variant="solid"
disabled={disabled}> 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 {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 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 {useModalControls} from '#/state/modals'
import { import {
useListCreateMutation, useListCreateMutation,
useListMetadataMutation, useListMetadataMutation,
} from '#/state/queries/list' } from '#/state/queries/list'
import {useAgent} from '#/state/session' 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 {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
@@ -359,7 +359,7 @@ export function Component({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
title: { title: {
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: '600',
fontSize: 24, fontSize: 24,
marginBottom: 18, marginBottom: 18,
}, },
@@ -373,7 +373,7 @@ const styles = StyleSheet.create({
marginTop: 20, marginTop: 20,
}, },
label: { label: {
fontWeight: 'bold', fontWeight: '600',
}, },
form: { form: {
paddingHorizontal: 6, paddingHorizontal: 6,
+10 -10
View File
@@ -8,16 +8,16 @@ import {Slider} from '@miblanchard/react-native-slider'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import ImageEditor, {Position} from 'react-avatar-editor' 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 {useModalControls} from '#/state/modals'
import {MAX_ALT_TEXT} from 'lib/constants' import {GalleryModel} from '#/state/models/media/gallery'
import {usePalette} from 'lib/hooks/usePalette' import {ImageModel} from '#/state/models/media/image'
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 {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import { import {
@@ -333,7 +333,7 @@ const styles = StyleSheet.create({
subsection: {marginTop: 12}, subsection: {marginTop: 12},
gap18: {gap: 18}, gap18: {gap: 18},
title: { title: {
fontWeight: 'bold', fontWeight: '600',
fontSize: 24, fontSize: 24,
}, },
btns: { btns: {
+11 -11
View File
@@ -15,18 +15,18 @@ import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useProfileUpdateMutation} from '#/state/queries/profile' 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 {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
@@ -261,12 +261,12 @@ export function Component({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
title: { title: {
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: '600',
fontSize: 24, fontSize: 24,
marginBottom: 18, marginBottom: 18,
}, },
label: { label: {
fontWeight: 'bold', fontWeight: '600',
paddingHorizontal: 4, paddingHorizontal: 4,
paddingBottom: 4, paddingBottom: 4,
marginTop: 20, marginTop: 20,
+7 -8
View File
@@ -1,19 +1,18 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' 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 {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {s} from '#/lib/styles'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import { import {
useOpenLink, useOpenLink,
useSetInAppBrowser, useSetInAppBrowser,
} from '#/state/preferences/in-app-browser' } 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] export const snapPoints = [350]
@@ -89,7 +88,7 @@ export function Component({href}: {href: string}) {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
title: { title: {
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: '600',
fontSize: 24, fontSize: 24,
marginBottom: 12, marginBottom: 12,
}, },
+6 -6
View File
@@ -9,7 +9,12 @@ import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {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 {useModalControls} from '#/state/modals'
import { import {
getMembership, getMembership,
@@ -19,11 +24,6 @@ import {
useListMembershipRemoveMutation, useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships' } from '#/state/queries/list-memberships'
import {useSession} from '#/state/session' 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 {MyLists} from '../lists/MyLists'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
@@ -71,7 +71,7 @@ export function Component({
style={[ style={[
{ {
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: '600',
fontSize: 20, fontSize: 20,
marginBottom: 12, marginBottom: 12,
paddingHorizontal: 12, paddingHorizontal: 12,
@@ -1,19 +1,20 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' 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 {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 {useModalControls} from '#/state/modals'
import { import {
useLanguagePrefs, useLanguagePrefs,
useLanguagePrefsApi, useLanguagePrefsApi,
} from '#/state/preferences/languages' } 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%'] export const snapPoints = ['100%']
@@ -37,10 +38,10 @@ export function Component({}: {}) {
langs.sort((a, b) => { langs.sort((a, b) => {
const hasA = const hasA =
langPrefs.contentLanguages.includes(a.code2) || langPrefs.contentLanguages.includes(a.code2) ||
deviceLocales.includes(a.code2) deviceLanguageCodes.includes(a.code2)
const hasB = const hasB =
langPrefs.contentLanguages.includes(b.code2) || langPrefs.contentLanguages.includes(b.code2) ||
deviceLocales.includes(b.code2) deviceLanguageCodes.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name) if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1 if (hasA) return -1
return 1 return 1
@@ -110,7 +111,7 @@ const styles = StyleSheet.create({
}, },
title: { title: {
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: '600',
fontSize: 24, fontSize: 24,
marginBottom: 12, marginBottom: 12,
}, },
@@ -1,20 +1,21 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' 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 {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 {useModalControls} from '#/state/modals'
import { import {
hasPostLanguage,
useLanguagePrefs, useLanguagePrefs,
useLanguagePrefsApi, useLanguagePrefsApi,
hasPostLanguage,
} from '#/state/preferences/languages' } 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%'] export const snapPoints = ['100%']
@@ -38,10 +39,10 @@ export function Component() {
langs.sort((a, b) => { langs.sort((a, b) => {
const hasA = const hasA =
hasPostLanguage(langPrefs.postLanguage, a.code2) || hasPostLanguage(langPrefs.postLanguage, a.code2) ||
deviceLocales.includes(a.code2) deviceLanguageCodes.includes(a.code2)
const hasB = const hasB =
hasPostLanguage(langPrefs.postLanguage, b.code2) || hasPostLanguage(langPrefs.postLanguage, b.code2) ||
deviceLocales.includes(b.code2) deviceLanguageCodes.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name) if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1 if (hasA) return -1
return 1 return 1
@@ -118,7 +119,7 @@ const styles = StyleSheet.create({
}, },
title: { title: {
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: '600',
fontSize: 24, fontSize: 24,
marginBottom: 12, marginBottom: 12,
}, },

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