diff --git a/src/components/BlockedGeoOverlay.tsx b/src/components/BlockedGeoOverlay.tsx new file mode 100644 index 0000000000..ae5790da92 --- /dev/null +++ b/src/components/BlockedGeoOverlay.tsx @@ -0,0 +1,109 @@ +import {useEffect} from 'react' +import {ScrollView, View} from 'react-native' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {Full as Logo, Mark} from '#/components/icons/Logo' +import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function BlockedGeoOverlay() { + const t = useTheme() + const {_} = useLingui() + const {gtPhone} = useBreakpoints() + const insets = useSafeAreaInsets() + + useEffect(() => { + // just counting overall hits here + logger.metric(`blockedGeoOverlay:shown`, {}) + }, []) + + const textStyles = [a.text_md, a.leading_normal] + const links = { + blog: { + to: `https://bsky.social/about/blog/08-22-2025-mississippi-hb1126`, + label: _(msg`Read our blog post`), + overridePresentation: false, + disableMismatchWarning: true, + style: textStyles, + }, + } + + const blocks = [ + _(msg`Unfortunately, Bluesky is unavailable in Mississippi right now.`), + _( + msg`A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies.`, + ), + _( + msg`As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending.`, + ), + _( + msg`For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi.`, + ), + <> + To learn more, read our{' '} + blog post. + , + ] + + return ( + + + + + + + Announcement + + + + + + {blocks.map((block, index) => ( + + {block} + + ))} + + + + + + ) +} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 6954be6a89..421a7fe9d6 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react' -import {type GestureResponderEvent} from 'react-native' +import {type GestureResponderEvent, Linking} from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' import { type LinkProps as RNLinkProps, @@ -13,6 +13,7 @@ import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import { convertBskyAppUrlIfNeeded, + createProxiedUrl, isBskyDownloadUrl, isExternalUrl, linkRequiresWarning, @@ -407,6 +408,91 @@ export function InlineLinkText({ ) } +/** + * A barebones version of `InlineLinkText`, for use outside a + * `react-navigation` context. + */ +export function SimpleInlineLinkText({ + children, + to, + style, + download, + selectable, + label, + disableUnderline, + shouldProxy, + ...rest +}: Omit< + InlineLinkProps, + | 'to' + | 'action' + | 'disableMismatchWarning' + | 'overridePresentation' + | 'onPress' + | 'onLongPress' + | 'shareOnLongPress' +> & { + to: string +}) { + const t = useTheme() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const flattenedStyle = flatten(style) || {} + const isExternal = isExternalUrl(to) + + let href = to + if (shouldProxy) { + href = createProxiedUrl(href) + } + + const onPress = () => { + Linking.openURL(href) + } + + return ( + + {children} + + ) +} + export function WebOnlyInlineLinkText({ children, to, diff --git a/src/components/icons/Logo.tsx b/src/components/icons/Logo.tsx index 6f16d8a44f..75c5cb420e 100644 --- a/src/components/icons/Logo.tsx +++ b/src/components/icons/Logo.tsx @@ -1,5 +1,42 @@ +import Svg, {Path} from 'react-native-svg' + +import {type Props, useCommonSVGProps} from './common' import {createSinglePathSVG} from './TEMPLATE' export const Mark = createSinglePathSVG({ path: 'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z', }) + +export function Full( + props: Omit & { + markFill?: Props['fill'] + textFill?: Props['fill'] + }, +) { + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) + const ratio = 123 / 555 + + return ( + + {gradient} + + + + ) +} diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 0c9ea1ef63..4a09d85935 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -475,4 +475,9 @@ export type MetricEvents = { 'ageAssurance:redirectDialogFail': {} 'ageAssurance:appealDialogOpen': {} 'ageAssurance:appealDialogSubmit': {} + + /* + * Specifically for the `BlockedGeoOverlay` + */ + 'blockedGeoOverlay:shown': {} } diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx index 4581996a09..a69161324f 100644 --- a/src/state/geolocation.tsx +++ b/src/state/geolocation.tsx @@ -25,6 +25,7 @@ const onGeolocationUpdate = ( */ export const DEFAULT_GEOLOCATION: Device['geolocation'] = { countryCode: undefined, + isAgeBlockedGeo: undefined, isAgeRestrictedGeo: false, } @@ -40,6 +41,7 @@ async function getGeolocation(): Promise { if (json.countryCode) { return { countryCode: json.countryCode, + isAgeBlockedGeo: json.isAgeBlockedGeo ?? false, isAgeRestrictedGeo: json.isAgeRestrictedGeo ?? false, } } else { diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 421264ac1e..a3f2336cf4 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -10,6 +10,7 @@ export type Device = { geolocation?: { countryCode: string | undefined isAgeRestrictedGeo: boolean | undefined + isAgeBlockedGeo: boolean | undefined } trendingBetaEnabled: boolean devMode: boolean diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 04fccc44cb..8b4c65b8fc 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -13,6 +13,7 @@ import {useNotificationsRegistration} from '#/lib/notifications/notifications' import {isStateAtTabRoot} from '#/lib/routes/helpers' import {isAndroid, isIOS} from '#/platform/detection' import {useDialogFullyExpandedCountContext} from '#/state/dialogs' +import {useGeolocation} from '#/state/geolocation' import {useSession} from '#/state/session' import { useIsDrawerOpen, @@ -26,6 +27,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 {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' @@ -180,9 +182,11 @@ function ShellInner() { ) } -export const Shell: React.FC = function ShellImpl() { - const fullyExpandedCount = useDialogFullyExpandedCountContext() +export function Shell() { const t = useTheme() + const {geolocation} = useGeolocation() + const fullyExpandedCount = useDialogFullyExpandedCountContext() + useIntentHandler() useEffect(() => { @@ -200,9 +204,13 @@ export const Shell: React.FC = function ShellImpl() { navigationBar: t.name !== 'light' ? 'light' : 'dark', }} /> - - - + {geolocation?.isAgeBlockedGeo ? ( + + ) : ( + + + + )} ) } diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 3c2bc58abd..f942ab49eb 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -5,11 +5,10 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {RemoveScrollBar} from 'react-remove-scroll-bar' -import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type NavigationProp} from '#/lib/routes/types' -import {colors} from '#/lib/styles' +import {useGeolocation} from '#/state/geolocation' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' import {useCloseAllActiveElements} from '#/state/util' @@ -18,6 +17,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 {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' @@ -130,24 +130,23 @@ function ShellInner() { ) } -export const Shell: React.FC = function ShellImpl() { - const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) +export function Shell() { + const t = useTheme() + const {geolocation} = useGeolocation() return ( - - - - + + {geolocation?.isAgeBlockedGeo ? ( + + ) : ( + + + + )} ) } const styles = StyleSheet.create({ - bgLight: { - backgroundColor: colors.white, - }, - bgDark: { - backgroundColor: colors.black, // TODO - }, drawerMask: { ...a.fixed, width: '100%',