From 76c584d981f195a580e132b786e101b3d0d32380 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 9 Sep 2024 20:57:32 -0500 Subject: [PATCH 01/24] WIP --- src/App.native.tsx | 2 + src/App.web.tsx | 2 + src/components/dialogs/nudges/TenMillion.tsx | 100 +++++++++++++++++++ src/components/dialogs/nudges/index.tsx | 53 ++++++++++ src/lib/hooks/useIntentHandler.ts | 6 +- src/view/shell/Composer.web.tsx | 1 + 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/components/dialogs/nudges/TenMillion.tsx create mode 100644 src/components/dialogs/nudges/index.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index 780d4058f90..95625bdff54 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -63,6 +63,7 @@ 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' +import {NudgeDialogs} from '#/components/dialogs/nudges' SplashScreen.preventAutoHideAsync() @@ -131,6 +132,7 @@ function InnerApp() { style={s.h100pct}> + diff --git a/src/App.web.tsx b/src/App.web.tsx index 3017a3a264f..79120ffdbf7 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -50,6 +50,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' +import {NudgeDialogs} from '#/components/dialogs/nudges' function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -113,6 +114,7 @@ function InnerApp() { + diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion.tsx new file mode 100644 index 00000000000..9b5d5eae628 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {View} from 'react-native' +import ViewShot from 'react-native-view-shot' + +import {atoms as a, useBreakpoints, tokens} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' +import {GradientFill} from '#/components/GradientFill' +import {Button, ButtonText} from '#/components/Button' +import {useComposerControls} from 'state/shell' + +import {useContext} from '#/components/dialogs/nudges' + +export function TenMillion() { + const {_} = useLingui() + const {controls} = useContext() + const {gtMobile} = useBreakpoints() + const {openComposer} = useComposerControls() + + const imageRef = React.useRef(null) + + const share = () => { + if (imageRef.current && imageRef.current.capture) { + imageRef.current.capture().then(uri => { + controls.tenMillion.close(() => { + setTimeout(() => { + openComposer({ + text: '10 milly, babyyy', + imageUris: [ + { + uri, + width: 1000, + height: 1000, + }, + ], + }) + }, 1e3) + }) + }) + } + } + + return ( + + + + + + + + + + 10 milly, babyyy + + + + + + + + ) +} diff --git a/src/components/dialogs/nudges/index.tsx b/src/components/dialogs/nudges/index.tsx new file mode 100644 index 00000000000..357d4e2b402 --- /dev/null +++ b/src/components/dialogs/nudges/index.tsx @@ -0,0 +1,53 @@ +import React from 'react' + +import * as Dialog from '#/components/Dialog' + +import {TenMillion} from '#/components/dialogs/nudges/TenMillion' + +type Context = { + controls: { + tenMillion: Dialog.DialogOuterProps['control'] + } +} + +const Context = React.createContext({ + // @ts-ignore + controls: {} +}) + +export function useContext() { + return React.useContext(Context) +} + +let SHOWN = false + +export function NudgeDialogs() { + const tenMillion = Dialog.useDialogControl() + + const ctx = React.useMemo(() => { + return { + controls: { + tenMillion + } + } + }, [tenMillion]) + + React.useEffect(() => { + const t = setTimeout(() => { + if (!SHOWN) { + SHOWN = true + ctx.controls.tenMillion.open() + } + }, 2e3) + + return () => { + clearTimeout(t) + } + }, [ctx]) + + return ( + + + + ) +} diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 8cccda48fbc..67f1c2c386a 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -71,7 +71,7 @@ export function useIntentHandler() { }, [incomingUrl, composeIntent, verifyEmailIntent]) } -function useComposeIntent() { +export function useComposeIntent() { const closeAllActiveElements = useCloseAllActiveElements() const {openComposer} = useComposerControls() const {hasSession} = useSession() @@ -97,6 +97,10 @@ function useComposeIntent() { if (part.includes('https://') || part.includes('http://')) { return false } + console.log({ + part, + text: VALID_IMAGE_REGEX.test(part), + }) // We also should just filter out cases that don't have all the info we need return VALID_IMAGE_REGEX.test(part) }) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 42696139e0a..ee1ed66226d 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -63,6 +63,7 @@ export function Composer({}: {winHeight: number}) { mention={state.mention} openEmojiPicker={onOpenPicker} text={state.text} + imageUris={state.imageUris} /> From 3c8b3b47823475b93a92dcf82a4cabbda625c323 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 10 Sep 2024 14:23:30 -0500 Subject: [PATCH 02/24] Progress on desktoip --- src/alf/index.tsx | 18 +- src/components/dialogs/nudges/TenMillion.tsx | 330 ++++++++++++++++--- src/view/icons/Logomark.tsx | 29 ++ 3 files changed, 331 insertions(+), 46 deletions(-) create mode 100644 src/view/icons/Logomark.tsx diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 5fa7d3b1a17..d699de6a5bb 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -18,9 +18,17 @@ export * from '#/alf/util/themeSelector' export const Context = React.createContext<{ themeName: ThemeName theme: Theme + themes: ReturnType }>({ themeName: 'light', theme: defaultTheme, + themes: createThemes({ + hues: { + primary: BLUE_HUE, + negative: RED_HUE, + positive: GREEN_HUE, + }, + }), }) export function ThemeProvider({ @@ -42,18 +50,22 @@ export function ThemeProvider({ ({ + themes, themeName: themeName, theme: theme, }), - [theme, themeName], + [theme, themeName, themes], )}> {children} ) } -export function useTheme() { - return React.useContext(Context).theme +export function useTheme(theme?: ThemeName) { + const ctx = React.useContext(Context) + return React.useMemo(() => { + return theme ? ctx.themes[theme] : ctx.theme + }, [theme, ctx]) } export function useBreakpoints() { diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion.tsx index 9b5d5eae628..869056977bb 100644 --- a/src/components/dialogs/nudges/TenMillion.tsx +++ b/src/components/dialogs/nudges/TenMillion.tsx @@ -1,25 +1,74 @@ import React from 'react' -import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' import {View} from 'react-native' import ViewShot from 'react-native-view-shot' +import {moderateProfile} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import {atoms as a, useBreakpoints, tokens} from '#/alf' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {isNative} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {useComposerControls} from 'state/shell' +import {formatCount} from '#/view/com/util/numeric/format' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {Logomark} from '#/view/icons/Logomark' +import { + atoms as a, + ThemeProvider, + tokens, + useBreakpoints, + useTheme, +} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {Text} from '#/components/Typography' +import {useContext} from '#/components/dialogs/nudges' +import {Divider} from '#/components/Divider' import {GradientFill} from '#/components/GradientFill' -import {Button, ButtonText} from '#/components/Button' -import {useComposerControls} from 'state/shell' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' -import {useContext} from '#/components/dialogs/nudges' +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 + } +} export function TenMillion() { - const {_} = useLingui() + const t = useTheme() + const lightTheme = useTheme('light') + const {_, i18n} = useLingui() const {controls} = useContext() const {gtMobile} = useBreakpoints() const {openComposer} = useComposerControls() - const imageRef = React.useRef(null) + const {currentAccount} = useSession() + const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ + did: currentAccount!.did, + }) // TODO PWI + const moderationOpts = useModerationOpts() + const moderation = React.useMemo(() => { + return profile && moderationOpts + ? moderateProfile(profile, moderationOpts) + : undefined + }, [profile, moderationOpts]) + + const isLoading = isProfileLoading || !moderation || !profile + + const userNumber = 56738 const share = () => { if (imageRef.current && imageRef.current.capture) { @@ -31,8 +80,8 @@ export function TenMillion() { imageUris: [ { uri, - width: 1000, - height: 1000, + width: WIDTH, + height: HEIGHT, }, ], }) @@ -48,52 +97,247 @@ export function TenMillion() { + style={[ + { + padding: 0, + }, + // gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + ]}> - + + + + + + {isLoading ? ( + + ) : ( + + + + + + {/* Centered content */} + + + + Celebrating {formatCount(i18n, 10000000)} users + {' '} + 🎉 + + + + # + + {i18n.number(userNumber)} + + + {/* End centered content */} + + + + + + + {sanitizeDisplayName( + profile.displayName || + sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + + + + {sanitizeHandle(profile.handle, '@')} + + + {profile.createdAt && ( + + {i18n.date(profile.createdAt, { + dateStyle: 'long', + })} + + )} + + + + + + )} + + + + + + + - + You're part of the next wave of the internet. + + + + Online culture is too important to be controlled by a few + corporations.{' '} + + We’re dedicated to building an open foundation for the social + internet so that we can all shape its future. + + + + + + + + Brag a little ;) + - 10 milly, babyyy + + - + - + ) diff --git a/src/view/icons/Logomark.tsx b/src/view/icons/Logomark.tsx new file mode 100644 index 00000000000..5715a1a404b --- /dev/null +++ b/src/view/icons/Logomark.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import Svg, {Path, PathProps, SvgProps} from 'react-native-svg' + +import {usePalette} from '#/lib/hooks/usePalette' + +const ratio = 54 / 61 + +export function Logomark({ + fill, + ...rest +}: {fill?: PathProps['fill']} & SvgProps) { + const pal = usePalette('default') + // @ts-ignore it's fiiiiine + const size = parseInt(rest.width || 32) + + return ( + + + + ) +} From eaf0081623154df995e81f2ae430a723539df800 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 10 Sep 2024 16:20:19 -0500 Subject: [PATCH 03/24] WIP, avi not working on web --- src/components/dialogs/nudges/TenMillion.tsx | 423 +++++++++++-------- src/lib/canvas.ts | 15 + src/view/com/util/UserAvatar.tsx | 4 + 3 files changed, 266 insertions(+), 176 deletions(-) create mode 100644 src/lib/canvas.ts diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion.tsx index 869056977bb..2be5e3491c0 100644 --- a/src/components/dialogs/nudges/TenMillion.tsx +++ b/src/components/dialogs/nudges/TenMillion.tsx @@ -1,10 +1,12 @@ import React from 'react' import {View} from 'react-native' import ViewShot from 'react-native-view-shot' +import {Image} from 'expo-image' import {moderateProfile} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {getCanvas} from '#/lib/canvas' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isNative} from '#/platform/detection' @@ -32,6 +34,7 @@ import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Ima import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' +const DEBUG = false const RATIO = 8 / 10 const WIDTH = 2000 const HEIGHT = WIDTH * RATIO @@ -47,6 +50,22 @@ function getFontSize(count: number) { } } +function Frame({children}: {children: React.ReactNode}) { + return ( + + {children} + + ) +} + export function TenMillion() { const t = useTheme() const lightTheme = useTheme('light') @@ -54,7 +73,6 @@ export function TenMillion() { const {controls} = useContext() const {gtMobile} = useBreakpoints() const {openComposer} = useComposerControls() - const imageRef = React.useRef(null) const {currentAccount} = useSession() const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ did: currentAccount!.did, @@ -65,220 +83,273 @@ export function TenMillion() { ? moderateProfile(profile, moderationOpts) : undefined }, [profile, moderationOpts]) + const [uri, setUri] = React.useState(null) - const isLoading = isProfileLoading || !moderation || !profile + const isLoadingData = isProfileLoading || !moderation || !profile + const isLoadingImage = !uri - const userNumber = 56738 + const userNumber = 56738 // TODO + + const captureInProgress = React.useRef(false) + const imageRef = React.useRef(null) const share = () => { - if (imageRef.current && imageRef.current.capture) { - imageRef.current.capture().then(uri => { - controls.tenMillion.close(() => { - setTimeout(() => { - openComposer({ - text: '10 milly, babyyy', - imageUris: [ - { - uri, - width: WIDTH, - height: HEIGHT, - }, - ], - }) - }, 1e3) - }) + if (uri) { + controls.tenMillion.close(() => { + setTimeout(() => { + openComposer({ + text: '10 milly, babyyy', + imageUris: [ + { + uri, + width: WIDTH, + height: HEIGHT, + }, + ], + }) + }, 1e3) }) } } - return ( - - + const onCanvasReady = async () => { + if ( + imageRef.current && + imageRef.current.capture && + !captureInProgress.current + ) { + captureInProgress.current = true + const uri = await imageRef.current.capture() + setUri(uri) + } + } - - { + 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() + } + } + + const canvas = isLoadingData ? null : ( + - - - + ]}> + + + + + + + - + + + - {isLoading ? ( - - ) : ( - + + + Celebrating {formatCount(i18n, 10000000)} users + {' '} + 🎉 + + - - - + # + + {i18n.number(userNumber)} + + + {/* End centered content */} - {/* Centered content */} - - - - Celebrating {formatCount(i18n, 10000000)} users - {' '} - 🎉 + + + + + + {sanitizeDisplayName( + profile.displayName || + sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} - + - # + {sanitizeHandle(profile.handle, '@')} - {i18n.number(userNumber)} - - - {/* End centered content */} - - - - - - {sanitizeDisplayName( - profile.displayName || - sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} + {profile.createdAt && ( + + {i18n.date(profile.createdAt, { + dateStyle: 'long', + })} - - - {sanitizeHandle(profile.handle, '@')} - - - {profile.createdAt && ( - - {i18n.date(profile.createdAt, { - dateStyle: 'long', - })} - - )} - - + )} - )} + - + + + + + + + ) + + return ( + + + + + + + + + {isLoadingData || isLoadingImage ? ( + + ) : ( + + )} - + + + {canvas} + onPress={download}> diff --git a/src/components/icons/Download.tsx b/src/components/icons/Download.tsx new file mode 100644 index 00000000000..86b4942864e --- /dev/null +++ b/src/components/icons/Download.tsx @@ -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', +}) From 11ecea22d422138b8346dccded7dcdd70191fae4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 10 Sep 2024 18:45:08 -0500 Subject: [PATCH 05/24] Add badges, clean up spacing --- .../nudges/TenMillion/icons/OnePercent.tsx | 15 ++++ .../TenMillion/icons/PointOnePercent.tsx | 15 ++++ .../nudges/TenMillion/icons/TenPercent.tsx | 15 ++++ .../TenMillion/icons/TwentyFivePercent.tsx | 15 ++++ .../{TenMillion.tsx => TenMillion/index.tsx} | 79 ++++++++++++++++--- 5 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx create mode 100644 src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx create mode 100644 src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx create mode 100644 src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx rename src/components/dialogs/nudges/{TenMillion.tsx => TenMillion/index.tsx} (84%) diff --git a/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx new file mode 100644 index 00000000000..9c8d47afd03 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function OnePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx new file mode 100644 index 00000000000..1f9467e4423 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function PointOnePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx new file mode 100644 index 00000000000..4197be83572 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function TenPercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx b/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx new file mode 100644 index 00000000000..0d379714107 --- /dev/null +++ b/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function TwentyFivePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx similarity index 84% rename from src/components/dialogs/nudges/TenMillion.tsx rename to src/components/dialogs/nudges/TenMillion/index.tsx index 5aa45f21419..fdac91f4fee 100644 --- a/src/components/dialogs/nudges/TenMillion.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -10,7 +10,7 @@ import {getCanvas} from '#/lib/canvas' import {shareUrl} from '#/lib/sharing' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' -import {isAndroid, isNative, isWeb} from '#/platform/detection' +import {isNative} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' @@ -28,6 +28,9 @@ import { import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useContext} from '#/components/dialogs/nudges' +import {OnePercent} from '#/components/dialogs/nudges/TenMillion/icons/OnePercent' +import {PointOnePercent} from '#/components/dialogs/nudges/TenMillion/icons/PointOnePercent' +import {TenPercent} from '#/components/dialogs/nudges/TenMillion/icons/TenPercent' import {Divider} from '#/components/Divider' import {GradientFill} from '#/components/GradientFill' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' @@ -35,6 +38,7 @@ import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/D import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' +// import {TwentyFivePercent} from '#/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent' const DEBUG = false const RATIO = 8 / 10 @@ -52,6 +56,20 @@ function getFontSize(count: number) { } } +function getPercentBadge(percent: number) { + if (percent <= 0.001) { + return PointOnePercent + } else if (percent <= 0.01) { + return OnePercent + } else if (percent <= 0.1) { + return TenPercent + } + // else if (percent <= 0.25) { + // return TwentyFivePercent + // } + return null +} + function Frame({children}: {children: React.ReactNode}) { return ( { if (uri) { @@ -151,7 +171,6 @@ export function TenMillion() { imageRef.current.capture // && // cavasRelayout === 'updated' ) { - console.log('LAYOUT') const uri = await imageRef.current.capture() setUri(uri) } @@ -230,7 +249,7 @@ export function TenMillion() { @@ -246,16 +265,22 @@ export function TenMillion() { {' '} 🎉 - + # @@ -275,6 +300,26 @@ export function TenMillion() { {i18n.number(userNumber)} + + {Badge && ( + + + + )} {/* End centered content */} @@ -398,15 +443,23 @@ export function TenMillion() { You're part of the next wave of the internet. - - Online culture is too important to be controlled by a few - corporations.{' '} + + + Online culture is too important to be controlled by a few + corporations. + {' '} - We’re dedicated to building an open foundation for the social - internet so that we can all shape its future. + + We’re dedicated to building an open foundation for the social + internet so that we can all shape its future. + + + Congratulations. We're glad you're here. + + Date: Wed, 11 Sep 2024 09:51:40 -0500 Subject: [PATCH 06/24] Copy --- .../dialogs/nudges/TenMillion/index.tsx | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx index fdac91f4fee..f42cd2282b6 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -440,24 +440,14 @@ export function TenMillion() { fontWeight: '900', }, ]}> - You're part of the next wave of the internet. + Thanks for being an early part of Bluesky. - + - Online culture is too important to be controlled by a few - corporations. + We're rebuilding the social internet together. Congratulations, + we're glad you're here. {' '} - - - We’re dedicated to building an open foundation for the social - internet so that we can all shape its future. - - - - - - Congratulations. We're glad you're here. @@ -471,7 +461,7 @@ export function TenMillion() { a.pt_xl, ]}> - Brag a little ;) + Brag a little! From f8edd11bc5788666e0e6d55c1082971c901f089e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 09:57:37 -0500 Subject: [PATCH 07/24] Don't open for logged out users --- src/components/dialogs/nudges/TenMillion/index.tsx | 7 ++++++- src/components/dialogs/nudges/index.tsx | 13 ++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx index f42cd2282b6..c2c6926f780 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -87,6 +87,11 @@ function Frame({children}: {children: React.ReactNode}) { } export function TenMillion() { + const {hasSession} = useSession() + return hasSession ? : null +} + +export function TenMillionInner() { const t = useTheme() const lightTheme = useTheme('light') const {_, i18n} = useLingui() @@ -96,7 +101,7 @@ export function TenMillion() { const {currentAccount} = useSession() const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ did: currentAccount!.did, - }) // TODO PWI + }) const moderationOpts = useModerationOpts() const moderation = React.useMemo(() => { return profile && moderationOpts diff --git a/src/components/dialogs/nudges/index.tsx b/src/components/dialogs/nudges/index.tsx index 357d4e2b402..eabe60c1765 100644 --- a/src/components/dialogs/nudges/index.tsx +++ b/src/components/dialogs/nudges/index.tsx @@ -1,7 +1,7 @@ import React from 'react' +import {useSession} from '#/state/session' import * as Dialog from '#/components/Dialog' - import {TenMillion} from '#/components/dialogs/nudges/TenMillion' type Context = { @@ -12,7 +12,7 @@ type Context = { const Context = React.createContext({ // @ts-ignore - controls: {} + controls: {}, }) export function useContext() { @@ -22,17 +22,20 @@ export function useContext() { let SHOWN = false export function NudgeDialogs() { + const {hasSession} = useSession() const tenMillion = Dialog.useDialogControl() const ctx = React.useMemo(() => { return { controls: { - tenMillion - } + tenMillion, + }, } }, [tenMillion]) React.useEffect(() => { + if (!hasSession) return + const t = setTimeout(() => { if (!SHOWN) { SHOWN = true @@ -43,7 +46,7 @@ export function NudgeDialogs() { return () => { clearTimeout(t) } - }, [ctx]) + }, [ctx, hasSession]) return ( From 77d60a5b8047c2a4b18206e99d89d25222b4601c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 18:18:04 -0500 Subject: [PATCH 08/24] Hook up data --- .../dialogs/nudges/TenMillion/index.tsx | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nudges/TenMillion/index.tsx index c2c6926f780..e110ed1ffb5 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nudges/TenMillion/index.tsx @@ -6,6 +6,7 @@ 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 {sanitizeDisplayName} from '#/lib/strings/display-names' @@ -13,7 +14,7 @@ import {sanitizeHandle} from '#/lib/strings/handles' import {isNative} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' -import {useSession} from '#/state/session' +import {useAgent, useSession} from '#/state/session' import {useComposerControls} from 'state/shell' import {formatCount} from '#/view/com/util/numeric/format' // import {UserAvatar} from '#/view/com/util/UserAvatar' @@ -109,14 +110,56 @@ export function TenMillionInner() { : undefined }, [profile, moderationOpts]) const [uri, setUri] = React.useState(null) + const [userNumber, setUserNumber] = React.useState(0) + const [error, setError] = React.useState('') - const isLoadingData = isProfileLoading || !moderation || !profile + const isLoadingData = + isProfileLoading || !moderation || !profile || !userNumber const isLoadingImage = !uri - const userNumber = 56_738 // TODO const percent = userNumber / 10_000_000 const Badge = getPercentBadge(percent) + const agent = useAgent() + React.useEffect(() => { + async function fetchUserNumber() { + if (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) { + setUserNumber(data.number) + } + } + } + + networkRetry(3, fetchUserNumber).catch(() => { + setError( + _( + msg`Oh no! We couldn't fetch your user number. Rest assured, we're glad you're here ❤️`, + ), + ) + }) + }, [ + _, + agent.session?.accessJwt, + setUserNumber, + controls.tenMillion, + setError, + ]) + const sharePost = () => { if (uri) { controls.tenMillion.close(() => { @@ -421,7 +464,34 @@ export function TenMillionInner() { - {isLoadingData || isLoadingImage ? ( + {error ? ( + + + (╯°□°)╯︵ ┻━┻ + + + {error} + + + ) : isLoadingData || isLoadingImage ? ( ) : ( Date: Wed, 11 Sep 2024 20:01:27 -0500 Subject: [PATCH 09/24] Rename --- src/App.native.tsx | 4 ++-- src/App.web.tsx | 4 ++-- .../{nudges => nuxs}/TenMillion/icons/OnePercent.tsx | 0 .../TenMillion/icons/PointOnePercent.tsx | 0 .../{nudges => nuxs}/TenMillion/icons/TenPercent.tsx | 0 .../TenMillion/icons/TwentyFivePercent.tsx | 0 .../dialogs/{nudges => nuxs}/TenMillion/index.tsx | 10 +++++----- src/components/dialogs/{nudges => nuxs}/index.tsx | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/OnePercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/PointOnePercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/TenPercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/icons/TwentyFivePercent.tsx (100%) rename src/components/dialogs/{nudges => nuxs}/TenMillion/index.tsx (97%) rename src/components/dialogs/{nudges => nuxs}/index.tsx (90%) diff --git a/src/App.native.tsx b/src/App.native.tsx index 95625bdff54..83f133e9901 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -57,13 +57,13 @@ import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} 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' -import {NudgeDialogs} from '#/components/dialogs/nudges' SplashScreen.preventAutoHideAsync() @@ -132,7 +132,7 @@ function InnerApp() { style={s.h100pct}> - + diff --git a/src/App.web.tsx b/src/App.web.tsx index 79120ffdbf7..ff9944fa4a0 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -46,11 +46,11 @@ import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' import {ThemeProvider as Alf} 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 {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' -import {NudgeDialogs} from '#/components/dialogs/nudges' function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -114,7 +114,7 @@ function InnerApp() { - + diff --git a/src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/OnePercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/PointOnePercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/TenPercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx similarity index 100% rename from src/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent.tsx rename to src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx diff --git a/src/components/dialogs/nudges/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx similarity index 97% rename from src/components/dialogs/nudges/TenMillion/index.tsx rename to src/components/dialogs/nuxs/TenMillion/index.tsx index e110ed1ffb5..663c0956070 100644 --- a/src/components/dialogs/nudges/TenMillion/index.tsx +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -28,10 +28,10 @@ import { } from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {useContext} from '#/components/dialogs/nudges' -import {OnePercent} from '#/components/dialogs/nudges/TenMillion/icons/OnePercent' -import {PointOnePercent} from '#/components/dialogs/nudges/TenMillion/icons/PointOnePercent' -import {TenPercent} from '#/components/dialogs/nudges/TenMillion/icons/TenPercent' +import {useContext} 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' @@ -39,7 +39,7 @@ import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/D import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -// import {TwentyFivePercent} from '#/components/dialogs/nudges/TenMillion/icons/TwentyFivePercent' +// import {TwentyFivePercent} from '#/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent' const DEBUG = false const RATIO = 8 / 10 diff --git a/src/components/dialogs/nudges/index.tsx b/src/components/dialogs/nuxs/index.tsx similarity index 90% rename from src/components/dialogs/nudges/index.tsx rename to src/components/dialogs/nuxs/index.tsx index eabe60c1765..401dd3e669b 100644 --- a/src/components/dialogs/nudges/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -2,7 +2,7 @@ import React from 'react' import {useSession} from '#/state/session' import * as Dialog from '#/components/Dialog' -import {TenMillion} from '#/components/dialogs/nudges/TenMillion' +import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' type Context = { controls: { @@ -21,7 +21,7 @@ export function useContext() { let SHOWN = false -export function NudgeDialogs() { +export function NuxDialogs() { const {hasSession} = useSession() const tenMillion = Dialog.useDialogControl() From 9bb385a4dd54aca2b21533b7dd919ac8d0b4aeef Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 21:20:39 -0500 Subject: [PATCH 10/24] Refactor, integrate nux, snoozing --- .../dialogs/nuxs/TenMillion/index.tsx | 180 ++++++++++-------- src/components/dialogs/nuxs/index.tsx | 80 +++++--- src/components/dialogs/nuxs/snoozing.ts | 18 ++ src/state/queries/nuxs/definitions.ts | 25 +-- src/storage/schema.ts | 4 +- 5 files changed, 179 insertions(+), 128 deletions(-) create mode 100644 src/components/dialogs/nuxs/snoozing.ts diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx index 663c0956070..d96456d4729 100644 --- a/src/components/dialogs/nuxs/TenMillion/index.tsx +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -1,5 +1,6 @@ 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 {moderateProfile} from '@atproto/api' @@ -17,7 +18,6 @@ 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 {UserAvatar} from '#/view/com/util/UserAvatar' import {Logomark} from '#/view/icons/Logomark' import { atoms as a, @@ -28,7 +28,7 @@ import { } from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {useContext} from '#/components/dialogs/nuxs' +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' @@ -39,7 +39,6 @@ import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/D import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -// import {TwentyFivePercent} from '#/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent' const DEBUG = false const RATIO = 8 / 10 @@ -65,9 +64,6 @@ function getPercentBadge(percent: number) { } else if (percent <= 0.1) { return TenPercent } - // else if (percent <= 0.25) { - // return TwentyFivePercent - // } return null } @@ -88,41 +84,13 @@ function Frame({children}: {children: React.ReactNode}) { } export function TenMillion() { - const {hasSession} = useSession() - return hasSession ? : null -} - -export function TenMillionInner() { - const t = useTheme() - const lightTheme = useTheme('light') - const {_, i18n} = useLingui() - const {controls} = useContext() - const {gtMobile} = useBreakpoints() - const {openComposer} = useComposerControls() - const {currentAccount} = useSession() - const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ - did: currentAccount!.did, - }) - const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { - return profile && moderationOpts - ? moderateProfile(profile, moderationOpts) - : undefined - }, [profile, moderationOpts]) - const [uri, setUri] = React.useState(null) + const agent = useAgent() + const nuxDialogs = useNuxDialogContext() const [userNumber, setUserNumber] = React.useState(0) - const [error, setError] = React.useState('') - - const isLoadingData = - isProfileLoading || !moderation || !profile || !userNumber - const isLoadingImage = !uri - - const percent = userNumber / 10_000_000 - const Badge = getPercentBadge(percent) - const agent = useAgent() React.useEffect(() => { async function fetchUserNumber() { + // TODO check for 3p PDS if (agent.session?.accessJwt) { const res = await fetch( `https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`, @@ -146,26 +114,83 @@ export function TenMillionInner() { } networkRetry(3, fetchUserNumber).catch(() => { - setError( - _( - msg`Oh no! We couldn't fetch your user number. Rest assured, we're glad you're here ❤️`, - ), - ) + nuxDialogs.dismissActiveNux() }) }, [ - _, agent.session?.accessJwt, setUserNumber, - controls.tenMillion, - setError, + nuxDialogs.dismissActiveNux, + nuxDialogs, ]) - const sharePost = () => { + return userNumber ? : null +} + +export function TenMillionInner({userNumber}: {userNumber: number}) { + 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(null) + const percent = userNumber / 10_000_000 + const Badge = getPercentBadge(percent) + const isLoadingData = isProfileLoading || !moderation || !profile + const isLoadingImage = !uri + + 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() + }, 3e3) + return () => { + clearTimeout(timeout) + } + }, [control]) + const onClose = React.useCallback(() => { + nuxDialogs.dismissActiveNux() + }, [nuxDialogs]) + + /* + * Actions + */ + const sharePost = React.useCallback(() => { if (uri) { - controls.tenMillion.close(() => { + control.close(() => { setTimeout(() => { openComposer({ - text: '10 milly, babyyy', + text: _( + msg`I'm user #${i18n.number( + userNumber, + )} out of 10M. What a ride 😎`, + ), // TODO imageUris: [ { uri, @@ -177,17 +202,15 @@ export function TenMillionInner() { }, 1e3) }) } - } - - const onNativeShare = () => { + }, [_, i18n, control, openComposer, uri, userNumber]) + const onNativeShare = React.useCallback(() => { if (uri) { - controls.tenMillion.close(() => { + control.close(() => { shareUrl(uri) }) } - } - - const download = async () => { + }, [uri, control]) + const download = React.useCallback(async () => { if (uri) { const canvas = await getCanvas(uri) const imgHref = canvas @@ -198,32 +221,24 @@ export function TenMillionInner() { link.setAttribute('href', imgHref) link.click() } - } + }, [uri]) + /* + * Canvas stuff + */ const imageRef = React.useRef(null) - // const captureInProgress = React.useRef(false) - // const [cavasRelayout, setCanvasRelayout] = React.useState('key') - // const onCanvasReady = async () => { - // if ( - // imageRef.current && - // imageRef.current.capture && - // !captureInProgress.current - // ) { - // captureInProgress.current = true - // setCanvasRelayout('updated') - // } - // } - const onCanvasLayout = async () => { + const captureInProgress = React.useRef(false) + const onCanvasReady = React.useCallback(async () => { if ( imageRef.current && - imageRef.current.capture // && - // cavasRelayout === 'updated' + imageRef.current.capture && + !captureInProgress.current ) { + captureInProgress.current = true const uri = await imageRef.current.capture() setUri(uri) } - } - + }, [setUri]) const canvas = isLoadingData ? null : ( + ) : ( - + + + )} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 401dd3e669b..6c4598cdb12 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -1,56 +1,80 @@ import React from 'react' +import {Nux, useNuxs, useUpsertNuxMutation} from '#/state/queries/nuxs' import {useSession} from '#/state/session' -import * as Dialog from '#/components/Dialog' +import {isSnoozed, snooze} from '#/components/dialogs/nuxs/snoozing' import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' type Context = { - controls: { - tenMillion: Dialog.DialogOuterProps['control'] - } + activeNux: Nux | undefined + dismissActiveNux: () => void } +const queuedNuxs = [Nux.TenMillionDialog] + const Context = React.createContext({ - // @ts-ignore - controls: {}, + activeNux: undefined, + dismissActiveNux: () => {}, }) -export function useContext() { +export function useNuxDialogContext() { return React.useContext(Context) } -let SHOWN = false - export function NuxDialogs() { const {hasSession} = useSession() - const tenMillion = Dialog.useDialogControl() + return hasSession ? : null +} - const ctx = React.useMemo(() => { - return { - controls: { - tenMillion, - }, - } - }, [tenMillion]) +function Inner() { + const {nuxs} = useNuxs() + const [snoozed, setSnoozed] = React.useState(() => { + return isSnoozed() + }) + const [activeNux, setActiveNux] = React.useState() + const {mutate: upsertNux} = useUpsertNuxMutation() + + const snoozeNuxDialog = React.useCallback(() => { + snooze() + setSnoozed(true) + }, [setSnoozed]) + + const dismissActiveNux = React.useCallback(() => { + setActiveNux(undefined) + upsertNux({ + id: activeNux!, + completed: true, + data: undefined, + }) + }, [activeNux, setActiveNux, upsertNux]) React.useEffect(() => { - if (!hasSession) return + if (snoozed) return + if (!nuxs) return + + for (const id of queuedNuxs) { + const nux = nuxs.find(nux => nux.id === id) - const t = setTimeout(() => { - if (!SHOWN) { - SHOWN = true - ctx.controls.tenMillion.open() - } - }, 2e3) + if (nux && nux.completed) continue - return () => { - clearTimeout(t) + setActiveNux(id) + // snooze immediately upon enabling + snoozeNuxDialog() + + break + } + }, [nuxs, snoozed, snoozeNuxDialog]) + + const ctx = React.useMemo(() => { + return { + activeNux, + dismissActiveNux, } - }, [ctx, hasSession]) + }, [activeNux, dismissActiveNux]) return ( - + {activeNux === Nux.TenMillionDialog && } ) } diff --git a/src/components/dialogs/nuxs/snoozing.ts b/src/components/dialogs/nuxs/snoozing.ts new file mode 100644 index 00000000000..a36efd8edcb --- /dev/null +++ b/src/components/dialogs/nuxs/snoozing.ts @@ -0,0 +1,18 @@ +import {simpleAreDatesEqual} from '#/lib/strings/time' +import {device} from '#/storage' + +export function snooze() { + device.set(['lastNuxDialog'], new Date().toISOString()) +} + +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 +} diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index c5cb1e9d9bc..865967d37ad 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -3,27 +3,16 @@ import zod from 'zod' import {BaseNux} from '#/state/queries/nuxs/types' export enum Nux { - One = 'one', - Two = 'two', + TenMillionDialog = 'TenMillionDialog', } export const nuxNames = new Set(Object.values(Nux)) -export type AppNux = - | BaseNux<{ - id: Nux.One - data: { - likes: number - } - }> - | BaseNux<{ - id: Nux.Two - data: undefined - }> +export type AppNux = BaseNux<{ + id: Nux.TenMillionDialog + data: undefined +}> -export const NuxSchemas = { - [Nux.One]: zod.object({ - likes: zod.number(), - }), - [Nux.Two]: undefined, +export const NuxSchemas: Record | undefined> = { + [Nux.TenMillionDialog]: undefined, } diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 6522d75a363..be074db430d 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -1,4 +1,6 @@ /** * Device data that's specific to the device and does not vary based account */ -export type Device = {} +export type Device = { + lastNuxDialog: string +} From c8b133863df5c6b417562f71f8a3c6feef280139 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 21:28:34 -0500 Subject: [PATCH 11/24] Fix some nux types --- src/components/dialogs/nuxs/index.tsx | 9 ++++++--- src/state/queries/nuxs/types.ts | 4 +--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 6c4598cdb12..36db7764d33 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -40,13 +40,16 @@ function Inner() { }, [setSnoozed]) const dismissActiveNux = React.useCallback(() => { + if (!activeNux) return setActiveNux(undefined) + const nux = nuxs?.find(nux => nux.id === activeNux) upsertNux({ - id: activeNux!, + id: activeNux, completed: true, - data: undefined, + data: nux?.data, + expiresAt: nux?.expiresAt, }) - }, [activeNux, setActiveNux, upsertNux]) + }, [activeNux, setActiveNux, upsertNux, nuxs]) React.useEffect(() => { if (snoozed) return diff --git a/src/state/queries/nuxs/types.ts b/src/state/queries/nuxs/types.ts index 5b791847042..2331582a1d9 100644 --- a/src/state/queries/nuxs/types.ts +++ b/src/state/queries/nuxs/types.ts @@ -4,6 +4,4 @@ export type Data = Record | undefined export type BaseNux< T extends Pick & {data: Data}, -> = T & { - completed: boolean -} +> = Pick & T From 6e78ce53d74e79e2349ab357c7270e30742d33a5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 21:47:25 -0500 Subject: [PATCH 12/24] Dev helpers, string cleanup --- .../dialogs/nuxs/TenMillion/index.tsx | 9 ++++----- src/components/dialogs/nuxs/index.tsx | 20 +++++++++++++++++-- src/components/dialogs/nuxs/snoozing.ts | 4 ++++ src/storage/schema.ts | 2 +- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx index d96456d4729..5da295ab604 100644 --- a/src/components/dialogs/nuxs/TenMillion/index.tsx +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -430,7 +430,6 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { style={[ a.text_sm, a.font_semibold, - , a.leading_tight, lightTheme.atoms.text_contrast_low, ]}> @@ -533,13 +532,13 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { fontWeight: '900', }, ]}> - Thanks for being an early part of Bluesky. + You're part of the next wave of the internet. - We're rebuilding the social internet together. Congratulations, - we're glad you're here. + Thanks for being part of our first 10 million users. We're glad + you're here. {' '} @@ -554,7 +553,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) { a.pt_xl, ]}> - Brag a little! + Brag a little!