about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-09-16 16:52:28 -0500
committerGitHub <noreply@github.com>2024-09-16 14:52:28 -0700
commitb69fd23456485d22c24b51da833d2707c718d61e (patch)
treebeb5f1d17aa9fb6eae314a6714c4913be39dcf5a
parent8daf6b78688ca20326a79fa9c7ca1cbd945786e1 (diff)
downloadvoidsky-b69fd23456485d22c24b51da833d2707c718d61e.tar.zst
Milly tweaks (#5365)
Co-authored-by: Hailey <me@haileyok.com>
-rw-r--r--src/components/Prompt.tsx4
-rw-r--r--src/components/dialogs/nuxs/TenMillion/Trigger.tsx129
-rw-r--r--src/components/dialogs/nuxs/TenMillion/index.tsx50
-rw-r--r--src/components/dialogs/nuxs/index.tsx42
-rw-r--r--src/lib/hooks/useIntentHandler.ts4
-rw-r--r--src/lib/statsig/gates.ts4
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx73
-rw-r--r--src/view/com/home/HomeHeaderLayoutMobile.tsx67
8 files changed, 304 insertions, 69 deletions
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx
index 86cb5c315..7836bbef9 100644
--- a/src/components/Prompt.tsx
+++ b/src/components/Prompt.tsx
@@ -59,7 +59,9 @@ export function Outer({
 export function TitleText({children}: React.PropsWithChildren<{}>) {
   const {titleId} = React.useContext(Context)
   return (
-    <Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
+    <Text
+      nativeID={titleId}
+      style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}>
       {children}
     </Text>
   )
diff --git a/src/components/dialogs/nuxs/TenMillion/Trigger.tsx b/src/components/dialogs/nuxs/TenMillion/Trigger.tsx
new file mode 100644
index 000000000..9616b3b1d
--- /dev/null
+++ b/src/components/dialogs/nuxs/TenMillion/Trigger.tsx
@@ -0,0 +1,129 @@
+import React from 'react'
+import {View} from 'react-native'
+import Svg, {Circle, Path} from 'react-native-svg'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
+import {atoms as a, ViewStyleProp} from '#/alf'
+import {Button, ButtonProps} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {InlineLinkText} from '#/components/Link'
+import * as Prompt from '#/components/Prompt'
+import {TenMillion} from './'
+
+export function Trigger({children}: {children: ButtonProps['children']}) {
+  const {_} = useLingui()
+  const {mutate: upsertNux} = useUpsertNuxMutation()
+  const [show, setShow] = React.useState(false)
+  const [fallback, setFallback] = React.useState(false)
+  const control = Prompt.usePromptControl()
+
+  const handleOnPress = () => {
+    if (!fallback) {
+      setShow(true)
+      upsertNux({
+        id: Nux.TenMillionDialog,
+        completed: true,
+        data: undefined,
+      })
+    } else {
+      control.open()
+    }
+  }
+
+  const onHandleFallback = () => {
+    setFallback(true)
+    control.open()
+  }
+
+  return (
+    <>
+      <Button
+        label={_(msg`Bluesky is celebrating 10 million users!`)}
+        onPress={handleOnPress}>
+        {children}
+      </Button>
+
+      {show && !fallback && (
+        <TenMillion
+          showTimeout={0}
+          onClose={() => setShow(false)}
+          onFallback={onHandleFallback}
+        />
+      )}
+
+      <Prompt.Outer control={control}>
+        <View style={{maxWidth: 300}}>
+          <Prompt.TitleText>
+            <Trans>Bluesky is celebrating 10 million users!</Trans>
+          </Prompt.TitleText>
+        </View>
+        <Prompt.DescriptionText>
+          <Trans>
+            Together, we're rebuilding the social internet. We're glad you're
+            here.
+          </Trans>
+        </Prompt.DescriptionText>
+        <Prompt.DescriptionText>
+          <Trans>
+            To learn more,{' '}
+            <InlineLinkText
+              label={_(msg`View our post`)}
+              to="/profile/bsky.app/post/3l47prg3wgy23"
+              onPress={() => {
+                control.close()
+              }}
+              style={[a.text_md, a.leading_snug]}>
+              <Trans>check out our post.</Trans>
+            </InlineLinkText>
+          </Trans>
+        </Prompt.DescriptionText>
+        <Dialog.Close />
+      </Prompt.Outer>
+    </>
+  )
+}
+
+export function Icon({width, style}: {width: number} & ViewStyleProp) {
+  return (
+    <Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
+      <Path
+        fill="#dd2e44"
+        d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
+      />
+      <Path
+        fill="#ea596e"
+        d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
+      />
+      <Path
+        fill="#a0041e"
+        d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
+      />
+      <Path
+        fill="#aa8dd8"
+        d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
+      />
+      <Path
+        fill="#77b255"
+        d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
+      />
+      <Path
+        fill="#aa8dd8"
+        d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
+      />
+      <Path
+        fill="#77b255"
+        d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
+      />
+      <Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
+      <Circle cx="2" cy="18" r="2" fill="#9266cc" />
+      <Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
+      <Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
+      <Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
+      <Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
+      <Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
+      <Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
+    </Svg>
+  )
+}
diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx
index 267065672..4e7a171aa 100644
--- a/src/components/dialogs/nuxs/TenMillion/index.tsx
+++ b/src/components/dialogs/nuxs/TenMillion/index.tsx
@@ -87,7 +87,15 @@ function Frame({children}: {children: React.ReactNode}) {
   )
 }
 
-export function TenMillion() {
+export function TenMillion({
+  showTimeout,
+  onClose,
+  onFallback,
+}: {
+  showTimeout?: number
+  onClose?: () => void
+  onFallback?: () => void
+}) {
   const agent = useAgent()
   const nuxDialogs = useNuxDialogContext()
   const [userNumber, setUserNumber] = React.useState<number>(0)
@@ -120,7 +128,11 @@ export function TenMillion() {
         } else {
           // should be rare
           nuxDialogs.dismissActiveNux()
+          onFallback?.()
         }
+      } else {
+        nuxDialogs.dismissActiveNux()
+        onFallback?.()
       }
     }
 
@@ -128,6 +140,7 @@ export function TenMillion() {
       fetching.current = true
       networkRetry(3, fetchUserNumber).catch(() => {
         nuxDialogs.dismissActiveNux()
+        onFallback?.()
       })
     }
   }, [
@@ -136,12 +149,27 @@ export function TenMillion() {
     setUserNumber,
     nuxDialogs.dismissActiveNux,
     nuxDialogs,
+    onFallback,
   ])
 
-  return userNumber ? <TenMillionInner userNumber={userNumber} /> : null
+  return userNumber ? (
+    <TenMillionInner
+      userNumber={userNumber}
+      showTimeout={showTimeout ?? 3e3}
+      onClose={onClose}
+    />
+  ) : null
 }
 
