From 751dfb20fd0d316da396e3c4fc53aaaaa8041dd1 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 26 Jan 2023 12:36:27 -0600 Subject: [PATCH] Add web polyfills --- package.json | 1 + src/lib/images.web.ts | 69 +++++++++++++++++ src/state/index.ts | 4 +- src/state/lib/api-polyfill.ts | 76 +++++++++++++++++++ src/state/lib/api-polyfill.web.ts | 4 + src/state/lib/api.ts | 75 ------------------ src/state/lib/bg-scheduler.ts | 18 +++++ src/state/lib/bg-scheduler.web.ts | 13 ++++ src/state/models/profile-view.ts | 2 +- src/state/models/root-store.ts | 9 +-- src/view/com/composer/PhotoCarouselPicker.tsx | 2 +- src/view/com/modals/EditProfile.tsx | 2 +- src/view/com/util/UserAvatar.tsx | 2 +- src/view/com/util/UserBanner.tsx | 8 +- src/view/com/util/images/ImageCropPicker.tsx | 6 ++ .../com/util/images/ImageCropPicker.web.tsx | 32 ++++++++ web/webpack.config.js | 1 + yarn.lock | 21 ++--- 18 files changed, 240 insertions(+), 105 deletions(-) create mode 100644 src/lib/images.web.ts create mode 100644 src/state/lib/api-polyfill.ts create mode 100644 src/state/lib/api-polyfill.web.ts create mode 100644 src/state/lib/bg-scheduler.ts create mode 100644 src/state/lib/bg-scheduler.web.ts create mode 100644 src/view/com/util/images/ImageCropPicker.tsx create mode 100644 src/view/com/util/images/ImageCropPicker.web.tsx diff --git a/package.json b/package.json index adedd032..8b135531 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "react-native-url-polyfill": "^1.3.0", "react-native-version-number": "^0.3.6", "react-native-web": "^0.18.11", + "react-native-web-linear-gradient": "^1.1.2", "rn-fetch-blob": "^0.12.0", "tlds": "^1.234.0", "zod": "^3.20.2" diff --git a/src/lib/images.web.ts b/src/lib/images.web.ts new file mode 100644 index 00000000..5158e005 --- /dev/null +++ b/src/lib/images.web.ts @@ -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 { + // TODO + throw new Error('TODO') +} + +export async function compressIfNeeded( + _img: Image, + _maxSize: number, +): Promise { + // 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') +} diff --git a/src/state/index.ts b/src/state/index.ts index 78fba2ec..654c15af 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -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) diff --git a/src/state/lib/api-polyfill.ts b/src/state/lib/api-polyfill.ts new file mode 100644 index 00000000..be6f90f7 --- /dev/null +++ b/src/state/lib/api-polyfill.ts @@ -0,0 +1,76 @@ +import {sessionClient as AtpApi} from '@atproto/api' + +export function doPolyfill() { + AtpApi.xrpc.fetch = fetchHandler +} + +interface FetchHandlerResponse { + status: number + headers: Record + body: ArrayBuffer | undefined +} + +async function fetchHandler( + reqUri: string, + reqMethod: string, + reqHeaders: Record, + reqBody: any, +): Promise { + 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 = {} + 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, + } +} diff --git a/src/state/lib/api-polyfill.web.ts b/src/state/lib/api-polyfill.web.ts new file mode 100644 index 00000000..1469cf90 --- /dev/null +++ b/src/state/lib/api-polyfill.web.ts @@ -0,0 +1,4 @@ +export function doPolyfill() { + // TODO needed? native fetch may work fine -prf + // AtpApi.xrpc.fetch = fetchHandler +} diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index e498bef1..c63f0e2f 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -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 - body: ArrayBuffer | undefined -} - -async function fetchHandler( - reqUri: string, - reqMethod: string, - reqHeaders: Record, - reqBody: any, -): Promise { - 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 = {} - 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, - } -} diff --git a/src/state/lib/bg-scheduler.ts b/src/state/lib/bg-scheduler.ts new file mode 100644 index 00000000..97ccb78b --- /dev/null +++ b/src/state/lib/bg-scheduler.ts @@ -0,0 +1,18 @@ +import BackgroundFetch, { + BackgroundFetchStatus, +} from 'react-native-background-fetch' + +export function configure( + handler: (taskId: string) => Promise, + timeoutHandler: (taskId: string) => Promise, +): Promise { + return BackgroundFetch.configure( + {minimumFetchInterval: 15}, + handler, + timeoutHandler, + ) +} + +export function finish(taskId: string) { + return BackgroundFetch.finish(taskId) +} diff --git a/src/state/lib/bg-scheduler.web.ts b/src/state/lib/bg-scheduler.web.ts new file mode 100644 index 00000000..91ec9428 --- /dev/null +++ b/src/state/lib/bg-scheduler.web.ts @@ -0,0 +1,13 @@ +type BackgroundFetchStatus = 0 | 1 | 2 + +export async function configure( + _handler: (taskId: string) => Promise, + _timeoutHandler: (taskId: string) => Promise, +): Promise { + // TODO + return 0 +} + +export function finish(_taskId: string) { + // TODO +} diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts index a1535693..3228c57e 100644 --- a/src/state/models/profile-view.ts +++ b/src/state/models/profile-view.ts @@ -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, diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index c4798ad0..2f6931cd 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -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) } } diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx index 383027de..eb5b4dcf 100644 --- a/src/view/com/composer/PhotoCarouselPicker.tsx +++ b/src/view/com/composer/PhotoCarouselPicker.tsx @@ -8,7 +8,7 @@ import { openPicker, openCamera, openCropper, -} from 'react-native-image-crop-picker' +} from '../util/images/ImageCropPicker' import { UserLocalPhotosModel, PhotoIdentifier, diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 8449fda6..380e76e7 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -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' diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index c9c255f4..d91607b6 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -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({ diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 5ce63835..fe606bc5 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -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, diff --git a/src/view/com/util/images/ImageCropPicker.tsx b/src/view/com/util/images/ImageCropPicker.tsx new file mode 100644 index 00000000..9cd4da9f --- /dev/null +++ b/src/view/com/util/images/ImageCropPicker.tsx @@ -0,0 +1,6 @@ +export { + openPicker, + openCamera, + openCropper, +} from 'react-native-image-crop-picker' +export type {Image} from 'react-native-image-crop-picker' diff --git a/src/view/com/util/images/ImageCropPicker.web.tsx b/src/view/com/util/images/ImageCropPicker.web.tsx new file mode 100644 index 00000000..a385e2e9 --- /dev/null +++ b/src/view/com/util/images/ImageCropPicker.web.tsx @@ -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 extends {mediaType: 'photo'} + ? Image + : O extends {mediaType: 'video'} + ? Video + : ImageOrVideo + +export async function openPicker( + _options: O, +): Promise>> { + // TODO + throw new Error('TODO') +} +export async function openCamera( + _options: O, +): Promise>> { + // TODO + throw new Error('TODO') +} +export async function openCropper(_options: Options): Promise { + // TODO + throw new Error('TODO') +} diff --git a/web/webpack.config.js b/web/webpack.config.js index 22270396..c7b248d8 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -70,6 +70,7 @@ module.exports = { resolve: { alias: { 'react-native$': 'react-native-web', + 'react-native-linear-gradient': 'react-native-web-linear-gradient', }, extensions: [ '.web.tsx', diff --git a/yarn.lock b/yarn.lock index bec4c873..84d6e3ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5471,7 +5471,7 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0: +enhanced-resolve@^5.10.0: version "5.12.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== @@ -9450,7 +9450,7 @@ micromatch@^3.1.10: snapdragon "^0.8.1" 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" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 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" 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: version "0.18.12" 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" 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" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -13002,16 +13007,6 @@ tryer@^1.0.1: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" 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: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"