···11import 'react-native-gesture-handler' // must be first
22+import '#/platform/polyfills'
33+24import {LogBox} from 'react-native'
33-44-import '#/platform/polyfills'
55-import {IS_TEST} from '#/env'
65import {registerRootComponent} from 'expo'
77-import {doPolyfill} from '#/lib/api/api-polyfill'
8697import App from '#/App'
1010-1111-doPolyfill()
88+import {IS_TEST} from '#/env'
1291310if (IS_TEST) {
1411 LogBox.ignoreAllLogs() // suppress all logs in tests
+2-3
index.web.js
···11import '#/platform/markBundleStartTime'
22-32import '#/platform/polyfills'
33+44import {registerRootComponent} from 'expo'
55-import {doPolyfill} from '#/lib/api/api-polyfill'
55+66import App from '#/App'
7788-doPolyfill()
98registerRootComponent(App)
···11-import RNFS from 'react-native-fs'
22-import {BskyAgent, jsonToLex, stringifyLex} from '@atproto/api'
33-44-const GET_TIMEOUT = 15e3 // 15s
55-const POST_TIMEOUT = 60e3 // 60s
66-77-export function doPolyfill() {
88- BskyAgent.configure({fetch: fetchHandler})
99-}
1010-1111-interface FetchHandlerResponse {
1212- status: number
1313- headers: Record<string, string>
1414- body: any
1515-}
1616-1717-async function fetchHandler(
1818- reqUri: string,
1919- reqMethod: string,
2020- reqHeaders: Record<string, string>,
2121- reqBody: any,
2222-): Promise<FetchHandlerResponse> {
2323- const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
2424- if (reqMimeType && reqMimeType.startsWith('application/json')) {
2525- reqBody = stringifyLex(reqBody)
2626- } else if (
2727- typeof reqBody === 'string' &&
2828- (reqBody.startsWith('/') || reqBody.startsWith('file:'))
2929- ) {
3030- if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) {
3131- // HACK
3232- // React native has a bug that inflates the size of jpegs on upload
3333- // we get around that by renaming the file ext to .bin
3434- // see https://github.com/facebook/react-native/issues/27099
3535- // -prf
3636- const newPath = reqBody.replace(/\.jpe?g$/, '.bin')
3737- await RNFS.moveFile(reqBody, newPath)
3838- reqBody = newPath
3939- }
4040- // NOTE
4141- // React native treats bodies with {uri: string} as file uploads to pull from cache
4242- // -prf
4343- reqBody = {uri: reqBody}
4444- }
4545-4646- const controller = new AbortController()
4747- const to = setTimeout(
4848- () => controller.abort(),
4949- reqMethod === 'post' ? POST_TIMEOUT : GET_TIMEOUT,
5050- )
5151-5252- const res = await fetch(reqUri, {
5353- method: reqMethod,
5454- headers: reqHeaders,
5555- body: reqBody,
5656- signal: controller.signal,
5757- })
5858-5959- const resStatus = res.status
6060- const resHeaders: Record<string, string> = {}
6161- res.headers.forEach((value: string, key: string) => {
6262- resHeaders[key] = value
6363- })
6464- const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type']
6565- let resBody
6666- if (resMimeType) {
6767- if (resMimeType.startsWith('application/json')) {
6868- resBody = jsonToLex(await res.json())
6969- } else if (resMimeType.startsWith('text/')) {
7070- resBody = await res.text()
7171- } else if (resMimeType === 'application/vnd.ipld.car') {
7272- resBody = await res.arrayBuffer()
7373- } else {
7474- throw new Error('Non-supported mime type')
7575- }
7676- }
7777-7878- clearTimeout(to)
7979-8080- return {
8181- status: resStatus,
8282- headers: resHeaders,
8383- body: resBody,
8484- }
8585-}
-3
src/lib/api/api-polyfill.web.ts
···11-export function doPolyfill() {
22- // no polyfill is needed on web
33-}
+11-14
src/lib/api/feed/custom.ts
···11import {
22 AppBskyFeedDefs,
33 AppBskyFeedGetFeed as GetCustomFeed,
44- AtpAgent,
54 BskyAgent,
65} from '@atproto/api'
76···5150 const agent = this.agent
5251 const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed)
53525454- const res = agent.session
5353+ const res = agent.did
5554 ? await this.agent.app.bsky.feed.getFeed(
5655 {
5756 ...this.params,
···106105 let contentLangs = getContentLanguages().join(',')
107106108107 // manually construct fetch call so we can add the `lang` cache-busting param
109109- let res = await AtpAgent.fetch!(
108108+ let res = await fetch(
110109 `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
111110 cursor ? `&cursor=${cursor}` : ''
112111 }&limit=${limit}&lang=${contentLangs}`,
113113- 'GET',
114114- {'Accept-Language': contentLangs},
115115- undefined,
112112+ {method: 'GET', headers: {'Accept-Language': contentLangs}},
116113 )
117117- if (res.body?.feed?.length) {
114114+ let data = res.ok ? await res.json() : null
115115+ if (data?.feed?.length) {
118116 return {
119117 success: true,
120120- data: res.body,
118118+ data,
121119 }
122120 }
123121124122 // no data, try again with language headers removed
125125- res = await AtpAgent.fetch!(
123123+ res = await fetch(
126124 `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
127125 cursor ? `&cursor=${cursor}` : ''
128126 }&limit=${limit}`,
129129- 'GET',
130130- {'Accept-Language': ''},
131131- undefined,
127127+ {method: 'GET', headers: {'Accept-Language': ''}},
132128 )
133133- if (res.body?.feed?.length) {
129129+ data = res.ok ? await res.json() : null
130130+ if (data?.feed?.length) {
134131 return {
135132 success: true,
136136- data: res.body,
133133+ data,
137134 }
138135 }
139136
+5-34
src/lib/api/index.ts
···66 AppBskyFeedThreadgate,
77 BskyAgent,
88 ComAtprotoLabelDefs,
99- ComAtprotoRepoUploadBlob,
109 RichText,
1110} from '@atproto/api'
1211import {AtUri} from '@atproto/api'
···1514import {ThreadgateSetting} from '#/state/queries/threadgate'
1615import {isNetworkError} from 'lib/strings/errors'
1716import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
1818-import {isNative, isWeb} from 'platform/detection'
1717+import {isNative} from 'platform/detection'
1918import {ImageModel} from 'state/models/media/image'
2019import {LinkMeta} from '../link-meta/link-meta'
2120import {safeDeleteAsync} from '../media/manip'
2121+import {uploadBlob} from './upload-blob'
2222+2323+export {uploadBlob}
22242325export interface ExternalEmbedDraft {
2426 uri: string
···2628 meta?: LinkMeta
2729 embed?: AppBskyEmbedRecord.Main
2830 localThumb?: ImageModel
2929-}
3030-3131-export async function uploadBlob(
3232- agent: BskyAgent,
3333- blob: string,
3434- encoding: string,
3535-): Promise<ComAtprotoRepoUploadBlob.Response> {
3636- if (isWeb) {
3737- // `blob` should be a data uri
3838- return agent.uploadBlob(convertDataURIToUint8Array(blob), {
3939- encoding,
4040- })
4141- } else {
4242- // `blob` should be a path to a file in the local FS
4343- return agent.uploadBlob(
4444- blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
4545- {encoding},
4646- )
4747- }
4831}
49325033interface PostOpts {
···301284302285 const postUrip = new AtUri(postUri)
303286 await agent.api.com.atproto.repo.putRecord({
304304- repo: agent.session!.did,
287287+ repo: agent.accountDid,
305288 collection: 'app.bsky.feed.threadgate',
306289 rkey: postUrip.rkey,
307290 record: {
···312295 },
313296 })
314297}
315315-316316-// helpers
317317-// =
318318-319319-function convertDataURIToUint8Array(uri: string): Uint8Array {
320320- var raw = window.atob(uri.substring(uri.indexOf(';base64,') + 8))
321321- var binary = new Uint8Array(new ArrayBuffer(raw.length))
322322- for (let i = 0; i < raw.length; i++) {
323323- binary[i] = raw.charCodeAt(i)
324324- }
325325- return binary
326326-}
+82
src/lib/api/upload-blob.ts
···11+import RNFS from 'react-native-fs'
22+import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api'
33+44+/**
55+ * @param encoding Allows overriding the blob's type
66+ */
77+export async function uploadBlob(
88+ agent: BskyAgent,
99+ input: string | Blob,
1010+ encoding?: string,
1111+): Promise<ComAtprotoRepoUploadBlob.Response> {
1212+ if (typeof input === 'string' && input.startsWith('file:')) {
1313+ const blob = await asBlob(input)
1414+ return agent.uploadBlob(blob, {encoding})
1515+ }
1616+1717+ if (typeof input === 'string' && input.startsWith('/')) {
1818+ const blob = await asBlob(`file://${input}`)
1919+ return agent.uploadBlob(blob, {encoding})
2020+ }
2121+2222+ if (typeof input === 'string' && input.startsWith('data:')) {
2323+ const blob = await fetch(input).then(r => r.blob())
2424+ return agent.uploadBlob(blob, {encoding})
2525+ }
2626+2727+ if (input instanceof Blob) {
2828+ return agent.uploadBlob(input, {encoding})
2929+ }
3030+3131+ throw new TypeError(`Invalid uploadBlob input: ${typeof input}`)
3232+}
3333+3434+async function asBlob(uri: string): Promise<Blob> {
3535+ return withSafeFile(uri, async safeUri => {
3636+ // Note
3737+ // Android does not support `fetch()` on `file://` URIs. for this reason, we
3838+ // use XMLHttpRequest instead of simply calling:
3939+4040+ // return fetch(safeUri.replace('file:///', 'file:/')).then(r => r.blob())
4141+4242+ return await new Promise((resolve, reject) => {
4343+ const xhr = new XMLHttpRequest()
4444+ xhr.onload = () => resolve(xhr.response)
4545+ xhr.onerror = () => reject(new Error('Failed to load blob'))
4646+ xhr.responseType = 'blob'
4747+ xhr.open('GET', safeUri, true)
4848+ xhr.send(null)
4949+ })
5050+ })
5151+}
5252+5353+// HACK
5454+// React native has a bug that inflates the size of jpegs on upload
5555+// we get around that by renaming the file ext to .bin
5656+// see https://github.com/facebook/react-native/issues/27099
5757+// -prf
5858+async function withSafeFile<T>(
5959+ uri: string,
6060+ fn: (path: string) => Promise<T>,
6161+): Promise<T> {
6262+ if (uri.endsWith('.jpeg') || uri.endsWith('.jpg')) {
6363+ // Since we don't "own" the file, we should avoid renaming or modifying it.
6464+ // Instead, let's copy it to a temporary file and use that (then remove the
6565+ // temporary file).
6666+ const newPath = uri.replace(/\.jpe?g$/, '.bin')
6767+ try {
6868+ await RNFS.copyFile(uri, newPath)
6969+ } catch {
7070+ // Failed to copy the file, just use the original
7171+ return await fn(uri)
7272+ }
7373+ try {
7474+ return await fn(newPath)
7575+ } finally {
7676+ // Remove the temporary file
7777+ await RNFS.unlink(newPath)
7878+ }
7979+ } else {
8080+ return fn(uri)
8181+ }
8282+}
+26
src/lib/api/upload-blob.web.ts
···11+import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api'
22+33+/**
44+ * @note It is recommended, on web, to use the `file` instance of the file
55+ * selector input element, rather than a `data:` URL, to avoid
66+ * loading the file into memory. `File` extends `Blob` "file" instances can
77+ * be passed directly to this function.
88+ */
99+export async function uploadBlob(
1010+ agent: BskyAgent,
1111+ input: string | Blob,
1212+ encoding?: string,
1313+): Promise<ComAtprotoRepoUploadBlob.Response> {
1414+ if (typeof input === 'string' && input.startsWith('data:')) {
1515+ const blob = await fetch(input).then(r => r.blob())
1616+ return agent.uploadBlob(blob, {encoding})
1717+ }
1818+1919+ if (input instanceof Blob) {
2020+ return agent.uploadBlob(input, {
2121+ encoding,
2222+ })
2323+ }
2424+2525+ throw new TypeError(`Invalid uploadBlob input: ${typeof input}`)
2626+}
+1-7
src/lib/media/manip.ts
···218218 // Normalize is necessary for Android, otherwise it doesn't delete.
219219 const normalizedPath = normalizePath(path)
220220 try {
221221- await Promise.allSettled([
222222- deleteAsync(normalizedPath, {idempotent: true}),
223223- // HACK: Try this one too. Might exist due to api-polyfill hack.
224224- deleteAsync(normalizedPath.replace(/\.jpe?g$/, '.bin'), {
225225- idempotent: true,
226226- }),
227227- ])
221221+ await deleteAsync(normalizedPath, {idempotent: true})
228222 } catch (e) {
229223 console.error('Failed to delete file', e)
230224 }
+1-1
src/screens/SignupQueued.tsx
···4040 const res = await agent.com.atproto.temp.checkSignupQueue()
4141 if (res.data.activated) {
4242 // ready to go, exchange the access token for a usable one and kick off onboarding
4343- await agent.refreshSession()
4343+ await agent.sessionManager.refreshSession()
4444 if (!isSignupQueued(agent.session?.accessJwt)) {
4545 onboardingDispatch({type: 'start'})
4646 }
+2-2
src/state/queries/preferences/index.ts
···3737 refetchOnWindowFocus: true,
3838 queryKey: preferencesQueryKey,
3939 queryFn: async () => {
4040- if (agent.session?.did === undefined) {
4040+ if (!agent.did) {
4141 return DEFAULT_LOGGED_OUT_PREFERENCES
4242 } else {
4343 const res = await agent.getPreferences()
44444545 // save to local storage to ensure there are labels on initial requests
4646 saveLabelers(
4747- agent.session.did,
4747+ agent.did,
4848 res.moderationPrefs.labelers.map(l => l.did),
4949 )
5050