-export function TenMillionInner({userNumber}: {userNumber: number}) {
+export function TenMillionInner({
+  userNumber,
+  showTimeout,
+  onClose: onCloseOuter,
+}: {
+  userNumber: number
+  showTimeout: number
+  onClose?: () => void
+}) {
   const t = useTheme()
   const lightTheme = useTheme('light')
   const {_, i18n} = useLingui()
@@ -184,14 +212,15 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
   React.useEffect(() => {
     const timeout = setTimeout(() => {
       control.open()
-    }, 3e3)
+    }, showTimeout)
     return () => {
       clearTimeout(timeout)
     }
-  }, [control])
+  }, [control, showTimeout])
   const onClose = React.useCallback(() => {
     nuxDialogs.dismissActiveNux()
-  }, [nuxDialogs])
+    onCloseOuter?.()
+  }, [nuxDialogs, onCloseOuter])
 
   /*
    * Actions
@@ -617,9 +646,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
                 a.gap_md,
                 a.pt_xl,
               ]}>
-              <Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
-                <Trans>Brag a little!</Trans>
-              </Text>
+              {gtMobile && (
+                <Text
+                  style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
+                  <Trans>Brag a little!</Trans>
+                </Text>
+              )}
 
               <Button
                 disabled={isLoadingImage}
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
index a13d99eb2..a38c87b68 100644
--- a/src/components/dialogs/nuxs/index.tsx
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -19,31 +19,12 @@ type Context = {
   dismissActiveNux: () => void
 }
 
-/**
- * If we fail to complete a NUX here, it may show again on next reload,
- * or if prefs state updates. If `true`, this fallback ensures that the last
- * shown NUX won't show again, at least for this session.
- *
- * This is temporary, and only needed for the 10Milly dialog rn, since we
- * aren't snoozing that one in device storage.
- */
-let __isSnoozedFallback = false
-
 const queuedNuxs: {
   id: Nux
-  enabled(props: {gate: ReturnType<typeof useGate>}): boolean
-  /**
-   * TEMP only intended for use with the 10Milly dialog rn, since there are no
-   * other NUX dialogs configured
-   */
-  unsafe_disableSnooze: boolean
+  enabled?: (props: {gate: ReturnType<typeof useGate>}) => boolean
 }[] = [
   {
     id: Nux.TenMillionDialog,
-    enabled({gate}) {
-      return gate('ten_million_dialog')
-    },
-    unsafe_disableSnooze: true,
   },
 ]
 
