Add web polyfills
This commit is contained in:
parent
d6ec627c8c
commit
751dfb20fd
18 changed files with 240 additions and 105 deletions
69
src/lib/images.web.ts
Normal file
69
src/lib/images.web.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import {Share} from 'react-native'
|
||||
|
||||
import * as Toast from '../view/com/util/Toast'
|
||||
|
||||
export interface DownloadAndResizeOpts {
|
||||
uri: string
|
||||
width: number
|
||||
height: number
|
||||
mode: 'contain' | 'cover' | 'stretch'
|
||||
maxSize: number
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
path: string
|
||||
mime: string
|
||||
size: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export async function downloadAndResize(_opts: DownloadAndResizeOpts) {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
||||
|
||||
export interface ResizeOpts {
|
||||
width: number
|
||||
height: number
|
||||
mode: 'contain' | 'cover' | 'stretch'
|
||||
maxSize: number
|
||||
}
|
||||
|
||||
export async function resize(
|
||||
_localUri: string,
|
||||
_opts: ResizeOpts,
|
||||
): Promise<Image> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
||||
|
||||
export async function compressIfNeeded(
|
||||
_img: Image,
|
||||
_maxSize: number,
|
||||
): Promise<Image> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
||||
|
||||
export interface Dim {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
export function scaleDownDimensions(dim: Dim, max: Dim): Dim {
|
||||
if (dim.width < max.width && dim.height < max.height) {
|
||||
return dim
|
||||
}
|
||||
let wScale = dim.width > max.width ? max.width / dim.width : 1
|
||||
let hScale = dim.height > max.height ? max.height / dim.height : 1
|
||||
if (wScale < hScale) {
|
||||
return {width: dim.width * wScale, height: dim.height * wScale}
|
||||
}
|
||||
return {width: dim.width * hScale, height: dim.height * hScale}
|
||||
}
|
||||
|
||||
export const saveImageModal = async (_opts: {uri: string}) => {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
|
@ -2,7 +2,7 @@ import {autorun} from 'mobx'
|
|||
import {Platform} from 'react-native'
|
||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import {RootStoreModel} from './models/root-store'
|
||||
import * as libapi from './lib/api'
|
||||
import * as apiPolyfill from './lib/api-polyfill'
|
||||
import * as storage from './lib/storage'
|
||||
|
||||
export const LOCAL_DEV_SERVICE =
|
||||
|
@ -17,7 +17,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
|
|||
let rootStore: RootStoreModel
|
||||
let data: any
|
||||
|
||||
libapi.doPolyfill()
|
||||
apiPolyfill.doPolyfill()
|
||||
|
||||
const api = AtpApi.service(serviceUri) as SessionServiceClient
|
||||
rootStore = new RootStoreModel(api)
|
||||
|
|
76
src/state/lib/api-polyfill.ts
Normal file
76
src/state/lib/api-polyfill.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {sessionClient as AtpApi} from '@atproto/api'
|
||||
|
||||
export function doPolyfill() {
|
||||
AtpApi.xrpc.fetch = fetchHandler
|
||||
}
|
||||
|
||||
interface FetchHandlerResponse {
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body: ArrayBuffer | undefined
|
||||
}
|
||||
|
||||
async function fetchHandler(
|
||||
reqUri: string,
|
||||
reqMethod: string,
|
||||
reqHeaders: Record<string, string>,
|
||||
reqBody: any,
|
||||
): Promise<FetchHandlerResponse> {
|
||||
const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
|
||||
if (reqMimeType && reqMimeType.startsWith('application/json')) {
|
||||
reqBody = JSON.stringify(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(), TIMEOUT)
|
||||
|
||||
const res = await fetch(reqUri, {
|
||||
method: reqMethod,
|
||||
headers: reqHeaders,
|
||||
body: reqBody,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
const resStatus = res.status
|
||||
const resHeaders: Record<string, string> = {}
|
||||
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 = await res.json()
|
||||
} else if (resMimeType.startsWith('text/')) {
|
||||
resBody = await res.text()
|
||||
} else {
|
||||
throw new Error('TODO: non-textual response body')
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(to)
|
||||
|
||||
return {
|
||||
status: resStatus,
|
||||
headers: resHeaders,
|
||||
body: resBody,
|
||||
}
|
||||
}
|
4
src/state/lib/api-polyfill.web.ts
Normal file
4
src/state/lib/api-polyfill.web.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export function doPolyfill() {
|
||||
// TODO needed? native fetch may work fine -prf
|
||||
// AtpApi.xrpc.fetch = fetchHandler
|
||||
}
|
|
@ -19,10 +19,6 @@ import {Image} from '../../lib/images'
|
|||
|
||||
const TIMEOUT = 10e3 // 10s
|
||||
|
||||
export function doPolyfill() {
|
||||
AtpApi.xrpc.fetch = fetchHandler
|
||||
}
|
||||
|
||||
export interface ExternalEmbedDraft {
|
||||
uri: string
|
||||
isLoading: boolean
|
||||
|
@ -199,74 +195,3 @@ export async function unfollow(store: RootStoreModel, followUri: string) {
|
|||
rkey: followUrip.rkey,
|
||||
})
|
||||
}
|
||||
|
||||
interface FetchHandlerResponse {
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body: ArrayBuffer | undefined
|
||||
}
|
||||
|
||||
async function fetchHandler(
|
||||
reqUri: string,
|
||||
reqMethod: string,
|
||||
reqHeaders: Record<string, string>,
|
||||
reqBody: any,
|
||||
): Promise<FetchHandlerResponse> {
|
||||
const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
|
||||
if (reqMimeType && reqMimeType.startsWith('application/json')) {
|
||||
reqBody = JSON.stringify(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(), TIMEOUT)
|
||||
|
||||
const res = await fetch(reqUri, {
|
||||
method: reqMethod,
|
||||
headers: reqHeaders,
|
||||
body: reqBody,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
const resStatus = res.status
|
||||
const resHeaders: Record<string, string> = {}
|
||||
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 = await res.json()
|
||||
} else if (resMimeType.startsWith('text/')) {
|
||||
resBody = await res.text()
|
||||
} else {
|
||||
throw new Error('TODO: non-textual response body')
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(to)
|
||||
|
||||
return {
|
||||
status: resStatus,
|
||||
headers: resHeaders,
|
||||
body: resBody,
|
||||
}
|
||||
}
|
||||
|
|
18
src/state/lib/bg-scheduler.ts
Normal file
18
src/state/lib/bg-scheduler.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import BackgroundFetch, {
|
||||
BackgroundFetchStatus,
|
||||
} from 'react-native-background-fetch'
|
||||
|
||||
export function configure(
|
||||
handler: (taskId: string) => Promise<void>,
|
||||
timeoutHandler: (taskId: string) => Promise<void>,
|
||||
): Promise<BackgroundFetchStatus> {
|
||||
return BackgroundFetch.configure(
|
||||
{minimumFetchInterval: 15},
|
||||
handler,
|
||||
timeoutHandler,
|
||||
)
|
||||
}
|
||||
|
||||
export function finish(taskId: string) {
|
||||
return BackgroundFetch.finish(taskId)
|
||||
}
|
13
src/state/lib/bg-scheduler.web.ts
Normal file
13
src/state/lib/bg-scheduler.web.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
type BackgroundFetchStatus = 0 | 1 | 2
|
||||
|
||||
export async function configure(
|
||||
_handler: (taskId: string) => Promise<void>,
|
||||
_timeoutHandler: (taskId: string) => Promise<void>,
|
||||
): Promise<BackgroundFetchStatus> {
|
||||
// TODO
|
||||
return 0
|
||||
}
|
||||
|
||||
export function finish(_taskId: string) {
|
||||
// TODO
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {Image as PickedImage} from 'react-native-image-crop-picker'
|
||||
import {Image as PickedImage} from '../../view/com/util/images/ImageCropPicker'
|
||||
import {
|
||||
AppBskyActorGetProfile as GetProfile,
|
||||
AppBskyActorProfile as Profile,
|
||||
|
|
|
@ -6,7 +6,7 @@ import {makeAutoObservable} from 'mobx'
|
|||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import {createContext, useContext} from 'react'
|
||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
||||
import BackgroundFetch from 'react-native-background-fetch'
|
||||
import * as BgScheduler from '../lib/bg-scheduler'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {LogModel} from './log'
|
||||
import {SessionModel} from './session'
|
||||
|
@ -124,8 +124,7 @@ export class RootStoreModel {
|
|||
// background fetch runs every 15 minutes *at most* and will get slowed down
|
||||
// based on some heuristics run by iOS, meaning it is not a reliable form of delivery
|
||||
// -prf
|
||||
BackgroundFetch.configure(
|
||||
{minimumFetchInterval: 15},
|
||||
BgScheduler.configure(
|
||||
this.onBgFetch.bind(this),
|
||||
this.onBgFetchTimeout.bind(this),
|
||||
).then(status => {
|
||||
|
@ -138,12 +137,12 @@ export class RootStoreModel {
|
|||
if (this.session.hasSession) {
|
||||
await this.me.bgFetchNotifications()
|
||||
}
|
||||
BackgroundFetch.finish(taskId)
|
||||
BgScheduler.finish(taskId)
|
||||
}
|
||||
|
||||
onBgFetchTimeout(taskId: string) {
|
||||
this.log.debug(`Background fetch timed out for task ${taskId}`)
|
||||
BackgroundFetch.finish(taskId)
|
||||
BgScheduler.finish(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
openPicker,
|
||||
openCamera,
|
||||
openCropper,
|
||||
} from 'react-native-image-crop-picker'
|
||||
} from '../util/images/ImageCropPicker'
|
||||
import {
|
||||
UserLocalPhotosModel,
|
||||
PhotoIdentifier,
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
|
||||
import {Image as PickedImage} from 'react-native-image-crop-picker'
|
||||
import {Image as PickedImage} from '../util/images/ImageCropPicker'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {useStores} from '../../../state'
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
openCropper,
|
||||
openPicker,
|
||||
Image as PickedImage,
|
||||
} from 'react-native-image-crop-picker'
|
||||
} from './images/ImageCropPicker'
|
||||
import {colors, gradients} from '../../lib/styles'
|
||||
|
||||
export function UserAvatar({
|
||||
|
|
|
@ -2,13 +2,9 @@ import React, {useCallback} from 'react'
|
|||
import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
|
||||
import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Image as PickedImage} from 'react-native-image-crop-picker'
|
||||
import {Image as PickedImage} from './images/ImageCropPicker'
|
||||
import {colors, gradients} from '../../lib/styles'
|
||||
import {
|
||||
openCamera,
|
||||
openCropper,
|
||||
openPicker,
|
||||
} from 'react-native-image-crop-picker'
|
||||
import {openCamera, openCropper, openPicker} from './images/ImageCropPicker'
|
||||
|
||||
export function UserBanner({
|
||||
banner,
|
||||
|
|
6
src/view/com/util/images/ImageCropPicker.tsx
Normal file
6
src/view/com/util/images/ImageCropPicker.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
openPicker,
|
||||
openCamera,
|
||||
openCropper,
|
||||
} from 'react-native-image-crop-picker'
|
||||
export type {Image} from 'react-native-image-crop-picker'
|
32
src/view/com/util/images/ImageCropPicker.web.tsx
Normal file
32
src/view/com/util/images/ImageCropPicker.web.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import type {
|
||||
Image,
|
||||
Video,
|
||||
ImageOrVideo,
|
||||
Options,
|
||||
PossibleArray,
|
||||
} from 'react-native-image-crop-picker'
|
||||
|
||||
export type {Image} from 'react-native-image-crop-picker'
|
||||
|
||||
type MediaType<O> = O extends {mediaType: 'photo'}
|
||||
? Image
|
||||
: O extends {mediaType: 'video'}
|
||||
? Video
|
||||
: ImageOrVideo
|
||||
|
||||
export async function openPicker<O extends Options>(
|
||||
_options: O,
|
||||
): Promise<PossibleArray<O, MediaType<O>>> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
||||
export async function openCamera<O extends Options>(
|
||||
_options: O,
|
||||
): Promise<PossibleArray<O, MediaType<O>>> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
||||
export async function openCropper(_options: Options): Promise<Image> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue