Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[LEG-246] Geo overlay (#8881)

* Add AgeBlockedGeo

* Add MaxMind usage text

* Add geo overlay

---------

Co-authored-by: rafael <rafael@blueskyweb.xyz>

authored by

Eric Bailey
rafael
and committed by
GitHub
912ab1bd f038ac70

+269 -22
+2 -2
bskyweb/cmd/bskyweb/server.go
··· 606 606 type IPCCResponse struct { 607 607 CC string `json:"countryCode"` 608 608 AgeRestrictedGeo bool `json:"isAgeRestrictedGeo,omitempty"` 609 + AgeBlockedGeo bool `json:"isAgeBlockedGeo,omitempty"` 609 610 } 610 611 611 - // IP address data is powered by IPinfo 612 - // https://ipinfo.io 612 + // This product includes GeoLite2 Data created by MaxMind, available from https://www.maxmind.com. 613 613 func (srv *Server) WebIpCC(c echo.Context) error { 614 614 realIP := c.RealIP() 615 615 addr, err := netip.ParseAddr(realIP)
+109
src/components/BlockedGeoOverlay.tsx
··· 1 + import {useEffect} from 'react' 2 + import {ScrollView, View} from 'react-native' 3 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {logger} from '#/logger' 8 + import {isWeb} from '#/platform/detection' 9 + import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 10 + import {Full as Logo, Mark} from '#/components/icons/Logo' 11 + import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link' 12 + import {Text} from '#/components/Typography' 13 + 14 + export function BlockedGeoOverlay() { 15 + const t = useTheme() 16 + const {_} = useLingui() 17 + const {gtPhone} = useBreakpoints() 18 + const insets = useSafeAreaInsets() 19 + 20 + useEffect(() => { 21 + // just counting overall hits here 22 + logger.metric(`blockedGeoOverlay:shown`, {}) 23 + }, []) 24 + 25 + const textStyles = [a.text_md, a.leading_normal] 26 + const links = { 27 + blog: { 28 + to: `https://bsky.social/about/blog/08-22-2025-mississippi-hb1126`, 29 + label: _(msg`Read our blog post`), 30 + overridePresentation: false, 31 + disableMismatchWarning: true, 32 + style: textStyles, 33 + }, 34 + } 35 + 36 + const blocks = [ 37 + _(msg`Unfortunately, Bluesky is unavailable in Mississippi right now.`), 38 + _( 39 + 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.`, 40 + ), 41 + _( 42 + msg`As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending.`, 43 + ), 44 + _( 45 + msg`For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi.`, 46 + ), 47 + <> 48 + To learn more, read our{' '} 49 + <InlineLinkText {...links.blog}>blog post</InlineLinkText>. 50 + </>, 51 + ] 52 + 53 + return ( 54 + <ScrollView 55 + contentContainerStyle={[ 56 + a.px_2xl, 57 + { 58 + paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding, 59 + paddingBottom: 100, 60 + }, 61 + ]}> 62 + <View 63 + style={[ 64 + a.mx_auto, 65 + web({ 66 + maxWidth: 440, 67 + paddingTop: gtPhone ? '8vh' : undefined, 68 + }), 69 + ]}> 70 + <View style={[a.align_start]}> 71 + <View 72 + style={[ 73 + a.pl_md, 74 + a.pr_lg, 75 + a.py_sm, 76 + a.rounded_full, 77 + a.flex_row, 78 + a.align_center, 79 + a.gap_xs, 80 + { 81 + backgroundColor: t.palette.primary_25, 82 + }, 83 + ]}> 84 + <Mark fill={t.palette.primary_600} width={14} /> 85 + <Text 86 + style={[ 87 + a.font_bold, 88 + { 89 + color: t.palette.primary_600, 90 + }, 91 + ]}> 92 + <Trans>Announcement</Trans> 93 + </Text> 94 + </View> 95 + </View> 96 + 97 + <View style={[a.gap_lg, {paddingTop: 32, paddingBottom: 48}]}> 98 + {blocks.map((block, index) => ( 99 + <Text key={index} style={[textStyles]}> 100 + {block} 101 + </Text> 102 + ))} 103 + </View> 104 + 105 + <Logo width={120} textFill={t.atoms.text.color} /> 106 + </View> 107 + </ScrollView> 108 + ) 109 + }
+87 -1
src/components/Link.tsx
··· 1 1 import React, {useMemo} from 'react' 2 - import {type GestureResponderEvent} from 'react-native' 2 + import {type GestureResponderEvent, Linking} from 'react-native' 3 3 import {sanitizeUrl} from '@braintree/sanitize-url' 4 4 import { 5 5 type LinkProps as RNLinkProps, ··· 13 13 import {shareUrl} from '#/lib/sharing' 14 14 import { 15 15 convertBskyAppUrlIfNeeded, 16 + createProxiedUrl, 16 17 isBskyDownloadUrl, 17 18 isExternalUrl, 18 19 linkRequiresWarning, ··· 387 388 role="link" 388 389 onPress={download ? undefined : onPress} 389 390 onLongPress={onLongPress} 391 + onMouseEnter={onHoverIn} 392 + onMouseLeave={onHoverOut} 393 + accessibilityRole="link" 394 + href={href} 395 + {...web({ 396 + hrefAttrs: { 397 + target: download ? undefined : isExternal ? 'blank' : undefined, 398 + rel: isExternal ? 'noopener noreferrer' : undefined, 399 + download, 400 + }, 401 + dataSet: { 402 + // default to no underline, apply this ourselves 403 + noUnderline: '1', 404 + }, 405 + })}> 406 + {children} 407 + </Text> 408 + ) 409 + } 410 + 411 + /** 412 + * A barebones version of `InlineLinkText`, for use outside a 413 + * `react-navigation` context. 414 + */ 415 + export function SimpleInlineLinkText({ 416 + children, 417 + to, 418 + style, 419 + download, 420 + selectable, 421 + label, 422 + disableUnderline, 423 + shouldProxy, 424 + ...rest 425 + }: Omit< 426 + InlineLinkProps, 427 + | 'to' 428 + | 'action' 429 + | 'disableMismatchWarning' 430 + | 'overridePresentation' 431 + | 'onPress' 432 + | 'onLongPress' 433 + | 'shareOnLongPress' 434 + > & { 435 + to: string 436 + }) { 437 + const t = useTheme() 438 + const { 439 + state: hovered, 440 + onIn: onHoverIn, 441 + onOut: onHoverOut, 442 + } = useInteractionState() 443 + const flattenedStyle = flatten(style) || {} 444 + const isExternal = isExternalUrl(to) 445 + 446 + let href = to 447 + if (shouldProxy) { 448 + href = createProxiedUrl(href) 449 + } 450 + 451 + const onPress = () => { 452 + Linking.openURL(href) 453 + } 454 + 455 + return ( 456 + <Text 457 + selectable={selectable} 458 + accessibilityHint="" 459 + accessibilityLabel={label} 460 + {...rest} 461 + style={[ 462 + {color: t.palette.primary_500}, 463 + hovered && 464 + !disableUnderline && { 465 + ...web({ 466 + outline: 0, 467 + textDecorationLine: 'underline', 468 + textDecorationColor: 469 + flattenedStyle.color ?? t.palette.primary_500, 470 + }), 471 + }, 472 + flattenedStyle, 473 + ]} 474 + role="link" 475 + onPress={onPress} 390 476 onMouseEnter={onHoverIn} 391 477 onMouseLeave={onHoverOut} 392 478 accessibilityRole="link"
+37
src/components/icons/Logo.tsx
··· 1 + import Svg, {Path} from 'react-native-svg' 2 + 3 + import {type Props, useCommonSVGProps} from './common' 1 4 import {createSinglePathSVG} from './TEMPLATE' 2 5 3 6 export const Mark = createSinglePathSVG({ 4 7 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', 5 8 }) 9 + 10 + export function Full( 11 + props: Omit<Props, 'fill' | 'size' | 'height'> & { 12 + markFill?: Props['fill'] 13 + textFill?: Props['fill'] 14 + }, 15 + ) { 16 + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) 17 + const ratio = 123 / 555 18 + 19 + return ( 20 + <Svg 21 + fill="none" 22 + {...rest} 23 + viewBox="0 0 555 123" 24 + width={size} 25 + height={size * ratio} 26 + style={[style]}> 27 + {gradient} 28 + <Path 29 + fill={props.markFill ?? fill} 30 + fillRule="evenodd" 31 + clipRule="evenodd" 32 + d="M101.821 7.673C112.575-.367 130-6.589 130 13.21c0 3.953-2.276 33.214-3.611 37.965-4.641 16.516-21.549 20.729-36.591 18.179 26.292 4.457 32.979 19.218 18.535 33.98-27.433 28.035-39.428-7.034-42.502-16.02-.563-1.647-.827-2.418-.831-1.763-.004-.655-.268.116-.831 1.763-3.074 8.986-15.07 44.055-42.502 16.02C7.223 88.571 13.91 73.81 40.202 69.353c-15.041 2.55-31.95-1.663-36.59-18.179C2.275 46.424 0 17.162 0 13.21 0-6.59 17.426-.368 28.18 7.673 43.084 18.817 59.114 41.413 65 53.54c5.886-12.125 21.917-34.722 36.821-45.866Z" 33 + /> 34 + <Path 35 + fill={props.textFill ?? fill} 36 + fillRule="evenodd" 37 + clipRule="evenodd" 38 + d="m454.459 63.823 24.128-25.056h32.638l4.825 15.104c3.561 11.357 6.664 22.598 9.422 33.72 2.527-9.6 5.744-20.84 9.536-33.603l4.826-15.221H555l-22.864 65.335c-2.413 6.673-5.4 11.475-9.192 14.168-3.791 2.693-9.192 3.98-16.315 3.98-2.413 0-4.481-.117-6.319-.352v-11.59h5.514c6.549 0 9.767-4.099 9.767-9.719 0-2.81-.92-6.908-2.758-12.177l-17.177-49.478-22.239 22.665L497.2 99.184h-16.545l-17.234-28.101-8.962 9.133v18.968h-14.246V15.817h14.246v48.006Zm-48.373-26.46c16.889 0 25.622 6.79 26.196 20.49h-13.673c-.344-7.377-4.595-9.954-12.523-9.954-6.894 0-10.341 2.342-10.341 7.026 0 4.215 2.987 6.089 9.881 7.377l7.469 1.17c14.361 2.694 20.566 8.08 20.566 18.384 0 12.176-9.652 18.967-26.311 18.967-17.235 0-26.311-6.908-27.116-20.842h14.132c.804 7.494 4.481 10.304 13.213 10.304 7.813 0 11.72-2.459 11.72-7.26 0-4.332-2.758-6.44-11.605-7.962l-6.778-1.17c-12.983-2.224-19.418-8.313-19.418-18.265 0-11.358 8.847-18.266 24.588-18.266ZM270.534 76.351c0 7.61 3.677 11.474 11.145 11.474 7.008 0 13.212-5.268 13.213-15.22v-33.84h14.476v60.418h-14.016v-8.782c-4.481 6.791-10.686 10.187-18.614 10.187-12.523 0-20.68-7.728-20.68-21.778V38.767h14.476v37.585Zm75.432-38.99c8.961 0 16.085 3.045 21.37 9.016s7.928 13.933 7.928 23.651v3.513h-44.35c1.034 10.42 6.664 15.572 15.396 15.572 6.663 0 11.144-2.927 13.557-8.664h13.903c-3.103 12.294-13.443 20.139-27.575 20.139-8.847 0-15.971-2.927-21.371-8.664-5.4-5.737-8.157-13.348-8.157-22.95 0-9.483 2.643-17.094 8.043-22.949 5.4-5.737 12.409-8.664 21.256-8.664ZM195.628 15.817c17.809 0 26.426 9.251 26.426 21.545 0 8.196-3.677 14.168-10.915 17.914 9.306 3.396 14.247 11.24 14.247 20.022 0 14.87-9.767 23.886-28.494 23.886h-38.26V15.817h36.996Zm51.264 83.367h-14.477V15.817h14.477v83.367ZM174.143 86.07h21.944c8.732 0 13.443-4.098 13.443-11.474 0-7.728-4.481-11.592-13.443-11.592h-21.944V86.07Zm171.708-37.233c-7.928 0-13.443 4.683-14.822 14.401h29.758c-1.264-8.781-6.549-14.401-14.936-14.401Zm-171.708 1.756h20.336c7.927 0 12.178-4.215 12.178-11.24 0-6.44-4.366-10.539-12.178-10.539h-20.336v21.779Z" 39 + /> 40 + </Svg> 41 + ) 42 + }
+5
src/logger/metrics.ts
··· 475 475 'ageAssurance:redirectDialogFail': {} 476 476 'ageAssurance:appealDialogOpen': {} 477 477 'ageAssurance:appealDialogSubmit': {} 478 + 479 + /* 480 + * Specifically for the `BlockedGeoOverlay` 481 + */ 482 + 'blockedGeoOverlay:shown': {} 478 483 }
+2
src/state/geolocation.tsx
··· 25 25 */ 26 26 export const DEFAULT_GEOLOCATION: Device['geolocation'] = { 27 27 countryCode: undefined, 28 + isAgeBlockedGeo: undefined, 28 29 isAgeRestrictedGeo: false, 29 30 } 30 31 ··· 40 41 if (json.countryCode) { 41 42 return { 42 43 countryCode: json.countryCode, 44 + isAgeBlockedGeo: json.isAgeBlockedGeo ?? false, 43 45 isAgeRestrictedGeo: json.isAgeRestrictedGeo ?? false, 44 46 } 45 47 } else {
+1
src/storage/schema.ts
··· 10 10 geolocation?: { 11 11 countryCode: string | undefined 12 12 isAgeRestrictedGeo: boolean | undefined 13 + isAgeBlockedGeo: boolean | undefined 13 14 } 14 15 trendingBetaEnabled: boolean 15 16 devMode: boolean
+13 -5
src/view/shell/index.tsx
··· 13 13 import {isStateAtTabRoot} from '#/lib/routes/helpers' 14 14 import {isAndroid, isIOS} from '#/platform/detection' 15 15 import {useDialogFullyExpandedCountContext} from '#/state/dialogs' 16 + import {useGeolocation} from '#/state/geolocation' 16 17 import {useSession} from '#/state/session' 17 18 import { 18 19 useIsDrawerOpen, ··· 26 27 import {atoms as a, select, useTheme} from '#/alf' 27 28 import {setSystemUITheme} from '#/alf/util/systemUI' 28 29 import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 30 + import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' 29 31 import {EmailDialog} from '#/components/dialogs/EmailDialog' 30 32 import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' 31 33 import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' ··· 180 182 ) 181 183 } 182 184 183 - export const Shell: React.FC = function ShellImpl() { 184 - const fullyExpandedCount = useDialogFullyExpandedCountContext() 185 + export function Shell() { 185 186 const t = useTheme() 187 + const {geolocation} = useGeolocation() 188 + const fullyExpandedCount = useDialogFullyExpandedCountContext() 189 + 186 190 useIntentHandler() 187 191 188 192 useEffect(() => { ··· 200 204 navigationBar: t.name !== 'light' ? 'light' : 'dark', 201 205 }} 202 206 /> 203 - <RoutesContainer> 204 - <ShellInner /> 205 - </RoutesContainer> 207 + {geolocation?.isAgeBlockedGeo ? ( 208 + <BlockedGeoOverlay /> 209 + ) : ( 210 + <RoutesContainer> 211 + <ShellInner /> 212 + </RoutesContainer> 213 + )} 206 214 </View> 207 215 ) 208 216 }
+13 -14
src/view/shell/index.web.tsx
··· 5 5 import {useNavigation} from '@react-navigation/native' 6 6 import {RemoveScrollBar} from 'react-remove-scroll-bar' 7 7 8 - import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 9 8 import {useIntentHandler} from '#/lib/hooks/useIntentHandler' 10 9 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 10 import {type NavigationProp} from '#/lib/routes/types' 12 - import {colors} from '#/lib/styles' 11 + import {useGeolocation} from '#/state/geolocation' 13 12 import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' 14 13 import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' 15 14 import {useCloseAllActiveElements} from '#/state/util' ··· 18 17 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 19 18 import {atoms as a, select, useTheme} from '#/alf' 20 19 import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 20 + import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' 21 21 import {EmailDialog} from '#/components/dialogs/EmailDialog' 22 22 import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' 23 23 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' ··· 130 130 ) 131 131 } 132 132 133 - export const Shell: React.FC = function ShellImpl() { 134 - const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) 133 + export function Shell() { 134 + const t = useTheme() 135 + const {geolocation} = useGeolocation() 135 136 return ( 136 - <View style={[a.util_screen_outer, pageBg]}> 137 - <RoutesContainer> 138 - <ShellInner /> 139 - </RoutesContainer> 137 + <View style={[a.util_screen_outer, t.atoms.bg]}> 138 + {geolocation?.isAgeBlockedGeo ? ( 139 + <BlockedGeoOverlay /> 140 + ) : ( 141 + <RoutesContainer> 142 + <ShellInner /> 143 + </RoutesContainer> 144 + )} 140 145 </View> 141 146 ) 142 147 } 143 148 144 149 const styles = StyleSheet.create({ 145 - bgLight: { 146 - backgroundColor: colors.white, 147 - }, 148 - bgDark: { 149 - backgroundColor: colors.black, // TODO 150 - }, 151 150 drawerMask: { 152 151 ...a.fixed, 153 152 width: '100%',