diff --git a/app.config.js b/app.config.js index ddd72f75f0..fe65c4fde5 100644 --- a/app.config.js +++ b/app.config.js @@ -55,6 +55,7 @@ module.exports = function (config) { : undefined const UPDATES_ENABLED = !!UPDATES_CHANNEL + const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN) const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${ IS_DEV ? 'dev' : '' }` @@ -186,7 +187,15 @@ module.exports = function (config) { }, plugins: [ 'expo-localization', - Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo', + USE_SENTRY && [ + '@sentry/react-native/expo', + { + organization: 'blueskyweb', + project: 'react-native', + release: VERSION, + dist: SENTRY_DIST, + }, + ], [ 'expo-build-properties', { @@ -221,6 +230,31 @@ module.exports = function (config) { './plugins/shareExtension/withShareExtensions.js', './plugins/notificationsExtension/withNotificationsExtension.js', './plugins/withAppDelegateReferrer.js', + [ + 'expo-font', + { + fonts: [ + // './assets/fonts/inter/Inter-Thin.otf', + // './assets/fonts/inter/Inter-ThinItalic.otf', + // './assets/fonts/inter/Inter-ExtraLight.otf', + // './assets/fonts/inter/Inter-ExtraLightItalic.otf', + // './assets/fonts/inter/Inter-Light.otf', + // './assets/fonts/inter/Inter-LightItalic.otf', + './assets/fonts/inter/Inter-Regular.otf', + './assets/fonts/inter/Inter-Italic.otf', + './assets/fonts/inter/Inter-Medium.otf', + './assets/fonts/inter/Inter-MediumItalic.otf', + './assets/fonts/inter/Inter-SemiBold.otf', + './assets/fonts/inter/Inter-SemiBoldItalic.otf', + './assets/fonts/inter/Inter-Bold.otf', + './assets/fonts/inter/Inter-BoldItalic.otf', + './assets/fonts/inter/Inter-ExtraBold.otf', + './assets/fonts/inter/Inter-ExtraBoldItalic.otf', + './assets/fonts/inter/Inter-Black.otf', + './assets/fonts/inter/Inter-BlackItalic.otf', + ], + }, + ], ].filter(Boolean), extra: { eas: { @@ -263,7 +297,7 @@ module.exports = function (config) { * @see https://docs.expo.dev/guides/using-sentry/#app-configuration */ { - file: 'sentry-expo/upload-sourcemaps', + file: './postHooks/uploadSentrySourcemapsPostHook', config: { organization: 'blueskyweb', project: 'react-native', diff --git a/assets/fonts/inter/Inter-Black.otf b/assets/fonts/inter/Inter-Black.otf new file mode 100644 index 0000000000..44d1779af6 Binary files /dev/null and b/assets/fonts/inter/Inter-Black.otf differ diff --git a/assets/fonts/inter/Inter-BlackItalic.otf b/assets/fonts/inter/Inter-BlackItalic.otf new file mode 100644 index 0000000000..6fc475e415 Binary files /dev/null and b/assets/fonts/inter/Inter-BlackItalic.otf differ diff --git a/assets/fonts/inter/Inter-Bold.otf b/assets/fonts/inter/Inter-Bold.otf new file mode 100644 index 0000000000..58a38073e8 Binary files /dev/null and b/assets/fonts/inter/Inter-Bold.otf differ diff --git a/assets/fonts/inter/Inter-BoldItalic.otf b/assets/fonts/inter/Inter-BoldItalic.otf new file mode 100644 index 0000000000..e67935aa5a Binary files /dev/null and b/assets/fonts/inter/Inter-BoldItalic.otf differ diff --git a/assets/fonts/inter/Inter-ExtraBold.otf b/assets/fonts/inter/Inter-ExtraBold.otf new file mode 100644 index 0000000000..66cd95228c Binary files /dev/null and b/assets/fonts/inter/Inter-ExtraBold.otf differ diff --git a/assets/fonts/inter/Inter-ExtraBoldItalic.otf b/assets/fonts/inter/Inter-ExtraBoldItalic.otf new file mode 100644 index 0000000000..f269814a64 Binary files /dev/null and b/assets/fonts/inter/Inter-ExtraBoldItalic.otf differ diff --git a/assets/fonts/inter/Inter-ExtraLight.otf b/assets/fonts/inter/Inter-ExtraLight.otf new file mode 100644 index 0000000000..b603db3c77 Binary files /dev/null and b/assets/fonts/inter/Inter-ExtraLight.otf differ diff --git a/assets/fonts/inter/Inter-ExtraLightItalic.otf b/assets/fonts/inter/Inter-ExtraLightItalic.otf new file mode 100644 index 0000000000..f65051941c Binary files /dev/null and b/assets/fonts/inter/Inter-ExtraLightItalic.otf differ diff --git a/assets/fonts/inter/Inter-Italic.otf b/assets/fonts/inter/Inter-Italic.otf new file mode 100644 index 0000000000..f78848b987 Binary files /dev/null and b/assets/fonts/inter/Inter-Italic.otf differ diff --git a/assets/fonts/inter/Inter-Light.otf b/assets/fonts/inter/Inter-Light.otf new file mode 100644 index 0000000000..7da794bd3b Binary files /dev/null and b/assets/fonts/inter/Inter-Light.otf differ diff --git a/assets/fonts/inter/Inter-LightItalic.otf b/assets/fonts/inter/Inter-LightItalic.otf new file mode 100644 index 0000000000..32ef937c52 Binary files /dev/null and b/assets/fonts/inter/Inter-LightItalic.otf differ diff --git a/assets/fonts/inter/Inter-Medium.otf b/assets/fonts/inter/Inter-Medium.otf new file mode 100644 index 0000000000..f44f89adac Binary files /dev/null and b/assets/fonts/inter/Inter-Medium.otf differ diff --git a/assets/fonts/inter/Inter-MediumItalic.otf b/assets/fonts/inter/Inter-MediumItalic.otf new file mode 100644 index 0000000000..1970f57214 Binary files /dev/null and b/assets/fonts/inter/Inter-MediumItalic.otf differ diff --git a/assets/fonts/inter/Inter-Regular.otf b/assets/fonts/inter/Inter-Regular.otf new file mode 100644 index 0000000000..2d0bd1d645 Binary files /dev/null and b/assets/fonts/inter/Inter-Regular.otf differ diff --git a/assets/fonts/inter/Inter-SemiBold.otf b/assets/fonts/inter/Inter-SemiBold.otf new file mode 100644 index 0000000000..52c84550ba Binary files /dev/null and b/assets/fonts/inter/Inter-SemiBold.otf differ diff --git a/assets/fonts/inter/Inter-SemiBoldItalic.otf b/assets/fonts/inter/Inter-SemiBoldItalic.otf new file mode 100644 index 0000000000..b725bfc883 Binary files /dev/null and b/assets/fonts/inter/Inter-SemiBoldItalic.otf differ diff --git a/assets/fonts/inter/Inter-Thin.otf b/assets/fonts/inter/Inter-Thin.otf new file mode 100644 index 0000000000..568a18560c Binary files /dev/null and b/assets/fonts/inter/Inter-Thin.otf differ diff --git a/assets/fonts/inter/Inter-ThinItalic.otf b/assets/fonts/inter/Inter-ThinItalic.otf new file mode 100644 index 0000000000..c5ed37c358 Binary files /dev/null and b/assets/fonts/inter/Inter-ThinItalic.otf differ diff --git a/assets/icons/textSize_stroke2_corner0_rounded.svg b/assets/icons/textSize_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..6c7537d100 --- /dev/null +++ b/assets/icons/textSize_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/titleCase_stroke2_corner0_rounded.svg b/assets/icons/titleCase_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..facdfc0e16 --- /dev/null +++ b/assets/icons/titleCase_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/package.json b/package.json index 48cc8bfc24..ba7882902c 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "expo-dev-client": "^4.0.14", "expo-device": "~6.0.2", "expo-file-system": "^17.0.1", + "expo-font": "~12.0.10", "expo-haptics": "^13.0.1", "expo-image": "~1.12.9", "expo-image-manipulator": "^12.0.5", @@ -200,7 +201,6 @@ "react-responsive": "^9.0.2", "react-textarea-autosize": "^8.5.3", "rn-fetch-blob": "^0.12.0", - "sentry-expo": "~7.0.1", "statsig-react-native-expo": "^4.6.1", "tippy.js": "^6.3.7", "tlds": "^1.234.0", diff --git a/patches/@sentry+react-native+5.32.0.patch b/patches/@sentry+react-native+5.32.0.patch new file mode 100644 index 0000000000..a5ccecc21a --- /dev/null +++ b/patches/@sentry+react-native+5.32.0.patch @@ -0,0 +1,36 @@ +diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js +index 7e0b4cd..177454c 100644 +--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js ++++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js +@@ -3,6 +3,8 @@ import { LogBox } from 'react-native'; + * This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning. + */ + export function ignoreRequireCycleLogs() { +- LogBox.ignoreLogs(['Require cycle:']); ++ try { ++ LogBox.ignoreLogs(['Require cycle:']); ++ } catch (e) {} + } + //# sourceMappingURL=ignorerequirecyclelogs.js.map +\ No newline at end of file +diff --git a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js +index 0f244f2..ae7dfb3 100755 +--- a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js ++++ b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js +@@ -174,6 +174,7 @@ if (!outputDir) { + process.exit(1); + } + ++const otherArgs = process.argv.slice(3); + const files = getAssetPathsSync(outputDir); + const groupedAssets = groupAssets(files); + +@@ -195,7 +196,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) { + + const isHermes = assets.find(asset => asset.endsWith('.hbc')); + const windowsCallback = process.platform === "win32" ? 'node ' : ''; +- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, { ++ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} ${otherArgs.join(' ')}`, { + env: { + ...process.env, + [SENTRY_PROJECT]: sentryProject, diff --git a/patches/@sentry+react-native+5.5.0.patch b/patches/@sentry+react-native+5.5.0.patch deleted file mode 100644 index 2962aa44c2..0000000000 --- a/patches/@sentry+react-native+5.5.0.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js -index 7e0b4cd..177454c 100644 ---- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js -+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js -@@ -3,6 +3,8 @@ import { LogBox } from 'react-native'; - * This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning. - */ - export function ignoreRequireCycleLogs() { -- LogBox.ignoreLogs(['Require cycle:']); -+ try { -+ LogBox.ignoreLogs(['Require cycle:']); -+ } catch (e) {} - } - //# sourceMappingURL=ignorerequirecyclelogs.js.map -\ No newline at end of file diff --git a/patches/expo-modules-core+1.12.11.patch b/patches/expo-modules-core+1.12.11.patch index 4bfecb3880..4878bb9f7e 100644 --- a/patches/expo-modules-core+1.12.11.patch +++ b/patches/expo-modules-core+1.12.11.patch @@ -12,28 +12,3 @@ index bb74e80..0aa0202 100644 Map 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..c421931 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() { -diff --git a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift -index ee2268a..4851b67 100644 ---- a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift -+++ b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift -@@ -173,7 +173,7 @@ public final class SharedObjectRegistry { - } - - internal func clear() { -- Self.lockQueue.async { -+ Self.lockQueue.sync { - self.pairs.removeAll() - } - } diff --git a/postHooks/uploadSentrySourcemapsPostHook.js b/postHooks/uploadSentrySourcemapsPostHook.js new file mode 100644 index 0000000000..756e64253c --- /dev/null +++ b/postHooks/uploadSentrySourcemapsPostHook.js @@ -0,0 +1,34 @@ +const exec = require('child_process').execSync + +const SENTRY_AUTH_TOKEN = process.env.SENTRY_AUTH_TOKEN + +module.exports = ({config}) => { + if (!SENTRY_AUTH_TOKEN) { + console.log( + 'SENTRY_AUTH_TOKEN environment variable must be set to upload sourcemaps. Skipping.', + ) + return + } + + const org = config.organization + const project = config.project + const release = config.release + const dist = config.dist + + if (!org || !project || !release || !dist) { + console.log( + '"organization", "project", "release", and "dist" must be set in the hook config to upload sourcemaps. Skipping.', + ) + return + } + + try { + console.log('Uploading sourcemaps to Sentry...') + exec( + `node node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps dist --url https://sentry.io/ -o ${org} -p ${project} -r ${release} -d ${dist}`, + ) + console.log('Sourcemaps uploaded to Sentry.') + } catch (e) { + console.error('Error uploading sourcemaps to Sentry:', e) + } +} diff --git a/src/App.native.tsx b/src/App.native.tsx index 2ec666e2cc..9214253aca 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -55,7 +55,7 @@ import {TestCtrls} from '#/view/com/testing/TestCtrls' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' -import {ThemeProvider as Alf} from '#/alf' +import {ThemeProvider as Alf, useFonts} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' @@ -106,62 +106,60 @@ function InnerApp() { }, [_]) return ( - - - - - - + + + + + + - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + ) } function App() { const [isReady, setReady] = useState(false) + const [loaded] = useFonts() React.useEffect(() => { initPersistedState().then(() => setReady(true)) }, []) - if (!isReady) { + if (!isReady || !loaded) { return null } diff --git a/src/App.web.tsx b/src/App.web.tsx index 6efe7cc022..1c66507336 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -46,7 +46,7 @@ import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/Video import * as Toast from '#/view/com/util/Toast' import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' -import {ThemeProvider as Alf} from '#/alf' +import {ThemeProvider as Alf, useFonts} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' @@ -96,62 +96,61 @@ function InnerApp() { return ( - - - - - - + + + + + + - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + ) } function App() { const [isReady, setReady] = useState(false) + const [loaded] = useFonts() React.useEffect(() => { initPersistedState().then(() => setReady(true)) }, []) - if (!isReady) { + if (!isReady || !loaded) { return null } diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index d2e7ffc2ee..9f75d305ae 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -225,43 +225,43 @@ export const atoms = { }, text_2xs: { fontSize: tokens.fontSize._2xs, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_xs: { fontSize: tokens.fontSize.xs, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_sm: { fontSize: tokens.fontSize.sm, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_md: { fontSize: tokens.fontSize.md, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_lg: { fontSize: tokens.fontSize.lg, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_xl: { fontSize: tokens.fontSize.xl, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_2xl: { fontSize: tokens.fontSize._2xl, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_3xl: { fontSize: tokens.fontSize._3xl, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_4xl: { fontSize: tokens.fontSize._4xl, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, text_5xl: { fontSize: tokens.fontSize._5xl, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, leading_tight: { lineHeight: 1.15, @@ -273,10 +273,7 @@ export const atoms = { lineHeight: 1.5, }, tracking_normal: { - letterSpacing: 0, - }, - tracking_wide: { - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, }, font_normal: { fontWeight: tokens.fontWeight.normal, diff --git a/src/alf/fonts.ts b/src/alf/fonts.ts new file mode 100644 index 0000000000..ce658fa05b --- /dev/null +++ b/src/alf/fonts.ts @@ -0,0 +1,111 @@ +import {useFonts as defaultUseFonts} from 'expo-font' + +import {isNative, isWeb} from '#/platform/detection' +import {Device, device} from '#/storage' + +const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif` + +const factor = 0.0625 // 1 - (15/16) +const fontScaleMultipliers: Record = { + '-2': 1 - factor * 3, + '-1': 1 - factor * 2, + '0': 1 - factor * 1, // default + '1': 1, + '2': 1 + factor * 1, +} + +export function computeFontScaleMultiplier(scale: Device['fontScale']) { + return fontScaleMultipliers[scale] +} + +export function getFontScale() { + return device.get(['fontScale']) ?? '0' +} + +export function setFontScale(fontScale: Device['fontScale']) { + device.set(['fontScale'], fontScale) +} + +export function getFontFamily() { + return device.get(['fontFamily']) || 'theme' +} + +export function setFontFamily(fontFamily: Device['fontFamily']) { + device.set(['fontFamily'], fontFamily) +} + +/* + * Unused fonts are commented out, but the files are there if we need them. + */ +export function useFonts() { + /** + * For native, the `expo-font` config plugin embeds the fonts in the + * application binary. But `expo-font` isn't supported on web, so we fall + * back to async loading here. + */ + if (isNative) return [true, null] + return defaultUseFonts({ + // 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'), + // 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'), + // 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'), + // 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'), + // 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'), + // '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-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-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'), + }) +} + +/* + * Unused fonts are commented out, but the files are there if we need them. + */ +export function applyFonts( + style: Record, + fontFamily: 'system' | 'theme', +) { + if (fontFamily === 'theme') { + style.fontFamily = + { + // '100': 'Inter-Thin', + // '200': 'Inter-ExtraLight', + // '300': 'Inter-Light', + '100': 'Inter-Regular', + '200': 'Inter-Regular', + '300': 'Inter-Regular', + '400': 'Inter-Regular', + '500': 'Inter-Medium', + '600': 'Inter-SemiBold', + '700': 'Inter-Bold', + '800': 'Inter-ExtraBold', + '900': 'Inter-Black', + }[style.fontWeight as string] || 'Inter-Regular' + + if (style.fontStyle === 'italic') { + if (style.fontFamily === 'Inter-Regular') { + style.fontFamily = 'Inter-Italic' + } else { + style.fontFamily += 'Italic' + } + } + + // fallback families only supported on web + if (isWeb) { + style.fontFamily += `, ${FAMILIES}` + } + } else { + // fallback families only supported on web + if (isWeb) { + style.fontFamily = style.fontFamily || FAMILIES + } + } +} diff --git a/src/alf/index.tsx b/src/alf/index.tsx index d699de6a5b..f9d93d4ca8 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -1,25 +1,47 @@ import React from 'react' import {useMediaQuery} from 'react-responsive' +import { + computeFontScaleMultiplier, + getFontFamily, + getFontScale, + setFontFamily as persistFontFamily, + setFontScale as persistFontScale, +} from '#/alf/fonts' import {createThemes, defaultTheme} from '#/alf/themes' import {Theme, ThemeName} from '#/alf/types' import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration' +import {Device} from '#/storage' export {atoms} from '#/alf/atoms' +export * from '#/alf/fonts' export * as tokens from '#/alf/tokens' export * from '#/alf/types' export * from '#/alf/util/flatten' export * from '#/alf/util/platform' export * from '#/alf/util/themeSelector' -/* - * Context - */ -export const Context = React.createContext<{ +export type Alf = { themeName: ThemeName theme: Theme themes: ReturnType -}>({ + fonts: { + scale: Exclude + scaleMultiplier: number + family: Device['fontFamily'] + setFontScale: (fontScale: Exclude) => void + setFontFamily: (fontFamily: Device['fontFamily']) => void + } + /** + * Feature flags or other gated options + */ + flags: {} +} + +/* + * Context + */ +export const Context = React.createContext({ themeName: 'light', theme: defaultTheme, themes: createThemes({ @@ -29,12 +51,48 @@ export const Context = React.createContext<{ positive: GREEN_HUE, }, }), + fonts: { + scale: getFontScale(), + scaleMultiplier: computeFontScaleMultiplier(getFontScale()), + family: getFontFamily(), + setFontScale: () => {}, + setFontFamily: () => {}, + }, + flags: {}, }) export function ThemeProvider({ children, theme: themeName, }: React.PropsWithChildren<{theme: ThemeName}>) { + const [fontScale, setFontScale] = React.useState(() => + getFontScale(), + ) + const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() => + computeFontScaleMultiplier(fontScale), + ) + const setFontScaleAndPersist = React.useCallback< + Alf['fonts']['setFontScale'] + >( + fontScale => { + setFontScale(fontScale) + persistFontScale(fontScale) + setFontScaleMultiplier(computeFontScaleMultiplier(fontScale)) + }, + [setFontScale], + ) + const [fontFamily, setFontFamily] = React.useState( + () => getFontFamily(), + ) + const setFontFamilyAndPersist = React.useCallback< + Alf['fonts']['setFontFamily'] + >( + fontFamily => { + setFontFamily(fontFamily) + persistFontFamily(fontFamily) + }, + [setFontFamily], + ) const themes = React.useMemo(() => { return createThemes({ hues: { @@ -44,28 +102,47 @@ export function ThemeProvider({ }, }) }, []) - const theme = themes[themeName] return ( ( () => ({ themes, themeName: themeName, - theme: theme, + theme: themes[themeName], + fonts: { + scale: fontScale, + scaleMultiplier: fontScaleMultiplier, + family: fontFamily, + setFontScale: setFontScaleAndPersist, + setFontFamily: setFontFamilyAndPersist, + }, + flags: {}, }), - [theme, themeName, themes], + [ + themeName, + themes, + fontScale, + setFontScaleAndPersist, + fontFamily, + setFontFamilyAndPersist, + fontScaleMultiplier, + ], )}> {children} ) } +export function useAlf() { + return React.useContext(Context) +} + export function useTheme(theme?: ThemeName) { - const ctx = React.useContext(Context) + const alf = useAlf() return React.useMemo(() => { - return theme ? ctx.themes[theme] : ctx.theme - }, [theme, ctx]) + return theme ? alf.themes[theme] : alf.theme + }, [theme, alf]) } export function useBreakpoints() { diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 0208945eeb..d43d2b67dd 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -1,3 +1,7 @@ +import {Platform} from 'react-native' + +export const TRACKING = Platform.OS === 'android' ? 0.1 : 0 + export const color = { temp_purple: 'rgb(105 0 255)', temp_purple_dark: 'rgb(83 0 202)', diff --git a/src/components/Button.tsx b/src/components/Button.tsx index d65444e1f7..704aa9d987 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -7,7 +7,6 @@ import { PressableProps, StyleProp, StyleSheet, - Text, TextProps, TextStyle, View, @@ -17,7 +16,7 @@ import {LinearGradient} from 'expo-linear-gradient' import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf' import {Props as SVGIconProps} from '#/components/icons/common' -import {normalizeTextStyles} from '#/components/Typography' +import {Text} from '#/components/Typography' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' export type ButtonColor = @@ -635,14 +634,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) { const textStyles = useSharedButtonTextStyles() return ( - + {children} ) diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index cdce3765f0..d5d92048ad 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -37,6 +37,7 @@ import {Portal} from '#/components/Portal' export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/types' +export * from '#/components/Dialog/utils' // @ts-ignore export const Input = createInput(BottomSheetTextInput) diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index aff1842f77..bf20bd2956 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -27,6 +27,7 @@ import {Portal} from '#/components/Portal' export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/types' +export * from '#/components/Dialog/utils' export {Input} from '#/components/forms/TextField' const stopPropagation = (e: any) => e.stopPropagation() diff --git a/src/components/Dialog/utils.ts b/src/components/Dialog/utils.ts new file mode 100644 index 0000000000..058d6e8041 --- /dev/null +++ b/src/components/Dialog/utils.ts @@ -0,0 +1,18 @@ +import React from 'react' + +import {DialogControlProps} from '#/components/Dialog/types' + +export function useAutoOpen(control: DialogControlProps, showTimeout?: number) { + React.useEffect(() => { + if (showTimeout) { + const timeout = setTimeout(() => { + control.open() + }, showTimeout) + return () => { + clearTimeout(timeout) + } + } else { + control.open() + } + }, [control, showTimeout]) +} diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index 31dd931c6a..15f88468a7 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -3,7 +3,7 @@ import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native' import {UITextView} from 'react-native-uitextview' import {isNative} from '#/platform/detection' -import {atoms, flatten, useTheme, web} from '#/alf' +import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf' export type TextProps = RNTextProps & { /** @@ -34,19 +34,30 @@ export function leading< * If the `lineHeight` value is > 2, we assume it's an absolute value and * returns it as-is. */ -export function normalizeTextStyles(styles: StyleProp) { +export function normalizeTextStyles( + styles: StyleProp, + { + fontScale, + fontFamily, + }: { + fontScale: number + fontFamily: Alf['fonts']['family'] + } & Pick, +) { const s = flatten(styles) // should always be defined on these components - const fontSize = s.fontSize || atoms.text_md.fontSize + s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale if (s?.lineHeight) { if (s.lineHeight !== 0 && s.lineHeight <= 2) { - s.lineHeight = Math.round(fontSize * s.lineHeight) + s.lineHeight = Math.round(s.fontSize * s.lineHeight) } } else if (!isNative) { s.lineHeight = s.fontSize } + applyFonts(s, fontFamily) + return s } @@ -54,8 +65,13 @@ export function normalizeTextStyles(styles: StyleProp) { * Our main text component. Use this most of the time. */ export function Text({style, selectable, ...rest}: TextProps) { + const {fonts, flags} = useAlf() const t = useTheme() - const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) + const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], { + fontScale: fonts.scaleMultiplier, + fontFamily: fonts.family, + flags, + }) return } diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx new file mode 100644 index 0000000000..f33cea8e78 --- /dev/null +++ b/src/components/dialogs/nuxs/NeueTypography.tsx @@ -0,0 +1,119 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings' +import {atoms as a, useAlf, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {useNuxDialogContext} from '#/components/dialogs/nuxs' +import {Divider} from '#/components/Divider' +import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize' +import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' +import {Text} from '#/components/Typography' + +export function NeueTypography() { + const t = useTheme() + const {_} = useLingui() + const nuxDialogs = useNuxDialogContext() + const control = Dialog.useDialogControl() + const {fonts} = useAlf() + + Dialog.useAutoOpen(control, 3e3) + + const onClose = React.useCallback(() => { + nuxDialogs.dismissActiveNux() + }, [nuxDialogs]) + + const onChangeFontFamily = React.useCallback( + (values: string[]) => { + const next = values[0] === 'system' ? 'system' : 'theme' + fonts.setFontFamily(next) + }, + [fonts], + ) + + const onChangeFontScale = React.useCallback( + (values: string[]) => { + const next = values[0] || ('0' as any) + fonts.setFontScale(next) + }, + [fonts], + ) + + return ( + + + + + + + + Introducing new font settings ✨ + + + + To the ensure the best possible experience, we're introducing a + new theme font, along with adjustable font sizing settings. + + + + + Defaults are shown below. You can edit these in your Appearance + Settings later. + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index a38c87b68e..b93831ad35 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {AppBskyActorDefs} from '@atproto/api' import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' @@ -8,9 +9,16 @@ import { useRemoveNuxsMutation, useUpsertNuxMutation, } from '#/state/queries/nuxs' -import {useSession} from '#/state/session' +import { + usePreferencesQuery, + UsePreferencesQueryResponse, +} from '#/state/queries/preferences' +import {useProfileQuery} from '#/state/queries/profile' +import {SessionAccount, useSession} from '#/state/session' import {useOnboardingState} from '#/state/shell' +import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography' import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' +// NUXs import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' import {IS_DEV} from '#/env' @@ -21,11 +29,27 @@ type Context = { const queuedNuxs: { id: Nux - enabled?: (props: {gate: ReturnType}) => boolean + enabled?: (props: { + gate: ReturnType + currentAccount: SessionAccount + currentProfile: AppBskyActorDefs.ProfileViewDetailed + preferences: UsePreferencesQueryResponse + }) => boolean }[] = [ { id: Nux.TenMillionDialog, }, + { + id: Nux.NeueTypography, + enabled(props) { + if (props.currentProfile.createdAt) { + if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) { + return true + } + } + return false + }, + }, ] const Context = React.createContext({ @@ -38,12 +62,31 @@ export function useNuxDialogContext() { } export function NuxDialogs() { - const {hasSession} = useSession() - const onboardingState = useOnboardingState() - return hasSession && !onboardingState.isActive ? : null + const {currentAccount} = useSession() + const {data: preferences} = usePreferencesQuery() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) + const onboardingActive = useOnboardingState().isActive + + const isLoading = + !currentAccount || !preferences || !profile || onboardingActive + return !isLoading ? ( + + ) : null } -function Inner() { +function Inner({ + currentAccount, + currentProfile, + preferences, +}: { + currentAccount: SessionAccount + currentProfile: AppBskyActorDefs.ProfileViewDetailed + preferences: UsePreferencesQueryResponse +}) { const gate = useGate() const {nuxs} = useNuxs() const [snoozed, setSnoozed] = React.useState(() => { @@ -80,10 +123,19 @@ function Inner() { const nux = nuxs.find(nux => nux.id === id) // check if completed first - if (nux && nux.completed) continue + if (nux && nux.completed) { + continue + } // then check gate (track exposure) - if (enabled && !enabled({gate})) continue + if ( + enabled && + !enabled({gate, currentAccount, currentProfile, preferences}) + ) { + continue + } + + logger.debug(`NUX dialogs: activating '${id}' NUX`) // we have a winner setActiveNux(id) @@ -104,7 +156,16 @@ function Inner() { break } - }, [nuxs, snoozed, snoozeNuxDialog, upsertNux, gate]) + }, [ + nuxs, + snoozed, + snoozeNuxDialog, + upsertNux, + gate, + currentAccount, + currentProfile, + preferences, + ]) const ctx = React.useMemo(() => { return { @@ -116,6 +177,7 @@ function Inner() { return ( {activeNux === Nux.TenMillionDialog && } + {activeNux === Nux.NeueTypography && } ) } diff --git a/src/components/icons/TextSize.tsx b/src/components/icons/TextSize.tsx new file mode 100644 index 0000000000..73a6a085db --- /dev/null +++ b/src/components/icons/TextSize.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const TextSize_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z', +}) diff --git a/src/components/icons/TitleCase.tsx b/src/components/icons/TitleCase.tsx new file mode 100644 index 0000000000..9d040c9e5a --- /dev/null +++ b/src/components/icons/TitleCase.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const TitleCase_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z', +}) diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx index a0cc7cd6e8..5f93d982fd 100644 --- a/src/lib/analytics/analytics.tsx +++ b/src/lib/analytics/analytics.tsx @@ -2,12 +2,12 @@ import React from 'react' import {AppState, AppStateStatus} from 'react-native' import AsyncStorage from '@react-native-async-storage/async-storage' import {createClient, SegmentClient} from '@segment/analytics-react-native' +import * as Sentry from '@sentry/react-native' import {sha256} from 'js-sha256' -import {Native} from 'sentry-expo' -import {useSession, SessionAccount} from '#/state/session' -import {ScreenPropertiesMap, TrackPropertiesMap} from './types' import {logger} from '#/logger' +import {SessionAccount, useSession} from '#/state/session' +import {ScreenPropertiesMap, TrackPropertiesMap} from './types' type AppInfo = { build?: string | undefined @@ -72,7 +72,7 @@ export function init(account: SessionAccount | undefined) { if (account.did) { const did_hashed = sha256(account.did) client.identify(did_hashed, {did_hashed}) - Native.setUser({id: did_hashed}) + Sentry.setUser({id: did_hashed}) logger.debug('Ping w/hash') } else { logger.debug('Ping w/o hash') diff --git a/src/lib/analytics/analytics.web.tsx b/src/lib/analytics/analytics.web.tsx index 72b28f9c0f..c7f0ed3b14 100644 --- a/src/lib/analytics/analytics.web.tsx +++ b/src/lib/analytics/analytics.web.tsx @@ -1,11 +1,11 @@ import React from 'react' import {createClient} from '@segment/analytics-react' +import * as Sentry from '@sentry/react-native' import {sha256} from 'js-sha256' -import {Browser} from 'sentry-expo' -import {ScreenPropertiesMap, TrackPropertiesMap} from './types' -import {useSession, SessionAccount} from '#/state/session' import {logger} from '#/logger' +import {SessionAccount, useSession} from '#/state/session' +import {ScreenPropertiesMap, TrackPropertiesMap} from './types' type SegmentClient = ReturnType @@ -70,7 +70,7 @@ export function init(account: SessionAccount | undefined) { if (account.did) { const did_hashed = sha256(account.did) client.identify(did_hashed, {did_hashed}) - Browser.setUser({id: did_hashed}) + Sentry.setUser({id: did_hashed}) logger.debug('Ping w/hash') } else { logger.debug('Ping w/o hash') diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts index 1180b0db6c..37d41e3a2d 100644 --- a/src/lib/sentry.ts +++ b/src/lib/sentry.ts @@ -5,7 +5,7 @@ import {Platform} from 'react-native' import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application' -import {init} from 'sentry-expo' +import {init} from '@sentry/react-native' import {BUILD_ENV, IS_DEV, IS_TESTFLIGHT} from 'lib/app-info' @@ -30,10 +30,10 @@ const dist = `${Platform.OS}.${nativeBuildVersion}.${ }${IS_DEV ? 'dev' : ''}` init({ + enabled: !__DEV__, autoSessionTracking: false, dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432', debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production - enableInExpoDevelopment: false, // enable this to test in dev environment: BUILD_ENV ?? 'development', dist, release, diff --git a/src/lib/styles.ts b/src/lib/styles.ts index d0ea4cdc17..6a3d796110 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -79,13 +79,13 @@ export const s = StyleSheet.create({ // font weights fw600: {fontWeight: '600'}, - bold: {fontWeight: 'bold'}, + bold: {fontWeight: '700'}, fw500: {fontWeight: '500'}, semiBold: {fontWeight: '500'}, fw400: {fontWeight: '400'}, normal: {fontWeight: '400'}, - fw300: {fontWeight: '300'}, - light: {fontWeight: '300'}, + fw300: {fontWeight: '400'}, + light: {fontWeight: '400'}, fw200: {fontWeight: '200'}, // text decoration diff --git a/src/lib/themes.ts b/src/lib/themes.ts index 9590f16596..d16f9f632a 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -1,5 +1,6 @@ import {Platform} from 'react-native' +import {tokens} from '#/alf' import {darkPalette, dimPalette, lightPalette} from '#/alf/themes' import {colors} from './styles' import type {Theme} from './ThemeContext' @@ -88,163 +89,163 @@ export const defaultTheme: Theme = { typography: { '2xl-thin': { fontSize: 18, - letterSpacing: 0.25, - fontWeight: '300', + letterSpacing: tokens.TRACKING, + fontWeight: '400', }, '2xl': { fontSize: 18, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, '2xl-medium': { fontSize: 18, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, '2xl-bold': { fontSize: 18, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '700', }, '2xl-heavy': { fontSize: 18, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '800', }, 'xl-thin': { fontSize: 17, - letterSpacing: 0.25, - fontWeight: '300', + letterSpacing: tokens.TRACKING, + fontWeight: '400', }, xl: { fontSize: 17, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, 'xl-medium': { fontSize: 17, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, 'xl-bold': { fontSize: 17, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '700', }, 'xl-heavy': { fontSize: 17, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '800', }, 'lg-thin': { fontSize: 16, - letterSpacing: 0.25, - fontWeight: '300', + letterSpacing: tokens.TRACKING, + fontWeight: '400', }, lg: { fontSize: 16, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, 'lg-medium': { fontSize: 16, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, 'lg-bold': { fontSize: 16, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '700', }, 'lg-heavy': { fontSize: 16, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '800', }, 'md-thin': { fontSize: 15, - letterSpacing: 0.25, - fontWeight: '300', + letterSpacing: tokens.TRACKING, + fontWeight: '400', }, md: { fontSize: 15, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, 'md-medium': { fontSize: 15, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, 'md-bold': { fontSize: 15, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '700', }, 'md-heavy': { fontSize: 15, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '800', }, 'sm-thin': { fontSize: 14, - letterSpacing: 0.25, - fontWeight: '300', + letterSpacing: tokens.TRACKING, + fontWeight: '400', }, sm: { fontSize: 14, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, 'sm-medium': { fontSize: 14, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, 'sm-bold': { fontSize: 14, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '700', }, 'sm-heavy': { fontSize: 14, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '800', }, 'xs-thin': { fontSize: 13, - letterSpacing: 0.25, - fontWeight: '300', + letterSpacing: tokens.TRACKING, + fontWeight: '400', }, xs: { fontSize: 13, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, 'xs-medium': { fontSize: 13, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, 'xs-bold': { fontSize: 13, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '700', }, 'xs-heavy': { fontSize: 13, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '800', }, 'title-2xl': { fontSize: 34, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, 'title-xl': { fontSize: 28, - letterSpacing: 0.25, + letterSpacing: tokens.TRACKING, fontWeight: '500', }, 'title-lg': { @@ -254,32 +255,32 @@ export const defaultTheme: Theme = { title: { fontWeight: '500', fontSize: 20, - letterSpacing: 0.15, + letterSpacing: tokens.TRACKING, }, 'title-sm': { fontWeight: 'bold', fontSize: 17, - letterSpacing: 0.15, + letterSpacing: tokens.TRACKING, }, 'post-text': { fontSize: 16, - letterSpacing: 0.2, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, 'post-text-lg': { fontSize: 20, - letterSpacing: 0.2, + letterSpacing: tokens.TRACKING, fontWeight: '400', }, 'button-lg': { fontWeight: '500', fontSize: 18, - letterSpacing: 0.5, + letterSpacing: tokens.TRACKING, }, button: { fontWeight: '500', fontSize: 14, - letterSpacing: 0.5, + letterSpacing: tokens.TRACKING, }, mono: { fontSize: 14, diff --git a/src/logger/__tests__/logger.test.ts b/src/logger/__tests__/logger.test.ts index 12d60abaac..a3ccd037d5 100644 --- a/src/logger/__tests__/logger.test.ts +++ b/src/logger/__tests__/logger.test.ts @@ -1,6 +1,6 @@ +import {beforeAll, describe, expect, jest, test} from '@jest/globals' +import * as Sentry from '@sentry/react-native' import {nanoid} from 'nanoid/non-secure' -import {jest, describe, expect, test, beforeAll} from '@jest/globals' -import {Native as Sentry} from 'sentry-expo' import {Logger, LogLevel, sentryTransport} from '#/logger' @@ -16,12 +16,10 @@ jest.mock('#/env', () => ({ LOG_DEBUG: '', })) -jest.mock('sentry-expo', () => ({ - Native: { - addBreadcrumb: jest.fn(), - captureException: jest.fn(), - captureMessage: jest.fn(), - }, +jest.mock('@sentry/react-native', () => ({ + addBreadcrumb: jest.fn(), + captureException: jest.fn(), + captureMessage: jest.fn(), })) beforeAll(() => { diff --git a/src/logger/sentry/index.ts b/src/logger/sentry/index.ts index a2ed8452d0..e771560e73 100644 --- a/src/logger/sentry/index.ts +++ b/src/logger/sentry/index.ts @@ -1 +1 @@ -export {Native as Sentry} from 'sentry-expo' +export * as Sentry from '@sentry/react-native' diff --git a/src/logger/sentry/index.web.ts b/src/logger/sentry/index.web.ts index 072b997f44..e771560e73 100644 --- a/src/logger/sentry/index.web.ts +++ b/src/logger/sentry/index.web.ts @@ -1 +1 @@ -export {Browser as Sentry} from 'sentry-expo' +export * as Sentry from '@sentry/react-native' diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index 00a04bbfb6..d675fb38ed 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -14,17 +14,21 @@ import {s} from '#/lib/styles' import {useSetThemePrefs, useThemePrefs} from '#/state/shell' import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' import {ScrollView} from '#/view/com/util/Views' -import {atoms as a, native, useTheme} from '#/alf' +import {atoms as a, native, useAlf, useTheme} from '#/alf' import * as ToggleButton from '#/components/forms/ToggleButton' +import {Props as SVGIconProps} from '#/components/icons/common' import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' +import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize' +import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' import {Text} from '#/components/Typography' type Props = NativeStackScreenProps export function AppearanceSettingsScreen({}: Props) { - const {_} = useLingui() const t = useTheme() + const {_} = useLingui() const {isTabletOrMobile} = useWebMediaQueries() + const {fonts} = useAlf() const {colorMode, darkTheme} = useThemePrefs() const {setColorMode, setDarkTheme} = useSetThemePrefs() @@ -54,6 +58,22 @@ export function AppearanceSettingsScreen({}: Props) { [setDarkTheme, darkTheme], ) + const onChangeFontFamily = useCallback( + (values: string[]) => { + const next = values[0] === 'system' ? 'system' : 'theme' + fonts.setFontFamily(next) + }, + [fonts], + ) + + const onChangeFontScale = useCallback( + (values: string[]) => { + const next = values[0] || ('0' as any) + fonts.setFontScale(next) + }, + [fonts], + ) + return ( @@ -71,65 +91,143 @@ export function AppearanceSettingsScreen({}: Props) { - - - - - Mode - - - - - - System - - - - - Light - - - - - Dark - - - - {colorMode !== 'light' && ( - - - - - Dark theme - - + + + - - - - Dim - - - - - Dark - - - - - )} + {colorMode !== 'light' && ( + + + + )} + + + + + ) } + +export function AppearanceToggleButtonGroup({ + title, + description, + icon: Icon, + items, + values, + onChange, +}: { + title: string + description?: string + icon: React.ComponentType + items: { + label: string + name: string + }[] + values: string[] + onChange: (values: string[]) => void +}) { + const t = useTheme() + return ( + + + + + {title} + + {description && ( + + {description} + + )} + + + {items.map(item => ( + + {item.label} + + ))} + + + ) +} diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 865967d37a..63a8079623 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -4,15 +4,22 @@ import {BaseNux} from '#/state/queries/nuxs/types' export enum Nux { TenMillionDialog = 'TenMillionDialog', + NeueTypography = 'NeueTypography', } export const nuxNames = new Set(Object.values(Nux)) -export type AppNux = BaseNux<{ - id: Nux.TenMillionDialog - data: undefined -}> +export type AppNux = + | BaseNux<{ + id: Nux.TenMillionDialog + data: undefined + }> + | BaseNux<{ + id: Nux.NeueTypography + data: undefined + }> export const NuxSchemas: Record | undefined> = { [Nux.TenMillionDialog]: undefined, + [Nux.NeueTypography]: undefined, } diff --git a/src/state/shell/composer/useComposerKeyboardShortcut.tsx b/src/state/shell/composer/useComposerKeyboardShortcut.tsx index 0cdc6c7543..01306e36ea 100644 --- a/src/state/shell/composer/useComposerKeyboardShortcut.tsx +++ b/src/state/shell/composer/useComposerKeyboardShortcut.tsx @@ -48,7 +48,7 @@ export function useComposerKeyboardShortcut() { function handler(event: KeyboardEvent) { if (shouldIgnore(event)) return if ( - openDialogs.current.size > 0 || + openDialogs?.current.size > 0 || isModalActive || activeLightbox || isDrawerOpen diff --git a/src/storage/index.ts b/src/storage/index.ts index 819ffab7ec..4be08170dd 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -2,6 +2,8 @@ import {MMKV} from 'react-native-mmkv' import {Device} from '#/storage/schema' +export * from '#/storage/schema' + /** * Generic storage class. DO NOT use this directly. Instead, use the exported * storage instances below. diff --git a/src/storage/schema.ts b/src/storage/schema.ts index bc41fd3edf..1a9656fede 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -2,5 +2,7 @@ * Device data that's specific to the device and does not vary based account */ export type Device = { + fontScale: '-2' | '-1' | '0' | '1' | '2' + fontFamily: 'system' | 'theme' lastNuxDialog: string | undefined } diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx index 2ea9586eed..52a45b0e2e 100644 --- a/src/view/com/util/text/Text.tsx +++ b/src/view/com/util/text/Text.tsx @@ -1,10 +1,11 @@ import React from 'react' -import {Text as RNText, TextProps} from 'react-native' +import {StyleSheet, Text as RNText, TextProps} from 'react-native' import {UITextView} from 'react-native-uitextview' import {lh, s} from 'lib/styles' import {TypographyVariant, useTheme} from 'lib/ThemeContext' import {isIOS, isWeb} from 'platform/detection' +import {applyFonts, useAlf} from '#/alf' export type CustomTextProps = TextProps & { type?: TypographyVariant @@ -32,11 +33,28 @@ export function Text({ const theme = useTheme() const typography = theme.typography[type] const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined + const {fonts} = useAlf() if (selectable && isIOS) { + const flattened = StyleSheet.flatten([ + s.black, + typography, + lineHeightStyle, + style, + ]) + + applyFonts(flattened, fonts.family) + + // should always be defined on `typography` + // @ts-ignore + if (flattened.fontSize) { + // @ts-ignore + flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier + } + return ( @@ -45,15 +63,26 @@ export function Text({ ) } + const flattened = StyleSheet.flatten([ + s.black, + typography, + isWeb && fontFamilyStyle, + lineHeightStyle, + style, + ]) + + applyFonts(flattened, fonts.family) + + // should always be defined on `typography` + // @ts-ignore + if (flattened.fontSize) { + // @ts-ignore + flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier + } + return (