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