From b57eef7050322859805d4af3433dc423d16a5a08 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 31 Jul 2025 15:46:32 -0500 Subject: [PATCH] Add blocking announcement dialog feature --- src/components/Dialog/context.ts | 1 + src/components/Dialog/index.tsx | 2 + src/components/Dialog/index.web.tsx | 15 +- src/components/Dialog/types.ts | 24 +- .../dialogs/BlockingAlertDialog.tsx | 211 ++++++++++++++++++ .../PolicyUpdate20250801.tsx | 141 ++++++++++++ .../dialogs/BlockingAnnouncements/common.tsx | 94 ++++++++ .../dialogs/BlockingAnnouncements/index.tsx | 21 ++ src/state/queries/nuxs/definitions.ts | 10 + src/view/shell/index.tsx | 2 + src/view/shell/index.web.tsx | 2 + 11 files changed, 508 insertions(+), 15 deletions(-) create mode 100644 src/components/dialogs/BlockingAlertDialog.tsx create mode 100644 src/components/dialogs/BlockingAnnouncements/PolicyUpdate20250801.tsx create mode 100644 src/components/dialogs/BlockingAnnouncements/common.tsx create mode 100644 src/components/dialogs/BlockingAnnouncements/index.tsx diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 2ecf5ba618..bee26d0788 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -22,6 +22,7 @@ export const Context = createContext({ disableDrag: false, setDisableDrag: () => {}, isWithinDialog: false, + preventDismiss: false, }) export function useDialogContext() { diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 4795385eec..f868fbf31f 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -54,6 +54,7 @@ export function Outer({ control, onClose, nativeOptions, + preventDismiss, testID, }: React.PropsWithChildren) { const themeName = useThemeName() @@ -165,6 +166,7 @@ export function Outer({ cornerRadius={20} backgroundColor={t.atoms.bg.backgroundColor} {...nativeOptions} + preventDismiss={preventDismiss ?? nativeOptions?.preventDismiss} onSnapPointChange={onSnapPointChange} onStateChange={onStateChange} disableDrag={disableDrag}> diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 12bd8819b1..3f0bb878cf 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -40,6 +40,7 @@ export function Outer({ control, onClose, webOptions, + preventDismiss, }: React.PropsWithChildren) { const {_} = useLingui() const {gtMobile} = useBreakpoints() @@ -52,7 +53,9 @@ export function Outer({ }, [setIsOpen, setDialogIsOpen, control.id]) const close = React.useCallback( - cb => { + (cb, force) => { + if (preventDismiss && !force) return + setDialogIsOpen(control.id, false) setIsOpen(false) @@ -72,7 +75,7 @@ export function Outer({ onClose?.() }, - [control.id, onClose, setDialogIsOpen], + [control.id, onClose, setDialogIsOpen, preventDismiss], ) const handleBackgroundPress = React.useCallback(async () => { @@ -96,8 +99,9 @@ export function Outer({ disableDrag: false, setDisableDrag: () => {}, isWithinDialog: true, + preventDismiss: preventDismiss ?? false, }), - [close], + [close, preventDismiss], ) return ( @@ -239,7 +243,10 @@ export const InnerFlatList = React.forwardRef< export function Close() { const {_} = useLingui() - const {close} = React.useContext(Context) + const {close, preventDismiss} = React.useContext(Context) + + if (preventDismiss) return null + return ( @@ -26,7 +26,7 @@ export type DialogControlRefProps = { open: ( options?: DialogControlOpenOptions & Partial, ) => void - close: (callback?: () => void) => void + close: (callback?: () => void, force?: boolean) => void } /** @@ -46,6 +46,7 @@ export type DialogContextProps = { setDisableDrag: React.Dispatch> // in the event that the hook is used outside of a dialog isWithinDialog: boolean + preventDismiss: boolean } export type DialogControlOpenOptions = { @@ -62,6 +63,7 @@ export type DialogOuterProps = { control: DialogControlProps onClose?: () => void nativeOptions?: Omit + preventDismiss?: boolean webOptions?: { alignCenter?: boolean } diff --git a/src/components/dialogs/BlockingAlertDialog.tsx b/src/components/dialogs/BlockingAlertDialog.tsx new file mode 100644 index 0000000000..eed52c58da --- /dev/null +++ b/src/components/dialogs/BlockingAlertDialog.tsx @@ -0,0 +1,211 @@ +import {type ReactNode, useCallback} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {Logo} from '#/view/icons/Logo' +import {atoms as a, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {InlineLinkText} from '#/components/Link' +import {Span, Text} from '#/components/Typography' + +export function BlockingAlertDialog() { + const {enabled} = usePolicyUpdate20250801() + + return ( + <> + {enabled && ( + + + + )} + + ) +} + +function useForceClose() { + const {close} = Dialog.useDialogContext() + return useCallback( + (cb?: () => void) => { + close(cb, true) + }, + [close], + ) +} + +export function BlockingAnnouncementDialogOuter({ + children, +}: { + children: ReactNode +}) { + const {signinDialogControl: control} = useGlobalDialogsControlContext() + + Dialog.useAutoOpen(control, 1e3) + + return ( + + + {children} + + ) +} + +export function AnnouncementBadge() { + const t = useTheme() + return ( + + + + + Announcement + + + + ) +} + +function usePolicyUpdate20250801() { + return { + enabled: true, + } +} + +function PolicyUpdate20250801() { + const t = useTheme() + const {_} = useLingui() + const forceClose = useForceClose() + + const linkStyle = [a.text_md] + const links = { + terms: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Terms of Service`), + style: linkStyle, + }, + privacy: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Privacy Policy`), + style: linkStyle, + }, + copyright: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Copyright Policy`), + style: linkStyle, + }, + guidelines: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Community Guidelines`), + style: linkStyle, + }, + blog: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Our blog post`), + style: linkStyle, + }, + } + + const handleClose = useCallback(() => { + forceClose(() => { + console.log('closed') + }) + }, [forceClose]) + + return ( + + + + + + + Hey there 👋 + + + + We’re updating our{' '} + Terms of Service + ,{' '} + Privacy Policy + , and{' '} + + Copyright Policy + + , effective September 1st, 2025. + + + + + We're also updating our{' '} + + Community Guidelines + + , and we want your input! These new + guidelines will take effect on{' '} + October 1st, 2025. + + + + + Learn more about these changes and how to share your thoughts with + us by{' '} + + reading our blog post. + + + + + + + + + + + By clicking "Continue" you acknowledge that you understand and + agree to these updates. + + + + + + ) +} diff --git a/src/components/dialogs/BlockingAnnouncements/PolicyUpdate20250801.tsx b/src/components/dialogs/BlockingAnnouncements/PolicyUpdate20250801.tsx new file mode 100644 index 0000000000..fb778db283 --- /dev/null +++ b/src/components/dialogs/BlockingAnnouncements/PolicyUpdate20250801.tsx @@ -0,0 +1,141 @@ +import {useCallback} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {Nux} from '#/state/queries/nuxs' +import {atoms as a, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import { + AnnouncementBadge, + useAnnouncementState, + useForceClose, +} from '#/components/dialogs/BlockingAnnouncements/common' +import {InlineLinkText} from '#/components/Link' +import {Span, Text} from '#/components/Typography' + +export function useLocalState() { + return useAnnouncementState({ + id: Nux.BlockingAnnouncementPolicyUpdate20250801, + }) +} + +export function Announcement() { + const t = useTheme() + const {_} = useLingui() + const forceClose = useForceClose() + const {complete} = useLocalState() + + const handleClose = useCallback(() => { + forceClose(() => complete()) + }, [forceClose, complete]) + + const linkStyle = [a.text_md] + const links = { + terms: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Terms of Service`), + style: linkStyle, + }, + privacy: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Privacy Policy`), + style: linkStyle, + }, + copyright: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Copyright Policy`), + style: linkStyle, + }, + guidelines: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Community Guidelines`), + style: linkStyle, + }, + blog: { + overridePresentation: true, + to: `https://bsky.social/about/support`, + label: _(msg`Our blog post`), + style: linkStyle, + }, + } + + return ( + + + + + + + Hey there 👋 + + + + We’re updating our{' '} + Terms of Service + ,{' '} + Privacy Policy + , and{' '} + + Copyright Policy + + , effective September 1st, 2025. + + + + + We're also updating our{' '} + + Community Guidelines + + , and we want your input! These new + guidelines will take effect on{' '} + October 1st, 2025. + + + + + Learn more about these changes and how to share your thoughts with + us by{' '} + + reading our blog post. + + + + + + + + + + + By clicking "Continue" you acknowledge that you understand and + agree to these updates. + + + + + + ) +} diff --git a/src/components/dialogs/BlockingAnnouncements/common.tsx b/src/components/dialogs/BlockingAnnouncements/common.tsx new file mode 100644 index 0000000000..cceaf49241 --- /dev/null +++ b/src/components/dialogs/BlockingAnnouncements/common.tsx @@ -0,0 +1,94 @@ +import {type ReactNode, useCallback, useMemo} from 'react' +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {type Nux, useNux, useSaveNux} from '#/state/queries/nuxs' +import {Logo} from '#/view/icons/Logo' +import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {Text} from '#/components/Typography' + +export function useForceClose() { + const {close} = Dialog.useDialogContext() + return useCallback( + (cb?: () => void) => { + close(cb, true) + }, + [close], + ) +} + +export function useAnnouncementState({id}: {id: Nux}) { + const nux = useNux(id) + const {mutate: save, variables} = useSaveNux() + const optimisticallyCompleted = !!variables?.completed + return useMemo( + () => ({ + /** + * Until data has loaded, assumed completed + */ + completed: + optimisticallyCompleted || nux.status === 'ready' + ? nux.nux?.completed === true + : true, + complete() { + save({ + id, + completed: true, + data: undefined, + }) + }, + }), + [id, nux, save, optimisticallyCompleted], + ) +} + +export function BlockingAnnouncementDialogOuter({ + children, +}: { + children: ReactNode +}) { + const {signinDialogControl: control} = useGlobalDialogsControlContext() + + Dialog.useAutoOpen(control, 1e3) + + return ( + + + {children} + + ) +} + +export function AnnouncementBadge() { + const t = useTheme() + return ( + + + + + Announcement + + + + ) +} diff --git a/src/components/dialogs/BlockingAnnouncements/index.tsx b/src/components/dialogs/BlockingAnnouncements/index.tsx new file mode 100644 index 0000000000..65368728c8 --- /dev/null +++ b/src/components/dialogs/BlockingAnnouncements/index.tsx @@ -0,0 +1,21 @@ +import {BlockingAnnouncementDialogOuter} from '#/components/dialogs/BlockingAnnouncements/common' +import * as PolicyUpdate20250801 from '#/components/dialogs/BlockingAnnouncements/PolicyUpdate20250801' + +export function BlockingAnnouncements() { + const policyUpdate20250801 = PolicyUpdate20250801.useLocalState() + + /* + * See `window.clearNux` example in `/state/queries/nuxs` for a way to clear + * NUX state for local testing and debugging. + */ + + return ( + <> + {!policyUpdate20250801.completed && ( + + + + )} + + ) +} diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 3d5c132f26..5fa706e5ae 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -9,6 +9,11 @@ export enum Nux { ActivitySubscriptions = 'ActivitySubscriptions', AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice', AgeAssuranceDismissibleFeedBanner = 'AgeAssuranceDismissibleFeedBanner', + + /* + * Blocking announcements. New IDs are required for each new announcement. + */ + BlockingAnnouncementPolicyUpdate20250801 = 'BlockingAnnouncementPolicyUpdate20250801', } export const nuxNames = new Set(Object.values(Nux)) @@ -38,6 +43,10 @@ export type AppNux = BaseNux< id: Nux.AgeAssuranceDismissibleFeedBanner data: undefined } + | { + id: Nux.BlockingAnnouncementPolicyUpdate20250801 + data: undefined + } > export const NuxSchemas: Record | undefined> = { @@ -47,4 +56,5 @@ export const NuxSchemas: Record | undefined> = { [Nux.ActivitySubscriptions]: undefined, [Nux.AgeAssuranceDismissibleNotice]: undefined, [Nux.AgeAssuranceDismissibleFeedBanner]: undefined, + [Nux.BlockingAnnouncementPolicyUpdate20250801]: undefined, } diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 4d1a8c51b6..ccddb61150 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -26,6 +26,7 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {setSystemUITheme} from '#/alf/util/systemUI' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' +import {BlockingAnnouncements} from '#/components/dialogs/BlockingAnnouncements' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' @@ -155,6 +156,7 @@ function ShellInner() { + diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 77c3f45f62..beda58f564 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -18,6 +18,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' +import {BlockingAnnouncements} from '#/components/dialogs/BlockingAnnouncements' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' @@ -70,6 +71,7 @@ function ShellInner() { +