From 904fd9254275ce484ab678719d3606786ae42c68 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 7 Aug 2024 17:13:29 -0700 Subject: [PATCH 1/7] Add logging of selected feed preference when displaying the following feed (#4789) (cherry picked from commit b3092413dd21b58340e4cec739770f6d10a70248) --- src/lib/statsig/events.ts | 6 ++++++ src/view/screens/Home.tsx | 30 ++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 159061eac97..997a366a41a 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -211,6 +211,12 @@ export type LogEvents = { 'feed:interstitial:profileCard:press': {} 'feed:interstitial:feedCard:press': {} + 'debug:followingPrefs': { + followingShowRepliesFromPref: 'all' | 'following' | 'off' + followingRepliesMinLikePref: number + } + 'debug:followingDisplayed': {} + 'test:all:always': {} 'test:all:sometimes': {} 'test:all:boosted_by_gate1': {reason: 'base' | 'gate1'} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index f7cecd872de..6ee8b3ada6c 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -9,7 +9,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent, LogEvents} from '#/lib/statsig/statsig' import {emitSoftReset} from '#/state/events' import {SavedFeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed' -import {FeedParams} from '#/state/queries/post-feed' +import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSession} from '#/state/session' @@ -108,6 +108,30 @@ function HomeScreenReady({ } }, [selectedIndex]) + // Temporary, remove when finished debugging + const debugHasLoggedFollowingPrefs = React.useRef(false) + const debugLogFollowingPrefs = React.useCallback( + (feed: FeedDescriptor) => { + if (debugHasLoggedFollowingPrefs.current) return + if (feed !== 'following') return + logEvent('debug:followingPrefs', { + followingShowRepliesFromPref: preferences.feedViewPrefs.hideReplies + ? 'off' + : preferences.feedViewPrefs.hideRepliesByUnfollowed + ? 'following' + : 'all', + followingRepliesMinLikePref: + preferences.feedViewPrefs.hideRepliesByLikeCount, + }) + debugHasLoggedFollowingPrefs.current = true + }, + [ + preferences.feedViewPrefs.hideReplies, + preferences.feedViewPrefs.hideRepliesByLikeCount, + preferences.feedViewPrefs.hideRepliesByUnfollowed, + ], + ) + const {hasSession} = useSession() const setMinimalShellMode = useSetMinimalShellMode() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() @@ -136,6 +160,7 @@ function HomeScreenReady({ feedUrl: selectedFeed, reason: 'focus', }) + debugLogFollowingPrefs(selectedFeed) } }), ) @@ -182,8 +207,9 @@ function HomeScreenReady({ feedUrl: feed, reason, }) + debugLogFollowingPrefs(feed) }, - [allFeeds], + [allFeeds, debugLogFollowingPrefs], ) const onPressSelected = React.useCallback(() => { From 12598b998a91042c546432f7cfc5fb883a8ae243 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 12 Aug 2024 14:00:15 -0700 Subject: [PATCH 2/7] Upgrade API, implement XRPC rework (#4857) Co-authored-by: Matthieu Sieben (cherry picked from commit 7df2327424e948e54b9731e5ab651e889f38a772) --- index.js | 9 +- index.web.js | 5 +- jest/test-pds.ts | 4 +- package.json | 4 +- patches/@atproto+lexicon+0.4.0.patch | 28 -- src/lib/api/api-polyfill.ts | 85 ---- src/lib/api/api-polyfill.web.ts | 3 - src/lib/api/feed/custom.ts | 25 +- src/lib/api/index.ts | 39 +- src/lib/api/upload-blob.ts | 82 ++++ src/lib/api/upload-blob.web.ts | 26 ++ src/lib/media/manip.ts | 8 +- src/screens/SignupQueued.tsx | 2 +- src/state/queries/preferences/index.ts | 4 +- src/state/session/__tests__/session-test.ts | 72 ++-- src/state/session/agent.ts | 44 +- src/state/session/index.tsx | 24 +- src/state/session/logging.ts | 2 +- yarn.lock | 435 ++++++++++++++------ 19 files changed, 542 insertions(+), 359 deletions(-) delete mode 100644 patches/@atproto+lexicon+0.4.0.patch delete mode 100644 src/lib/api/api-polyfill.ts delete mode 100644 src/lib/api/api-polyfill.web.ts create mode 100644 src/lib/api/upload-blob.ts create mode 100644 src/lib/api/upload-blob.web.ts diff --git a/index.js b/index.js index 7630d0538a8..2f13ce1ea19 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,11 @@ import 'react-native-gesture-handler' // must be first -import {LogBox} from 'react-native' - import '#/platform/polyfills' -import {IS_TEST} from '#/env' + +import {LogBox} from 'react-native' import {registerRootComponent} from 'expo' -import {doPolyfill} from '#/lib/api/api-polyfill' import App from '#/App' - -doPolyfill() +import {IS_TEST} from '#/env' if (IS_TEST) { LogBox.ignoreAllLogs() // suppress all logs in tests diff --git a/index.web.js b/index.web.js index 9623734512d..be75bc772ed 100644 --- a/index.web.js +++ b/index.web.js @@ -1,9 +1,8 @@ import '#/platform/markBundleStartTime' - import '#/platform/polyfills' + import {registerRootComponent} from 'expo' -import {doPolyfill} from '#/lib/api/api-polyfill' + import App from '#/App' -doPolyfill() registerRootComponent(App) diff --git a/jest/test-pds.ts b/jest/test-pds.ts index 2fe623ca981..bfcc970c2fe 100644 --- a/jest/test-pds.ts +++ b/jest/test-pds.ts @@ -156,7 +156,7 @@ class Mocker { } async createUser(name: string) { - const agent = new BskyAgent({service: this.agent.service}) + const agent = new BskyAgent({service: this.service}) const inviteRes = await agent.api.com.atproto.server.createInviteCode( {useCount: 1}, @@ -332,7 +332,7 @@ class Mocker { } async createInvite(forAccount: string) { - const agent = new BskyAgent({service: this.agent.service}) + const agent = new BskyAgent({service: this.service}) await agent.api.com.atproto.server.createInviteCode( {useCount: 1, forAccount}, { diff --git a/package.json b/package.json index 51177943bb0..ce6dceb9fa2 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.25", + "@atproto/api": "0.13.0", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", @@ -207,7 +207,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.5", + "@atproto/dev-env": "^0.3.39", "@babel/core": "^7.23.2", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", diff --git a/patches/@atproto+lexicon+0.4.0.patch b/patches/@atproto+lexicon+0.4.0.patch deleted file mode 100644 index 4643db32af9..00000000000 --- a/patches/@atproto+lexicon+0.4.0.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/node_modules/@atproto/lexicon/dist/validators/complex.js b/node_modules/@atproto/lexicon/dist/validators/complex.js -index 32d7798..9d688b7 100644 ---- a/node_modules/@atproto/lexicon/dist/validators/complex.js -+++ b/node_modules/@atproto/lexicon/dist/validators/complex.js -@@ -113,7 +113,22 @@ function object(lexicons, path, def, value) { - if (value[key] === null && nullableProps.has(key)) { - continue; - } -- const propDef = def.properties[key]; -+ const propDef = def.properties[key] -+ if (typeof value[key] === 'undefined' && !requiredProps.has(key)) { -+ // Fast path for non-required undefined props. -+ if ( -+ propDef.type === 'integer' || -+ propDef.type === 'boolean' || -+ propDef.type === 'string' -+ ) { -+ if (typeof propDef.default === 'undefined') { -+ continue -+ } -+ } else { -+ // Other types have no defaults. -+ continue -+ } -+ } - const propPath = `${path}/${key}`; - const validated = (0, util_1.validateOneOf)(lexicons, propPath, propDef, value[key]); - const propValue = validated.success ? validated.value : value[key]; diff --git a/src/lib/api/api-polyfill.ts b/src/lib/api/api-polyfill.ts deleted file mode 100644 index e3aec763161..00000000000 --- a/src/lib/api/api-polyfill.ts +++ /dev/null @@ -1,85 +0,0 @@ -import RNFS from 'react-native-fs' -import {BskyAgent, jsonToLex, stringifyLex} from '@atproto/api' - -const GET_TIMEOUT = 15e3 // 15s -const POST_TIMEOUT = 60e3 // 60s - -export function doPolyfill() { - BskyAgent.configure({fetch: fetchHandler}) -} - -interface FetchHandlerResponse { - status: number - headers: Record - body: any -} - -async function fetchHandler( - reqUri: string, - reqMethod: string, - reqHeaders: Record, - reqBody: any, -): Promise { - const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type'] - if (reqMimeType && reqMimeType.startsWith('application/json')) { - reqBody = stringifyLex(reqBody) - } else if ( - typeof reqBody === 'string' && - (reqBody.startsWith('/') || reqBody.startsWith('file:')) - ) { - if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) { - // HACK - // React native has a bug that inflates the size of jpegs on upload - // we get around that by renaming the file ext to .bin - // see https://github.com/facebook/react-native/issues/27099 - // -prf - const newPath = reqBody.replace(/\.jpe?g$/, '.bin') - await RNFS.moveFile(reqBody, newPath) - reqBody = newPath - } - // NOTE - // React native treats bodies with {uri: string} as file uploads to pull from cache - // -prf - reqBody = {uri: reqBody} - } - - const controller = new AbortController() - const to = setTimeout( - () => controller.abort(), - reqMethod === 'post' ? POST_TIMEOUT : GET_TIMEOUT, - ) - - const res = await fetch(reqUri, { - method: reqMethod, - headers: reqHeaders, - body: reqBody, - signal: controller.signal, - }) - - const resStatus = res.status - const resHeaders: Record = {} - res.headers.forEach((value: string, key: string) => { - resHeaders[key] = value - }) - const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type'] - let resBody - if (resMimeType) { - if (resMimeType.startsWith('application/json')) { - resBody = jsonToLex(await res.json()) - } else if (resMimeType.startsWith('text/')) { - resBody = await res.text() - } else if (resMimeType === 'application/vnd.ipld.car') { - resBody = await res.arrayBuffer() - } else { - throw new Error('Non-supported mime type') - } - } - - clearTimeout(to) - - return { - status: resStatus, - headers: resHeaders, - body: resBody, - } -} diff --git a/src/lib/api/api-polyfill.web.ts b/src/lib/api/api-polyfill.web.ts deleted file mode 100644 index 1ad22b3d022..00000000000 --- a/src/lib/api/api-polyfill.web.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function doPolyfill() { - // no polyfill is needed on web -} diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index eb54dd29c14..6db96a8d639 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -1,7 +1,6 @@ import { AppBskyFeedDefs, AppBskyFeedGetFeed as GetCustomFeed, - AtpAgent, BskyAgent, } from '@atproto/api' @@ -51,7 +50,7 @@ export class CustomFeedAPI implements FeedAPI { const agent = this.agent const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) - const res = agent.session + const res = agent.did ? await this.agent.app.bsky.feed.getFeed( { ...this.params, @@ -106,34 +105,32 @@ async function loggedOutFetch({ let contentLangs = getContentLanguages().join(',') // manually construct fetch call so we can add the `lang` cache-busting param - let res = await AtpAgent.fetch!( + let res = await fetch( `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ cursor ? `&cursor=${cursor}` : '' }&limit=${limit}&lang=${contentLangs}`, - 'GET', - {'Accept-Language': contentLangs}, - undefined, + {method: 'GET', headers: {'Accept-Language': contentLangs}}, ) - if (res.body?.feed?.length) { + let data = res.ok ? await res.json() : null + if (data?.feed?.length) { return { success: true, - data: res.body, + data, } } // no data, try again with language headers removed - res = await AtpAgent.fetch!( + res = await fetch( `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ cursor ? `&cursor=${cursor}` : '' }&limit=${limit}`, - 'GET', - {'Accept-Language': ''}, - undefined, + {method: 'GET', headers: {'Accept-Language': ''}}, ) - if (res.body?.feed?.length) { + data = res.ok ? await res.json() : null + if (data?.feed?.length) { return { success: true, - data: res.body, + data, } } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 5b1c998cb84..d2d8bcde2cb 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -6,7 +6,6 @@ import { AppBskyFeedThreadgate, BskyAgent, ComAtprotoLabelDefs, - ComAtprotoRepoUploadBlob, RichText, } from '@atproto/api' import {AtUri} from '@atproto/api' @@ -15,10 +14,13 @@ import {logger} from '#/logger' import {ThreadgateSetting} from '#/state/queries/threadgate' import {isNetworkError} from 'lib/strings/errors' import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip' -import {isNative, isWeb} from 'platform/detection' +import {isNative} from 'platform/detection' import {ImageModel} from 'state/models/media/image' import {LinkMeta} from '../link-meta/link-meta' import {safeDeleteAsync} from '../media/manip' +import {uploadBlob} from './upload-blob' + +export {uploadBlob} export interface ExternalEmbedDraft { uri: string @@ -28,25 +30,6 @@ export interface ExternalEmbedDraft { localThumb?: ImageModel } -export async function uploadBlob( - agent: BskyAgent, - blob: string, - encoding: string, -): Promise { - if (isWeb) { - // `blob` should be a data uri - return agent.uploadBlob(convertDataURIToUint8Array(blob), { - encoding, - }) - } else { - // `blob` should be a path to a file in the local FS - return agent.uploadBlob( - blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts - {encoding}, - ) - } -} - interface PostOpts { rawText: string replyTo?: string @@ -297,7 +280,7 @@ export async function createThreadgate( const postUrip = new AtUri(postUri) await agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + repo: agent.accountDid, collection: 'app.bsky.feed.threadgate', rkey: postUrip.rkey, record: { @@ -308,15 +291,3 @@ export async function createThreadgate( }, }) } - -// helpers -// = - -function convertDataURIToUint8Array(uri: string): Uint8Array { - var raw = window.atob(uri.substring(uri.indexOf(';base64,') + 8)) - var binary = new Uint8Array(new ArrayBuffer(raw.length)) - for (let i = 0; i < raw.length; i++) { - binary[i] = raw.charCodeAt(i) - } - return binary -} diff --git a/src/lib/api/upload-blob.ts b/src/lib/api/upload-blob.ts new file mode 100644 index 00000000000..0814d5185b9 --- /dev/null +++ b/src/lib/api/upload-blob.ts @@ -0,0 +1,82 @@ +import RNFS from 'react-native-fs' +import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api' + +/** + * @param encoding Allows overriding the blob's type + */ +export async function uploadBlob( + agent: BskyAgent, + input: string | Blob, + encoding?: string, +): Promise { + if (typeof input === 'string' && input.startsWith('file:')) { + const blob = await asBlob(input) + return agent.uploadBlob(blob, {encoding}) + } + + if (typeof input === 'string' && input.startsWith('/')) { + const blob = await asBlob(`file://${input}`) + return agent.uploadBlob(blob, {encoding}) + } + + if (typeof input === 'string' && input.startsWith('data:')) { + const blob = await fetch(input).then(r => r.blob()) + return agent.uploadBlob(blob, {encoding}) + } + + if (input instanceof Blob) { + return agent.uploadBlob(input, {encoding}) + } + + throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) +} + +async function asBlob(uri: string): Promise { + return withSafeFile(uri, async safeUri => { + // Note + // Android does not support `fetch()` on `file://` URIs. for this reason, we + // use XMLHttpRequest instead of simply calling: + + // return fetch(safeUri.replace('file:///', 'file:/')).then(r => r.blob()) + + return await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.onload = () => resolve(xhr.response) + xhr.onerror = () => reject(new Error('Failed to load blob')) + xhr.responseType = 'blob' + xhr.open('GET', safeUri, true) + xhr.send(null) + }) + }) +} + +// HACK +// React native has a bug that inflates the size of jpegs on upload +// we get around that by renaming the file ext to .bin +// see https://github.com/facebook/react-native/issues/27099 +// -prf +async function withSafeFile( + uri: string, + fn: (path: string) => Promise, +): Promise { + if (uri.endsWith('.jpeg') || uri.endsWith('.jpg')) { + // Since we don't "own" the file, we should avoid renaming or modifying it. + // Instead, let's copy it to a temporary file and use that (then remove the + // temporary file). + const newPath = uri.replace(/\.jpe?g$/, '.bin') + try { + await RNFS.copyFile(uri, newPath) + } catch { + // Failed to copy the file, just use the original + return await fn(uri) + } + try { + return await fn(newPath) + } finally { + // Remove the temporary file + await RNFS.unlink(newPath) + } + } else { + return fn(uri) + } +} diff --git a/src/lib/api/upload-blob.web.ts b/src/lib/api/upload-blob.web.ts new file mode 100644 index 00000000000..d3c52190c11 --- /dev/null +++ b/src/lib/api/upload-blob.web.ts @@ -0,0 +1,26 @@ +import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api' + +/** + * @note It is recommended, on web, to use the `file` instance of the file + * selector input element, rather than a `data:` URL, to avoid + * loading the file into memory. `File` extends `Blob` "file" instances can + * be passed directly to this function. + */ +export async function uploadBlob( + agent: BskyAgent, + input: string | Blob, + encoding?: string, +): Promise { + if (typeof input === 'string' && input.startsWith('data:')) { + const blob = await fetch(input).then(r => r.blob()) + return agent.uploadBlob(blob, {encoding}) + } + + if (input instanceof Blob) { + return agent.uploadBlob(input, { + encoding, + }) + } + + throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) +} diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 3e647004bb8..3f01e98c5ee 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -218,13 +218,7 @@ export async function safeDeleteAsync(path: string) { // Normalize is necessary for Android, otherwise it doesn't delete. const normalizedPath = normalizePath(path) try { - await Promise.allSettled([ - deleteAsync(normalizedPath, {idempotent: true}), - // HACK: Try this one too. Might exist due to api-polyfill hack. - deleteAsync(normalizedPath.replace(/\.jpe?g$/, '.bin'), { - idempotent: true, - }), - ]) + await deleteAsync(normalizedPath, {idempotent: true}) } catch (e) { console.error('Failed to delete file', e) } diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx index 4e4fedcfae2..69ef93618d8 100644 --- a/src/screens/SignupQueued.tsx +++ b/src/screens/SignupQueued.tsx @@ -40,7 +40,7 @@ export function SignupQueued() { const res = await agent.com.atproto.temp.checkSignupQueue() if (res.data.activated) { // ready to go, exchange the access token for a usable one and kick off onboarding - await agent.refreshSession() + await agent.sessionManager.refreshSession() if (!isSignupQueued(agent.session?.accessJwt)) { onboardingDispatch({type: 'start'}) } diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 9bb57fcaf62..a264abfe5d2 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -37,14 +37,14 @@ export function usePreferencesQuery() { refetchOnWindowFocus: true, queryKey: preferencesQueryKey, queryFn: async () => { - if (agent.session?.did === undefined) { + if (!agent.did) { return DEFAULT_LOGGED_OUT_PREFERENCES } else { const res = await agent.getPreferences() // save to local storage to ensure there are labels on initial requests saveLabelers( - agent.session.did, + agent.did, res.moderationPrefs.labelers.map(l => l.did), ) diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index 486604169a4..731b66b0e95 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -27,7 +27,7 @@ describe('session', () => { `) const agent = new BskyAgent({service: 'https://alice.com'}) - agent.session = { + agent.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -118,7 +118,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -166,7 +166,7 @@ describe('session', () => { `) const agent2 = new BskyAgent({service: 'https://bob.com'}) - agent2.session = { + agent2.sessionManager.session = { active: true, did: 'bob-did', handle: 'bob.test', @@ -230,7 +230,7 @@ describe('session', () => { `) const agent3 = new BskyAgent({service: 'https://alice.com'}) - agent3.session = { + agent3.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice-updated.test', @@ -294,7 +294,7 @@ describe('session', () => { `) const agent4 = new BskyAgent({service: 'https://jay.com'}) - agent4.session = { + agent4.sessionManager.session = { active: true, did: 'jay-did', handle: 'jay.test', @@ -445,7 +445,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -502,7 +502,7 @@ describe('session', () => { `) const agent2 = new BskyAgent({service: 'https://alice.com'}) - agent2.session = { + agent2.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -553,7 +553,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -598,7 +598,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -606,7 +606,7 @@ describe('session', () => { refreshJwt: 'alice-refresh-jwt-1', } const agent2 = new BskyAgent({service: 'https://bob.com'}) - agent2.session = { + agent2.sessionManager.session = { active: true, did: 'bob-did', handle: 'bob.test', @@ -678,7 +678,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -695,7 +695,7 @@ describe('session', () => { expect(state.accounts.length).toBe(1) expect(state.currentAgentState.did).toBe('alice-did') - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice-updated.test', @@ -748,7 +748,7 @@ describe('session', () => { } `) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice-updated.test', @@ -801,7 +801,7 @@ describe('session', () => { } `) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice-updated.test', @@ -859,7 +859,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -876,7 +876,7 @@ describe('session', () => { expect(state.accounts.length).toBe(1) expect(state.currentAgentState.did).toBe('alice-did') - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice-updated.test', @@ -907,7 +907,7 @@ describe('session', () => { ]) expect(lastState === state).toBe(true) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice-updated.test', @@ -931,7 +931,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -940,7 +940,7 @@ describe('session', () => { } const agent2 = new BskyAgent({service: 'https://bob.com'}) - agent2.session = { + agent2.sessionManager.session = { active: true, did: 'bob-did', handle: 'bob.test', @@ -965,7 +965,7 @@ describe('session', () => { expect(state.accounts.length).toBe(2) expect(state.currentAgentState.did).toBe('bob-did') - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice-updated.test', @@ -1032,7 +1032,7 @@ describe('session', () => { } `) - agent2.session = { + agent2.sessionManager.session = { active: true, did: 'bob-did', handle: 'bob-updated.test', @@ -1099,7 +1099,7 @@ describe('session', () => { // Ignore other events for inactive agent. const lastState = state - agent1.session = undefined + agent1.sessionManager.session = undefined state = run(state, [ { type: 'received-agent-event', @@ -1126,7 +1126,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -1135,7 +1135,7 @@ describe('session', () => { } const agent2 = new BskyAgent({service: 'https://bob.com'}) - agent2.session = { + agent2.sessionManager.session = { active: true, did: 'bob-did', handle: 'bob.test', @@ -1162,7 +1162,7 @@ describe('session', () => { expect(state.accounts.length).toBe(1) expect(state.currentAgentState.did).toBe('bob-did') - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -1188,7 +1188,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -1206,7 +1206,7 @@ describe('session', () => { expect(state.accounts.length).toBe(1) expect(state.currentAgentState.did).toBe('alice-did') - agent1.session = undefined + agent1.sessionManager.session = undefined state = run(state, [ { type: 'received-agent-event', @@ -1255,7 +1255,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -1273,7 +1273,7 @@ describe('session', () => { expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') expect(state.currentAgentState.did).toBe('alice-did') - agent1.session = undefined + agent1.sessionManager.session = undefined state = run(state, [ { type: 'received-agent-event', @@ -1320,7 +1320,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -1338,7 +1338,7 @@ describe('session', () => { expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') expect(state.currentAgentState.did).toBe('alice-did') - agent1.session = undefined + agent1.sessionManager.session = undefined state = run(state, [ { type: 'received-agent-event', @@ -1385,7 +1385,7 @@ describe('session', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) - agent1.session = { + agent1.sessionManager.session = { active: true, did: 'alice-did', handle: 'alice.test', @@ -1393,7 +1393,7 @@ describe('session', () => { refreshJwt: 'alice-refresh-jwt-1', } const agent2 = new BskyAgent({service: 'https://bob.com'}) - agent2.session = { + agent2.sessionManager.session = { active: true, did: 'bob-did', handle: 'bob.test', @@ -1416,7 +1416,7 @@ describe('session', () => { expect(state.currentAgentState.did).toBe('bob-did') const anotherTabAgent1 = new BskyAgent({service: 'https://jay.com'}) - anotherTabAgent1.session = { + anotherTabAgent1.sessionManager.session = { active: true, did: 'jay-did', handle: 'jay.test', @@ -1424,7 +1424,7 @@ describe('session', () => { refreshJwt: 'jay-refresh-jwt-1', } const anotherTabAgent2 = new BskyAgent({service: 'https://alice.com'}) - anotherTabAgent2.session = { + anotherTabAgent2.sessionManager.session = { active: true, did: 'bob-did', handle: 'bob.test', @@ -1492,7 +1492,7 @@ describe('session', () => { `) const anotherTabAgent3 = new BskyAgent({service: 'https://clarence.com'}) - anotherTabAgent3.session = { + anotherTabAgent3.sessionManager.session = { active: true, did: 'clarence-did', handle: 'clarence.test', diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 4456ab0bf9b..73be34bb277 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -1,4 +1,9 @@ -import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api' +import { + AtpPersistSessionHandler, + AtpSessionData, + AtpSessionEvent, + BskyAgent, +} from '@atproto/api' import {TID} from '@atproto/common-web' import {networkRetry} from '#/lib/async/retry' @@ -20,6 +25,8 @@ import { import {SessionAccount} from './types' import {isSessionExpired, isSignupQueued} from './util' +type SetPersistSessionHandler = (cb: AtpPersistSessionHandler) => void + export function createPublicAgent() { configureModerationForGuest() // Side effect but only relevant for tests return new BskyAgent({service: PUBLIC_BSKY_SERVICE}) @@ -32,10 +39,11 @@ export async function createAgentAndResume( did: string, event: AtpSessionEvent, ) => void, + setPersistSessionHandler: SetPersistSessionHandler, ) { const agent = new BskyAgent({service: storedAccount.service}) if (storedAccount.pdsUrl) { - agent.pdsUrl = agent.api.xrpc.uri = new URL(storedAccount.pdsUrl) + agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl) } const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency') const moderation = configureModerationForAccount(agent, storedAccount) @@ -43,9 +51,8 @@ export async function createAgentAndResume( if (isSessionExpired(storedAccount)) { await networkRetry(1, () => agent.resumeSession(prevSession)) } else { - agent.session = prevSession + agent.sessionManager.session = prevSession if (!storedAccount.signupQueued) { - // Intentionally not awaited to unblock the UI: networkRetry(3, () => agent.resumeSession(prevSession)).catch( (e: any) => { logger.error(`networkRetry failed to resume session`, { @@ -60,7 +67,13 @@ export async function createAgentAndResume( } } - return prepareAgent(agent, gates, moderation, onSessionChange) + return prepareAgent( + agent, + gates, + moderation, + onSessionChange, + setPersistSessionHandler, + ) } export async function createAgentAndLogin( @@ -80,6 +93,7 @@ export async function createAgentAndLogin( did: string, event: AtpSessionEvent, ) => void, + setPersistSessionHandler: SetPersistSessionHandler, ) { const agent = new BskyAgent({service}) await agent.login({identifier, password, authFactorToken}) @@ -87,7 +101,13 @@ export async function createAgentAndLogin( const account = agentToSessionAccountOrThrow(agent) const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const moderation = configureModerationForAccount(agent, account) - return prepareAgent(agent, moderation, gates, onSessionChange) + return prepareAgent( + agent, + moderation, + gates, + onSessionChange, + setPersistSessionHandler, + ) } export async function createAgentAndCreateAccount( @@ -115,6 +135,7 @@ export async function createAgentAndCreateAccount( did: string, event: AtpSessionEvent, ) => void, + setPersistSessionHandler: SetPersistSessionHandler, ) { const agent = new BskyAgent({service}) await agent.createAccount({ @@ -174,7 +195,13 @@ export async function createAgentAndCreateAccount( logger.error(e, {context: `session: failed snoozeEmailConfirmationPrompt`}) } - return prepareAgent(agent, gates, moderation, onSessionChange) + return prepareAgent( + agent, + gates, + moderation, + onSessionChange, + setPersistSessionHandler, + ) } async function prepareAgent( @@ -187,13 +214,14 @@ async function prepareAgent( did: string, event: AtpSessionEvent, ) => void, + setPersistSessionHandler: (cb: AtpPersistSessionHandler) => void, ) { // There's nothing else left to do, so block on them here. await Promise.all([gates, moderation]) // Now the agent is ready. const account = agentToSessionAccountOrThrow(agent) - agent.setPersistSessionHandler(event => { + setPersistSessionHandler(event => { onSessionChange(agent, account.did, event) if (event !== 'create' && event !== 'update') { addSessionErrorLog(account.did, event) diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 3aac19025d3..a47404b8a30 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -1,5 +1,9 @@ import React from 'react' -import {AtpSessionEvent, BskyAgent} from '@atproto/api' +import { + AtpPersistSessionHandler, + AtpSessionEvent, + BskyAgent, +} from '@atproto/api' import {track} from '#/lib/analytics/analytics' import {logEvent} from '#/lib/statsig/statsig' @@ -47,6 +51,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return initialState }) + const persistSessionHandler = React.useRef< + AtpPersistSessionHandler | undefined + >(undefined) + const setPersistSessionHandler = ( + newHandler: AtpPersistSessionHandler | undefined, + ) => { + persistSessionHandler.current = newHandler + } + const onAgentSessionChange = React.useCallback( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away. @@ -73,6 +86,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const {agent, account} = await createAgentAndCreateAccount( params, onAgentSessionChange, + setPersistSessionHandler, ) if (signal.aborted) { @@ -97,6 +111,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const {agent, account} = await createAgentAndLogin( params, onAgentSessionChange, + setPersistSessionHandler, ) if (signal.aborted) { @@ -138,6 +153,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const {agent, account} = await createAgentAndResume( storedAccount, onAgentSessionChange, + setPersistSessionHandler, ) if (signal.aborted) { @@ -202,7 +218,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } else { const agent = state.currentAgentState.agent as BskyAgent const prevSession = agent.session - agent.session = sessionAccountToSession(syncedAccount) + agent.sessionManager.session = sessionAccountToSession(syncedAccount) addSessionDebugLog({ type: 'agent:patch', agent, @@ -249,8 +265,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { addSessionDebugLog({type: 'agent:switch', prevAgent, nextAgent: agent}) // We never reuse agents so let's fully neutralize the previous one. // This ensures it won't try to consume any refresh tokens. - prevAgent.session = undefined - prevAgent.setPersistSessionHandler(undefined) + prevAgent.sessionManager.session = undefined + setPersistSessionHandler(undefined) } }, [agent]) diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts index b57f1fa0b0b..7e1df500bec 100644 --- a/src/state/session/logging.ts +++ b/src/state/session/logging.ts @@ -56,7 +56,7 @@ type Log = type: 'agent:patch' agent: object prevSession: AtpSessionData | undefined - nextSession: AtpSessionData + nextSession: AtpSessionData | undefined } export function wrapSessionReducerForLogging(reducer: Reducer): Reducer { diff --git a/yarn.lock b/yarn.lock index 6450d33b790..2437aa00247 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,39 +34,65 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@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== +"@atproto-labs/fetch-node@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.0.tgz#692666d57ec24a7ba0813077a303baccf26108e0" + integrity sha512-DUHgaGw8LBqiGg51pUDuWK/alMcmNbpcK7ALzlF2Gw//TNLTsgrj0qY9aEtK+np9rEC+x/o3bN4SGnuQEpgqIg== dependencies: - "@atproto/common-web" "^0.3.0" - "@atproto/lexicon" "^0.4.0" - "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.5.0" - await-lock "^2.2.2" - multiformats "^9.9.0" - tlds "^1.234.0" + "@atproto-labs/fetch" "0.1.0" + "@atproto-labs/pipe" "0.1.0" + ipaddr.js "^2.1.0" + psl "^1.9.0" + undici "^6.14.1" + +"@atproto-labs/fetch@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.0.tgz#50a46943fd2f321dd748de28c73ba7cbfa493132" + integrity sha512-uirja+uA/C4HNk7vayM+AJqsccxQn2wVziUHxbsjJGt/K6Q8ZOKDaEX2+GrcXvpUVcqUKh+94JFjuzH+CAEUlg== + dependencies: + "@atproto-labs/pipe" "0.1.0" + optionalDependencies: + zod "^3.23.8" + +"@atproto-labs/pipe@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.0.tgz#c8d86923b6d8e900d39efe6fdcdf0d897c434086" + integrity sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w== + +"@atproto-labs/simple-store-memory@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.1.tgz#54526a1f8ec978822be9fad75106ad8b78500dd3" + integrity sha512-PCRqhnZ8NBNBvLku53O56T0lsVOtclfIrQU/rwLCc4+p45/SBPrRYNBi6YFq5rxZbK6Njos9MCmILV/KLQxrWA== + dependencies: + "@atproto-labs/simple-store" "0.1.1" + lru-cache "^10.2.0" + +"@atproto-labs/simple-store@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106" + integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg== -"@atproto/api@^0.12.3": - version "0.12.3" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.3.tgz#5b7b1c7d4210ee9315961504900c8409395cbb17" - integrity sha512-y/kGpIEo+mKGQ7VOphpqCAigTI0LZRmDThNChTfSzDKm9TzEobwiw0zUID0Yw6ot1iLLFx3nKURmuZAYlEuobw== +"@atproto/api@0.13.0", "@atproto/api@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.0.tgz#d1c65a407f1c3c6aba5be9425f4f739a01419bd8" + integrity sha512-04kzIDkoEVSP7zMVOT5ezCVQcOrbXWjGYO2YBc3/tBvQ90V1pl9I+mLyz1uUHE+wRE1IRWKACcWhAz8SrYz3pA== dependencies: "@atproto/common-web" "^0.3.0" - "@atproto/lexicon" "^0.4.0" + "@atproto/lexicon" "^0.4.1" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.5.0" + "@atproto/xrpc" "^0.6.0" + await-lock "^2.2.2" multiformats "^9.9.0" tlds "^1.234.0" -"@atproto/aws@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.0.tgz#17f3faf744824457cabd62f87be8bf08cacf8029" - integrity sha512-F09SHiC9CX3ydfrvYZbkpfES48UGCQNnznNVgJ3QyKSN8ON+BoWmGCpAFtn3AWeEoU0w9h0hypNvUm5nORv+5g== +"@atproto/aws@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.2.tgz#703e5e06f288bcf61c6d99a990738f1e7299e653" + integrity sha512-j7eR7+sQumFsc66/5xyCDez9JtR6dlZc+fOdwdh85nCJD4zmQyU4r1CKrA48wQ3tkzze+ASEb1SgODuIQmIugA== dependencies: - "@atproto/common" "^0.4.0" + "@atproto/common" "^0.4.1" "@atproto/crypto" "^0.4.0" - "@atproto/repo" "^0.4.0" + "@atproto/repo" "^0.4.2" "@aws-sdk/client-cloudfront" "^3.261.0" "@aws-sdk/client-kms" "^3.196.0" "@aws-sdk/client-s3" "^3.224.0" @@ -76,19 +102,19 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.45": - version "0.0.45" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.45.tgz#c3083d8038fe8c5ff921d9bcb0b5a043cc840827" - integrity sha512-osWeigdYzQH2vZki+eszCR8ta9zdUB4om79aFmnE+zvxw7HFduwAAbcHf6kmmiLCfaOWvCsYb1wS2i3IC66TAg== +"@atproto/bsky@^0.0.74": + version "0.0.74" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.74.tgz#b735af6ded16778604378710a2e871350c29570a" + integrity sha512-vyukmlBamoET0sZnDMOeTGAkQNV7KbHg65uIQ6OX4/QGynyaQP8SvSF0OsEBzBqOraxV1w9WT8AZrUbyl3uvIg== dependencies: - "@atproto/api" "^0.12.3" - "@atproto/common" "^0.4.0" + "@atproto/api" "^0.13.0" + "@atproto/common" "^0.4.1" "@atproto/crypto" "^0.4.0" "@atproto/identity" "^0.4.0" - "@atproto/lexicon" "^0.4.0" - "@atproto/repo" "^0.4.0" + "@atproto/lexicon" "^0.4.1" + "@atproto/repo" "^0.4.2" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc-server" "^0.5.1" + "@atproto/xrpc-server" "^0.6.1" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" "@connectrpc/connect-express" "^1.1.4" @@ -105,19 +131,20 @@ multiformats "^9.9.0" p-queue "^6.6.2" pg "^8.10.0" - pino "^8.15.0" + pino "^8.21.0" pino-http "^8.2.1" sharp "^0.32.6" + statsig-node "^5.23.1" structured-headers "^1.0.1" typed-emitter "^2.1.0" uint8arrays "3.0.0" -"@atproto/bsync@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.3.tgz#2b0b8ef3686cf177846a80088317f2e89d1bf88f" - integrity sha512-tJRwNgXzfNV57lzgWPvjtb1OMlMJH9SpsMeYhIii16zcaFUWwsb474BicKpkGRT+iCvtYzBT6gWlZE2Ijnhf7w== +"@atproto/bsync@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.5.tgz#bf2fa45e4595fda12addcd6784314e4dbe409046" + integrity sha512-xCCMHy14y4tQoXiGrfd0XjSnc4q7I9bUNqju9E8jrP95QTDedH1FQgybStbUIbHt0eEqY5v9E7iZBH3n7Kiz7A== dependencies: - "@atproto/common" "^0.4.0" + "@atproto/common" "^0.4.1" "@atproto/syntax" "^0.3.0" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" @@ -158,17 +185,17 @@ pino "^8.6.1" zod "^3.14.2" -"@atproto/common@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.0.tgz#d77696c7eb545426df727837d9ee333b429fe7ef" - integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g== +"@atproto/common@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.1.tgz#ca6fce47001ce8d031acd3fb4942fbfd81f72c43" + integrity sha512-uL7kQIcBTbvkBDNfxMXL6lBH4fO2DQpHd2BryJxMtbw/4iEPKe9xBYApwECHhEIk9+zhhpTRZ15FJ3gxTXN82Q== dependencies: "@atproto/common-web" "^0.3.0" "@ipld/dag-cbor" "^7.0.3" cbor-x "^1.5.1" iso-datestring-validator "^2.2.2" multiformats "^9.9.0" - pino "^8.15.0" + pino "^8.21.0" "@atproto/crypto@0.1.0": version "0.1.0" @@ -190,22 +217,22 @@ "@noble/hashes" "^1.3.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.5.tgz#cd13313dbc52131731d039a1d22808ee8193505d" - integrity sha512-dqRNihzX1xIHbWPHmfYsliUUXyZn5FFhCeButrGie5soQmHA4okQJTB1XWDly3mdHLjUM90g+5zjRSAKoui77Q== +"@atproto/dev-env@^0.3.39": + version "0.3.39" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.39.tgz#f498f087d4da43d5f86805c07d5f2b781e60fd6f" + integrity sha512-rIeUO99DL8/gRKYEAkAFuTn77y8letEbKMXnfpsVX2YHD89VRdDyMxkYzRu2+31UjtGv62I+qTLLKQS4EcFItA== dependencies: - "@atproto/api" "^0.12.3" - "@atproto/bsky" "^0.0.45" - "@atproto/bsync" "^0.0.3" + "@atproto/api" "^0.13.0" + "@atproto/bsky" "^0.0.74" + "@atproto/bsync" "^0.0.5" "@atproto/common-web" "^0.3.0" "@atproto/crypto" "^0.4.0" "@atproto/identity" "^0.4.0" - "@atproto/lexicon" "^0.4.0" - "@atproto/ozone" "^0.1.7" - "@atproto/pds" "^0.4.14" + "@atproto/lexicon" "^0.4.1" + "@atproto/ozone" "^0.1.36" + "@atproto/pds" "^0.4.48" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc-server" "^0.5.1" + "@atproto/xrpc-server" "^0.6.1" "@did-plc/lib" "^0.0.1" "@did-plc/server" "^0.0.1" axios "^0.27.2" @@ -224,30 +251,79 @@ "@atproto/crypto" "^0.4.0" axios "^0.27.2" -"@atproto/lexicon@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576" - integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ== +"@atproto/jwk-jose@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.2.tgz#236eadb740b498689d9a912d1254aa9ff58890a1" + integrity sha512-lDwc/6lLn2aZ/JpyyggyjLFsJPMntrVzryyGUx5aNpuTS8SIuc4Ky0REhxqfLopQXJJZCuRRjagHG3uP05/moQ== + dependencies: + "@atproto/jwk" "0.1.1" + jose "^5.2.0" + +"@atproto/jwk@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.1.1.tgz#15bcad4a1778eeb20c82108e0ec55fef45cd07b6" + integrity sha512-6h/bj1APUk7QcV9t/oA6+9DB5NZx9SZru9x+/pV5oHFI9Xz4ZuM5+dq1PfsJV54pZyqdnZ6W6M717cxoC7q7og== + dependencies: + multiformats "^9.9.0" + zod "^3.23.8" + +"@atproto/lexicon@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0" + integrity sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA== dependencies: "@atproto/common-web" "^0.3.0" "@atproto/syntax" "^0.3.0" iso-datestring-validator "^2.2.2" multiformats "^9.9.0" - zod "^3.21.4" + zod "^3.23.8" -"@atproto/ozone@^0.1.7": - version "0.1.7" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.7.tgz#248d88e1acfe56936651754975472d03d047d689" - integrity sha512-vvaV0MFynOzZJcL8m8mEW21o1FFIkP+wHTXEC9LJrL3h03+PMaby8Ujmif6WX5eikhfxvr9xsU/Jxbi/iValuQ== +"@atproto/oauth-provider@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.1.2.tgz#a576a4c7795c7938a994e76192c19a2e73ffcddf" + integrity sha512-z1YKK0XLDfSDtLP5ntPCviEtajvUHbI4TwzYQ5X9CAL9PoXjqhQg0U/csg1wGDs8qkbphF9gni9M2stlpH7H0g== + dependencies: + "@atproto-labs/fetch" "0.1.0" + "@atproto-labs/fetch-node" "0.1.0" + "@atproto-labs/pipe" "0.1.0" + "@atproto-labs/simple-store" "0.1.1" + "@atproto-labs/simple-store-memory" "0.1.1" + "@atproto/jwk" "0.1.1" + "@atproto/jwk-jose" "0.1.2" + "@atproto/oauth-types" "0.1.2" + "@hapi/accept" "^6.0.3" + "@hapi/bourne" "^3.0.0" + cookie "^0.6.0" + http-errors "^2.0.0" + jose "^5.2.0" + oidc-token-hash "^5.0.3" + psl "^1.9.0" + zod "^3.23.8" + optionalDependencies: + ioredis "^5.3.2" + keygrip "^1.1.0" + +"@atproto/oauth-types@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.1.2.tgz#d6c497c8e5f88f1875c630adde4ed9c5d8a8b4f4" + integrity sha512-yySPPTLxteFJ3O3xVWEhvBFx7rczgo4LK2nQNeqAPMZdYd5dpgvuZZ88nQQge074BfuOc0MWTnr0kPdxQMjjPw== + dependencies: + "@atproto/jwk" "0.1.1" + zod "^3.23.8" + +"@atproto/ozone@^0.1.36": + version "0.1.36" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.36.tgz#6a1a71fdff3ff486c5951a9e491e954b51703d53" + integrity sha512-BQThLU5RFG+/bZli/fj5YrFU8jW5rkium7aplfJX2eHkV6huJnBU5DcgracjH2paPGC5L/zjYtibz5spqatKAg== dependencies: - "@atproto/api" "^0.12.3" - "@atproto/common" "^0.4.0" + "@atproto/api" "^0.13.0" + "@atproto/common" "^0.4.1" "@atproto/crypto" "^0.4.0" "@atproto/identity" "^0.4.0" - "@atproto/lexicon" "^0.4.0" + "@atproto/lexicon" "^0.4.1" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.5.0" - "@atproto/xrpc-server" "^0.5.1" + "@atproto/xrpc" "^0.6.0" + "@atproto/xrpc-server" "^0.6.1" "@did-plc/lib" "^0.0.1" axios "^1.6.7" compression "^1.7.4" @@ -255,30 +331,34 @@ express "^4.17.2" http-terminator "^3.2.0" kysely "^0.22.0" + lande "^1.0.10" multiformats "^9.9.0" p-queue "^6.6.2" pg "^8.10.0" pino-http "^8.2.1" + structured-headers "^1.0.1" typed-emitter "^2.1.0" uint8arrays "3.0.0" -"@atproto/pds@^0.4.14": - version "0.4.14" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.14.tgz#5b55ef307323bda712f2ddaba5c1fff7740ed91b" - integrity sha512-rqVcvtw5oMuuJIpWZbSSTSx19+JaZyUcg9OEjdlUmyEpToRN88zTEQySEksymrrLQkW/LPRyWGd7WthbGEuEfQ== +"@atproto/pds@^0.4.48": + version "0.4.48" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.48.tgz#34f29846a0585f5cc33f1685eb75ad730b7dcb9f" + integrity sha512-B5FpmECkGtA0EyhiB5rfhmQArmGekqqyzFnPlNpO5vOUrTTVKc9mgGfHLVJtrnwDUfGAuIgpigqZ8HgwS0DnMA== dependencies: - "@atproto/api" "^0.12.3" - "@atproto/aws" "^0.2.0" - "@atproto/common" "^0.4.0" + "@atproto-labs/fetch-node" "0.1.0" + "@atproto/api" "^0.13.0" + "@atproto/aws" "^0.2.2" + "@atproto/common" "^0.4.1" "@atproto/crypto" "^0.4.0" "@atproto/identity" "^0.4.0" - "@atproto/lexicon" "^0.4.0" - "@atproto/repo" "^0.4.0" + "@atproto/lexicon" "^0.4.1" + "@atproto/oauth-provider" "^0.1.2" + "@atproto/repo" "^0.4.2" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.5.0" - "@atproto/xrpc-server" "^0.5.1" + "@atproto/xrpc" "^0.6.0" + "@atproto/xrpc-server" "^0.6.1" "@did-plc/lib" "^0.0.4" - better-sqlite3 "^9.4.0" + better-sqlite3 "^10.0.0" bytes "^3.1.2" compression "^1.7.4" cors "^2.8.5" @@ -297,41 +377,42 @@ nodemailer "^6.8.0" nodemailer-html-to-text "^3.2.0" p-queue "^6.6.2" - pino "^8.15.0" + pino "^8.21.0" pino-http "^8.2.1" sharp "^0.32.6" typed-emitter "^2.1.0" uint8arrays "3.0.0" - zod "^3.21.4" + zod "^3.23.8" -"@atproto/repo@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.4.0.tgz#e5d3195a8e4233c9bf060737b18ddee905af2d9a" - integrity sha512-LB0DF/D8r8hB+qiGB0sWZuq7TSJYbWel+t572aCrLeCOmbRgnLkGPLUTOOUvLFYv8xz1BPZTbI8hy/vcUV79VA== +"@atproto/repo@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.4.2.tgz#311eef52ef5df0b6f969fb4b329935a32db05313" + integrity sha512-6hEGA3BmasPCoBGaIN/jKAjKJidCf+z8exkx/77V3WB7TboucSLHn/8gg+Xf03U7bJd6mn3F0YmPaRfJwqIT8w== dependencies: - "@atproto/common" "^0.4.0" + "@atproto/common" "^0.4.1" "@atproto/common-web" "^0.3.0" "@atproto/crypto" "^0.4.0" - "@atproto/lexicon" "^0.4.0" + "@atproto/lexicon" "^0.4.1" "@ipld/car" "^3.2.3" "@ipld/dag-cbor" "^7.0.0" multiformats "^9.9.0" uint8arrays "3.0.0" - zod "^3.21.4" + zod "^3.23.8" "@atproto/syntax@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== -"@atproto/xrpc-server@^0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.5.1.tgz#f63c86ba60bd5b9c5a641ea57191ff83d9db41fd" - integrity sha512-SXU6dscVe5iYxPeV79QIFs/yEEu7LLOzyHGoHG1kSNO6DjwxXTdcWOc8GSYGV6H+7VycOoPZPkyD9q4teJlj/w== +"@atproto/xrpc-server@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.6.1.tgz#c8c75065ab6bc1a7f5c121b558acb5213f2afda6" + integrity sha512-Qm0aJC1LbYYHaRGWoh0D2iG48VwRha1T1NEP/D5UkD4GzfjT8m5PDiZBtcyspJD/BEC7UYX9/BhMYCoZLQMYcA== dependencies: - "@atproto/common" "^0.4.0" + "@atproto/common" "^0.4.1" "@atproto/crypto" "^0.4.0" - "@atproto/lexicon" "^0.4.0" + "@atproto/lexicon" "^0.4.1" + "@atproto/xrpc" "^0.6.0" cbor-x "^1.5.1" express "^4.17.2" http-errors "^2.0.0" @@ -339,15 +420,15 @@ rate-limiter-flexible "^2.4.1" uint8arrays "3.0.0" ws "^8.12.0" - zod "^3.21.4" + zod "^3.23.8" -"@atproto/xrpc@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032" - integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog== +"@atproto/xrpc@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.0.tgz#668c3262e67e2afa65951ea79a03bfe3720ddf5c" + integrity sha512-5BbhBTv5j6MC3iIQ4+vYxQE7nLy2dDGQ+LYJrH8PptOCUdq0Pwg6aRccQ3y52kUZlhE/mzOTZ8Ngiy9pSAyfVQ== dependencies: - "@atproto/lexicon" "^0.4.0" - zod "^3.21.4" + "@atproto/lexicon" "^0.4.1" + zod "^3.23.8" "@aws-crypto/crc32@3.0.0": version "3.0.0" @@ -4001,6 +4082,31 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@hapi/accept@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab" + integrity sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw== + dependencies: + "@hapi/boom" "^10.0.1" + "@hapi/hoek" "^11.0.2" + +"@hapi/boom@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685" + integrity sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA== + dependencies: + "@hapi/hoek" "^11.0.2" + +"@hapi/bourne@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-3.0.0.tgz#f11fdf7dda62fe8e336fa7c6642d9041f30356d7" + integrity sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w== + +"@hapi/hoek@^11.0.2": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.4.tgz#42a7f244fd3dd777792bfb74b8c6340ae9182f37" + integrity sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ== + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -9453,10 +9559,10 @@ better-opn@~3.0.2: dependencies: open "^8.0.4" -better-sqlite3@^9.4.0: - version "9.4.5" - resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-9.4.5.tgz#1d3422443a9924637cb06cc3ccc941b2ae932c65" - integrity sha512-uFVyoyZR9BNcjSca+cp3MWCv6upAv+tbMC4SWM51NIMhoQOm4tjIkyxFO/ZsYdGAF61WJBgdzyJcz4OokJi0gQ== +better-sqlite3@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-10.1.0.tgz#8dc07e496fc014a7cd2211f79e591f6ba92838e8" + integrity sha512-hqpHJaCfKEZFaAWdMh6crdzRWyzQzfP6Ih8TYI0vFn01a6ZTDSbJIMXN+6AMBaBOh99DzUy8l3PsV9R3qnJDng== dependencies: bindings "^1.5.0" prebuild-install "^7.1.1" @@ -10305,6 +10411,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + copy-webpack-plugin@^10.2.0: version "10.2.4" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe" @@ -13774,6 +13885,11 @@ ip-regex@^2.1.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" integrity sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw== +ip3country@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ip3country/-/ip3country-5.0.0.tgz#f1394b050c51ba9c10cc691c8eb240bba3d7177a" + integrity sha512-lcFLMFU4eO1Z7tIpbVFZkaZ5ltqpeaRx7L9NsAbA9uA7/O/rj3RF8+evE5gDitooaTTIqjdzZrenFO/OOxQ2ew== + ipaddr.js@1.9.1, ipaddr.js@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -13784,6 +13900,11 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + is-arguments@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -15346,6 +15467,11 @@ jose@^5.0.1: resolved "https://registry.yarnpkg.com/jose/-/jose-5.1.3.tgz#303959d85c51b5cb14725f930270b72be56abdca" integrity sha512-GPExOkcMsCLBTi1YetY2LmkoY559fss0+0KVa6kOfb2YFe84nAM7Nm/XzuZozah4iHgmBGrCOHL5/cy670SBRw== +jose@^5.2.0: + version "5.6.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.6.3.tgz#415688bc84875461c86dfe271ea6029112a23e27" + integrity sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g== + js-base64@^3.7.2: version "3.7.5" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca" @@ -15598,6 +15724,13 @@ key-encoder@^2.0.3: bn.js "^4.11.8" elliptic "^6.4.1" +keygrip@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + dependencies: + tsscmp "1.0.6" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -16771,6 +16904,13 @@ node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, nod dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.13: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1, node-forge@^1.2.1, node-forge@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -17019,6 +17159,11 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +oidc-token-hash@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6" + integrity sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw== + on-exit-leak-free@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" @@ -17562,18 +17707,18 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== -pino-abstract-transport@v1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" - integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== dependencies: readable-stream "^4.0.0" split2 "^4.0.0" -pino-abstract-transport@v1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz#083d98f966262164504afb989bccd05f665937a8" - integrity sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA== +pino-abstract-transport@v1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== dependencies: readable-stream "^4.0.0" split2 "^4.0.0" @@ -17610,22 +17755,22 @@ pino@^8.0.0, pino@^8.11.0, pino@^8.6.1: sonic-boom "^3.1.0" thread-stream "^2.0.0" -pino@^8.15.0: - version "8.15.1" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.15.1.tgz#04b815ff7aa4e46b1bbab88d8010aaa2b17eaba4" - integrity sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA== +pino@^8.21.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== dependencies: atomic-sleep "^1.0.0" fast-redact "^3.1.1" on-exit-leak-free "^2.1.0" - pino-abstract-transport v1.1.0 + pino-abstract-transport "^1.2.0" pino-std-serializers "^6.0.0" - process-warning "^2.0.0" + process-warning "^3.0.0" quick-format-unescaped "^4.0.3" real-require "^0.2.0" safe-stable-stringify "^2.3.1" - sonic-boom "^3.1.0" - thread-stream "^2.0.0" + sonic-boom "^3.7.0" + thread-stream "^2.6.0" pirates@^4.0.1, pirates@^4.0.4, pirates@^4.0.5: version "4.0.6" @@ -18387,6 +18532,11 @@ process-warning@^2.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626" integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg== +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -20224,6 +20374,13 @@ sonic-boom@^3.1.0: dependencies: atomic-sleep "^1.0.0" +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + source-list-map@^2.0.0, source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -20414,6 +20571,16 @@ statsig-js@4.45.1: js-sha256 "^0.10.1" uuid "^8.3.2" +statsig-node@^5.23.1: + version "5.25.1" + resolved "https://registry.yarnpkg.com/statsig-node/-/statsig-node-5.25.1.tgz#6d8ea9ecaad6c09250e5ff7d33eda9fd0f9c05f4" + integrity sha512-K8+1psxFVdFr5LyXwDotJqBl7uKt8vbZO2e/9zzbLI4yDOuLDoItG5Ju5QAR0oUfEdEAANOzwV2yA052Wrc/Xw== + dependencies: + ip3country "^5.0.0" + node-fetch "^2.6.13" + ua-parser-js "^1.0.2" + uuid "^8.3.2" + statsig-react-native-expo@^4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/statsig-react-native-expo/-/statsig-react-native-expo-4.6.1.tgz#0bdf49fee7112f7f28bff2405f4ba0c1727bb3d6" @@ -21047,6 +21214,13 @@ thread-stream@^2.0.0: dependencies: real-require "^0.2.0" +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== + dependencies: + real-require "^0.2.0" + throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" @@ -21246,6 +21420,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -21383,6 +21562,11 @@ ua-parser-js@^0.7.33: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== +ua-parser-js@^1.0.2: + version "1.0.38" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2" + integrity sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ== + ua-parser-js@^1.0.35: version "1.0.35" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011" @@ -21427,6 +21611,11 @@ undici@^5.28.2: dependencies: "@fastify/busboy" "^2.0.0" +undici@^6.14.1: + version "6.19.5" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.5.tgz#5829101361b583b53206e81579f4df71c56d6be8" + integrity sha512-LryC15SWzqQsREHIOUybavaIHF5IoL0dJ9aWWxL/PgT1KfqAW5225FZpDUFlt9xiDMS2/S7DOKhFWA7RLksWdg== + unfetch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-3.1.2.tgz#dc271ef77a2800768f7b459673c5604b5101ef77" @@ -22598,7 +22787,7 @@ zod-validation-error@^3.0.3: resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.0.tgz#2cfe81b62d044e0453d1aa3ae7c32a2f36dde9af" integrity sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw== -zod@3.23.8, zod@^3.14.2, zod@^3.20.2, zod@^3.21.4, zod@^3.22.4: +zod@3.23.8, zod@^3.14.2, zod@^3.20.2, zod@^3.21.4, zod@^3.22.4, zod@^3.23.8: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== From ac5656991df93199f3c4ee58aa6e598b6d50cc12 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 12 Aug 2024 19:43:06 -0700 Subject: [PATCH 3/7] subclass agent to add setPersistSessionHandler (#4928) Co-authored-by: Dan Abramov (cherry picked from commit 3c04d9bd84b2836b3438a659c99cb16009f3af67) --- src/state/session/agent.ts | 118 +++++++++++++++++------------------- src/state/session/index.tsx | 24 ++------ 2 files changed, 60 insertions(+), 82 deletions(-) diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 73be34bb277..ea6af677cf5 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -1,9 +1,4 @@ -import { - AtpPersistSessionHandler, - AtpSessionData, - AtpSessionEvent, - BskyAgent, -} from '@atproto/api' +import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api' import {TID} from '@atproto/common-web' import {networkRetry} from '#/lib/async/retry' @@ -25,11 +20,9 @@ import { import {SessionAccount} from './types' import {isSessionExpired, isSignupQueued} from './util' -type SetPersistSessionHandler = (cb: AtpPersistSessionHandler) => void - export function createPublicAgent() { configureModerationForGuest() // Side effect but only relevant for tests - return new BskyAgent({service: PUBLIC_BSKY_SERVICE}) + return new BskyAppAgent({service: PUBLIC_BSKY_SERVICE}) } export async function createAgentAndResume( @@ -39,9 +32,8 @@ export async function createAgentAndResume( did: string, event: AtpSessionEvent, ) => void, - setPersistSessionHandler: SetPersistSessionHandler, ) { - const agent = new BskyAgent({service: storedAccount.service}) + const agent = new BskyAppAgent({service: storedAccount.service}) if (storedAccount.pdsUrl) { agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl) } @@ -67,13 +59,7 @@ export async function createAgentAndResume( } } - return prepareAgent( - agent, - gates, - moderation, - onSessionChange, - setPersistSessionHandler, - ) + return agent.prepare(gates, moderation, onSessionChange) } export async function createAgentAndLogin( @@ -93,21 +79,14 @@ export async function createAgentAndLogin( did: string, event: AtpSessionEvent, ) => void, - setPersistSessionHandler: SetPersistSessionHandler, ) { - const agent = new BskyAgent({service}) + const agent = new BskyAppAgent({service}) await agent.login({identifier, password, authFactorToken}) const account = agentToSessionAccountOrThrow(agent) const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const moderation = configureModerationForAccount(agent, account) - return prepareAgent( - agent, - moderation, - gates, - onSessionChange, - setPersistSessionHandler, - ) + return agent.prepare(gates, moderation, onSessionChange) } export async function createAgentAndCreateAccount( @@ -135,9 +114,8 @@ export async function createAgentAndCreateAccount( did: string, event: AtpSessionEvent, ) => void, - setPersistSessionHandler: SetPersistSessionHandler, ) { - const agent = new BskyAgent({service}) + const agent = new BskyAppAgent({service}) await agent.createAccount({ email, password, @@ -195,39 +173,7 @@ export async function createAgentAndCreateAccount( logger.error(e, {context: `session: failed snoozeEmailConfirmationPrompt`}) } - return prepareAgent( - agent, - gates, - moderation, - onSessionChange, - setPersistSessionHandler, - ) -} - -async function prepareAgent( - agent: BskyAgent, - // Not awaited in the calling code so we can delay blocking on them. - gates: Promise, - moderation: Promise, - onSessionChange: ( - agent: BskyAgent, - did: string, - event: AtpSessionEvent, - ) => void, - setPersistSessionHandler: (cb: AtpPersistSessionHandler) => void, -) { - // There's nothing else left to do, so block on them here. - await Promise.all([gates, moderation]) - - // Now the agent is ready. - const account = agentToSessionAccountOrThrow(agent) - setPersistSessionHandler(event => { - onSessionChange(agent, account.did, event) - if (event !== 'create' && event !== 'update') { - addSessionErrorLog(account.did, event) - } - }) - return {agent, account} + return agent.prepare(gates, moderation, onSessionChange) } export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount { @@ -279,3 +225,51 @@ export function sessionAccountToSession( status: account.status, } } + +// Not exported. Use factories above to create it. +class BskyAppAgent extends BskyAgent { + persistSessionHandler: ((event: AtpSessionEvent) => void) | undefined = + undefined + + constructor({service}: {service: string}) { + super({ + service, + persistSession: (event: AtpSessionEvent) => { + if (this.persistSessionHandler) { + this.persistSessionHandler(event) + } + }, + }) + } + + async prepare( + // Not awaited in the calling code so we can delay blocking on them. + gates: Promise, + moderation: Promise, + onSessionChange: ( + agent: BskyAgent, + did: string, + event: AtpSessionEvent, + ) => void, + ) { + // There's nothing else left to do, so block on them here. + await Promise.all([gates, moderation]) + + // Now the agent is ready. + const account = agentToSessionAccountOrThrow(this) + this.persistSessionHandler = event => { + onSessionChange(this, account.did, event) + if (event !== 'create' && event !== 'update') { + addSessionErrorLog(account.did, event) + } + } + return {account, agent: this} + } + + dispose() { + this.sessionManager.session = undefined + this.persistSessionHandler = undefined + } +} + +export type {BskyAppAgent} diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index a47404b8a30..9495d6b7733 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -1,9 +1,5 @@ import React from 'react' -import { - AtpPersistSessionHandler, - AtpSessionEvent, - BskyAgent, -} from '@atproto/api' +import {AtpSessionEvent, BskyAgent} from '@atproto/api' import {track} from '#/lib/analytics/analytics' import {logEvent} from '#/lib/statsig/statsig' @@ -15,6 +11,7 @@ import {IS_DEV} from '#/env' import {emitSessionDropped} from '../events' import { agentToSessionAccount, + BskyAppAgent, createAgentAndCreateAccount, createAgentAndLogin, createAgentAndResume, @@ -51,15 +48,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return initialState }) - const persistSessionHandler = React.useRef< - AtpPersistSessionHandler | undefined - >(undefined) - const setPersistSessionHandler = ( - newHandler: AtpPersistSessionHandler | undefined, - ) => { - persistSessionHandler.current = newHandler - } - const onAgentSessionChange = React.useCallback( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away. @@ -86,7 +74,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const {agent, account} = await createAgentAndCreateAccount( params, onAgentSessionChange, - setPersistSessionHandler, ) if (signal.aborted) { @@ -111,7 +98,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const {agent, account} = await createAgentAndLogin( params, onAgentSessionChange, - setPersistSessionHandler, ) if (signal.aborted) { @@ -153,7 +139,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const {agent, account} = await createAgentAndResume( storedAccount, onAgentSessionChange, - setPersistSessionHandler, ) if (signal.aborted) { @@ -255,7 +240,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // @ts-ignore if (IS_DEV && isWeb) window.agent = state.currentAgentState.agent - const agent = state.currentAgentState.agent as BskyAgent + const agent = state.currentAgentState.agent as BskyAppAgent const currentAgentRef = React.useRef(agent) React.useEffect(() => { if (currentAgentRef.current !== agent) { @@ -265,8 +250,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { addSessionDebugLog({type: 'agent:switch', prevAgent, nextAgent: agent}) // We never reuse agents so let's fully neutralize the previous one. // This ensures it won't try to consume any refresh tokens. - prevAgent.sessionManager.session = undefined - setPersistSessionHandler(undefined) + prevAgent.dispose() } }, [agent]) From 96cfe89c7b46eaefaddf2786da9fbdacd3c2eed5 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 14 Aug 2024 10:34:37 -0700 Subject: [PATCH 4/7] Remove .withProxy() calls (#4929) (cherry picked from commit 7e11b862e931b5351bd3463d984ab11ee9b46522) --- src/components/ReportDialog/SubmitView.tsx | 35 +++++--------- .../moderation/LabelsOnMeDialog.tsx | 43 ++++++----------- src/lib/statsig/gates.ts | 1 - src/state/feed-feedback.tsx | 46 ++++++------------- 4 files changed, 40 insertions(+), 85 deletions(-) diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index 7ceece75b67..2def0fa4b4d 100644 --- a/src/components/ReportDialog/SubmitView.tsx +++ b/src/components/ReportDialog/SubmitView.tsx @@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react' import {getLabelingServiceTitle} from '#/lib/moderation' import {ReportOption} from '#/lib/moderation/useReportOptions' -import {useGate} from '#/lib/statsig/statsig' import {useAgent} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import * as Toast from '#/view/com/util/Toast' @@ -37,7 +36,6 @@ export function SubmitView({ const t = useTheme() const {_} = useLingui() const agent = useAgent() - const gate = useGate() const [details, setDetails] = React.useState('') const [submitting, setSubmitting] = React.useState(false) const [selectedServices, setSelectedServices] = React.useState([ @@ -63,27 +61,17 @@ export function SubmitView({ } const results = await Promise.all( selectedServices.map(did => { - if (gate('session_withproxy_fix')) { - return agent - .createModerationReport(report, { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${did}#atproto_labeler`, - }, - }) - .then( - _ => true, - _ => false, - ) - } else { - return agent - .withProxy('atproto_labeler', did) - .createModerationReport(report) - .then( - _ => true, - _ => false, - ) - } + return agent + .createModerationReport(report, { + encoding: 'application/json', + headers: { + 'atproto-proxy': `${did}#atproto_labeler`, + }, + }) + .then( + _ => true, + _ => false, + ) }), ) @@ -108,7 +96,6 @@ export function SubmitView({ onSubmitComplete, setError, agent, - gate, ]) return ( diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index e581d22c1bf..9ac76545e8d 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -7,7 +7,6 @@ import {useMutation} from '@tanstack/react-query' import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {makeProfileLink} from '#/lib/routes/links' -import {useGate} from '#/lib/statsig/statsig' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {useAgent, useSession} from '#/state/session' @@ -202,42 +201,28 @@ function AppealForm({ const [details, setDetails] = React.useState('') const isAccountReport = 'did' in subject const agent = useAgent() - const gate = useGate() const {mutate, isPending} = useMutation({ mutationFn: async () => { const $type = !isAccountReport ? 'com.atproto.repo.strongRef' : 'com.atproto.admin.defs#repoRef' - if (gate('session_withproxy_fix')) { - await agent.createModerationReport( - { - reasonType: ComAtprotoModerationDefs.REASONAPPEAL, - subject: { - $type, - ...subject, - }, - reason: details, + await agent.createModerationReport( + { + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, + subject: { + $type, + ...subject, }, - { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${label.src}#atproto_labeler`, - }, + reason: details, + }, + { + encoding: 'application/json', + headers: { + 'atproto-proxy': `${label.src}#atproto_labeler`, }, - ) - } else { - await agent - .withProxy('atproto_labeler', label.src) - .createModerationReport({ - reasonType: ComAtprotoModerationDefs.REASONAPPEAL, - subject: { - $type, - ...subject, - }, - reason: details, - }) - } + }, + ) }, onError: err => { logger.error('Failed to submit label appeal', {message: err}) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 58a60232bec..52c720bc8e5 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -7,7 +7,6 @@ export type Gate = | 'new_user_progress_guide' | 'onboarding_minimum_interests' | 'request_notifications_permission_after_onboarding_v2' - | 'session_withproxy_fix' | 'show_avi_follow_button' | 'show_follow_back_label_v2' | 'suggested_feeds_interstitial' diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 59b4bf78a46..fc7917ae93f 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -1,11 +1,10 @@ import React from 'react' import {AppState, AppStateStatus} from 'react-native' -import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' +import {AppBskyFeedDefs} from '@atproto/api' import throttle from 'lodash.throttle' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' -import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed' import {getFeedPostSlice} from '#/view/com/posts/Feed' @@ -25,7 +24,6 @@ const stateContext = React.createContext({ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { const agent = useAgent() - const gate = useGate() const enabled = isDiscoverFeed(feed) && hasSession const queue = React.useRef>(new Set()) const history = React.useRef< @@ -49,34 +47,20 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { queue.current.clear() // Send to the feed - if (gate('session_withproxy_fix')) { - agent.app.bsky.feed - .sendInteractions( - {interactions}, - { - encoding: 'application/json', - headers: { - // TODO when we start sending to other feeds, we need to grab their DID -prf - 'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg', - }, + agent.app.bsky.feed + .sendInteractions( + {interactions}, + { + encoding: 'application/json', + headers: { + // TODO when we start sending to other feeds, we need to grab their DID -prf + 'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg', }, - ) - .catch((e: any) => { - logger.warn('Failed to send feed interactions', {error: e}) - }) - } else { - const proxyAgent = agent.withProxy( - // @ts-ignore TODO need to update withProxy() to support this key -prf - 'bsky_fg', - // TODO when we start sending to other feeds, we need to grab their DID -prf - 'did:web:discover.bsky.app', - ) as BskyAgent - proxyAgent.app.bsky.feed - .sendInteractions({interactions}) - .catch((e: any) => { - logger.warn('Failed to send feed interactions', {error: e}) - }) - } + }, + ) + .catch((e: any) => { + logger.warn('Failed to send feed interactions', {error: e}) + }) // Send to Statsig if (aggregatedStats.current === null) { @@ -84,7 +68,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { } sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) throttledFlushAggregatedStats() - }, [agent, gate, throttledFlushAggregatedStats]) + }, [agent, throttledFlushAggregatedStats]) const sendToFeed = React.useMemo( () => From abfa4bb54bfe1e781f94febd4deb942dc7c8e2e9 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 13 Aug 2024 18:51:49 +0100 Subject: [PATCH 5/7] Don't kick to login screen on network error (#4911) * Don't kick the user on network errors * Track online status for RQ * Use health endpoint * Update test with new behavior * Only poll while offline * Handle races between the check and network events * Reduce the poll kickoff interval * Don't cache partially fetched pinned feeds This isn't a new issue but it's more prominent with the offline handling. We're currently silently caching pinned infos that failed to fetch. This avoids showing a big spinner on failure but it also kills all feeds which is very confusing. If the request to get feed gens fails, let's fail the whole query. Then it can be retried. (cherry picked from commit 57be2ea15b5bea019abf95a590640d688b7a8633) --- src/lib/react-query.tsx | 67 ++++++++++++++++++++- src/state/events.ts | 16 +++++ src/state/queries/feed.ts | 3 +- src/state/session/__tests__/session-test.ts | 10 ++- src/state/session/agent.ts | 27 +++++++++ src/state/session/reducer.ts | 8 +-- 6 files changed, 117 insertions(+), 14 deletions(-) diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index be507216aa2..5abfccd7f6b 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -2,18 +2,83 @@ import React, {useRef, useState} from 'react' import {AppState, AppStateStatus} from 'react-native' import AsyncStorage from '@react-native-async-storage/async-storage' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' -import {focusManager, QueryClient} from '@tanstack/react-query' +import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query' import { PersistQueryClientProvider, PersistQueryClientProviderProps, } from '@tanstack/react-query-persist-client' import {isNative} from '#/platform/detection' +import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' // any query keys in this array will be persisted to AsyncStorage export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info' const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot] +async function checkIsOnline(): Promise { + try { + const controller = new AbortController() + setTimeout(() => { + controller.abort() + }, 15e3) + const res = await fetch('https://public.api.bsky.app/xrpc/_health', { + cache: 'no-store', + signal: controller.signal, + }) + const json = await res.json() + if (json.version) { + return true + } else { + return false + } + } catch (e) { + return false + } +} + +let receivedNetworkLost = false +let receivedNetworkConfirmed = false +let isNetworkStateUnclear = false + +listenNetworkLost(() => { + receivedNetworkLost = true + onlineManager.setOnline(false) +}) + +listenNetworkConfirmed(() => { + receivedNetworkConfirmed = true + onlineManager.setOnline(true) +}) + +let checkPromise: Promise | undefined +function checkIsOnlineIfNeeded() { + if (checkPromise) { + return + } + receivedNetworkLost = false + receivedNetworkConfirmed = false + checkPromise = checkIsOnline().then(nextIsOnline => { + checkPromise = undefined + if (nextIsOnline && receivedNetworkLost) { + isNetworkStateUnclear = true + } + if (!nextIsOnline && receivedNetworkConfirmed) { + isNetworkStateUnclear = true + } + if (!isNetworkStateUnclear) { + onlineManager.setOnline(nextIsOnline) + } + }) +} + +setInterval(() => { + if (AppState.currentState === 'active') { + if (!onlineManager.isOnline() || isNetworkStateUnclear) { + checkIsOnlineIfNeeded() + } + } +}, 2000) + focusManager.setEventListener(onFocus => { if (isNative) { const subscription = AppState.addEventListener( diff --git a/src/state/events.ts b/src/state/events.ts index 1384abdedad..dcd36464ec1 100644 --- a/src/state/events.ts +++ b/src/state/events.ts @@ -22,6 +22,22 @@ export function listenSessionDropped(fn: () => void): UnlistenFn { return () => emitter.off('session-dropped', fn) } +export function emitNetworkConfirmed() { + emitter.emit('network-confirmed') +} +export function listenNetworkConfirmed(fn: () => void): UnlistenFn { + emitter.on('network-confirmed', fn) + return () => emitter.off('network-confirmed', fn) +} + +export function emitNetworkLost() { + emitter.emit('network-lost') +} +export function listenNetworkLost(fn: () => void): UnlistenFn { + emitter.on('network-lost', fn) + return () => emitter.off('network-lost', fn) +} + export function emitPostCreated() { emitter.emit('post-created') } diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 36555c18135..45a14387b9b 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -441,7 +441,8 @@ export function usePinnedFeedsInfos() { }), ) - await Promise.allSettled([feedsPromise, ...listsPromises]) + await feedsPromise // Fail the whole query if it fails. + await Promise.allSettled(listsPromises) // Ignore individual failing ones. // order the feeds/lists in the order they were pinned const result: SavedFeedSourceInfo[] = [] diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index 731b66b0e95..cb4c6a35bbd 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -1184,7 +1184,7 @@ describe('session', () => { expect(state.currentAgentState.did).toBe('bob-did') }) - it('does soft logout on network error', () => { + it('ignores network errors', () => { let state = getInitialState([]) const agent1 = new BskyAgent({service: 'https://alice.com'}) @@ -1217,11 +1217,9 @@ describe('session', () => { }, ]) expect(state.accounts.length).toBe(1) - // Network error should reset current user but not reset the tokens. - // TODO: We might want to remove or change this behavior? expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentAgentState.did).toBe('alice-did') expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -1242,9 +1240,9 @@ describe('session', () => { ], "currentAgentState": { "agent": { - "service": "https://public.api.bsky.app/", + "service": "https://alice.com/", }, - "did": undefined, + "did": "alice-did", }, "needsPersist": true, } diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index ea6af677cf5..8a48cf95e56 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -12,6 +12,7 @@ import {tryFetchGates} from '#/lib/statsig/statsig' import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' +import {emitNetworkConfirmed, emitNetworkLost} from '../events' import {addSessionErrorLog} from './logging' import { configureModerationForAccount, @@ -227,6 +228,7 @@ export function sessionAccountToSession( } // Not exported. Use factories above to create it. +let realFetch = globalThis.fetch class BskyAppAgent extends BskyAgent { persistSessionHandler: ((event: AtpSessionEvent) => void) | undefined = undefined @@ -234,6 +236,23 @@ class BskyAppAgent extends BskyAgent { constructor({service}: {service: string}) { super({ service, + async fetch(...args) { + let success = false + try { + const result = await realFetch(...args) + success = true + return result + } catch (e) { + success = false + throw e + } finally { + if (success) { + emitNetworkConfirmed() + } else { + emitNetworkLost() + } + } + }, persistSession: (event: AtpSessionEvent) => { if (this.persistSessionHandler) { this.persistSessionHandler(event) @@ -257,7 +276,15 @@ class BskyAppAgent extends BskyAgent { // Now the agent is ready. const account = agentToSessionAccountOrThrow(this) + let lastSession = this.sessionManager.session this.persistSessionHandler = event => { + if (this.sessionManager.session) { + lastSession = this.sessionManager.session + } else if (event === 'network-error') { + // Put it back, we'll try again later. + this.sessionManager.session = lastSession + } + onSessionChange(this, account.did, event) if (event !== 'create' && event !== 'update') { addSessionErrorLog(account.did, event) diff --git a/src/state/session/reducer.ts b/src/state/session/reducer.ts index 0a537b42c6e..b49198514c7 100644 --- a/src/state/session/reducer.ts +++ b/src/state/session/reducer.ts @@ -79,12 +79,8 @@ let reducer = (state: State, action: Action): State => { return state } if (sessionEvent === 'network-error') { - // Don't change stored accounts but kick to the choose account screen. - return { - accounts: state.accounts, - currentAgentState: createPublicAgentState(), - needsPersist: true, - } + // Assume it's transient. + return state } const existingAccount = state.accounts.find(a => a.did === accountDid) if ( From cfb0be8b4bfbc22134bab3371019b1fd90d34b95 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 1 Aug 2024 08:29:27 -0700 Subject: [PATCH 6/7] Update muted words dialog with `expiresAt` and `actorTarget` (#4801) * WIP not working dropdown * Update MutedWords dialog * Add i18n formatDistance * Comments * Handle text wrapping * Update label copy Co-authored-by: Hailey * Fix alignment * Improve translation output * Revert toggle changes * Better types for useFormatDistance * Tweaks * Integrate new sdk version into TagMenu * Use ampersand Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Bump SDK --------- Co-authored-by: Hailey Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> (cherry picked from commit b0e130a4d85f2056bddcbf210aa7ea4068d41686) --- src/components/TagMenu/index.tsx | 56 ++-- src/components/TagMenu/index.web.tsx | 36 ++- src/components/dialogs/MutedWords.tsx | 373 +++++++++++++++++++------ src/components/hooks/dates.ts | 69 +++++ src/state/queries/preferences/index.ts | 15 + 5 files changed, 427 insertions(+), 122 deletions(-) create mode 100644 src/components/hooks/dates.ts diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx index 0ed70366714..2c6a0b674c1 100644 --- a/src/components/TagMenu/index.tsx +++ b/src/components/TagMenu/index.tsx @@ -1,27 +1,27 @@ import React from 'react' import {View} from 'react-native' -import {useNavigation} from '@react-navigation/native' -import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' -import {atoms as a, native, useTheme} from '#/alf' -import * as Dialog from '#/components/Dialog' -import {Text} from '#/components/Typography' -import {Button, ButtonText} from '#/components/Button' -import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' -import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' -import {Divider} from '#/components/Divider' -import {Link} from '#/components/Link' import {makeSearchLink} from '#/lib/routes/links' import {NavigationProp} from '#/lib/routes/types' +import {isInvalidHandle} from '#/lib/strings/handles' import { usePreferencesQuery, + useRemoveMutedWordsMutation, useUpsertMutedWordsMutation, - useRemoveMutedWordMutation, } from '#/state/queries/preferences' +import {atoms as a, native, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' +import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' +import {Link} from '#/components/Link' import {Loader} from '#/components/Loader' -import {isInvalidHandle} from '#/lib/strings/handles' +import {Text} from '#/components/Typography' export function useTagMenuControl() { return Dialog.useDialogControl() @@ -52,10 +52,10 @@ export function TagMenu({ reset: resetUpsert, } = useUpsertMutedWordsMutation() const { - mutateAsync: removeMutedWord, + mutateAsync: removeMutedWords, variables: optimisticRemove, reset: resetRemove, - } = useRemoveMutedWordMutation() + } = useRemoveMutedWordsMutation() const displayTag = '#' + tag const isMuted = Boolean( @@ -65,9 +65,20 @@ export function TagMenu({ optimisticUpsert?.find( m => m.value === tag && m.targets.includes('tag'), )) && - !(optimisticRemove?.value === tag), + !optimisticRemove?.find(m => m?.value === tag), ) + /* + * Mute word records that exactly match the tag in question. + */ + const removeableMuteWords = React.useMemo(() => { + return ( + preferences?.moderationPrefs.mutedWords?.filter(word => { + return word.value === tag + }) || [] + ) + }, [tag, preferences?.moderationPrefs?.mutedWords]) + return ( <> {children} @@ -212,13 +223,16 @@ export function TagMenu({ control.close(() => { if (isMuted) { resetUpsert() - removeMutedWord({ - value: tag, - targets: ['tag'], - }) + removeMutedWords(removeableMuteWords) } else { resetRemove() - upsertMutedWord([{value: tag, targets: ['tag']}]) + upsertMutedWord([ + { + value: tag, + targets: ['tag'], + actorTarget: 'all', + }, + ]) } }) }}> diff --git a/src/components/TagMenu/index.web.tsx b/src/components/TagMenu/index.web.tsx index 4336223861c..b6c306439ae 100644 --- a/src/components/TagMenu/index.web.tsx +++ b/src/components/TagMenu/index.web.tsx @@ -3,16 +3,16 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {isInvalidHandle} from '#/lib/strings/handles' -import {EventStopper} from '#/view/com/util/EventStopper' -import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown' import {NavigationProp} from '#/lib/routes/types' +import {isInvalidHandle} from '#/lib/strings/handles' +import {enforceLen} from '#/lib/strings/helpers' import { usePreferencesQuery, + useRemoveMutedWordsMutation, useUpsertMutedWordsMutation, - useRemoveMutedWordMutation, } from '#/state/queries/preferences' -import {enforceLen} from '#/lib/strings/helpers' +import {EventStopper} from '#/view/com/util/EventStopper' +import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown' import {web} from '#/alf' import * as Dialog from '#/components/Dialog' @@ -47,8 +47,8 @@ export function TagMenu({ const {data: preferences} = usePreferencesQuery() const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} = useUpsertMutedWordsMutation() - const {mutateAsync: removeMutedWord, variables: optimisticRemove} = - useRemoveMutedWordMutation() + const {mutateAsync: removeMutedWords, variables: optimisticRemove} = + useRemoveMutedWordsMutation() const isMuted = Boolean( (preferences?.moderationPrefs.mutedWords?.find( m => m.value === tag && m.targets.includes('tag'), @@ -56,10 +56,21 @@ export function TagMenu({ optimisticUpsert?.find( m => m.value === tag && m.targets.includes('tag'), )) && - !(optimisticRemove?.value === tag), + !optimisticRemove?.find(m => m?.value === tag), ) const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle') + /* + * Mute word records that exactly match the tag in question. + */ + const removeableMuteWords = React.useMemo(() => { + return ( + preferences?.moderationPrefs.mutedWords?.filter(word => { + return word.value === tag + }) || [] + ) + }, [tag, preferences?.moderationPrefs?.mutedWords]) + const dropdownItems = React.useMemo(() => { return [ { @@ -105,9 +116,11 @@ export function TagMenu({ : _(msg`Mute ${truncatedTag}`), onPress() { if (isMuted) { - removeMutedWord({value: tag, targets: ['tag']}) + removeMutedWords(removeableMuteWords) } else { - upsertMutedWord([{value: tag, targets: ['tag']}]) + upsertMutedWord([ + {value: tag, targets: ['tag'], actorTarget: 'all'}, + ]) } }, testID: 'tagMenuMute', @@ -129,7 +142,8 @@ export function TagMenu({ tag, truncatedTag, upsertMutedWord, - removeMutedWord, + removeMutedWords, + removeableMuteWords, ]) return ( diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 526652be955..38273aad540 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Keyboard, View} from 'react-native' +import {View} from 'react-native' import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -24,6 +24,7 @@ import * as Dialog from '#/components/Dialog' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {Divider} from '#/components/Divider' import * as Toggle from '#/components/forms/Toggle' +import {useFormatDistance} from '#/components/hooks/dates' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' @@ -32,6 +33,8 @@ import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' +const ONE_DAY = 24 * 60 * 60 * 1000 + export function MutedWordsDialog() { const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() return ( @@ -53,16 +56,32 @@ function MutedWordsInner() { } = usePreferencesQuery() const {isPending, mutateAsync: addMutedWord} = useUpsertMutedWordsMutation() const [field, setField] = React.useState('') - const [options, setOptions] = React.useState(['content']) + const [targets, setTargets] = React.useState(['content']) const [error, setError] = React.useState('') + const [durations, setDurations] = React.useState(['forever']) + const [excludeFollowing, setExcludeFollowing] = React.useState(false) const submit = React.useCallback(async () => { const sanitizedValue = sanitizeMutedWordValue(field) - const targets = ['tag', options.includes('content') && 'content'].filter( + const surfaces = ['tag', targets.includes('content') && 'content'].filter( Boolean, ) as AppBskyActorDefs.MutedWord['targets'] + const actorTarget = excludeFollowing ? 'exclude-following' : 'all' + + const now = Date.now() + const rawDuration = durations.at(0) + // undefined evaluates to 'forever' + let duration: string | undefined + + if (rawDuration === '24_hours') { + duration = new Date(now + ONE_DAY).toISOString() + } else if (rawDuration === '7_days') { + duration = new Date(now + 7 * ONE_DAY).toISOString() + } else if (rawDuration === '30_days') { + duration = new Date(now + 30 * ONE_DAY).toISOString() + } - if (!sanitizedValue || !targets.length) { + if (!sanitizedValue || !surfaces.length) { setField('') setError(_(msg`Please enter a valid word, tag, or phrase to mute`)) return @@ -70,28 +89,37 @@ function MutedWordsInner() { try { // send raw value and rely on SDK as sanitization source of truth - await addMutedWord([{value: field, targets}]) + await addMutedWord([ + { + value: field, + targets: surfaces, + actorTarget, + expiresAt: duration, + }, + ]) setField('') } catch (e: any) { logger.error(`Failed to save muted word`, {message: e.message}) setError(e.message) } - }, [_, field, options, addMutedWord, setField]) + }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) return ( - + Add muted words and tags - Posts can be muted based on their text, their tags, or both. + Posts can be muted based on their text, their tags, or both. We + recommend avoiding common words that appear in many posts, since it + can result in no posts being shown. - + + + + values={durations} + onChange={setDurations}> + + Duration: + + + + + + + + + Forever + + + + + + + + + + + 24 hours + + + + + + + + + + + + + 7 days + + + + + + + + + + + 30 days + + + + + + + + + + + Mute in: + + + + style={[a.flex_1]}> - + - - Mute in text & tags + + Text & tags @@ -140,34 +273,64 @@ function MutedWordsInner() { + style={[a.flex_1]}> - + - - Mute in tags only + + Tags only - - + + + Options: + + + + + + + Exclude users you follow + + + + + + + + + + {error && ( )} - - - - We recommend avoiding common words that appear in many posts, - since it can result in no posts being shown. - - @@ -268,6 +417,9 @@ function MutedWordRow({ const {_} = useLingui() const {isPending, mutateAsync: removeMutedWord} = useRemoveMutedWordMutation() const control = Prompt.usePromptControl() + const expiryDate = word.expiresAt ? new Date(word.expiresAt) : undefined + const isExpired = expiryDate && expiryDate < new Date() + const formatDistance = useFormatDistance() const remove = React.useCallback(async () => { control.close() @@ -280,7 +432,7 @@ function MutedWordRow({ control={control} title={_(msg`Are you sure?`)} description={_( - msg`This will delete ${word.value} from your muted words. You can always add it back later.`, + msg`This will delete "${word.value}" from your muted words. You can always add it back later.`, )} onConfirm={remove} confirmButtonCta={_(msg`Remove`)} @@ -289,53 +441,94 @@ function MutedWordRow({ - - {word.value} - + + + + {word.targets.find(t => t === 'content') ? ( + + {word.value}{' '} + + in{' '} + + text & tags + + + + ) : ( + + {word.value}{' '} + + in{' '} + + tags + + + + )} + + - - {word.targets.map(target => ( - + {(expiryDate || word.actorTarget === 'exclude-following') && ( + - {target === 'content' ? _(msg`text`) : _(msg`tag`)} + style={[ + a.flex_1, + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ]}> + {expiryDate && ( + <> + {isExpired ? ( + Expired + ) : ( + + Expires{' '} + {formatDistance(expiryDate, new Date(), { + addSuffix: true, + })} + + )} + + )} + {word.actorTarget === 'exclude-following' && ( + <> + {' • '} + Excludes users you follow + + )} - ))} - - + )} + + ) diff --git a/src/components/hooks/dates.ts b/src/components/hooks/dates.ts new file mode 100644 index 00000000000..b0f94133b74 --- /dev/null +++ b/src/components/hooks/dates.ts @@ -0,0 +1,69 @@ +/** + * Hooks for date-fns localized formatters. + * + * Our app supports some languages that are not included in date-fns by + * default, in which case it will fall back to English. + * + * {@link https://github.com/date-fns/date-fns/blob/main/docs/i18n.md} + */ + +import React from 'react' +import {formatDistance, Locale} from 'date-fns' +import { + ca, + de, + es, + fi, + fr, + hi, + id, + it, + ja, + ko, + ptBR, + tr, + uk, + zhCN, + zhTW, +} from 'date-fns/locale' + +import {AppLanguage} from '#/locale/languages' +import {useLanguagePrefs} from '#/state/preferences' + +/** + * {@link AppLanguage} + */ +const locales: Record = { + en: undefined, + ca, + de, + es, + fi, + fr, + ga: undefined, + hi, + id, + it, + ja, + ko, + ['pt-BR']: ptBR, + tr, + uk, + ['zh-CN']: zhCN, + ['zh-TW']: zhTW, +} + +/** + * Returns a localized `formatDistance` function. + * {@link formatDistance} + */ +export function useFormatDistance() { + const {appLanguage} = useLanguagePrefs() + return React.useCallback( + (date, baseDate, options) => { + const locale = locales[appLanguage as AppLanguage] + return formatDistance(date, baseDate, {...options, locale: locale}) + }, + [appLanguage], + ) +} diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index a264abfe5d2..ab866d5e2a5 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -343,6 +343,21 @@ export function useRemoveMutedWordMutation() { }) } +export function useRemoveMutedWordsMutation() { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => { + await agent.removeMutedWords(mutedWords) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} + export function useQueueNudgesMutation() { const queryClient = useQueryClient() const agent = useAgent() From 64dd81dfe49c8f39a8f7d3681959a84d3de10d01 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 14 Aug 2024 12:06:31 -0700 Subject: [PATCH 7/7] revert some mute word ui changes --- src/components/dialogs/MutedWords.tsx | 138 +------------------------- 1 file changed, 4 insertions(+), 134 deletions(-) diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 38273aad540..0c85bc6f637 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -48,7 +48,6 @@ export function MutedWordsDialog() { function MutedWordsInner() { const t = useTheme() const {_} = useLingui() - const {gtMobile} = useBreakpoints() const { isLoading: isPreferencesLoading, data: preferences, @@ -58,8 +57,8 @@ function MutedWordsInner() { const [field, setField] = React.useState('') const [targets, setTargets] = React.useState(['content']) const [error, setError] = React.useState('') - const [durations, setDurations] = React.useState(['forever']) - const [excludeFollowing, setExcludeFollowing] = React.useState(false) + const [durations] = React.useState(['forever']) + const [excludeFollowing] = React.useState(false) const submit = React.useCallback(async () => { const sanitizedValue = sanitizeMutedWordValue(field) @@ -113,13 +112,11 @@ function MutedWordsInner() { - Posts can be muted based on their text, their tags, or both. We - recommend avoiding common words that appear in many posts, since it - can result in no posts being shown. + Posts can be muted based on their text, their tags, or both. - + - - - Duration: - - - - - - - - - - Forever - - - - - - - - - - - 24 hours - - - - - - - - - - - - - 7 days - - - - - - - - - - - 30 days - - - - - - - - - - - Options: - - - - - - - Exclude users you follow - - - - - -