| ... | @@ -209,8 +209,6 @@ function ThreadMarkdownImageView(props: { | ... | @@ -209,8 +209,6 @@ function ThreadMarkdownImageView(props: { |
| 209 | const [availableWidth, setAvailableWidth] = useState(0); | 209 | const [availableWidth, setAvailableWidth] = useState(0); |
| 210 | const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); | 210 | const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); |
| 211 | const [failedUri, setFailedUri] = useState<string | null>(null); | 211 | const [failedUri, setFailedUri] = useState<string | null>(null); |
| 212 | const activeUriRef = useRef(props.uri); | | |
| 213 | activeUriRef.current = props.uri; | | |
| 214 | | 212 | |
| 215 | useEffect(() => { | 213 | useEffect(() => { |
| 216 | setSourceSize(null); | 214 | setSourceSize(null); |
| ... | @@ -272,23 +270,12 @@ function ThreadMarkdownImageView(props: { | ... | @@ -272,23 +270,12 @@ function ThreadMarkdownImageView(props: { |
| 272 | overflow: "hidden", | 270 | overflow: "hidden", |
| 273 | }} | 271 | }} |
| 274 | > | 272 | > |
| 275 | <Image | 273 | <ThreadMarkdownImageRequest |
| 276 | source={{ uri: props.uri }} | 274 | key={props.uri} |
| 277 | resizeMode="contain" | 275 | uri={props.uri} |
| 278 | accessible={false} | 276 | onLoad={setSourceSize} |
| 279 | onLoad={(event) => { | | |
| 280 | if (activeUriRef.current !== props.uri) return; | | |
| 281 | const { width, height } = event.nativeEvent.source; | | |
| 282 | setSourceSize({ width, height }); | | |
| 283 | }} | | |
| 284 | onError={() => setFailedUri(props.uri)} | 277 | onError={() => setFailedUri(props.uri)} |
| 285 | style={{ | | |
| 286 | width: "100%", | | |
| 287 | height: "100%", | | |
| 288 | opacity: displaySize === null ? 0 : 1, | | |
| 289 | }} | | |
| 290 | /> | 278 | /> |
| 291 | {displaySize === null ? <ActivityIndicator style={StyleSheet.absoluteFill} /> : null} | | |
| 292 | </View> | 279 | </View> |
| 293 | </TouchableOpacity> | 280 | </TouchableOpacity> |
| 294 | )} | 281 | )} |
| ... | @@ -301,6 +288,38 @@ function ThreadMarkdownImageView(props: { | ... | @@ -301,6 +288,38 @@ function ThreadMarkdownImageView(props: { |
| 301 | ); | 288 | ); |
| 302 | } | 289 | } |
| 303 | | 290 | |
| | 291 | function ThreadMarkdownImageRequest(props: { |
| | 292 | readonly uri: string; |
| | 293 | readonly onLoad: (sourceSize: { width: number; height: number }) => void; |
| | 294 | readonly onError: () => void; |
| | 295 | }) { |
| | 296 | const [loaded, setLoaded] = useState(false); |
| | 297 | |
| | 298 | return ( |
| | 299 | <> |
| | 300 | <Image |
| | 301 | source={{ uri: props.uri }} |
| | 302 | resizeMode="contain" |
| | 303 | accessible={false} |
| | 304 | onLoad={(event) => { |
| | 305 | setLoaded(true); |
| | 306 | props.onLoad(event.nativeEvent.source); |
| | 307 | }} |
| | 308 | onError={props.onError} |
| | 309 | style={{ width: "100%", height: "100%", opacity: loaded ? 1 : 0 }} |
| | 310 | /> |
| | 311 | {loaded ? null : ( |
| | 312 | <View |
| | 313 | pointerEvents="none" |
| | 314 | style={[StyleSheet.absoluteFill, { alignItems: "center", justifyContent: "center" }]} |
| | 315 | > |
| | 316 | <Text className="text-xs text-foreground-muted">Loading image…</Text> |
| | 317 | </View> |
| | 318 | )} |
| | 319 | </> |
| | 320 | ); |
| | 321 | } |
| | 322 | |
| 304 | /** Markdown image whose src is a workspace file — loads through a signed asset URL. */ | 323 | /** Markdown image whose src is a workspace file — loads through a signed asset URL. */ |
| 305 | function ThreadMarkdownImage(props: { | 324 | function ThreadMarkdownImage(props: { |
| 306 | readonly environmentId: EnvironmentId; | 325 | readonly environmentId: EnvironmentId; |