Add feed banner
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
import {useMemo} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||||
|
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||||
|
import {atoms as a, select, useTheme} from '#/alf'
|
||||||
|
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||||
|
import {Button} from '#/components/Button'
|
||||||
|
import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
|
||||||
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
import {Link} from '#/components/Link'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function useInternalState() {
|
||||||
|
const {isReady, isDeclaredUnderage, isAgeRestricted, lastInitiatedAt} =
|
||||||
|
useAgeAssurance()
|
||||||
|
const {nux} = useNux(Nux.AgeAssuranceDismissibleHeaderButton)
|
||||||
|
const {mutate: save, variables} = useSaveNux()
|
||||||
|
const hidden = !!variables
|
||||||
|
|
||||||
|
const visible = useMemo(() => {
|
||||||
|
if (!isReady) return false
|
||||||
|
if (isDeclaredUnderage) return false
|
||||||
|
if (!isAgeRestricted) return false
|
||||||
|
if (lastInitiatedAt) return false
|
||||||
|
if (hidden) return false
|
||||||
|
if (nux && nux.completed) return false
|
||||||
|
return true
|
||||||
|
}, [
|
||||||
|
isReady,
|
||||||
|
isDeclaredUnderage,
|
||||||
|
isAgeRestricted,
|
||||||
|
lastInitiatedAt,
|
||||||
|
hidden,
|
||||||
|
nux,
|
||||||
|
])
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
save({
|
||||||
|
id: Nux.AgeAssuranceDismissibleHeaderButton,
|
||||||
|
completed: true,
|
||||||
|
data: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {visible, close}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgeAssuranceDismissibleFeedBanner() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {visible, close} = useInternalState()
|
||||||
|
const copy = useAgeAssuranceCopy()
|
||||||
|
|
||||||
|
if (!visible) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.px_lg,
|
||||||
|
{
|
||||||
|
paddingVertical: 10,
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.primary_25,
|
||||||
|
dark: t.palette.primary_25,
|
||||||
|
dim: t.palette.primary_25,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Link
|
||||||
|
label={_(msg`Learn more about age assurance`)}
|
||||||
|
to="/settings/account"
|
||||||
|
onPress={close}
|
||||||
|
style={[a.w_full, a.justify_between, a.align_center, a.gap_md]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.primary_100,
|
||||||
|
dark: t.palette.primary_100,
|
||||||
|
dim: t.palette.primary_100,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Shield size="lg" />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
{
|
||||||
|
paddingRight: 40,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text style={[a.leading_snug]}>{copy.banner}</Text>
|
||||||
|
</View>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={_(msg`Don't show again`)}
|
||||||
|
size="small"
|
||||||
|
onPress={close}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
{
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
paddingRight: a.px_md.paddingLeft,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<X
|
||||||
|
width={20}
|
||||||
|
fill={select(t.name, {
|
||||||
|
light: t.palette.primary_600,
|
||||||
|
dark: t.palette.primary_600,
|
||||||
|
dim: t.palette.primary_600,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import {useMemo} from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
|
||||||
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
|
||||||
import {atoms as a, select, useTheme} from '#/alf'
|
|
||||||
import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
|
|
||||||
import {Link} from '#/components/Link'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
export function useInternalState() {
|
|
||||||
const {isReady, isDeclaredUnderage, isAgeRestricted, lastInitiatedAt} =
|
|
||||||
useAgeAssurance()
|
|
||||||
const {nux} = useNux(Nux.AgeAssuranceDismissibleHeaderButton)
|
|
||||||
const {mutate: save, variables} = useSaveNux()
|
|
||||||
const hidden = !!variables
|
|
||||||
|
|
||||||
const visible = useMemo(() => {
|
|
||||||
if (!isReady) return false
|
|
||||||
if (isDeclaredUnderage) return false
|
|
||||||
if (!isAgeRestricted) return false
|
|
||||||
if (lastInitiatedAt) return false
|
|
||||||
if (hidden) return false
|
|
||||||
if (nux && nux.completed) return false
|
|
||||||
return true
|
|
||||||
}, [
|
|
||||||
isReady,
|
|
||||||
isDeclaredUnderage,
|
|
||||||
isAgeRestricted,
|
|
||||||
lastInitiatedAt,
|
|
||||||
hidden,
|
|
||||||
nux,
|
|
||||||
])
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
save({
|
|
||||||
id: Nux.AgeAssuranceDismissibleHeaderButton,
|
|
||||||
completed: true,
|
|
||||||
data: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {visible, close}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AgeAssuranceDismissibleHeaderButton() {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {visible, close} = useInternalState()
|
|
||||||
|
|
||||||
if (!visible) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
label={_(msg`Learn more about age assurance`)}
|
|
||||||
to="/settings/account"
|
|
||||||
onPress={close}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_xs,
|
|
||||||
a.px_sm,
|
|
||||||
a.pr_sm,
|
|
||||||
a.rounded_full,
|
|
||||||
{
|
|
||||||
paddingVertical: 6,
|
|
||||||
backgroundColor: select(t.name, {
|
|
||||||
light: t.palette.primary_100,
|
|
||||||
dark: t.palette.primary_100,
|
|
||||||
dim: t.palette.primary_100,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Shield size="sm" />
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.font_bold,
|
|
||||||
a.leading_snug,
|
|
||||||
{
|
|
||||||
color: select(t.name, {
|
|
||||||
light: t.palette.primary_800,
|
|
||||||
dark: t.palette.primary_800,
|
|
||||||
dim: t.palette.primary_800,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Trans>Age Assurance</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,9 @@ export function useAgeAssuranceCopy() {
|
|||||||
notice: _(
|
notice: _(
|
||||||
msg`The laws in your location require that you verify your age before accessing certain features on Bluesky like adult content and direct messaging.`,
|
msg`The laws in your location require that you verify your age before accessing certain features on Bluesky like adult content and direct messaging.`,
|
||||||
),
|
),
|
||||||
|
banner: _(
|
||||||
|
msg`The laws in your location require you verify your age. Tap to learn more.`,
|
||||||
|
),
|
||||||
chatsInfoText: _(
|
chatsInfoText: _(
|
||||||
msg`Don't worry! All existing messages and settings are saved and will be available after you've been verified to be 18 or older.`,
|
msg`Don't worry! All existing messages and settings are saved and will be available after you've been verified to be 18 or older.`,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function useIsAgeAssuranceEnabled() {
|
|||||||
const {geolocation} = useGeolocation()
|
const {geolocation} = useGeolocation()
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return gate('age_assurance') && !!geolocation?.isAgeRestrictedGeo
|
const enabled = __DEV__ || gate('age_assurance')
|
||||||
|
return enabled && !!geolocation?.isAgeRestrictedGeo
|
||||||
}, [geolocation, gate])
|
}, [geolocation, gate])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,16 @@ import {
|
|||||||
import {useLiveNowConfig} from '#/state/service-config'
|
import {useLiveNowConfig} from '#/state/service-config'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
|
import {useSelectedFeed} from '#/state/shell/selected-feed'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||||
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||||
import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
|
import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
|
||||||
|
import {
|
||||||
|
AgeAssuranceDismissibleFeedBanner,
|
||||||
|
useInternalState as useAgeAssuranceBannerState,
|
||||||
|
} from '#/components/ageAssurance/AgeAssuranceDismissibleFeedBanner'
|
||||||
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
|
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
|
||||||
import {
|
import {
|
||||||
PostFeedVideoGridRow,
|
PostFeedVideoGridRow,
|
||||||
@@ -131,6 +136,10 @@ type FeedRow =
|
|||||||
type: 'showLessFollowup'
|
type: 'showLessFollowup'
|
||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'ageAssuranceBanner'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
export function getItemsForFeedback(feedRow: FeedRow): {
|
export function getItemsForFeedback(feedRow: FeedRow): {
|
||||||
item: FeedPostSliceItem
|
item: FeedPostSliceItem
|
||||||
@@ -335,6 +344,14 @@ let PostFeed = ({
|
|||||||
|
|
||||||
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
|
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
|
||||||
|
|
||||||
|
const ageAssuranceBannerState = useAgeAssuranceBannerState()
|
||||||
|
const selectedFeed = useSelectedFeed()
|
||||||
|
/**
|
||||||
|
* Cached value of whether the current feed was selected at startup. We don't
|
||||||
|
* want this to update when user swipes.
|
||||||
|
*/
|
||||||
|
const isCurrentFeedAtStartupSelected = useRef(selectedFeed === feed)
|
||||||
|
|
||||||
const feedItems: FeedRow[] = useMemo(() => {
|
const feedItems: FeedRow[] = useMemo(() => {
|
||||||
// wraps a slice item, and replaces it with a showLessFollowup item
|
// wraps a slice item, and replaces it with a showLessFollowup item
|
||||||
// if the user has pressed show less on it
|
// if the user has pressed show less on it
|
||||||
@@ -450,6 +467,21 @@ let PostFeed = ({
|
|||||||
type: 'interstitialProgressGuide',
|
type: 'interstitialProgressGuide',
|
||||||
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* Only insert if Discover was the last selected feed at
|
||||||
|
* startup, the progress guide isn't shown, and the
|
||||||
|
* banner is eligible to be shown.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
isCurrentFeedAtStartupSelected.current &&
|
||||||
|
ageAssuranceBannerState.visible
|
||||||
|
) {
|
||||||
|
arr.push({
|
||||||
|
type: 'ageAssuranceBanner',
|
||||||
|
key: 'ageAssuranceBanner-' + sliceIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!rightNavVisible && !trendingDisabled) {
|
if (!rightNavVisible && !trendingDisabled) {
|
||||||
arr.push({
|
arr.push({
|
||||||
@@ -478,6 +510,17 @@ let PostFeed = ({
|
|||||||
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* Only insert if this feed was the last selected feed at
|
||||||
|
* startup and the banner is eligible to be shown.
|
||||||
|
*/
|
||||||
|
if (isCurrentFeedAtStartupSelected.current) {
|
||||||
|
arr.push({
|
||||||
|
type: 'ageAssuranceBanner',
|
||||||
|
key: 'ageAssuranceBanner-' + sliceIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +623,7 @@ let PostFeed = ({
|
|||||||
isVideoFeed,
|
isVideoFeed,
|
||||||
areVideoFeedsEnabled,
|
areVideoFeedsEnabled,
|
||||||
hasPressedShowLessUris,
|
hasPressedShowLessUris,
|
||||||
|
ageAssuranceBannerState,
|
||||||
])
|
])
|
||||||
|
|
||||||
// events
|
// events
|
||||||
@@ -666,6 +710,8 @@ let PostFeed = ({
|
|||||||
return <SuggestedFollows feed={feed} />
|
return <SuggestedFollows feed={feed} />
|
||||||
} else if (row.type === 'interstitialProgressGuide') {
|
} else if (row.type === 'interstitialProgressGuide') {
|
||||||
return <ProgressGuide />
|
return <ProgressGuide />
|
||||||
|
} else if (row.type === 'ageAssuranceBanner') {
|
||||||
|
return <AgeAssuranceDismissibleFeedBanner />
|
||||||
} else if (row.type === 'interstitialTrending') {
|
} else if (row.type === 'interstitialTrending') {
|
||||||
return <TrendingInterstitial />
|
return <TrendingInterstitial />
|
||||||
} else if (row.type === 'interstitialTrendingVideos') {
|
} else if (row.type === 'interstitialTrendingVideos') {
|
||||||
|
|||||||
Reference in New Issue
Block a user