diff --git a/src/screens/Settings/DeerSettings.tsx b/src/screens/Settings/DeerSettings.tsx index 27acd3b75..423fb8614 100644 --- a/src/screens/Settings/DeerSettings.tsx +++ b/src/screens/Settings/DeerSettings.tsx @@ -1,8 +1,10 @@ import {useState} from 'react' +import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {type NativeStackScreenProps} from '@react-navigation/native-stack' +import {usePalette} from '#/lib/hooks/usePalette' import {type CommonNavigatorParams} from '#/lib/routes/types' import {type Gate} from '#/lib/statsig/gates' import { @@ -10,6 +12,8 @@ import { useDangerousSetGate, useGatesCache, } from '#/lib/statsig/statsig' +import {isWeb} from '#/platform/detection' +import {setGeolocation, useGeolocation} from '#/state/geolocation' import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' import { useConstellationEnabled, @@ -19,17 +23,88 @@ import { useDirectFetchRecords, useSetDirectFetchRecords, } from '#/state/preferences/direct-fetch-records' +import {TextInput} from '#/view/com/modals/util' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' import * as Toggle from '#/components/forms/Toggle' import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' +import {Earth_Stroke2_Corner2_Rounded as GlobeIcon} from '#/components/icons/Globe' +import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' import * as Layout from '#/components/Layout' +import {Text} from '#/components/Typography' type Props = NativeStackScreenProps +function GeolocationSettingsDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {_} = useLingui() + + const [hasChanged, setHasChanged] = useState(false) + const [countryCode, setCountryCode] = useState('') + + const submit = () => { + setGeolocation({countryCode}) + control.close() + } + + return ( + + + + + + Geolocation ISO 3166-1 Code + + + + + { + setCountryCode(value.toUpperCase()) + setHasChanged(true) + }} + maxLength={2} + placeholder="BR" + placeholderTextColor={pal.colors.textLight} + onSubmitEditing={submit} + accessibilityHint={_( + msg`Input 2 letter ISO 3166-1 country code to use as location`, + )} + /> + + + + + + + + + + ) +} + export function DeerSettingsScreen({}: Props) { const {_} = useLingui() @@ -42,6 +117,9 @@ export function DeerSettingsScreen({}: Props) { const directFetchRecords = useDirectFetchRecords() const setDirectFetchRecords = useSetDirectFetchRecords() + const location = useGeolocation() + const setLocationControl = Dialog.useDialogControl() + const [gates, setGatesView] = useState(Object.fromEntries(useGatesCache())) const dangerousSetGate = useDangerousSetGate() const setGate = (gate: Gate, value: boolean) => { @@ -121,6 +199,27 @@ export function DeerSettingsScreen({}: Props) { + + + + {`ISO 3166-1 Location (currently ${ + location.geolocation?.countryCode ?? '?' + })`} + + setLocationControl.open()} + /> + + + + + Geolocation country code informs required regional labelers and + currency behavior. + + + + @@ -141,7 +240,7 @@ export function DeerSettingsScreen({}: Props) { - + Gates @@ -176,6 +275,17 @@ export function DeerSettingsScreen({}: Props) { + ) } + +const styles = { + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 14, + paddingVertical: 10, + fontSize: 16, + }, +} diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx index 0024cd41d..f64fd90a3 100644 --- a/src/state/geolocation.tsx +++ b/src/state/geolocation.tsx @@ -1,9 +1,8 @@ import React from 'react' import EventEmitter from 'eventemitter3' -import {networkRetry} from '#/lib/async/retry' import {logger} from '#/logger' -import {Device, device} from '#/storage' +import {type Device, device} from '#/storage' const events = new EventEmitter() const EVENT = 'geolocation-updated' @@ -24,25 +23,7 @@ const onGeolocationUpdate = ( * additional mod authorities. */ export const DEFAULT_GEOLOCATION: Device['geolocation'] = { - countryCode: undefined, -} - -async function getGeolocation(): Promise { - const res = await fetch(`https://bsky.app/ipcc`) - - if (!res.ok) { - throw new Error(`geolocation: lookup failed ${res.status}`) - } - - const json = await res.json() - - if (json.countryCode) { - return { - countryCode: json.countryCode, - } - } else { - return undefined - } + countryCode: 'US', } /** @@ -64,52 +45,15 @@ export function beginResolveGeolocation() { * In dev, IP server is unavailable, so we just set the default geolocation * and fail closed. */ - if (__DEV__) { - geolocationResolution = new Promise(y => y()) + geolocationResolution = new Promise(y => y()) + if (device.get(['geolocation']) == undefined) { device.set(['geolocation'], DEFAULT_GEOLOCATION) - return } +} - geolocationResolution = new Promise(async resolve => { - try { - // Try once, fail fast - const geolocation = await getGeolocation() - if (geolocation) { - device.set(['geolocation'], geolocation) - emitGeolocationUpdate(geolocation) - logger.debug(`geolocation: success`, {geolocation}) - } else { - // endpoint should throw on all failures, this is insurance - throw new Error(`geolocation: nothing returned from initial request`) - } - } catch (e: any) { - logger.error(`geolocation: failed initial request`, { - safeMessage: e.message, - }) - - // set to default - device.set(['geolocation'], DEFAULT_GEOLOCATION) - - // retry 3 times, but don't await, proceed with default - networkRetry(3, getGeolocation) - .then(geolocation => { - if (geolocation) { - device.set(['geolocation'], geolocation) - emitGeolocationUpdate(geolocation) - logger.debug(`geolocation: success`, {geolocation}) - } else { - // endpoint should throw on all failures, this is insurance - throw new Error(`geolocation: nothing returned from retries`) - } - }) - .catch((e: any) => { - // complete fail closed - logger.error(`geolocation: failed retries`, {safeMessage: e.message}) - }) - } finally { - resolve(undefined) - } - }) +export function setGeolocation(geolocation: Device['geolocation']) { + device.set(['geolocation'], geolocation) + emitGeolocationUpdate(geolocation) } /** diff --git a/src/state/preferences/country-code.tsx b/src/state/preferences/country-code.tsx new file mode 100644 index 000000000..3ff103b5e --- /dev/null +++ b/src/state/preferences/country-code.tsx @@ -0,0 +1,47 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['countryCode'] +type SetContext = (v: persisted.Schema['countryCode']) => void + +const stateContext = React.createContext( + persisted.defaults.countryCode, +) +const setContext = React.createContext( + (_: persisted.Schema['countryCode']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState(persisted.get('countryCode')) + + const setStateWrapped = React.useCallback( + (countryCode: persisted.Schema['countryCode']) => { + setState(countryCode) + persisted.write('countryCode', countryCode) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate('countryCode', nextCountryCode => { + setState(nextCountryCode) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useCountryCode() { + return React.useContext(stateContext) +} + +export function useSetCountryCode() { + return React.useContext(setContext) +} diff --git a/src/state/queries/direct-fetch-record.ts b/src/state/queries/direct-fetch-record.ts index 3a801e076..2c326fb3e 100644 --- a/src/state/queries/direct-fetch-record.ts +++ b/src/state/queries/direct-fetch-record.ts @@ -32,28 +32,29 @@ export function useDirectFetchRecord({ } try { - // TODO: parallel, series fetch sucks there isn't a dependency - const profile = (await agent.getProfile({actor: urip.host})).data - const {data} = await retry( - 2, - e => { - if (e.message.includes(`Could not locate record:`)) { - return false - } - return true - }, - () => - agent.api.com.atproto.repo.getRecord({ - repo: urip.host, - collection: 'app.bsky.feed.post', - rkey: urip.rkey, - }), - ) - if ( - data.value && - bsky.validate(data.value, AppBskyFeedPost.validateRecord) - ) { - const record = data.value + const [profile, record] = await Promise.all([ + (async () => (await agent.getProfile({actor: urip.host})).data)(), + (async () => + ( + await retry( + 2, + e => { + if (e.message.includes(`Could not locate record:`)) { + return false + } + return true + }, + () => + agent.api.com.atproto.repo.getRecord({ + repo: urip.host, + collection: 'app.bsky.feed.post', + rkey: urip.rkey, + }), + ) + ).data.value)(), + ]) + + if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) { return { $type: 'app.bsky.embed.record#viewRecord', uri, diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 00686d97c..7308100c9 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -37,6 +37,7 @@ import {useResolveLinkQuery} from '#/state/queries/resolve-link' import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' +import {Loader} from '#/components/Loader' import {RichText} from '#/components/RichText' import {SubtleWebHover} from '#/components/SubtleWebHover' import * as bsky from '#/types/bsky' @@ -114,13 +115,13 @@ export function MaybeQuoteEmbed({ return ( - + {directFetchEnabled ? ( + + ) : ( + + )} - {directFetchEnabled ? ( - Blocked... - ) : ( - Blocked - )} + Blocked ) @@ -160,14 +161,17 @@ export function MaybeQuoteEmbed({ return ( - + {directFetchEnabled ? ( + + ) : ( + + )} {isViewerOwner ? ( Removed by you ) : ( Removed by author )} - {directFetchEnabled ? ... : undefined} )