diff options
author | Eric Bailey <git@esb.lol> | 2024-09-16 16:52:28 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-16 14:52:28 -0700 |
commit | b69fd23456485d22c24b51da833d2707c718d61e (patch) | |
tree | beb5f1d17aa9fb6eae314a6714c4913be39dcf5a | |
parent | 8daf6b78688ca20326a79fa9c7ca1cbd945786e1 (diff) | |
download | voidsky-b69fd23456485d22c24b51da833d2707c718d61e.tar.zst |
Milly tweaks (#5365)
Co-authored-by: Hailey <me@haileyok.com>
-rw-r--r-- | src/components/Prompt.tsx | 4 | ||||
-rw-r--r-- | src/components/dialogs/nuxs/TenMillion/Trigger.tsx | 129 | ||||
-rw-r--r-- | src/components/dialogs/nuxs/TenMillion/index.tsx | 50 | ||||
-rw-r--r-- | src/components/dialogs/nuxs/index.tsx | 42 | ||||
-rw-r--r-- | src/lib/hooks/useIntentHandler.ts | 4 | ||||
-rw-r--r-- | src/lib/statsig/gates.ts | 4 | ||||
-rw-r--r-- | src/view/com/home/HomeHeaderLayout.web.tsx | 73 | ||||
-rw-r--r-- | src/view/com/home/HomeHeaderLayoutMobile.tsx | 67 |
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, |