Compare commits
	
		
			No commits in common. "fa997fa7c163f39ca9cc09af99b0b2562df3cc3b" and "026f23ae2a0557b24664666f600f83bfa503d878" have entirely different histories.
		
	
	
		
			fa997fa7c1
			...
			026f23ae2a
		
	
		
					 142 changed files with 8550 additions and 11155 deletions
				
			
		| 
						 | 
					@ -74,7 +74,8 @@ appId: xyz.blueskyweb.app
 | 
				
			||||||
- tapOn: "Delete List"
 | 
					- tapOn: "Delete List"
 | 
				
			||||||
- tapOn:
 | 
					- tapOn:
 | 
				
			||||||
    id: "confirmBtn"
 | 
					    id: "confirmBtn"
 | 
				
			||||||
- assertVisible: "This list is empty!"
 | 
					- assertVisible:
 | 
				
			||||||
 | 
					    id: "listsEmpty"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- tapOn:
 | 
					- tapOn:
 | 
				
			||||||
    label: "Create a new curatelist"
 | 
					    label: "Create a new curatelist"
 | 
				
			||||||
| 
						 | 
					@ -160,6 +161,17 @@ 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,12 +21,14 @@ appId: xyz.blueskyweb.app
 | 
				
			||||||
    id: "likeBtn"
 | 
					    id: "likeBtn"
 | 
				
			||||||
    childOf:
 | 
					    childOf:
 | 
				
			||||||
        id: "postThreadItem-by-bob.test"
 | 
					        id: "postThreadItem-by-bob.test"
 | 
				
			||||||
- assertVisible: "1 like"
 | 
					- assertVisible:
 | 
				
			||||||
 | 
					    id: "likeCount-expanded"
 | 
				
			||||||
- tapOn:
 | 
					- tapOn:
 | 
				
			||||||
    id: "likeBtn"
 | 
					    id: "likeBtn"
 | 
				
			||||||
    childOf:
 | 
					    childOf:
 | 
				
			||||||
        id: "postThreadItem-by-bob.test"
 | 
					        id: "postThreadItem-by-bob.test"
 | 
				
			||||||
- assertNotVisible: "1 like"
 | 
					- assertNotVisible:
 | 
				
			||||||
 | 
					    id: "likeCount-expanded"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Can like a reply post
 | 
					# Can like a reply post
 | 
				
			||||||
- tapOn:
 | 
					- tapOn:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,6 @@ 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' : ''
 | 
				
			||||||
  }`
 | 
					  }`
 | 
				
			||||||
