Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add web polyfills

+240 -105
+1
package.json
··· 66 66 "react-native-url-polyfill": "^1.3.0", 67 67 "react-native-version-number": "^0.3.6", 68 68 "react-native-web": "^0.18.11", 69 + "react-native-web-linear-gradient": "^1.1.2", 69 70 "rn-fetch-blob": "^0.12.0", 70 71 "tlds": "^1.234.0", 71 72 "zod": "^3.20.2"
+69
src/lib/images.web.ts
··· 1 + import {Share} from 'react-native' 2 + 3 + import * as Toast from '../view/com/util/Toast' 4 + 5 + export interface DownloadAndResizeOpts { 6 + uri: string 7 + width: number 8 + height: number 9 + mode: 'contain' | 'cover' | 'stretch' 10 + maxSize: number 11 + timeout: number 12 + } 13 + 14 + export interface Image { 15 + path: string 16 + mime: string 17 + size: number 18 + width: number 19 + height: number 20 + } 21 + 22 + export async function downloadAndResize(_opts: DownloadAndResizeOpts) { 23 + // TODO 24 + throw new Error('TODO') 25 + } 26 + 27 + export interface ResizeOpts { 28 + width: number 29 + height: number 30 + mode: 'contain' | 'cover' | 'stretch' 31 + maxSize: number 32 + } 33 + 34 + export async function resize( 35 + _localUri: string, 36 + _opts: ResizeOpts, 37 + ): Promise<Image> { 38 + // TODO 39 + throw new Error('TODO') 40 + } 41 + 42 + export async function compressIfNeeded( 43 + _img: Image, 44 + _maxSize: number, 45 + ): Promise<Image> { 46 + // TODO 47 + throw new Error('TODO') 48 + } 49 + 50 + export interface Dim { 51 + width: number 52 + height: number 53 + } 54 + export function scaleDownDimensions(dim: Dim, max: Dim): Dim { 55 + if (dim.width < max.width && dim.height < max.height) { 56 + return dim 57 + } 58 + let wScale = dim.width > max.width ? max.width / dim.width : 1 59 + let hScale = dim.height > max.height ? max.height / dim.height : 1 60 + if (wScale < hScale) { 61 + return {width: dim.width * wScale, height: dim.height * wScale} 62 + } 63 + return {width: dim.width * hScale, height: dim.height * hScale} 64 + } 65 + 66 + export const saveImageModal = async (_opts: {uri: string}) => { 67 + // TODO 68 + throw new Error('TODO') 69 + }
+2 -2
src/state/index.ts
··· 2 2 import {Platform} from 'react-native' 3 3 import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' 4 4 import {RootStoreModel} from './models/root-store' 5 - import * as libapi from './lib/api' 5 + import * as apiPolyfill from './lib/api-polyfill' 6 6 import * as storage from './lib/storage' 7 7 8 8 export const LOCAL_DEV_SERVICE = ··· 17 17 let rootStore: RootStoreModel 18 18 let data: any 19 19 20 - libapi.doPolyfill() 20 + apiPolyfill.doPolyfill() 21 21 22 22 const api = AtpApi.service(serviceUri) as SessionServiceClient 23 23 rootStore = new RootStoreModel(api)
+76
src/state/lib/api-polyfill.ts
··· 1 + import {sessionClient as AtpApi} from '@atproto/api' 2 + 3 + export function doPolyfill() { 4 + AtpApi.xrpc.fetch = fetchHandler 5 + } 6 + 7 + interface FetchHandlerResponse { 8 + status: number 9 + headers: Record<string, string> 10 + body: ArrayBuffer | undefined 11 + } 12 + 13 + async function fetchHandler( 14 + reqUri: string, 15 + reqMethod: string, 16 + reqHeaders: Record<string, string>, 17 + reqBody: any, 18 + ): Promise<FetchHandlerResponse> { 19 + const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type'] 20 + if (reqMimeType && reqMimeType.startsWith('application/json')) { 21 + reqBody = JSON.stringify(reqBody) 22 + } else if ( 23 + typeof reqBody === 'string' && 24 + (reqBody.startsWith('/') || reqBody.startsWith('file:')) 25 + ) { 26 + if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) { 27 + // HACK 28 + // React native has a bug that inflates the size of jpegs on upload 29 + // we get around that by renaming the file ext to .bin 30 + // see https://github.com/facebook/react-native/issues/27099 31 + // -prf 32 + const newPath = reqBody.replace(/\.jpe?g$/, '.bin') 33 + await RNFS.moveFile(reqBody, newPath) 34 + reqBody = newPath 35 + } 36 + // NOTE 37 + // React native treats bodies with {uri: string} as file uploads to pull from cache 38 + // -prf 39 + reqBody = {uri: reqBody} 40 + } 41 + 42 + const controller = new AbortController() 43 + const to = setTimeout(() => controller.abort(), TIMEOUT) 44 + 45 + const res = await fetch(reqUri, { 46 + method: reqMethod, 47 + headers: reqHeaders, 48 + body: reqBody, 49 + signal: controller.signal, 50 + }) 51 + 52 + const resStatus = res.status 53 + const resHeaders: Record<string, string> = {} 54 + res.headers.forEach((value: string, key: string) => { 55 + resHeaders[key] = value 56 + }) 57 + const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type'] 58 + let resBody 59 + if (resMimeType) { 60 + if (resMimeType.startsWith('application/json')) { 61 + resBody = await res.json() 62 + } else if (resMimeType.startsWith('text/')) { 63 + resBody = await res.text() 64 + } else { 65 + throw new Error('TODO: non-textual response body') 66 + } 67 + } 68 + 69 + clearTimeout(to) 70 + 71 + return { 72 + status: resStatus, 73 + headers: resHeaders, 74 + body: resBody, 75 + } 76 + }
+4
src/state/lib/api-polyfill.web.ts
··· 1 + export function doPolyfill() { 2 + // TODO needed? native fetch may work fine -prf 3 + // AtpApi.xrpc.fetch = fetchHandler 4 + }
-75
src/state/lib/api.ts
··· 19 19 20 20 const TIMEOUT = 10e3 // 10s 21 21 22 - export function doPolyfill() { 23 - AtpApi.xrpc.fetch = fetchHandler 24 - } 25 - 26 22 export interface ExternalEmbedDraft { 27 23 uri: string 28 24 isLoading: boolean ··· 199 195 rkey: followUrip.rkey, 200 196 }) 201 197 } 202 - 203 - interface FetchHandlerResponse { 204 - status: number 205 - headers: Record<string, string> 206 - body: ArrayBuffer | undefined 207 - } 208 - 209 - async function fetchHandler( 210 - reqUri: string, 211 - reqMethod: string, 212 - reqHeaders: Record<string, string>, 213 - reqBody: any, 214 - ): Promise<FetchHandlerResponse> { 215 - const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type'] 216 - if (reqMimeType && reqMimeType.startsWith('application/json')) { 217 - reqBody = JSON.stringify(reqBody) 218 - } else if ( 219 - typeof reqBody === 'string' && 220 - (reqBody.startsWith('/') || reqBody.startsWith('file:')) 221 - ) { 222 - if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) { 223 - // HACK 224 - // React native has a bug that inflates the size of jpegs on upload 225 - // we get around that by renaming the file ext to .bin 226 - // see https://github.com/facebook/react-native/issues/27099 227 - // -prf 228 - const newPath = reqBody.replace(/\.jpe?g$/, '.bin') 229 - await RNFS.moveFile(reqBody, newPath) 230 - reqBody = newPath 231 - } 232 - // NOTE 233 - // React native treats bodies with {uri: string} as file uploads to pull from cache 234 - // -prf 235 - reqBody = {uri: reqBody} 236 - } 237 - 238 - const controller = new AbortController() 239 - const to = setTimeout(() => controller.abort(), TIMEOUT) 240 - 241 - const res = await fetch(reqUri, { 242 - method: reqMethod, 243 - headers: reqHeaders, 244 - body: reqBody, 245 - signal: controller.signal, 246 - }) 247 - 248 - const resStatus = res.status 249 - const resHeaders: Record<string, string> = {} 250 - res.headers.forEach((value: string, key: string) => { 251 - resHeaders[key] = value 252 - }) 253 - const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type'] 254 - let resBody 255 - if (resMimeType) { 256 - if (resMimeType.startsWith('application/json')) { 257 - resBody = await res.json() 258 - } else if (resMimeType.startsWith('text/')) { 259 - resBody = await res.text() 260 - } else { 261 - throw new Error('TODO: non-textual response body') 262 - } 263 - } 264 - 265 - clearTimeout(to) 266 - 267 - return { 268 - status: resStatus, 269 - headers: resHeaders, 270 - body: resBody, 271 - } 272 - }
+18
src/state/lib/bg-scheduler.ts
··· 1 + import BackgroundFetch, { 2 + BackgroundFetchStatus, 3 + } from 'react-native-background-fetch' 4 + 5 + export function configure( 6 + handler: (taskId: string) => Promise<void>, 7 + timeoutHandler: (taskId: string) => Promise<void>, 8 + ): Promise<BackgroundFetchStatus> { 9 + return BackgroundFetch.configure( 10 + {minimumFetchInterval: 15}, 11 + handler, 12 + timeoutHandler, 13 + ) 14 + } 15 + 16 + export function finish(taskId: string) { 17 + return BackgroundFetch.finish(taskId) 18 + }
+13
src/state/lib/bg-scheduler.web.ts
··· 1 + type BackgroundFetchStatus = 0 | 1 | 2 2 + 3 + export async function configure( 4 + _handler: (taskId: string) => Promise<void>, 5 + _timeoutHandler: (taskId: string) => Promise<void>, 6 + ): Promise<BackgroundFetchStatus> { 7 + // TODO 8 + return 0 9 + } 10 + 11 + export function finish(_taskId: string) { 12 + // TODO 13 + }
+1 -1
src/state/models/profile-view.ts
··· 1 1 import {makeAutoObservable, runInAction} from 'mobx' 2 - import {Image as PickedImage} from 'react-native-image-crop-picker' 2 + import {Image as PickedImage} from '../../view/com/util/images/ImageCropPicker' 3 3 import { 4 4 AppBskyActorGetProfile as GetProfile, 5 5 AppBskyActorProfile as Profile,
+4 -5
src/state/models/root-store.ts
··· 6 6 import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' 7 7 import {createContext, useContext} from 'react' 8 8 import {DeviceEventEmitter, EmitterSubscription} from 'react-native' 9 - import BackgroundFetch from 'react-native-background-fetch' 9 + import * as BgScheduler from '../lib/bg-scheduler' 10 10 import {isObj, hasProp} from '../lib/type-guards' 11 11 import {LogModel} from './log' 12 12 import {SessionModel} from './session' ··· 124 124 // background fetch runs every 15 minutes *at most* and will get slowed down 125 125 // based on some heuristics run by iOS, meaning it is not a reliable form of delivery 126 126 // -prf 127 - BackgroundFetch.configure( 128 - {minimumFetchInterval: 15}, 127 + BgScheduler.configure( 129 128 this.onBgFetch.bind(this), 130 129 this.onBgFetchTimeout.bind(this), 131 130 ).then(status => { ··· 138 137 if (this.session.hasSession) { 139 138 await this.me.bgFetchNotifications() 140 139 } 141 - BackgroundFetch.finish(taskId) 140 + BgScheduler.finish(taskId) 142 141 } 143 142 144 143 onBgFetchTimeout(taskId: string) { 145 144 this.log.debug(`Background fetch timed out for task ${taskId}`) 146 - BackgroundFetch.finish(taskId) 145 + BgScheduler.finish(taskId) 147 146 } 148 147 } 149 148
+1 -1
src/view/com/composer/PhotoCarouselPicker.tsx
··· 8 8 openPicker, 9 9 openCamera, 10 10 openCropper, 11 - } from 'react-native-image-crop-picker' 11 + } from '../util/images/ImageCropPicker' 12 12 import { 13 13 UserLocalPhotosModel, 14 14 PhotoIdentifier,
+1 -1
src/view/com/modals/EditProfile.tsx
··· 8 8 } from 'react-native' 9 9 import LinearGradient from 'react-native-linear-gradient' 10 10 import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet' 11 - import {Image as PickedImage} from 'react-native-image-crop-picker' 11 + import {Image as PickedImage} from '../util/images/ImageCropPicker' 12 12 import {Text} from '../util/text/Text' 13 13 import {ErrorMessage} from '../util/error/ErrorMessage' 14 14 import {useStores} from '../../../state'
+1 -1
src/view/com/util/UserAvatar.tsx
··· 7 7 openCropper, 8 8 openPicker, 9 9 Image as PickedImage, 10 - } from 'react-native-image-crop-picker' 10 + } from './images/ImageCropPicker' 11 11 import {colors, gradients} from '../../lib/styles' 12 12 13 13 export function UserAvatar({
+2 -6
src/view/com/util/UserBanner.tsx
··· 2 2 import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native' 3 3 import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {Image as PickedImage} from 'react-native-image-crop-picker' 5 + import {Image as PickedImage} from './images/ImageCropPicker' 6 6 import {colors, gradients} from '../../lib/styles' 7 - import { 8 - openCamera, 9 - openCropper, 10 - openPicker, 11 - } from 'react-native-image-crop-picker' 7 + import {openCamera, openCropper, openPicker} from './images/ImageCropPicker' 12 8 13 9 export function UserBanner({ 14 10 banner,
+6
src/view/com/util/images/ImageCropPicker.tsx
··· 1 + export { 2 + openPicker, 3 + openCamera, 4 + openCropper, 5 + } from 'react-native-image-crop-picker' 6 + export type {Image} from 'react-native-image-crop-picker'
+32
src/view/com/util/images/ImageCropPicker.web.tsx
··· 1 + import type { 2 + Image, 3 + Video, 4 + ImageOrVideo, 5 + Options, 6 + PossibleArray, 7 + } from 'react-native-image-crop-picker' 8 + 9 + export type {Image} from 'react-native-image-crop-picker' 10 + 11 + type MediaType<O> = O extends {mediaType: 'photo'} 12 + ? Image 13 + : O extends {mediaType: 'video'} 14 + ? Video 15 + : ImageOrVideo 16 + 17 + export async function openPicker<O extends Options>( 18 + _options: O, 19 + ): Promise<PossibleArray<O, MediaType<O>>> { 20 + // TODO 21 + throw new Error('TODO') 22 + } 23 + export async function openCamera<O extends Options>( 24 + _options: O, 25 + ): Promise<PossibleArray<O, MediaType<O>>> { 26 + // TODO 27 + throw new Error('TODO') 28 + } 29 + export async function openCropper(_options: Options): Promise<Image> { 30 + // TODO 31 + throw new Error('TODO') 32 + }
+1
web/webpack.config.js
··· 70 70 resolve: { 71 71 alias: { 72 72 'react-native$': 'react-native-web', 73 + 'react-native-linear-gradient': 'react-native-web-linear-gradient', 73 74 }, 74 75 extensions: [ 75 76 '.web.tsx',
+8 -13
yarn.lock
··· 5471 5471 dependencies: 5472 5472 once "^1.4.0" 5473 5473 5474 - enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0: 5474 + enhanced-resolve@^5.10.0: 5475 5475 version "5.12.0" 5476 5476 resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" 5477 5477 integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== ··· 9450 9450 snapdragon "^0.8.1" 9451 9451 to-regex "^3.0.2" 9452 9452 9453 - micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: 9453 + micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: 9454 9454 version "4.0.5" 9455 9455 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 9456 9456 integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== ··· 11401 11401 resolved "https://registry.yarnpkg.com/react-native-version-number/-/react-native-version-number-0.3.6.tgz#dd8b1435fc217df0a166d7e4a61fdc993f3e7437" 11402 11402 integrity sha512-TdyXiK90NiwmSbmAUlUBOV6WI1QGoqtvZZzI5zQY4fKl67B3ZrZn/h+Wy/OYIKKFMfePSiyfeIs8LtHGOZ/NgA== 11403 11403 11404 + react-native-web-linear-gradient@^1.1.2: 11405 + version "1.1.2" 11406 + resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7" 11407 + integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A== 11408 + 11404 11409 react-native-web@^0.18.11: 11405 11410 version "0.18.12" 11406 11411 resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.12.tgz#d4bb3a783ece2514ba0508d7805b09c0a98f5a8e" ··· 12045 12050 resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" 12046 12051 integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== 12047 12052 12048 - semver@7.3.8, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: 12053 + semver@7.3.8, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: 12049 12054 version "7.3.8" 12050 12055 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" 12051 12056 integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== ··· 13001 13006 version "1.0.1" 13002 13007 resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" 13003 13008 integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== 13004 - 13005 - ts-loader@^9.4.2: 13006 - version "9.4.2" 13007 - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" 13008 - integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA== 13009 - dependencies: 13010 - chalk "^4.1.0" 13011 - enhanced-resolve "^5.0.0" 13012 - micromatch "^4.0.0" 13013 - semver "^7.3.4" 13014 13009 13015 13010 tsconfig-paths@^3.14.1: 13016 13011 version "3.14.1"