diff options
author | Eric Bailey <git@esb.lol> | 2024-09-10 16:20:19 -0500 |
---|---|---|
committer | Eric Bailey <git@esb.lol> | 2024-09-11 19:58:16 -0500 |
commit | eaf0081623154df995e81f2ae430a723539df800 (patch) | |
tree | 704a27f675e28ac81151a92404cf7889ebb2cd97 | |
parent | 3c8b3b47823475b93a92dcf82a4cabbda625c323 (diff) | |
download | voidsky-eaf0081623154df995e81f2ae430a723539df800.tar.zst |
WIP, avi not working on web
-rw-r--r-- | src/components/dialogs/nudges/TenMillion.tsx | 423 | ||||
-rw-r--r-- | src/lib/canvas.ts | 15 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 4 |
3 files changed, 266 insertions, 176 deletions
diff --git a/src/components/dialogs/nudges/TenMillion.tsx b/src/components/dialogs/nudges/TenMillion.tsx index 869056977..2be5e3491 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 ( + <View + style={[ + a.relative, + a.w_full, + a.overflow_hidden, + { + paddingTop: '80%', + }, + ]}> + {children} + </View> + ) +} + 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<ViewShot>(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<string | null>(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<ViewShot>(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 ( - <Dialog.Outer control={controls.tenMillion}> - <Dialog.Handle /> + const onCanvasReady = async () => { + if ( + imageRef.current && + imageRef.current.capture && + !captureInProgress.current + ) { + captureInProgress.current = true + const uri = await imageRef.current.capture() + setUri(uri) + } + } - <Dialog.ScrollableInner - label={_(msg`Ten Million`)} - style={[ - { - padding: 0, - }, - // gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, - ]}> - <View - style={[ - a.rounded_md, - a.overflow_hidden, - isNative && { - borderTopLeftRadius: 40, - borderTopRightRadius: 40, + 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 canvas = isLoadingData ? null : ( + <View + style={[ + a.absolute, + a.overflow_hidden, + DEBUG + ? { + width: 600, + height: 600 * RATIO, + } + : { + width: 1, + height: 1, }, - ]}> - <ThemeProvider theme="light"> - <View - style={[ - a.relative, - a.w_full, - a.overflow_hidden, - { - paddingTop: '80%', - }, - ]}> - <ViewShot - ref={imageRef} - options={{width: WIDTH, height: HEIGHT}} - style={[a.absolute, a.inset_0]}> + ]}> + <View style={{width: 600}}> + <ThemeProvider theme="light"> + <Frame> + <ViewShot + ref={imageRef} + options={{width: WIDTH, height: HEIGHT}} + style={[a.absolute, a.inset_0]}> + <View + style={[ + a.absolute, + a.inset_0, + a.align_center, + a.justify_center, + { + top: -1, + bottom: -1, + left: -1, + right: -1, + paddingVertical: 32, + paddingHorizontal: 48, + }, + ]}> + <GradientFill gradient={tokens.gradients.bonfire} /> + <View style={[ - a.absolute, - a.inset_0, + a.flex_1, + a.w_full, a.align_center, a.justify_center, + a.rounded_md, { - top: -1, - bottom: -1, - left: -1, - right: -1, - paddingVertical: 32, - paddingHorizontal: 48, + backgroundColor: 'white', + shadowRadius: 32, + shadowOpacity: 0.1, + elevation: 24, + shadowColor: tokens.gradients.bonfire.values[0][1], }, ]}> - <GradientFill gradient={tokens.gradients.bonfire} /> + <View + style={[ + a.absolute, + a.px_xl, + a.py_xl, + { + top: 0, + left: 0, + }, + ]}> + <Logomark fill={t.palette.primary_500} width={36} /> + </View> - {isLoading ? ( - <Loader size="xl" fill="white" /> - ) : ( - <View + {/* Centered content */} + <View + style={[ + { + paddingBottom: 48, + }, + ]}> + <Text + style={[ + a.text_md, + a.font_bold, + a.text_center, + a.pb_xs, + lightTheme.atoms.text_contrast_medium, + ]}> + <Trans> + Celebrating {formatCount(i18n, 10000000)} users + </Trans>{' '} + 🎉 + </Text> + <Text style={[ - a.flex_1, - a.w_full, - a.align_center, - a.justify_center, - a.rounded_md, + a.relative, + a.text_center, { - backgroundColor: 'white', - shadowRadius: 32, - shadowOpacity: 0.1, - elevation: 24, - shadowColor: tokens.gradients.bonfire.values[0][1], + fontStyle: 'italic', + fontSize: getFontSize(userNumber), + fontWeight: '900', + letterSpacing: -2, }, ]}> - <View + <Text style={[ a.absolute, - a.px_xl, - a.py_xl, { - top: 0, - left: 0, + color: t.palette.primary_500, + fontSize: 32, + left: -18, + top: 8, }, ]}> - <Logomark fill={t.palette.primary_500} width={36} /> - </View> + # + </Text> + {i18n.number(userNumber)} + </Text> + </View> + {/* End centered content */} - {/* Centered content */} - <View - style={[ - { - paddingBottom: 48, - }, - ]}> - <Text - style={[ - a.text_md, - a.font_bold, - a.text_center, - a.pb_xs, - lightTheme.atoms.text_contrast_medium, - ]}> - <Trans> - Celebrating {formatCount(i18n, 10000000)} users - </Trans>{' '} - 🎉 + <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]}> + {sanitizeDisplayName( + profile.displayName || + sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} </Text> - <Text - style={[ - a.relative, - a.text_center, - { - fontStyle: 'italic', - fontSize: getFontSize(userNumber), - fontWeight: '900', - letterSpacing: -2, - }, - ]}> + <View style={[a.flex_row, a.justify_between]}> <Text style={[ - a.absolute, - { - color: t.palette.primary_500, - fontSize: 32, - left: -18, - top: 8, - }, + a.text_sm, + a.font_semibold, + lightTheme.atoms.text_contrast_medium, ]}> - # + {sanitizeHandle(profile.handle, '@')} </Text> - {i18n.number(userNumber)} - </Text> - </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')} - /> - <View style={[a.gap_2xs, a.flex_1]}> - <Text style={[a.text_sm, a.font_bold]}> - {sanitizeDisplayName( - profile.displayName || - sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} + {profile.createdAt && ( + <Text + style={[ + a.text_sm, + a.font_semibold, + lightTheme.atoms.text_contrast_low, + ]}> + {i18n.date(profile.createdAt, { + dateStyle: 'long', + })} </Text> - <View style={[a.flex_row, a.justify_between]}> - <Text - style={[ - a.text_sm, - a.font_semibold, - lightTheme.atoms.text_contrast_medium, - ]}> - {sanitizeHandle(profile.handle, '@')} - </Text> - - {profile.createdAt && ( - <Text - style={[ - a.text_sm, - a.font_semibold, - lightTheme.atoms.text_contrast_low, - ]}> - {i18n.date(profile.createdAt, { - dateStyle: 'long', - })} - </Text> - )} - </View> - </View> + )} </View> </View> </View> - )} + </View> </View> - </ViewShot> + </View> + </ViewShot> + </Frame> + </ThemeProvider> + </View> + </View> + ) + + return ( + <Dialog.Outer control={controls.tenMillion}> + <Dialog.Handle /> + + <Dialog.ScrollableInner + label={_(msg`Ten Million`)} + style={[ + { + padding: 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} /> + {isLoadingData || isLoadingImage ? ( + <Loader size="xl" fill="white" /> + ) : ( + <Image + accessibilityIgnoresInvertColors + source={{uri}} + style={[a.w_full, a.h_full]} + /> + )} </View> - </ThemeProvider> + </Frame> + + {canvas} <View style={[gtMobile ? a.p_2xl : a.p_xl]}> <Text @@ -321,7 +392,7 @@ export function TenMillion() { variant="solid" color="secondary" shape="square" - onPress={share}> + onPress={download}> <ButtonIcon icon={Share} /> </Button> <Button diff --git a/src/lib/canvas.ts b/src/lib/canvas.ts new file mode 100644 index 000000000..760c0e67f --- /dev/null +++ b/src/lib/canvas.ts @@ -0,0 +1,15 @@ +export const getCanvas = (base64: string): Promise<HTMLCanvasElement> => { + return new Promise(resolve => { + const image = new Image() + image.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = image.width + canvas.height = image.height + + const ctx = canvas.getContext('2d') + ctx?.drawImage(image, 0, 0) + resolve(canvas) + } + image.src = base64 + }) +} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 66c9708bf..eb46a8bdb 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -43,6 +43,7 @@ interface BaseUserAvatarProps { interface UserAvatarProps extends BaseUserAvatarProps { moderation?: ModerationUI usePlainRNImage?: boolean + onLoad?: () => void } interface EditableUserAvatarProps extends BaseUserAvatarProps { @@ -174,6 +175,7 @@ let UserAvatar = ({ avatar, moderation, usePlainRNImage = false, + onLoad, }: UserAvatarProps): React.ReactNode => { const pal = usePalette('default') const backgroundColor = pal.colors.backgroundLight @@ -224,6 +226,7 @@ let UserAvatar = ({ uri: hackModifyThumbnailPath(avatar, size < 90), }} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} + onLoad={onLoad} /> ) : ( <HighPriorityImage @@ -234,6 +237,7 @@ let UserAvatar = ({ uri: hackModifyThumbnailPath(avatar, size < 90), }} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} + onLoad={onLoad} /> )} {alert} |