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