Milly tweaks (#5365)

Co-authored-by: Hailey <me@haileyok.com>
zio/dev^2
Eric Bailey 2024-09-16 16:52:28 -05:00 committed by GitHub
parent 8daf6b7868
commit b69fd23456
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 304 additions and 69 deletions

View File

@ -59,7 +59,9 @@ export function Outer({
export function TitleText({children}: React.PropsWithChildren<{}>) { export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context) const {titleId} = React.useContext(Context)
return ( 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} {children}
</Text> </Text>
) )

View File

@ -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>
)
}

View File

@ -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 agent = useAgent()
const nuxDialogs = useNuxDialogContext() const nuxDialogs = useNuxDialogContext()
const [userNumber, setUserNumber] = React.useState<number>(0) const [userNumber, setUserNumber] = React.useState<number>(0)
@ -120,7 +128,11 @@ export function TenMillion() {
} else { } else {
// should be rare // should be rare
nuxDialogs.dismissActiveNux() nuxDialogs.dismissActiveNux()
onFallback?.()
} }
} else {
nuxDialogs.dismissActiveNux()
onFallback?.()
} }
} }
@ -128,6 +140,7 @@ export function TenMillion() {
fetching.current = true fetching.current = true
networkRetry(3, fetchUserNumber).catch(() => { networkRetry(3, fetchUserNumber).catch(() => {
nuxDialogs.dismissActiveNux() nuxDialogs.dismissActiveNux()
onFallback?.()
}) })
} }
}, [ }, [
@ -136,12 +149,27 @@ export function TenMillion() {
setUserNumber, setUserNumber,
nuxDialogs.dismissActiveNux, nuxDialogs.dismissActiveNux,
nuxDialogs, 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 t = useTheme()
const lightTheme = useTheme('light') const lightTheme = useTheme('light')
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
@ -184,14 +212,15 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
React.useEffect(() => { React.useEffect(() => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
control.open() control.open()
}, 3e3) }, showTimeout)
return () => { return () => {
clearTimeout(timeout) clearTimeout(timeout)
} }
}, [control]) }, [control, showTimeout])
const onClose = React.useCallback(() => { const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux() nuxDialogs.dismissActiveNux()
}, [nuxDialogs]) onCloseOuter?.()
}, [nuxDialogs, onCloseOuter])
/* /*
* Actions * Actions
@ -617,9 +646,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
a.gap_md, a.gap_md,
a.pt_xl, a.pt_xl,
]}> ]}>
<Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}> {gtMobile && (
<Trans>Brag a little!</Trans> <Text
</Text> style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
<Trans>Brag a little!</Trans>
</Text>
)}
<Button <Button
disabled={isLoadingImage} disabled={isLoadingImage}

View File

@ -19,31 +19,12 @@ type Context = {
dismissActiveNux: () => void 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: { const queuedNuxs: {
id: Nux id: Nux
enabled(props: {gate: ReturnType<typeof useGate>}): boolean 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
}[] = [ }[] = [
{ {
id: Nux.TenMillionDialog, id: Nux.TenMillionDialog,
enabled({gate}) {
return gate('ten_million_dialog')
},
unsafe_disableSnooze: true,
}, },
] ]
@ -92,30 +73,23 @@ function Inner() {
} }
React.useEffect(() => { React.useEffect(() => {
if (__isSnoozedFallback) return
if (snoozed) return if (snoozed) return
if (!nuxs) 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) const nux = nuxs.find(nux => nux.id === id)
// check if completed first // check if completed first
if (nux && nux.completed) continue if (nux && nux.completed) continue
// then check gate (track exposure) // then check gate (track exposure)
if (!enabled({gate})) continue if (enabled && !enabled({gate})) continue
// we have a winner // we have a winner
setActiveNux(id) setActiveNux(id)
/** // immediately snooze for a day
* TEMP only intended for use with the 10Milly dialog rn, since there are no snoozeNuxDialog()
* other NUX dialogs configured
*/
if (!unsafe_disableSnooze) {
// immediately snooze for a day
snoozeNuxDialog()
}
// immediately update remote data (affects next reload) // immediately update remote data (affects next reload)
upsertNux({ upsertNux({
@ -126,12 +100,6 @@ function Inner() {
logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, { logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
safeMessage: e.message, safeMessage: e.message,
}) })
/*
* TEMP only intended for use with the 10Milly dialog rn
*/
if (unsafe_disableSnooze) {
__isSnoozedFallback = true
}
}) })
break break

View File

@ -97,10 +97,6 @@ export function useComposeIntent() {
if (part.includes('https://') || part.includes('http://')) { if (part.includes('https://') || part.includes('http://')) {
return false 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 // We also should just filter out cases that don't have all the info we need
return VALID_IMAGE_REGEX.test(part) return VALID_IMAGE_REGEX.test(part)
}) })

View File

@ -1,5 +1,3 @@
export type Gate = export type Gate =
// Keep this alphabetic please. // Keep this alphabetic please.
| 'debug_show_feedcontext' 'debug_show_feedcontext' | 'suggested_feeds_interstitial'
| 'suggested_feeds_interstitial'
| 'ten_million_dialog'

View File

@ -1,6 +1,15 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' 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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@ -8,11 +17,11 @@ import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout' import {useShellLayout} from '#/state/shell/shell-layout'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform' import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 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 {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 {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
import {useKawaiiMode} from '../../../state/preferences/kawaii'
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
export function HomeHeaderLayout(props: { export function HomeHeaderLayout(props: {
@ -40,7 +49,42 @@ function HomeHeaderLayoutDesktopAndTablet({
const {hasSession} = useSession() const {hasSession} = useSession()
const {_} = useLingui() 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 ( return (
<> <>
@ -57,21 +101,30 @@ function HomeHeaderLayoutDesktopAndTablet({
t.atoms.bg, t.atoms.bg,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
styles.bar, styles.bar,
kawaii && {paddingTop: 22, paddingBottom: 16},
]}> ]}>
<View <Animated.View
style={[ style={[
a.absolute, a.absolute,
a.inset_0, a.inset_0,
a.pt_lg, a.pt_lg,
a.m_auto, a.m_auto,
kawaii && {paddingTop: 4, paddingBottom: 0},
{ {
width: kawaii ? 84 : 28, width: 28,
}, },
animatedStyle,
]}> ]}>
<Logo width={kawaii ? 60 : 28} /> <Trigger>
</View> {ctx => (
<Icon
width={28}
style={{
opacity: ctx.hovered || ctx.pressed ? 0.8 : 1,
}}
/>
)}
</Trigger>
{/* <Logo width={28} /> */}
</Animated.View>
<Link <Link
to="/feeds" to="/feeds"

View File

@ -1,6 +1,15 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' 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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@ -11,10 +20,11 @@ import {HITSLOP_10} from 'lib/constants'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform' import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Logo} from '#/view/icons/Logo' // import {Logo} from '#/view/icons/Logo'
import {atoms} from '#/alf' import {atoms} from '#/alf'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {atoms as a} 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 {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
@ -39,6 +49,43 @@ export function HomeHeaderLayoutMobile({
setDrawerOpen(true) setDrawerOpen(true)
}, [setDrawerOpen]) }, [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 ( return (
<Animated.View <Animated.View
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} 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} /> <Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View> <Animated.View style={animatedStyle}>
<Logo width={30} /> <Trigger>
</View> {ctx => (
<Icon
width={28}
style={{
opacity: ctx.pressed ? 0.8 : 1,
}}
/>
)}
</Trigger>
{/* <Logo width={30} /> */}
</Animated.View>
<View <View
style={[ style={[
atoms.flex_row, atoms.flex_row,