···11+import {Share} from 'react-native'
22+33+import * as Toast from '../view/com/util/Toast'
44+55+export interface DownloadAndResizeOpts {
66+ uri: string
77+ width: number
88+ height: number
99+ mode: 'contain' | 'cover' | 'stretch'
1010+ maxSize: number
1111+ timeout: number
1212+}
1313+1414+export interface Image {
1515+ path: string
1616+ mime: string
1717+ size: number
1818+ width: number
1919+ height: number
2020+}
2121+2222+export async function downloadAndResize(_opts: DownloadAndResizeOpts) {
2323+ // TODO
2424+ throw new Error('TODO')
2525+}
2626+2727+export interface ResizeOpts {
2828+ width: number
2929+ height: number
3030+ mode: 'contain' | 'cover' | 'stretch'
3131+ maxSize: number
3232+}
3333+3434+export async function resize(
3535+ _localUri: string,
3636+ _opts: ResizeOpts,
3737+): Promise<Image> {
3838+ // TODO
3939+ throw new Error('TODO')
4040+}
4141+4242+export async function compressIfNeeded(
4343+ _img: Image,
4444+ _maxSize: number,
4545+): Promise<Image> {
4646+ // TODO
4747+ throw new Error('TODO')
4848+}
4949+5050+export interface Dim {
5151+ width: number
5252+ height: number
5353+}
5454+export function scaleDownDimensions(dim: Dim, max: Dim): Dim {
5555+ if (dim.width < max.width && dim.height < max.height) {
5656+ return dim
5757+ }
5858+ let wScale = dim.width > max.width ? max.width / dim.width : 1
5959+ let hScale = dim.height > max.height ? max.height / dim.height : 1
6060+ if (wScale < hScale) {
6161+ return {width: dim.width * wScale, height: dim.height * wScale}
6262+ }
6363+ return {width: dim.width * hScale, height: dim.height * hScale}
6464+}
6565+6666+export const saveImageModal = async (_opts: {uri: string}) => {
6767+ // TODO
6868+ throw new Error('TODO')
6969+}
+2-2
src/state/index.ts
···22import {Platform} from 'react-native'
33import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
44import {RootStoreModel} from './models/root-store'
55-import * as libapi from './lib/api'
55+import * as apiPolyfill from './lib/api-polyfill'
66import * as storage from './lib/storage'
7788export const LOCAL_DEV_SERVICE =
···1717 let rootStore: RootStoreModel
1818 let data: any
19192020- libapi.doPolyfill()
2020+ apiPolyfill.doPolyfill()
21212222 const api = AtpApi.service(serviceUri) as SessionServiceClient
2323 rootStore = new RootStoreModel(api)
+76
src/state/lib/api-polyfill.ts
···11+import {sessionClient as AtpApi} from '@atproto/api'
22+33+export function doPolyfill() {
44+ AtpApi.xrpc.fetch = fetchHandler
55+}
66+77+interface FetchHandlerResponse {
88+ status: number
99+ headers: Record<string, string>
1010+ body: ArrayBuffer | undefined
1111+}
1212+1313+async function fetchHandler(
1414+ reqUri: string,
1515+ reqMethod: string,
1616+ reqHeaders: Record<string, string>,
1717+ reqBody: any,
1818+): Promise<FetchHandlerResponse> {
1919+ const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
2020+ if (reqMimeType && reqMimeType.startsWith('application/json')) {
2121+ reqBody = JSON.stringify(reqBody)
2222+ } else if (
2323+ typeof reqBody === 'string' &&
2424+ (reqBody.startsWith('/') || reqBody.startsWith('file:'))
2525+ ) {
2626+ if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) {
2727+ // HACK
2828+ // React native has a bug that inflates the size of jpegs on upload
2929+ // we get around that by renaming the file ext to .bin
3030+ // see https://github.com/facebook/react-native/issues/27099
3131+ // -prf
3232+ const newPath = reqBody.replace(/\.jpe?g$/, '.bin')
3333+ await RNFS.moveFile(reqBody, newPath)
3434+ reqBody = newPath
3535+ }
3636+ // NOTE
3737+ // React native treats bodies with {uri: string} as file uploads to pull from cache
3838+ // -prf
3939+ reqBody = {uri: reqBody}
4040+ }
4141+4242+ const controller = new AbortController()
4343+ const to = setTimeout(() => controller.abort(), TIMEOUT)
4444+4545+ const res = await fetch(reqUri, {
4646+ method: reqMethod,
4747+ headers: reqHeaders,
4848+ body: reqBody,
4949+ signal: controller.signal,
5050+ })
5151+5252+ const resStatus = res.status
5353+ const resHeaders: Record<string, string> = {}
5454+ res.headers.forEach((value: string, key: string) => {
5555+ resHeaders[key] = value
5656+ })
5757+ const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type']
5858+ let resBody
5959+ if (resMimeType) {
6060+ if (resMimeType.startsWith('application/json')) {
6161+ resBody = await res.json()
6262+ } else if (resMimeType.startsWith('text/')) {
6363+ resBody = await res.text()
6464+ } else {
6565+ throw new Error('TODO: non-textual response body')
6666+ }
6767+ }
6868+6969+ clearTimeout(to)
7070+7171+ return {
7272+ status: resStatus,
7373+ headers: resHeaders,
7474+ body: resBody,
7575+ }
7676+}
+4
src/state/lib/api-polyfill.web.ts
···11+export function doPolyfill() {
22+ // TODO needed? native fetch may work fine -prf
33+ // AtpApi.xrpc.fetch = fetchHandler
44+}
-75
src/state/lib/api.ts
···19192020const TIMEOUT = 10e3 // 10s
21212222-export function doPolyfill() {
2323- AtpApi.xrpc.fetch = fetchHandler
2424-}
2525-2622export interface ExternalEmbedDraft {
2723 uri: string
2824 isLoading: boolean
···199195 rkey: followUrip.rkey,
200196 })
201197}
202202-203203-interface FetchHandlerResponse {
204204- status: number
205205- headers: Record<string, string>
206206- body: ArrayBuffer | undefined
207207-}
208208-209209-async function fetchHandler(
210210- reqUri: string,
211211- reqMethod: string,
212212- reqHeaders: Record<string, string>,
213213- reqBody: any,
214214-): Promise<FetchHandlerResponse> {
215215- const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
216216- if (reqMimeType && reqMimeType.startsWith('application/json')) {
217217- reqBody = JSON.stringify(reqBody)
218218- } else if (
219219- typeof reqBody === 'string' &&
220220- (reqBody.startsWith('/') || reqBody.startsWith('file:'))
221221- ) {
222222- if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) {
223223- // HACK
224224- // React native has a bug that inflates the size of jpegs on upload
225225- // we get around that by renaming the file ext to .bin
226226- // see https://github.com/facebook/react-native/issues/27099
227227- // -prf
228228- const newPath = reqBody.replace(/\.jpe?g$/, '.bin')
229229- await RNFS.moveFile(reqBody, newPath)
230230- reqBody = newPath
231231- }
232232- // NOTE
233233- // React native treats bodies with {uri: string} as file uploads to pull from cache
234234- // -prf
235235- reqBody = {uri: reqBody}
236236- }
237237-238238- const controller = new AbortController()
239239- const to = setTimeout(() => controller.abort(), TIMEOUT)
240240-241241- const res = await fetch(reqUri, {
242242- method: reqMethod,
243243- headers: reqHeaders,
244244- body: reqBody,
245245- signal: controller.signal,
246246- })
247247-248248- const resStatus = res.status
249249- const resHeaders: Record<string, string> = {}
250250- res.headers.forEach((value: string, key: string) => {
251251- resHeaders[key] = value
252252- })
253253- const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type']
254254- let resBody
255255- if (resMimeType) {
256256- if (resMimeType.startsWith('application/json')) {
257257- resBody = await res.json()
258258- } else if (resMimeType.startsWith('text/')) {
259259- resBody = await res.text()
260260- } else {
261261- throw new Error('TODO: non-textual response body')
262262- }
263263- }
264264-265265- clearTimeout(to)
266266-267267- return {
268268- status: resStatus,
269269- headers: resHeaders,
270270- body: resBody,
271271- }
272272-}
···11import {makeAutoObservable, runInAction} from 'mobx'
22-import {Image as PickedImage} from 'react-native-image-crop-picker'
22+import {Image as PickedImage} from '../../view/com/util/images/ImageCropPicker'
33import {
44 AppBskyActorGetProfile as GetProfile,
55 AppBskyActorProfile as Profile,
+4-5
src/state/models/root-store.ts
···66import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
77import {createContext, useContext} from 'react'
88import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
99-import BackgroundFetch from 'react-native-background-fetch'
99+import * as BgScheduler from '../lib/bg-scheduler'
1010import {isObj, hasProp} from '../lib/type-guards'
1111import {LogModel} from './log'
1212import {SessionModel} from './session'
···124124 // background fetch runs every 15 minutes *at most* and will get slowed down
125125 // based on some heuristics run by iOS, meaning it is not a reliable form of delivery
126126 // -prf
127127- BackgroundFetch.configure(
128128- {minimumFetchInterval: 15},
127127+ BgScheduler.configure(
129128 this.onBgFetch.bind(this),
130129 this.onBgFetchTimeout.bind(this),
131130 ).then(status => {
···138137 if (this.session.hasSession) {
139138 await this.me.bgFetchNotifications()
140139 }
141141- BackgroundFetch.finish(taskId)
140140+ BgScheduler.finish(taskId)
142141 }
143142144143 onBgFetchTimeout(taskId: string) {
145144 this.log.debug(`Background fetch timed out for task ${taskId}`)
146146- BackgroundFetch.finish(taskId)
145145+ BgScheduler.finish(taskId)
147146 }
148147}
149148
+1-1
src/view/com/composer/PhotoCarouselPicker.tsx
···88 openPicker,
99 openCamera,
1010 openCropper,
1111-} from 'react-native-image-crop-picker'
1111+} from '../util/images/ImageCropPicker'
1212import {
1313 UserLocalPhotosModel,
1414 PhotoIdentifier,
+1-1
src/view/com/modals/EditProfile.tsx
···88} from 'react-native'
99import LinearGradient from 'react-native-linear-gradient'
1010import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
1111-import {Image as PickedImage} from 'react-native-image-crop-picker'
1111+import {Image as PickedImage} from '../util/images/ImageCropPicker'
1212import {Text} from '../util/text/Text'
1313import {ErrorMessage} from '../util/error/ErrorMessage'
1414import {useStores} from '../../../state'
+1-1
src/view/com/util/UserAvatar.tsx
···77 openCropper,
88 openPicker,
99 Image as PickedImage,
1010-} from 'react-native-image-crop-picker'
1010+} from './images/ImageCropPicker'
1111import {colors, gradients} from '../../lib/styles'
12121313export function UserAvatar({
+2-6
src/view/com/util/UserBanner.tsx
···22import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
33import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
44import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
55-import {Image as PickedImage} from 'react-native-image-crop-picker'
55+import {Image as PickedImage} from './images/ImageCropPicker'
66import {colors, gradients} from '../../lib/styles'
77-import {
88- openCamera,
99- openCropper,
1010- openPicker,
1111-} from 'react-native-image-crop-picker'
77+import {openCamera, openCropper, openPicker} from './images/ImageCropPicker'
128139export function UserBanner({
1410 banner,
+6
src/view/com/util/images/ImageCropPicker.tsx
···11+export {
22+ openPicker,
33+ openCamera,
44+ openCropper,
55+} from 'react-native-image-crop-picker'
66+export type {Image} from 'react-native-image-crop-picker'
+32
src/view/com/util/images/ImageCropPicker.web.tsx
···11+import type {
22+ Image,
33+ Video,
44+ ImageOrVideo,
55+ Options,
66+ PossibleArray,
77+} from 'react-native-image-crop-picker'
88+99+export type {Image} from 'react-native-image-crop-picker'
1010+1111+type MediaType<O> = O extends {mediaType: 'photo'}
1212+ ? Image
1313+ : O extends {mediaType: 'video'}
1414+ ? Video
1515+ : ImageOrVideo
1616+1717+export async function openPicker<O extends Options>(
1818+ _options: O,
1919+): Promise<PossibleArray<O, MediaType<O>>> {
2020+ // TODO
2121+ throw new Error('TODO')
2222+}
2323+export async function openCamera<O extends Options>(
2424+ _options: O,
2525+): Promise<PossibleArray<O, MediaType<O>>> {
2626+ // TODO
2727+ throw new Error('TODO')
2828+}
2929+export async function openCropper(_options: Options): Promise<Image> {
3030+ // TODO
3131+ throw new Error('TODO')
3232+}