Add prefs methods, hook up to Discover banner
This commit is contained in:
@@ -35,6 +35,7 @@ module.exports = {
|
||||
'Admonition',
|
||||
'Admonition.Admonition',
|
||||
'Toast.Action',
|
||||
'toast.Action',
|
||||
'AgeAssuranceAdmonition',
|
||||
'Span',
|
||||
'StackedButton',
|
||||
|
||||
+18
-22
@@ -5,12 +5,15 @@ import {useLingui} from '@lingui/react'
|
||||
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||
import {atoms as a, useLayoutBreakpoints, useTheme} from '#/alf'
|
||||
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 * as Prompt from '#/components/Prompt'
|
||||
import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide'
|
||||
import {
|
||||
LiveEventFeedOptionsMenu,
|
||||
useDialogControl,
|
||||
} from '#/features/liveEvents/components/LiveEventFeedOptionsMenu'
|
||||
import {useLiveEvents} from '#/features/liveEvents/context'
|
||||
import {device, useStorage} from '#/storage'
|
||||
import {useLiveEventPreferences} from '#/features/liveEvents/preferences'
|
||||
|
||||
export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
||||
const t = useTheme()
|
||||
@@ -18,14 +21,17 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
||||
const events = useLiveEvents()
|
||||
const {rightNavVisible} = useLayoutBreakpoints()
|
||||
const {trendingDisabled} = useTrendingSettings()
|
||||
const promptControl = Prompt.usePromptControl()
|
||||
const [bannerDisabled, setBannerDisabled] = useStorage(device, [
|
||||
'liveEventFeedsBannerDisabled',
|
||||
])
|
||||
const optionsMenuControl = useDialogControl()
|
||||
const {data, isLoading} = useLiveEventPreferences()
|
||||
|
||||
// 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 feedHidden = feed?.id ? hiddenFeedIds.includes(feed?.id || '') : false
|
||||
|
||||
if (!feed || bannerDisabled) {
|
||||
if (!feed || hideAllFeeds || feedHidden) {
|
||||
if (!rightNavVisible && !trendingDisabled) {
|
||||
// only show trending on mobile when live event banner is not shown
|
||||
return <TrendingInterstitial />
|
||||
@@ -52,7 +58,7 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
||||
shape="round"
|
||||
style={[a.absolute, a.z_10, {top: 6, right: 6}]}
|
||||
onPress={() => {
|
||||
promptControl.open()
|
||||
optionsMenuControl.open()
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<>
|
||||
@@ -67,8 +73,8 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<CloseIcon
|
||||
size="xs"
|
||||
<EllipsisIcon
|
||||
size="sm"
|
||||
fill={
|
||||
image.textColor === 'light'
|
||||
? t.palette.white
|
||||
@@ -82,17 +88,7 @@ export function DiscoverFeedLiveEventFeedsAndTrendingBanner() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Prompt.Basic
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
<LiveEventFeedOptionsMenu feed={feed} control={optionsMenuControl} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
images: Record<LiveEventFeedImageLayout, LiveEventFeedImage>
|
||||
|
||||
@@ -788,4 +788,9 @@ export type MetricEvents = {
|
||||
}
|
||||
// user pressed the remove all data button
|
||||
'contacts:settings:removeData': {}
|
||||
|
||||
'liveEventFeeds:hideFeed': {feedId: string}
|
||||
'liveEventFeeds:unhideFeed': {feedId: string}
|
||||
'liveEventFeed:hideAllFeeds': {}
|
||||
'liveEventFeed:unhideAllFeeds': {}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ export type Device = {
|
||||
activitySubscriptionsNudged?: boolean
|
||||
threadgateNudged?: boolean
|
||||
|
||||
liveEventFeedsBannerDisabled?: boolean
|
||||
|
||||
/**
|
||||
* Policy update overlays. New IDs are required for each new announcement.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user