| ... | ... | @@ -209,8 +209,6 @@ function ThreadMarkdownImageView(props: { |
| 209 | 209 | const [availableWidth, setAvailableWidth] = useState(0); |
| 210 | 210 | const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); |
| 211 | 211 | const [failedUri, setFailedUri] = useState<string | null>(null); |
| 212 | | const activeUriRef = useRef(props.uri); |
| 213 | | activeUriRef.current = props.uri; |
| 214 | 212 | |
| 215 | 213 | useEffect(() => { |
| 216 | 214 | setSourceSize(null); |
| ... | ... | @@ -272,23 +270,12 @@ function ThreadMarkdownImageView(props: { |
| 272 | 270 | overflow: "hidden", |
| 273 | 271 | }} |
| 274 | 272 | > |
| 275 | | <Image |
| 276 | | source={{ uri: props.uri }} |
| 277 | | resizeMode="contain" |
| 278 | | accessible={false} |
| 279 | | onLoad={(event) => { |
| 280 | | if (activeUriRef.current !== props.uri) return; |
| 281 | | const { width, height } = event.nativeEvent.source; |
| 282 | | setSourceSize({ width, height }); |
| 283 | | }} |
| 273 | <ThreadMarkdownImageRequest |
| 274 | key={props.uri} |
| 275 | uri={props.uri} |
| 276 | onLoad={setSourceSize} |
| 284 | 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 | 279 | </View> |
| 293 | 280 | </TouchableOpacity> |
| 294 | 281 | )} |
| ... | ... | @@ -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 | 323 | /** Markdown image whose src is a workspace file — loads through a signed asset URL. */ |
| 305 | 324 | function ThreadMarkdownImage(props: { |
| 306 | 325 | readonly environmentId: EnvironmentId; |