Add web polyfills

zio/stable
Paul Frazee 2023-01-26 12:36:27 -06:00
parent d6ec627c8c
commit 751dfb20fd
18 changed files with 240 additions and 105 deletions

View File

@ -66,6 +66,7 @@
"react-native-url-polyfill": "^1.3.0", "react-native-url-polyfill": "^1.3.0",
"react-native-version-number": "^0.3.6", "react-native-version-number": "^0.3.6",
"react-native-web": "^0.18.11", "react-native-web": "^0.18.11",
"react-native-web-linear-gradient": "^1.1.2",
"rn-fetch-blob": "^0.12.0", "rn-fetch-blob": "^0.12.0",
"tlds": "^1.234.0", "tlds": "^1.234.0",
"zod": "^3.20.2" "zod": "^3.20.2"

View 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')
}

View File

@ -2,7 +2,7 @@ import {autorun} from 'mobx'
import {Platform} from 'react-native' import {Platform} from 'react-native'
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
import {RootStoreModel} from './models/root-store' 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' import * as storage from './lib/storage'
export const LOCAL_DEV_SERVICE = export const LOCAL_DEV_SERVICE =
@ -17,7 +17,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
let rootStore: RootStoreModel let rootStore: RootStoreModel
let data: any let data: any
libapi.doPolyfill() apiPolyfill.doPolyfill()
const api = AtpApi.service(serviceUri) as SessionServiceClient const api = AtpApi.service(serviceUri) as SessionServiceClient
rootStore = new RootStoreModel(api) rootStore = new RootStoreModel(api)

View 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,
}
}

View File

@ -0,0 +1,4 @@
export function doPolyfill() {
// TODO needed? native fetch may work fine -prf
// AtpApi.xrpc.fetch = fetchHandler
}

View File

@ -19,10 +19,6 @@ import {Image} from '../../lib/images'
const TIMEOUT = 10e3 // 10s const TIMEOUT = 10e3 // 10s
export function doPolyfill() {
AtpApi.xrpc.fetch = fetchHandler
}
export interface ExternalEmbedDraft { export interface ExternalEmbedDraft {
uri: string uri: string
isLoading: boolean isLoading: boolean
@ -199,74 +195,3 @@ export async function unfollow(store: RootStoreModel, followUri: string) {
rkey: followUrip.rkey, 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,
}
}

View 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)
}

View 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
}

View File

