An ATproto social media client -- with an independent Appview.
fork

Configure Feed

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

at main 180 lines 4.5 kB view raw
1import { 2 getCurrentPositionAsync, 3 type LocationGeocodedAddress, 4 reverseGeocodeAsync, 5} from 'expo-location' 6 7import {logger} from '#/state/geolocation/logger' 8import {type DeviceLocation} from '#/state/geolocation/types' 9import {type Device} from '#/storage' 10 11/** 12 * Maps full US region names to their short codes. 13 * 14 * Context: in some cases, like on Android, we get the full region name instead 15 * of the short code. We may need to expand this in the future to other 16 * countries, hence the prefix. 17 */ 18export const USRegionNameToRegionCode: { 19 [regionName: string]: string 20} = { 21 Alabama: 'AL', 22 Alaska: 'AK', 23 Arizona: 'AZ', 24 Arkansas: 'AR', 25 California: 'CA', 26 Colorado: 'CO', 27 Connecticut: 'CT', 28 Delaware: 'DE', 29 Florida: 'FL', 30 Georgia: 'GA', 31 Hawaii: 'HI', 32 Idaho: 'ID', 33 Illinois: 'IL', 34 Indiana: 'IN', 35 Iowa: 'IA', 36 Kansas: 'KS', 37 Kentucky: 'KY', 38 Louisiana: 'LA', 39 Maine: 'ME', 40 Maryland: 'MD', 41 Massachusetts: 'MA', 42 Michigan: 'MI', 43 Minnesota: 'MN', 44 Mississippi: 'MS', 45 Missouri: 'MO', 46 Montana: 'MT', 47 Nebraska: 'NE', 48 Nevada: 'NV', 49 ['New Hampshire']: 'NH', 50 ['New Jersey']: 'NJ', 51 ['New Mexico']: 'NM', 52 ['New York']: 'NY', 53 ['North Carolina']: 'NC', 54 ['North Dakota']: 'ND', 55 Ohio: 'OH', 56 Oklahoma: 'OK', 57 Oregon: 'OR', 58 Pennsylvania: 'PA', 59 ['Rhode Island']: 'RI', 60 ['South Carolina']: 'SC', 61 ['South Dakota']: 'SD', 62 Tennessee: 'TN', 63 Texas: 'TX', 64 Utah: 'UT', 65 Vermont: 'VT', 66 Virginia: 'VA', 67 Washington: 'WA', 68 ['West Virginia']: 'WV', 69 Wisconsin: 'WI', 70 Wyoming: 'WY', 71} 72 73/** 74 * Normalizes a `LocationGeocodedAddress` into a `DeviceLocation`. 75 * 76 * We don't want or care about the full location data, so we trim it down and 77 * normalize certain fields, like region, into the format we need. 78 */ 79export function normalizeDeviceLocation( 80 location: LocationGeocodedAddress, 81): DeviceLocation { 82 let {isoCountryCode, region} = location 83 84 if (region) { 85 if (isoCountryCode === 'US') { 86 region = USRegionNameToRegionCode[region] ?? region 87 } 88 } 89 90 return { 91 countryCode: isoCountryCode ?? undefined, 92 regionCode: region ?? undefined, 93 } 94} 95 96/** 97 * Combines precise location data with the geolocation config fetched from the 98 * IP service, with preference to the precise data. 99 */ 100export function mergeGeolocation( 101 location?: DeviceLocation, 102 config?: Device['geolocation'], 103): DeviceLocation { 104 if (location?.countryCode) return location 105 return { 106 countryCode: config?.countryCode, 107 regionCode: config?.regionCode, 108 } 109} 110 111/** 112 * Computes the geolocation status (age-restricted, age-blocked) based on the 113 * given location and geolocation config. `location` here should be merged with 114 * `mergeGeolocation()` ahead of time if needed. 115 */ 116export function computeGeolocationStatus( 117 location: DeviceLocation, 118 config: Device['geolocation'], 119) { 120 /** 121 * We can't do anything if we don't have this data. 122 */ 123 if (!location.countryCode) { 124 return { 125 ...location, 126 isAgeRestrictedGeo: false, 127 isAgeBlockedGeo: false, 128 } 129 } 130 131 const isAgeRestrictedGeo = config?.ageRestrictedGeos?.some(rule => { 132 if (rule.countryCode === location.countryCode) { 133 if (!rule.regionCode) { 134 return true // whole country is blocked 135 } else if (rule.regionCode === location.regionCode) { 136 return true 137 } 138 } 139 }) 140 141 const isAgeBlockedGeo = config?.ageBlockedGeos?.some(rule => { 142 if (rule.countryCode === location.countryCode) { 143 if (!rule.regionCode) { 144 return true // whole country is blocked 145 } else if (rule.regionCode === location.regionCode) { 146 return true 147 } 148 } 149 }) 150 151 return { 152 ...location, 153 isAgeRestrictedGeo: !!isAgeRestrictedGeo, 154 isAgeBlockedGeo: !!isAgeBlockedGeo, 155 } 156} 157 158export async function getDeviceGeolocation(): Promise<DeviceLocation> { 159 try { 160 const geocode = await getCurrentPositionAsync() 161 const locations = await reverseGeocodeAsync({ 162 latitude: geocode.coords.latitude, 163 longitude: geocode.coords.longitude, 164 }) 165 const location = locations.at(0) 166 const normalized = location ? normalizeDeviceLocation(location) : undefined 167 return { 168 countryCode: normalized?.countryCode ?? undefined, 169 regionCode: normalized?.regionCode ?? undefined, 170 } 171 } catch (e) { 172 logger.error('getDeviceGeolocation: failed', { 173 safeMessage: e, 174 }) 175 return { 176 countryCode: undefined, 177 regionCode: undefined, 178 } 179 } 180}