Add prefs methods, hook up to Discover banner
This commit is contained in:
@@ -35,6 +35,7 @@ module.exports = {
|
|||||||
'Admonition',
|
'Admonition',
|
||||||
'Admonition.Admonition',
|
'Admonition.Admonition',
|
||||||
'Toast.Action',
|
'Toast.Action',
|
||||||
|
'toast.Action',
|
||||||
'AgeAssuranceAdmonition',
|
'AgeAssuranceAdmonition',
|
||||||
'Span',
|
'Span',
|
||||||
'StackedButton',
|
'StackedButton',
|
||||||
|
|||||||
+18
-22
@@ -5,12 +5,15 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useTrendingSettings} from '#/state/preferences/trending'
|
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||||
import {atoms as a, useLayoutBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useLayoutBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as CloseIcon} from '#/components/icons/Times'
|
import {DotGrid_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
||||||
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
||||||
import * as Prompt from '#/components/Prompt'
|
|
||||||
import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide'
|
import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide'
|
||||||
|
import {
|
||||||
|
LiveEventFeedOptionsMenu,
|
||||||
|
useDialogControl,
|
||||||
|
} from '#/features/liveEvents/components/LiveEventFeedOptionsMenu'
|
||||||
import {useLiveEvents} from '#/features/liveEvents/context'
|
import {useLiveEvents} from '#/features/liveEvents/context'
|
||||||
import {device, useStorage} from '#/storage'
|
import {useLiveEventPreferences} from '#/features/liveEvents/preferences'
|
||||||
|
|
||||||
export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -18,14 +21,17 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
|||||||
const events = useLiveEvents()
|
const events = useLiveEvents()
|
||||||
const {rightNavVisible} = useLayoutBreakpoints()
|
const {rightNavVisible} = useLayoutBreakpoints()
|
||||||
const {trendingDisabled} = useTrendingSettings()
|
const {trendingDisabled} = useTrendingSettings()
|
||||||
const promptControl = Prompt.usePromptControl()
|
const optionsMenuControl = useDialogControl()
|
||||||
const [bannerDisabled, setBannerDisabled] = useStorage(device, [
|
const {data, isLoading} = useLiveEventPreferences()
|
||||||
'liveEventFeedsBannerDisabled',
|
|
||||||
])
|
|
||||||
|
|
||||||
|
// user prefs should be loaded by now, but for TS-sake
|
||||||
|
if (isLoading) return null
|
||||||
|
|
||||||
|
const {hideAllFeeds, hiddenFeedIds} = data
|
||||||
const feed = events.feeds.at(0)
|
const feed = events.feeds.at(0)
|
||||||
|
const feedHidden = feed?.id ? hiddenFeedIds.includes(feed?.id || '') : false
|
||||||
|
|
||||||
if (!feed || bannerDisabled) {
|
if (!feed || hideAllFeeds || feedHidden) {
|
||||||
if (!rightNavVisible && !trendingDisabled) {
|
if (!rightNavVisible && !trendingDisabled) {
|
||||||
// only show trending on mobile when live event banner is not shown
|
// only show trending on mobile when live event banner is not shown
|
||||||
return <TrendingInterstitial />
|
return <TrendingInterstitial />
|
||||||
@@ -52,7 +58,7 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
|||||||
shape="round"
|
shape="round"
|
||||||
style={[a.absolute, a.z_10, {top: 6, right: 6}]}
|
style={[a.absolute, a.z_10, {top: 6, right: 6}]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
promptControl.open()
|
optionsMenuControl.open()
|
||||||
}}>
|
}}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<>
|
<>
|
||||||
@@ -67,8 +73,8 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<CloseIcon
|
<EllipsisIcon
|
||||||
size="xs"
|
size="sm"
|
||||||
fill={
|
fill={
|
||||||
image.textColor === 'light'
|
image.textColor === 'light'
|
||||||
? t.palette.white
|
? t.palette.white
|
||||||
@@ -82,17 +88,7 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Prompt.Basic
|
<LiveEventFeedOptionsMenu feed={feed} control={optionsMenuControl} />
|
||||||
control={promptControl}
|
|
||||||
title={_(msg`Disable the live event banner?`)}
|
|
||||||
description={_(
|
|
||||||
msg`Live events appear occasionally when something exciting is happening that we think you might like. You can always re-enable this banner from your Content & Media settings page.`,
|
|
||||||
)}
|
|
||||||
confirmButtonCta={_(msg`Disable banner`)}
|
|
||||||
onConfirm={() => {
|
|
||||||
setBannerDisabled(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import {useRef} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
|
import {atoms as a, web} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as toast from '#/components/Toast'
|
||||||
|
import {Span, Text} from '#/components/Typography'
|
||||||
|
import {
|
||||||
|
type LiveEventPreferencesAction,
|
||||||
|
useUpdateLiveEventPreferences,
|
||||||
|
} from '#/features/liveEvents/preferences'
|
||||||
|
import {type LiveEventFeed} from '#/features/liveEvents/types'
|
||||||
|
|
||||||
|
export {useDialogControl} from '#/components/Dialog'
|
||||||
|
|
||||||
|
export function LiveEventFeedOptionsMenu({
|
||||||
|
control,
|
||||||
|
feed,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogControlProps
|
||||||
|
feed: LiveEventFeed
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
return (
|
||||||
|
<Dialog.Outer control={control}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
label={_(msg`Configure live event banner`)}
|
||||||
|
style={[web({maxWidth: 400})]}>
|
||||||
|
<Inner control={control} feed={feed} />
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inner({
|
||||||
|
control,
|
||||||
|
feed,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogControlProps
|
||||||
|
feed: LiveEventFeed
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const canUndo = useRef(true)
|
||||||
|
const undoAction = useRef<LiveEventPreferencesAction | null>(null)
|
||||||
|
const {
|
||||||
|
isPending,
|
||||||
|
mutate: update,
|
||||||
|
error: rawError,
|
||||||
|
variables,
|
||||||
|
} = useUpdateLiveEventPreferences({
|
||||||
|
onSuccess() {
|
||||||
|
toast.show(
|
||||||
|
<toast.Outer>
|
||||||
|
<toast.Icon />
|
||||||
|
<toast.Text>
|
||||||
|
<Trans>Your live event preferences have been updated.</Trans>
|
||||||
|
</toast.Text>
|
||||||
|
{canUndo.current && undoAction.current && (
|
||||||
|
<toast.Action
|
||||||
|
label={_(msg`Undo`)}
|
||||||
|
onPress={() => {
|
||||||
|
if (undoAction.current) {
|
||||||
|
update(undoAction.current)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Trans>Undo</Trans>
|
||||||
|
</toast.Action>
|
||||||
|
)}
|
||||||
|
</toast.Outer>,
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// must protect to avoid closing an already closed dialog
|
||||||
|
if (canUndo.current) {
|
||||||
|
canUndo.current = false
|
||||||
|
control.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const cleanError = useCleanError()
|
||||||
|
const error = rawError ? cleanError(rawError) : undefined
|
||||||
|
|
||||||
|
const isHidingFeed = variables?.type === 'hideFeed' && isPending
|
||||||
|
const isHidingAllFeeds = variables?.type === 'toggleHideAllFeeds' && isPending
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.gap_lg]}>
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.text_2xl, a.font_semi_bold, a.leading_snug]}>
|
||||||
|
<Trans>Live event options</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Live events appear occasionally when something exciting is
|
||||||
|
happening. If you'd like, you can hide this particular event, or all
|
||||||
|
events.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
If you choose to hide all events, you can always re-enable them from{' '}
|
||||||
|
<Span style={[a.font_semi_bold]}>Settings → Content & Media</Span>.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Hide this event`)}
|
||||||
|
size="large"
|
||||||
|
color="primary_subtle"
|
||||||
|
onPress={() => {
|
||||||
|
update({type: 'hideFeed', id: feed.id})
|
||||||
|
undoAction.current = {type: 'unhideFeed', id: feed.id}
|
||||||
|
}}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Hide this event</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
{isHidingFeed && <ButtonIcon icon={Loader} />}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Hide all events`)}
|
||||||
|
size="large"
|
||||||
|
color="secondary"
|
||||||
|
onPress={() => {
|
||||||
|
update({type: 'toggleHideAllFeeds'})
|
||||||
|
undoAction.current = {type: 'toggleHideAllFeeds'}
|
||||||
|
}}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Hide all events</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
{isHidingAllFeeds && <ButtonIcon icon={Loader} />}
|
||||||
|
</Button>
|
||||||
|
{isNative && (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Cancel`)}
|
||||||
|
size="large"
|
||||||
|
color="secondary_inverted"
|
||||||
|
onPress={() => control.close()}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Admonition type="error">
|
||||||
|
{error.clean || error.raw || _(msg`An unknown error occurred.`)}
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import {useEffect} from 'react'
|
||||||
|
import {type Agent} from '@atproto/api'
|
||||||
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {
|
||||||
|
preferencesQueryKey,
|
||||||
|
usePreferencesQuery,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
|
export type LiveEventPreferencesAction = Parameters<
|
||||||
|
Agent['updateLiveEventPreferences']
|
||||||
|
>[0]
|
||||||
|
|
||||||
|
export function useLiveEventPreferences() {
|
||||||
|
const query = usePreferencesQuery()
|
||||||
|
useWebOnlyDebugLiveEventPreferences()
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
data: query.data?.liveEventPreferences || {
|
||||||
|
hideAllFeeds: false,
|
||||||
|
hiddenFeedIds: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useWebOnlyDebugLiveEventPreferences() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isWeb && typeof window !== 'undefined') {
|
||||||
|
// @ts-ignore
|
||||||
|
window.__updateLiveEventPreferences = async (
|
||||||
|
action: LiveEventPreferencesAction,
|
||||||
|
) => {
|
||||||
|
await agent.updateLiveEventPreferences(action)
|
||||||
|
// triggers a refetch
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: preferencesQueryKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [agent, queryClient])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateLiveEventPreferences(props?: {
|
||||||
|
onSuccess?: () => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useMutation<void, Error, LiveEventPreferencesAction>({
|
||||||
|
onError: props?.onError,
|
||||||
|
onSuccess: props?.onSuccess,
|
||||||
|
mutationFn: async action => {
|
||||||
|
const updated = await agent.updateLiveEventPreferences(action)
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case 'hideFeed': {
|
||||||
|
logger.metric('liveEventFeeds:hideFeed', {feedId: action.id})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'unhideFeed': {
|
||||||
|
logger.metric('liveEventFeeds:unhideFeed', {feedId: action.id})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'toggleHideAllFeeds': {
|
||||||
|
if (updated?.hideAllFeeds) {
|
||||||
|
logger.metric('liveEventFeed:hideAllFeeds', {})
|
||||||
|
} else {
|
||||||
|
logger.metric('liveEventFeed:unhideAllFeeds', {})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggers a refetch
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: preferencesQueryKey,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export type LiveEventFeedImage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type LiveEventFeed = {
|
export type LiveEventFeed = {
|
||||||
|
id: string
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
images: Record<LiveEventFeedImageLayout, LiveEventFeedImage>
|
images: Record<LiveEventFeedImageLayout, LiveEventFeedImage>
|
||||||
|
|||||||
@@ -788,4 +788,9 @@ export type MetricEvents = {
|
|||||||
}
|
}
|
||||||
// user pressed the remove all data button
|
// user pressed the remove all data button
|
||||||
'contacts:settings:removeData': {}
|
'contacts:settings:removeData': {}
|
||||||
|
|
||||||
|
'liveEventFeeds:hideFeed': {feedId: string}
|
||||||
|
'liveEventFeeds:unhideFeed': {feedId: string}
|
||||||
|
'liveEventFeed:hideAllFeeds': {}
|
||||||
|
'liveEventFeed:unhideAllFeeds': {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ export type Device = {
|
|||||||
activitySubscriptionsNudged?: boolean
|
activitySubscriptionsNudged?: boolean
|
||||||
threadgateNudged?: boolean
|
threadgateNudged?: boolean
|
||||||
|
|
||||||
liveEventFeedsBannerDisabled?: boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Policy update overlays. New IDs are required for each new announcement.
|
* Policy update overlays. New IDs are required for each new announcement.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user