Bluesky app fork with some witchin' additions 💫
1import {useMemo} from 'react'
2import {
3 ageAssuranceRuleIDs as ids,
4 type AppBskyAgeassuranceDefs,
5 type AtpAgent,
6 getAgeAssuranceRegionConfig,
7 type ModerationPrefs,
8} from '@atproto/api'
9
10import {getAge} from '#/lib/strings/time'
11import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
12import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
13import {
14 getDidFromAgentSession,
15 getOtherRequiredDataFromCache,
16 useAgeAssuranceDataContext,
17} from '#/ageAssurance/data'
18import {AgeAssuranceAccess} from '#/ageAssurance/types'
19import {type Geolocation, useGeolocation} from '#/geolocation'
20
21export const MIN_ACCESS_AGE = 1
22const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = {
23 countryCode: '*',
24 regionCode: undefined,
25 minAccessAge: MIN_ACCESS_AGE,
26 rules: [
27 {
28 $type: ids.IfDeclaredOverAge,
29 age: MIN_ACCESS_AGE,
30 access: AgeAssuranceAccess.Full,
31 },
32 {
33 $type: ids.Default,
34 access: AgeAssuranceAccess.None,
35 },
36 ],
37}
38
39/**
40 * Get age assurance region config based on geolocation, with fallback to
41 * app defaults if no region config is found.
42 *
43 * See {@link getAgeAssuranceRegionConfig} for the generic option, which can
44 * return undefined if the geolocation does not match any AA region.
45 */
46export function getAgeAssuranceRegionConfigWithFallback(
47 config: AppBskyAgeassuranceDefs.Config,
48 geolocation: Geolocation,
49): AppBskyAgeassuranceDefs.ConfigRegion {
50 const region = getAgeAssuranceRegionConfig(config, {
51 countryCode: geolocation.countryCode ?? '',
52 regionCode: geolocation.regionCode,
53 })
54
55 return region || FALLBACK_REGION_CONFIG
56}
57
58/**
59 * Hook to get the age assurance region config based on current geolocation.
60 * Does not fall-back to our app defaults. If no config is found, returns
61 * undefined, which indicates no regional age assurance rules apply.
62 */
63export function useAgeAssuranceRegionConfig() {
64 const geolocation = useGeolocation()
65 const {config} = useAgeAssuranceDataContext()
66 return useMemo(() => {
67 if (!config) return
68 // use generic helper, we want to potentially return undefined
69 return getAgeAssuranceRegionConfig(config, {
70 countryCode: geolocation.countryCode ?? '',
71 regionCode: geolocation.regionCode,
72 })
73 }, [config, geolocation])
74}
75
76/**
77 * Hook to get the age assurance region config based on current geolocation.
78 * Falls back to our app defaults if no region config is found.
79 */
80export function useAgeAssuranceRegionConfigWithFallback() {
81 return useAgeAssuranceRegionConfig() || FALLBACK_REGION_CONFIG
82}
83
84/**
85 * Some users may have erroneously set their birth date to the current date
86 * if one wasn't set on their account. We previously didn't do validation on
87 * the bday dialog, and it defaulted to the current date. This bug _has_ been
88 * seen in production, so we need to check for it where possible.
89 */
90export function isLegacyBirthdateBug(birthDate: string) {
91 return ['2025', '2024', '2023'].includes((birthDate || '').slice(0, 4))
92}
93
94/**
95 * Returns whether the date (converted to an age as a whole integer) is under
96 * the provided minimum age.
97 */
98export function isUnderAge(birthDate: string, age: number) {
99 return getAge(new Date(birthDate)) < age
100}
101
102export function getBirthdateStringFromAge(age: number) {
103 const today = new Date()
104 return new Date(
105 today.getFullYear() - age,
106 today.getMonth(),
107 today.getDate() - 1, // set to day before to ensure age is reached
108 ).toISOString()
109}
110
111export const makeAgeRestrictedModerationPrefs = (
112 prefs: ModerationPrefs,
113): ModerationPrefs => ({
114 ...prefs,
115 adultContentEnabled: false,
116 labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
117})
118
119/**
120 * Checks our cache of the actor's chat declaration record, and if it's not
121 * already restricted, restricts it.
122 */
123export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) {
124 const did = getDidFromAgentSession(agent)
125 if (!did) return
126 const data = getOtherRequiredDataFromCache({did})
127 // ...update the chat setting record if allowIncoming is not already 'none'.
128 if (data?.actorDeclaration?.allowIncoming === 'none') return
129 restrictChatSettings({agent, did})
130}