diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx
index 663c095607..d96456d472 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 401dd3e669..6c4598cdb1 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 0000000000..a36efd8edc
--- /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 c5cb1e9d9b..865967d37a 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 6522d75a36..be074db430 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
+}