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