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

Configure Feed

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

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