diff --git a/shared/app/index.native.tsx b/shared/app/index.native.tsx
index 35ee779518d0..88a89f640e08 100644
--- a/shared/app/index.native.tsx
+++ b/shared/app/index.native.tsx
@@ -13,6 +13,7 @@ import {SafeAreaProvider, initialWindowMetrics} from 'react-native-safe-area-con
import {makeEngine} from '../engine'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {enableFreeze} from 'react-native-screens'
+import {Image as ExpoImage} from 'expo-image'
import {setKeyboardUp} from '@/styles/keyboard-state'
import {setServiceDecoration} from '@/common-adapters/markdown/react'
import ServiceDecoration from '@/common-adapters/markdown/service-decoration'
@@ -27,6 +28,10 @@ logger.info('INIT App index module load')
enableFreeze(true)
setServiceDecoration(ServiceDecoration)
+// SDWebImage (used by expo-image) flushes its memory cache on iOS memory warnings, but
+// the simulator never sends memory warnings. Cap the cache so loading hundreds of chat
+// images doesn't exhaust VM in the simulator. On a real device this is a safety net only.
+ExpoImage.configureCache({maxMemoryCost: 100 * 1024 * 1024})
module.hot?.accept(() => {
console.log('accepted update in shared/index.native')
diff --git a/shared/chat/audio/audio-player.tsx b/shared/chat/audio/audio-player.tsx
index 6127f19e7dac..ab41861b3830 100644
--- a/shared/chat/audio/audio-player.tsx
+++ b/shared/chat/audio/audio-player.tsx
@@ -69,8 +69,13 @@ const AudioPlayer = (props: Props) => {
const {duration, big, maxWidth, url, visAmps} = props
const [playedRatio, setPlayedRatio] = React.useState(0)
const [paused, setPaused] = React.useState(true)
+ // Only mount AudioVideo after the user first taps play; calling useAudioPlayer
+ // unconditionally for every message in the list spawns CoreMedia threads per
+ // message and exhausts VM memory.
+ const [everPlayed, setEverPlayed] = React.useState(false)
const onClick = () => {
if (paused) {
+ setEverPlayed(true)
setPaused(false)
} else {
setPaused(true)
@@ -104,7 +109,7 @@ const AudioPlayer = (props: Props) => {
{formatAudioRecordDuration(timeLeft)}
- {url.length > 0 && (
+ {url.length > 0 && everPlayed && (
)}
diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx
index a47dccecc3f9..fb5690b40212 100644
--- a/shared/chat/conversation/list-area/index.tsx
+++ b/shared/chat/conversation/list-area/index.tsx
@@ -476,6 +476,7 @@ const NativeConversationList = function NativeConversationList() {
const listRef = React.useRef(null)
const {markInitiallyLoadedThreadAsRead} = Hooks.useActions()
+
const keyExtractor = (ordinal: ItemType) => {
return String(ordinal)
}
@@ -617,6 +618,7 @@ const NativeConversationList = function NativeConversationList() {
keyExtractor={keyExtractor}
ref={listRef}
renderScrollComponent={renderScrollComponent}
+ windowSize={3}
maintainVisibleContentPosition={
// MUST do this else if you come into a new thread it'll slowly scroll down when it loads
numOrdinals ? maintainVisibleContentPosition : undefined
diff --git a/shared/chat/conversation/messages/attachment/video/videoimpl.tsx b/shared/chat/conversation/messages/attachment/video/videoimpl.tsx
index 52207a2b4b42..dd9f23b1def8 100644
--- a/shared/chat/conversation/messages/attachment/video/videoimpl.tsx
+++ b/shared/chat/conversation/messages/attachment/video/videoimpl.tsx
@@ -53,31 +53,60 @@ const DesktopVideoImpl = (p: Props) => {
)
}
-const NativeVideoImpl = (p: Props) => {
- const {allowPlay, message, showPopup} = p
- const {fileURL: url, transferState, videoDuration} = message
- const {previewURL, height, width} = getAttachmentPreviewSize(message)
- const {showPoster, reveal} = usePosterState(url)
- const sourceUri = `${url}&contentforce=true`
+// Separated into its own component so useVideoPlayer is only called when the
+// user actually taps play. Calling useVideoPlayer unconditionally for every
+// video message in the list caused CoreMedia to initialize a player per
+// message, spawning dozens of network threads and exhausting VM memory.
+type NativeActiveVideoProps = {
+ sourceUri: string
+ height: number
+ width: number
+}
+const NativeActiveVideo = (p: NativeActiveVideoProps) => {
+ const {sourceUri, height, width} = p
const player = useVideoPlayer(sourceUri, pl => {
pl.loop = false
+ pl.play()
})
-
- const onPress = () => {
- reveal()
- player.play()
- }
-
useEventListener(player, 'playToEnd', () => {
player.replay()
})
+ return (
+
+ )
+}
+
+const NativeVideoImpl = (p: Props) => {
+ const {allowPlay, message, showPopup} = p
+ const {fileURL: url, transferState, videoDuration} = message
+ const {previewURL, height, width} = getAttachmentPreviewSize(message)
+ const [playerActive, setPlayerActive] = React.useState(false)
+ const [lastUrl, setLastUrl] = React.useState(url)
+ if (lastUrl !== url) {
+ setLastUrl(url)
+ setPlayerActive(false)
+ }
+ const sourceUri = `${url}&contentforce=true`
return (
<>
- {showPoster ? (
-
+ {playerActive && url ? (
+
+ ) : (
+ {
+ if (allowPlay) setPlayerActive(true)
+ }}
+ style={nativeStyles.pressable}
+ onLongPress={showPopup}
+ >
{
- ) : (
-
)}
>
)
diff --git a/shared/chat/conversation/messages/text/unfurl/unfurl-list/image/video.tsx b/shared/chat/conversation/messages/text/unfurl/unfurl-list/image/video.tsx
index cc0ef41e7069..37c2424dd184 100644
--- a/shared/chat/conversation/messages/text/unfurl/unfurl-list/image/video.tsx
+++ b/shared/chat/conversation/messages/text/unfurl/unfurl-list/image/video.tsx
@@ -53,12 +53,18 @@ const DesktopVideo = (p: Props) => {
)
}
-const NativeVideo = (props: Props) => {
- const {autoPlay, onClick, url, style, width, height} = props
- const {playing, setPlaying} = usePlayState(url, autoPlay)
+// Separated into its own component so useVideoPlayer is only called when the
+// player is needed. Calling it unconditionally for every unfurl in the list
+// spawns CoreMedia threads per message and exhausts VM memory.
+type NativeActiveVideoProps = {
+ sourceUri: string
+ autoPlay: boolean
+ playing: boolean
+ style: object
+}
- const uri = url.length > 0 ? url : 'https://'
- const sourceUri = `${uri}&autoplay=${autoPlay ? 'true' : 'false'}&contentforce=true`
+const NativeActiveVideo = (props: NativeActiveVideoProps) => {
+ const {sourceUri, autoPlay, playing, style} = props
const player = useVideoPlayer(sourceUri, p => {
p.loop = true
@@ -85,22 +91,50 @@ const NativeVideo = (props: Props) => {
return () => sub.remove()
}, [player])
+ return (
+
+ )
+}
+
+const NativeVideo = (props: Props) => {
+ const {autoPlay, onClick, url, style, width, height} = props
+ const {playing, setPlaying} = usePlayState(url, autoPlay)
+ // Activate the player when autoPlay is true or the user first taps play.
+ // Reset when URL changes so a new source gets a fresh player.
+ const [active, setActive] = React.useState(autoPlay)
+ const [lastUrl, setLastUrl] = React.useState(url)
+ if (lastUrl !== url) {
+ setLastUrl(url)
+ setActive(autoPlay)
+ }
+
+ const uri = url.length > 0 ? url : 'https://'
+ const sourceUri = `${uri}&autoplay=${autoPlay ? 'true' : 'false'}&contentforce=true`
+
const _onClick = () => {
if (onClick) {
onClick()
return
}
+ setActive(true)
setPlaying(p => !p)
}
return (
-
+ {active && (
+
+ )}
{!playing && }