about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-09-10 16:20:19 -0500
committerEric Bailey <git@esb.lol>2024-09-11 19:58:16 -0500
commiteaf0081623154df995e81f2ae430a723539df800 (patch)
tree704a27f675e28ac81151a92404cf7889ebb2cd97 /src
parent3c8b3b47823475b93a92dcf82a4cabbda625c323 (diff)
downloadvoidsky-eaf0081623154df995e81f2ae430a723539df800.tar.zst
WIP, avi not working on web
Diffstat (limited to 'src')
-rw-r--r--src/components/dialogs/nudges/TenMillion.tsx423
-rw-r--r--src/lib/canvas.ts15
-rw-r--r--src/view/com/util/UserAvatar.tsx4
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}