@@ -92,30 +73,23 @@ function Inner() {
   }
 
   React.useEffect(() => {
-    if (__isSnoozedFallback) return
     if (snoozed) return
     if (!nuxs) return
 
-    for (const {id, enabled, unsafe_disableSnooze} of queuedNuxs) {
+    for (const {id, enabled} of queuedNuxs) {
       const nux = nuxs.find(nux => nux.id === id)
 
       // check if completed first
       if (nux && nux.completed) continue
 
       // then check gate (track exposure)
-      if (!enabled({gate})) continue
+      if (enabled && !enabled({gate})) continue
 
       // we have a winner
       setActiveNux(id)
 
-      /**
-       * TEMP only intended for use with the 10Milly dialog rn, since there are no
-       * other NUX dialogs configured
-       */
-      if (!unsafe_disableSnooze) {
-        // immediately snooze for a day
-        snoozeNuxDialog()
-      }
+      // immediately snooze for a day
+      snoozeNuxDialog()
 
       // immediately update remote data (affects next reload)
       upsertNux({
@@ -126,12 +100,6 @@ function Inner() {
         logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
           safeMessage: e.message,
         })
-        /*
-         * TEMP only intended for use with the 10Milly dialog rn
-         */
-        if (unsafe_disableSnooze) {
-          __isSnoozedFallback = true
-        }
       })
 
       break
diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts
index 67f1c2c38..fd1638703 100644
--- a/src/lib/hooks/useIntentHandler.ts
+++ b/src/lib/hooks/useIntentHandler.ts
@@ -97,10 +97,6 @@ export function useComposeIntent() {
           if (part.includes('https://') || part.includes('http://')) {
             return false
           }
-          console.log({
-            part,
-            text: VALID_IMAGE_REGEX.test(part),
-          })
           // We also should just filter out cases that don't have all the info we need
           return VALID_IMAGE_REGEX.test(part)
         })
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index 909b93e6b..7966767d1 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -1,5 +1,3 @@
 export type Gate =
   // Keep this alphabetic please.
-  | 'debug_show_feedcontext'
-  | 'suggested_feeds_interstitial'
-  | 'ten_million_dialog'
+  'debug_show_feedcontext' | 'suggested_feeds_interstitial'
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index 28f29ec78..9bfa82cd2 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -1,6 +1,15 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import Animated from 'react-native-reanimated'
+import Animated, {
+  useAnimatedStyle,
+  useReducedMotion,
+  useSharedValue,
+  withDelay,
+  withRepeat,
+  withSequence,
+  withSpring,
+  withTiming,
+} from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -8,11 +17,11 @@ import {useSession} from '#/state/session'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {Logo} from '#/view/icons/Logo'
+// import {Logo} from '#/view/icons/Logo'
 import {atoms as a, useTheme} from '#/alf'
+import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
 import {Link} from '#/components/Link'
-import {useKawaiiMode} from '../../../state/preferences/kawaii'
 import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
 
 export function HomeHeaderLayout(props: {
@@ -40,7 +49,42 @@ function HomeHeaderLayoutDesktopAndTablet({
   const {hasSession} = useSession()
   const {_} = useLingui()
 
-  const kawaii = useKawaiiMode()
+  // TEMPORARY - REMOVE AFTER MILLY
+  // This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
+  // 🎉
+  const rotate = useSharedValue(0)
+  const reducedMotion = useReducedMotion()
+
+  // Run this a single time on app mount.
+  React.useEffect(() => {
+    if (reducedMotion) return
+
+    // Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
+    rotate.value = withDelay(
+      1000,
+      withRepeat(
+        withSequence(
+          withTiming(10, {duration: 100}),
+          withSpring(0, {
+            mass: 1,
+            damping: 1,
+            stiffness: 200,
+            overshootClamping: false,
+          }),
+        ),
+        2,
+        false,
+      ),
+    )
+  }, [rotate, reducedMotion])
+
+  const animatedStyle = useAnimatedStyle(() => ({
+    transform: [
+      {
+        rotateZ: `${rotate.value}deg`,
+      },
+    ],
+  }))
 
   return (
     <>
@@ -57,21 +101,30 @@ function HomeHeaderLayoutDesktopAndTablet({
             t.atoms.bg,
             t.atoms.border_contrast_low,
             styles.bar,
-            kawaii && {paddingTop: 22, paddingBottom: 16},
           ]}>
-          <View
+          <Animated.View
             style={[
               a.absolute,
               a.inset_0,
               a.pt_lg,
               a.m_auto,
-              kawaii && {paddingTop: 4, paddingBottom: 0},
               {
-                width: kawaii ? 84 : 28,
+                width: 28,
               },
+              animatedStyle,
             ]}>
-            <Logo width={kawaii ? 60 : 28} />
-          </View>
+            <Trigger>
+              {ctx => (
+                <Icon
+                  width={28}
+                  style={{
+                    opacity: ctx.hovered || ctx.pressed ? 0.8 : 1,
+                  }}
+                />
+              )}
+            </Trigger>
+            {/* <Logo width={28} /> */}
+          </Animated.View>
 
           <Link
             to="/feeds"
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index e537abfaa..4817757b5 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -1,6 +1,15 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import Animated from 'react-native-reanimated'
+import Animated, {
+  useAnimatedStyle,
+  useReducedMotion,
+  useSharedValue,
+  withDelay,
+  withRepeat,
+  withSequence,
+  withSpring,
+  withTiming,
+} from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -11,10 +20,11 @@ import {HITSLOP_10} from 'lib/constants'
 import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
-import {Logo} from '#/view/icons/Logo'
+// import {Logo} from '#/view/icons/Logo'
 import {atoms} from '#/alf'
 import {useTheme} from '#/alf'
 import {atoms as a} from '#/alf'
+import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
 import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
 import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
@@ -39,6 +49,43 @@ export function HomeHeaderLayoutMobile({
     setDrawerOpen(true)
   }, [setDrawerOpen])
 
+  // TEMPORARY - REMOVE AFTER MILLY
+  // This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
+  // 🎉
+  const rotate = useSharedValue(0)
+  const reducedMotion = useReducedMotion()
+
+  // Run this a single time on app mount.
+  React.useEffect(() => {
+    if (reducedMotion) return
+
+    // Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
+    rotate.value = withDelay(
+      1000,
+      withRepeat(
+        withSequence(
+          withTiming(10, {duration: 100}),
+          withSpring(0, {
+            mass: 1,
+            damping: 1,
+            stiffness: 200,
+            overshootClamping: false,
+          }),
+        ),
+        2,
+        false,
+      ),
+    )
+  }, [rotate, reducedMotion])
+
+  const animatedStyle = useAnimatedStyle(() => ({
+    transform: [
+      {
+        rotateZ: `${rotate.value}deg`,
+      },
+    ],
+  }))
+
   return (
     <Animated.View
       style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
@@ -59,9 +106,19 @@ export function HomeHeaderLayoutMobile({
             <Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
           </TouchableOpacity>
         </View>
-        <View>
-          <Logo width={30} />
-        </View>
+        <Animated.View style={animatedStyle}>
+          <Trigger>
+            {ctx => (
+              <Icon
+                width={28}
+                style={{
+                  opacity: ctx.pressed ? 0.8 : 1,
+                }}
+              />
+            )}
+          </Trigger>
+          {/* <Logo width={30} /> */}
+        </Animated.View>
         <View
           style={[
             atoms.flex_row,