From 9bb385a4dd54aca2b21533b7dd919ac8d0b4aeef Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 11 Sep 2024 21:20:39 -0500 Subject: [PATCH] Refactor, integrate nux, snoozing --- .../dialogs/nuxs/TenMillion/index.tsx | 182 ++++++++++-------- src/components/dialogs/nuxs/index.tsx | 84 +++++--- src/components/dialogs/nuxs/snoozing.ts | 18 ++ src/state/queries/nuxs/definitions.ts | 25 +-- src/storage/schema.ts | 4 +- 5 files changed, 182 insertions(+), 131 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 663c0956..d96456d4 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 [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() + const nuxDialogs = useNuxDialogContext() + const [userNumber, setUserNumber] = React.useState(0) + 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 401dd3e6..6c4598cd 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 +} + +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 (snoozed) return + if (!nuxs) return + + for (const id of queuedNuxs) { + const nux = nuxs.find(nux => nux.id === id) + + if (nux && nux.completed) continue + + setActiveNux(id) + // snooze immediately upon enabling + snoozeNuxDialog() + + break + } + }, [nuxs, snoozed, snoozeNuxDialog]) const ctx = React.useMemo(() => { return { - controls: { - tenMillion, - }, + activeNux, + dismissActiveNux, } - }, [tenMillion]) - - React.useEffect(() => { - if (!hasSession) return - - const t = setTimeout(() => { - if (!SHOWN) { - SHOWN = true - ctx.controls.tenMillion.open() - } - }, 2e3) - - return () => { - clearTimeout(t) - } - }, [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 00000000..a36efd8e --- /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 c5cb1e9d..865967d3 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 6522d75a..be074db4 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 +}