This commit is contained in:
Eric Bailey
2024-09-11 20:01:27 -05:00
parent 77d60a5b80
commit 63444052e8
8 changed files with 11 additions and 11 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,574 @@
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 {networkRetry} from '#/lib/async/retry'
import {getCanvas} from '#/lib/canvas'
import {shareUrl} from '#/lib/sharing'
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 {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,
ThemeProvider,
tokens,
useBreakpoints,
useTheme,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
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'
import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
// import {TwentyFivePercent} from '#/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent'
const DEBUG = false
const RATIO = 8 / 10
const WIDTH = 2000
const HEIGHT = WIDTH * RATIO
function getFontSize(count: number) {
const length = count.toString().length
if (length < 7) {
return 80
} else if (length < 5) {
return 100
} else {
return 70
}
}
function getPercentBadge(percent: number) {
if (percent <= 0.001) {
return PointOnePercent
} else if (percent <= 0.01) {
return OnePercent
} else if (percent <= 0.1) {
return TenPercent
}
// else if (percent <= 0.25) {
// return TwentyFivePercent
// }
return null
}
function Frame({children}: {children: React.ReactNode}) {
return (
<View
style={[
a.relative,
a.w_full,
a.overflow_hidden,
{
paddingTop: '80%',
},
]}>
{children}
</View>
)
}
export function TenMillion() {
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()
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(() => {
setTimeout(() => {
openComposer({
text: '10 milly, babyyy',
imageUris: [
{
uri,
width: WIDTH,
height: HEIGHT,
},
],
})
}, 1e3)
})
}
}
const onNativeShare = () => {
if (uri) {
controls.tenMillion.close(() => {
shareUrl(uri)
})
}
}
const download = async () => {
if (uri) {
const canvas = await getCanvas(uri)
const imgHref = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream')
const link = document.createElement('a')
link.setAttribute('download', `Bluesky 10M Users.png`)
link.setAttribute('href', imgHref)
link.click()
}
}
const imageRef = React.useRef<ViewShot>(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 () => {
if (
imageRef.current &&
imageRef.current.capture // &&
// cavasRelayout === 'updated'
) {
const uri = await imageRef.current.capture()
setUri(uri)
}
}
const canvas = isLoadingData ? null : (
<View
style={[
a.absolute,
a.overflow_hidden,
DEBUG
? {
width: 600,
height: 600 * RATIO,
}
: {
width: 1,
height: 1,
},
]}>
<View style={{width: 600}}>
<ThemeProvider theme="light">
<Frame>
<ViewShot
ref={imageRef}
options={{width: WIDTH, height: HEIGHT}}
style={[a.absolute, a.inset_0]}>
<View
// key={cavasRelayout}
onLayout={onCanvasLayout}
style={[
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
{
top: -1,
bottom: -1,
left: -1,
right: -1,
paddingVertical: 48,
paddingHorizontal: 48,
},
]}>
<GradientFill gradient={tokens.gradients.bonfire} />
<View
style={[
a.flex_1,
a.w_full,
a.align_center,
a.justify_center,
a.rounded_md,
{
backgroundColor: 'white',
shadowRadius: 32,
shadowOpacity: 0.1,
elevation: 24,
shadowColor: tokens.gradients.bonfire.values[0][1],
},
]}>
<View
style={[
a.absolute,
a.px_xl,
a.py_xl,
{
top: 0,
left: 0,
},
]}>
<Logomark fill={t.palette.primary_500} width={36} />
</View>
{/* Centered content */}
<View
style={[
{
paddingBottom: isNative ? 0 : 24,
},
]}>
<Text
style={[
a.text_md,
a.font_bold,
a.text_center,
a.pb_sm,
lightTheme.atoms.text_contrast_medium,
]}>
<Trans>
Celebrating {formatCount(i18n, 10000000)} users
</Trans>{' '}
🎉
</Text>
<View style={[a.flex_row, a.align_start]}>
<Text
style={[
a.absolute,
{
color: t.palette.primary_500,
fontSize: 32,
fontWeight: '900',
width: 32,
top: isNative ? -10 : 0,
left: 0,
transform: [
{
translateX: -16,
},
],
},
]}>
#
</Text>
<Text
style={[
a.relative,
a.text_center,
{
fontStyle: 'italic',
fontSize: getFontSize(userNumber),
lineHeight: getFontSize(userNumber),
fontWeight: '900',
letterSpacing: -2,
},
]}>
{i18n.number(userNumber)}
</Text>
</View>
{Badge && (
<View
style={[
a.absolute,
{
width: 64,
height: 64,
top: isNative ? 75 : 85,
right: '5%',
transform: [
{
rotate: '8deg',
},
],
},
]}>
<Badge fill={t.palette.primary_500} />
</View>
)}
</View>
{/* End centered content */}
<View
style={[
a.absolute,
a.px_xl,
a.py_xl,
{
bottom: 0,
left: 0,
right: 0,
},
]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
{/*
<UserAvatar
size={36}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
onLoad={onCanvasReady}
/>
*/}
<View style={[a.gap_2xs, a.flex_1]}>
<Text style={[a.text_sm, a.font_bold, a.leading_tight]}>
{sanitizeDisplayName(
profile.displayName ||
sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
</Text>
<View style={[a.flex_row, a.justify_between]}>
<Text
style={[
a.text_sm,
a.font_semibold,
,
a.leading_tight,
lightTheme.atoms.text_contrast_medium,
]}>
{sanitizeHandle(profile.handle, '@')}
</Text>
{profile.createdAt && (
<Text
style={[
a.text_sm,
a.font_semibold,
,
a.leading_tight,
lightTheme.atoms.text_contrast_low,
]}>
<Trans>
Joined{' '}
{i18n.date(profile.createdAt, {
dateStyle: 'long',
})}
</Trans>
</Text>
)}
</View>
</View>
</View>
</View>
</View>
</View>
</ViewShot>
</Frame>
</ThemeProvider>
</View>
</View>
)
return (
<Dialog.Outer control={controls.tenMillion}>
<Dialog.ScrollableInner
label={_(msg`Ten Million`)}
style={[
{
padding: 0,
paddingTop: 0,
},
]}>
<View
style={[
a.rounded_md,
a.overflow_hidden,
isNative && {
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
},
]}>
<Frame>
<View
style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}>
<GradientFill gradient={tokens.gradients.bonfire} />
{error ? (
<View>
<Text
style={[
a.text_md,
a.leading_snug,
a.text_center,
a.pb_md,
{
maxWidth: 300,
},
]}>
(°°)
</Text>
<Text
style={[
a.text_xl,
a.font_bold,
a.leading_snug,
a.text_center,
{
maxWidth: 300,
},
]}>
{error}
</Text>
</View>
) : isLoadingData || isLoadingImage ? (
<Loader size="xl" fill="white" />
) : (
<Image
accessibilityIgnoresInvertColors
source={{uri}}
style={[a.w_full, a.h_full]}
/>
)}
</View>
</Frame>
{canvas}
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
<Text
style={[
a.text_5xl,
a.leading_tight,
a.pb_lg,
{
fontWeight: '900',
},
]}>
Thanks for being an early part of Bluesky.
</Text>
<Text style={[a.leading_snug, a.text_lg, a.pb_xl]}>
<Trans>
We're rebuilding the social internet together. Congratulations,
we're glad you're here.
</Trans>{' '}
</Text>
<Divider />
<View
style={[
a.flex_row,
a.align_center,
a.justify_end,
a.gap_md,
a.pt_xl,
]}>
<Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
Brag a little!
</Text>
<Button
disabled={isLoadingImage}
label={
isNative
? _(msg`Share image externally`)
: _(msg`Download image`)
}
size="large"
variant="solid"
color="secondary"
shape="square"
onPress={isNative ? onNativeShare : download}>
<ButtonIcon icon={isNative ? Share : Download} />
</Button>
<Button
disabled={isLoadingImage}
label={_(msg`Share image in post`)}
size="large"
variant="solid"
color="primary"
onPress={sharePost}>
<ButtonText>{_(msg`Share`)}</ButtonText>
<ButtonIcon position="right" icon={ImageIcon} />
</Button>
</View>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+56
View File
@@ -0,0 +1,56 @@
import React from 'react'
import {useSession} from '#/state/session'
import * as Dialog from '#/components/Dialog'
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
type Context = {
controls: {
tenMillion: Dialog.DialogOuterProps['control']
}
}
const Context = React.createContext<Context>({
// @ts-ignore
controls: {},
})
export function useContext() {
return React.useContext(Context)
}
let SHOWN = false
export function NuxDialogs() {
const {hasSession} = useSession()
const tenMillion = Dialog.useDialogControl()
const ctx = React.useMemo(() => {
return {
controls: {
tenMillion,
},
}
}, [tenMillion])
React.useEffect(() => {
if (!hasSession) return
const t = setTimeout(() => {
if (!SHOWN) {
SHOWN = true
ctx.controls.tenMillion.open()
}
}, 2e3)
return () => {
clearTimeout(t)
}
}, [ctx, hasSession])
return (
<Context.Provider value={ctx}>
<TenMillion />
</Context.Provider>
)
}