@ -1,5 +1,5 @@
import {makeAutoObservable, runInAction} from 'mobx' 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 { import {
AppBskyActorGetProfile as GetProfile, AppBskyActorGetProfile as GetProfile,
AppBskyActorProfile as Profile, AppBskyActorProfile as Profile,

View File

@ -6,7 +6,7 @@ import {makeAutoObservable} from 'mobx'
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
import {createContext, useContext} from 'react' import {createContext, useContext} from 'react'
import {DeviceEventEmitter, EmitterSubscription} from 'react-native' 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 {isObj, hasProp} from '../lib/type-guards'
import {LogModel} from './log' import {LogModel} from './log'
import {SessionModel} from './session' import {SessionModel} from './session'
@ -124,8 +124,7 @@ export class RootStoreModel {
// background fetch runs every 15 minutes *at most* and will get slowed down // 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 // based on some heuristics run by iOS, meaning it is not a reliable form of delivery
// -prf // -prf
BackgroundFetch.configure( BgScheduler.configure(
{minimumFetchInterval: 15},
this.onBgFetch.bind(this), this.onBgFetch.bind(this),
this.onBgFetchTimeout.bind(this), this.onBgFetchTimeout.bind(this),
).then(status => { ).then(status => {
@ -138,12 +137,12 @@ export class RootStoreModel {
if (this.session.hasSession) { if (this.session.hasSession) {
await this.me.bgFetchNotifications() await this.me.bgFetchNotifications()
} }
BackgroundFetch.finish(taskId) BgScheduler.finish(taskId)
} }
onBgFetchTimeout(taskId: string) { onBgFetchTimeout(taskId: string) {
this.log.debug(`Background fetch timed out for task ${taskId}`) this.log.debug(`Background fetch timed out for task ${taskId}`)
BackgroundFetch.finish(taskId) BgScheduler.finish(taskId)
} }
} }

View File

@ -8,7 +8,7 @@ import {
openPicker, openPicker,
openCamera, openCamera,
openCropper, openCropper,
} from 'react-native-image-crop-picker' } from '../util/images/ImageCropPicker'
import { import {
UserLocalPhotosModel, UserLocalPhotosModel,
PhotoIdentifier, PhotoIdentifier,

View File

@ -8,7 +8,7 @@ import {
} from 'react-native' } from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet' 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 {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {useStores} from '../../../state' import {useStores} from '../../../state'

View File

@ -7,7 +7,7 @@ import {
openCropper, openCropper,
openPicker, openPicker,
Image as PickedImage, Image as PickedImage,
} from 'react-native-image-crop-picker' } from './images/ImageCropPicker'
import {colors, gradients} from '../../lib/styles' import {colors, gradients} from '../../lib/styles'
export function UserAvatar({ export function UserAvatar({

View File

@ -2,13 +2,9 @@ import React, {useCallback} from 'react'
import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native' import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg' import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 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 {colors, gradients} from '../../lib/styles'
import { import {openCamera, openCropper, openPicker} from './images/ImageCropPicker'
openCamera,
openCropper,
openPicker,
} from 'react-native-image-crop-picker'
export function UserBanner({ export function UserBanner({
banner, banner,

View File

@ -0,0 +1,6 @@
export {
openPicker,
openCamera,
openCropper,
} from 'react-native-image-crop-picker'
export type {Image} from 'react-native-image-crop-picker'

View 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')
}

View File

@ -70,6 +70,7 @@ module.exports = {
resolve: { resolve: {
alias: { alias: {
'react-native$': 'react-native-web', 'react-native$': 'react-native-web',
'react-native-linear-gradient': 'react-native-web-linear-gradient',
}, },
extensions: [ extensions: [
'.web.tsx', '.web.tsx',

View File

@ -5471,7 +5471,7 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies: dependencies:
once "^1.4.0" once "^1.4.0"
enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0: enhanced-resolve@^5.10.0:
version "5.12.0" version "5.12.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
@ -9450,7 +9450,7 @@ micromatch@^3.1.10:
snapdragon "^0.8.1" snapdragon "^0.8.1"
to-regex "^3.0.2" to-regex "^3.0.2"
micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.5" version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
@ -11401,6 +11401,11 @@ react-native-version-number@^0.3.6:
resolved "https://registry.yarnpkg.com/react-native-version-number/-/react-native-version-number-0.3.6.tgz#dd8b1435fc217df0a166d7e4a61fdc993f3e7437" resolved "https://registry.yarnpkg.com/react-native-version-number/-/react-native-version-number-0.3.6.tgz#dd8b1435fc217df0a166d7e4a61fdc993f3e7437"
integrity sha512-TdyXiK90NiwmSbmAUlUBOV6WI1QGoqtvZZzI5zQY4fKl67B3ZrZn/h+Wy/OYIKKFMfePSiyfeIs8LtHGOZ/NgA== integrity sha512-TdyXiK90NiwmSbmAUlUBOV6WI1QGoqtvZZzI5zQY4fKl67B3ZrZn/h+Wy/OYIKKFMfePSiyfeIs8LtHGOZ/NgA==
react-native-web-linear-gradient@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7"
integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A==
react-native-web@^0.18.11: react-native-web@^0.18.11:
version "0.18.12" version "0.18.12"
resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.12.tgz#d4bb3a783ece2514ba0508d7805b09c0a98f5a8e" resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.12.tgz#d4bb3a783ece2514ba0508d7805b09c0a98f5a8e"
@ -12045,7 +12050,7 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
semver@7.3.8, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: semver@7.3.8, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8:
version "7.3.8" version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
@ -13002,16 +13007,6 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-loader@^9.4.2:
version "9.4.2"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78"
integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==
dependencies:
chalk "^4.1.0"
enhanced-resolve "^5.0.0"
micromatch "^4.0.0"
semver "^7.3.4"
tsconfig-paths@^3.14.1: tsconfig-paths@^3.14.1:
version "3.14.1" version "3.14.1"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"