upstream:main → zio/dev
commit
fa997fa7c1
|
@ -74,8 +74,7 @@ appId: xyz.blueskyweb.app
|
||||||
- tapOn: "Delete List"
|
- tapOn: "Delete List"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "confirmBtn"
|
id: "confirmBtn"
|
||||||
- assertVisible:
|
- assertVisible: "This list is empty!"
|
||||||
id: "listsEmpty"
|
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
label: "Create a new curatelist"
|
label: "Create a new curatelist"
|
||||||
|
@ -161,17 +160,6 @@ appId: xyz.blueskyweb.app
|
||||||
- assertNotVisible:
|
- assertNotVisible:
|
||||||
id: "userAddRemoveListsModal"
|
id: "userAddRemoveListsModal"
|
||||||
|
|
||||||
- tapOn:
|
|
||||||
label: "Shows the curatelist on my profile"
|
|
||||||
id: "bottomBarProfileBtn"
|
|
||||||
- swipe:
|
|
||||||
from:
|
|
||||||
id: "profilePager-selector"
|
|
||||||
direction: LEFT
|
|
||||||
- tapOn:
|
|
||||||
id: "profilePager-selector-6"
|
|
||||||
- tapOn: "Good Ppl"
|
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
label: "Adds and removes users on curatelists from the profile"
|
label: "Adds and removes users on curatelists from the profile"
|
||||||
id: "bottomBarSearchBtn"
|
id: "bottomBarSearchBtn"
|
||||||
|
|
|
@ -21,14 +21,12 @@ appId: xyz.blueskyweb.app
|
||||||
id: "likeBtn"
|
id: "likeBtn"
|
||||||
childOf:
|
childOf:
|
||||||
id: "postThreadItem-by-bob.test"
|
id: "postThreadItem-by-bob.test"
|
||||||
- assertVisible:
|
- assertVisible: "1 like"
|
||||||
id: "likeCount-expanded"
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "likeBtn"
|
id: "likeBtn"
|
||||||
childOf:
|
childOf:
|
||||||
id: "postThreadItem-by-bob.test"
|
id: "postThreadItem-by-bob.test"
|
||||||
- assertNotVisible:
|
- assertNotVisible: "1 like"
|
||||||
id: "likeCount-expanded"
|
|
||||||
|
|
||||||
# Can like a reply post
|
# Can like a reply post
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|
|
@ -55,6 +55,7 @@ module.exports = function (config) {
|
||||||
: undefined
|
: undefined
|
||||||
const UPDATES_ENABLED = !!UPDATES_CHANNEL
|
const UPDATES_ENABLED = !!UPDATES_CHANNEL
|
||||||
|
|
||||||
|
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||||
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
|
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
|
||||||
IS_DEV ? 'dev' : ''
|
IS_DEV ? 'dev' : ''
|
||||||
}`
|
}`
|
||||||
|
@ -186,7 +187,15 @@ module.exports = function (config) {
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
'expo-localization',
|
'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',
|
'expo-build-properties',
|
||||||
{
|
{
|
||||||
|
@ -211,7 +220,6 @@ module.exports = function (config) {
|
||||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'expo-video',
|
|
||||||
'react-native-compressor',
|
'react-native-compressor',
|
||||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||||
'./plugins/withAndroidManifestPlugin.js',
|
'./plugins/withAndroidManifestPlugin.js',
|
||||||
|
@ -222,6 +230,31 @@ module.exports = function (config) {
|
||||||
'./plugins/shareExtension/withShareExtensions.js',
|
'./plugins/shareExtension/withShareExtensions.js',
|
||||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||||
'./plugins/withAppDelegateReferrer.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),
|
].filter(Boolean),
|
||||||
extra: {
|
extra: {
|
||||||
eas: {
|
eas: {
|
||||||
|
@ -264,7 +297,7 @@ module.exports = function (config) {
|
||||||
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
|
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
file: 'sentry-expo/upload-sourcemaps',
|
file: './postHooks/uploadSentrySourcemapsPostHook',
|
||||||
config: {
|
config: {
|
||||||
organization: 'blueskyweb',
|
organization: 'blueskyweb',
|
||||||
project: 'react-native',
|
project: 'react-native',
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 378 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 317 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 986 B |
|
@ -258,6 +258,51 @@
|
||||||
.force-no-clicks * {
|
.force-no-clicks * {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=range][orient=vertical] {
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
appearance: slider-vertical;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: bottom;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"][orient=vertical]::-webkit-slider-runnable-track {
|
||||||
|
background: white;
|
||||||
|
height: 100%;
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"][orient=vertical]::-moz-range-track {
|
||||||
|
background: white;
|
||||||
|
height: 100%;
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: white;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"][orient=vertical]::-moz-range-thumb {
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: white;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% include "scripts.html" %}
|
{% include "scripts.html" %}
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||||
|
|
|
@ -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,
|
|
@ -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
|
|
|
@ -12,28 +12,3 @@ index bb74e80..0aa0202 100644
|
||||||
|
|
||||||
Map<String, Object> constants = new HashMap<>(3);
|
Map<String, Object> constants = new HashMap<>(3);
|
||||||
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
|
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
|
||||||
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
|
|
||||||
index 109d3fe..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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,17 +52,17 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext'
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {Shell} from '#/view/shell'
|
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 {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
|
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
|
@ -106,63 +106,60 @@ function InnerApp() {
|
||||||
}, [_])
|
}, [_])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alf theme={theme}>
|
<StatsigProvider
|
||||||
<ThemeProvider theme={theme}>
|
// Resets the entire tree below when it changes:
|
||||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
key={currentAccount?.did}>
|
||||||
<ActiveVideoProvider>
|
<Alf theme={theme}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
<React.Fragment
|
<VideoVolumeProvider>
|
||||||
// Resets the entire tree below when it changes:
|
|
||||||
key={currentAccount?.did}>
|
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<StatsigProvider>
|
<MessagesProvider>
|
||||||
<MessagesProvider>
|
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
<LabelDefsProvider>
|
||||||
<LabelDefsProvider>
|
<ModerationOptsProvider>
|
||||||
<ModerationOptsProvider>
|
<LoggedOutViewProvider>
|
||||||
<LoggedOutViewProvider>
|
<SelectedFeedProvider>
|
||||||
<SelectedFeedProvider>
|
<HiddenRepliesProvider>
|
||||||
<HiddenRepliesProvider>
|
<UnreadNotifsProvider>
|
||||||
<UnreadNotifsProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<BackgroundNotificationPreferencesProvider>
|
<MutedThreadsProvider>
|
||||||
<MutedThreadsProvider>
|
<ProgressGuideProvider>
|
||||||
<ProgressGuideProvider>
|
<GestureHandlerRootView style={s.h100pct}>
|
||||||
<GestureHandlerRootView
|
<TestCtrls />
|
||||||
style={s.h100pct}>
|
<Shell />
|
||||||
<TestCtrls />
|
<NuxDialogs />
|
||||||
<Shell />
|
</GestureHandlerRootView>
|
||||||
</GestureHandlerRootView>
|
</ProgressGuideProvider>
|
||||||
</ProgressGuideProvider>
|
</MutedThreadsProvider>
|
||||||
</MutedThreadsProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</UnreadNotifsProvider>
|
||||||
</UnreadNotifsProvider>
|
</HiddenRepliesProvider>
|
||||||
</HiddenRepliesProvider>
|
</SelectedFeedProvider>
|
||||||
</SelectedFeedProvider>
|
</LoggedOutViewProvider>
|
||||||
</LoggedOutViewProvider>
|
</ModerationOptsProvider>
|
||||||
</ModerationOptsProvider>
|
</LabelDefsProvider>
|
||||||
</LabelDefsProvider>
|
</MessagesProvider>
|
||||||
</MessagesProvider>
|
|
||||||
</StatsigProvider>
|
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</React.Fragment>
|
</VideoVolumeProvider>
|
||||||
</RootSiblingParent>
|
</RootSiblingParent>
|
||||||
</ActiveVideoProvider>
|
</Splash>
|
||||||
</Splash>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</Alf>
|
||||||
</Alf>
|
</StatsigProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
const [loaded] = useFonts()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
|
||||||
PlatformInfo.setAudioActive(false)
|
|
||||||
initPersistedState().then(() => setReady(true))
|
initPersistedState().then(() => setReady(true))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady || !loaded) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,17 +35,20 @@ import {
|
||||||
} from '#/state/session'
|
} from '#/state/session'
|
||||||
import {readLastActiveAccount} from '#/state/session/util'
|
import {readLastActiveAccount} from '#/state/session/util'
|
||||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||||
|
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
|
||||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||||
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||||
import {Shell} from '#/view/shell/index'
|
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 {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
|
@ -60,6 +63,8 @@ function InnerApp() {
|
||||||
useIntentHandler()
|
useIntentHandler()
|
||||||
const hasCheckedReferrer = useStarterPackEntry()
|
const hasCheckedReferrer = useStarterPackEntry()
|
||||||
|
|
||||||
|
useComposerKeyboardShortcut()
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function onLaunch(account?: SessionAccount) {
|
async function onLaunch(account?: SessionAccount) {
|
||||||
|
@ -91,15 +96,15 @@ function InnerApp() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardProvider enabled={false}>
|
<KeyboardProvider enabled={false}>
|
||||||
<Alf theme={theme}>
|
<StatsigProvider
|
||||||
<ThemeProvider theme={theme}>
|
// Resets the entire tree below when it changes:
|
||||||
<RootSiblingParent>
|
key={currentAccount?.did}>
|
||||||
<ActiveVideoProvider>
|
<Alf theme={theme}>
|
||||||
<React.Fragment
|
<ThemeProvider theme={theme}>
|
||||||
// Resets the entire tree below when it changes:
|
<RootSiblingParent>
|
||||||
key={currentAccount?.did}>
|
<VideoVolumeProvider>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<ActiveVideoProvider>
|
||||||
<StatsigProvider>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<MessagesProvider>
|
<MessagesProvider>
|
||||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||||
<LabelDefsProvider>
|
<LabelDefsProvider>
|
||||||
|
@ -113,6 +118,7 @@ function InnerApp() {
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<ProgressGuideProvider>
|
<ProgressGuideProvider>
|
||||||
<Shell />
|
<Shell />
|
||||||
|
<NuxDialogs />
|
||||||
</ProgressGuideProvider>
|
</ProgressGuideProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
|
@ -124,26 +130,27 @@ function InnerApp() {
|
||||||
</ModerationOptsProvider>
|
</ModerationOptsProvider>
|
||||||
</LabelDefsProvider>
|
</LabelDefsProvider>
|
||||||
</MessagesProvider>
|
</MessagesProvider>
|
||||||
</StatsigProvider>
|
</QueryProvider>
|
||||||
</QueryProvider>
|
<ToastContainer />
|
||||||
</React.Fragment>
|
</ActiveVideoProvider>
|
||||||
<ToastContainer />
|
</VideoVolumeProvider>
|
||||||
</ActiveVideoProvider>
|
</RootSiblingParent>
|
||||||
</RootSiblingParent>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</Alf>
|
||||||
</Alf>
|
</StatsigProvider>
|
||||||
</KeyboardProvider>
|
</KeyboardProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
const [loaded, error] = useFonts()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
initPersistedState().then(() => setReady(true))
|
initPersistedState().then(() => setReady(true))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady || (!loaded && !error)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -225,43 +225,43 @@ export const atoms = {
|
||||||
},
|
},
|
||||||
text_2xs: {
|
text_2xs: {
|
||||||
fontSize: tokens.fontSize._2xs,
|
fontSize: tokens.fontSize._2xs,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_xs: {
|
text_xs: {
|
||||||
fontSize: tokens.fontSize.xs,
|
fontSize: tokens.fontSize.xs,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_sm: {
|
text_sm: {
|
||||||
fontSize: tokens.fontSize.sm,
|
fontSize: tokens.fontSize.sm,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_md: {
|
text_md: {
|
||||||
fontSize: tokens.fontSize.md,
|
fontSize: tokens.fontSize.md,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_lg: {
|
text_lg: {
|
||||||
fontSize: tokens.fontSize.lg,
|
fontSize: tokens.fontSize.lg,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_xl: {
|
text_xl: {
|
||||||
fontSize: tokens.fontSize.xl,
|
fontSize: tokens.fontSize.xl,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_2xl: {
|
text_2xl: {
|
||||||
fontSize: tokens.fontSize._2xl,
|
fontSize: tokens.fontSize._2xl,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_3xl: {
|
text_3xl: {
|
||||||
fontSize: tokens.fontSize._3xl,
|
fontSize: tokens.fontSize._3xl,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_4xl: {
|
text_4xl: {
|
||||||
fontSize: tokens.fontSize._4xl,
|
fontSize: tokens.fontSize._4xl,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
text_5xl: {
|
text_5xl: {
|
||||||
fontSize: tokens.fontSize._5xl,
|
fontSize: tokens.fontSize._5xl,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
leading_tight: {
|
leading_tight: {
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
|
@ -273,10 +273,7 @@ export const atoms = {
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
},
|
},
|
||||||
tracking_normal: {
|
tracking_normal: {
|
||||||
letterSpacing: 0,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
|
||||||
tracking_wide: {
|
|
||||||
letterSpacing: 0.25,
|
|
||||||
},
|
},
|
||||||
font_normal: {
|
font_normal: {
|
||||||
fontWeight: tokens.fontWeight.normal,
|
fontWeight: tokens.fontWeight.normal,
|
||||||
|
|
|
@ -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<Device['fontScale'], number> = {
|
||||||
|
'-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<string, any>,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,98 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {useMediaQuery} from 'react-responsive'
|
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 {createThemes, defaultTheme} from '#/alf/themes'
|
||||||
import {Theme, ThemeName} from '#/alf/types'
|
import {Theme, ThemeName} from '#/alf/types'
|
||||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||||
|
import {Device} from '#/storage'
|
||||||
|
|
||||||
export {atoms} from '#/alf/atoms'
|
export {atoms} from '#/alf/atoms'
|
||||||
|
export * from '#/alf/fonts'
|
||||||
export * as tokens from '#/alf/tokens'
|
export * as tokens from '#/alf/tokens'
|
||||||
export * from '#/alf/types'
|
export * from '#/alf/types'
|
||||||
export * from '#/alf/util/flatten'
|
export * from '#/alf/util/flatten'
|
||||||
export * from '#/alf/util/platform'
|
export * from '#/alf/util/platform'
|
||||||
export * from '#/alf/util/themeSelector'
|
export * from '#/alf/util/themeSelector'
|
||||||
|
|
||||||
|
export type Alf = {
|
||||||
|
themeName: ThemeName
|
||||||
|
theme: Theme
|
||||||
|
themes: ReturnType<typeof createThemes>
|
||||||
|
fonts: {
|
||||||
|
scale: Exclude<Device['fontScale'], undefined>
|
||||||
|
scaleMultiplier: number
|
||||||
|
family: Device['fontFamily']
|
||||||
|
setFontScale: (fontScale: Exclude<Device['fontScale'], undefined>) => void
|
||||||
|
setFontFamily: (fontFamily: Device['fontFamily']) => void
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Feature flags or other gated options
|
||||||
|
*/
|
||||||
|
flags: {}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Context
|
* Context
|
||||||
*/
|
*/
|
||||||
export const Context = React.createContext<{
|
export const Context = React.createContext<Alf>({
|
||||||
themeName: ThemeName
|
|
||||||
theme: Theme
|
|
||||||
}>({
|
|
||||||
themeName: 'light',
|
themeName: 'light',
|
||||||
theme: defaultTheme,
|
theme: defaultTheme,
|
||||||
|
themes: createThemes({
|
||||||
|
hues: {
|
||||||
|
primary: BLUE_HUE,
|
||||||
|
negative: RED_HUE,
|
||||||
|
positive: GREEN_HUE,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fonts: {
|
||||||
|
scale: getFontScale(),
|
||||||
|
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
|
||||||
|
family: getFontFamily(),
|
||||||
|
setFontScale: () => {},
|
||||||
|
setFontFamily: () => {},
|
||||||
|
},
|
||||||
|
flags: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
theme: themeName,
|
theme: themeName,
|
||||||
}: React.PropsWithChildren<{theme: ThemeName}>) {
|
}: React.PropsWithChildren<{theme: ThemeName}>) {
|
||||||
|
const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() =>
|
||||||
|
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<Alf['fonts']['family']>(
|
||||||
|
() => getFontFamily(),
|
||||||
|
)
|
||||||
|
const setFontFamilyAndPersist = React.useCallback<
|
||||||
|
Alf['fonts']['setFontFamily']
|
||||||
|
>(
|
||||||
|
fontFamily => {
|
||||||
|
setFontFamily(fontFamily)
|
||||||
|
persistFontFamily(fontFamily)
|
||||||
|
},
|
||||||
|
[setFontFamily],
|
||||||
|
)
|
||||||
const themes = React.useMemo(() => {
|
const themes = React.useMemo(() => {
|
||||||
return createThemes({
|
return createThemes({
|
||||||
hues: {
|
hues: {
|
||||||
|
@ -36,24 +102,47 @@ export function ThemeProvider({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
const theme = themes[themeName]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider
|
<Context.Provider
|
||||||
value={React.useMemo(
|
value={React.useMemo<Alf>(
|
||||||
() => ({
|
() => ({
|
||||||
|
themes,
|
||||||
themeName: themeName,
|
themeName: themeName,
|
||||||
theme: theme,
|
theme: themes[themeName],
|
||||||
|
fonts: {
|
||||||
|
scale: fontScale,
|
||||||
|
scaleMultiplier: fontScaleMultiplier,
|
||||||
|
family: fontFamily,
|
||||||
|
setFontScale: setFontScaleAndPersist,
|
||||||
|
setFontFamily: setFontFamilyAndPersist,
|
||||||
|
},
|
||||||
|
flags: {},
|
||||||
}),
|
}),
|
||||||
[theme, themeName],
|
[
|
||||||
|
themeName,
|
||||||
|
themes,
|
||||||
|
fontScale,
|
||||||
|
setFontScaleAndPersist,
|
||||||
|
fontFamily,
|
||||||
|
setFontFamilyAndPersist,
|
||||||
|
fontScaleMultiplier,
|
||||||
|
],
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTheme() {
|
export function useAlf() {
|
||||||
return React.useContext(Context).theme
|
return React.useContext(Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme(theme?: ThemeName) {
|
||||||
|
const alf = useAlf()
|
||||||
|
return React.useMemo(() => {
|
||||||
|
return theme ? alf.themes[theme] : alf.theme
|
||||||
|
}, [theme, alf])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBreakpoints() {
|
export function useBreakpoints() {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
export const TRACKING = Platform.OS === 'android' ? 0.1 : 0
|
||||||
|
|
||||||
export const color = {
|
export const color = {
|
||||||
temp_purple: 'rgb(105 0 255)',
|
temp_purple: 'rgb(105 0 255)',
|
||||||
temp_purple_dark: 'rgb(83 0 202)',
|
temp_purple_dark: 'rgb(83 0 202)',
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
PressableProps,
|
PressableProps,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
|
||||||
TextProps,
|
TextProps,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
View,
|
View,
|
||||||
|
@ -17,7 +16,7 @@ import {LinearGradient} from 'expo-linear-gradient'
|
||||||
|
|
||||||
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
|
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
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 ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
|
||||||
export type ButtonColor =
|
export type ButtonColor =
|
||||||
|
@ -635,14 +634,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||||
const textStyles = useSharedButtonTextStyles()
|
const textStyles = useSharedButtonTextStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
|
||||||
{...rest}
|
|
||||||
style={normalizeTextStyles([
|
|
||||||
a.font_bold,
|
|
||||||
a.text_center,
|
|
||||||
textStyles,
|
|
||||||
style,
|
|
||||||
])}>
|
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {Portal} from '#/components/Portal'
|
||||||
|
|
||||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||||
export * from '#/components/Dialog/types'
|
export * from '#/components/Dialog/types'
|
||||||
|
export * from '#/components/Dialog/utils'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const Input = createInput(BottomSheetTextInput)
|
export const Input = createInput(BottomSheetTextInput)
|
||||||
|
|
||||||
|
@ -256,7 +257,7 @@ export const ScrollableInner = React.forwardRef<
|
||||||
borderTopLeftRadius: 40,
|
borderTopLeftRadius: 40,
|
||||||
borderTopRightRadius: 40,
|
borderTopRightRadius: 40,
|
||||||
},
|
},
|
||||||
flatten(style),
|
style,
|
||||||
]}
|
]}
|
||||||
contentContainerStyle={a.pb_4xl}
|
contentContainerStyle={a.pb_4xl}
|
||||||
ref={ref}>
|
ref={ref}>
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {Portal} from '#/components/Portal'
|
||||||
|
|
||||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||||
export * from '#/components/Dialog/types'
|
export * from '#/components/Dialog/types'
|
||||||
|
export * from '#/components/Dialog/utils'
|
||||||
export {Input} from '#/components/forms/TextField'
|
export {Input} from '#/components/forms/TextField'
|
||||||
|
|
||||||
const stopPropagation = (e: any) => e.stopPropagation()
|
const stopPropagation = (e: any) => e.stopPropagation()
|
||||||
|
|
|
@ -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])
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||||
|
|
||||||
|
export function Fill({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: {children?: React.ReactNode} & ViewStyleProp) {
|
||||||
|
return <View style={[a.absolute, a.inset_0, style]}>{children}</View>
|
||||||
|
}
|
|
@ -75,6 +75,7 @@ export function LikedByList({uri}: {uri: string}) {
|
||||||
isLoading={isUriLoading || isLikedByLoading}
|
isLoading={isUriLoading || isLikedByLoading}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
emptyType="results"
|
emptyType="results"
|
||||||
|
emptyTitle={_(msg`No likes yet`)}
|
||||||
emptyMessage={_(
|
emptyMessage={_(
|
||||||
msg`Nobody has liked this yet. Maybe you should be the first!`,
|
msg`Nobody has liked this yet. Maybe you should be the first!`,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||||
|
import {Fill} from '#/components/Fill'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies and thin border within a bounding box. Used to contrast media from
|
||||||
|
* bg of the container.
|
||||||
|
*/
|
||||||
|
export function MediaInsetBorder({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
opaque,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode
|
||||||
|
/**
|
||||||
|
* Used where this border needs to match adjacent borders, such as in
|
||||||
|
* external link previews
|
||||||
|
*/
|
||||||
|
opaque?: boolean
|
||||||
|
} & ViewStyleProp) {
|
||||||
|
const t = useTheme()
|
||||||
|
const isLight = t.name === 'light'
|
||||||
|
return (
|
||||||
|
<Fill
|
||||||
|
style={[
|
||||||
|
a.rounded_sm,
|
||||||
|
a.border,
|
||||||
|
opaque
|
||||||
|
? [t.atoms.border_contrast_low]
|
||||||
|
: [
|
||||||
|
isLight
|
||||||
|
? t.atoms.border_contrast_low
|
||||||
|
: t.atoms.border_contrast_high,
|
||||||
|
{opacity: 0.6},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Fill>
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {parseTenorGif} from '#/lib/strings/embed-player'
|
import {parseTenorGif} from '#/lib/strings/embed-player'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
|
|
||||||
|
@ -104,6 +105,7 @@ export function ImageItem({
|
||||||
accessibilityHint={alt}
|
accessibilityHint={alt}
|
||||||
accessibilityLabel=""
|
accessibilityLabel=""
|
||||||
/>
|
/>
|
||||||
|
<MediaInsetBorder style={[a.rounded_xs]} />
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -59,7 +59,9 @@ export function Outer({
|
||||||
export function TitleText({children}: React.PropsWithChildren<{}>) {
|
export function TitleText({children}: React.PropsWithChildren<{}>) {
|
||||||
const {titleId} = React.useContext(Context)
|
const {titleId} = React.useContext(Context)
|
||||||
return (
|
return (
|
||||||
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
|
<Text
|
||||||
|
nativeID={titleId}
|
||||||
|
style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface ProfilesListProps {
|
||||||
|
|
||||||
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||||
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
|
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
|
||||||
const feed: FeedDescriptor = `list|${listUri}|as_following`
|
const feed: FeedDescriptor = `list|${listUri}`
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const onScrollToTop = useCallback(() => {
|
const onScrollToTop = useCallback(() => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
||||||
import {UITextView} from 'react-native-uitextview'
|
import {UITextView} from 'react-native-uitextview'
|
||||||
|
|
||||||
import {isNative} from '#/platform/detection'
|
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 & {
|
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
|
* If the `lineHeight` value is > 2, we assume it's an absolute value and
|
||||||
* returns it as-is.
|
* returns it as-is.
|
||||||
*/
|
*/
|
||||||
export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
|
export function normalizeTextStyles(
|
||||||
|
styles: StyleProp<TextStyle>,
|
||||||
|
{
|
||||||
|
fontScale,
|
||||||
|
fontFamily,
|
||||||
|
}: {
|
||||||
|
fontScale: number
|
||||||
|
fontFamily: Alf['fonts']['family']
|
||||||
|
} & Pick<Alf, 'flags'>,
|
||||||
|
) {
|
||||||
const s = flatten(styles)
|
const s = flatten(styles)
|
||||||
// should always be defined on these components
|
// 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) {
|
||||||
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
|
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) {
|
} else if (!isNative) {
|
||||||
s.lineHeight = s.fontSize
|
s.lineHeight = s.fontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyFonts(s, fontFamily)
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,8 +65,13 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
|
||||||
* Our main text component. Use this most of the time.
|
* Our main text component. Use this most of the time.
|
||||||
*/
|
*/
|
||||||
export function Text({style, selectable, ...rest}: TextProps) {
|
export function Text({style, selectable, ...rest}: TextProps) {
|
||||||
|
const {fonts, flags} = useAlf()
|
||||||
const t = useTheme()
|
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 <UITextView selectable={selectable} uiTextView style={s} {...rest} />
|
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Dialog.Outer control={control} onClose={onClose}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
|
||||||
|
<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'}]}>
|
||||||
|
<Trans>Introducing new font settings ✨</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_lg, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
To the ensure the best possible experience, we're introducing a
|
||||||
|
new theme font, along with adjustable font sizing settings.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>
|
||||||
|
Defaults are shown below. You can edit these in your Appearance
|
||||||
|
Settings later.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<View style={[a.gap_lg]}>
|
||||||
|
<AppearanceToggleButtonGroup
|
||||||
|
title={_(msg`Font`)}
|
||||||
|
description={_(
|
||||||
|
msg`For the best experience, we recommend using the theme font.`,
|
||||||
|
)}
|
||||||
|
icon={Aa}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: _(msg`System`),
|
||||||
|
name: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: _(msg`Theme`),
|
||||||
|
name: 'theme',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
values={[fonts.family]}
|
||||||
|
onChange={onChangeFontFamily}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AppearanceToggleButtonGroup
|
||||||
|
title={_(msg`Font size`)}
|
||||||
|
icon={TextSize}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: _(msg`Smaller`),
|
||||||
|
name: '-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: _(msg`Default`),
|
||||||
|
name: '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: _(msg`Larger`),
|
||||||
|
name: '1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
values={[fonts.scale]}
|
||||||
|
onChange={onChangeFontScale}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import Svg, {Circle, Path} from 'react-native-svg'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
|
||||||
|
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||||
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {InlineLinkText} from '#/components/Link'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {TenMillion} from './'
|
||||||
|
|
||||||
|
export function Trigger({children}: {children: ButtonProps['children']}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {mutate: upsertNux} = useUpsertNuxMutation()
|
||||||
|
const [show, setShow] = React.useState(false)
|
||||||
|
const [fallback, setFallback] = React.useState(false)
|
||||||
|
const control = Prompt.usePromptControl()
|
||||||
|
|
||||||
|
const handleOnPress = () => {
|
||||||
|
if (!fallback) {
|
||||||
|
setShow(true)
|
||||||
|
upsertNux({
|
||||||
|
id: Nux.TenMillionDialog,
|
||||||
|
completed: true,
|
||||||
|
data: undefined,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
control.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onHandleFallback = () => {
|
||||||
|
setFallback(true)
|
||||||
|
control.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Bluesky is celebrating 10 million users!`)}
|
||||||
|
onPress={handleOnPress}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{show && !fallback && (
|
||||||
|
<TenMillion
|
||||||
|
showTimeout={0}
|
||||||
|
onClose={() => setShow(false)}
|
||||||
|
onFallback={onHandleFallback}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Prompt.Outer control={control}>
|
||||||
|
<View style={{maxWidth: 300}}>
|
||||||
|
<Prompt.TitleText>
|
||||||
|
<Trans>Bluesky is celebrating 10 million users!</Trans>
|
||||||
|
</Prompt.TitleText>
|
||||||
|
</View>
|
||||||
|
<Prompt.DescriptionText>
|
||||||
|
<Trans>
|
||||||
|
Together, we're rebuilding the social internet. We're glad you're
|
||||||
|
here.
|
||||||
|
</Trans>
|
||||||
|
</Prompt.DescriptionText>
|
||||||
|
<Prompt.DescriptionText>
|
||||||
|
<Trans>
|
||||||
|
To learn more,{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`View our post`)}
|
||||||
|
to="/profile/bsky.app/post/3l47prg3wgy23"
|
||||||
|
onPress={() => {
|
||||||
|
control.close()
|
||||||
|
}}
|
||||||
|
style={[a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>check out our post.</Trans>
|
||||||
|
</InlineLinkText>
|
||||||
|
</Trans>
|
||||||
|
</Prompt.DescriptionText>
|
||||||
|
<Dialog.Close />
|
||||||
|
</Prompt.Outer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Icon({width, style}: {width: number} & ViewStyleProp) {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
|
||||||
|
<Path
|
||||||
|
fill="#dd2e44"
|
||||||
|
d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
|
||||||
|
/>
|
||||||
|
<Path
|
||||||
|
fill="#ea596e"
|
||||||
|
d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
|
||||||
|
/>
|
||||||
|
<Path
|
||||||
|
fill="#a0041e"
|
||||||
|
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
|
||||||
|
/>
|
||||||
|
<Path
|
||||||
|
fill="#aa8dd8"
|
||||||
|
d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
|
||||||
|
/>
|
||||||
|
<Path
|
||||||
|
fill="#77b255"
|
||||||
|
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
|
||||||
|
/>
|
||||||
|
<Path
|
||||||
|
fill="#aa8dd8"
|
||||||
|
d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
|
||||||
|
/>
|
||||||
|
<Path
|
||||||
|
fill="#77b255"
|
||||||
|
d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
|
||||||
|
/>
|
||||||
|
<Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
|
||||||
|
<Circle cx="2" cy="18" r="2" fill="#9266cc" />
|
||||||
|
<Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
|
||||||
|
<Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
|
||||||
|
<Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
|
||||||
|
<Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
|
||||||
|
<Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
|
||||||
|
<Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,719 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import Animated, {FadeIn} from 'react-native-reanimated'
|
||||||
|
import ViewShot from 'react-native-view-shot'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
|
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||||
|
import * as MediaLibrary from 'expo-media-library'
|
||||||
|
import {moderateProfile} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
|
import {getCanvas} from '#/lib/canvas'
|
||||||
|
import {shareUrl} from '#/lib/sharing'
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
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 {formatCount} from '#/view/com/util/numeric/format'
|
||||||
|
import {Logomark} from '#/view/icons/Logomark'
|
||||||
|
import * as Toast from 'view/com/util/Toast'
|
||||||
|
import {
|
||||||
|
atoms as a,
|
||||||
|
ThemeProvider,
|
||||||
|
tokens,
|
||||||
|
useBreakpoints,
|
||||||
|
useTheme,
|
||||||
|
} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||||
|
import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent'
|
||||||
|
import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent'
|
||||||
|
import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
|
import {GradientFill} from '#/components/GradientFill'
|
||||||
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download'
|
||||||
|
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
const DEBUG = false
|
||||||
|
const RATIO = 8 / 10
|
||||||
|
const WIDTH = 2000
|
||||||
|
const HEIGHT = WIDTH * RATIO
|
||||||
|
|
||||||
|
function getFontSize(count: number) {
|
||||||
|
const length = count.toString().length
|
||||||
|
if (length < 7) {
|
||||||
|
return 80
|
||||||
|
} else if (length < 5) {
|
||||||
|
return 100
|
||||||
|
} else {
|
||||||
|
return 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPercentBadge(percent: number) {
|
||||||
|
if (percent <= 0.001) {
|
||||||
|
return PointOnePercent
|
||||||
|
} else if (percent <= 0.01) {
|
||||||
|
return OnePercent
|
||||||
|
} else if (percent <= 0.1) {
|
||||||
|
return TenPercent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Frame({children}: {children: React.ReactNode}) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
a.overflow_hidden,
|
||||||
|
{
|
||||||
|
paddingTop: '80%',
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TenMillion({
|
||||||
|
showTimeout,
|
||||||
|
onClose,
|
||||||
|
onFallback,
|
||||||
|
}: {
|
||||||
|
showTimeout?: number
|
||||||
|
onClose?: () => void
|
||||||
|
onFallback?: () => void
|
||||||
|
}) {
|
||||||
|
const agent = useAgent()
|
||||||
|
const nuxDialogs = useNuxDialogContext()
|
||||||
|
const [userNumber, setUserNumber] = React.useState<number>(0)
|
||||||
|
const fetching = React.useRef(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
async function fetchUserNumber() {
|
||||||
|
const isBlueskyHosted = agent.sessionManager.pdsUrl
|
||||||
|
?.toString()
|
||||||
|
.includes('bsky.network')
|
||||||
|
|
||||||
|
if (isBlueskyHosted && agent.session?.accessJwt) {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${agent.session.accessJwt}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Network request failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (data.number && data.number <= 10_000_000) {
|
||||||
|
setUserNumber(data.number)
|
||||||
|
} else {
|
||||||
|
// should be rare
|
||||||
|
nuxDialogs.dismissActiveNux()
|
||||||
|
onFallback?.()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nuxDialogs.dismissActiveNux()
|
||||||
|
onFallback?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fetching.current) {
|
||||||
|
fetching.current = true
|
||||||
|
networkRetry(3, fetchUserNumber).catch(() => {
|
||||||
|
nuxDialogs.dismissActiveNux()
|
||||||
|
onFallback?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
agent.sessionManager.pdsUrl,
|
||||||
|
agent.session?.accessJwt,
|
||||||
|
setUserNumber,
|
||||||
|
nuxDialogs.dismissActiveNux,
|
||||||
|
nuxDialogs,
|
||||||
|
onFallback,
|
||||||
|
])
|
||||||
|
|
||||||
|
return userNumber ? (
|
||||||
|
<TenMillionInner
|
||||||
|
userNumber={userNumber}
|
||||||
|
showTimeout={showTimeout ?? 3e3}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TenMillionInner({
|
||||||
|
userNumber,
|
||||||
|
showTimeout,
|
||||||
|
onClose: onCloseOuter,
|
||||||
|
}: {
|
||||||
|
userNumber: number
|
||||||
|
showTimeout: number
|
||||||
|
onClose?: () => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const lightTheme = useTheme('light')
|
||||||
|
const {_, i18n} = useLingui()
|
||||||
|
const control = Dialog.useDialogControl()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {openComposer} = useComposerControls()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const {
|
||||||
|
isLoading: isProfileLoading,
|
||||||
|
data: profile,
|
||||||
|
error: profileError,
|
||||||
|
} = useProfileQuery({
|
||||||
|
did: currentAccount!.did,
|
||||||
|
})
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const nuxDialogs = useNuxDialogContext()
|
||||||
|
const moderation = React.useMemo(() => {
|
||||||
|
return profile && moderationOpts
|
||||||
|
? moderateProfile(profile, moderationOpts)
|
||||||
|
: undefined
|
||||||
|
}, [profile, moderationOpts])
|
||||||
|
const [uri, setUri] = React.useState<string | null>(null)
|
||||||
|
const percent = userNumber / 10_000_000
|
||||||
|
const Badge = getPercentBadge(percent)
|
||||||
|
const isLoadingData = isProfileLoading || !moderation || !profile
|
||||||
|
const isLoadingImage = !uri
|
||||||
|
|
||||||
|
const displayName = React.useMemo(() => {
|
||||||
|
if (!profile || !moderation) return ''
|
||||||
|
return sanitizeDisplayName(
|
||||||
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)
|
||||||
|
}, [profile, moderation])
|
||||||
|
const handle = React.useMemo(() => {
|
||||||
|
if (!profile) return ''
|
||||||
|
return sanitizeHandle(profile.handle, '@')
|
||||||
|
}, [profile])
|
||||||
|
const joinedDate = React.useMemo(() => {
|
||||||
|
if (!profile || !profile.createdAt) return ''
|
||||||
|
const date = i18n.date(profile.createdAt, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
return date
|
||||||
|
}, [i18n, profile])
|
||||||
|
|
||||||
|
const error: string = React.useMemo(() => {
|
||||||
|
if (profileError) {
|
||||||
|
return _(
|
||||||
|
msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}, [_, profileError])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Opening and closing
|
||||||
|
*/
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
control.open()
|
||||||
|
}, showTimeout)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [control, showTimeout])
|
||||||
|
const onClose = React.useCallback(() => {
|
||||||
|
nuxDialogs.dismissActiveNux()
|
||||||
|
onCloseOuter?.()
|
||||||
|
}, [nuxDialogs, onCloseOuter])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
const sharePost = React.useCallback(() => {
|
||||||
|
if (uri) {
|
||||||
|
control.close(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
logEvent('tmd:post', {})
|
||||||
|
openComposer({
|
||||||
|
text: _(
|
||||||
|
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
|
||||||
|
userNumber,
|
||||||
|
)}!`,
|
||||||
|
),
|
||||||
|
imageUris: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
width: WIDTH,
|
||||||
|
height: HEIGHT,
|
||||||
|
altText: _(
|
||||||
|
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
|
||||||
|
userNumber,
|
||||||
|
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}, 1e3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
_,
|
||||||
|
i18n,
|
||||||
|
control,
|
||||||
|
openComposer,
|
||||||
|
uri,
|
||||||
|
userNumber,
|
||||||
|
displayName,
|
||||||
|
handle,
|
||||||
|
joinedDate,
|
||||||
|
])
|
||||||
|
const onNativeShare = React.useCallback(() => {
|
||||||
|
if (uri) {
|
||||||
|
control.close(() => {
|
||||||
|
logEvent('tmd:share', {})
|
||||||
|
shareUrl(uri)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [uri, control])
|
||||||
|
const onNativeDownload = React.useCallback(async () => {
|
||||||
|
if (uri) {
|
||||||
|
const res = await requestMediaLibraryPermissionsAsync()
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
Toast.show(
|
||||||
|
_(
|
||||||
|
msg`You must grant access to your photo library to save the image.`,
|
||||||
|
),
|
||||||
|
'xmark',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MediaLibrary.createAssetAsync(uri)
|
||||||
|
logEvent('tmd:download', {})
|
||||||
|
Toast.show(_(msg`Image saved to your camera roll!`))
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.log(e)
|
||||||
|
Toast.show(_(msg`An error occurred while saving the image!`), 'xmark')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [_, uri])
|
||||||
|
const onWebDownload = React.useCallback(async () => {
|
||||||
|
if (uri) {
|
||||||
|
const canvas = await getCanvas(uri)
|
||||||
|
const imgHref = canvas
|
||||||
|
.toDataURL('image/png')
|
||||||
|
.replace('image/png', 'image/octet-stream')
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.setAttribute('download', `Bluesky 10M Users.png`)
|
||||||
|
link.setAttribute('href', imgHref)
|
||||||
|
link.click()
|
||||||
|
logEvent('tmd:download', {})
|
||||||
|
}
|
||||||
|
}, [uri])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Canvas stuff
|
||||||
|
*/
|
||||||
|
const imageRef = React.useRef<ViewShot>(null)
|
||||||
|
const captureInProgress = React.useRef(false)
|
||||||
|
const onCanvasReady = React.useCallback(async () => {
|
||||||
|
if (
|
||||||
|
imageRef.current &&
|
||||||
|
imageRef.current.capture &&
|
||||||
|
!captureInProgress.current
|
||||||
|
) {
|
||||||
|
captureInProgress.current = true
|
||||||
|
const uri = await imageRef.current.capture()
|
||||||
|
setUri(uri)
|
||||||
|
}
|
||||||
|
}, [setUri])
|
||||||
|
const canvas = isLoadingData ? null : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.overflow_hidden,
|
||||||
|
DEBUG
|
||||||
|
? {
|
||||||
|
width: 600,
|
||||||
|
height: 600 * RATIO,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={{width: 600}}>
|
||||||
|
<ThemeProvider theme="light">
|
||||||
|
<Frame>
|
||||||
|
<ViewShot
|
||||||
|
ref={imageRef}
|
||||||
|
options={{width: WIDTH, height: HEIGHT}}
|
||||||
|
style={[a.absolute, a.inset_0]}>
|
||||||
|
<View
|
||||||
|
onLayout={onCanvasReady}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
{
|
||||||
|
top: -1,
|
||||||
|
bottom: -1,
|
||||||
|
left: -1,
|
||||||
|
right: -1,
|
||||||
|
paddingVertical: 48,
|
||||||
|
paddingHorizontal: 48,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<GradientFill gradient={tokens.gradients.bonfire} />
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.w_full,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_md,
|
||||||
|
{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
shadowRadius: 32,
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
elevation: 24,
|
||||||
|
shadowColor: tokens.gradients.bonfire.values[0][1],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.px_xl,
|
||||||
|
a.py_xl,
|
||||||
|
{
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Logomark fill={t.palette.primary_500} width={36} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Centered content */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
paddingBottom: isNative ? 0 : 24,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_bold,
|
||||||
|
a.text_center,
|
||||||
|
a.pb_sm,
|
||||||
|
lightTheme.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
Celebrating {formatCount(i18n, 10000000)} users
|
||||||
|
</Trans>{' '}
|
||||||
|
🎉
|
||||||
|
</Text>
|
||||||
|
<View style={[a.flex_row, a.align_start]}>
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{
|
||||||
|
color: t.palette.primary_500,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: '900',
|
||||||
|
width: 32,
|
||||||
|
top: isNative ? -10 : 0,
|
||||||
|
left: 0,
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateX: -16,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
#
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.text_center,
|
||||||
|
{
|
||||||
|
fontStyle: 'italic',
|
||||||
|
fontSize: getFontSize(userNumber),
|
||||||
|
lineHeight: getFontSize(userNumber),
|
||||||
|
fontWeight: '900',
|
||||||
|
letterSpacing: -2,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{i18n.number(userNumber)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{Badge && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
top: isNative ? 75 : 85,
|
||||||
|
right: '5%',
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
rotate: '8deg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Badge fill={t.palette.primary_500} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{/* End centered content */}
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.px_xl,
|
||||||
|
a.py_xl,
|
||||||
|
{
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
{/*
|
||||||
|
<UserAvatar
|
||||||
|
size={36}
|
||||||
|
avatar={profile.avatar}
|
||||||
|
moderation={moderation.ui('avatar')}
|
||||||
|
onLoad={onCanvasReady}
|
||||||
|
/>
|
||||||
|
*/}
|
||||||
|
<View style={[a.gap_2xs, a.flex_1]}>
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.text_sm,
|
||||||
|
a.font_bold,
|
||||||
|
a.leading_tight,
|
||||||
|
{maxWidth: '60%'},
|
||||||
|
]}>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.text_sm,
|
||||||
|
a.font_semibold,
|
||||||
|
a.leading_snug,
|
||||||
|
lightTheme.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
{handle}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{profile.createdAt && (
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
numberOfLines={1}
|
||||||
|
ellipsizeMode="head"
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.text_sm,
|
||||||
|
a.font_semibold,
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_right,
|
||||||
|
lightTheme.atoms.text_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Trans>Joined on {joinedDate}</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ViewShot>
|
||||||
|
</Frame>
|
||||||
|
</ThemeProvider>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Outer control={control} onClose={onClose}>
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
label={_(msg`Ten Million`)}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
padding: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
isNative && {
|
||||||
|
borderTopLeftRadius: 40,
|
||||||
|
borderTopRightRadius: 40,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Frame>
|
||||||
|
<View
|
||||||
|
style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}>
|
||||||
|
<GradientFill gradient={tokens.gradients.bonfire} />
|
||||||
|
{error ? (
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_center,
|
||||||
|
a.pb_md,
|
||||||
|
{
|
||||||
|
maxWidth: 300,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
(╯°□°)╯︵ ┻━┻
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xl,
|
||||||
|
a.font_bold,
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_center,
|
||||||
|
{
|
||||||
|
maxWidth: 300,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : isLoadingData || isLoadingImage ? (
|
||||||
|
<Loader size="xl" fill="white" />
|
||||||
|
) : (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.duration(150)}
|
||||||
|
style={[a.w_full, a.h_full]}>
|
||||||
|
<Image
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
source={{uri}}
|
||||||
|
style={[a.w_full, a.h_full]}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
{canvas}
|
||||||
|
|
||||||
|
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
style={[
|
||||||
|
a.text_5xl,
|
||||||
|
a.leading_tight,
|
||||||
|
a.pb_lg,
|
||||||
|
{
|
||||||
|
fontWeight: '900',
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Trans>Thanks for being one of our first 10 million users.</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[a.leading_snug, a.text_lg, a.pb_xl]}>
|
||||||
|
<Trans>
|
||||||
|
Together, we're rebuilding the social internet. We're glad
|
||||||
|
you're here.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_end,
|
||||||
|
a.gap_md,
|
||||||
|
a.pt_xl,
|
||||||
|
]}>
|
||||||
|
{gtMobile && (
|
||||||
|
<Text
|
||||||
|
style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Brag a little!</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isLoadingImage}
|
||||||
|
label={
|
||||||
|
isNative && isIOS
|
||||||
|
? _(msg`Share image externally`)
|
||||||
|
: _(msg`Download image`)
|
||||||
|
}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
shape="square"
|
||||||
|
onPress={
|
||||||
|
isNative
|
||||||
|
? isIOS
|
||||||
|
? onNativeShare
|
||||||
|
: onNativeDownload
|
||||||
|
: onWebDownload
|
||||||
|
}>
|
||||||
|
<ButtonIcon icon={isNative && isIOS ? Share : Download} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isLoadingImage}
|
||||||
|
label={_(msg`Share image in post`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
onPress={sharePost}>
|
||||||
|
<ButtonText>{_(msg`Share`)}</ButtonText>
|
||||||
|
<ButtonIcon position="right" icon={ImageIcon} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
|
|
||||||
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {
|
||||||
|
Nux,
|
||||||
|
useNuxs,
|
||||||
|
useRemoveNuxsMutation,
|
||||||
|
useUpsertNuxMutation,
|
||||||
|
} from '#/state/queries/nuxs'
|
||||||
|
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'
|
||||||
|
|
||||||
|
type Context = {
|
||||||
|
activeNux: Nux | undefined
|
||||||
|
dismissActiveNux: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const queuedNuxs: {
|
||||||
|
id: Nux
|
||||||
|
enabled?: (props: {
|
||||||
|
gate: ReturnType<typeof useGate>
|
||||||
|
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<Context>({
|
||||||
|
activeNux: undefined,
|
||||||
|
dismissActiveNux: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useNuxDialogContext() {
|
||||||
|
return React.useContext(Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NuxDialogs() {
|
||||||
|
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 ? (
|
||||||
|
<Inner
|
||||||
|
currentAccount={currentAccount}
|
||||||
|
currentProfile={profile}
|
||||||
|
preferences={preferences}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inner({
|
||||||
|
currentAccount,
|
||||||
|
currentProfile,
|
||||||
|
preferences,
|
||||||
|
}: {
|
||||||
|
currentAccount: SessionAccount
|
||||||
|
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
|
preferences: UsePreferencesQueryResponse
|
||||||
|
}) {
|
||||||
|
const gate = useGate()
|
||||||
|
const {nuxs} = useNuxs()
|
||||||
|
const [snoozed, setSnoozed] = React.useState(() => {
|
||||||
|
return isSnoozed()
|
||||||
|
})
|
||||||
|
const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
|
||||||
|
const {mutateAsync: upsertNux} = useUpsertNuxMutation()
|
||||||
|
const {mutate: removeNuxs} = useRemoveNuxsMutation()
|
||||||
|
|
||||||
|
const snoozeNuxDialog = React.useCallback(() => {
|
||||||
|
snooze()
|
||||||
|
setSnoozed(true)
|
||||||
|
}, [setSnoozed])
|
||||||
|
|
||||||
|
const dismissActiveNux = React.useCallback(() => {
|
||||||
|
if (!activeNux) return
|
||||||
|
setActiveNux(undefined)
|
||||||
|
}, [activeNux, setActiveNux])
|
||||||
|
|
||||||
|
if (IS_DEV && typeof window !== 'undefined') {
|
||||||
|
// @ts-ignore
|
||||||
|
window.clearNuxDialog = (id: Nux) => {
|
||||||
|
if (!IS_DEV || !id) return
|
||||||
|
removeNuxs([id])
|
||||||
|
unsnooze()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (snoozed) return
|
||||||
|
if (!nuxs) return
|
||||||
|
|
||||||
|
for (const {id, enabled} of queuedNuxs) {
|
||||||
|
const nux = nuxs.find(nux => nux.id === id)
|
||||||
|
|
||||||
|
// check if completed first
|
||||||
|
if (nux && nux.completed) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// then check gate (track exposure)
|
||||||
|
if (
|
||||||
|
enabled &&
|
||||||
|
!enabled({gate, currentAccount, currentProfile, preferences})
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`NUX dialogs: activating '${id}' NUX`)
|
||||||
|
|
||||||
|
// we have a winner
|
||||||
|
setActiveNux(id)
|
||||||
|
|
||||||
|
// immediately snooze for a day
|
||||||
|
snoozeNuxDialog()
|
||||||
|
|
||||||
|
// immediately update remote data (affects next reload)
|
||||||
|
upsertNux({
|
||||||
|
id,
|
||||||
|
completed: true,
|
||||||
|
data: undefined,
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
|
||||||
|
safeMessage: e.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
nuxs,
|
||||||
|
snoozed,
|
||||||
|
snoozeNuxDialog,
|
||||||
|
upsertNux,
|
||||||
|
gate,
|
||||||
|
currentAccount,
|
||||||
|
currentProfile,
|
||||||
|
preferences,
|
||||||
|
])
|
||||||
|
|
||||||
|
const ctx = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
activeNux,
|
||||||
|
dismissActiveNux,
|
||||||
|
}
|
||||||
|
}, [activeNux, dismissActiveNux])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={ctx}>
|
||||||
|
{activeNux === Nux.TenMillionDialog && <TenMillion />}
|
||||||
|
{activeNux === Nux.NeueTypography && <NeueTypography />}
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import {simpleAreDatesEqual} from '#/lib/strings/time'
|
||||||
|
import {device} from '#/storage'
|
||||||
|
|
||||||
|
export function snooze() {
|
||||||
|
device.set(['lastNuxDialog'], new Date().toISOString())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsnooze() {
|
||||||
|
device.set(['lastNuxDialog'], undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSnoozed() {
|
||||||
|
const lastNuxDialog = device.get(['lastNuxDialog'])
|
||||||
|
if (!lastNuxDialog) return false
|
||||||
|
const last = new Date(lastNuxDialog)
|
||||||
|
const now = new Date()
|
||||||
|
// already snoozed today
|
||||||
|
if (simpleAreDatesEqual(last, now)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Download_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z',
|
||||||
|
})
|
|
@ -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',
|
||||||
|
})
|
|
@ -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',
|
||||||
|
})
|
|
@ -4,7 +4,7 @@ import {View} from 'react-native'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||||
|
|
||||||
export function PlayButtonIcon({size = 36}: {size?: number}) {
|
export function PlayButtonIcon({size = 32}: {size?: number}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975
|
const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975
|
||||||
const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25
|
const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25
|
||||||
|
|
|
@ -2,12 +2,12 @@ import React from 'react'
|
||||||
import {AppState, AppStateStatus} from 'react-native'
|
import {AppState, AppStateStatus} from 'react-native'
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
import {createClient, SegmentClient} from '@segment/analytics-react-native'
|
import {createClient, SegmentClient} from '@segment/analytics-react-native'
|
||||||
|
import * as Sentry from '@sentry/react-native'
|
||||||
import {sha256} from 'js-sha256'
|
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 {logger} from '#/logger'
|
||||||
|
import {SessionAccount, useSession} from '#/state/session'
|
||||||
|
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
|
||||||
|
|
||||||
type AppInfo = {
|
type AppInfo = {
|
||||||
build?: string | undefined
|
build?: string | undefined
|
||||||
|
@ -72,7 +72,7 @@ export function init(account: SessionAccount | undefined) {
|
||||||
if (account.did) {
|
if (account.did) {
|
||||||
const did_hashed = sha256(account.did)
|
const did_hashed = sha256(account.did)
|
||||||
client.identify(did_hashed, {did_hashed})
|
client.identify(did_hashed, {did_hashed})
|
||||||
Native.setUser({id: did_hashed})
|
Sentry.setUser({id: did_hashed})
|
||||||
logger.debug('Ping w/hash')
|
logger.debug('Ping w/hash')
|
||||||
} else {
|
} else {
|
||||||
logger.debug('Ping w/o hash')
|
logger.debug('Ping w/o hash')
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {createClient} from '@segment/analytics-react'
|
import {createClient} from '@segment/analytics-react'
|
||||||
|
import * as Sentry from '@sentry/react-native'
|
||||||
import {sha256} from 'js-sha256'
|
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 {logger} from '#/logger'
|
||||||
|
import {SessionAccount, useSession} from '#/state/session'
|
||||||
|
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
|
||||||
|
|
||||||
type SegmentClient = ReturnType<typeof createClient>
|
type SegmentClient = ReturnType<typeof createClient>
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ export function init(account: SessionAccount | undefined) {
|
||||||
if (account.did) {
|
if (account.did) {
|
||||||
const did_hashed = sha256(account.did)
|
const did_hashed = sha256(account.did)
|
||||||
client.identify(did_hashed, {did_hashed})
|
client.identify(did_hashed, {did_hashed})
|
||||||
Browser.setUser({id: did_hashed})
|
Sentry.setUser({id: did_hashed})
|
||||||
logger.debug('Ping w/hash')
|
logger.debug('Ping w/hash')
|
||||||
} else {
|
} else {
|
||||||
logger.debug('Ping w/o hash')
|
logger.debug('Ping w/o hash')
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetFeed as GetCustomFeed,
|
AppBskyFeedGetFeed as GetCustomFeed,
|
||||||
BskyAgent,
|
BskyAgent,
|
||||||
|
jsonStringToLex,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {getContentLanguages} from '#/state/preferences/languages'
|
import {getContentLanguages} from '#/state/preferences/languages'
|
||||||
|
@ -111,7 +112,7 @@ async function loggedOutFetch({
|
||||||
}&limit=${limit}&lang=${contentLangs}`,
|
}&limit=${limit}&lang=${contentLangs}`,
|
||||||
{method: 'GET', headers: {'Accept-Language': contentLangs}},
|
{method: 'GET', headers: {'Accept-Language': contentLangs}},
|
||||||
)
|
)
|
||||||
let data = res.ok ? await res.json() : null
|
let data = res.ok ? jsonStringToLex(await res.text()) : null
|
||||||
if (data?.feed?.length) {
|
if (data?.feed?.length) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -126,7 +127,7 @@ async function loggedOutFetch({
|
||||||
}&limit=${limit}`,
|
}&limit=${limit}`,
|
||||||
{method: 'GET', headers: {'Accept-Language': ''}},
|
{method: 'GET', headers: {'Accept-Language': ''}},
|
||||||
)
|
)
|
||||||
data = res.ok ? await res.json() : null
|
data = res.ok ? jsonStringToLex(await res.text()) : null
|
||||||
if (data?.feed?.length) {
|
if (data?.feed?.length) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
export const getCanvas = (base64: string): Promise<HTMLCanvasElement> => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const image = new Image()
|
||||||
|
image.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = image.width
|
||||||
|
canvas.height = image.height
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx?.drawImage(image, 0, 0)
|
||||||
|
resolve(canvas)
|
||||||
|
}
|
||||||
|
image.src = base64
|
||||||
|
})
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from '#/components/icons/Heart2'
|
} from '#/components/icons/Heart2'
|
||||||
|
|
||||||
const animationConfig = {
|
const animationConfig = {
|
||||||
duration: 400,
|
duration: 600,
|
||||||
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
fill: 'forwards' as FillMode,
|
fill: 'forwards' as FillMode,
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ export function useIntentHandler() {
|
||||||
}, [incomingUrl, composeIntent, verifyEmailIntent])
|
}, [incomingUrl, composeIntent, verifyEmailIntent])
|
||||||
}
|
}
|
||||||
|
|
||||||
function useComposeIntent() {
|
export function useComposeIntent() {
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import {Platform} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
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'
|
import {BUILD_ENV, IS_DEV, IS_TESTFLIGHT} from 'lib/app-info'
|
||||||
|
|
||||||
|
@ -30,10 +30,10 @@ const dist = `${Platform.OS}.${nativeBuildVersion}.${
|
||||||
}${IS_DEV ? 'dev' : ''}`
|
}${IS_DEV ? 'dev' : ''}`
|
||||||
|
|
||||||
init({
|
init({
|
||||||
|
enabled: !__DEV__,
|
||||||
autoSessionTracking: false,
|
autoSessionTracking: false,
|
||||||
dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432',
|
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
|
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',
|
environment: BUILD_ENV ?? 'development',
|
||||||
dist,
|
dist,
|
||||||
release,
|
release,
|
||||||
|
|
|
@ -225,4 +225,8 @@ export type LogEvents = {
|
||||||
'test:gate1:sometimes': {}
|
'test:gate1:sometimes': {}
|
||||||
'test:gate2:always': {}
|
'test:gate2:always': {}
|
||||||
'test:gate2:sometimes': {}
|
'test:gate2:sometimes': {}
|
||||||
|
|
||||||
|
'tmd:share': {}
|
||||||
|
'tmd:download': {}
|
||||||
|
'tmd:post': {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,13 +79,13 @@ export const s = StyleSheet.create({
|
||||||
|
|
||||||
// font weights
|
// font weights
|
||||||
fw600: {fontWeight: '600'},
|
fw600: {fontWeight: '600'},
|
||||||
bold: {fontWeight: 'bold'},
|
bold: {fontWeight: '700'},
|
||||||
fw500: {fontWeight: '500'},
|
fw500: {fontWeight: '500'},
|
||||||
semiBold: {fontWeight: '500'},
|
semiBold: {fontWeight: '500'},
|
||||||
fw400: {fontWeight: '400'},
|
fw400: {fontWeight: '400'},
|
||||||
normal: {fontWeight: '400'},
|
normal: {fontWeight: '400'},
|
||||||
fw300: {fontWeight: '300'},
|
fw300: {fontWeight: '400'},
|
||||||
light: {fontWeight: '300'},
|
light: {fontWeight: '400'},
|
||||||
fw200: {fontWeight: '200'},
|
fw200: {fontWeight: '200'},
|
||||||
|
|
||||||
// text decoration
|
// text decoration
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {Platform} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
import {tokens} from '#/alf'
|
||||||
import {darkPalette, dimPalette, lightPalette} from '#/alf/themes'
|
import {darkPalette, dimPalette, lightPalette} from '#/alf/themes'
|
||||||
import {colors} from './styles'
|
import {colors} from './styles'
|
||||||
import type {Theme} from './ThemeContext'
|
import type {Theme} from './ThemeContext'
|
||||||
|
@ -88,163 +89,163 @@ export const defaultTheme: Theme = {
|
||||||
typography: {
|
typography: {
|
||||||
'2xl-thin': {
|
'2xl-thin': {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '300',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'2xl': {
|
'2xl': {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'2xl-medium': {
|
'2xl-medium': {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'2xl-bold': {
|
'2xl-bold': {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
'2xl-heavy': {
|
'2xl-heavy': {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
},
|
},
|
||||||
'xl-thin': {
|
'xl-thin': {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '300',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
xl: {
|
xl: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'xl-medium': {
|
'xl-medium': {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'xl-bold': {
|
'xl-bold': {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
'xl-heavy': {
|
'xl-heavy': {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
},
|
},
|
||||||
'lg-thin': {
|
'lg-thin': {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '300',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'lg-medium': {
|
'lg-medium': {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'lg-bold': {
|
'lg-bold': {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
'lg-heavy': {
|
'lg-heavy': {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
},
|
},
|
||||||
'md-thin': {
|
'md-thin': {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '300',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'md-medium': {
|
'md-medium': {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'md-bold': {
|
'md-bold': {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
'md-heavy': {
|
'md-heavy': {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
},
|
},
|
||||||
'sm-thin': {
|
'sm-thin': {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '300',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
sm: {
|
sm: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'sm-medium': {
|
'sm-medium': {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'sm-bold': {
|
'sm-bold': {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
'sm-heavy': {
|
'sm-heavy': {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
},
|
},
|
||||||
'xs-thin': {
|
'xs-thin': {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '300',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
xs: {
|
xs: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'xs-medium': {
|
'xs-medium': {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'xs-bold': {
|
'xs-bold': {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
'xs-heavy': {
|
'xs-heavy': {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
},
|
},
|
||||||
|
|
||||||
'title-2xl': {
|
'title-2xl': {
|
||||||
fontSize: 34,
|
fontSize: 34,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'title-xl': {
|
'title-xl': {
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
letterSpacing: 0.25,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
'title-lg': {
|
'title-lg': {
|
||||||
|
@ -254,32 +255,32 @@ export const defaultTheme: Theme = {
|
||||||
title: {
|
title: {
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
letterSpacing: 0.15,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
'title-sm': {
|
'title-sm': {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
letterSpacing: 0.15,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
'post-text': {
|
'post-text': {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'post-text-lg': {
|
'post-text-lg': {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: tokens.TRACKING,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
'button-lg': {
|
'button-lg': {
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: tokens.TRACKING,
|
||||||
},
|
},
|
||||||
mono: {
|
mono: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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 {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'
|
import {Logger, LogLevel, sentryTransport} from '#/logger'
|
||||||
|
|
||||||
|
@ -16,12 +16,10 @@ jest.mock('#/env', () => ({
|
||||||
LOG_DEBUG: '',
|
LOG_DEBUG: '',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('sentry-expo', () => ({
|
jest.mock('@sentry/react-native', () => ({
|
||||||
Native: {
|
addBreadcrumb: jest.fn(),
|
||||||
addBreadcrumb: jest.fn(),
|
captureException: jest.fn(),
|
||||||
captureException: jest.fn(),
|
captureMessage: jest.fn(),
|
||||||
captureMessage: jest.fn(),
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export {Native as Sentry} from 'sentry-expo'
|
export * as Sentry from '@sentry/react-native'
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export {Browser as Sentry} from 'sentry-expo'
|
export * as Sentry from '@sentry/react-native'
|
||||||
|
|
|
@ -144,8 +144,7 @@ export const ForgotPasswordForm = ({
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color={'primary'}
|
color={'primary'}
|
||||||
size="medium"
|
size="medium"
|
||||||
onPress={onPressNext}
|
onPress={onPressNext}>
|
||||||
disabled={!email}>
|
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Next</Trans>
|
<Trans>Next</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
|
|
@ -60,7 +60,6 @@ export const LoginForm = ({
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
const [isReady, setIsReady] = useState<boolean>(false)
|
|
||||||
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
||||||
useState<boolean>(false)
|
useState<boolean>(false)
|
||||||
const identifierValueRef = useRef<string>(initialHandle || '')
|
const identifierValueRef = useRef<string>(initialHandle || '')
|
||||||
|
@ -83,12 +82,18 @@ export const LoginForm = ({
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||||
setError('')
|
setError('')
|
||||||
setIsProcessing(true)
|
|
||||||
|
|
||||||
const identifier = identifierValueRef.current.toLowerCase().trim()
|
const identifier = identifierValueRef.current.toLowerCase().trim()
|
||||||
const password = passwordValueRef.current
|
const password = passwordValueRef.current
|
||||||
const authFactorToken = authFactorTokenValueRef.current
|
const authFactorToken = authFactorTokenValueRef.current
|
||||||
|
|
||||||
|
if (!identifier || !password) {
|
||||||
|
setError(_(msg`Invalid username or password`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// try to guess the handle if the user just gave their own username
|
// try to guess the handle if the user just gave their own username
|
||||||
let fullIdent = identifier
|
let fullIdent = identifier
|
||||||
|
@ -157,22 +162,6 @@ export const LoginForm = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkIsReady = () => {
|
|
||||||
if (
|
|
||||||
!!serviceDescription &&
|
|
||||||
!!identifierValueRef.current &&
|
|
||||||
!!passwordValueRef.current
|
|
||||||
) {
|
|
||||||
if (!isReady) {
|
|
||||||
setIsReady(true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isReady) {
|
|
||||||
setIsReady(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
|
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
|
||||||
<View>
|
<View>
|
||||||
|
@ -204,7 +193,6 @@ export const LoginForm = ({
|
||||||
defaultValue={initialHandle || ''}
|
defaultValue={initialHandle || ''}
|
||||||
onChangeText={v => {
|
onChangeText={v => {
|
||||||
identifierValueRef.current = v
|
identifierValueRef.current = v
|
||||||
checkIsReady()
|
|
||||||
}}
|
}}
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
passwordRef.current?.focus()
|
passwordRef.current?.focus()
|
||||||
|
@ -233,7 +221,6 @@ export const LoginForm = ({
|
||||||
clearButtonMode="while-editing"
|
clearButtonMode="while-editing"
|
||||||
onChangeText={v => {
|
onChangeText={v => {
|
||||||
passwordValueRef.current = v
|
passwordValueRef.current = v
|
||||||
checkIsReady()
|
|
||||||
}}
|
}}
|
||||||
onSubmitEditing={onPressNext}
|
onSubmitEditing={onPressNext}
|
||||||
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
||||||
|
@ -325,7 +312,7 @@ export const LoginForm = ({
|
||||||
<Trans>Connecting...</Trans>
|
<Trans>Connecting...</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : isReady ? (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
testID="loginNextButton"
|
testID="loginNextButton"
|
||||||
label={_(msg`Next`)}
|
label={_(msg`Next`)}
|
||||||
|
@ -339,7 +326,7 @@ export const LoginForm = ({
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
{isProcessing && <ButtonIcon icon={Loader} />}
|
{isProcessing && <ButtonIcon icon={Loader} />}
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,17 +14,21 @@ import {s} from '#/lib/styles'
|
||||||
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
||||||
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
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 * 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 {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
|
||||||
import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
|
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'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
|
||||||
export function AppearanceSettingsScreen({}: Props) {
|
export function AppearanceSettingsScreen({}: Props) {
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const {isTabletOrMobile} = useWebMediaQueries()
|
const {isTabletOrMobile} = useWebMediaQueries()
|
||||||
|
const {fonts} = useAlf()
|
||||||
|
|
||||||
const {colorMode, darkTheme} = useThemePrefs()
|
const {colorMode, darkTheme} = useThemePrefs()
|
||||||
const {setColorMode, setDarkTheme} = useSetThemePrefs()
|
const {setColorMode, setDarkTheme} = useSetThemePrefs()
|
||||||
|
@ -54,6 +58,22 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||||
[setDarkTheme, darkTheme],
|
[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 (
|
return (
|
||||||
<LayoutAnimationConfig skipExiting skipEntering>
|
<LayoutAnimationConfig skipExiting skipEntering>
|
||||||
<View testID="preferencesThreadsScreen" style={s.hContentRegion}>
|
<View testID="preferencesThreadsScreen" style={s.hContentRegion}>
|
||||||
|
@ -71,65 +91,143 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||||
</View>
|
</View>
|
||||||
</SimpleViewHeader>
|
</SimpleViewHeader>
|
||||||
|
|
||||||
<View style={[a.p_xl, a.gap_lg]}>
|
<View style={[a.gap_3xl, a.pt_xl, a.px_xl]}>
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
<View style={[a.gap_lg]}>
|
||||||
<PhoneIcon style={t.atoms.text} />
|
<AppearanceToggleButtonGroup
|
||||||
<Text style={a.text_md}>
|
title={_(msg`Color mode`)}
|
||||||
<Trans>Mode</Trans>
|
icon={PhoneIcon}
|
||||||
</Text>
|
items={[
|
||||||
</View>
|
{
|
||||||
<ToggleButton.Group
|
label: _(msg`System`),
|
||||||
label={_(msg`Dark mode`)}
|
name: 'system',
|
||||||
values={[colorMode]}
|
},
|
||||||
onChange={onChangeAppearance}>
|
{
|
||||||
<ToggleButton.Button label={_(msg`System`)} name="system">
|
label: _(msg`Light`),
|
||||||
<ToggleButton.ButtonText>
|
name: 'light',
|
||||||
<Trans>System</Trans>
|
},
|
||||||
</ToggleButton.ButtonText>
|
{
|
||||||
</ToggleButton.Button>
|
label: _(msg`Dark`),
|
||||||
<ToggleButton.Button label={_(msg`Light`)} name="light">
|
name: 'dark',
|
||||||
<ToggleButton.ButtonText>
|
},
|
||||||
<Trans>Light</Trans>
|
]}
|
||||||
</ToggleButton.ButtonText>
|
values={[colorMode]}
|
||||||
</ToggleButton.Button>
|
onChange={onChangeAppearance}
|
||||||
<ToggleButton.Button label={_(msg`Dark`)} name="dark">
|
/>
|
||||||
<ToggleButton.ButtonText>
|
|
||||||
<Trans>Dark</Trans>
|
|
||||||
</ToggleButton.ButtonText>
|
|
||||||
</ToggleButton.Button>
|
|
||||||
</ToggleButton.Group>
|
|
||||||
{colorMode !== 'light' && (
|
|
||||||
<Animated.View
|
|
||||||
entering={native(FadeInDown)}
|
|
||||||
exiting={native(FadeOutDown)}
|
|
||||||
style={[a.mt_md, a.gap_lg]}>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
|
||||||
<MoonIcon style={t.atoms.text} />
|
|
||||||
<Text style={a.text_md}>
|
|
||||||
<Trans>Dark theme</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ToggleButton.Group
|
{colorMode !== 'light' && (
|
||||||
label={_(msg`Dark theme`)}
|
<Animated.View
|
||||||
values={[darkTheme ?? 'dim']}
|
entering={native(FadeInDown)}
|
||||||
onChange={onChangeDarkTheme}>
|
exiting={native(FadeOutDown)}>
|
||||||
<ToggleButton.Button label={_(msg`Dim`)} name="dim">
|
<AppearanceToggleButtonGroup
|
||||||
<ToggleButton.ButtonText>
|
title={_(msg`Dark theme`)}
|
||||||
<Trans>Dim</Trans>
|
icon={MoonIcon}
|
||||||
</ToggleButton.ButtonText>
|
items={[
|
||||||
</ToggleButton.Button>
|
{
|
||||||
<ToggleButton.Button label={_(msg`Dark`)} name="dark">
|
label: _(msg`Dim`),
|
||||||
<ToggleButton.ButtonText>
|
name: 'dim',
|
||||||
<Trans>Dark</Trans>
|
},
|
||||||
</ToggleButton.ButtonText>
|
{
|
||||||
</ToggleButton.Button>
|
label: _(msg`Dark`),
|
||||||
</ToggleButton.Group>
|
name: 'dark',
|
||||||
</Animated.View>
|
},
|
||||||
)}
|
]}
|
||||||
|
values={[darkTheme ?? 'dim']}
|
||||||
|
onChange={onChangeDarkTheme}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AppearanceToggleButtonGroup
|
||||||
|
title={_(msg`Font`)}
|
||||||
|
description={_(
|
||||||
|
msg`For the best experience, we recommend using the theme font.`,
|
||||||
|
)}
|
||||||
|
icon={Aa}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: _(msg`System`),
|
||||||
|
name: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: _(msg`Theme`),
|
||||||
|
name: 'theme',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
values={[fonts.family]}
|
||||||
|
onChange={onChangeFontFamily}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AppearanceToggleButtonGroup
|
||||||
|
title={_(msg`Font size`)}
|
||||||
|
icon={TextSize}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: _(msg`Smaller`),
|
||||||
|
name: '-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: _(msg`Default`),
|
||||||
|
name: '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: _(msg`Larger`),
|
||||||
|
name: '1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
values={[fonts.scale]}
|
||||||
|
onChange={onChangeFontScale}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</LayoutAnimationConfig>
|
</LayoutAnimationConfig>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AppearanceToggleButtonGroup({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon: Icon,
|
||||||
|
items,
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
icon: React.ComponentType<SVGIconProps>
|
||||||
|
items: {
|
||||||
|
label: string
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
values: string[]
|
||||||
|
onChange: (values: string[]) => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
<View style={[a.gap_xs]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||||
|
<Icon style={t.atoms.text} />
|
||||||
|
<Text style={[a.text_md, a.font_bold]}>{title}</Text>
|
||||||
|
</View>
|
||||||
|
{description && (
|
||||||
|
<Text
|
||||||
|
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<ToggleButton.Group label={title} values={values} onChange={onChange}>
|
||||||
|
{items.map(item => (
|
||||||
|
<ToggleButton.Button
|
||||||
|
key={item.name}
|
||||||
|
label={item.label}
|
||||||
|
name={item.name}>
|
||||||
|
<ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
))}
|
||||||
|
</ToggleButton.Group>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {ImageModel} from './image'
|
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {openPicker} from 'lib/media/picker'
|
|
||||||
import {getImageDim} from 'lib/media/manip'
|
import {getImageDim} from 'lib/media/manip'
|
||||||
|
import {openPicker} from 'lib/media/picker'
|
||||||
|
import {ImageInitOptions, ImageModel} from './image'
|
||||||
|
|
||||||
interface InitialImageUri {
|
interface InitialImageUri {
|
||||||
uri: string
|
uri: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
altText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GalleryModel {
|
export class GalleryModel {
|
||||||
images: ImageModel[] = []
|
images: ImageModel[] = []
|
||||||
|
|
||||||
constructor(uris?: {uri: string; width: number; height: number}[]) {
|
constructor(uris?: InitialImageUri[]) {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
|
||||||
if (uris) {
|
if (uris) {
|
||||||
|
@ -33,7 +34,7 @@ export class GalleryModel {
|
||||||
return this.images.some(image => image.altText.trim() === '')
|
return this.images.some(image => image.altText.trim() === '')
|
||||||
}
|
}
|
||||||
|
|
||||||
*add(image_: Omit<RNImage, 'size'>) {
|
*add(image_: ImageInitOptions) {
|
||||||
if (this.size >= 4) {
|
if (this.size >= 4) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -59,7 +60,6 @@ export class GalleryModel {
|
||||||
path: uri,
|
path: uri,
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
mime: 'image/jpeg',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
@ -100,10 +100,10 @@ export class GalleryModel {
|
||||||
async addFromUris(uris: InitialImageUri[]) {
|
async addFromUris(uris: InitialImageUri[]) {
|
||||||
for (const uriObj of uris) {
|
for (const uriObj of uris) {
|
||||||
this.add({
|
this.add({
|
||||||
mime: 'image/jpeg',
|
|
||||||
height: uriObj.height,
|
height: uriObj.height,
|
||||||
width: uriObj.width,
|
width: uriObj.width,
|
||||||
path: uriObj.uri,
|
path: uriObj.uri,
|
||||||
|
altText: uriObj.altText,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
|
||||||
import {POST_IMG_MAX} from 'lib/constants'
|
|
||||||
import * as ImageManipulator from 'expo-image-manipulator'
|
import * as ImageManipulator from 'expo-image-manipulator'
|
||||||
import {getDataUriSize} from 'lib/media/util'
|
|
||||||
import {openCropper} from 'lib/media/picker'
|
|
||||||
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
|
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
|
||||||
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {Position} from 'react-avatar-editor'
|
import {Position} from 'react-avatar-editor'
|
||||||
import {Dimensions} from 'lib/media/types'
|
|
||||||
import {isIOS} from 'platform/detection'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {POST_IMG_MAX} from 'lib/constants'
|
||||||
|
import {openCropper} from 'lib/media/picker'
|
||||||
|
import {Dimensions} from 'lib/media/types'
|
||||||
|
import {getDataUriSize} from 'lib/media/util'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
|
|
||||||
export interface ImageManipulationAttributes {
|
export interface ImageManipulationAttributes {
|
||||||
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
||||||
|
@ -19,6 +20,13 @@ export interface ImageManipulationAttributes {
|
||||||
flipVertical?: boolean
|
flipVertical?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageInitOptions {
|
||||||
|
path: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
altText?: string
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_IMAGE_SIZE_IN_BYTES = 976560
|
const MAX_IMAGE_SIZE_IN_BYTES = 976560
|
||||||
|
|
||||||
export class ImageModel implements Omit<RNImage, 'size'> {
|
export class ImageModel implements Omit<RNImage, 'size'> {
|
||||||
|
@ -41,12 +49,15 @@ export class ImageModel implements Omit<RNImage, 'size'> {
|
||||||
}
|
}
|
||||||
prevAttributes: ImageManipulationAttributes = {}
|
prevAttributes: ImageManipulationAttributes = {}
|
||||||
|
|
||||||
constructor(image: Omit<RNImage, 'size'>) {
|
constructor(image: ImageInitOptions) {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
|
||||||
this.path = image.path
|
this.path = image.path
|
||||||
this.width = image.width
|
this.width = image.width
|
||||||
this.height = image.height
|
this.height = image.height
|
||||||
|
if (image.altText !== undefined) {
|
||||||
|
this.setAltText(image.altText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
|
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
|
||||||
|
|
|
@ -21,31 +21,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
||||||
if (feedDesc.startsWith('feedgen')) {
|
if (feedDesc.startsWith('feedgen')) {
|
||||||
return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)]
|
return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)]
|
||||||
}
|
}
|
||||||
if (feedDesc.startsWith('list')) {
|
if (feedDesc === 'following' || feedDesc.startsWith('list')) {
|
||||||
let feedTuners = []
|
|
||||||
if (feedDesc.endsWith('|as_following')) {
|
|
||||||
// Same as Following tuners below, copypaste for now.
|
|
||||||
feedTuners.push(FeedTuner.removeOrphans)
|
|
||||||
if (preferences?.feedViewPrefs.hideReposts) {
|
|
||||||
feedTuners.push(FeedTuner.removeReposts)
|
|
||||||
}
|
|
||||||
if (preferences?.feedViewPrefs.hideReplies) {
|
|
||||||
feedTuners.push(FeedTuner.removeReplies)
|
|
||||||
} else {
|
|
||||||
feedTuners.push(
|
|
||||||
FeedTuner.followedRepliesOnly({
|
|
||||||
userDid: currentAccount?.did || '',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (preferences?.feedViewPrefs.hideQuotePosts) {
|
|
||||||
feedTuners.push(FeedTuner.removeQuotePosts)
|
|
||||||
}
|
|
||||||
feedTuners.push(FeedTuner.dedupThreads)
|
|
||||||
}
|
|
||||||
return feedTuners
|
|
||||||
}
|
|
||||||
if (feedDesc === 'following') {
|
|
||||||
const feedTuners = [FeedTuner.removeOrphans]
|
const feedTuners = [FeedTuner.removeOrphans]
|
||||||
|
|
||||||
if (preferences?.feedViewPrefs.hideReposts) {
|
if (preferences?.feedViewPrefs.hideReposts) {
|
||||||
|
|
|
@ -175,19 +175,9 @@ async function fetchSubjects(
|
||||||
}> {
|
}> {
|
||||||
const postUris = new Set<string>()
|
const postUris = new Set<string>()
|
||||||
const packUris = new Set<string>()
|
const packUris = new Set<string>()
|
||||||
|
|
||||||
const postUrisWithLikes = new Set<string>()
|
|
||||||
const postUrisWithReposts = new Set<string>()
|
|
||||||
|
|
||||||
for (const notif of groupedNotifs) {
|
for (const notif of groupedNotifs) {
|
||||||
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
|
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
|
||||||
postUris.add(notif.subjectUri)
|
postUris.add(notif.subjectUri)
|
||||||
if (notif.type === 'post-like') {
|
|
||||||
postUrisWithLikes.add(notif.subjectUri)
|
|
||||||
}
|
|
||||||
if (notif.type === 'repost') {
|
|
||||||
postUrisWithReposts.add(notif.subjectUri)
|
|
||||||
}
|
|
||||||
} else if (
|
} else if (
|
||||||
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
|
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
|
||||||
) {
|
) {
|
||||||
|
@ -216,15 +206,6 @@ async function fetchSubjects(
|
||||||
AppBskyFeedPost.validateRecord(post.record).success
|
AppBskyFeedPost.validateRecord(post.record).success
|
||||||
) {
|
) {
|
||||||
postsMap.set(post.uri, post)
|
postsMap.set(post.uri, post)
|
||||||
|
|
||||||
// HACK. In some cases, the appview appears to lag behind and returns empty counters.
|
|
||||||
// To prevent scroll jump due to missing metrics, fill in 1 like/repost instead of 0.
|
|
||||||
if (post.likeCount === 0 && postUrisWithLikes.has(post.uri)) {
|
|
||||||
post.likeCount = 1
|
|
||||||
}
|
|
||||||
if (post.repostCount === 0 && postUrisWithReposts.has(post.uri)) {
|
|
||||||
post.repostCount = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const pack of packsChunks.flat()) {
|
for (const pack of packsChunks.flat()) {
|
||||||
|
|
|
@ -3,27 +3,23 @@ import zod from 'zod'
|
||||||
import {BaseNux} from '#/state/queries/nuxs/types'
|
import {BaseNux} from '#/state/queries/nuxs/types'
|
||||||
|
|
||||||
export enum Nux {
|
export enum Nux {
|
||||||
One = 'one',
|
TenMillionDialog = 'TenMillionDialog',
|
||||||
Two = 'two',
|
NeueTypography = 'NeueTypography',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nuxNames = new Set(Object.values(Nux))
|
export const nuxNames = new Set(Object.values(Nux))
|
||||||
|
|
||||||
export type AppNux =
|
export type AppNux =
|
||||||
| BaseNux<{
|
| BaseNux<{
|
||||||
id: Nux.One
|
id: Nux.TenMillionDialog
|
||||||
data: {
|
data: undefined
|
||||||
likes: number
|
|
||||||
}
|
|
||||||
}>
|
}>
|
||||||
| BaseNux<{
|
| BaseNux<{
|
||||||
id: Nux.Two
|
id: Nux.NeueTypography
|
||||||
data: undefined
|
data: undefined
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export const NuxSchemas = {
|
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
||||||
[Nux.One]: zod.object({
|
[Nux.TenMillionDialog]: undefined,
|
||||||
likes: zod.number(),
|
[Nux.NeueTypography]: undefined,
|
||||||
}),
|
|
||||||
[Nux.Two]: undefined,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ export function useUpsertNuxMutation() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
retry: 3,
|
||||||
mutationFn: async (nux: AppNux) => {
|
mutationFn: async (nux: AppNux) => {
|
||||||
await agent.bskyAppUpsertNux(serializeAppNux(nux))
|
await agent.bskyAppUpsertNux(serializeAppNux(nux))
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
|
@ -72,6 +73,7 @@ export function useRemoveNuxsMutation() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
retry: 3,
|
||||||
mutationFn: async (ids: string[]) => {
|
mutationFn: async (ids: string[]) => {
|
||||||
await agent.bskyAppRemoveNuxs(ids)
|
await agent.bskyAppRemoveNuxs(ids)
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
|
|
|
@ -4,6 +4,4 @@ export type Data = Record<string, unknown> | undefined
|
||||||
|
|
||||||
export type BaseNux<
|
export type BaseNux<
|
||||||
T extends Pick<AppBskyActorDefs.Nux, 'id' | 'expiresAt'> & {data: Data},
|
T extends Pick<AppBskyActorDefs.Nux, 'id' | 'expiresAt'> & {data: Data},
|
||||||
> = T & {
|
> = Pick<AppBskyActorDefs.Nux, 'id' | 'completed' | 'expiresAt'> & T
|
||||||
completed: boolean
|
|
||||||
}
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue