diff --git a/.env.example b/.env.example index ac8dcab1f..96a1548d6 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,3 @@ EXPO_PUBLIC_BITDRIFT_API_KEY= # bapp-config web worker URL BAPP_CONFIG_DEV_URL= - -# Dev-only passthrough value for bapp-config web worker -BAPP_CONFIG_DEV_BYPASS_SECRET= diff --git a/__e2e__/flows/thread-muting.yml b/__e2e__/flows/thread-muting.yml index 3a6cb18a8..6a3a18b82 100644 --- a/__e2e__/flows/thread-muting.yml +++ b/__e2e__/flows/thread-muting.yml @@ -31,8 +31,6 @@ appId: app.witchsky id: "viewHeaderHomeFeedPrefsBtn" - tapOn: id: "replyBtn" -- tapOn: - id: "replyBtn" - inputText: "Reply 1" - tapOn: id: "composerPublishBtn" @@ -71,8 +69,6 @@ appId: app.witchsky id: "profilePager-selector-1" - tapOn: id: "replyBtn" -- tapOn: - id: "replyBtn" - inputText: "Reply 2" - tapOn: id: "composerPublishBtn" diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 8ca282c9d..e60640a41 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -957,20 +957,20 @@ describe('parseStarterPackHttpUri', () => { }) it('returns the at uri when the input is a valid starterpack at uri', () => { - const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey' + const validAtUri = 'at://did:plc:123/app.bsky.graph.starterpack/rkey' expect(parseStarterPackUri(validAtUri)).toEqual({ - name: 'did:123', + name: 'did:plc:123', rkey: 'rkey', }) }) it('returns null when the at uri has no rkey', () => { - const validAtUri = 'at://did:123/app.bsky.graph.starterpack' + const validAtUri = 'at://did:plc:123/app.bsky.graph.starterpack' expect(parseStarterPackUri(validAtUri)).toEqual(null) }) it('returns null when the collection is not app.bsky.graph.starterpack', () => { - const validAtUri = 'at://did:123/app.bsky.graph.list/rkey' + const validAtUri = 'at://did:plc:123/app.bsky.graph.list/rkey' expect(parseStarterPackUri(validAtUri)).toEqual(null) }) diff --git a/bskyweb/cmd/bskyweb/renderer.go b/bskyweb/cmd/bskyweb/renderer.go index 4bf8b80c5..6e02456f5 100644 --- a/bskyweb/cmd/bskyweb/renderer.go +++ b/bskyweb/cmd/bskyweb/renderer.go @@ -71,7 +71,7 @@ func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Cont if r.Debug { t, err = pongo2.FromFile(name) } else { - t, err = r.TemplateSet.FromFile(name) + t, err = r.TemplateSet.FromCache(name) } if err != nil { diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 66572a404..81ab0ce6e 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -68,11 +68,19 @@ width: 100%; } #splash { + display: flex; position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + align-items: center; + justify-content: center; + } + #splash svg { + position: relative; + top: -50px; width: 100px; - left: 50%; - top: 50%; - transform: translateX(-50%) translateY(-50%) translateY(-50px); } /** * We need these styles to prevent shifting due to scrollbar show/hide on @@ -106,7 +114,7 @@
- +
diff --git a/package.json b/package.json index b824d22ac..4f8574c40 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.18.0", + "@atproto/api": "^0.18.4", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.5", @@ -103,7 +103,7 @@ "@react-navigation/native-stack": "^7.3.13", "@sentry/react-native": "~6.20.0", "@tanstack/query-async-storage-persister": "^5.25.0", - "@tanstack/react-query": "^5.8.1", + "@tanstack/react-query": "5.25.0", "@tanstack/react-query-persist-client": "^5.25.0", "@tiptap/core": "^2.9.1", "@tiptap/extension-document": "^2.9.1", @@ -222,7 +222,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.181", + "@atproto/dev-env": "^0.3.193", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", diff --git a/patches/expo-modules-core+3.0.24.patch b/patches/expo-modules-core+3.0.24.patch index f3d9bfd14..d39609993 100644 --- a/patches/expo-modules-core+3.0.24.patch +++ b/patches/expo-modules-core+3.0.24.patch @@ -1,3 +1,32 @@ +diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt +index d300fc2..0890878 100644 +--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt ++++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt +@@ -3,8 +3,8 @@ package expo.modules.kotlin.activityresult + import androidx.activity.result.ActivityResultCallback + import androidx.activity.result.contract.ActivityResultContract + import java.io.Serializable ++import kotlinx.coroutines.suspendCancellableCoroutine + import kotlin.coroutines.resume +-import kotlin.coroutines.suspendCoroutine + + /** + * A launcher for a previously-[AppContextActivityResultCaller.registerForActivityResult] prepared call +@@ -22,8 +22,12 @@ abstract class AppContextActivityResultLauncher { + */ + abstract fun launch(input: I, callback: ActivityResultCallback) + +- suspend fun launch(input: I): O = suspendCoroutine { continuation -> +- launch(input) { output -> continuation.resume(output) } ++ suspend fun launch(input: I): O = suspendCancellableCoroutine { continuation -> ++ launch(input) { output -> ++ if (continuation.isActive) { ++ continuation.resume(output) ++ } ++ } + } + + abstract val contract: AppContextActivityResultContract diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt index 47c4d15..afe138d 100644 --- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt diff --git a/src/App.native.tsx b/src/App.native.tsx index 30a5e8129..fb3008627 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -25,16 +25,10 @@ import I18nProvider from '#/locale/i18nProvider' import {logger} from '#/logger' import {isAndroid, isIOS} from '#/platform/detection' import {Provider as A11yProvider} from '#/state/a11y' -import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as EmailVerificationProvider} from '#/state/email-verification' import {listenSessionDropped} from '#/state/events' -import { - beginResolveGeolocationConfig, - ensureGeolocationConfigIsResolved, - Provider as GeolocationProvider, -} from '#/state/geolocation' import {GlobalGestureEventsProvider} from '#/state/global-gesture-events' import {Provider as HomeBadgeProvider} from '#/state/home-badge' import {Provider as LightboxStateProvider} from '#/state/lightbox' @@ -56,6 +50,7 @@ import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' +import {Provider as OnboardingProvider} from '#/state/shell/onboarding' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' @@ -73,6 +68,9 @@ import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdate import {Provider as PortalProvider} from '#/components/Portal' import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {ToastOutlet} from '#/components/Toast' +import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' +import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -93,7 +91,8 @@ if (isAndroid) { /** * Begin geolocation ASAP */ -beginResolveGeolocationConfig() +Geo.resolve() +prefetchAgeAssuranceConfig() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -143,7 +142,7 @@ function InnerApp() { - + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} @@ -186,7 +185,7 @@ function InnerApp() { - + @@ -203,10 +202,9 @@ function App() { const [isReady, setReady] = useState(false) React.useEffect(() => { - Promise.all([ - initPersistedState(), - ensureGeolocationConfigIsResolved(), - ]).then(() => setReady(true)) + Promise.all([initPersistedState(), Geo.resolve()]).then(() => + setReady(true), + ) }, []) if (!isReady) { @@ -218,36 +216,38 @@ function App() { * that is set up in the InnerApp component above. */ return ( - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index b7cba6122..f4b514dfc 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -14,16 +14,10 @@ import {ThemeProvider} from '#/lib/ThemeContext' import I18nProvider from '#/locale/i18nProvider' import {logger} from '#/logger' import {Provider as A11yProvider} from '#/state/a11y' -import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as EmailVerificationProvider} from '#/state/email-verification' import {listenSessionDropped} from '#/state/events' -import { - beginResolveGeolocationConfig, - ensureGeolocationConfigIsResolved, - Provider as GeolocationProvider, -} from '#/state/geolocation' import {Provider as HomeBadgeProvider} from '#/state/home-badge' import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' @@ -44,6 +38,7 @@ import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' +import {Provider as OnboardingProvider} from '#/state/shell/onboarding' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' @@ -61,13 +56,18 @@ import {Provider as PortalProvider} from '#/components/Portal' import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {ToastOutlet} from '#/components/Toast' +import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' +import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import * as Geo from '#/geolocation' +import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' /** * Begin geolocation ASAP */ -beginResolveGeolocationConfig() +Geo.resolve() +prefetchAgeAssuranceConfig() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -104,7 +104,7 @@ function InnerApp() { }, [_]) // wait for session to resume - if (!isReady || !hasCheckedReferrer) return null + if (!isReady || !hasCheckedReferrer) return return ( @@ -118,7 +118,7 @@ function InnerApp() { - + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} @@ -157,7 +157,7 @@ function InnerApp() { - + @@ -174,14 +174,13 @@ function App() { const [isReady, setReady] = useState(false) React.useEffect(() => { - Promise.all([ - initPersistedState(), - ensureGeolocationConfigIsResolved(), - ]).then(() => setReady(true)) + Promise.all([initPersistedState(), Geo.resolve()]).then(() => + setReady(true), + ) }, []) if (!isReady) { - return null + return } /* @@ -189,29 +188,31 @@ function App() { * that is set up in the InnerApp component above. */ return ( - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - + ) } diff --git a/src/components/BlockedGeoOverlay.tsx b/src/components/BlockedGeoOverlay.tsx deleted file mode 100644 index 9061a0cf8..000000000 --- a/src/components/BlockedGeoOverlay.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import {useEffect} from 'react' -import {ScrollView, View} from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' -import {useDeviceGeolocationApi} from '#/state/geolocation' -import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' -import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' -import {Divider} from '#/components/Divider' -import {Full as Logo, Mark} from '#/components/icons/Logo' -import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation' -import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link' -import {Outlet as PortalOutlet} from '#/components/Portal' -import * as Toast from '#/components/Toast' -import {Text} from '#/components/Typography' -import {BottomSheetOutlet} from '#/../modules/bottom-sheet' - -export function BlockedGeoOverlay() { - const t = useTheme() - const {_} = useLingui() - const {gtPhone} = useBreakpoints() - const insets = useSafeAreaInsets() - const geoDialog = Dialog.useDialogControl() - const {setDeviceGeolocation} = useDeviceGeolocationApi() - - const enableSquareButtons = useEnableSquareButtons() - - useEffect(() => { - // just counting overall hits here - logger.metric(`blockedGeoOverlay:shown`, {}) - }, []) - - const textStyles = [a.text_md, a.leading_normal] - const links = { - blog: { - to: `https://bsky.social/about/blog/08-22-2025-mississippi-hb1126`, - label: _(msg`Read our blog post`), - overridePresentation: false, - disableMismatchWarning: true, - style: textStyles, - }, - } - - const blocks = [ - _(msg`Unfortunately, Bluesky is unavailable in Mississippi right now.`), - _( - msg`A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies.`, - ), - _( - msg`As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending.`, - ), - _( - msg`For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi.`, - ), - <> - To learn more, read our{' '} - blog post. - , - ] - - return ( - <> - - - - - - - Announcement - - - - - - {blocks.map((block, index) => ( - - {block} - - ))} - - - {!isWeb && ( - <> - - - - - - - Not in Mississippi? - - - - Confirm your location with GPS. Your location data is not - tracked and does not leave your device. - - - - - - { - if (props.geolocationStatus.isAgeBlockedGeo) { - props.disableDialogAction() - props.setDialogError( - _( - msg`We're sorry, but based on your device's location, you are currently located in a region where we cannot provide access at this time.`, - ), - ) - } else { - props.closeDialog(() => { - // set this after close! - setDeviceGeolocation({ - countryCode: props.geolocationStatus.countryCode, - regionCode: props.geolocationStatus.regionCode, - }) - Toast.show(_(msg`Thanks! You're all set.`), { - type: 'success', - }) - }) - } - }} - /> - - )} - - - - - - - - {/* - * While this blocking overlay is up, other dialogs in the shell - * are not mounted, so it _should_ be safe to use these here - * without fear of other modals showing up. - */} - - - - ) -} diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 5a9bbaf41..3ad9fcb7b 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useCallback, useEffect, useRef} from 'react' import {ScrollView, View} from 'react-native' import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -8,6 +8,7 @@ import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' +import {type MetricEvents} from '#/logger/metrics' import {isIOS} from '#/platform/detection' import {useHideSimilarAccountsRecomm} from '#/state/preferences/hide-similar-accounts-recommendations' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -27,13 +28,14 @@ import { web, } from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' import * as FeedCard from '#/components/FeedCard' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' -import {InlineLinkText, Link} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' +import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog' import {ProgressGuideList} from './ProgressGuide/List' const MOBILE_CARD_WIDTH = 165 @@ -240,17 +242,20 @@ export function ProfileGrid({ profiles, recId, viewContext = 'feed', + isVisible = true, }: { isSuggestionsLoading: boolean profiles: bsky.profile.AnyProfileView[] recId?: number error: Error | null viewContext: 'profile' | 'profileHeader' | 'feed' + isVisible?: boolean }) { const t = useTheme() const {_} = useLingui() const moderationOpts = useModerationOpts() const {gtMobile} = useBreakpoints() + const followDialogControl = useDialogControl() const isLoading = isSuggestionsLoading || !moderationOpts const isProfileHeaderContext = viewContext === 'profileHeader' @@ -259,6 +264,84 @@ export function ProfileGrid({ const maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6 const minLength = gtMobile ? 3 : 4 + // Track seen profiles + const seenProfilesRef = useRef>(new Set()) + const containerRef = useRef(null) + const hasTrackedRef = useRef(false) + const logContext: MetricEvents['suggestedUser:seen']['logContext'] = + isFeedContext + ? 'InterstitialDiscover' + : isProfileHeaderContext + ? 'Profile' + : 'InterstitialProfile' + + // Callback to fire seen events + const fireSeen = useCallback(() => { + if (isLoading || error || !profiles.length) return + if (hasTrackedRef.current) return + hasTrackedRef.current = true + + const profilesToShow = profiles.slice(0, maxLength) + profilesToShow.forEach((profile, index) => { + if (!seenProfilesRef.current.has(profile.did)) { + seenProfilesRef.current.add(profile.did) + logger.metric( + 'suggestedUser:seen', + { + logContext, + recId, + position: index, + suggestedDid: profile.did, + category: null, + }, + {statsig: true}, + ) + } + }) + }, [isLoading, error, profiles, maxLength, logContext, recId]) + + // For profile header, fire when isVisible becomes true + useEffect(() => { + if (isProfileHeaderContext) { + if (!isVisible) { + hasTrackedRef.current = false + return + } + fireSeen() + } + }, [isVisible, isProfileHeaderContext, fireSeen]) + + // For feed interstitials, use IntersectionObserver to detect actual visibility + useEffect(() => { + if (isProfileHeaderContext) return // handled above + if (isLoading || error || !profiles.length) return + + const node = containerRef.current + if (!node) return + + // Use IntersectionObserver on web to detect when actually visible + if (typeof IntersectionObserver !== 'undefined') { + const observer = new IntersectionObserver( + entries => { + if (entries[0]?.isIntersecting) { + fireSeen() + observer.disconnect() + } + }, + {threshold: 0.5}, + ) + // @ts-ignore - web only + observer.observe(node) + return () => observer.disconnect() + } else { + // On native, delay slightly to account for layout shifts during hydration + const timeout = setTimeout(() => { + fireSeen() + }, 500) + return () => clearTimeout(timeout) + } + }, [isProfileHeaderContext, isLoading, error, profiles.length, fireSeen]) + // hide similar accounts const hideSimilarAccountsRecomm = useHideSimilarAccountsRecomm() @@ -293,6 +376,8 @@ export function ProfileGrid({ : 'InterstitialProfile', recId, position: index, + suggestedDid: profile.did, + category: null, }) }} style={[ @@ -353,6 +438,8 @@ export function ProfileGrid({ location: 'Card', recId, position: index, + suggestedDid: profile.did, + category: null, }) }} /> @@ -367,8 +454,19 @@ export function ProfileGrid({ return null } - if (!hideSimilarAccountsRecomm) { - return ( + if (hideSimilarAccountsRecomm) { + return null + } + + return ( + {!isProfileHeaderContext && ( - { - logger.metric('suggestedUser:seeMore', { + followDialogControl.open() + logEvent('suggestedUser:seeMore', { logContext: isFeedContext ? 'Explore' : 'Profile', }) }}> - See more - + {({hovered}) => ( + + See more + + )} + )} + + {gtMobile ? ( @@ -422,29 +535,32 @@ export function ProfileGrid({ decelerationRate="fast"> {content} - {!isProfileHeaderContext && } + {!isProfileHeaderContext && ( + { + followDialogControl.open() + logger.metric('suggestedUser:seeMore', { + logContext: 'Explore', + }) + }} + /> + )} )} - ) - } + + ) } -function SeeMoreSuggestedProfilesCard() { +function SeeMoreSuggestedProfilesCard({onPress}: {onPress: () => void}) { const t = useTheme() const {_} = useLingui() return ( - { - logger.metric('suggestedUser:seeMore', { - logContext: 'Explore', - }) - }} + ) } diff --git a/src/components/Link.tsx b/src/components/Link.tsx index a7da3f504..5bfbd043e 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -426,6 +426,7 @@ export function SimpleInlineLinkText({ label, disableUnderline, shouldProxy, + onPress: outerOnPress, ...rest }: Omit< InlineLinkProps, @@ -433,7 +434,6 @@ export function SimpleInlineLinkText({ | 'action' | 'disableMismatchWarning' | 'overridePresentation' - | 'onPress' | 'onLongPress' | 'shareOnLongPress' > & { @@ -453,7 +453,9 @@ export function SimpleInlineLinkText({ href = createProxiedUrl(href) } - const onPress = () => { + const onPress = (e: GestureResponderEvent) => { + const exitEarlyIfFalse = outerOnPress?.(e) + if (exitEarlyIfFalse === false) return Linking.openURL(href) } @@ -522,7 +524,7 @@ export function WebOnlyInlineLinkText({ export function createStaticClick( onPressHandler: Exclude, ): { - to: BaseLinkProps['to'] + to: string onPress: Exclude } { return { diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx index 57a991865..adddae8a3 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx @@ -12,7 +12,6 @@ import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' -import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useShowExternalShareButtons} from '#/state/preferences/external-share-buttons' import {useSession} from '#/state/session' @@ -27,6 +26,7 @@ import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/i import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane' import {SquareArrowTopRight_Stroke2_Corner0_Rounded as ExternalIcon} from '#/components/icons/SquareArrowTopRight' import * as Menu from '#/components/Menu' +import {useAgeAssurance} from '#/ageAssurance' import {useDevMode} from '#/storage/hooks/dev-mode' import {RecentChats} from './RecentChats' import {type ShareMenuItemsProps} from './ShareMenuItems.types' @@ -40,7 +40,7 @@ let ShareMenuItems = ({ const navigation = useNavigation() const sendViaChatControl = useDialogControl() const [devModeEnabled] = useDevMode() - const {isAgeRestricted} = useAgeAssurance() + const aa = useAgeAssurance() const openLink = useOpenLink() const postUri = post.uri @@ -129,7 +129,7 @@ let ShareMenuItems = ({ return ( <> - {hasSession && !isAgeRestricted && ( + {hasSession && aa.state.access === aa.Access.Full && ( diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx index f8677e8e8..0043e1bf4 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx @@ -11,7 +11,6 @@ import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' -import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useShowExternalShareButtons} from '#/state/preferences/external-share-buttons' import {useSession} from '#/state/session' @@ -25,6 +24,7 @@ import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon} from '#/compon import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane' import {SquareArrowTopRight_Stroke2_Corner0_Rounded as ExternalIcon} from '#/components/icons/SquareArrowTopRight' import * as Menu from '#/components/Menu' +import {useAgeAssurance} from '#/ageAssurance' import {useDevMode} from '#/storage/hooks/dev-mode' import {type ShareMenuItemsProps} from './ShareMenuItems.types' @@ -41,7 +41,7 @@ let ShareMenuItems = ({ const embedPostControl = useDialogControl() const sendViaChatControl = useDialogControl() const [devModeEnabled] = useDevMode() - const {isAgeRestricted} = useAgeAssurance() + const aa = useAgeAssurance() const openLink = useOpenLink() const postUri = post.uri @@ -157,7 +157,7 @@ let ShareMenuItems = ({ )} - {hasSession && !isAgeRestricted && ( + {hasSession && aa.state.access === aa.Access.Full && ( + + + + ) +} + // Fine to keep this top-level. let lastSelectedInterest = '' let lastSearchText = '' -function DialogInner({guide}: {guide: Follow10ProgressGuide}) { +function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { const {_} = useLingui() const interestsDisplayNames = useInterestsDisplayNames() const {data: preferences} = usePreferencesQuery() @@ -226,6 +249,43 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) { [moderationOpts], ) + // Track seen profiles + const seenProfilesRef = useRef>(new Set()) + const itemsRef = useRef(items) + itemsRef.current = items + const selectedInterestRef = useRef(selectedInterest) + selectedInterestRef.current = selectedInterest + + const onViewableItemsChanged = useRef( + ({viewableItems}: {viewableItems: ViewToken[]}) => { + for (const viewableItem of viewableItems) { + const item = viewableItem.item as Item + if (item.type === 'profile') { + if (!seenProfilesRef.current.has(item.profile.did)) { + seenProfilesRef.current.add(item.profile.did) + const position = itemsRef.current.findIndex( + i => i.type === 'profile' && i.profile.did === item.profile.did, + ) + logger.metric( + 'suggestedUser:seen', + { + logContext: 'ProgressGuide', + recId: undefined, + position: position !== -1 ? position : 0, + suggestedDid: item.profile.did, + category: selectedInterestRef.current, + }, + {statsig: true}, + ) + } + } + } + }, + ).current + const viewabilityConfig = useRef({ + itemVisiblePercentThreshold: 50, + }).current + const onSelectTab = useCallback( (interest: string) => { setSelectedInterest(interest) @@ -273,6 +333,8 @@ function DialogInner({guide}: {guide: Follow10ProgressGuide}) { scrollIndicatorInsets={{top: headerHeight}} initialNumToRender={8} maxToRenderPerBatch={8} + onViewableItemsChanged={onViewableItemsChanged} + viewabilityConfig={viewabilityConfig} /> ) } @@ -289,7 +351,7 @@ let Header = ({ selectedInterest, interestsDisplayNames, }: { - guide: Follow10ProgressGuide + guide?: Follow10ProgressGuide inputRef: React.RefObject listRef: React.RefObject onSelectTab: (v: string) => void @@ -340,7 +402,7 @@ let Header = ({ } Header = memo(Header) -function HeaderTop({guide}: {guide: Follow10ProgressGuide}) { +function HeaderTop({guide}: {guide?: Follow10ProgressGuide}) { const {_} = useLingui() const t = useTheme() const control = Dialog.useDialogContext() @@ -363,14 +425,16 @@ function HeaderTop({guide}: {guide: Follow10ProgressGuide}) { ]}> Find people to follow - - - + {guide && ( + + + + )} {isWeb ? ( + + Content filters + - - - ) : !isDeclaredUnderage ? ( - <> - - - You must complete age assurance in order to access the settings - below. - - + + {aaCopy.notice} + - - - {!isDeclaredUnderage && ( - <> - - - Enable adult content + + + {aa.state.access === aa.Access.Full && ( + <> + + + Enable adult content + + + + + {adultContentEnabled ? ( + Enabled + ) : ( + Disabled + )} - - - - {adultContentEnabled ? ( - Enabled - ) : ( - Disabled - )} - - - - + - {disabledOnIOS && ( - - - - Adult content can only be enabled via the Web at{' '} - { - evt.preventDefault() - Linking.openURL('https://bsky.app/') - return false - }}> - bsky.app - - . - - - - )} + + + {adultContentUIDisabledOnIOS && ( + + + + Adult content can only be enabled via the Web at{' '} + { + evt.preventDefault() + Linking.openURL('https://bsky.app/') + return false + }}> + bsky.app + + . + + + + )} - {adultContentEnabled && ( - <> - - - - - - - - - - )} + {adultContentEnabled && ( + <> + + + + + + + + )} - - - - ) : null} + + )} + + 0 && !isFollowingAll + // Track seen profiles - shared ref across all cards + const seenProfilesRef = useRef>(new Set()) + const onProfileSeen = useCallback( + (did: string, position: number) => { + if (!seenProfilesRef.current.has(did)) { + seenProfilesRef.current.add(did) + logger.metric( + 'suggestedUser:seen', + { + logContext: 'Onboarding', + recId: undefined, + position, + suggestedDid: did, + category: selectedInterest, + }, + {statsig: true}, + ) + } + }, + [selectedInterest], + ) + return ( @@ -193,6 +222,8 @@ export function StepSuggestedAccounts() { profile={user} moderationOpts={moderationOpts} position={index} + category={selectedInterest} + onSeen={onProfileSeen} /> ))} @@ -303,16 +334,53 @@ function SuggestedProfileCard({ profile, moderationOpts, position, + category, + onSeen, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts position: number + category: string | null + onSeen: (did: string, position: number) => void }) { const t = useTheme() + const cardRef = useRef(null) + const hasTrackedRef = useRef(false) + + useEffect(() => { + const node = cardRef.current + if (!node || hasTrackedRef.current) return + + if (isWeb && typeof IntersectionObserver !== 'undefined') { + const observer = new IntersectionObserver( + entries => { + if (entries[0]?.isIntersecting && !hasTrackedRef.current) { + hasTrackedRef.current = true + onSeen(profile.did, position) + observer.disconnect() + } + }, + {threshold: 0.5}, + ) + // @ts-ignore - web only + observer.observe(node) + return () => observer.disconnect() + } else { + // Native: use a short delay to account for initial layout + const timeout = setTimeout(() => { + if (!hasTrackedRef.current) { + hasTrackedRef.current = true + onSeen(profile.did, position) + } + }, 500) + return () => clearTimeout(timeout) + } + }, [onSeen, profile.did, position]) + return ( { @@ -557,6 +561,12 @@ export function PostThread({uri}: {uri: string}) { onEndReached={onEndReached} onEndReachedThreshold={4} onStartReachedThreshold={1} + onItemSeen={item => { + // Track post:view for parent posts and replies (non-anchor posts) + if (item.type === 'threadPost' && item.depth !== 0) { + trackThreadItemView(item.value.post) + } + }} /** * NATIVE ONLY * {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition} diff --git a/src/screens/Profile/Header/SuggestedFollows.tsx b/src/screens/Profile/Header/SuggestedFollows.tsx index 3a9eb170a..48856cef7 100644 --- a/src/screens/Profile/Header/SuggestedFollows.tsx +++ b/src/screens/Profile/Header/SuggestedFollows.tsx @@ -47,6 +47,7 @@ export function AnimatedProfileHeaderSuggestedFollows({ recId={data.recId} error={error} viewContext="profileHeader" + isVisible={isExpanded} /> ) diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index a449b3e5d..4a2ae68e9 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -1030,26 +1030,48 @@ export function Explore({ // track headers and report module viewability const alreadyReportedRef = useRef>(new Map()) - const onItemSeen = useCallback((item: ExploreScreenItems) => { - let module: MetricEvents['explore:module:seen']['module'] - if (item.type === 'trendingTopics' || item.type === 'trendingVideos') { - module = item.type - } else if (item.type === 'profile') { - module = 'suggestedAccounts' - } else if (item.type === 'feed') { - module = 'suggestedFeeds' - } else if (item.type === 'starterPack') { - module = 'suggestedStarterPacks' - } else if (item.type === 'preview:sliceItem') { - module = `feed:feedgen|${item.feed.uri}` - } else { - return - } - if (!alreadyReportedRef.current.has(module)) { - alreadyReportedRef.current.set(module, module) - logger.metric('explore:module:seen', {module}, {statsig: false}) - } - }, []) + const seenProfilesRef = useRef>(new Set()) + const onItemSeen = useCallback( + (item: ExploreScreenItems) => { + let module: MetricEvents['explore:module:seen']['module'] + if (item.type === 'trendingTopics' || item.type === 'trendingVideos') { + module = item.type + } else if (item.type === 'profile') { + module = 'suggestedAccounts' + // Track individual profile seen events + if (!seenProfilesRef.current.has(item.profile.did)) { + seenProfilesRef.current.add(item.profile.did) + const position = suggestedFollowsModule.findIndex( + i => i.type === 'profile' && i.profile.did === item.profile.did, + ) + logger.metric( + 'suggestedUser:seen', + { + logContext: 'Explore', + recId: item.recId, + position: position !== -1 ? position - 1 : 0, // -1 to account for header + suggestedDid: item.profile.did, + category: null, + }, + {statsig: true}, + ) + } + } else if (item.type === 'feed') { + module = 'suggestedFeeds' + } else if (item.type === 'starterPack') { + module = 'suggestedStarterPacks' + } else if (item.type === 'preview:sliceItem') { + module = `feed:feedgen|${item.feed.uri}` + } else { + return + } + if (!alreadyReportedRef.current.has(module)) { + alreadyReportedRef.current.set(module, module) + logger.metric('explore:module:seen', {module}, {statsig: false}) + } + }, + [suggestedFollowsModule], + ) return ( { return augmentSearchQuery(query || '', {did: currentAccount?.did}) @@ -341,6 +343,11 @@ let SearchScreenPostResults = ({ refreshing={isPTR} onRefresh={onPullToRefresh} onEndReached={onEndReached} + onItemSeen={item => { + if (item.type === 'post') { + trackPostView(item.post) + } + }} desktopFixedHeight ListFooterComponent={ - { const postShadow = usePostShadow(post) const {width, height} = useSafeAreaFrame() - const {sendInteraction} = useFeedFeedbackContext() + const {sendInteraction, feedDescriptor} = useFeedFeedbackContext() + const hasTrackedView = useRef(false) useEffect(() => { if (active) { @@ -505,8 +504,31 @@ let VideoItem = ({ feedContext, reqId, }) + + // Track post:view event + if (!hasTrackedView.current) { + hasTrackedView.current = true + logger.metric( + 'post:view', + { + uri: post.uri, + authorDid: post.author.did, + logContext: 'ImmersiveVideo', + feedDescriptor, + }, + {statsig: false}, + ) + } } - }, [active, post.uri, feedContext, reqId, sendInteraction]) + }, [ + active, + post.uri, + post.author.did, + feedContext, + reqId, + sendInteraction, + feedDescriptor, + ]) // TODO: high-performance android phones should also // be capable of rendering 3 video players, but currently diff --git a/src/state/ageAssurance/const.ts b/src/state/ageAssurance/const.ts deleted file mode 100644 index a0844adc2..000000000 --- a/src/state/ageAssurance/const.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {type ModerationPrefs} from '@atproto/api' - -import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' - -export const makeAgeRestrictedModerationPrefs = ( - prefs: ModerationPrefs, -): ModerationPrefs => ({ - ...prefs, - adultContentEnabled: false, - labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, -}) diff --git a/src/state/ageAssurance/index.tsx b/src/state/ageAssurance/index.tsx deleted file mode 100644 index e85672b7c..000000000 --- a/src/state/ageAssurance/index.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import {createContext, useContext, useMemo, useState} from 'react' -import {type AppBskyUnspeccedDefs} from '@atproto/api' -import {useQuery} from '@tanstack/react-query' - -import {networkRetry} from '#/lib/async/retry' -import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications' -import {isNetworkError} from '#/lib/strings/errors' -import { - type AgeAssuranceAPIContextType, - type AgeAssuranceContextType, -} from '#/state/ageAssurance/types' -import {useIsAgeAssuranceEnabled} from '#/state/ageAssurance/useIsAgeAssuranceEnabled' -import {logger} from '#/state/ageAssurance/util' -import {useGeolocationStatus} from '#/state/geolocation' -import {useAgent} from '#/state/session' - -export const createAgeAssuranceQueryKey = (did: string) => - ['ageAssurance', did] as const - -const DEFAULT_AGE_ASSURANCE_STATE: AppBskyUnspeccedDefs.AgeAssuranceState = { - lastInitiatedAt: undefined, - status: 'unknown', -} - -const AgeAssuranceContext = createContext({ - status: 'unknown', - isReady: false, - lastInitiatedAt: undefined, - isAgeRestricted: false, -}) -AgeAssuranceContext.displayName = 'AgeAssuranceContext' - -const AgeAssuranceAPIContext = createContext({ - // @ts-ignore can't be bothered to type this - refetch: () => Promise.resolve(), -}) -AgeAssuranceAPIContext.displayName = 'AgeAssuranceAPIContext' - -/** - * Low-level provider for fetching age assurance state on app load. Do not add - * any other data fetching in here to avoid complications and reduced - * performance. - */ -export function Provider({children}: {children: React.ReactNode}) { - const agent = useAgent() - const {status: geolocation} = useGeolocationStatus() - const isAgeAssuranceEnabled = useIsAgeAssuranceEnabled() - const getAndRegisterPushToken = useGetAndRegisterPushToken() - const [refetchWhilePending, setRefetchWhilePending] = useState(false) - - const {data, isFetched, refetch} = useQuery({ - /** - * This is load bearing. We always want this query to run and end in a - * "fetched" state, even if we fall back to defaults. This lets the rest of - * the app know that we've at least attempted to load the AA state. - * - * However, it only needs to run if AA is enabled. - */ - enabled: isAgeAssuranceEnabled, - refetchOnWindowFocus: refetchWhilePending, - queryKey: createAgeAssuranceQueryKey(agent.session?.did ?? 'never'), - async queryFn() { - if (!agent.session) return null - - try { - const {data} = await networkRetry(3, () => - agent.app.bsky.unspecced.getAgeAssuranceState(), - ) - // const {data} = { - // data: { - // lastInitiatedAt: new Date().toISOString(), - // status: 'pending', - // } as AppBskyUnspeccedDefs.AgeAssuranceState, - // } - - logger.debug(`fetch`, { - data, - account: agent.session?.did, - }) - - await getAndRegisterPushToken({ - isAgeRestricted: - !!geolocation?.isAgeRestrictedGeo && data.status !== 'assured', - }) - - return data - } catch (e) { - if (!isNetworkError(e)) { - logger.error(`ageAssurance: failed to fetch`, {safeMessage: e}) - } - // don't re-throw error, we'll just fall back to defaults - return null - } - }, - }) - - /** - * Derive state, or fall back to defaults - */ - const ageAssuranceContext = useMemo(() => { - const {status, lastInitiatedAt} = data || DEFAULT_AGE_ASSURANCE_STATE - const ctx: AgeAssuranceContextType = { - isReady: isFetched || !isAgeAssuranceEnabled, - status, - lastInitiatedAt, - isAgeRestricted: isAgeAssuranceEnabled ? status !== 'assured' : false, - } - logger.debug(`context`, ctx) - return ctx - }, [isFetched, data, isAgeAssuranceEnabled]) - - if ( - !!ageAssuranceContext.lastInitiatedAt && - ageAssuranceContext.status === 'pending' && - !refetchWhilePending - ) { - /* - * If we have a pending state, we want to refetch on window focus to ensure - * that we get the latest state when the user returns to the app. - */ - setRefetchWhilePending(true) - } else if ( - !!ageAssuranceContext.lastInitiatedAt && - ageAssuranceContext.status !== 'pending' && - refetchWhilePending - ) { - setRefetchWhilePending(false) - } - - const ageAssuranceAPIContext = useMemo( - () => ({ - refetch, - }), - [refetch], - ) - - return ( - - - {children} - - - ) -} - -/** - * Access to low-level AA state. Prefer using {@link useAgeInfo} for a - * more user-friendly interface. - */ -export function useAgeAssuranceContext() { - return useContext(AgeAssuranceContext) -} - -export function useAgeAssuranceAPIContext() { - return useContext(AgeAssuranceAPIContext) -} diff --git a/src/state/ageAssurance/types.ts b/src/state/ageAssurance/types.ts deleted file mode 100644 index 63febb3cf..000000000 --- a/src/state/ageAssurance/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {type AppBskyUnspeccedDefs} from '@atproto/api' -import {type QueryObserverBaseResult} from '@tanstack/react-query' - -export type AgeAssuranceContextType = { - /** - * Whether the age assurance state has been fetched from the server. If user - * is not in a region that requires AA, or AA is otherwise disabled, this - * will always be `true`. - */ - isReady: boolean - /** - * The server-reported status of the user's age verification process. - */ - status: AppBskyUnspeccedDefs.AgeAssuranceState['status'] - /** - * The last time the age assurance state was attempted by the user. - */ - lastInitiatedAt: AppBskyUnspeccedDefs.AgeAssuranceState['lastInitiatedAt'] - /** - * Indicates the user is age restricted based on the requirements of their - * region, and their server-provided age assurance status. Does not factor in - * the user's declared age. If AA is otherise disabled, this will always be - * `false`. - */ - isAgeRestricted: boolean -} - -export type AgeAssuranceAPIContextType = { - /** - * Refreshes the age assurance state by fetching it from the server. - */ - refetch: QueryObserverBaseResult['refetch'] -} diff --git a/src/state/ageAssurance/useAgeAssurance.ts b/src/state/ageAssurance/useAgeAssurance.ts deleted file mode 100644 index 061384868..000000000 --- a/src/state/ageAssurance/useAgeAssurance.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {useMemo} from 'react' - -import {useAgeAssuranceContext} from '#/state/ageAssurance' -import {logger} from '#/state/ageAssurance/util' -import {usePreferencesQuery} from '#/state/queries/preferences' - -type AgeAssurance = ReturnType & { - /** - * The age the user has declared in their preferences, if any. - */ - declaredAge: number | undefined - /** - * Indicates whether the user has declared an age under 18. - */ - isDeclaredUnderage: boolean -} - -/** - * Computed age information based on age assurance status and the user's - * declared age. Use this instead of {@link useAgeAssuranceContext} to get a - * more user-friendly interface. - */ -export function useAgeAssurance(): AgeAssurance { - const aa = useAgeAssuranceContext() - const {isFetched: preferencesLoaded, data: preferences} = - usePreferencesQuery() - const declaredAge = preferences?.userAge - - return useMemo(() => { - const isReady = aa.isReady && preferencesLoaded - const isDeclaredUnderage = - declaredAge !== undefined ? declaredAge < 18 : false - const state: AgeAssurance = { - isReady, - status: aa.status, - lastInitiatedAt: aa.lastInitiatedAt, - isAgeRestricted: aa.isAgeRestricted, - declaredAge, - isDeclaredUnderage, - } - logger.debug(`state`, state) - return state - }, [aa, preferencesLoaded, declaredAge]) -} diff --git a/src/state/ageAssurance/useInitAgeAssurance.ts b/src/state/ageAssurance/useInitAgeAssurance.ts deleted file mode 100644 index b658afb89..000000000 --- a/src/state/ageAssurance/useInitAgeAssurance.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - type AppBskyUnspeccedDefs, - type AppBskyUnspeccedInitAgeAssurance, - AtpAgent, -} from '@atproto/api' -import {useMutation, useQueryClient} from '@tanstack/react-query' - -import {wait} from '#/lib/async/wait' -import { - // DEV_ENV_APPVIEW, - PUBLIC_APPVIEW, - PUBLIC_APPVIEW_DID, -} from '#/lib/constants' -import {isNetworkError} from '#/lib/hooks/useCleanError' -import {logger} from '#/logger' -import {createAgeAssuranceQueryKey} from '#/state/ageAssurance' -import {type DeviceLocation, useGeolocationStatus} from '#/state/geolocation' -import {useAgent} from '#/state/session' - -let APPVIEW = PUBLIC_APPVIEW -let APPVIEW_DID = PUBLIC_APPVIEW_DID - -/* - * Uncomment if using the local dev-env - */ -// if (__DEV__) { -// APPVIEW = DEV_ENV_APPVIEW -// /* -// * IMPORTANT: you need to get this value from `http://localhost:2581` -// * introspection endpoint and updated in `constants`, since it changes -// * every time you run the dev-env. -// */ -// APPVIEW_DID = `` -// } - -/** - * Creates an ISO country code string from the given geolocation data. - * Examples: `GB` or `GB-ENG` - */ -function createISOCountryCode( - geolocation: Omit & { - countryCode: string - }, -): string { - return geolocation.countryCode.toUpperCase() -} - -export function useInitAgeAssurance() { - const qc = useQueryClient() - const agent = useAgent() - const {status: geolocation} = useGeolocationStatus() - return useMutation({ - async mutationFn( - props: Omit, - ) { - const countryCode = geolocation?.countryCode - const regionCode = geolocation?.regionCode - if (!countryCode) { - throw new Error(`Geolocation not available, cannot init age assurance.`) - } - - const { - data: {token}, - } = await agent.com.atproto.server.getServiceAuth({ - aud: APPVIEW_DID, - lxm: `app.bsky.unspecced.initAgeAssurance`, - }) - - const appView = new AtpAgent({service: APPVIEW}) - appView.sessionManager.session = {...agent.session!} - appView.sessionManager.session.accessJwt = token - appView.sessionManager.session.refreshJwt = '' - - /* - * 2s wait is good actually. Email sending takes a hot sec and this helps - * ensure the email is ready for the user once they open their inbox. - */ - const {data} = await wait( - 2e3, - appView.app.bsky.unspecced.initAgeAssurance({ - ...props, - countryCode: createISOCountryCode({ - countryCode, - regionCode, - }), - }), - ) - - qc.setQueryData( - createAgeAssuranceQueryKey(agent.session?.did ?? 'never'), - () => data, - ) - }, - onError(e) { - if (!isNetworkError(e)) { - logger.error(`useInitAgeAssurance failed`, { - safeMessage: e, - }) - } - }, - }) -} diff --git a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts b/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts deleted file mode 100644 index 6e85edd0b..000000000 --- a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {useMemo} from 'react' - -import {useGeolocationStatus} from '#/state/geolocation' - -export function useIsAgeAssuranceEnabled() { - const {status: geolocation} = useGeolocationStatus() - - return useMemo(() => { - return !!geolocation?.isAgeRestrictedGeo - }, [geolocation]) -} diff --git a/src/state/ageAssurance/util.ts b/src/state/ageAssurance/util.ts deleted file mode 100644 index 6b0e97b1c..000000000 --- a/src/state/ageAssurance/util.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {Logger} from '#/logger' - -export const logger = Logger.create(Logger.Context.AgeAssurance) diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index d9dbefe1f..2def3ef5d 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -13,7 +13,10 @@ import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts' import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '#/state/queries/my-muted-accounts' import {findAllProfilesInQueryData as findAllProfilesInNotifsQueryData} from '#/state/queries/notifications/feed' -import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '#/state/queries/post-feed' +import { + type FeedPage, + findAllProfilesInQueryData as findAllProfilesInFeedsQueryData, +} from '#/state/queries/post-feed' import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by' import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes' import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by' @@ -112,6 +115,81 @@ export function useMaybeProfileShadow< }, [profile, shadow]) } +/** + * Takes a list of posts, and returns a list of DIDs that should be filtered out + * + * Note: it doesn't retroactively scan the cache, but only listens to new updates. + * The use case here is intended for removing a post from a feed after you mute the author + */ +export function usePostAuthorShadowFilter(data?: FeedPage[]) { + const [trackedDids, setTrackedDids] = useState( + () => + data?.flatMap(page => + page.slices.flatMap(slice => + slice.items.map(item => item.post.author.did), + ), + ) ?? [], + ) + const [authors, setAuthors] = useState( + new Map(), + ) + + const [prevData, setPrevData] = useState(data) + if (data !== prevData) { + const newAuthors = new Set(trackedDids) + let hasNew = false + for (const slice of data?.flatMap(page => page.slices) ?? []) { + for (const item of slice.items) { + const author = item.post.author + if (!newAuthors.has(author.did)) { + hasNew = true + newAuthors.add(author.did) + } + } + } + if (hasNew) setTrackedDids([...newAuthors]) + setPrevData(data) + } + + useEffect(() => { + const unsubs: Array<() => void> = [] + + for (const did of trackedDids) { + function onUpdate(value: Partial) { + setAuthors(prev => { + const prevValue = prev.get(did) + const next = new Map(prev) + next.set(did, { + blocked: Boolean(value.blockingUri ?? prevValue?.blocked ?? false), + muted: Boolean(value.muted ?? prevValue?.muted ?? false), + }) + return next + }) + } + emitter.addListener(did, onUpdate) + unsubs.push(() => { + emitter.removeListener(did, onUpdate) + }) + } + + return () => { + unsubs.map(fn => fn()) + } + }, [trackedDids]) + + return useMemo(() => { + const dids: Array = [] + + for (const [did, value] of authors.entries()) { + if (value.blocked || value.muted) { + dids.push(did) + } + } + + return dids + }, [authors]) +} + export function updateProfileShadow( queryClient: QueryClient, did: string, diff --git a/src/state/geolocation/config.ts b/src/state/geolocation/config.ts deleted file mode 100644 index 913b674cb..000000000 --- a/src/state/geolocation/config.ts +++ /dev/null @@ -1,141 +0,0 @@ -import {networkRetry} from '#/lib/async/retry' -import { - DEFAULT_GEOLOCATION_CONFIG, - GEOLOCATION_CONFIG_URL, -} from '#/state/geolocation/const' -import {emitGeolocationConfigUpdate} from '#/state/geolocation/events' -import {logger} from '#/state/geolocation/logger' -import {BAPP_CONFIG_DEV_BYPASS_SECRET, IS_DEV} from '#/env' -import {type Device, device} from '#/storage' - -async function getGeolocationConfig( - url: string, -): Promise { - const res = await fetch(url, { - headers: IS_DEV - ? { - 'x-dev-bypass-secret': BAPP_CONFIG_DEV_BYPASS_SECRET, - } - : undefined, - }) - - if (!res.ok) { - throw new Error(`config: fetch failed ${res.status}`) - } - - const json = await res.json() - - if (json.countryCode) { - /** - * Only construct known values here, ignore any extras. - */ - const config: Device['geolocation'] = { - countryCode: json.countryCode, - regionCode: json.regionCode ?? undefined, - ageRestrictedGeos: json.ageRestrictedGeos ?? [], - ageBlockedGeos: json.ageBlockedGeos ?? [], - } - logger.debug(`config: success`) - return config - } else { - return undefined - } -} - -/** - * Local promise used within this file only. - */ -let geolocationConfigResolution: Promise<{success: boolean}> | undefined - -/** - * Begin the process of resolving geolocation config. This should be called - * once at app start. - * - * THIS METHOD SHOULD NEVER THROW. - * - * This method is otherwise not used for any purpose. To ensure geolocation - * config is resolved, use {@link ensureGeolocationConfigIsResolved} - */ -export function beginResolveGeolocationConfig() { - /** - * Here for debug purposes. Uncomment to prevent hitting the remote geo service, and apply whatever data you require for testing. - */ - // if (__DEV__) { - // geolocationConfigResolution = new Promise(y => y({success: true})) - // device.set(['deviceGeolocation'], undefined) // clears GPS data - // device.set(['geolocation'], DEFAULT_GEOLOCATION_CONFIG) // clears bapp-config data - // return - // } - - geolocationConfigResolution = new Promise(async resolve => { - let success = true - - try { - // Try once, fail fast - const config = await getGeolocationConfig(GEOLOCATION_CONFIG_URL) - if (config) { - device.set(['geolocation'], config) - emitGeolocationConfigUpdate(config) - } else { - // endpoint should throw on all failures, this is insurance - throw new Error( - `geolocation config: nothing returned from initial request`, - ) - } - } catch (e: any) { - success = false - - logger.debug(`config: failed initial request`, { - safeMessage: e.message, - }) - - // set to default - device.set(['geolocation'], DEFAULT_GEOLOCATION_CONFIG) - - // retry 3 times, but don't await, proceed with default - networkRetry(3, () => getGeolocationConfig(GEOLOCATION_CONFIG_URL)) - .then(config => { - if (config) { - device.set(['geolocation'], config) - emitGeolocationConfigUpdate(config) - success = true - } else { - // endpoint should throw on all failures, this is insurance - throw new Error(`config: nothing returned from retries`) - } - }) - .catch((e: any) => { - // complete fail closed - logger.debug(`config: failed retries`, { - safeMessage: e.message, - }) - }) - } finally { - resolve({success}) - } - }) -} - -/** - * Ensure that geolocation config has been resolved, or at the very least attempted - * once. Subsequent retries will not be captured by this `await`. Those will be - * reported via {@link emitGeolocationConfigUpdate}. - */ -export async function ensureGeolocationConfigIsResolved() { - if (!geolocationConfigResolution) { - throw new Error(`config: beginResolveGeolocationConfig not called yet`) - } - - const cached = device.get(['geolocation']) - if (cached) { - logger.debug(`config: using cache`) - } else { - logger.debug(`config: no cache`) - const {success} = await geolocationConfigResolution - if (success) { - logger.debug(`config: resolved`) - } else { - logger.info(`config: failed to resolve`) - } - } -} diff --git a/src/state/geolocation/const.ts b/src/state/geolocation/const.ts deleted file mode 100644 index 916ac999f..000000000 --- a/src/state/geolocation/const.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {type GeolocationStatus} from '#/state/geolocation/types' -import {BAPP_CONFIG_DEV_URL, IS_DEV} from '#/env' -import {type Device} from '#/storage' - -export const IPCC_URL = `https://bsky.app/ipcc` -export const BAPP_CONFIG_URL_PROD = `/config.txt` -export const BAPP_CONFIG_URL = IS_DEV - ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_URL_PROD) - : BAPP_CONFIG_URL_PROD -export const GEOLOCATION_CONFIG_URL = BAPP_CONFIG_URL - -/** - * Default geolocation config. - */ -export const DEFAULT_GEOLOCATION_CONFIG: Device['geolocation'] = { - countryCode: 'US', - regionCode: 'CA', - ageRestrictedGeos: [], - ageBlockedGeos: [], -} - -/** - * Default geolocation status. - */ -export const DEFAULT_GEOLOCATION_STATUS: GeolocationStatus = { - countryCode: 'US', - regionCode: 'CA', - isAgeRestrictedGeo: false, - isAgeBlockedGeo: false, -} diff --git a/src/state/geolocation/events.ts b/src/state/geolocation/events.ts deleted file mode 100644 index 61433bb2a..000000000 --- a/src/state/geolocation/events.ts +++ /dev/null @@ -1,19 +0,0 @@ -import EventEmitter from 'eventemitter3' - -import {type Device} from '#/storage' - -const events = new EventEmitter() -const EVENT = 'geolocation-config-updated' - -export const emitGeolocationConfigUpdate = (config: Device['geolocation']) => { - events.emit(EVENT, config) -} - -export const onGeolocationConfigUpdate = ( - listener: (config: Device['geolocation']) => void, -) => { - events.on(EVENT, listener) - return () => { - events.off(EVENT, listener) - } -} diff --git a/src/state/geolocation/index.tsx b/src/state/geolocation/index.tsx deleted file mode 100644 index 8bddb23fb..000000000 --- a/src/state/geolocation/index.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React from 'react' - -import { - DEFAULT_GEOLOCATION_CONFIG, - DEFAULT_GEOLOCATION_STATUS, -} from '#/state/geolocation/const' -import {onGeolocationConfigUpdate} from '#/state/geolocation/events' -import {logger} from '#/state/geolocation/logger' -import { - type DeviceLocation, - type GeolocationStatus, -} from '#/state/geolocation/types' -import {useSyncedDeviceGeolocation} from '#/state/geolocation/useSyncedDeviceGeolocation' -import { - computeGeolocationStatus, - mergeGeolocation, -} from '#/state/geolocation/util' -import {type Device, device} from '#/storage' - -export * from '#/state/geolocation/config' -export * from '#/state/geolocation/types' -export * from '#/state/geolocation/util' - -type DeviceGeolocationContext = { - deviceGeolocation: DeviceLocation | undefined -} - -type DeviceGeolocationAPIContext = { - setDeviceGeolocation(deviceGeolocation: DeviceLocation): void -} - -type GeolocationConfigContext = { - config: Device['geolocation'] -} - -type GeolocationStatusContext = { - /** - * Merged geolocation from config and device GPS (if available). - */ - location: DeviceLocation - /** - * Computed geolocation status based on the merged location and config. - */ - status: GeolocationStatus -} - -const DeviceGeolocationContext = React.createContext({ - deviceGeolocation: undefined, -}) -DeviceGeolocationContext.displayName = 'DeviceGeolocationContext' - -const DeviceGeolocationAPIContext = - React.createContext({ - setDeviceGeolocation: () => {}, - }) -DeviceGeolocationAPIContext.displayName = 'DeviceGeolocationAPIContext' - -const GeolocationConfigContext = React.createContext({ - config: DEFAULT_GEOLOCATION_CONFIG, -}) -GeolocationConfigContext.displayName = 'GeolocationConfigContext' - -const GeolocationStatusContext = React.createContext({ - location: { - countryCode: undefined, - regionCode: undefined, - }, - status: DEFAULT_GEOLOCATION_STATUS, -}) -GeolocationStatusContext.displayName = 'GeolocationStatusContext' - -/** - * Provider of geolocation config and computed geolocation status. - */ -export function GeolocationStatusProvider({ - children, -}: { - children: React.ReactNode -}) { - const {deviceGeolocation} = React.useContext(DeviceGeolocationContext) - const [config, setConfig] = React.useState(() => { - const initial = device.get(['geolocation']) || DEFAULT_GEOLOCATION_CONFIG - return initial - }) - - React.useEffect(() => { - return onGeolocationConfigUpdate(config => { - setConfig(config!) - }) - }, []) - - const configContext = React.useMemo(() => ({config}), [config]) - const statusContext = React.useMemo(() => { - if (deviceGeolocation?.countryCode) { - logger.debug('has device geolocation available') - } - const geolocation = mergeGeolocation(deviceGeolocation, config) - const status = computeGeolocationStatus(geolocation, config) - // ensure this remains debug and never leaves device - logger.debug('result', {deviceGeolocation, geolocation, status, config}) - return {location: geolocation, status} - }, [config, deviceGeolocation]) - - return ( - - - {children} - - - ) -} - -/** - * Provider of providers. Provides device geolocation data to lower-level - * `GeolocationStatusProvider`, and device geolocation APIs to children. - */ -export function Provider({children}: {children: React.ReactNode}) { - const [deviceGeolocation, setDeviceGeolocation] = useSyncedDeviceGeolocation() - - const handleSetDeviceGeolocation = React.useCallback( - (location: DeviceLocation) => { - logger.debug('setting device geolocation') - setDeviceGeolocation({ - countryCode: location.countryCode ?? undefined, - regionCode: location.regionCode ?? undefined, - }) - }, - [setDeviceGeolocation], - ) - - return ( - ({setDeviceGeolocation: handleSetDeviceGeolocation}), - [handleSetDeviceGeolocation], - )}> - ({deviceGeolocation}), [deviceGeolocation])}> - {children} - - - ) -} - -export function useDeviceGeolocationApi() { - return React.useContext(DeviceGeolocationAPIContext) -} - -export function useGeolocationConfig() { - return React.useContext(GeolocationConfigContext) -} - -export function useGeolocationStatus() { - return React.useContext(GeolocationStatusContext) -} diff --git a/src/state/geolocation/logger.ts b/src/state/geolocation/logger.ts deleted file mode 100644 index afda81136..000000000 --- a/src/state/geolocation/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {Logger} from '#/logger' - -export const logger = Logger.create(Logger.Context.Geolocation) diff --git a/src/state/geolocation/types.ts b/src/state/geolocation/types.ts deleted file mode 100644 index 174761649..000000000 --- a/src/state/geolocation/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type DeviceLocation = { - countryCode: string | undefined - regionCode: string | undefined -} - -export type GeolocationStatus = DeviceLocation & { - isAgeRestrictedGeo: boolean - isAgeBlockedGeo: boolean -} diff --git a/src/state/geolocation/useRequestDeviceLocation.ts b/src/state/geolocation/useRequestDeviceLocation.ts deleted file mode 100644 index 64e05b056..000000000 --- a/src/state/geolocation/useRequestDeviceLocation.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {useCallback} from 'react' -import * as Location from 'expo-location' - -import {type DeviceLocation} from '#/state/geolocation/types' -import {getDeviceGeolocation} from '#/state/geolocation/util' - -export {PermissionStatus} from 'expo-location' - -export function useRequestDeviceLocation(): () => Promise< - | { - granted: true - location: DeviceLocation | undefined - } - | { - granted: false - status: { - canAskAgain: boolean - /** - * Enum, use `PermissionStatus` export for comparisons - */ - permissionStatus: Location.PermissionStatus - } - } -> { - return useCallback(async () => { - const status = await Location.requestForegroundPermissionsAsync() - - if (status.granted) { - return { - granted: true, - location: await getDeviceGeolocation(), - } - } else { - return { - granted: false, - status: { - canAskAgain: status.canAskAgain, - permissionStatus: status.status, - }, - } - } - }, []) -} diff --git a/src/state/geolocation/useSyncedDeviceGeolocation.ts b/src/state/geolocation/useSyncedDeviceGeolocation.ts deleted file mode 100644 index fea6198d4..000000000 --- a/src/state/geolocation/useSyncedDeviceGeolocation.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {useEffect, useRef} from 'react' -import * as Location from 'expo-location' -import {createPermissionHook} from 'expo-modules-core' - -import {logger} from '#/state/geolocation/logger' -import {getDeviceGeolocation} from '#/state/geolocation/util' -import {device, useStorage} from '#/storage' - -/** - * Location.useForegroundPermissions on web just errors if the navigator.permissions API is not available. - * We need to catch and ignore it, since it's effectively denied. - * @see https://github.com/expo/expo/blob/72f1562ed9cce5ff6dfe04aa415b71632a3d4b87/packages/expo-location/src/Location.ts#L290-L293 - */ -const useForegroundPermissions = createPermissionHook({ - getMethod: () => - Location.getForegroundPermissionsAsync().catch(error => { - logger.debug( - 'useForegroundPermission: error getting location permissions', - {safeMessage: error}, - ) - return { - status: Location.PermissionStatus.DENIED, - granted: false, - canAskAgain: false, - expires: 0, - } - }), - requestMethod: () => - Location.requestForegroundPermissionsAsync().catch(error => { - logger.debug( - 'useForegroundPermission: error requesting location permissions', - {safeMessage: error}, - ) - return { - status: Location.PermissionStatus.DENIED, - granted: false, - canAskAgain: false, - expires: 0, - } - }), -}) - -/** - * Hook to get and sync the device geolocation from the device GPS and store it - * using device storage. If permissions are not granted, it will clear any cached - * storage value. - */ -export function useSyncedDeviceGeolocation() { - const synced = useRef(false) - const [status] = useForegroundPermissions() - const [deviceGeolocation, setDeviceGeolocation] = useStorage(device, [ - 'deviceGeolocation', - ]) - - useEffect(() => { - async function get() { - // no need to set this more than once per session - if (synced.current) return - - logger.debug('useSyncedDeviceGeolocation: checking perms') - - if (status?.granted) { - const location = await getDeviceGeolocation() - if (location) { - logger.debug('useSyncedDeviceGeolocation: syncing location') - setDeviceGeolocation(location) - synced.current = true - } - } else { - const hasCachedValue = device.get(['deviceGeolocation']) !== undefined - - /** - * If we have a cached value, but user has revoked permissions, - * quietly (will take effect lazily) clear this out. - */ - if (hasCachedValue) { - logger.debug( - 'useSyncedDeviceGeolocation: clearing cached location, perms revoked', - ) - device.set(['deviceGeolocation'], undefined) - } - } - } - - get().catch(e => { - logger.error('useSyncedDeviceGeolocation: failed to sync', { - safeMessage: e, - }) - }) - }, [status, setDeviceGeolocation]) - - return [deviceGeolocation, setDeviceGeolocation] as const -} diff --git a/src/state/geolocation/util.ts b/src/state/geolocation/util.ts deleted file mode 100644 index c92b42b13..000000000 --- a/src/state/geolocation/util.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { - getCurrentPositionAsync, - type LocationGeocodedAddress, - reverseGeocodeAsync, -} from 'expo-location' - -import {logger} from '#/state/geolocation/logger' -import {type DeviceLocation} from '#/state/geolocation/types' -import {type Device} from '#/storage' - -/** - * Maps full US region names to their short codes. - * - * Context: in some cases, like on Android, we get the full region name instead - * of the short code. We may need to expand this in the future to other - * countries, hence the prefix. - */ -export const USRegionNameToRegionCode: { - [regionName: string]: string -} = { - Alabama: 'AL', - Alaska: 'AK', - Arizona: 'AZ', - Arkansas: 'AR', - California: 'CA', - Colorado: 'CO', - Connecticut: 'CT', - Delaware: 'DE', - Florida: 'FL', - Georgia: 'GA', - Hawaii: 'HI', - Idaho: 'ID', - Illinois: 'IL', - Indiana: 'IN', - Iowa: 'IA', - Kansas: 'KS', - Kentucky: 'KY', - Louisiana: 'LA', - Maine: 'ME', - Maryland: 'MD', - Massachusetts: 'MA', - Michigan: 'MI', - Minnesota: 'MN', - Mississippi: 'MS', - Missouri: 'MO', - Montana: 'MT', - Nebraska: 'NE', - Nevada: 'NV', - ['New Hampshire']: 'NH', - ['New Jersey']: 'NJ', - ['New Mexico']: 'NM', - ['New York']: 'NY', - ['North Carolina']: 'NC', - ['North Dakota']: 'ND', - Ohio: 'OH', - Oklahoma: 'OK', - Oregon: 'OR', - Pennsylvania: 'PA', - ['Rhode Island']: 'RI', - ['South Carolina']: 'SC', - ['South Dakota']: 'SD', - Tennessee: 'TN', - Texas: 'TX', - Utah: 'UT', - Vermont: 'VT', - Virginia: 'VA', - Washington: 'WA', - ['West Virginia']: 'WV', - Wisconsin: 'WI', - Wyoming: 'WY', -} - -/** - * Normalizes a `LocationGeocodedAddress` into a `DeviceLocation`. - * - * We don't want or care about the full location data, so we trim it down and - * normalize certain fields, like region, into the format we need. - */ -export function normalizeDeviceLocation( - location: LocationGeocodedAddress, -): DeviceLocation { - let {isoCountryCode, region} = location - - if (region) { - if (isoCountryCode === 'US') { - region = USRegionNameToRegionCode[region] ?? region - } - } - - return { - countryCode: isoCountryCode ?? undefined, - regionCode: region ?? undefined, - } -} - -/** - * Combines precise location data with the geolocation config fetched from the - * IP service, with preference to the precise data. - */ -export function mergeGeolocation( - location?: DeviceLocation, - config?: Device['geolocation'], -): DeviceLocation { - if (location?.countryCode) return location - return { - countryCode: config?.countryCode, - regionCode: config?.regionCode, - } -} - -/** - * Computes the geolocation status (age-restricted, age-blocked) based on the - * given location and geolocation config. `location` here should be merged with - * `mergeGeolocation()` ahead of time if needed. - */ -export function computeGeolocationStatus( - location: DeviceLocation, - config: Device['geolocation'], -) { - /** - * We can't do anything if we don't have this data. - */ - if (!location.countryCode) { - return { - ...location, - isAgeRestrictedGeo: false, - isAgeBlockedGeo: false, - } - } - - const isAgeRestrictedGeo = config?.ageRestrictedGeos?.some(rule => { - if (rule.countryCode === location.countryCode) { - if (!rule.regionCode) { - return true // whole country is blocked - } else if (rule.regionCode === location.regionCode) { - return true - } - } - }) - - const isAgeBlockedGeo = config?.ageBlockedGeos?.some(rule => { - if (rule.countryCode === location.countryCode) { - if (!rule.regionCode) { - return true // whole country is blocked - } else if (rule.regionCode === location.regionCode) { - return true - } - } - }) - - return { - ...location, - isAgeRestrictedGeo: !!isAgeRestrictedGeo, - isAgeBlockedGeo: !!isAgeBlockedGeo, - } -} - -export async function getDeviceGeolocation(): Promise { - try { - const geocode = await getCurrentPositionAsync() - const locations = await reverseGeocodeAsync({ - latitude: geocode.coords.latitude, - longitude: geocode.coords.longitude, - }) - const location = locations.at(0) - const normalized = location ? normalizeDeviceLocation(location) : undefined - return { - countryCode: normalized?.countryCode ?? undefined, - regionCode: normalized?.regionCode ?? undefined, - } - } catch (e) { - logger.error('getDeviceGeolocation: failed', { - safeMessage: e, - }) - return { - countryCode: undefined, - regionCode: undefined, - } - } -} diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 6da2249ba..9b12ac9e9 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -31,7 +31,6 @@ import {aggregateUserInterests} from '#/lib/api/feed/utils' import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip' import {DISCOVER_FEED_URI} from '#/lib/constants' import {logger} from '#/logger' -import {useAgeAssuranceContext} from '#/state/ageAssurance' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' import {useAgent} from '#/state/session' @@ -142,12 +141,8 @@ export function usePostFeedQuery( * available for the remainder of the session, so this delay only affects cold * loads. -esb */ - const {isReady: isAgeAssuranceReady} = useAgeAssuranceContext() const enabled = - opts?.enabled !== false && - Boolean(moderationOpts) && - Boolean(preferences) && - isAgeAssuranceReady + opts?.enabled !== false && Boolean(moderationOpts) && Boolean(preferences) const userInterests = aggregateUserInterests(preferences) const followingPinnedIndex = preferences?.savedFeeds?.findIndex( diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 3e92aa8ff..42b173a74 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -27,6 +27,7 @@ export function usePostQuery(uri: string | undefined) { const res = await agent.resolveHandle({ handle: urip.host, }) + // @ts-expect-error TODO new-sdk-migration urip.host = res.data.did } @@ -55,6 +56,7 @@ export function useGetPost() { const res = await agent.resolveHandle({ handle: urip.host, }) + // @ts-expect-error TODO new-sdk-migration urip.host = res.data.did } diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index 95eb94f8e..92689d534 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -36,6 +36,7 @@ export async function getPostgateRecord({ const res = await agent.resolveHandle({ handle: urip.host, }) + // @ts-expect-error TODO new-sdk-migration urip.host = res.data.did } diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 07195996e..4128ddd55 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -10,8 +10,6 @@ import {PROD_DEFAULT_FEED} from '#/lib/constants' import {replaceEqualDeep} from '#/lib/functions' import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' -import {useAgeAssuranceContext} from '#/state/ageAssurance' -import {makeAgeRestrictedModerationPrefs} from '#/state/ageAssurance/const' import {STALE} from '#/state/queries' import { DEFAULT_HOME_FEED_PREFS, @@ -24,6 +22,7 @@ import { } from '#/state/queries/preferences/types' import {useBlankPrefAuthedAgent as useAgent} from '#/state/session' import {saveLabelers} from '#/state/session/agent-config' +import {useAgeAssurance} from '#/ageAssurance' export * from '#/state/queries/preferences/const' export * from '#/state/queries/preferences/moderation' @@ -34,7 +33,7 @@ export const preferencesQueryKey = [preferencesQueryKeyRoot] export function usePreferencesQuery() { const agent = useAgent() - const {isAgeRestricted} = useAgeAssuranceContext() + const aa = useAgeAssurance() return useQuery({ staleTime: STALE.SECONDS.FIFTEEN, @@ -75,18 +74,19 @@ export function usePreferencesQuery() { }, select: useCallback( (data: UsePreferencesQueryResponse) => { - const isUnderage = (data.userAge || 0) < 18 - if (isUnderage || isAgeRestricted) { + /** + * Prefs are all downstream of age assurance now. For logged-out + * users, we override moderation prefs based on AA state. + */ + if (aa.state.access !== aa.Access.Full) { data = { ...data, - moderationPrefs: makeAgeRestrictedModerationPrefs( - data.moderationPrefs, - ), + moderationPrefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, } } return data }, - [isAgeRestricted], + [aa], ), }) } @@ -168,21 +168,6 @@ export function usePreferencesSetAdultContentMutation() { }) } -export function usePreferencesSetBirthDateMutation() { - const queryClient = useQueryClient() - const agent = useAgent() - - return useMutation({ - mutationFn: async ({birthDate}: {birthDate: Date}) => { - await agent.setPersonalDetails({birthDate: birthDate.toISOString()}) - // triggers a refetch - await queryClient.invalidateQueries({ - queryKey: preferencesQueryKey, - }) - }, - }) -} - export function useSetFeedViewPreferencesMutation() { const queryClient = useQueryClient() const agent = useAgent() diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts index 850fb3fb6..b40e38072 100644 --- a/src/state/queries/resolve-uri.ts +++ b/src/state/queries/resolve-uri.ts @@ -17,6 +17,7 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult { const urip = new AtUri(uri || '') const res = useResolveDidQuery(urip.host) if (res.data) { + // @ts-expect-error TODO new-sdk-migration urip.host = res.data return { ...res, diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index 5a34eb310..e760873fb 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -97,6 +97,7 @@ export async function getThreadgateRecord({ const res = await agent.resolveHandle({ handle: urip.host, }) + // @ts-expect-error TODO new-sdk-migration urip.host = res.data.did } diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index c58171bf9..eb56944ba 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -10,6 +10,9 @@ jest.mock('jwt-decode', () => ({ }, })) +jest.mock('../../birthdate') +jest.mock('../../../ageAssurance/data') + describe('session', () => { it('can log in and out', () => { let state = getInitialState([]) diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index abff0dc6b..c71ae9ca1 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -1,10 +1,12 @@ import { Agent as BaseAgent, + type AppBskyActorProfile, type AtprotoServiceType, type AtpSessionData, type AtpSessionEvent, BskyAgent, type Did, + type Un$Typed, } from '@atproto/api' import {type FetchHandler} from '@atproto/api/dist/agent' import {type SessionManager} from '@atproto/api/dist/session-manager' @@ -24,7 +26,13 @@ import { import {tryFetchGates} from '#/lib/statsig/statsig' import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' +import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' +import { + prefetchAgeAssuranceData, + setBirthdateForDid, + setCreatedAtForDid, +} from '#/ageAssurance/data' import {emitNetworkConfirmed, emitNetworkLost} from '../events' import {readCustomAppViewDidUri} from '../preferences/custom-appview-did' import {addSessionErrorLog} from './logging' @@ -81,11 +89,17 @@ export async function createAgentAndResume( } } + // after session is attached + const aa = prefetchAgeAssuranceData({agent}) + const proxyDid = readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY agent.configureProxy(proxyDid) - return agent.prepare(gates, moderation, onSessionChange) + return agent.prepare({ + resolvers: [gates, moderation, aa], + onSessionChange, + }) } export async function createAgentAndLogin( @@ -117,12 +131,16 @@ export async function createAgentAndLogin( const account = agentToSessionAccountOrThrow(agent) const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const moderation = configureModerationForAccount(agent, account) + const aa = prefetchAgeAssuranceData({agent}) const proxyDid = readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY agent.configureProxy(proxyDid) - return agent.prepare(gates, moderation, onSessionChange) + return agent.prepare({ + resolvers: [gates, moderation, aa], + onSessionChange, + }) } export async function createAgentAndCreateAccount( @@ -164,42 +182,122 @@ export async function createAgentAndCreateAccount( const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const moderation = configureModerationForAccount(agent, account) + const createdAt = new Date().toISOString() + const birthdate = birthDate.toISOString() + + /* + * Since we have a race with account creation, profile creation, and AA + * state, set these values locally to ensure sync reads. Values are written + * to the server in the next step, so on subsequent reloads, the server will + * be the source of truth. + */ + setCreatedAtForDid({did: account.did, createdAt}) + setBirthdateForDid({did: account.did, birthdate}) + snoozeBirthdateUpdateAllowedForDid(account.did) + // do this last + const aa = prefetchAgeAssuranceData({agent}) + // Not awaited so that we can still get into onboarding. // This is OK because we won't let you toggle adult stuff until you set the date. if (IS_PROD_SERVICE(service)) { - try { - networkRetry(1, async () => { - await agent.setPersonalDetails({birthDate: birthDate.toISOString()}) - await agent.overwriteSavedFeeds([ - { - ...DISCOVER_SAVED_FEED, - id: TID.nextStr(), - }, - { - ...TIMELINE_SAVED_FEED, - id: TID.nextStr(), - }, - ]) - - if (getAge(birthDate) < 18) { - await agent.api.com.atproto.repo.putRecord({ - repo: account.did, - collection: 'chat.bsky.actor.declaration', - rkey: 'self', - record: { - $type: 'chat.bsky.actor.declaration', - allowIncoming: 'none', - }, + Promise.allSettled( + [ + networkRetry(3, () => { + return agent.setPersonalDetails({ + birthDate: birthdate, }) - } - }) - } catch (e: any) { - logger.error(e, { - message: `session: createAgentAndCreateAccount failed to save personal details and feeds`, - }) - } + }).catch(e => { + logger.info(`createAgentAndCreateAccount: failed to set birthDate`) + throw e + }), + networkRetry(3, () => { + return agent.upsertProfile(prev => { + const next: Un$Typed = prev || {} + next.displayName = handle + next.createdAt = createdAt + return next + }) + }).catch(e => { + logger.info( + `createAgentAndCreateAccount: failed to set initial profile`, + ) + throw e + }), + networkRetry(1, () => { + return agent.overwriteSavedFeeds([ + { + ...DISCOVER_SAVED_FEED, + id: TID.nextStr(), + }, + { + ...TIMELINE_SAVED_FEED, + id: TID.nextStr(), + }, + ]) + }).catch(e => { + logger.info( + `createAgentAndCreateAccount: failed to set initial feeds`, + ) + throw e + }), + getAge(birthDate) < 18 && + networkRetry(3, () => { + return agent.com.atproto.repo.putRecord({ + repo: account.did, + collection: 'chat.bsky.actor.declaration', + rkey: 'self', + record: { + $type: 'chat.bsky.actor.declaration', + allowIncoming: 'none', + }, + }) + }).catch(e => { + logger.info( + `createAgentAndCreateAccount: failed to set chat declaration`, + ) + throw e + }), + ].filter(Boolean), + ).then(promises => { + const rejected = promises.filter(p => p.status === 'rejected') + if (rejected.length > 0) { + logger.error( + `session: createAgentAndCreateAccount failed to save personal details and feeds`, + ) + } + }) } else { - agent.setPersonalDetails({birthDate: birthDate.toISOString()}) + Promise.allSettled( + [ + networkRetry(3, () => { + return agent.setPersonalDetails({ + birthDate: birthDate.toISOString(), + }) + }).catch(e => { + logger.info(`createAgentAndCreateAccount: failed to set birthDate`) + throw e + }), + networkRetry(3, () => { + return agent.upsertProfile(prev => { + const next: Un$Typed = prev || {} + next.createdAt = prev?.createdAt || new Date().toISOString() + return next + }) + }).catch(e => { + logger.info( + `createAgentAndCreateAccount: failed to set initial profile`, + ) + throw e + }), + ].filter(Boolean), + ).then(promises => { + const rejected = promises.filter(p => p.status === 'rejected') + if (rejected.length > 0) { + logger.error( + `session: createAgentAndCreateAccount failed to save personal details and feeds`, + ) + } + }) } try { @@ -213,7 +311,10 @@ export async function createAgentAndCreateAccount( readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY agent.configureProxy(proxyDid) - return agent.prepare(gates, moderation, onSessionChange) + return agent.prepare({ + resolvers: [gates, moderation, aa], + onSessionChange, + }) } export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount { @@ -320,18 +421,20 @@ class BskyAppAgent extends BskyAgent { } } - async prepare( + async prepare({ + resolvers, + onSessionChange, + }: { // Not awaited in the calling code so we can delay blocking on them. - gates: Promise, - moderation: Promise, + resolvers: Promise[] onSessionChange: ( agent: BskyAgent, did: string, event: AtpSessionEvent, - ) => void, - ) { + ) => void + }) { // There's nothing else left to do, so block on them here. - await Promise.all([gates, moderation]) + await Promise.all(resolvers) // Now the agent is ready. const account = agentToSessionAccountOrThrow(this) diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index a18e372c4..3f12c5bfb 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -24,6 +24,11 @@ import { type SessionApiContext, type SessionStateContext, } from '#/state/session/types' +import {useOnboardingDispatch} from '#/state/shell/onboarding' +import { + clearAgeAssuranceData, + clearAgeAssuranceDataForDid, +} from '#/ageAssurance/data' const StateContext = React.createContext({ accounts: [], @@ -91,6 +96,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const cancelPendingTask = useOneTaskAtATime() const [store] = React.useState(() => new SessionStore()) const state = React.useSyncExternalStore(store.subscribe, store.getState) + const onboardingDispatch = useOnboardingDispatch() const onAgentSessionChange = React.useCallback( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { @@ -166,6 +172,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { logContext => { addSessionDebugLog({type: 'method:start', method: 'logout'}) cancelPendingTask() + const prevState = store.getState() store.dispatch({ type: 'logged-out-current-account', }) @@ -175,8 +182,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) { {statsig: true}, ) addSessionDebugLog({type: 'method:end', method: 'logout'}) + if (prevState.currentAgentState.did) { + clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did}) + } + // reset onboarding flow on logout + onboardingDispatch({type: 'skip'}) }, - [store, cancelPendingTask], + [store, cancelPendingTask, onboardingDispatch], ) const logoutEveryAccount = React.useCallback< @@ -194,12 +206,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) { {statsig: true}, ) addSessionDebugLog({type: 'method:end', method: 'logout'}) + clearAgeAssuranceData() + // reset onboarding flow on logout + onboardingDispatch({type: 'skip'}) }, - [store, cancelPendingTask], + [store, cancelPendingTask, onboardingDispatch], ) const resumeSession = React.useCallback( - async storedAccount => { + async (storedAccount, isSwitchingAccounts = false) => { addSessionDebugLog({ type: 'method:start', method: 'resumeSession', @@ -220,8 +235,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) { newAccount: account, }) addSessionDebugLog({type: 'method:end', method: 'resumeSession', account}) + if (isSwitchingAccounts) { + // reset onboarding flow on switch account + onboardingDispatch({type: 'skip'}) + } }, - [store, onAgentSessionChange, cancelPendingTask], + [store, onAgentSessionChange, cancelPendingTask, onboardingDispatch], ) const partialRefreshSession = React.useCallback< @@ -254,6 +273,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { accountDid: account.did, }) addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) + clearAgeAssuranceDataForDid({did: account.did}) }, [store, cancelPendingTask], ) diff --git a/src/state/session/types.ts b/src/state/session/types.ts index 4621b4f04..2c1da187c 100644 --- a/src/state/session/types.ts +++ b/src/state/session/types.ts @@ -38,7 +38,10 @@ export type SessionApiContext = { logoutEveryAccount: ( logContext: LogEvents['account:loggedOut']['logContext'], ) => void - resumeSession: (account: SessionAccount) => Promise + resumeSession: ( + account: SessionAccount, + isSwitchingAccounts?: boolean, + ) => Promise removeAccount: (account: SessionAccount) => void /** * Calls `getSession` and updates select fields on the current account and diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx index e62f01447..77bd2e46e 100644 --- a/src/state/shell/composer/index.tsx +++ b/src/state/shell/composer/index.tsx @@ -42,8 +42,21 @@ export interface ComposerOpts { mention?: string // handle of user to mention openEmojiPicker?: (pos: EmojiPickerPosition | undefined) => void text?: string - imageUris?: {uri: string; width: number; height: number; altText?: string; blobRef?: BlobRef}[] - videoUri?: {uri: string; width: number; height: number; blobRef?: BlobRef; altText?: string} + imageUris?: { + uri: string + width: number + height: number + altText?: string + blobRef?: BlobRef + }[] + videoUri?: { + uri: string + width: number + height: number + blobRef?: BlobRef + altText?: string + } + openGallery?: boolean } type StateContext = ComposerOpts | undefined diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx index 809a521bf..461551490 100644 --- a/src/state/shell/index.tsx +++ b/src/state/shell/index.tsx @@ -2,7 +2,6 @@ import {Provider as ColorModeProvider} from './color-mode' import {Provider as DrawerOpenProvider} from './drawer-open' import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled' import {Provider as MinimalModeProvider} from './minimal-mode' -import {Provider as OnboardingProvider} from './onboarding' import {Provider as ShellLayoutProvder} from './shell-layout' import {Provider as TickEveryMinuteProvider} from './tick-every-minute' @@ -23,9 +22,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { - - {children} - + {children} diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx index 17fe18840..c5b7d1c2b 100644 --- a/src/state/unstable-post-source.tsx +++ b/src/state/unstable-post-source.tsx @@ -81,6 +81,7 @@ export function useUnstablePostSource(key: string) { */ export function buildPostSourceKey(key: string, handle: string) { const urip = new AtUri(key) + // @ts-expect-error TODO new-sdk-migration urip.host = handle return urip.toString() } diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 6eb3ca82e..8341f405b 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -1,4 +1,5 @@ import {type ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' +import {type Geolocation} from '#/geolocation/types' /** * Device data that's specific to the device and does not vary based account @@ -25,13 +26,21 @@ export type Device = { regionCode: string | undefined }[] } + + /** + * The raw response from the geolocation service, if available. We + * cache this here and update it lazily on session start. + */ + geolocationServiceResponse?: Geolocation /** * The GPS-based geolocation, if the user has granted permission. */ - deviceGeolocation?: { - countryCode: string | undefined - regionCode: string | undefined - } + deviceGeolocation?: Geolocation + /** + * The merged geolocation, combining `geolocationServiceResponse` and + * `deviceGeolocation`, with preference to `deviceGeolocation`. + */ + mergedGeolocation?: Geolocation trendingBetaEnabled: boolean devMode: boolean @@ -53,4 +62,10 @@ export type Device = { export type Account = { searchTermHistory?: string[] searchAccountHistory?: string[] + + /** + * The ISO date string of when this account's birthdate was last updated on + * this device. + */ + birthdateLastUpdatedAt?: string } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 2ef6b42b4..57cdd3af1 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -174,6 +174,7 @@ export const ComposePost = ({ text: initText, imageUris: initImageUris, videoUri: initVideoUri, + openGallery, cancelRef, }: Props & { cancelRef?: React.RefObject @@ -723,6 +724,7 @@ export const ComposePost = ({ }} currentLanguages={currentLanguages} onSelectLanguage={onSelectLanguage} + openGallery={openGallery} /> ) @@ -1361,6 +1363,7 @@ function ComposerFooter({ onAddPost, currentLanguages, onSelectLanguage, + openGallery, }: { post: PostDraft dispatch: (action: PostAction) => void @@ -1371,6 +1374,7 @@ function ComposerFooter({ onAddPost: () => void currentLanguages: string[] onSelectLanguage?: (language: string) => void + openGallery?: boolean }) { const t = useTheme() const {_} = useLingui() @@ -1492,6 +1496,7 @@ function ComposerFooter({ allowedAssetTypes={selectedAssetsType} selectedAssetsCount={selectedAssetsCount} onSelectAssets={onSelectAssets} + autoOpen={openGallery} /> void + /** + * If true, automatically open the media picker when the component mounts. + */ + autoOpen?: boolean } /** @@ -359,12 +363,14 @@ export function SelectMediaButton({ allowedAssetTypes, selectedAssetsCount, onSelectAssets, + autoOpen, }: SelectMediaButtonProps) { const {_} = useLingui() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() const sheetWrapper = useSheetWrapper() const t = useTheme() + const hasAutoOpened = useRef(false) const selectionCountRemaining = MAX_IMAGES - selectedAssetsCount @@ -461,6 +467,13 @@ export function SelectMediaButton({ selectionCountRemaining, ]) + useEffect(() => { + if (autoOpen && !hasAutoOpened.current && !disabled) { + hasAutoOpened.current = true + onPressSelectMedia() + } + }, [autoOpen, disabled, onPressSelectMedia]) + const enableSquareButtons = useEnableSquareButtons() return ( diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx index ee61be979..cf985ab4a 100644 --- a/src/view/com/notifications/NotificationFeed.tsx +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -9,6 +9,7 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' import {cleanError} from '#/lib/strings/errors' import {s} from '#/lib/styles' import {logger} from '#/logger' @@ -49,6 +50,7 @@ export function NotificationFeed({ const t = useTheme() const {_} = useLingui() const moderationOpts = useModerationOpts() + const trackPostView = usePostViewTracking('Notifications') const { data, isFetching, @@ -183,6 +185,16 @@ export function NotificationFeed({ onEndReached={onEndReached} onEndReachedThreshold={2} onScrolledDownChange={onScrolledDownChange} + onItemSeen={item => { + if ( + (item.type === 'reply' || + item.type === 'mention' || + item.type === 'quote') && + item.subject + ) { + trackPostView(item.subject) + } + }} contentContainerStyle={s.contentContainer} desktopFixedHeight initialNumToRender={initialNumToRender} diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx index 8f178968d..1f48f50d3 100644 --- a/src/view/com/post-thread/PostQuotes.tsx +++ b/src/view/com/post-thread/PostQuotes.tsx @@ -9,6 +9,7 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -44,6 +45,7 @@ export function PostQuotes({uri}: {uri: string}) { const {_} = useLingui() const initialNumToRender = useInitialNumToRender() const [isPTRing, setIsPTRing] = useState(false) + const trackPostView = usePostViewTracking('PostQuotes') const { data: resolvedUri, @@ -123,6 +125,7 @@ export function PostQuotes({uri}: {uri: string}) { onRefresh={onRefresh} onEndReached={onEndReached} onEndReachedThreshold={4} + onItemSeen={item => trackPostView(item.post)} ListFooterComponent={ { // wraps a slice item, and replaces it with a showLessFollowup item // if the user has pressed show less on it @@ -498,7 +510,11 @@ let PostFeed = ({ // eslint-disable-next-line @typescript-eslint/no-shadow item => item.uri === slice.feedPostUri, ) - if (item && AppBskyEmbedVideo.isView(item.post.embed)) { + if ( + item && + AppBskyEmbedVideo.isView(item.post.embed) && + !blockedOrMutedAuthors.includes(item.post.author.did) + ) { videos.push({ item, feedContext: slice.feedContext, @@ -580,6 +596,18 @@ let PostFeed = ({ 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, }) } + // Show composer prompt for Discover and Following feeds + if ( + hasSession && + (feedUriOrActorDid === DISCOVER_FEED_URI || + feed === 'following') && + gate('show_composer_prompt') + ) { + arr.push({ + type: 'composerPrompt', + key: 'composerPrompt-' + sliceIndex, + }) + } } else if (sliceIndex === 15) { if (areVideoFeedsEnabled && !trendingVideoDisabled) { arr.push({ @@ -593,6 +621,16 @@ let PostFeed = ({ key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, }) } + } else if (feedKind === 'following') { + if (sliceIndex === 0) { + // Show composer prompt for Following feed + if (hasSession && gate('show_composer_prompt')) { + arr.push({ + type: 'composerPrompt', + key: 'composerPrompt-' + sliceIndex, + }) + } + } } else if (feedKind === 'profile') { if (sliceIndex === 5) { arr.push({ @@ -626,6 +664,12 @@ let PostFeed = ({ key: 'sliceFallbackMarker-' + sliceIndex + '-' + lastFetchedAt, }) + } else if ( + slice.items.some(item => + blockedOrMutedAuthors.includes(item.post.author.did), + ) + ) { + // skip } else if (slice.isIncompleteThread && slice.items.length >= 3) { const beforeLast = slice.items.length - 2 const last = slice.items.length - 1 @@ -707,6 +751,7 @@ let PostFeed = ({ isEmpty, lastFetchedAt, data, + feed, feedType, feedUriOrActorDid, feedTab, @@ -722,6 +767,8 @@ let PostFeed = ({ hasPressedShowLessUris, ageAssuranceBannerState, isCurrentFeedAtStartupSelected, + gate, + blockedOrMutedAuthors, ]) // events @@ -812,6 +859,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'composerPrompt') { + return } else if (row.type === 'interstitialTrendingVideos') { return } else if (row.type === 'fallbackMarker') { diff --git a/src/view/com/posts/PostFeedReason.tsx b/src/view/com/posts/PostFeedReason.tsx index e9a96871d..e23a77584 100644 --- a/src/view/com/posts/PostFeedReason.tsx +++ b/src/view/com/posts/PostFeedReason.tsx @@ -10,7 +10,7 @@ import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin' import {Repost_Stroke2_Corner3_Rounded as RepostIcon} from '#/components/icons/Repost' -import {Link, WebOnlyInlineLinkText} from '#/components/Link' +import {Link} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {Text} from '#/components/Typography' import {FeedNameText} from '../util/FeedInfoText' diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index 3bafc0c98..39c251293 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -71,6 +71,32 @@ export function ProfileFollowers({name}: {name: string}) { return [] }, [data]) + // Track pagination events - fire for page 3+ (pages 1-2 may auto-load) + const paginationTrackingRef = React.useRef<{ + did: string | undefined + page: number + }>({did: undefined, page: 0}) + React.useEffect(() => { + const currentPageCount = data?.pages?.length || 0 + // Reset tracking when profile changes + if (paginationTrackingRef.current.did !== resolvedDid) { + paginationTrackingRef.current = {did: resolvedDid, page: currentPageCount} + return + } + if ( + resolvedDid && + currentPageCount >= 3 && + currentPageCount > paginationTrackingRef.current.page + ) { + logger.metric('profile:followers:paginate', { + contextProfileDid: resolvedDid, + itemCount: followers.length, + page: currentPageCount, + }) + } + paginationTrackingRef.current.page = currentPageCount + }, [data?.pages?.length, resolvedDid, followers.length]) + const onRefresh = React.useCallback(async () => { setIsPTRing(true) try { @@ -96,6 +122,16 @@ export function ProfileFollowers({name}: {name: string}) { [resolvedDid], ) + // track pageview + React.useEffect(() => { + if (resolvedDid) { + logger.metric('profile:followers:view', { + contextProfileDid: resolvedDid, + isOwnProfile: isMe, + }) + } + }, [resolvedDid, isMe]) + // track seen items const seenItemsRef = React.useRef>(new Set()) React.useEffect(() => { diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index 28e323041..243f3705b 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -82,6 +82,32 @@ export function ProfileFollows({name}: {name: string}) { return [] }, [data]) + // Track pagination events - fire for page 3+ (pages 1-2 may auto-load) + const paginationTrackingRef = React.useRef<{ + did: string | undefined + page: number + }>({did: undefined, page: 0}) + React.useEffect(() => { + const currentPageCount = data?.pages?.length || 0 + // Reset tracking when profile changes + if (paginationTrackingRef.current.did !== resolvedDid) { + paginationTrackingRef.current = {did: resolvedDid, page: currentPageCount} + return + } + if ( + resolvedDid && + currentPageCount >= 3 && + currentPageCount > paginationTrackingRef.current.page + ) { + logger.metric('profile:following:paginate', { + contextProfileDid: resolvedDid, + itemCount: follows.length, + page: currentPageCount, + }) + } + paginationTrackingRef.current.page = currentPageCount + }, [data?.pages?.length, resolvedDid, follows.length]) + const onRefresh = React.useCallback(async () => { setIsPTRing(true) try { @@ -99,7 +125,7 @@ export function ProfileFollows({name}: {name: string}) { } catch (err) { logger.error('Failed to load more follows', {error: err}) } - }, [error, fetchNextPage, hasNextPage, isFetchingNextPage]) + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) const renderItemWithContext = React.useCallback( ({item, index}: {item: ActorDefs.ProfileView; index: number}) => @@ -107,6 +133,16 @@ export function ProfileFollows({name}: {name: string}) { [resolvedDid], ) + // track pageview + React.useEffect(() => { + if (resolvedDid) { + logger.metric('profile:following:view', { + contextProfileDid: resolvedDid, + isOwnProfile: isMe, + }) + } + }, [resolvedDid, isMe]) + // track seen items const seenItemsRef = React.useRef>(new Set()) React.useEffect(() => { diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index f7eee5fee..e8c972c66 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -3,12 +3,15 @@ import {View} from 'react-native' import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' -import {Sentry} from '#/logger/sentry/lib' import {useSetThemePrefs} from '#/state/shell' import {ListContained} from '#/view/screens/Storybook/ListContained' import {atoms as a, ThemeProvider} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' +import { + useDeviceGeolocationApi, + useRequestDeviceGeolocation, +} from '#/geolocation' import {Admonitions} from './Admonitions' import {Breakpoints} from './Breakpoints' import {Buttons} from './Buttons' @@ -45,6 +48,8 @@ function StorybookInner() { const {setColorMode, setDarkTheme} = useSetThemePrefs() const [showContainedList, setShowContainedList] = React.useState(false) const navigation = useNavigation() + const requestDeviceGeolocation = useRequestDeviceGeolocation() + const {setDeviceGeolocation} = useDeviceGeolocationApi() return ( <> @@ -97,11 +102,17 @@ function StorybookInner() { Open Shared Prefs Tester diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx index 8a0ee6005..3e4a551f0 100644 --- a/src/view/shell/Composer.ios.tsx +++ b/src/view/shell/Composer.ios.tsx @@ -45,6 +45,7 @@ export function Composer({}: {winHeight: number}) { text={state?.text} imageUris={state?.imageUris} videoUri={state?.videoUri} + openGallery={state?.openGallery} /> diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index a17de6163..a01cc9744 100644 --- a/src/view/shell/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -55,6 +55,7 @@ export function Composer({winHeight}: {winHeight: number}) { text={state.text} imageUris={state.imageUris} videoUri={state.videoUri} + openGallery={state.openGallery} /> ) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 4f1baee59..639a3dc15 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -111,6 +111,9 @@ function Inner({state}: {state: ComposerOpts}) { text={state.text} imageUris={state.imageUris} videoUri={state.videoUri} + openGallery={state.openGallery} + videoUri={state.videoUri} + openGallery={state.openGallery} /> diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 5075f05cb..c12141adb 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -13,7 +13,6 @@ import {useNotificationsRegistration} from '#/lib/notifications/notifications' import {isStateAtTabRoot} from '#/lib/routes/helpers' import {isAndroid, isIOS} from '#/platform/detection' import {useDialogFullyExpandedCountContext} from '#/state/dialogs' -import {useGeolocationStatus} from '#/state/geolocation' import {useSession} from '#/state/session' import { useIsDrawerOpen, @@ -27,7 +26,6 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {setSystemUITheme} from '#/alf/util/systemUI' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' -import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' @@ -38,6 +36,9 @@ import { usePolicyUpdateContext, } from '#/components/PolicyUpdateOverlay' import {Outlet as PortalOutlet} from '#/components/Portal' +import {useAgeAssurance} from '#/ageAssurance' +import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' +import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' import {RoutesContainer, TabsNavigator} from '#/Navigation' import {BottomSheetOutlet} from '../../../modules/bottom-sheet' import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView' @@ -193,7 +194,7 @@ function DrawerLayout({children}: {children: React.ReactNode}) { export function Shell() { const t = useTheme() - const {status: geolocation} = useGeolocationStatus() + const aa = useAgeAssurance() const fullyExpandedCount = useDialogFullyExpandedCountContext() useIntentHandler() @@ -213,13 +214,15 @@ export function Shell() { navigationBar: t.name !== 'light' ? 'light' : 'dark', }} /> - {geolocation?.isAgeBlockedGeo ? ( - + {aa.state.access === aa.Access.None ? ( + ) : ( )} + + ) } diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 8ce65874e..961a5069c 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -8,7 +8,6 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar' import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type NavigationProp} from '#/lib/routes/types' -import {useGeolocationStatus} from '#/state/geolocation' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' import {useCloseAllActiveElements} from '#/state/util' @@ -17,7 +16,6 @@ import {ModalsContainer} from '#/view/com/modals/Modal' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' -import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' @@ -29,6 +27,9 @@ import { } from '#/components/PolicyUpdateOverlay' import {Outlet as PortalOutlet} from '#/components/Portal' import {WelcomeModal} from '#/components/WelcomeModal' +import {useAgeAssurance} from '#/ageAssurance' +import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' +import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' import {FlatNavigator, RoutesContainer} from '#/Navigation' import {Composer} from './Composer.web' import {DrawerContent} from './Drawer' @@ -172,16 +173,18 @@ function ShellInner() { export function Shell() { const t = useTheme() - const {status: geolocation} = useGeolocationStatus() + const aa = useAgeAssurance() return ( - {geolocation?.isAgeBlockedGeo ? ( - + {aa.state.access === aa.Access.None ? ( + ) : ( )} + + ) } diff --git a/web/index.html b/web/index.html index 902ed140a..007c24c26 100644 --- a/web/index.html +++ b/web/index.html @@ -74,11 +74,19 @@ width: 100%; } #splash { + display: flex; position: fixed; - width: 125px; - left: 50%; - top: 50%; - transform: translateX(-50%) translateY(-50%) translateY(-50px); + top: 0; + bottom: 0; + left: 0; + right: 0; + align-items: center; + justify-content: center; + } + #splash svg { + position: relative; + top: -50px; + width: 100px; } /** * We need these styles to prevent shifting due to scrollbar show/hide on diff --git a/yarn.lock b/yarn.lock index a6d1b2c7b..2ba69676d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,10 +20,22 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@atproto-labs/fetch-node@0.1.10", "@atproto-labs/fetch-node@^0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.10.tgz#bfed87125503d8227e6755399a3d6c8f1fade941" - integrity sha512-o7hGaonA71A6p7O107VhM6UBUN/g9tTyYohMp1q0Kf6xQ4npnuZYRSHSf2g6reSfGQJ1GoFNjBObETTT1ge/jQ== +"@atproto-labs/did-resolver@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@atproto-labs/did-resolver/-/did-resolver-0.2.4.tgz#3df8f94845fae10bb284303d6e73ffaa5a91b158" + integrity sha512-sbXxBnAJWsKv/FEGG6a/WLz7zQYUr1vA2TXvNnPwwJQJCjPwEJMOh1vM22wBr185Phy7D2GD88PcRokn7eUVyw== + dependencies: + "@atproto-labs/fetch" "0.2.3" + "@atproto-labs/pipe" "0.1.1" + "@atproto-labs/simple-store" "0.3.0" + "@atproto-labs/simple-store-memory" "0.1.4" + "@atproto/did" "0.2.3" + zod "^3.23.8" + +"@atproto-labs/fetch-node@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.2.0.tgz#438989f3165f52e21e7636fb87ea9c7317ae7f2a" + integrity sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q== dependencies: "@atproto-labs/fetch" "0.2.3" "@atproto-labs/pipe" "0.1.1" @@ -62,51 +74,37 @@ resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz#65c0a5c949fe6c8dc3bdaf13ab40848f20073593" integrity sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ== -"@atproto-labs/xrpc-utils@0.0.22": - version "0.0.22" - resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.22.tgz#b842faced84647ab691c1fdd07bfc4fd398c741f" - integrity sha512-XGDbTmVgibtcR6FwJepD/QKofG1B5EBBPebk/IVF4aHeBE/6jOd7DnfuKrBimv2GJ2JGrlvHXmjYZdfmCtYEbw== +"@atproto-labs/xrpc-utils@0.0.24": + version "0.0.24" + resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.24.tgz#0546778b9b83854d8a160dc4dea145e5a23ae8fc" + integrity sha512-wWXd2Ht47UsL/UbDCr3twMFSZrh0xSI56u4O3kz0DTU4G+530mCG71mMVE6eeYcR+j6FEjp7o2Ld6c7wFklYGw== dependencies: - "@atproto/xrpc" "^0.7.5" - "@atproto/xrpc-server" "^0.9.5" + "@atproto/xrpc" "^0.7.6" + "@atproto/xrpc-server" "^0.10.0" -"@atproto/api@^0.17.1": - version "0.17.1" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.17.1.tgz#37e261a4739022e6ba1cfd58efef282be5a2328f" - integrity sha512-MjW6zVP8PsxPhvOpSWIZLoEiFOK0oKIokeHoUgG1CLHGXNnz2TwBGrrPglyiE0j9GYFD5p6lAsHx8Dbx/9j5vg== +"@atproto/api@^0.18.4": + version "0.18.4" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.4.tgz#e6742f3b81acec2bcf63dd3787304166eb2891cb" + integrity sha512-+kSxto/GRFXRFFlGwfERrwEKnC6OqTgK34BUToer/Fv08q4WMR+GYPRabbWlnDoJWu3owcQfeYdcblQ88vi16g== dependencies: - "@atproto/common-web" "^0.4.3" - "@atproto/lexicon" "^0.5.1" - "@atproto/syntax" "^0.4.1" - "@atproto/xrpc" "^0.7.5" + "@atproto/common-web" "^0.4.6" + "@atproto/lexicon" "^0.5.2" + "@atproto/syntax" "^0.4.2" + "@atproto/xrpc" "^0.7.6" await-lock "^2.2.2" multiformats "^9.9.0" tlds "^1.234.0" zod "^3.23.8" -"@atproto/api@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.0.tgz#d8c54ddc4521d915f0af238a4bfebd119e18197f" - integrity sha512-2GxKPhhvMocDjRU7VpNj+cvCdmCHVAmRwyfNgRLMrJtPZvrosFoi9VATX+7eKN0FZvYvy8KdLSkCcpP2owH3IA== +"@atproto/aws@^0.2.31": + version "0.2.31" + resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.31.tgz#e46d7db34ee57c4f9817269f1e73a7eddba2b9b8" + integrity sha512-jzw0v4KttNqeEZtkIujIOCvWABJl4m8WQuSOlJtYeJhn/xaBC1+CNt7qnvYCzBd+T5nncZU8junHSEbOMdVwiA== dependencies: - "@atproto/common-web" "^0.4.3" - "@atproto/lexicon" "^0.5.1" - "@atproto/syntax" "^0.4.1" - "@atproto/xrpc" "^0.7.5" - await-lock "^2.2.2" - multiformats "^9.9.0" - tlds "^1.234.0" - zod "^3.23.8" - -"@atproto/aws@^0.2.30": - version "0.2.30" - resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.30.tgz#17c882a2ec838fc6ff2a6c76f66a12e5f29d227e" - integrity sha512-oB/whUIWwSOEqUazz5meN3/AlovBdRc224uRPNy9aC6+qmNKfHKiMfo0ytFhGYdm4GtEd2HYwIT3KR/Rtc2RRA== - dependencies: - "@atproto/common" "^0.4.12" - "@atproto/common-web" "^0.4.3" + "@atproto/common" "^0.5.0" + "@atproto/common-web" "^0.4.4" "@atproto/crypto" "^0.4.4" - "@atproto/repo" "^0.8.10" + "@atproto/repo" "^0.8.11" "@aws-sdk/client-cloudfront" "^3.879.0" "@aws-sdk/client-kms" "^3.879.0" "@aws-sdk/client-s3" "^3.879.0" @@ -116,23 +114,23 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.188": - version "0.0.188" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.188.tgz#d1bbf17dfb85b5efbaa0bfd9d1b99e37e79425c0" - integrity sha512-ZOYKo2W/pbTccglvATrzoP8Md3W5+zHJm+XRpLrKLynqNa5QmoLtZgv2iKKfwTWx1b8BZ6YNAVcfYct1T0nTxA== - dependencies: - "@atproto-labs/fetch-node" "0.1.10" - "@atproto-labs/xrpc-utils" "0.0.22" - "@atproto/api" "^0.17.1" - "@atproto/common" "^0.4.12" - "@atproto/crypto" "^0.4.4" - "@atproto/did" "^0.2.1" - "@atproto/identity" "^0.4.9" - "@atproto/lexicon" "^0.5.1" - "@atproto/repo" "^0.8.10" - "@atproto/sync" "^0.1.35" - "@atproto/syntax" "^0.4.1" - "@atproto/xrpc-server" "^0.9.5" +"@atproto/bsky@^0.0.199": + version "0.0.199" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.199.tgz#44ee12e0192b0f946745317bed474525f45e02c9" + integrity sha512-DtW1G5k8dSBP5rs3fid0RRV+5efsWe2WG0AB1MxhnhU8718CnpGsPX8sx6uZtMvaTKgDynfN7fPNtaxNomVGFA== + dependencies: + "@atproto-labs/fetch-node" "0.2.0" + "@atproto-labs/xrpc-utils" "0.0.24" + "@atproto/api" "^0.18.4" + "@atproto/common" "^0.5.2" + "@atproto/crypto" "^0.4.5" + "@atproto/did" "^0.2.3" + "@atproto/identity" "^0.4.10" + "@atproto/lexicon" "^0.5.2" + "@atproto/repo" "^0.8.11" + "@atproto/sync" "^0.1.38" + "@atproto/syntax" "^0.4.2" + "@atproto/xrpc-server" "^0.10.2" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" "@connectrpc/connect-express" "^1.1.4" @@ -166,12 +164,12 @@ undici "^6.19.8" zod "3.23.8" -"@atproto/bsync@^0.0.22": - version "0.0.22" - resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.22.tgz#eec667dc90200bcea91dd1055c6bfad7fcf5a1e5" - integrity sha512-V2sEHDJQKCWt4Lx8KHFRy6D6IHJgtUfOsPGXbKYzrCVJF/36v3XACim7uuUxmrd/rxtY/zP5sUVkvv0o5waaZw== +"@atproto/bsync@^0.0.23": + version "0.0.23" + resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.23.tgz#502b9617a958c8248919ed96ce7d4318b06afb34" + integrity sha512-ONbbY1oavc02ItBzXpo7dam0aNZ4ufDdGlgqLjBV5ZQnP35qmPfdq01plf8H9B2rerOLzz5PaxOBber35KffUA== dependencies: - "@atproto/common" "^0.4.12" + "@atproto/common" "^0.5.0" "@atproto/syntax" "^0.4.1" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" @@ -182,14 +180,13 @@ pino-http "^8.2.1" typed-emitter "^2.1.0" -"@atproto/common-web@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.3.tgz#b4480220b5682db09da45f4ef906eb7619c838b5" - integrity sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg== +"@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": + version "0.4.6" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.6.tgz#e32395d44d812610fd99f718b8644308b828d68b" + integrity sha512-+2mG/1oBcB/ZmYIU1ltrFMIiuy9aByKAkb2Fos/0eTdczcLBaH17k0KoxMGvhfsujN2r62XlanOAMzysa7lv1g== dependencies: - graphemer "^1.4.0" - multiformats "^9.9.0" - uint8arrays "3.0.0" + "@atproto/lex-data" "0.0.2" + "@atproto/lex-json" "0.0.2" zod "^3.23.8" "@atproto/common@0.1.0": @@ -212,14 +209,14 @@ pino "^8.6.1" zod "^3.14.2" -"@atproto/common@^0.4.12": - version "0.4.12" - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.12.tgz#284a264526edfe7cbe44a225210edec319970f43" - integrity sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ== +"@atproto/common@^0.5.0", "@atproto/common@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.5.2.tgz#207917bce1d83399c8e6068bc2c2b17e596f25e8" + integrity sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA== dependencies: - "@atproto/common-web" "^0.4.3" - "@ipld/dag-cbor" "^7.0.3" - cbor-x "^1.5.1" + "@atproto/common-web" "^0.4.6" + "@atproto/lex-cbor" "0.0.2" + "@atproto/lex-data" "0.0.2" iso-datestring-validator "^2.2.2" multiformats "^9.9.0" pino "^8.21.0" @@ -235,6 +232,15 @@ one-webcrypto "^1.0.3" uint8arrays "3.0.0" +"@atproto/crypto@0.4.5", "@atproto/crypto@^0.4.5": + version "0.4.5" + resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.4.5.tgz#fc6ad4fdfe8338147196c8050791cc6a22657eb6" + integrity sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw== + dependencies: + "@noble/curves" "^1.7.0" + "@noble/hashes" "^1.6.1" + uint8arrays "3.0.0" + "@atproto/crypto@^0.4.4": version "0.4.4" resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.4.4.tgz#3bd5066643d08e09da55bd59ac1f319d1fcff803" @@ -244,23 +250,23 @@ "@noble/hashes" "^1.6.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.181": - version "0.3.181" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.181.tgz#30241e0d171e9ae6450865b7830a9531cf235a27" - integrity sha512-b797q3neeI5flanIHsoOamXOH971An1ZsFwINJ351UQi/j5Dv8gemVikrVAKy6dPtxn1rIRhADRfH1yZjWattw== - dependencies: - "@atproto/api" "^0.17.1" - "@atproto/bsky" "^0.0.188" - "@atproto/bsync" "^0.0.22" - "@atproto/common-web" "^0.4.3" - "@atproto/crypto" "^0.4.4" - "@atproto/identity" "^0.4.9" - "@atproto/lexicon" "^0.5.1" - "@atproto/ozone" "^0.1.147" - "@atproto/pds" "^0.4.184" - "@atproto/sync" "^0.1.35" - "@atproto/syntax" "^0.4.1" - "@atproto/xrpc-server" "^0.9.5" +"@atproto/dev-env@^0.3.193": + version "0.3.193" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.193.tgz#198333a46d3f337f36fe30c7456c6287cebc8393" + integrity sha512-QPftK8FbVLGrj9IhDnPP7JCIIweYNXZ/zeaZAjolZxpIaCouPHYXXgRMZqrYxN8+RmO0s6T9czm5+xaFuXjkkw== + dependencies: + "@atproto/api" "^0.18.4" + "@atproto/bsky" "^0.0.199" + "@atproto/bsync" "^0.0.23" + "@atproto/common-web" "^0.4.6" + "@atproto/crypto" "^0.4.5" + "@atproto/identity" "^0.4.10" + "@atproto/lexicon" "^0.5.2" + "@atproto/ozone" "^0.1.159" + "@atproto/pds" "^0.4.197" + "@atproto/sync" "^0.1.38" + "@atproto/syntax" "^0.4.2" + "@atproto/xrpc-server" "^0.10.2" "@did-plc/lib" "^0.0.1" "@did-plc/server" "^0.0.1" dotenv "^16.0.3" @@ -270,19 +276,19 @@ uint8arrays "3.0.0" undici "^6.14.1" -"@atproto/did@0.2.1", "@atproto/did@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.2.1.tgz#3367b50b3ec38ed846c2b9b9f6e63c9091f526f0" - integrity sha512-1i5BTU2GnBaaeYWhxUOnuEKFVq9euT5+dQPFabHpa927BlJ54PmLGyBBaOI7/NbLmN5HWwBa18SBkMpg3jGZRA== +"@atproto/did@0.2.3", "@atproto/did@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.2.3.tgz#83cd18ae105324913b30a2259cd9d886677b2d15" + integrity sha512-VI8JJkSizvM2cHYJa37WlbzeCm5tWpojyc1/Zy8q8OOjyoy6X4S4BEfoP941oJcpxpMTObamibQIXQDo7tnIjg== dependencies: zod "^3.23.8" -"@atproto/identity@^0.4.9": - version "0.4.9" - resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.4.9.tgz#06d435807ba871717ff4c99741706b7696f8e254" - integrity sha512-pRYCaeaEJMZ4vQlRQYYTrF3cMiRp21n/k/pUT1o7dgKby56zuLErDmFXkbKfKWPf7SgWRgamSaNmsGLqAOD7lQ== +"@atproto/identity@^0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.4.10.tgz#0ddf3dabef420333a86858512c9f06127b25cada" + integrity sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ== dependencies: - "@atproto/common-web" "^0.4.3" + "@atproto/common-web" "^0.4.4" "@atproto/crypto" "^0.4.4" "@atproto/jwk-jose@0.1.11": @@ -301,74 +307,132 @@ multiformats "^9.9.0" zod "^3.23.8" -"@atproto/lexicon-resolver@0.2.2", "@atproto/lexicon-resolver@^0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@atproto/lexicon-resolver/-/lexicon-resolver-0.2.2.tgz#2a91a1908f6b327c41cb5c290eb80aed5ef593c0" - integrity sha512-m1YS8lK+R9JcH3Q4d01CEv5rhuTeo406iPBhVnNfoBFEVYMI3Acdo2/9e5hBoNhr4W6l4LI8qJxplYJcsWNh5A== +"@atproto/lex-cbor@0.0.2", "@atproto/lex-cbor@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@atproto/lex-cbor/-/lex-cbor-0.0.2.tgz#b05035940407f64dfad80289855776814ff85314" + integrity sha512-sTr3UCL2SgxEoYVpzJGgWTnNl4TpngP5tMcRyaOvi21Se4m3oR4RDsoVDPz8AS6XphiteRwzwPstquN7aWWMbA== dependencies: - "@atproto-labs/fetch-node" "^0.1.10" - "@atproto/identity" "^0.4.9" - "@atproto/lexicon" "^0.5.1" - "@atproto/repo" "^0.8.10" - "@atproto/syntax" "^0.4.1" - "@atproto/xrpc" "^0.7.5" + "@atproto/lex-data" "0.0.2" + multiformats "^9.9.0" + tslib "^2.8.1" + +"@atproto/lex-client@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@atproto/lex-client/-/lex-client-0.0.3.tgz#07079d42fe7b09fa44dde160f72579bed7aee486" + integrity sha512-EvS6tmRA5jJwsWleVpxRYpbNpfm9a9VT2A/muFdPuvUuYRPzVKm2cKCperwEnQmT7HuTA7p35dIg/0if75V0Qw== + dependencies: + "@atproto/lex-data" "0.0.2" + "@atproto/lex-json" "0.0.2" + "@atproto/lex-schema" "0.0.3" + tslib "^2.8.1" + +"@atproto/lex-data@0.0.2", "@atproto/lex-data@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.2.tgz#f90e7ac52dd6056199a84efc7a3c5196de7ceb63" + integrity sha512-euV2rDGi+coH8qvZOU+ieUOEbwPwff9ca6IiXIqjZJ76AvlIpj7vtAyIRCxHUW2BoU6h9yqyJgn9MKD2a7oIwg== + dependencies: + "@atproto/syntax" "0.4.2" multiformats "^9.9.0" + tslib "^2.8.1" + uint8arrays "3.0.0" + unicode-segmenter "^0.14.0" -"@atproto/lexicon@0.5.1", "@atproto/lexicon@^0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.5.1.tgz#e9b7d5c70dc5a38518a8069cd80fea77ab526947" - integrity sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A== +"@atproto/lex-document@0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.4.tgz#2ada83e30bcef84cc0ff010c5dd8eb2514160731" + integrity sha512-oYT3MHcAkXgPfgSzSgZuBy6R/kxkIdFAr4ohcykGquyxm0ZLWFWilgKzqKiOSzFHY0SX+Q9sWKGVt5y45ebLIw== dependencies: - "@atproto/common-web" "^0.4.3" + "@atproto/lex-schema" "0.0.3" + core-js "^3" + tslib "^2.8.1" + +"@atproto/lex-json@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.2.tgz#c4d3b6a8e965898cbc80478ecd461ddd8ac38493" + integrity sha512-Pd72lO+l2rhOTutnf11omh9ZkoB/elbzE3HSmn2wuZlyH1mRhTYvoH8BOGokWQwbZkCE8LL3nOqMT3gHCD2l7g== + dependencies: + "@atproto/lex-data" "0.0.2" + tslib "^2.8.1" + +"@atproto/lex-resolver@0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.4.tgz#769b10e95860a055bce6d7dbf876f2d7612b1c7a" + integrity sha512-ZEZYXGCYXhDy9kxPOr/WacXr62gg4R9zNf2VNk6Y/BZ+9hycW/rlEoLMo6rAsG8PxvA6D3h3o87z6xQZEI/oyw== + dependencies: + "@atproto-labs/did-resolver" "0.2.4" + "@atproto/crypto" "0.4.5" + "@atproto/lex-client" "0.0.3" + "@atproto/lex-data" "0.0.2" + "@atproto/lex-document" "0.0.4" + "@atproto/lex-schema" "0.0.3" + "@atproto/repo" "0.8.11" + "@atproto/syntax" "0.4.2" + tslib "^2.8.1" + +"@atproto/lex-schema@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@atproto/lex-schema/-/lex-schema-0.0.3.tgz#2f5a65b3592577d0056e51742f834991093a1cd3" + integrity sha512-GI0YWGRxTa/qQMHfkIrWzdEALN64ZMcKjD5lHIwuggDg8a2TwLvaN0WafSnivJGZ9m7oUNu5b97MJiDoJdeAUw== + dependencies: + "@atproto/lex-data" "0.0.2" + "@atproto/syntax" "0.4.2" + tslib "^2.8.1" + +"@atproto/lexicon@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.5.2.tgz#c2fb39b952644c9d88203850e0d61a26b39338ec" + integrity sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ== + dependencies: + "@atproto/common-web" "^0.4.4" "@atproto/syntax" "^0.4.1" iso-datestring-validator "^2.2.2" multiformats "^9.9.0" zod "^3.23.8" -"@atproto/oauth-provider-api@0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.3.1.tgz#ade10e010d4b1c9cc8fc7afa3fa9e90d49ab05b9" - integrity sha512-dEffyXP5GG2ohDb+YeLjrJ8ynueBcppEOiAnxfFED+uoIKI9TrfowgvZ4uFFhpNpuaceS0f6cO8CDfsU8NuuYQ== +"@atproto/oauth-provider-api@0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.3.4.tgz#bce9b1a5a6bd759b0de8f6b80c4aced9b64f0c79" + integrity sha512-K3gBqyf9VlYE6tvfD0EDya9WQ9XWtbuhxkI1XHyCIyAvAemhBGoJ1As0ESo3UpJmd2JhA2DmLj4oOvBqknamBA== dependencies: "@atproto/jwk" "0.6.0" - "@atproto/oauth-types" "0.4.2" + "@atproto/oauth-types" "0.5.2" -"@atproto/oauth-provider-frontend@0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.2.2.tgz#67bc69df02cc845dadae3d564cb0de8c0c8a5d7e" - integrity sha512-iP/ZoYiCrctLutPlnHUzX81AJ1fP0OzpkkokBxlnHGr4AZnkDihZRscWPSVtWqrW2xOZXAdgtf2y35vcO0TpWw== +"@atproto/oauth-provider-frontend@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.2.5.tgz#6a62475bf70cc9d198e8e5f6683fb0c8d39797a0" + integrity sha512-9+23B2Wp2G5UvHPiKQGwoK3sOu3JHa+jVfWjbUkXhho0HGL60hAbyrdm0C6n3UER/mLfn8MTjzW9jQSuJXHosg== optionalDependencies: - "@atproto/oauth-provider-api" "0.3.1" + "@atproto/oauth-provider-api" "0.3.4" -"@atproto/oauth-provider-ui@0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.3.2.tgz#402abf1505692330651280e639e2527eb2968148" - integrity sha512-nlU4CWYxTQbw/0GYBVhX8s66RZ4AE+4nWYLa/MaIew7YSjZANDSbUohqMa804ewTRLARnZECH0rUKzbXRl1kow== +"@atproto/oauth-provider-ui@0.3.6": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.3.6.tgz#1181040d33b19ed7124f5ad12833200bcd7892e6" + integrity sha512-uxnBWEX/Ht2JJbeibMhCu3OatKchhQGV4v5KfXzTylX2VIZrRmG8PVr5YnHmijjJZD+NgeDWlFSdyGdZZ7qU9w== optionalDependencies: - "@atproto/oauth-provider-api" "0.3.1" + "@atproto/oauth-provider-api" "0.3.4" -"@atproto/oauth-provider@^0.13.2": - version "0.13.2" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.13.2.tgz#7dd9148a9d4c3c8bc226be2c151fdbf4a6eed5e5" - integrity sha512-R3T63DzCei2nip5aLy4jldNiOEBDQ0g2S5UCCyhlAYarLgNRtBZzrF2L+Mx0L9AQOvsZGTxoo5fFoeFFMsAFcQ== +"@atproto/oauth-provider@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.14.0.tgz#bcae0a70250a0ca93853a90f75538acacbda46a4" + integrity sha512-eznIEvLu7iZ6mg90R8mn+WiCFkMJywHjB0wn5a9/ajWuUWPHhNSxllWy1BXtwjLvC1g1ECAwAXAY+x3Y8yfgaA== dependencies: "@atproto-labs/fetch" "0.2.3" - "@atproto-labs/fetch-node" "0.1.10" + "@atproto-labs/fetch-node" "0.2.0" "@atproto-labs/pipe" "0.1.1" "@atproto-labs/simple-store" "0.3.0" "@atproto-labs/simple-store-memory" "0.1.4" - "@atproto/common" "^0.4.12" - "@atproto/did" "0.2.1" + "@atproto/common" "^0.5.2" + "@atproto/did" "0.2.3" "@atproto/jwk" "0.6.0" "@atproto/jwk-jose" "0.1.11" - "@atproto/lexicon" "0.5.1" - "@atproto/lexicon-resolver" "0.2.2" - "@atproto/oauth-provider-api" "0.3.1" - "@atproto/oauth-provider-frontend" "0.2.2" - "@atproto/oauth-provider-ui" "0.3.2" - "@atproto/oauth-scopes" "0.2.1" - "@atproto/oauth-types" "0.4.2" - "@atproto/syntax" "0.4.1" + "@atproto/lex-document" "0.0.4" + "@atproto/lex-resolver" "0.0.4" + "@atproto/oauth-provider-api" "0.3.4" + "@atproto/oauth-provider-frontend" "0.2.5" + "@atproto/oauth-provider-ui" "0.3.6" + "@atproto/oauth-scopes" "0.3.0" + "@atproto/oauth-types" "0.5.2" + "@atproto/syntax" "0.4.2" "@hapi/accept" "^6.0.3" "@hapi/address" "^5.1.1" "@hapi/bourne" "^3.0.0" @@ -381,37 +445,37 @@ jose "^5.2.0" zod "^3.23.8" -"@atproto/oauth-scopes@0.2.1", "@atproto/oauth-scopes@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@atproto/oauth-scopes/-/oauth-scopes-0.2.1.tgz#8b710fa847e662f5e9f18dfded304d4c1b844530" - integrity sha512-C3MfE89Y02RwgePhXR7VvFNcUIjpwn1iWpSCzoGBMEM8lDjgdt+Xc2S025CD1QiWVi03NaP4m8EqeADOVgSNRA== +"@atproto/oauth-scopes@0.3.0", "@atproto/oauth-scopes@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/oauth-scopes/-/oauth-scopes-0.3.0.tgz#f171e73222ad0f24d520c7f9742ecb11f2cb2d43" + integrity sha512-aMCnzOYdLBEPysz5nNHuf4qnWFY1GTheCzWm7lKsPX447B0RvAiuK0SSMULtIOpvqMnQCTf7EMHmbdZUogII8w== dependencies: - "@atproto/did" "^0.2.1" - "@atproto/lexicon" "^0.5.1" - "@atproto/syntax" "^0.4.1" + "@atproto/did" "^0.2.3" + "@atproto/syntax" "^0.4.2" -"@atproto/oauth-types@0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.4.2.tgz#6d9dabeeb7998258d13e88254a30d51cf0de5568" - integrity sha512-gcfNTyFsPJcYDf79M0iKHykWqzxloscioKoerdIN3MTS3htiNOSgZjm2p8ho7pdrElLzea3qktuhTQI39j1XFQ== +"@atproto/oauth-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.5.2.tgz#443d2b004403f33fbdcbe4f3406f645c2785fe04" + integrity sha512-9DCDvtvCanTwAaU5UakYDO0hzcOITS3RutK5zfLytE5Y9unj0REmTDdN8Xd8YCfUJl7T/9pYpf04Uyq7bFTASg== dependencies: - "@atproto/did" "0.2.1" + "@atproto/did" "0.2.3" "@atproto/jwk" "0.6.0" zod "^3.23.8" -"@atproto/ozone@^0.1.147": - version "0.1.147" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.147.tgz#20a91e2c9e266542d13c7bacf0ded6ddab8217c1" - integrity sha512-QzFOjhCjoAlTWe9kukby3vrCJD7hxFU8309ijB6ifimcA1FhT4jvLM7OcYlT+BdSK4PfrixwOSp4Ut8OtF3Wpg== - dependencies: - "@atproto/api" "^0.17.1" - "@atproto/common" "^0.4.12" - "@atproto/crypto" "^0.4.4" - "@atproto/identity" "^0.4.9" - "@atproto/lexicon" "^0.5.1" - "@atproto/syntax" "^0.4.1" - "@atproto/xrpc" "^0.7.5" - "@atproto/xrpc-server" "^0.9.5" +"@atproto/ozone@^0.1.159": + version "0.1.159" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.159.tgz#217408581120754d3711eb68699e213d78757052" + integrity sha512-d2XVTXA0KA5hbXRgEek+jgafQ3Im6xBEJ7encsVVMc3bf6KTd7zegXmHKHddo7MS8qJbwcOgzj9N/bUQw7DuCw== + dependencies: + "@atproto/api" "^0.18.4" + "@atproto/common" "^0.5.2" + "@atproto/crypto" "^0.4.5" + "@atproto/identity" "^0.4.10" + "@atproto/lexicon" "^0.5.2" + "@atproto/syntax" "^0.4.2" + "@atproto/ws-client" "^0.0.3" + "@atproto/xrpc" "^0.7.6" + "@atproto/xrpc-server" "^0.10.2" "@did-plc/lib" "^0.0.1" compression "^1.7.4" cors "^2.8.5" @@ -429,29 +493,30 @@ undici "^6.14.1" ws "^8.12.0" -"@atproto/pds@^0.4.184": - version "0.4.184" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.184.tgz#cee06a36b5bff8f931d1609f8d4a0dedfd921fe4" - integrity sha512-TkbDHAIu0IoUU2fTvjs/z3U/cXsC7hTtFBNJiE1wUeiCWqigxOIwSojAqVXU3pgxt4I+64kta5KBS1n4GYPOXg== +"@atproto/pds@^0.4.197": + version "0.4.197" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.197.tgz#16992c19a6b45dfe1c802b17f6702b6af113ffd9" + integrity sha512-KzPKo00/eOgsShklVLXlHT5ogKVXcOkMLt3NeEQzC3IOQRvAC4BRjPrFs2zRuQoWfTD3ClSpGVsF2zN5bQw+KQ== dependencies: - "@atproto-labs/fetch-node" "0.1.10" + "@atproto-labs/fetch-node" "0.2.0" "@atproto-labs/simple-store" "0.3.0" "@atproto-labs/simple-store-memory" "0.1.4" "@atproto-labs/simple-store-redis" "0.0.1" - "@atproto-labs/xrpc-utils" "0.0.22" - "@atproto/api" "^0.17.1" - "@atproto/aws" "^0.2.30" - "@atproto/common" "^0.4.12" - "@atproto/crypto" "^0.4.4" - "@atproto/identity" "^0.4.9" - "@atproto/lexicon" "^0.5.1" - "@atproto/lexicon-resolver" "^0.2.2" - "@atproto/oauth-provider" "^0.13.2" - "@atproto/oauth-scopes" "^0.2.1" - "@atproto/repo" "^0.8.10" - "@atproto/syntax" "^0.4.1" - "@atproto/xrpc" "^0.7.5" - "@atproto/xrpc-server" "^0.9.5" + "@atproto-labs/xrpc-utils" "0.0.24" + "@atproto/api" "^0.18.4" + "@atproto/aws" "^0.2.31" + "@atproto/common" "^0.5.2" + "@atproto/crypto" "^0.4.5" + "@atproto/identity" "^0.4.10" + "@atproto/lex-cbor" "^0.0.2" + "@atproto/lex-data" "^0.0.2" + "@atproto/lexicon" "^0.5.2" + "@atproto/oauth-provider" "^0.14.0" + "@atproto/oauth-scopes" "^0.3.0" + "@atproto/repo" "^0.8.11" + "@atproto/syntax" "^0.4.2" + "@atproto/xrpc" "^0.7.6" + "@atproto/xrpc-server" "^0.10.2" "@did-plc/lib" "^0.0.4" "@hapi/address" "^5.1.1" better-sqlite3 "^10.0.0" @@ -481,65 +546,79 @@ undici "^6.19.8" zod "^3.23.8" -"@atproto/repo@^0.8.10": - version "0.8.10" - resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.8.10.tgz#a7776bb21630e4d5d5f698dbb9d8bca0f80811e6" - integrity sha512-REs6TZGyxNaYsjqLf447u+gSdyzhvMkVbxMBiKt1ouEVRkiho1CY32+omn62UkpCuGK2y6SCf6x3sVMctgmX4g== +"@atproto/repo@0.8.11", "@atproto/repo@^0.8.11": + version "0.8.11" + resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.8.11.tgz#3698e4164811adbeb269fd4412639babc4be90ca" + integrity sha512-b/WCu5ITws4ILHoXiZz0XXB5U9C08fUVzkBQDwpnme62GXv8gUaAPL/ttG61OusW09ARwMMQm4vxoP0hTFg+zA== dependencies: - "@atproto/common" "^0.4.12" - "@atproto/common-web" "^0.4.3" + "@atproto/common" "^0.5.0" + "@atproto/common-web" "^0.4.4" "@atproto/crypto" "^0.4.4" - "@atproto/lexicon" "^0.5.1" + "@atproto/lexicon" "^0.5.2" "@ipld/dag-cbor" "^7.0.0" multiformats "^9.9.0" uint8arrays "3.0.0" varint "^6.0.0" zod "^3.23.8" -"@atproto/sync@^0.1.35": - version "0.1.35" - resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.35.tgz#6d4dd66043946d20254b31dd2262e86148d78e8e" - integrity sha512-MPvmTjJYCilZEQF1ds7itzF9tNEZtw4Ez0HeMO5E5GaPtTAccBU3AsTxwWST87EX5qsVxMlBTq2go6G6+Swd7Q== +"@atproto/sync@^0.1.38": + version "0.1.38" + resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.38.tgz#26244d1e916e6b5c30545eb55c3c3e68cdf1b4a8" + integrity sha512-2rE0SM21Nk4hWw/XcIYFnzlWO6/gBg8mrzuWbOvDhD49sA/wW4zyjaHZ5t1gvk28/SLok2VZiIR8nYBdbf7F5Q== dependencies: - "@atproto/common" "^0.4.12" - "@atproto/identity" "^0.4.9" - "@atproto/lexicon" "^0.5.1" - "@atproto/repo" "^0.8.10" + "@atproto/common" "^0.5.0" + "@atproto/identity" "^0.4.10" + "@atproto/lexicon" "^0.5.2" + "@atproto/repo" "^0.8.11" "@atproto/syntax" "^0.4.1" - "@atproto/xrpc-server" "^0.9.5" + "@atproto/xrpc-server" "^0.10.0" multiformats "^9.9.0" p-queue "^6.6.2" ws "^8.12.0" -"@atproto/syntax@0.4.1", "@atproto/syntax@^0.4.1": +"@atproto/syntax@0.4.2", "@atproto/syntax@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.2.tgz#a83ff62b82bf84308d78ad836c802bad6a52174a" + integrity sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA== + +"@atproto/syntax@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.1.tgz#f77bc610ae0914449ff3f4731861e3da429915f5" integrity sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw== -"@atproto/xrpc-server@^0.9.5": - version "0.9.5" - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.9.5.tgz#3a036ce2db85bcac40103fd160fef3ed7c364e2b" - integrity sha512-V0srjUgy6mQ5yf9+MSNBLs457m4qclEaWZsnqIE7RfYywvntexTAbMoo7J7ONfTNwdmA9Gw4oLak2z2cDAET4w== +"@atproto/ws-client@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@atproto/ws-client/-/ws-client-0.0.3.tgz#bcf350e1c8e0aa2063b01acb01067504cad6a0c2" + integrity sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA== dependencies: - "@atproto/common" "^0.4.12" - "@atproto/crypto" "^0.4.4" - "@atproto/lexicon" "^0.5.1" - "@atproto/xrpc" "^0.7.5" - cbor-x "^1.5.1" + "@atproto/common" "^0.5.0" + ws "^8.12.0" + +"@atproto/xrpc-server@^0.10.0", "@atproto/xrpc-server@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.10.2.tgz#b68f42a7b6df5bb8081525e5c981a709b9b02739" + integrity sha512-5AzN8xoV8K1Omn45z6qKH414+B3Z35D536rrScwF3aQGDEdpObAS+vya9UoSg+Gvm2+oOtVEbVri7riLTBW3Vg== + dependencies: + "@atproto/common" "^0.5.2" + "@atproto/crypto" "^0.4.5" + "@atproto/lex-cbor" "0.0.2" + "@atproto/lex-data" "0.0.2" + "@atproto/lexicon" "^0.5.2" + "@atproto/ws-client" "^0.0.3" + "@atproto/xrpc" "^0.7.6" express "^4.17.2" http-errors "^2.0.0" mime-types "^2.1.35" rate-limiter-flexible "^2.4.1" - uint8arrays "3.0.0" ws "^8.12.0" zod "^3.23.8" -"@atproto/xrpc@^0.7.5": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.5.tgz#40cef1a657b5f28af8ebec9e3dac5872e58e88ea" - integrity sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA== +"@atproto/xrpc@^0.7.6": + version "0.7.6" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.6.tgz#bc12b0e37f81fa76589691634d4fac9774fd0cb5" + integrity sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA== dependencies: - "@atproto/lexicon" "^0.5.1" + "@atproto/lexicon" "^0.5.2" zod "^3.23.8" "@aws-crypto/crc32@5.2.0": @@ -3677,36 +3756,6 @@ resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.7.0.tgz#cecddc8162a231642b410bc7b99309cd5969733c" integrity sha512-jIsRadRsyxf6ERBU1auY2c1k3doFdqh15F4HRZs4BELVuBtpN+3ipkXqcsWE+rD+EQNigeR29SfQ+ES6UX/jGg== -"@cbor-extract/cbor-extract-darwin-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6" - integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA== - -"@cbor-extract/cbor-extract-darwin-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f" - integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw== - -"@cbor-extract/cbor-extract-linux-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63" - integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ== - -"@cbor-extract/cbor-extract-linux-arm@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41" - integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ== - -"@cbor-extract/cbor-extract-linux-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd" - integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA== - -"@cbor-extract/cbor-extract-win32-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7" - integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw== - "@connectrpc/connect-express@^1.1.4": version "1.3.0" resolved "https://registry.yarnpkg.com/@connectrpc/connect-express/-/connect-express-1.3.0.tgz#605cb536e041f5866868421ae00b1a794dcdd1ed" @@ -7189,11 +7238,6 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.25.0.tgz#e08ed0a9fad34c8005d1a282e57280031ac50cdc" integrity sha512-vlobHP64HTuSE68lWF1mEhwSRC5Q7gaT+a/m9S+ItuN+ruSOxe1rFnR9j0ACWQ314BPhBEVKfBQ6mHL0OWfdbQ== -"@tanstack/query-core@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.8.1.tgz#5215a028370d9b2f32e83787a0ea119e2f977996" - integrity sha512-Y0enatz2zQXBAsd7XmajlCs+WaitdR7dIFkqz9Xd7HL4KV04JOigWVreYseTmNH7YFSBSC/BJ9uuNp1MAf+GfA== - "@tanstack/query-persist-client-core@5.25.0": version "5.25.0" resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.25.0.tgz#52fa634a8067d7b965854a532a33077fd4df0eff" @@ -7208,12 +7252,12 @@ dependencies: "@tanstack/query-persist-client-core" "5.25.0" -"@tanstack/react-query@^5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.8.1.tgz#22a122016e23a39acd90341954a895980ec21ade" - integrity sha512-YMagxS8iNPOLg0pK6WOjdSDlAvWKOf69udLOwQrBVmkC2SRLNLko7elo5Ro3ptlJkXvTVHidxC/h5KGi5bH1XQ== +"@tanstack/react-query@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.25.0.tgz#f4dac794cf10dd956aa56dbbdf67049a5ba2669d" + integrity sha512-u+n5R7mLO7RmeiIonpaCRVXNRWtZEef/aVZ/XGWRPa7trBIvGtzlfo0Ah7ZtnTYfrKEVwnZ/tzRCBcoiqJ/tFw== dependencies: - "@tanstack/query-core" "5.8.1" + "@tanstack/query-core" "5.25.0" "@testing-library/jest-native@^5.4.3": version "5.4.3" @@ -9220,27 +9264,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001587, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz" integrity sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw== -cbor-extract@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" - integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA== - dependencies: - node-gyp-build-optional-packages "5.0.3" - optionalDependencies: - "@cbor-extract/cbor-extract-darwin-arm64" "2.1.1" - "@cbor-extract/cbor-extract-darwin-x64" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm64" "2.1.1" - "@cbor-extract/cbor-extract-linux-x64" "2.1.1" - "@cbor-extract/cbor-extract-win32-x64" "2.1.1" - -cbor-x@^1.5.1: - version "1.5.4" - resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.4.tgz#8f0754fa8589cbd7339b613b2b5717d133508e98" - integrity sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw== - optionalDependencies: - cbor-extract "^2.1.1" - cborg@^1.6.0: version "1.10.2" resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" @@ -9711,6 +9734,11 @@ core-js-pure@^3.23.3: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.32.1.tgz#5775b88f9062885f67b6d7edce59984e89d276f3" integrity sha512-f52QZwkFVDPf7UEQZGHKx6NYxsxmVGJe5DIvbzOdRMJlmT6yv0KDjR8rmy3ngr/t5wU54c7Sp/qIJH0ppbhVpQ== +core-js@^3: + version "3.47.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.47.0.tgz#436ef07650e191afeb84c24481b298bd60eb4a17" + integrity sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -15163,11 +15191,6 @@ node-forge@^1, node-forge@^1.2.1, node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-gyp-build-optional-packages@5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" - integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== - node-html-parser@^5.2.0: version "5.4.2" resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a" @@ -19056,7 +19079,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2, tslib@^2.6.2: +tslib@2, tslib@^2.6.2, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -19338,6 +19361,11 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +unicode-segmenter@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/unicode-segmenter/-/unicode-segmenter-0.14.0.tgz#090128182bcc710327a1b7e4af4f5834444eaa61" + integrity sha512-AH4lhPCJANUnSLEKnM4byboctePJzltF4xj8b+NbNiYeAkAXGh7px2K/4NANFp7dnr6+zB3e6HLu8Jj8SKyvYg== + unimodules-app-loader@~6.0.7: version "6.0.7" resolved "https://registry.yarnpkg.com/unimodules-app-loader/-/unimodules-app-loader-6.0.7.tgz#d88db74075815bcdc088c6c6823a2b08394a1225"