Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 8c3553cd66ad07ef8c8c4e760b495cf6ce08cc8d 407 lines 14 kB view raw
1import {useCallback, useEffect} from 'react' 2import {ScrollView, View} from 'react-native' 3import {useSafeAreaInsets} from 'react-native-safe-area-context' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import { 8 SupportCode, 9 useCreateSupportLink, 10} from '#/lib/hooks/useCreateSupportLink' 11import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 12import {logger} from '#/logger' 13import {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 14import {useSessionApi} from '#/state/session' 15import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 16import {Admonition} from '#/components/Admonition' 17import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' 18import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 19import {AgeAssuranceInitDialog} from '#/components/ageAssurance/AgeAssuranceInitDialog' 20import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21import {useDialogControl} from '#/components/Dialog' 22import * as Dialog from '#/components/Dialog' 23import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 24import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' 25import {Full as Logo} from '#/components/icons/Logo' 26import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield' 27import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 28import {Outlet as PortalOutlet} from '#/components/Portal' 29import * as Toast from '#/components/Toast' 30import {Text} from '#/components/Typography' 31import {BottomSheetOutlet} from '#/../modules/bottom-sheet' 32import {useAgeAssurance} from '#/ageAssurance' 33import {useAgeAssuranceDataContext} from '#/ageAssurance/data' 34import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' 35import { 36 isLegacyBirthdateBug, 37 useAgeAssuranceRegionConfig, 38} from '#/ageAssurance/util' 39import {IS_WEB} from '#/env' 40import {IS_NATIVE} from '#/env' 41import {useDeviceGeolocationApi} from '#/geolocation' 42 43const textStyles = [a.text_md, a.leading_snug] 44 45export function NoAccessScreen() { 46 const t = useTheme() 47 const {_} = useLingui() 48 const {gtPhone} = useBreakpoints() 49 const insets = useSafeAreaInsets() 50 const birthdateControl = useDialogControl() 51 const {data} = useAgeAssuranceDataContext() 52 const region = useAgeAssuranceRegionConfig() 53 const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed() 54 const {logoutCurrentAccount} = useSessionApi() 55 const createSupportLink = useCreateSupportLink() 56 57 const aa = useAgeAssurance() 58 const isBlocked = aa.state.status === aa.Status.Blocked 59 const isAARegion = !!region 60 const hasDeclaredAge = data?.declaredAge !== undefined 61 const canUpdateBirthday = 62 isBirthdateUpdateAllowed || isLegacyBirthdateBug(data?.birthdate || '') 63 64 useEffect(() => { 65 // just counting overall hits here 66 logger.metric(`blockedGeoOverlay:shown`, {}) 67 logger.metric(`ageAssurance:noAccessScreen:shown`, { 68 accountCreatedAt: data?.accountCreatedAt || 'unknown', 69 isAARegion, 70 hasDeclaredAge, 71 canUpdateBirthday, 72 }) 73 // eslint-disable-next-line react-hooks/exhaustive-deps 74 }, []) 75 76 const onPressLogout = useCallback(() => { 77 if (IS_WEB) { 78 // We're switching accounts, which remounts the entire app. 79 // On mobile, this gets us Home, but on the web we also need reset the URL. 80 // We can't change the URL via a navigate() call because the navigator 81 // itself is about to unmount, and it calls pushState() too late. 82 // So we change the URL ourselves. The navigator will pick it up on remount. 83 history.pushState(null, '', '/') 84 } 85 logoutCurrentAccount('AgeAssuranceNoAccessScreen') 86 }, [logoutCurrentAccount]) 87 88 const orgAdmonition = ( 89 <Admonition type="tip"> 90 <Trans> 91 For organizational accounts, use the birthdate of the person who is 92 responsible for the account. 93 </Trans> 94 </Admonition> 95 ) 96 97 const birthdateUpdateText = canUpdateBirthday ? ( 98 <> 99 <Text style={[textStyles]}> 100 <Trans> 101 If you believe your birthdate is incorrect, you can update it by{' '} 102 <SimpleInlineLinkText 103 label={_(msg`Click here to update your birthdate`)} 104 style={[textStyles]} 105 {...createStaticClick(() => { 106 logger.metric( 107 'ageAssurance:noAccessScreen:openBirthdateDialog', 108 {}, 109 ) 110 birthdateControl.open() 111 })}> 112 clicking here 113 </SimpleInlineLinkText> 114 . 115 </Trans> 116 </Text> 117 118 {orgAdmonition} 119 </> 120 ) : ( 121 <Text style={[textStyles]}> 122 <Trans> 123 If you believe your birthdate is incorrect, please{' '} 124 <SimpleInlineLinkText 125 to={createSupportLink({code: SupportCode.AA_BIRTHDATE})} 126 label={_(msg`Click here to contact our support team`)} 127 style={[textStyles]}> 128 contact our support team 129 </SimpleInlineLinkText> 130 . 131 </Trans> 132 </Text> 133 ) 134 135 return ( 136 <> 137 <View style={[a.util_screen_outer, a.flex_1]}> 138 <ScrollView 139 contentContainerStyle={[ 140 a.px_2xl, 141 { 142 paddingTop: IS_WEB 143 ? a.p_5xl.padding 144 : insets.top + a.p_2xl.padding, 145 paddingBottom: 100, 146 }, 147 ]}> 148 <View 149 style={[ 150 a.mx_auto, 151 a.w_full, 152 web({ 153 maxWidth: 380, 154 paddingTop: gtPhone ? '8vh' : undefined, 155 }), 156 { 157 gap: 32, 158 }, 159 ]}> 160 <View style={[a.align_start]}> 161 <AgeAssuranceBadge /> 162 </View> 163 164 {hasDeclaredAge ? ( 165 <> 166 {isAARegion ? ( 167 <> 168 <View style={[a.gap_lg]}> 169 <Text style={[textStyles]}> 170 <Trans>Hey there!</Trans> 171 </Text> 172 <Text style={[textStyles]}> 173 <Trans> 174 You are accessing Bluesky from a region that legally 175 requires us to verify your age before allowing you to 176 access the app. 177 </Trans> 178 </Text> 179 180 {!aa.flags.isOverRegionMinAccessAge && ( 181 <Text style={[textStyles]}> 182 <Trans> 183 Unfortunately, your declared age indicates that you 184 are not old enough to access Bluesky in your region. 185 </Trans> 186 </Text> 187 )} 188 189 {!isBlocked && birthdateUpdateText} 190 </View> 191 192 {aa.flags.isOverRegionMinAccessAge && <AccessSection />} 193 </> 194 ) : ( 195 <View style={[a.gap_lg]}> 196 <Text style={[textStyles]}> 197 <Trans> 198 Unfortunately, the birthdate you have saved to your 199 profile makes you too young to access Bluesky. 200 </Trans> 201 </Text> 202 203 {birthdateUpdateText} 204 </View> 205 )} 206 </> 207 ) : ( 208 <View style={[a.gap_lg]}> 209 <Text style={[textStyles]}> 210 <Trans>Hi there!</Trans> 211 </Text> 212 <Text style={[textStyles]}> 213 <Trans> 214 In order to provide an age-appropriate experience, we need 215 to know your birthdate. This is a one-time thing, and your 216 data will be kept private. 217 </Trans> 218 </Text> 219 <Text style={[textStyles]}> 220 <Trans> 221 Set your birthdate below and we'll get you back to posting 222 and exploring in no time! 223 </Trans> 224 </Text> 225 <Button 226 color="primary" 227 size="large" 228 label={_(msg`Click here to update your birthdate`)} 229 onPress={() => birthdateControl.open()}> 230 <ButtonText> 231 <Trans>Add your birthdate</Trans> 232 </ButtonText> 233 </Button> 234 235 {orgAdmonition} 236 </View> 237 )} 238 239 <View style={[a.pt_lg, a.gap_xl]}> 240 <Logo width={120} textFill={t.atoms.text.color} /> 241 <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}> 242 <Trans> 243 To log out,{' '} 244 <SimpleInlineLinkText 245 label={_(msg`Click here to log out`)} 246 {...createStaticClick(() => { 247 onPressLogout() 248 })}> 249 click here 250 </SimpleInlineLinkText> 251 . 252 </Trans> 253 </Text> 254 </View> 255 </View> 256 </ScrollView> 257 </View> 258 259 <BirthDateSettingsDialog control={birthdateControl} /> 260 261 {/* 262 * While this blocking overlay is up, other dialogs in the shell 263 * are not mounted, so it _should_ be safe to use these here 264 * without fear of other modals showing up. 265 */} 266 <BottomSheetOutlet /> 267 <PortalOutlet /> 268 </> 269 ) 270} 271 272function AccessSection() { 273 const t = useTheme() 274 const {_, i18n} = useLingui() 275 const control = useDialogControl() 276 const appealControl = Dialog.useDialogControl() 277 const locationControl = Dialog.useDialogControl() 278 const getTimeAgo = useGetTimeAgo() 279 const {setDeviceGeolocation} = useDeviceGeolocationApi() 280 const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess() 281 282 const aa = useAgeAssurance() 283 const {status, lastInitiatedAt} = aa.state 284 const isBlocked = status === aa.Status.Blocked 285 const hasInitiated = !!lastInitiatedAt 286 const timeAgo = lastInitiatedAt 287 ? getTimeAgo(lastInitiatedAt, new Date()) 288 : null 289 const diff = lastInitiatedAt 290 ? dateDiff(lastInitiatedAt, new Date(), 'down') 291 : null 292 293 return ( 294 <> 295 <AgeAssuranceInitDialog control={control} /> 296 <AgeAssuranceAppealDialog control={appealControl} /> 297 298 <View style={[a.gap_xl]}> 299 {isBlocked ? ( 300 <Admonition type="warning"> 301 <Trans> 302 You are currently unable to access Bluesky's Age Assurance flow. 303 Please{' '} 304 <SimpleInlineLinkText 305 label={_(msg`Contact our moderation team`)} 306 {...createStaticClick(() => { 307 appealControl.open() 308 logger.metric('ageAssurance:appealDialogOpen', {}) 309 })}> 310 contact our moderation team 311 </SimpleInlineLinkText>{' '} 312 if you believe this is an error. 313 </Trans> 314 </Admonition> 315 ) : ( 316 <> 317 <View style={[a.gap_md]}> 318 <Button 319 label={_(msg`Verify now`)} 320 size="large" 321 color={hasInitiated ? 'secondary' : 'primary'} 322 onPress={() => { 323 control.open() 324 logger.metric('ageAssurance:initDialogOpen', { 325 hasInitiatedPreviously: hasInitiated, 326 }) 327 }}> 328 <ButtonIcon icon={ShieldIcon} /> 329 <ButtonText> 330 {hasInitiated ? ( 331 <Trans>Verify again</Trans> 332 ) : ( 333 <Trans>Verify now</Trans> 334 )} 335 </ButtonText> 336 </Button> 337 338 {lastInitiatedAt && timeAgo && diff ? ( 339 <Text 340 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]} 341 title={i18n.date(lastInitiatedAt, { 342 dateStyle: 'medium', 343 timeStyle: 'medium', 344 })}> 345 {diff.value === 0 ? ( 346 <Trans>Last initiated just now</Trans> 347 ) : ( 348 <Trans>Last initiated {timeAgo} ago</Trans> 349 )} 350 </Text> 351 ) : ( 352 <Text 353 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}> 354 <Trans>Age assurance only takes a few minutes</Trans> 355 </Text> 356 )} 357 </View> 358 </> 359 )} 360 361 <View style={[a.gap_xs]}> 362 {IS_NATIVE && ( 363 <> 364 <Admonition> 365 <Trans> 366 Is your location not accurate?{' '} 367 <SimpleInlineLinkText 368 label={_(msg`Confirm your location`)} 369 {...createStaticClick(() => { 370 locationControl.open() 371 })}> 372 Tap here to confirm your location. 373 </SimpleInlineLinkText>{' '} 374 </Trans> 375 </Admonition> 376 377 <DeviceLocationRequestDialog 378 control={locationControl} 379 onLocationAcquired={props => { 380 const access = computeAgeAssuranceRegionAccess( 381 props.geolocation, 382 ) 383 if (access !== aa.Access.Full) { 384 props.disableDialogAction() 385 props.setDialogError( 386 _( 387 msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`, 388 ), 389 ) 390 } else { 391 props.closeDialog(() => { 392 // set this after close! 393 setDeviceGeolocation(props.geolocation) 394 Toast.show(_(msg`Thanks! You're all set.`), { 395 type: 'success', 396 }) 397 }) 398 } 399 }} 400 /> 401 </> 402 )} 403 </View> 404 </View> 405 </> 406 ) 407}