···1import 'react-native-gesture-handler' // must be first
002import {LogBox} from 'react-native'
3-4-import '#/platform/polyfills'
5-import {IS_TEST} from '#/env'
6import {registerRootComponent} from 'expo'
7-import {doPolyfill} from '#/lib/api/api-polyfill'
89import App from '#/App'
10-11-doPolyfill()
1213if (IS_TEST) {
14 LogBox.ignoreAllLogs() // suppress all logs in tests
···1import 'react-native-gesture-handler' // must be first
2+import '#/platform/polyfills'
3+4import {LogBox} from 'react-native'
0005import {registerRootComponent} from 'expo'
067import App from '#/App'
8+import {IS_TEST} from '#/env'
0910if (IS_TEST) {
11 LogBox.ignoreAllLogs() // suppress all logs in tests
+2-3
index.web.js
···1import '#/platform/markBundleStartTime'
2-3import '#/platform/polyfills'
04import {registerRootComponent} from 'expo'
5-import {doPolyfill} from '#/lib/api/api-polyfill'
6import App from '#/App'
78-doPolyfill()
9registerRootComponent(App)
···1import '#/platform/markBundleStartTime'
02import '#/platform/polyfills'
3+4import {registerRootComponent} from 'expo'
5+6import App from '#/App'
708registerRootComponent(App)
···1+import RNFS from 'react-native-fs'
2+import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api'
3+4+/**
5+ * @param encoding Allows overriding the blob's type
6+ */
7+export async function uploadBlob(
8+ agent: BskyAgent,
9+ input: string | Blob,
10+ encoding?: string,
11+): Promise<ComAtprotoRepoUploadBlob.Response> {
12+ if (typeof input === 'string' && input.startsWith('file:')) {
13+ const blob = await asBlob(input)
14+ return agent.uploadBlob(blob, {encoding})
15+ }
16+17+ if (typeof input === 'string' && input.startsWith('/')) {
18+ const blob = await asBlob(`file://${input}`)
19+ return agent.uploadBlob(blob, {encoding})
20+ }
21+22+ if (typeof input === 'string' && input.startsWith('data:')) {
23+ const blob = await fetch(input).then(r => r.blob())
24+ return agent.uploadBlob(blob, {encoding})
25+ }
26+27+ if (input instanceof Blob) {
28+ return agent.uploadBlob(input, {encoding})
29+ }
30+31+ throw new TypeError(`Invalid uploadBlob input: ${typeof input}`)
32+}
33+34+async function asBlob(uri: string): Promise<Blob> {
35+ return withSafeFile(uri, async safeUri => {
36+ // Note
37+ // Android does not support `fetch()` on `file://` URIs. for this reason, we
38+ // use XMLHttpRequest instead of simply calling:
39+40+ // return fetch(safeUri.replace('file:///', 'file:/')).then(r => r.blob())
41+42+ return await new Promise((resolve, reject) => {
43+ const xhr = new XMLHttpRequest()
44+ xhr.onload = () => resolve(xhr.response)
45+ xhr.onerror = () => reject(new Error('Failed to load blob'))
46+ xhr.responseType = 'blob'
47+ xhr.open('GET', safeUri, true)
48+ xhr.send(null)
49+ })
50+ })
51+}
52+53+// HACK
54+// React native has a bug that inflates the size of jpegs on upload
55+// we get around that by renaming the file ext to .bin
56+// see https://github.com/facebook/react-native/issues/27099
57+// -prf
58+async function withSafeFile<T>(
59+ uri: string,
60+ fn: (path: string) => Promise<T>,
61+): Promise<T> {
62+ if (uri.endsWith('.jpeg') || uri.endsWith('.jpg')) {
63+ // Since we don't "own" the file, we should avoid renaming or modifying it.
64+ // Instead, let's copy it to a temporary file and use that (then remove the
65+ // temporary file).
66+ const newPath = uri.replace(/\.jpe?g$/, '.bin')
67+ try {
68+ await RNFS.copyFile(uri, newPath)
69+ } catch {
70+ // Failed to copy the file, just use the original
71+ return await fn(uri)
72+ }
73+ try {
74+ return await fn(newPath)
75+ } finally {
76+ // Remove the temporary file
77+ await RNFS.unlink(newPath)
78+ }
79+ } else {
80+ return fn(uri)
81+ }
82+}
+26
src/lib/api/upload-blob.web.ts
···00000000000000000000000000
···1+import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api'
2+3+/**
4+ * @note It is recommended, on web, to use the `file` instance of the file
5+ * selector input element, rather than a `data:` URL, to avoid
6+ * loading the file into memory. `File` extends `Blob` "file" instances can
7+ * be passed directly to this function.
8+ */
9+export async function uploadBlob(
10+ agent: BskyAgent,
11+ input: string | Blob,
12+ encoding?: string,
13+): Promise<ComAtprotoRepoUploadBlob.Response> {
14+ if (typeof input === 'string' && input.startsWith('data:')) {
15+ const blob = await fetch(input).then(r => r.blob())
16+ return agent.uploadBlob(blob, {encoding})
17+ }
18+19+ if (input instanceof Blob) {
20+ return agent.uploadBlob(input, {
21+ encoding,
22+ })
23+ }
24+25+ throw new TypeError(`Invalid uploadBlob input: ${typeof input}`)
26+}
+1-7
src/lib/media/manip.ts
···218 // Normalize is necessary for Android, otherwise it doesn't delete.
219 const normalizedPath = normalizePath(path)
220 try {
221- await Promise.allSettled([
222- deleteAsync(normalizedPath, {idempotent: true}),
223- // HACK: Try this one too. Might exist due to api-polyfill hack.
224- deleteAsync(normalizedPath.replace(/\.jpe?g$/, '.bin'), {
225- idempotent: true,
226- }),
227- ])
228 } catch (e) {
229 console.error('Failed to delete file', e)
230 }
···218 // Normalize is necessary for Android, otherwise it doesn't delete.
219 const normalizedPath = normalizePath(path)
220 try {
221+ await deleteAsync(normalizedPath, {idempotent: true})
000000222 } catch (e) {
223 console.error('Failed to delete file', e)
224 }
+1-1
src/screens/SignupQueued.tsx
···40 const res = await agent.com.atproto.temp.checkSignupQueue()
41 if (res.data.activated) {
42 // ready to go, exchange the access token for a usable one and kick off onboarding
43- await agent.refreshSession()
44 if (!isSignupQueued(agent.session?.accessJwt)) {
45 onboardingDispatch({type: 'start'})
46 }
···40 const res = await agent.com.atproto.temp.checkSignupQueue()
41 if (res.data.activated) {
42 // ready to go, exchange the access token for a usable one and kick off onboarding
43+ await agent.sessionManager.refreshSession()
44 if (!isSignupQueued(agent.session?.accessJwt)) {
45 onboardingDispatch({type: 'start'})
46 }
+2-2
src/state/queries/preferences/index.ts
···37 refetchOnWindowFocus: true,
38 queryKey: preferencesQueryKey,
39 queryFn: async () => {
40- if (agent.session?.did === undefined) {
41 return DEFAULT_LOGGED_OUT_PREFERENCES
42 } else {
43 const res = await agent.getPreferences()
4445 // save to local storage to ensure there are labels on initial requests
46 saveLabelers(
47- agent.session.did,
48 res.moderationPrefs.labelers.map(l => l.did),
49 )
50
···37 refetchOnWindowFocus: true,
38 queryKey: preferencesQueryKey,
39 queryFn: async () => {
40+ if (!agent.did) {
41 return DEFAULT_LOGGED_OUT_PREFERENCES
42 } else {
43 const res = await agent.getPreferences()
4445 // save to local storage to ensure there are labels on initial requests
46 saveLabelers(
47+ agent.did,
48 res.moderationPrefs.labelers.map(l => l.did),
49 )
50