parent
8daf6b7868
commit
b69fd23456
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue