From cfb8a3160e0092990bafd05cb97006720400448a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 24 Jul 2024 20:09:20 +0100 Subject: [PATCH] Priority notifications (#4798) * new settings screen * bring back the spinner * add experimental language * fix typo, change leading * integrate priority notifications API * update package * use refetch instead of invalidateQueries * fix read-after-write issue by polling for update * add spinner for initial load * rm onmutate, it's overcomplicated * set error state eagerly * Change language in description Co-authored-by: Hailey * prettier * add `Toggle.Platform` * extract out mutation hook + error state * rm useless cache mutation * disambiguate isError and isPending * rm unused isError --------- Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com> Co-authored-by: Hailey --- bskyweb/cmd/bskyweb/server.go | 1 + package.json | 2 +- src/Navigation.tsx | 6 + src/components/Lists.tsx | 2 +- src/components/forms/Toggle.tsx | 3 + src/components/icons/Gear.tsx | 5 - src/lib/async/until.ts | 10 +- src/lib/routes/types.ts | 15 +-- src/routes.ts | 1 + src/screens/Messages/Conversation/index.tsx | 1 + src/screens/Messages/List/index.tsx | 2 +- src/screens/Messages/Settings.tsx | 2 +- src/state/queries/notifications/feed.ts | 37 ++++--- src/state/queries/notifications/settings.ts | 67 +++++++++++ src/state/queries/notifications/types.ts | 1 + src/state/queries/notifications/util.ts | 8 +- src/view/com/notifications/Feed.tsx | 7 +- src/view/screens/Notifications.tsx | 117 +++++++++++++------- src/view/screens/NotificationsSettings.tsx | 94 ++++++++++++++++ yarn.lock | 8 +- 20 files changed, 305 insertions(+), 84 deletions(-) delete mode 100644 src/components/icons/Gear.tsx create mode 100644 src/state/queries/notifications/settings.ts create mode 100644 src/view/screens/NotificationsSettings.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index d7e41a4c..61a524a7 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -197,6 +197,7 @@ func serve(cctx *cli.Context) error { e.GET("/search", server.WebGeneric) e.GET("/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) + e.GET("/notifications/settings", server.WebGeneric) e.GET("/lists", server.WebGeneric) e.GET("/moderation", server.WebGeneric) e.GET("/moderation/modlists", server.WebGeneric) diff --git a/package.json b/package.json index b56dd9e3..51177943 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "^0.12.23", + "@atproto/api": "0.12.25", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 9e9b4944..8646577c 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -76,6 +76,7 @@ import {LogScreen} from './view/screens/Log' import {ModerationModlistsScreen} from './view/screens/ModerationModlists' import {NotFoundScreen} from './view/screens/NotFound' import {NotificationsScreen} from './view/screens/Notifications' +import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings' import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy' import {PostThreadScreen} from './view/screens/PostThread' @@ -324,6 +325,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => MessagesSettingsScreen} options={{title: title(msg`Chat settings`), requireAuth: true}} /> + NotificationsSettingsScreen} + options={{title: title(msg`Notification settings`), requireAuth: true}} + /> FeedsScreen} diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index 3368d076..e706e101 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -189,7 +189,7 @@ let ListMaybePlaceholder = ({ return ( ) } + +export const Platform = isNative ? Switch : Checkbox diff --git a/src/components/icons/Gear.tsx b/src/components/icons/Gear.tsx deleted file mode 100644 index 980b7413..00000000 --- a/src/components/icons/Gear.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {createSinglePathSVG} from './TEMPLATE' - -export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({ - path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', -}) diff --git a/src/lib/async/until.ts b/src/lib/async/until.ts index db53c921..1b7a5763 100644 --- a/src/lib/async/until.ts +++ b/src/lib/async/until.ts @@ -1,10 +1,10 @@ import {timeout} from './timeout' -export async function until( +export async function until( retries: number, delay: number, - cond: (v: any, err: any) => boolean, - fn: () => Promise, + cond: (v: T, err: any) => boolean, + fn: () => Promise, ): Promise { while (retries > 0) { try { @@ -13,7 +13,9 @@ export async function until( return true } } catch (e: any) { - if (cond(undefined, e)) { + // TODO: change the type signature of cond to accept undefined + // however this breaks every existing usage of until -sfn + if (cond(undefined as unknown as T, e)) { return true } } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index bda93fb4..fbb66c9e 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -42,14 +42,13 @@ export type CommonNavigatorParams = { Hashtag: {tag: string; author?: string} MessagesConversation: {conversation: string; embed?: string} MessagesSettings: undefined + NotificationsSettings: undefined Feeds: undefined Start: {name: string; rkey: string} StarterPack: {name: string; rkey: string; new?: boolean} StarterPackShort: {code: string} StarterPackWizard: undefined - StarterPackEdit: { - rkey?: string - } + StarterPackEdit: {rkey?: string} } export type BottomTabNavigatorParams = CommonNavigatorParams & { @@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & { } export type NotificationsTabNavigatorParams = CommonNavigatorParams & { - Notifications: undefined + Notifications: {show?: 'all'} } export type MyProfileTabNavigatorParams = CommonNavigatorParams & { @@ -84,7 +83,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Home: undefined Search: {q?: string} Feeds: undefined - Notifications: undefined + Notifications: {show?: 'all'} Hashtag: {tag: string; author?: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -96,7 +95,7 @@ export type AllNavigatorParams = CommonNavigatorParams & { Search: {q?: string} Feeds: undefined NotificationsTab: undefined - Notifications: undefined + Notifications: {show?: 'all'} MyProfileTab: undefined Hashtag: {tag: string; author?: string} MessagesTab: undefined @@ -105,9 +104,7 @@ export type AllNavigatorParams = CommonNavigatorParams & { StarterPack: {name: string; rkey: string; new?: boolean} StarterPackShort: {code: string} StarterPackWizard: undefined - StarterPackEdit: { - rkey?: string - } + StarterPackEdit: {rkey?: string} } // NOTE diff --git a/src/routes.ts b/src/routes.ts index a76d8c4c..ddf4fb39 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -5,6 +5,7 @@ export const router = new Router({ Search: '/search', Feeds: '/feeds', Notifications: '/notifications', + NotificationsSettings: '/notifications/settings', Settings: '/settings', LanguageSettings: '/settings/language', Lists: '/lists', diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx index a99ef8d4..d14ed160 100644 --- a/src/screens/Messages/Conversation/index.tsx +++ b/src/screens/Messages/Conversation/index.tsx @@ -106,6 +106,7 @@ function Inner() { title={_(msg`Something went wrong`)} message={_(msg`We couldn't load this conversation`)} onRetry={() => convoState.error.retry()} + sideBorders={false} /> ) diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx index 0b1fe2a9..2fd9990c 100644 --- a/src/screens/Messages/List/index.tsx +++ b/src/screens/Messages/List/index.tsx @@ -309,7 +309,7 @@ function DesktopHeader({ a.gap_lg, a.px_lg, a.pr_md, - a.py_md, + a.py_sm, a.border_b, t.atoms.border_contrast_low, ]}> diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index 3d7e6013..df469d13 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -107,7 +107,7 @@ export function MessagesSettingsScreen({}: Props) { a.rounded_md, t.atoms.bg_contrast_25, ]}> - + You can continue ongoing conversations regardless of which setting you choose. diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 17ee9092..3cafcb71 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -46,11 +46,14 @@ const PAGE_SIZE = 30 type RQPageParam = string | undefined const RQKEY_ROOT = 'notification-feed' -export function RQKEY() { - return [RQKEY_ROOT] +export function RQKEY(priority?: false) { + return [RQKEY_ROOT, priority] } -export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { +export function useNotificationFeedQuery(opts?: { + enabled?: boolean + overridePriorityNotifications?: boolean +}) { const agent = useAgent() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() @@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { const lastPageCountRef = useRef(0) const gate = useGate() + // false: force showing all notifications + // undefined: let the server decide + const priority = opts?.overridePriorityNotifications ? false : undefined + const query = useInfiniteQuery< FeedPage, Error, @@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { RQPageParam >({ staleTime: STALE.INFINITY, - queryKey: RQKEY(), + queryKey: RQKEY(priority), async queryFn({pageParam}: {pageParam: RQPageParam}) { let page if (!pageParam) { @@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { page = unreads.getCachedUnreadPage() } if (!page) { - page = ( - await fetchPage({ - agent, - limit: PAGE_SIZE, - cursor: pageParam, - queryClient, - moderationOpts, - fetchAdditionalData: true, - shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'), - }) - ).page + const {page: fetchedPage} = await fetchPage({ + agent, + limit: PAGE_SIZE, + cursor: pageParam, + queryClient, + moderationOpts, + fetchAdditionalData: true, + shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'), + priority, + }) + page = fetchedPage } // if the first page has an unread, mark all read diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts new file mode 100644 index 00000000..78ecbd9f --- /dev/null +++ b/src/state/queries/notifications/settings.ts @@ -0,0 +1,67 @@ +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {until} from '#/lib/async/until' +import {logger} from '#/logger' +import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' +import {useAgent} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' + +export function useNotificationsSettingsMutation() { + const {_} = useLingui() + const agent = useAgent() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (keys: string[]) => { + const enabled = keys[0] === 'enabled' + + await agent.api.app.bsky.notification.putPreferences({ + priority: enabled, + }) + + await until( + 5, // 5 tries + 1e3, // 1s delay between tries + res => res.data.priority === enabled, + () => agent.api.app.bsky.notification.listNotifications({limit: 1}), + ) + + eagerlySetCachedPriority(queryClient, enabled) + }, + onError: err => { + logger.error('Failed to save notification preferences', { + safeMessage: err, + }) + Toast.show( + _(msg`Failed to save notification preferences, please try again`), + 'xmark', + ) + }, + onSuccess: () => { + Toast.show(_(msg`Preference saved`)) + }, + onSettled: () => { + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()}) + }, + }) +} + +function eagerlySetCachedPriority( + queryClient: ReturnType, + enabled: boolean, +) { + queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => { + if (!old) return old + return { + ...old, + pages: old.pages.map((page: any) => { + return { + ...page, + priority: enabled, + } + }), + } + }) +} diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts index d40a07b1..c96374eb 100644 --- a/src/state/queries/notifications/types.ts +++ b/src/state/queries/notifications/types.ts @@ -22,6 +22,7 @@ export interface FeedPage { cursor: string | undefined seenAt: Date items: FeedNotification[] + priority: boolean } export interface CachedFeedPage { diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 2f2c242d..7651e414 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -39,10 +39,15 @@ export async function fetchPage({ moderationOpts: ModerationOpts | undefined fetchAdditionalData: boolean shouldUngroupFollowBacks?: () => boolean -}): Promise<{page: FeedPage; indexedAt: string | undefined}> { + priority?: boolean +}): Promise<{ + page: FeedPage + indexedAt: string | undefined +}> { const res = await agent.listNotifications({ limit, cursor, + // priority, }) const indexedAt = res.data.notifications[0]?.indexedAt @@ -88,6 +93,7 @@ export async function fetchPage({ cursor: res.data.cursor, seenAt, items: notifsGrouped, + priority: res.data.priority ?? false, }, indexedAt, } diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index e2f12e84..3e7fdfc7 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -35,11 +35,13 @@ export function Feed({ onPressTryAgain, onScrolledDownChange, ListHeaderComponent, + overridePriorityNotifications, }: { scrollElRef?: ListRef onPressTryAgain?: () => void onScrolledDownChange: (isScrolledDown: boolean) => void ListHeaderComponent?: () => JSX.Element + overridePriorityNotifications?: boolean }) { const initialNumToRender = useInitialNumToRender() @@ -59,7 +61,10 @@ export function Feed({ hasNextPage, isFetchingNextPage, fetchNextPage, - } = useNotificationFeedQuery({enabled: !!moderationOpts}) + } = useNotificationFeedQuery({ + enabled: !!moderationOpts, + overridePriorityNotifications, + }) const isEmpty = !isFetching && !data?.pages[0]?.items.length const items = React.useMemo(() => { diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index f1ae7945..073e91c4 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -1,11 +1,19 @@ -import React from 'react' +import React, {useCallback} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' +import {useAnalytics} from '#/lib/analytics/analytics' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {ComposeIcon2} from '#/lib/icons' +import { + NativeStackScreenProps, + NotificationsTabNavigatorParams, +} from '#/lib/routes/types' +import {s} from '#/lib/styles' import {logger} from '#/logger' import {isNative} from '#/platform/detection' import {emitSoftReset, listenSoftReset} from '#/state/events' @@ -17,37 +25,32 @@ import { import {truncateAndInvalidate} from '#/state/queries/util' import {useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {ComposeIcon2} from 'lib/icons' -import { - NativeStackScreenProps, - NotificationsTabNavigatorParams, -} from 'lib/routes/types' -import {colors, s} from 'lib/styles' -import {TextLink} from 'view/com/util/Link' +import {Feed} from '#/view/com/notifications/Feed' +import {FAB} from '#/view/com/util/fab/FAB' +import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' +import {ViewHeader} from '#/view/com/util/ViewHeader' import {ListMethods} from 'view/com/util/List' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {CenteredView} from 'view/com/util/Views' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' +import {Link} from '#/components/Link' import {Loader} from '#/components/Loader' -import {Feed} from '../com/notifications/Feed' -import {FAB} from '../com/util/fab/FAB' -import {MainScrollProvider} from '../com/util/MainScrollProvider' -import {ViewHeader} from '../com/util/ViewHeader' +import {Text} from '#/components/Typography' type Props = NativeStackScreenProps< NotificationsTabNavigatorParams, 'Notifications' > -export function NotificationsScreen({}: Props) { +export function NotificationsScreen({route: {params}}: Props) { const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) const scrollElRef = React.useRef(null) const {screen} = useAnalytics() - const pal = usePalette('default') + const t = useTheme() const {isDesktop} = useWebMediaQueries() const queryClient = useQueryClient() const unreadNotifs = useUnreadNotifications() @@ -109,56 +112,87 @@ export function NotificationsScreen({}: Props) { return listenSoftReset(onPressLoadLatest) }, [onPressLoadLatest, isScreenFocused]) + const renderButton = useCallback(() => { + return ( + + + + ) + }, [_, t]) + const ListHeaderComponent = React.useCallback(() => { if (isDesktop) { return ( - - Notifications{' '} + + + {isLoadingLatest ? : <>} + {renderButton()} + ) } return <> - }, [isDesktop, pal, hasNew, isLoadingLatest]) + }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest]) const renderHeaderSpinner = React.useCallback(() => { return ( - + {isLoadingLatest ? : <>} + {renderButton()} ) - }, [isLoadingLatest]) + }, [renderButton, isLoadingLatest]) return ( {(isScrolledDown || hasNew) && ( diff --git a/src/view/screens/NotificationsSettings.tsx b/src/view/screens/NotificationsSettings.tsx new file mode 100644 index 00000000..2716a07f --- /dev/null +++ b/src/view/screens/NotificationsSettings.tsx @@ -0,0 +1,94 @@ +import React from 'react' +import {View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' +import {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {CenteredView} from '#/view/com/util/Views' +import {atoms as a, useTheme} from '#/alf' +import {Error} from '#/components/Error' +import * as Toggle from '#/components/forms/Toggle' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +type Props = NativeStackScreenProps +export function NotificationsSettingsScreen({}: Props) { + const {_} = useLingui() + const t = useTheme() + + const {data, isError: isQueryError, refetch} = useNotificationFeedQuery() + const serverPriority = data?.pages.at(0)?.priority + + const { + mutate: onChangePriority, + isPending: isMutationPending, + variables, + } = useNotificationsSettingsMutation() + + const priority = isMutationPending + ? variables[0] === 'enabled' + : serverPriority + + return ( + + + {isQueryError ? ( + + ) : ( + + + {' '} + Notification filters + + + + + + Enable priority notifications + + {!data ? : } + + + + + + + Experimental: When this preference is enabled, you'll only + receive reply and quote notifications from users you follow. + We'll continue to add more controls here over time. + + + + + )} + + ) +} diff --git a/yarn.lock b/yarn.lock index a83d41db..6450d33b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,10 +34,10 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@^0.12.23": - version "0.12.23" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af" - integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA== +"@atproto/api@0.12.25": + version "0.12.25" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.25.tgz#9eeb51484106a5e07f89f124e505674a3574f93b" + integrity sha512-IV3vGPnDw9bmyP/JOd8YKbm8fOpRAgJpEUVnIZNVb/Vo8v+WOroOjrJxtzdHOcXTL9IEcTTyXSCc7yE7kwhN2A== dependencies: "@atproto/common-web" "^0.3.0" "@atproto/lexicon" "^0.4.0"