| 
						 | 
					@ -187,15 +186,7 @@ module.exports = function (config) {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      plugins: [
 | 
					      plugins: [
 | 
				
			||||||
        'expo-localization',
 | 
					        'expo-localization',
 | 
				
			||||||
        USE_SENTRY && [
 | 
					        Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
 | 
				
			||||||
          '@sentry/react-native/expo',
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            organization: 'blueskyweb',
 | 
					 | 
				
			||||||
            project: 'react-native',
 | 
					 | 
				
			||||||
            release: VERSION,
 | 
					 | 
				
			||||||
            dist: SENTRY_DIST,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
          'expo-build-properties',
 | 
					          'expo-build-properties',
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
| 
						 | 
					@ -220,6 +211,7 @@ 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',
 | 
				
			||||||
| 
						 | 
					@ -230,31 +222,6 @@ 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: {
 | 
				
			||||||
| 
						 | 
					@ -297,7 +264,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: './postHooks/uploadSentrySourcemapsPostHook',
 | 
					            file: 'sentry-expo/upload-sourcemaps',
 | 
				
			||||||
            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.
										
									
								
							| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 378 B  | 
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 317 B  | 
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 986 B  | 
| 
						 | 
					@ -258,51 +258,6 @@
 | 
				
			||||||
    .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">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,36 +0,0 @@
 | 
				
			||||||
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
 | 
					 | 
				
			||||||
index 7e0b4cd..177454c 100644
 | 
					 | 
				
			||||||
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
 | 
					 | 
				
			||||||
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
 | 
					 | 
				
			||||||
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
 | 
					 | 
				
			||||||
  * This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
 | 
					 | 
				
			||||||
  */
 | 
					 | 
				
			||||||
 export function ignoreRequireCycleLogs() {
 | 
					 | 
				
			||||||
-    LogBox.ignoreLogs(['Require cycle:']);
 | 
					 | 
				
			||||||
+    try {
 | 
					 | 
				
			||||||
+        LogBox.ignoreLogs(['Require cycle:']);
 | 
					 | 
				
			||||||
+    } catch (e) {}
 | 
					 | 
				
			||||||
 }
 | 
					 | 
				
			||||||
 //# sourceMappingURL=ignorerequirecyclelogs.js.map
 | 
					 | 
				
			||||||
\ No newline at end of file
 | 
					 | 
				
			||||||
diff --git a/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,
 | 
					 | 
				
			||||||
							
								
								
									
										15
									
								
								patches/@sentry+react-native+5.5.0.patch
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								patches/@sentry+react-native+5.5.0.patch
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					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,3 +12,28 @@ 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()
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,34 +0,0 @@
 | 
				
			||||||
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 VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
 | 
					import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext'
 | 
				
			||||||
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, useFonts} from '#/alf'
 | 
					import {ThemeProvider as Alf} 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,15 +106,16 @@ function InnerApp() {
 | 
				
			||||||
  }, [_])
 | 
					  }, [_])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <StatsigProvider
 | 
					 | 
				
			||||||
      // Resets the entire tree below when it changes:
 | 
					 | 
				
			||||||
      key={currentAccount?.did}>
 | 
					 | 
				
			||||||
    <Alf theme={theme}>
 | 
					    <Alf theme={theme}>
 | 
				
			||||||
      <ThemeProvider theme={theme}>
 | 
					      <ThemeProvider theme={theme}>
 | 
				
			||||||
        <Splash isReady={isReady && hasCheckedReferrer}>
 | 
					        <Splash isReady={isReady && hasCheckedReferrer}>
 | 
				
			||||||
 | 
					          <ActiveVideoProvider>
 | 
				
			||||||
            <RootSiblingParent>
 | 
					            <RootSiblingParent>
 | 
				
			||||||
              <VideoVolumeProvider>
 | 
					              <React.Fragment
 | 
				
			||||||
 | 
					                // 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>
 | 
				
			||||||
| 
						 | 
					@ -126,10 +127,10 @@ function InnerApp() {
 | 
				
			||||||
                                  <BackgroundNotificationPreferencesProvider>
 | 
					                                  <BackgroundNotificationPreferencesProvider>
 | 
				
			||||||
                                    <MutedThreadsProvider>
 | 
					                                    <MutedThreadsProvider>
 | 
				
			||||||
                                      <ProgressGuideProvider>
 | 
					                                      <ProgressGuideProvider>
 | 
				
			||||||
                                      <GestureHandlerRootView style={s.h100pct}>
 | 
					                                        <GestureHandlerRootView
 | 
				
			||||||
 | 
					                                          style={s.h100pct}>
 | 
				
			||||||
                                          <TestCtrls />
 | 
					                                          <TestCtrls />
 | 
				
			||||||
                                          <Shell />
 | 
					                                          <Shell />
 | 
				
			||||||
                                        <NuxDialogs />
 | 
					 | 
				
			||||||
                                        </GestureHandlerRootView>
 | 
					                                        </GestureHandlerRootView>
 | 
				
			||||||
                                      </ProgressGuideProvider>
 | 
					                                      </ProgressGuideProvider>
 | 
				
			||||||
                                    </MutedThreadsProvider>
 | 
					                                    </MutedThreadsProvider>
 | 
				
			||||||
| 
						 | 
					@ -141,25 +142,27 @@ function InnerApp() {
 | 
				
			||||||
                        </ModerationOptsProvider>
 | 
					                        </ModerationOptsProvider>
 | 
				
			||||||
                      </LabelDefsProvider>
 | 
					                      </LabelDefsProvider>
 | 
				
			||||||
                    </MessagesProvider>
 | 
					                    </MessagesProvider>
 | 
				
			||||||
 | 
					                  </StatsigProvider>
 | 
				
			||||||
                </QueryProvider>
 | 
					                </QueryProvider>
 | 
				
			||||||
              </VideoVolumeProvider>
 | 
					              </React.Fragment>
 | 
				
			||||||
            </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 || !loaded) {
 | 
					  if (!isReady) {
 | 
				
			||||||
    return null
 | 
					    return null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,20 +35,17 @@ 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, useFonts} from '#/alf'
 | 
					import {ThemeProvider as Alf} 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'
 | 
				
			||||||
| 
						 | 
					@ -63,8 +60,6 @@ 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) {
 | 
				
			||||||
| 
						 | 
					@ -96,15 +91,15 @@ function InnerApp() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <KeyboardProvider enabled={false}>
 | 
					    <KeyboardProvider enabled={false}>
 | 
				
			||||||
      <StatsigProvider
 | 
					 | 
				
			||||||
        // Resets the entire tree below when it changes:
 | 
					 | 
				
			||||||
        key={currentAccount?.did}>
 | 
					 | 
				
			||||||
      <Alf theme={theme}>
 | 
					      <Alf theme={theme}>
 | 
				
			||||||
        <ThemeProvider theme={theme}>
 | 
					        <ThemeProvider theme={theme}>
 | 
				
			||||||
          <RootSiblingParent>
 | 
					          <RootSiblingParent>
 | 
				
			||||||
              <VideoVolumeProvider>
 | 
					 | 
				
			||||||
            <ActiveVideoProvider>
 | 
					            <ActiveVideoProvider>
 | 
				
			||||||
 | 
					              <React.Fragment
 | 
				
			||||||
 | 
					                // 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>
 | 
				
			||||||
| 
						 | 
					@ -118,7 +113,6 @@ function InnerApp() {
 | 
				
			||||||
                                      <SafeAreaProvider>
 | 
					                                      <SafeAreaProvider>
 | 
				
			||||||
                                        <ProgressGuideProvider>
 | 
					                                        <ProgressGuideProvider>
 | 
				
			||||||
                                          <Shell />
 | 
					                                          <Shell />
 | 
				
			||||||
                                          <NuxDialogs />
 | 
					 | 
				
			||||||
                                        </ProgressGuideProvider>
 | 
					                                        </ProgressGuideProvider>
 | 
				
			||||||
                                      </SafeAreaProvider>
 | 
					                                      </SafeAreaProvider>
 | 
				
			||||||
                                    </MutedThreadsProvider>
 | 
					                                    </MutedThreadsProvider>
 | 
				
			||||||
| 
						 | 
					@ -130,27 +124,26 @@ function InnerApp() {
 | 
				
			||||||
                        </ModerationOptsProvider>
 | 
					                        </ModerationOptsProvider>
 | 
				
			||||||
                      </LabelDefsProvider>
 | 
					                      </LabelDefsProvider>
 | 
				
			||||||
                    </MessagesProvider>
 | 
					                    </MessagesProvider>
 | 
				
			||||||
 | 
					                  </StatsigProvider>
 | 
				
			||||||
                </QueryProvider>
 | 
					                </QueryProvider>
 | 
				
			||||||
 | 
					              </React.Fragment>
 | 
				
			||||||
              <ToastContainer />
 | 
					              <ToastContainer />
 | 
				
			||||||
            </ActiveVideoProvider>
 | 
					            </ActiveVideoProvider>
 | 
				
			||||||
              </VideoVolumeProvider>
 | 
					 | 
				
			||||||
          </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 || (!loaded && !error)) {
 | 
					  if (!isReady) {
 | 
				
			||||||
    return null
 | 
					    return null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -225,43 +225,43 @@ export const atoms = {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_2xs: {
 | 
					  text_2xs: {
 | 
				
			||||||
    fontSize: tokens.fontSize._2xs,
 | 
					    fontSize: tokens.fontSize._2xs,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_xs: {
 | 
					  text_xs: {
 | 
				
			||||||
    fontSize: tokens.fontSize.xs,
 | 
					    fontSize: tokens.fontSize.xs,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_sm: {
 | 
					  text_sm: {
 | 
				
			||||||
    fontSize: tokens.fontSize.sm,
 | 
					    fontSize: tokens.fontSize.sm,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_md: {
 | 
					  text_md: {
 | 
				
			||||||
    fontSize: tokens.fontSize.md,
 | 
					    fontSize: tokens.fontSize.md,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_lg: {
 | 
					  text_lg: {
 | 
				
			||||||
    fontSize: tokens.fontSize.lg,
 | 
					    fontSize: tokens.fontSize.lg,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_xl: {
 | 
					  text_xl: {
 | 
				
			||||||
    fontSize: tokens.fontSize.xl,
 | 
					    fontSize: tokens.fontSize.xl,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_2xl: {
 | 
					  text_2xl: {
 | 
				
			||||||
    fontSize: tokens.fontSize._2xl,
 | 
					    fontSize: tokens.fontSize._2xl,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_3xl: {
 | 
					  text_3xl: {
 | 
				
			||||||
    fontSize: tokens.fontSize._3xl,
 | 
					    fontSize: tokens.fontSize._3xl,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_4xl: {
 | 
					  text_4xl: {
 | 
				
			||||||
    fontSize: tokens.fontSize._4xl,
 | 
					    fontSize: tokens.fontSize._4xl,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  text_5xl: {
 | 
					  text_5xl: {
 | 
				
			||||||
    fontSize: tokens.fontSize._5xl,
 | 
					    fontSize: tokens.fontSize._5xl,
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  leading_tight: {
 | 
					  leading_tight: {
 | 
				
			||||||
    lineHeight: 1.15,
 | 
					    lineHeight: 1.15,
 | 
				
			||||||
| 
						 | 
					@ -273,7 +273,10 @@ export const atoms = {
 | 
				
			||||||
    lineHeight: 1.5,
 | 
					    lineHeight: 1.5,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  tracking_normal: {
 | 
					  tracking_normal: {
 | 
				
			||||||
    letterSpacing: tokens.TRACKING,
 | 
					    letterSpacing: 0,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  tracking_wide: {
 | 
				
			||||||
 | 
					    letterSpacing: 0.25,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  font_normal: {
 | 
					  font_normal: {
 | 
				
			||||||
    fontWeight: tokens.fontWeight.normal,
 | 
					    fontWeight: tokens.fontWeight.normal,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										111
									
								
								src/alf/fonts.ts
									
										
									
									
									
								
							
							
						
						
									
										111
									
								
								src/alf/fonts.ts
									
										
									
									
									
								
							| 
						 | 
					@ -1,111 +0,0 @@
 | 
				
			||||||
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,98 +1,32 @@
 | 
				
			||||||
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<Alf>({
 | 
					export const Context = React.createContext<{
 | 
				
			||||||
 | 
					  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: {
 | 
				
			||||||
| 
						 | 
					@ -102,47 +36,24 @@ export function ThemeProvider({
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					  const theme = themes[themeName]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Context.Provider
 | 
					    <Context.Provider
 | 
				
			||||||
      value={React.useMemo<Alf>(
 | 
					      value={React.useMemo(
 | 
				
			||||||
        () => ({
 | 
					        () => ({
 | 
				
			||||||
          themes,
 | 
					 | 
				
			||||||
          themeName: themeName,
 | 
					          themeName: themeName,
 | 
				
			||||||
          theme: themes[themeName],
 | 
					          theme: theme,
 | 
				
			||||||
          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 useAlf() {
 | 
					export function useTheme() {
 | 
				
			||||||
  return React.useContext(Context)
 | 
					  return React.useContext(Context).theme
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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,7 +1,3 @@
 | 
				
			||||||
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,6 +7,7 @@ import {
 | 
				
			||||||
  PressableProps,
 | 
					  PressableProps,
 | 
				
			||||||
  StyleProp,
 | 
					  StyleProp,
 | 
				
			||||||
  StyleSheet,
 | 
					  StyleSheet,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
  TextProps,
 | 
					  TextProps,
 | 
				
			||||||
  TextStyle,
 | 
					  TextStyle,
 | 
				
			||||||
  View,
 | 
					  View,
 | 
				
			||||||
| 
						 | 
					@ -16,7 +17,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 {Text} from '#/components/Typography'
 | 
					import {normalizeTextStyles} from '#/components/Typography'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
 | 
					export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
 | 
				
			||||||
export type ButtonColor =
 | 
					export type ButtonColor =
 | 
				
			||||||
| 
						 | 
					@ -634,7 +635,14 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
 | 
				
			||||||
  const textStyles = useSharedButtonTextStyles()
 | 
					  const textStyles = useSharedButtonTextStyles()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
 | 
					    <Text
 | 
				
			||||||
 | 
					      {...rest}
 | 
				
			||||||
 | 
					      style={normalizeTextStyles([
 | 
				
			||||||
 | 
					        a.font_bold,
 | 
				
			||||||
 | 
					        a.text_center,
 | 
				
			||||||
 | 
					        textStyles,
 | 
				
			||||||
 | 
					        style,
 | 
				
			||||||
 | 
					      ])}>
 | 
				
			||||||
      {children}
 | 
					      {children}
 | 
				
			||||||
    </Text>
 | 
					    </Text>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,6 @@ 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -257,7 +256,7 @@ export const ScrollableInner = React.forwardRef<
 | 
				
			||||||
          borderTopLeftRadius: 40,
 | 
					          borderTopLeftRadius: 40,
 | 
				
			||||||
          borderTopRightRadius: 40,
 | 
					          borderTopRightRadius: 40,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        style,
 | 
					        flatten(style),
 | 
				
			||||||
      ]}
 | 
					      ]}
 | 
				
			||||||
      contentContainerStyle={a.pb_4xl}
 | 
					      contentContainerStyle={a.pb_4xl}
 | 
				
			||||||
      ref={ref}>
 | 
					      ref={ref}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,6 @@ 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()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
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])
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,11 +0,0 @@
 | 
				
			||||||
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,7 +75,6 @@ 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!`,
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,45 +0,0 @@
 | 
				
			||||||
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,7 +11,6 @@ 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'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +104,6 @@ export function ImageItem({
 | 
				
			||||||
        accessibilityHint={alt}
 | 
					        accessibilityHint={alt}
 | 
				
			||||||
        accessibilityLabel=""
 | 
					        accessibilityLabel=""
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <MediaInsetBorder style={[a.rounded_xs]} />
 | 
					 | 
				
			||||||
      {children}
 | 
					      {children}
 | 
				
			||||||
    </View>
 | 
					    </View>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,9 +59,7 @@ 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
 | 
					    <Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
 | 
				
			||||||
      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}`
 | 
					    const feed: FeedDescriptor = `list|${listUri}|as_following`
 | 
				
			||||||
    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 {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
 | 
					import {atoms, flatten, useTheme, web} from '#/alf'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type TextProps = RNTextProps & {
 | 
					export type TextProps = RNTextProps & {
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
| 
						 | 
					@ -34,30 +34,19 @@ 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(
 | 
					export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
 | 
				
			||||||
  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
 | 
				
			||||||
  s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
 | 
					  const fontSize = s.fontSize || atoms.text_md.fontSize
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (s?.lineHeight) {
 | 
					  if (s?.lineHeight) {
 | 
				
			||||||
    if (s.lineHeight !== 0 && s.lineHeight <= 2) {
 | 
					    if (s.lineHeight !== 0 && s.lineHeight <= 2) {
 | 
				
			||||||
      s.lineHeight = Math.round(s.fontSize * s.lineHeight)
 | 
					      s.lineHeight = Math.round(fontSize * s.lineHeight)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else if (!isNative) {
 | 
					  } else if (!isNative) {
 | 
				
			||||||
    s.lineHeight = s.fontSize
 | 
					    s.lineHeight = s.fontSize
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  applyFonts(s, fontFamily)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return s
 | 
					  return s
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,13 +54,8 @@ export function normalizeTextStyles(
 | 
				
			||||||
 * 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} />
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,119 +0,0 @@
 | 
				
			||||||
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>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,129 +0,0 @@
 | 
				
			||||||
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
											
										
									
								
							| 
						 | 
					@ -1,719 +0,0 @@
 | 
				
			||||||
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>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,183 +0,0 @@
 | 
				
			||||||
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>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,22 +0,0 @@
 | 
				
			||||||
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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +0,0 @@
 | 
				
			||||||
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',
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +0,0 @@
 | 
				
			||||||
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',
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +0,0 @@
 | 
				
			||||||
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 = 32}: {size?: number}) {
 | 
					export function PlayButtonIcon({size = 36}: {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 {logger} from '#/logger'
 | 
					import {useSession, SessionAccount} from '#/state/session'
 | 
				
			||||||
import {SessionAccount, useSession} from '#/state/session'
 | 
					 | 
				
			||||||
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
 | 
					import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
 | 
				
			||||||
 | 
					import {logger} from '#/logger'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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})
 | 
				
			||||||
      Sentry.setUser({id: did_hashed})
 | 
					      Native.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 {logger} from '#/logger'
 | 
					 | 
				
			||||||
import {SessionAccount, useSession} from '#/state/session'
 | 
					 | 
				
			||||||
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
 | 
					import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
 | 
				
			||||||
 | 
					import {useSession, SessionAccount} from '#/state/session'
 | 
				
			||||||
 | 
					import {logger} from '#/logger'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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})
 | 
				
			||||||
      Sentry.setUser({id: did_hashed})
 | 
					      Browser.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,7 +2,6 @@ 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'
 | 
				
			||||||
| 
						 | 
					@ -112,7 +111,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 ? jsonStringToLex(await res.text()) : null
 | 
					  let data = res.ok ? await res.json() : null
 | 
				
			||||||
  if (data?.feed?.length) {
 | 
					  if (data?.feed?.length) {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      success: true,
 | 
					      success: true,
 | 
				
			||||||
| 
						 | 
					@ -127,7 +126,7 @@ async function loggedOutFetch({
 | 
				
			||||||
    }&limit=${limit}`,
 | 
					    }&limit=${limit}`,
 | 
				
			||||||
    {method: 'GET', headers: {'Accept-Language': ''}},
 | 
					    {method: 'GET', headers: {'Accept-Language': ''}},
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
  data = res.ok ? jsonStringToLex(await res.text()) : null
 | 
					  data = res.ok ? await res.json() : null
 | 
				
			||||||
  if (data?.feed?.length) {
 | 
					  if (data?.feed?.length) {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      success: true,
 | 
					      success: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +0,0 @@
 | 
				
			||||||
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: 600,
 | 
					  duration: 400,
 | 
				
			||||||
  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])
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useComposeIntent() {
 | 
					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/react-native'
 | 
					import {init} from 'sentry-expo'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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,8 +225,4 @@ 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: '700'},
 | 
					  bold: {fontWeight: 'bold'},
 | 
				
			||||||
  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: '400'},
 | 
					  fw300: {fontWeight: '300'},
 | 
				
			||||||
  light: {fontWeight: '400'},
 | 
					  light: {fontWeight: '300'},
 | 
				
			||||||
  fw200: {fontWeight: '200'},
 | 
					  fw200: {fontWeight: '200'},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // text decoration
 | 
					  // text decoration
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
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'
 | 
				
			||||||
| 
						 | 
					@ -89,163 +88,163 @@ export const defaultTheme: Theme = {
 | 
				
			||||||
  typography: {
 | 
					  typography: {
 | 
				
			||||||
    '2xl-thin': {
 | 
					    '2xl-thin': {
 | 
				
			||||||
      fontSize: 18,
 | 
					      fontSize: 18,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '300',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    '2xl': {
 | 
					    '2xl': {
 | 
				
			||||||
      fontSize: 18,
 | 
					      fontSize: 18,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    '2xl-medium': {
 | 
					    '2xl-medium': {
 | 
				
			||||||
      fontSize: 18,
 | 
					      fontSize: 18,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    '2xl-bold': {
 | 
					    '2xl-bold': {
 | 
				
			||||||
      fontSize: 18,
 | 
					      fontSize: 18,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '700',
 | 
					      fontWeight: '700',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    '2xl-heavy': {
 | 
					    '2xl-heavy': {
 | 
				
			||||||
      fontSize: 18,
 | 
					      fontSize: 18,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '800',
 | 
					      fontWeight: '800',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xl-thin': {
 | 
					    'xl-thin': {
 | 
				
			||||||
      fontSize: 17,
 | 
					      fontSize: 17,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '300',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    xl: {
 | 
					    xl: {
 | 
				
			||||||
      fontSize: 17,
 | 
					      fontSize: 17,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xl-medium': {
 | 
					    'xl-medium': {
 | 
				
			||||||
      fontSize: 17,
 | 
					      fontSize: 17,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xl-bold': {
 | 
					    'xl-bold': {
 | 
				
			||||||
      fontSize: 17,
 | 
					      fontSize: 17,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '700',
 | 
					      fontWeight: '700',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xl-heavy': {
 | 
					    'xl-heavy': {
 | 
				
			||||||
      fontSize: 17,
 | 
					      fontSize: 17,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '800',
 | 
					      fontWeight: '800',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'lg-thin': {
 | 
					    'lg-thin': {
 | 
				
			||||||
      fontSize: 16,
 | 
					      fontSize: 16,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '300',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    lg: {
 | 
					    lg: {
 | 
				
			||||||
      fontSize: 16,
 | 
					      fontSize: 16,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'lg-medium': {
 | 
					    'lg-medium': {
 | 
				
			||||||
      fontSize: 16,
 | 
					      fontSize: 16,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'lg-bold': {
 | 
					    'lg-bold': {
 | 
				
			||||||
      fontSize: 16,
 | 
					      fontSize: 16,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '700',
 | 
					      fontWeight: '700',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'lg-heavy': {
 | 
					    'lg-heavy': {
 | 
				
			||||||
      fontSize: 16,
 | 
					      fontSize: 16,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '800',
 | 
					      fontWeight: '800',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'md-thin': {
 | 
					    'md-thin': {
 | 
				
			||||||
      fontSize: 15,
 | 
					      fontSize: 15,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '300',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    md: {
 | 
					    md: {
 | 
				
			||||||
      fontSize: 15,
 | 
					      fontSize: 15,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'md-medium': {
 | 
					    'md-medium': {
 | 
				
			||||||
      fontSize: 15,
 | 
					      fontSize: 15,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'md-bold': {
 | 
					    'md-bold': {
 | 
				
			||||||
      fontSize: 15,
 | 
					      fontSize: 15,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '700',
 | 
					      fontWeight: '700',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'md-heavy': {
 | 
					    'md-heavy': {
 | 
				
			||||||
      fontSize: 15,
 | 
					      fontSize: 15,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '800',
 | 
					      fontWeight: '800',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'sm-thin': {
 | 
					    'sm-thin': {
 | 
				
			||||||
      fontSize: 14,
 | 
					      fontSize: 14,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '300',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    sm: {
 | 
					    sm: {
 | 
				
			||||||
      fontSize: 14,
 | 
					      fontSize: 14,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'sm-medium': {
 | 
					    'sm-medium': {
 | 
				
			||||||
      fontSize: 14,
 | 
					      fontSize: 14,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'sm-bold': {
 | 
					    'sm-bold': {
 | 
				
			||||||
      fontSize: 14,
 | 
					      fontSize: 14,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '700',
 | 
					      fontWeight: '700',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'sm-heavy': {
 | 
					    'sm-heavy': {
 | 
				
			||||||
      fontSize: 14,
 | 
					      fontSize: 14,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '800',
 | 
					      fontWeight: '800',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xs-thin': {
 | 
					    'xs-thin': {
 | 
				
			||||||
      fontSize: 13,
 | 
					      fontSize: 13,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '300',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    xs: {
 | 
					    xs: {
 | 
				
			||||||
      fontSize: 13,
 | 
					      fontSize: 13,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xs-medium': {
 | 
					    'xs-medium': {
 | 
				
			||||||
      fontSize: 13,
 | 
					      fontSize: 13,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xs-bold': {
 | 
					    'xs-bold': {
 | 
				
			||||||
      fontSize: 13,
 | 
					      fontSize: 13,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '700',
 | 
					      fontWeight: '700',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'xs-heavy': {
 | 
					    'xs-heavy': {
 | 
				
			||||||
      fontSize: 13,
 | 
					      fontSize: 13,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '800',
 | 
					      fontWeight: '800',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    'title-2xl': {
 | 
					    'title-2xl': {
 | 
				
			||||||
      fontSize: 34,
 | 
					      fontSize: 34,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'title-xl': {
 | 
					    'title-xl': {
 | 
				
			||||||
      fontSize: 28,
 | 
					      fontSize: 28,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.25,
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'title-lg': {
 | 
					    'title-lg': {
 | 
				
			||||||
| 
						 | 
					@ -255,32 +254,32 @@ export const defaultTheme: Theme = {
 | 
				
			||||||
    title: {
 | 
					    title: {
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
      fontSize: 20,
 | 
					      fontSize: 20,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.15,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'title-sm': {
 | 
					    'title-sm': {
 | 
				
			||||||
      fontWeight: 'bold',
 | 
					      fontWeight: 'bold',
 | 
				
			||||||
      fontSize: 17,
 | 
					      fontSize: 17,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.15,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'post-text': {
 | 
					    'post-text': {
 | 
				
			||||||
      fontSize: 16,
 | 
					      fontSize: 16,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.2,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'post-text-lg': {
 | 
					    'post-text-lg': {
 | 
				
			||||||
      fontSize: 20,
 | 
					      fontSize: 20,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.2,
 | 
				
			||||||
      fontWeight: '400',
 | 
					      fontWeight: '400',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'button-lg': {
 | 
					    'button-lg': {
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
      fontSize: 18,
 | 
					      fontSize: 18,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.5,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    button: {
 | 
					    button: {
 | 
				
			||||||
      fontWeight: '500',
 | 
					      fontWeight: '500',
 | 
				
			||||||
      fontSize: 14,
 | 
					      fontSize: 14,
 | 
				
			||||||
      letterSpacing: tokens.TRACKING,
 | 
					      letterSpacing: 0.5,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    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,10 +16,12 @@ jest.mock('#/env', () => ({
 | 
				
			||||||
  LOG_DEBUG: '',
 | 
					  LOG_DEBUG: '',
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jest.mock('@sentry/react-native', () => ({
 | 
					jest.mock('sentry-expo', () => ({
 | 
				
			||||||
 | 
					  Native: {
 | 
				
			||||||
    addBreadcrumb: jest.fn(),
 | 
					    addBreadcrumb: jest.fn(),
 | 
				
			||||||
    captureException: jest.fn(),
 | 
					    captureException: jest.fn(),
 | 
				
			||||||
    captureMessage: jest.fn(),
 | 
					    captureMessage: jest.fn(),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
beforeAll(() => {
 | 
					beforeAll(() => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
export * as Sentry from '@sentry/react-native'
 | 
					export {Native as Sentry} from 'sentry-expo'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
export * as Sentry from '@sentry/react-native'
 | 
					export {Browser as Sentry} from 'sentry-expo'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -144,7 +144,8 @@ 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,6 +60,7 @@ 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 || '')
 | 
				
			||||||
| 
						 | 
					@ -82,18 +83,12 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -162,6 +157,22 @@ 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>
 | 
				
			||||||
| 
						 | 
					@ -193,6 +204,7 @@ 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()
 | 
				
			||||||
| 
						 | 
					@ -221,6 +233,7 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -312,7 +325,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`)}
 | 
				
			||||||
| 
						 | 
					@ -326,7 +339,7 @@ export const LoginForm = ({
 | 
				
			||||||
            </ButtonText>
 | 
					            </ButtonText>
 | 
				
			||||||
            {isProcessing && <ButtonIcon icon={Loader} />}
 | 
					            {isProcessing && <ButtonIcon icon={Loader} />}
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        )}
 | 
					        ) : undefined}
 | 
				
			||||||
      </View>
 | 
					      </View>
 | 
				
			||||||
    </FormContainer>
 | 
					    </FormContainer>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,21 +14,17 @@ 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, useAlf, useTheme} from '#/alf'
 | 
					import {atoms as a, native, 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 t = useTheme()
 | 
					 | 
				
			||||||
  const {_} = useLingui()
 | 
					  const {_} = useLingui()
 | 
				
			||||||
 | 
					  const t = useTheme()
 | 
				
			||||||
  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()
 | 
				
			||||||
| 
						 | 
					@ -58,22 +54,6 @@ 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}>
 | 
				
			||||||
| 
						 | 
					@ -91,143 +71,65 @@ export function AppearanceSettingsScreen({}: Props) {
 | 
				
			||||||
            </View>
 | 
					            </View>
 | 
				
			||||||
          </SimpleViewHeader>
 | 
					          </SimpleViewHeader>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <View style={[a.gap_3xl, a.pt_xl, a.px_xl]}>
 | 
					          <View style={[a.p_xl, a.gap_lg]}>
 | 
				
			||||||
            <View style={[a.gap_lg]}>
 | 
					            <View style={[a.flex_row, a.align_center, a.gap_md]}>
 | 
				
			||||||
              <AppearanceToggleButtonGroup
 | 
					              <PhoneIcon style={t.atoms.text} />
 | 
				
			||||||
                title={_(msg`Color mode`)}
 | 
					              <Text style={a.text_md}>
 | 
				
			||||||
                icon={PhoneIcon}
 | 
					                <Trans>Mode</Trans>
 | 
				
			||||||
                items={[
 | 
					              </Text>
 | 
				
			||||||
                  {
 | 
					            </View>
 | 
				
			||||||
                    label: _(msg`System`),
 | 
					            <ToggleButton.Group
 | 
				
			||||||
                    name: 'system',
 | 
					              label={_(msg`Dark mode`)}
 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                  {
 | 
					 | 
				
			||||||
                    label: _(msg`Light`),
 | 
					 | 
				
			||||||
                    name: 'light',
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                  {
 | 
					 | 
				
			||||||
                    label: _(msg`Dark`),
 | 
					 | 
				
			||||||
                    name: 'dark',
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ]}
 | 
					 | 
				
			||||||
              values={[colorMode]}
 | 
					              values={[colorMode]}
 | 
				
			||||||
                onChange={onChangeAppearance}
 | 
					              onChange={onChangeAppearance}>
 | 
				
			||||||
              />
 | 
					              <ToggleButton.Button label={_(msg`System`)} name="system">
 | 
				
			||||||
 | 
					                <ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					                  <Trans>System</Trans>
 | 
				
			||||||
 | 
					                </ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					              </ToggleButton.Button>
 | 
				
			||||||
 | 
					              <ToggleButton.Button label={_(msg`Light`)} name="light">
 | 
				
			||||||
 | 
					                <ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					                  <Trans>Light</Trans>
 | 
				
			||||||
 | 
					                </ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					              </ToggleButton.Button>
 | 
				
			||||||
 | 
					              <ToggleButton.Button label={_(msg`Dark`)} name="dark">
 | 
				
			||||||
 | 
					                <ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					                  <Trans>Dark</Trans>
 | 
				
			||||||
 | 
					                </ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					              </ToggleButton.Button>
 | 
				
			||||||
 | 
					            </ToggleButton.Group>
 | 
				
			||||||
            {colorMode !== 'light' && (
 | 
					            {colorMode !== 'light' && (
 | 
				
			||||||
              <Animated.View
 | 
					              <Animated.View
 | 
				
			||||||
                entering={native(FadeInDown)}
 | 
					                entering={native(FadeInDown)}
 | 
				
			||||||
                  exiting={native(FadeOutDown)}>
 | 
					                exiting={native(FadeOutDown)}
 | 
				
			||||||
                  <AppearanceToggleButtonGroup
 | 
					                style={[a.mt_md, a.gap_lg]}>
 | 
				
			||||||
                    title={_(msg`Dark theme`)}
 | 
					                <View style={[a.flex_row, a.align_center, a.gap_md]}>
 | 
				
			||||||
                    icon={MoonIcon}
 | 
					                  <MoonIcon style={t.atoms.text} />
 | 
				
			||||||
                    items={[
 | 
					                  <Text style={a.text_md}>
 | 
				
			||||||
                      {
 | 
					                    <Trans>Dark theme</Trans>
 | 
				
			||||||
                        label: _(msg`Dim`),
 | 
					                  </Text>
 | 
				
			||||||
                        name: 'dim',
 | 
					                </View>
 | 
				
			||||||
                      },
 | 
					
 | 
				
			||||||
                      {
 | 
					                <ToggleButton.Group
 | 
				
			||||||
                        label: _(msg`Dark`),
 | 
					                  label={_(msg`Dark theme`)}
 | 
				
			||||||
                        name: 'dark',
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    ]}
 | 
					 | 
				
			||||||
                  values={[darkTheme ?? 'dim']}
 | 
					                  values={[darkTheme ?? 'dim']}
 | 
				
			||||||
                    onChange={onChangeDarkTheme}
 | 
					                  onChange={onChangeDarkTheme}>
 | 
				
			||||||
                  />
 | 
					                  <ToggleButton.Button label={_(msg`Dim`)} name="dim">
 | 
				
			||||||
 | 
					                    <ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					                      <Trans>Dim</Trans>
 | 
				
			||||||
 | 
					                    </ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					                  </ToggleButton.Button>
 | 
				
			||||||
 | 
					                  <ToggleButton.Button label={_(msg`Dark`)} name="dark">
 | 
				
			||||||
 | 
					                    <ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					                      <Trans>Dark</Trans>
 | 
				
			||||||
 | 
					                    </ToggleButton.ButtonText>
 | 
				
			||||||
 | 
					                  </ToggleButton.Button>
 | 
				
			||||||
 | 
					                </ToggleButton.Group>
 | 
				
			||||||
              </Animated.View>
 | 
					              </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,20 +1,19 @@
 | 
				
			||||||
import {makeAutoObservable, runInAction} from 'mobx'
 | 
					import {makeAutoObservable, runInAction} from 'mobx'
 | 
				
			||||||
 | 
					import {ImageModel} from './image'
 | 
				
			||||||
import {getImageDim} from 'lib/media/manip'
 | 
					import {Image as RNImage} from 'react-native-image-crop-picker'
 | 
				
			||||||
import {openPicker} from 'lib/media/picker'
 | 
					import {openPicker} from 'lib/media/picker'
 | 
				
			||||||
import {ImageInitOptions, ImageModel} from './image'
 | 
					import {getImageDim} from 'lib/media/manip'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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?: InitialImageUri[]) {
 | 
					  constructor(uris?: {uri: string; width: number; height: number}[]) {
 | 
				
			||||||
    makeAutoObservable(this)
 | 
					    makeAutoObservable(this)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (uris) {
 | 
					    if (uris) {
 | 
				
			||||||
| 
						 | 
					@ -34,7 +33,7 @@ export class GalleryModel {
 | 
				
			||||||
    return this.images.some(image => image.altText.trim() === '')
 | 
					    return this.images.some(image => image.altText.trim() === '')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  *add(image_: ImageInitOptions) {
 | 
					  *add(image_: Omit<RNImage, 'size'>) {
 | 
				
			||||||
    if (this.size >= 4) {
 | 
					    if (this.size >= 4) {
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -60,6 +59,7 @@ 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,15 +1,14 @@
 | 
				
			||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
 | 
					import {Image as RNImage} from 'react-native-image-crop-picker'
 | 
				
			||||||
import * as ImageManipulator from 'expo-image-manipulator'
 | 
					 | 
				
			||||||
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
 | 
					 | 
				
			||||||
import {makeAutoObservable, runInAction} from 'mobx'
 | 
					import {makeAutoObservable, runInAction} from 'mobx'
 | 
				
			||||||
import {Position} from 'react-avatar-editor'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {logger} from '#/logger'
 | 
					 | 
				
			||||||
import {POST_IMG_MAX} from 'lib/constants'
 | 
					import {POST_IMG_MAX} from 'lib/constants'
 | 
				
			||||||
import {openCropper} from 'lib/media/picker'
 | 
					import * as ImageManipulator from 'expo-image-manipulator'
 | 
				
			||||||
import {Dimensions} from 'lib/media/types'
 | 
					 | 
				
			||||||
import {getDataUriSize} from 'lib/media/util'
 | 
					import {getDataUriSize} from 'lib/media/util'
 | 
				
			||||||
 | 
					import {openCropper} from 'lib/media/picker'
 | 
				
			||||||
 | 
					import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
 | 
				
			||||||
 | 
					import {Position} from 'react-avatar-editor'
 | 
				
			||||||
 | 
					import {Dimensions} from 'lib/media/types'
 | 
				
			||||||
import {isIOS} from 'platform/detection'
 | 
					import {isIOS} from 'platform/detection'
 | 
				
			||||||
 | 
					import {logger} from '#/logger'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ImageManipulationAttributes {
 | 
					export interface ImageManipulationAttributes {
 | 
				
			||||||
  aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
 | 
					  aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
 | 
				
			||||||
| 
						 | 
					@ -20,13 +19,6 @@ 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'> {
 | 
				
			||||||
| 
						 | 
					@ -49,15 +41,12 @@ export class ImageModel implements Omit<RNImage, 'size'> {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  prevAttributes: ImageManipulationAttributes = {}
 | 
					  prevAttributes: ImageManipulationAttributes = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(image: ImageInitOptions) {
 | 
					  constructor(image: Omit<RNImage, 'size'>) {
 | 
				
			||||||
    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,7 +21,31 @@ 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 === 'following' || feedDesc.startsWith('list')) {
 | 
					    if (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,9 +175,19 @@ 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')
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
| 
						 | 
					@ -206,6 +216,15 @@ 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,23 +3,27 @@ import zod from 'zod'
 | 
				
			||||||
import {BaseNux} from '#/state/queries/nuxs/types'
 | 
					import {BaseNux} from '#/state/queries/nuxs/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum Nux {
 | 
					export enum Nux {
 | 
				
			||||||
  TenMillionDialog = 'TenMillionDialog',
 | 
					  One = 'one',
 | 
				
			||||||
  NeueTypography = 'NeueTypography',
 | 
					  Two = 'two',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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.TenMillionDialog
 | 
					      id: Nux.One
 | 
				
			||||||
      data: undefined
 | 
					      data: {
 | 
				
			||||||
 | 
					        likes: number
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }>
 | 
					    }>
 | 
				
			||||||
  | BaseNux<{
 | 
					  | BaseNux<{
 | 
				
			||||||
      id: Nux.NeueTypography
 | 
					      id: Nux.Two
 | 
				
			||||||
      data: undefined
 | 
					      data: undefined
 | 
				
			||||||
    }>
 | 
					    }>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
 | 
					export const NuxSchemas = {
 | 
				
			||||||
  [Nux.TenMillionDialog]: undefined,
 | 
					  [Nux.One]: zod.object({
 | 
				
			||||||
  [Nux.NeueTypography]: undefined,
 | 
					    likes: zod.number(),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  [Nux.Two]: undefined,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -73,7 +72,6 @@ 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,4 +4,6 @@ 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},
 | 
				
			||||||
> = Pick<AppBskyActorDefs.Nux, 'id' | 'completed' | 'expiresAt'> & T
 | 
					> = T & {
 | 
				
			||||||
 | 
					  completed: boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue