diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index e44ee3907..6ff8b691c 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -5,6 +5,11 @@ import RNFS from 'react-native-fs'
import uuid from 'react-native-uuid'
import * as Toast from 'view/com/util/Toast'
+export interface Dim {
+ width: number
+ height: number
+}
+
export interface DownloadAndResizeOpts {
uri: string
width: number
@@ -119,10 +124,6 @@ export async function compressIfNeeded(
return finalImg
}
-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
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
new file mode 100644
index 000000000..ff0486278
--- /dev/null
+++ b/src/state/models/cache/image-sizes.ts
@@ -0,0 +1,37 @@
+import {Image} from 'react-native'
+import {Dim} from 'lib/media/manip'
+
+export class ImageSizesCache {
+ sizes: Map<string, Dim> = new Map()
+ private activeRequests: Map<string, Promise<Dim>> = new Map()
+
+ constructor() {}
+
+ get(uri: string): Dim | undefined {
+ return this.sizes.get(uri)
+ }
+
+ async fetch(uri: string): Promise<Dim> {
+ const dim = this.sizes.get(uri)
+ if (dim) {
+ return dim
+ }
+ const prom =
+ this.activeRequests.get(uri) ||
+ new Promise<Dim>(resolve => {
+ Image.getSize(
+ uri,
+ (width: number, height: number) => resolve({width, height}),
+ (err: any) => {
+ console.error('Failed to fetch image dimensions for', uri, err)
+ resolve({width: 0, height: 0})
+ },
+ )
+ })
+ this.activeRequests.set(uri, prom)
+ const res = await prom
+ this.activeRequests.delete(uri)
+ this.sizes.set(uri, res)
+ return res
+ }
+}
diff --git a/src/state/models/link-metas-view.ts b/src/state/models/cache/link-metas.ts
index 59447008a..607968c80 100644
--- a/src/state/models/link-metas-view.ts
+++ b/src/state/models/cache/link-metas.ts
@@ -1,10 +1,10 @@
import {makeAutoObservable} from 'mobx'
import {LRUMap} from 'lru_map'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
import {LinkMeta, getLinkMeta} from 'lib/link-meta/link-meta'
type CacheValue = Promise<LinkMeta> | LinkMeta
-export class LinkMetasViewModel {
+export class LinkMetasCache {
cache: LRUMap<string, CacheValue> = new LRUMap(100)
constructor(public rootStore: RootStoreModel) {
diff --git a/src/state/models/my-follows.ts b/src/state/models/cache/my-follows.ts
index bf1bf9600..725b7841e 100644
--- a/src/state/models/my-follows.ts
+++ b/src/state/models/cache/my-follows.ts
@@ -1,6 +1,6 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
import {bundleAsync} from 'lib/async/bundle'
const CACHE_TTL = 1000 * 60 * 60 // hourly
@@ -16,7 +16,7 @@ type Profile =
* follows. It should be periodically refreshed and updated any time
* the user makes a change to their follows.
*/
-export class MyFollowsModel {
+export class MyFollowsCache {
// data
followDidToRecordMap: Record<string, string> = {}
lastSync = 0
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 192e8f19f..120749155 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -2,7 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {RootStoreModel} from './root-store'
import {FeedModel} from './feed-view'
import {NotificationsViewModel} from './notifications-view'
-import {MyFollowsModel} from './my-follows'
+import {MyFollowsCache} from './cache/my-follows'
import {isObj, hasProp} from 'lib/type-guards'
export class MeModel {
@@ -15,7 +15,7 @@ export class MeModel {
followersCount: number | undefined
mainFeed: FeedModel
notifications: NotificationsViewModel
- follows: MyFollowsModel
+ follows: MyFollowsCache
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
@@ -27,7 +27,7 @@ export class MeModel {
algorithm: 'reverse-chronological',
})
this.notifications = new NotificationsViewModel(this.rootStore, {})
- this.follows = new MyFollowsModel(this.rootStore)
+ this.follows = new MyFollowsCache(this.rootStore)
}
clear() {
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 03550f1b0..4a8d09b41 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -13,10 +13,11 @@ import {LogModel} from './log'
import {SessionModel} from './session'
import {ShellUiModel} from './ui/shell'
import {ProfilesViewModel} from './profiles-view'
-import {LinkMetasViewModel} from './link-metas-view'
+import {LinkMetasCache} from './cache/link-metas'
import {NotificationsViewItemModel} from './notifications-view'
import {MeModel} from './me'
import {resetToTab} from '../../Navigation'
+import {ImageSizesCache} from './cache/image-sizes'
export const appInfo = z.object({
build: z.string(),
@@ -34,7 +35,8 @@ export class RootStoreModel {
shell = new ShellUiModel(this)
me = new MeModel(this)
profiles = new ProfilesViewModel(this)
- linkMetas = new LinkMetasViewModel(this)
+ linkMetas = new LinkMetasCache(this)
+ imageSizes = new ImageSizesCache()
// HACK
// this flag is to track the lexicon breaking refactor
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 0443c7be4..24dbe6a52 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -1,7 +1,15 @@
import React from 'react'
-import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'
-import Image, {OnLoadEvent} from 'view/com/util/images/Image'
+import {
+ Image,
+ StyleProp,
+ StyleSheet,
+ TouchableOpacity,
+ ViewStyle,
+} from 'react-native'
+// import Image from 'view/com/util/images/Image'
import {clamp} from 'lib/numbers'
+import {useStores} from 'state/index'
+import {Dim} from 'lib/media/manip'
export const DELAY_PRESS_IN = 500
const MIN_ASPECT_RATIO = 0.33 // 1/3
@@ -22,16 +30,27 @@ export function AutoSizedImage({
style?: StyleProp<ViewStyle>
children?: React.ReactNode
}) {
- const [aspectRatio, setAspectRatio] = React.useState<number>(1)
- const onLoad = (e: OnLoadEvent) => {
- setAspectRatio(
- clamp(
- e.nativeEvent.width / e.nativeEvent.height,
- MIN_ASPECT_RATIO,
- MAX_ASPECT_RATIO,
- ),
- )
- }
+ const store = useStores()
+ const [dim, setDim] = React.useState<Dim | undefined>(
+ store.imageSizes.get(uri),
+ )
+ const [aspectRatio, setAspectRatio] = React.useState<number>(
+ dim ? calc(dim) : 1,
+ )
+ React.useEffect(() => {
+ let aborted = false
+ if (dim) {
+ return
+ }
+ store.imageSizes.fetch(uri).then(newDim => {
+ if (aborted) {
+ return
+ }
+ setDim(newDim)
+ setAspectRatio(calc(newDim))
+ })
+ }, [dim, setDim, setAspectRatio, store, uri])
+
return (
<TouchableOpacity
onPress={onPress}
@@ -39,16 +58,19 @@ export function AutoSizedImage({
onPressIn={onPressIn}
delayPressIn={DELAY_PRESS_IN}
style={[styles.container, style]}>
- <Image
- style={[styles.image, {aspectRatio}]}
- source={{uri}}
- onLoad={onLoad}
- />
+ <Image style={[styles.image, {aspectRatio}]} source={{uri}} />
{children}
</TouchableOpacity>
)
}
+function calc(dim: Dim) {
+ if (dim.width === 0 || dim.height === 0) {
+ return 1
+ }
+ return clamp(dim.width / dim.height, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
+}
+
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
|