Refactor, integrate nux, snoozing
parent
63444052e8
commit
9bb385a4dd
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import Animated, {FadeIn} from 'react-native-reanimated'
|
||||||
import ViewShot from 'react-native-view-shot'
|
import ViewShot from 'react-native-view-shot'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {moderateProfile} from '@atproto/api'
|
import {moderateProfile} from '@atproto/api'
|
||||||
|
@ -17,7 +18,6 @@ import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useComposerControls} from 'state/shell'
|
import {useComposerControls} from 'state/shell'
|
||||||
import {formatCount} from '#/view/com/util/numeric/format'
|
import {formatCount} from '#/view/com/util/numeric/format'
|
||||||
// import {UserAvatar} from '#/view/com/util/UserAvatar'
|
|
||||||
import {Logomark} from '#/view/icons/Logomark'
|
import {Logomark} from '#/view/icons/Logomark'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
|
@ -28,7 +28,7 @@ import {
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
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 {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent'
|
||||||
import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent'
|
import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent'
|
||||||
import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent'
|
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 {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
// import {TwentyFivePercent} from '#/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent'
|
|
||||||
|
|
||||||
const DEBUG = false
|
const DEBUG = false
|
||||||
const RATIO = 8 / 10
|
const RATIO = 8 / 10
|
||||||
|
@ -65,9 +64,6 @@ function getPercentBadge(percent: number) {
|
||||||
} else if (percent <= 0.1) {
|
} else if (percent <= 0.1) {
|
||||||
return TenPercent
|
return TenPercent
|
||||||
}
|
}
|
||||||
// else if (percent <= 0.25) {
|
|
||||||
// return TwentyFivePercent
|
|
||||||
// }
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,41 +84,13 @@ function Frame({children}: {children: React.ReactNode}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TenMillion() {
|
export function TenMillion() {
|
||||||
const {hasSession} = useSession()
|
|
||||||
return hasSession ? <TenMillionInner /> : 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<string | null>(null)
|
|
||||||
const [userNumber, setUserNumber] = React.useState<number>(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 agent = useAgent()
|
||||||
|
const nuxDialogs = useNuxDialogContext()
|
||||||
|
const [userNumber, setUserNumber] = React.useState<number>(0)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function fetchUserNumber() {
|
async function fetchUserNumber() {
|
||||||
|
// TODO check for 3p PDS
|
||||||
if (agent.session?.accessJwt) {
|
if (agent.session?.accessJwt) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`,
|
`https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`,
|
||||||
|
@ -146,26 +114,83 @@ export function TenMillionInner() {
|
||||||
}
|
}
|
||||||
|
|
||||||
networkRetry(3, fetchUserNumber).catch(() => {
|
networkRetry(3, fetchUserNumber).catch(() => {
|
||||||
setError(
|
nuxDialogs.dismissActiveNux()
|
||||||
_(
|
|
||||||
msg`Oh no! We couldn't fetch your user number. Rest assured, we're glad you're here ❤️`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}, [
|
}, [
|
||||||
_,
|
|
||||||
agent.session?.accessJwt,
|
agent.session?.accessJwt,
|
||||||
setUserNumber,
|
setUserNumber,
|
||||||
controls.tenMillion,
|
nuxDialogs.dismissActiveNux,
|
||||||
setError,
|
nuxDialogs,
|
||||||
])
|
])
|
||||||
|
|
||||||
const sharePost = () => {
|
return userNumber ? <TenMillionInner userNumber={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<string | null>(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) {
|
if (uri) {
|
||||||
controls.tenMillion.close(() => {
|
control.close(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
openComposer({
|
openComposer({
|
||||||
text: '10 milly, babyyy',
|
text: _(
|
||||||
|
msg`I'm user #${i18n.number(
|
||||||
|
userNumber,
|
||||||
|
)} out of 10M. What a ride 😎`,
|
||||||
|
), // TODO
|
||||||
imageUris: [
|
imageUris: [
|
||||||
{
|
{
|
||||||
uri,
|
uri,
|
||||||
|
@ -177,17 +202,15 @@ export function TenMillionInner() {
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}, [_, i18n, control, openComposer, uri, userNumber])
|
||||||
|
const onNativeShare = React.useCallback(() => {
|
||||||
const onNativeShare = () => {
|
|
||||||
if (uri) {
|
if (uri) {
|
||||||
controls.tenMillion.close(() => {
|
control.close(() => {
|
||||||
shareUrl(uri)
|
shareUrl(uri)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}, [uri, control])
|
||||||
|
const download = React.useCallback(async () => {
|
||||||
const download = async () => {
|
|
||||||
if (uri) {
|
if (uri) {
|
||||||
const canvas = await getCanvas(uri)
|
const canvas = await getCanvas(uri)
|
||||||
const imgHref = canvas
|
const imgHref = canvas
|
||||||
|
@ -198,32 +221,24 @@ export function TenMillionInner() {
|
||||||
link.setAttribute('href', imgHref)
|
link.setAttribute('href', imgHref)
|
||||||
link.click()
|
link.click()
|
||||||
}
|
}
|
||||||
}
|
}, [uri])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Canvas stuff
|
||||||
|
*/
|
||||||
const imageRef = React.useRef<ViewShot>(null)
|
const imageRef = React.useRef<ViewShot>(null)
|
||||||
// const captureInProgress = React.useRef(false)
|
const captureInProgress = React.useRef(false)
|
||||||
// const [cavasRelayout, setCanvasRelayout] = React.useState('key')
|
const onCanvasReady = React.useCallback(async () => {
|
||||||
// const onCanvasReady = async () => {
|
|
||||||
// if (
|
|
||||||
// imageRef.current &&
|
|
||||||
// imageRef.current.capture &&
|
|
||||||
// !captureInProgress.current
|
|
||||||
// ) {
|
|
||||||
// captureInProgress.current = true
|
|
||||||
// setCanvasRelayout('updated')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
const onCanvasLayout = async () => {
|
|
||||||
if (
|
if (
|
||||||
imageRef.current &&
|
imageRef.current &&
|
||||||
imageRef.current.capture // &&
|
imageRef.current.capture &&
|
||||||
// cavasRelayout === 'updated'
|
!captureInProgress.current
|
||||||
) {
|
) {
|
||||||
|
captureInProgress.current = true
|
||||||
const uri = await imageRef.current.capture()
|
const uri = await imageRef.current.capture()
|
||||||
setUri(uri)
|
setUri(uri)
|
||||||
}
|
}
|
||||||
}
|
}, [setUri])
|
||||||
|
|
||||||
const canvas = isLoadingData ? null : (
|
const canvas = isLoadingData ? null : (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -247,8 +262,7 @@ export function TenMillionInner() {
|
||||||
options={{width: WIDTH, height: HEIGHT}}
|
options={{width: WIDTH, height: HEIGHT}}
|
||||||
style={[a.absolute, a.inset_0]}>
|
style={[a.absolute, a.inset_0]}>
|
||||||
<View
|
<View
|
||||||
// key={cavasRelayout}
|
onLayout={onCanvasReady}
|
||||||
onLayout={onCanvasLayout}
|
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
a.inset_0,
|
a.inset_0,
|
||||||
|
@ -442,7 +456,7 @@ export function TenMillionInner() {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={controls.tenMillion}>
|
<Dialog.Outer control={control} onClose={onClose}>
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Ten Million`)}
|
label={_(msg`Ten Million`)}
|
||||||
style={[
|
style={[
|
||||||
|
@ -494,11 +508,15 @@ export function TenMillionInner() {
|
||||||
) : isLoadingData || isLoadingImage ? (
|
) : isLoadingData || isLoadingImage ? (
|
||||||
<Loader size="xl" fill="white" />
|
<Loader size="xl" fill="white" />
|
||||||
) : (
|
) : (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.duration(150)}
|
||||||
|
style={[a.w_full, a.h_full]}>
|
||||||
<Image
|
<Image
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
source={{uri}}
|
source={{uri}}
|
||||||
style={[a.w_full, a.h_full]}
|
style={[a.w_full, a.h_full]}
|
||||||
/>
|
/>
|
||||||
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|
|
@ -1,56 +1,80 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import {Nux, useNuxs, useUpsertNuxMutation} from '#/state/queries/nuxs'
|
||||||
import {useSession} from '#/state/session'
|
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'
|
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
controls: {
|
activeNux: Nux | undefined
|
||||||
tenMillion: Dialog.DialogOuterProps['control']
|
dismissActiveNux: () => void
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queuedNuxs = [Nux.TenMillionDialog]
|
||||||
|
|
||||||
const Context = React.createContext<Context>({
|
const Context = React.createContext<Context>({
|
||||||
// @ts-ignore
|
activeNux: undefined,
|
||||||
controls: {},
|
dismissActiveNux: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useContext() {
|
export function useNuxDialogContext() {
|
||||||
return React.useContext(Context)
|
return React.useContext(Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
let SHOWN = false
|
|
||||||
|
|
||||||
export function NuxDialogs() {
|
export function NuxDialogs() {
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const tenMillion = Dialog.useDialogControl()
|
return hasSession ? <Inner /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inner() {
|
||||||
|
const {nuxs} = useNuxs()
|
||||||
|
const [snoozed, setSnoozed] = React.useState(() => {
|
||||||
|
return isSnoozed()
|
||||||
|
})
|
||||||
|
const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
|
||||||
|
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(() => {
|
const ctx = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
controls: {
|
activeNux,
|
||||||
tenMillion,
|
dismissActiveNux,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}, [tenMillion])
|
}, [activeNux, dismissActiveNux])
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!hasSession) return
|
|
||||||
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
if (!SHOWN) {
|
|
||||||
SHOWN = true
|
|
||||||
ctx.controls.tenMillion.open()
|
|
||||||
}
|
|
||||||
}, 2e3)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(t)
|
|
||||||
}
|
|
||||||
}, [ctx, hasSession])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider value={ctx}>
|
<Context.Provider value={ctx}>
|
||||||
<TenMillion />
|
{activeNux === Nux.TenMillionDialog && <TenMillion />}
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -3,27 +3,16 @@ import zod from 'zod'
|
||||||
import {BaseNux} from '#/state/queries/nuxs/types'
|
import {BaseNux} from '#/state/queries/nuxs/types'
|
||||||
|
|
||||||
export enum Nux {
|
export enum Nux {
|
||||||
One = 'one',
|
TenMillionDialog = 'TenMillionDialog',
|
||||||
Two = 'two',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nuxNames = new Set(Object.values(Nux))
|
export const nuxNames = new Set(Object.values(Nux))
|
||||||
|
|
||||||
export type AppNux =
|
export type AppNux = BaseNux<{
|
||||||
| BaseNux<{
|
id: Nux.TenMillionDialog
|
||||||
id: Nux.One
|
|
||||||
data: {
|
|
||||||
likes: number
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
| BaseNux<{
|
|
||||||
id: Nux.Two
|
|
||||||
data: undefined
|
data: undefined
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export const NuxSchemas = {
|
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
||||||
[Nux.One]: zod.object({
|
[Nux.TenMillionDialog]: undefined,
|
||||||
likes: zod.number(),
|
|
||||||
}),
|
|
||||||
[Nux.Two]: undefined,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Device data that's specific to the device and does not vary based account
|
* Device data that's specific to the device and does not vary based account
|
||||||
*/
|
*/
|
||||||
export type Device = {}
|
export type Device = {
|
||||||
|
lastNuxDialog: string
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue