Skip to content

🔥[🐛] Message actions can become invisible on iOS #3710

Description

@zbarbuto

Issue

As an example use-case - a twitch-style live stream with chat overlayed on top. We have a video fill the screen and place our stream chat over the top (tested with both absolute positioning and react-native-teleport - see app examples below - issue occurs with both approaches).

In both cases, when we tap-hold a message to react - the message disappears and the actions are invisible.

Screen.Recording.2026-07-06.at.3.36.29.pm.mov

(note, portrait/landscape doesn't matter - landscape just better matches the use-case)

Steps to reproduce

  1. Open the stream
  2. Tap and hold a message
  3. The message disappears and the actions are invisible
  4. Tapping outside causes the message input to briefly disappear before the message appears again

Expected behavior

Reactions should work the same as when not in an overlay/portal. This is the case on Android:

Screen_Recording_20260706_154543_ReactNativeSandbox.mp4

Project Related Information

Example using absolute position

Click To Expand

import { NavigationContainer, useNavigation } from '@react-navigation/native';
import {
  createNativeStackNavigator,
  type NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { useVideoPlayer, VideoView } from 'expo-video';
import { useRef } from 'react';
import {
  Pressable,
  StatusBar,
  StyleSheet,
  Text,
  useColorScheme,
  useWindowDimensions,
  View,
} from 'react-native';
import 'react-native-gesture-handler';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import {
  Channel,
  Chat,
  MessageComposer,
  MessageList,
  OverlayProvider,
  useChatContext,
  useCreateChatClient,
} from 'stream-chat-expo';
import {
  channelId,
  channelType,
  chatApiKey,
  chatUserId,
  chatUserName,
  chatUserToken,
} from './config';

const liveStream = 'https://live.143b.ch/cam/flux/ts:abr.m3u8';

const user = {
  id: chatUserId,
  name: chatUserName,
};

const Stack = createNativeStackNavigator<any>();

function App() {
  const isDarkMode = useColorScheme() === 'dark';

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.flex}>
        <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
        <ChatWrapper>
          <NavigationContainer>
            <Stack.Navigator screenOptions={{ headerShown: false }}>
              <Stack.Screen name="Home" component={HomeScreen} />
              <Stack.Screen
                name="Video"
                component={VideoScreen}
                options={{
                  animation: 'slide_from_bottom',
                  animationDuration: 280,
                  gestureEnabled: false,
                  presentation: 'fullScreenModal',
                  headerShown: false,
                  autoHideHomeIndicator: true,
                }}
              />
            </Stack.Navigator>
          </NavigationContainer>
        </ChatWrapper>
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
}

// HomeScreen.tsx
function HomeScreen({ navigation }: NativeStackScreenProps<any>) {
  return (
    <SafeAreaView style={styles.home}>
      <Pressable onPress={() => navigation.navigate('Video')}>
        <Text>Open Stream</Text>
      </Pressable>
    </SafeAreaView>
  );
}

// VideoScreen.tsx
function VideoScreen() {
  const navigation = useNavigation();
  const player = useVideoPlayer(
    { uri: liveStream, contentType: 'hls' },
    videoPlayer => {
      videoPlayer.play();
    },
  );
  const { width, height } = useWindowDimensions();

  return (
    <View style={styles.flex}>
      <VideoView
        style={StyleSheet.absoluteFill}
        player={player}
        contentFit="cover"
        nativeControls={false}
      />
      <Pressable style={[{ top: 24 }]} onPress={() => navigation.goBack()}>
        <Text></Text>
      </Pressable>
      <View
        style={{
          height,
          width: width * 0.4,
          position: 'absolute',
          top: 0,
          left: 0,
        }}
      >
        <ChatChannel />
      </View>
    </View>
  );
}

export const ChatWrapper = ({ children }: { children: React.ReactNode }) => {
  const chatClient = useCreateChatClient({
    apiKey: chatApiKey,
    userData: user,
    tokenOrProvider: chatUserToken,
  });

  if (!chatClient) {
    return (
      <SafeAreaView style={styles.flex}>
        <Text>Loading chat ...</Text>
      </SafeAreaView>
    );
  }

  return (
    <View style={styles.flex}>
      <OverlayProvider
        value={{
          style: {
            messageList: {
              container: {
                backgroundColor: 'rgba(0, 0, 0, 0.5)',
              },
            },
          },
        }}
      >
        <Chat client={chatClient}>{children}</Chat>
      </OverlayProvider>
    </View>
  );
};

const ChatChannel = () => {
  const { client } = useChatContext();
  const channel = useRef(client.channel(channelType, channelId));

  return (
    <Channel channel={channel.current}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

const styles = StyleSheet.create({
  flex: {
    flex: 1,
  },
  home: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  chatOverlay: {
    width: '40%',
  },
});

export default App;

Example using portal

Click To Expand

import { NavigationContainer, useNavigation } from '@react-navigation/native';
import {
  createNativeStackNavigator,
  type NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { useVideoPlayer, VideoView } from 'expo-video';
import { useRef } from 'react';
import {
  Pressable,
  StatusBar,
  StyleSheet,
  Text,
  useColorScheme,
  useWindowDimensions,
  View,
} from 'react-native';
import 'react-native-gesture-handler';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import {
  Channel,
  Chat,
  MessageComposer,
  MessageList,
  OverlayProvider,
  useChatContext,
  useCreateChatClient,
} from 'stream-chat-expo';
import {
  channelId,
  channelType,
  chatApiKey,
  chatUserId,
  chatUserName,
  chatUserToken,
} from './config';

const liveStream = 'https://live.143b.ch/cam/flux/ts:abr.m3u8';

const user = {
  id: chatUserId,
  name: chatUserName,
};

const Stack = createNativeStackNavigator<any>();

function App() {
  const isDarkMode = useColorScheme() === 'dark';

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.flex}>
        <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
        <ChatWrapper>
          <NavigationContainer>
            <Stack.Navigator screenOptions={{ headerShown: false }}>
              <Stack.Screen name="Home" component={HomeScreen} />
              <Stack.Screen
                name="Video"
                component={VideoScreen}
                options={{
                  animation: 'slide_from_bottom',
                  animationDuration: 280,
                  gestureEnabled: false,
                  presentation: 'fullScreenModal',
                  headerShown: false,
                  autoHideHomeIndicator: true,
                }}
              />
            </Stack.Navigator>
          </NavigationContainer>
        </ChatWrapper>
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
}

// HomeScreen.tsx
function HomeScreen({ navigation }: NativeStackScreenProps<any>) {
  return (
    <SafeAreaView style={styles.home}>
      <Pressable onPress={() => navigation.navigate('Video')}>
        <Text>Open Stream</Text>
      </Pressable>
    </SafeAreaView>
  );
}

// VideoScreen.tsx
function VideoScreen() {
  const navigation = useNavigation();
  const player = useVideoPlayer(
    { uri: liveStream, contentType: 'hls' },
    videoPlayer => {
      videoPlayer.play();
    },
  );
  const { width, height } = useWindowDimensions();

  return (
    <View style={styles.flex}>
      <VideoView
        style={StyleSheet.absoluteFill}
        player={player}
        contentFit="cover"
        nativeControls={false}
      />
      <Pressable style={[{ top: 24 }]} onPress={() => navigation.goBack()}>
        <Text></Text>
      </Pressable>
      <View
        style={{
          height,
          width: width * 0.4,
          position: 'absolute',
          top: 0,
          left: 0,
        }}
      >
        <ChatChannel />
      </View>
    </View>
  );
}

export const ChatWrapper = ({ children }: { children: React.ReactNode }) => {
  const chatClient = useCreateChatClient({
    apiKey: chatApiKey,
    userData: user,
    tokenOrProvider: chatUserToken,
  });

  if (!chatClient) {
    return (
      <SafeAreaView style={styles.flex}>
        <Text>Loading chat ...</Text>
      </SafeAreaView>
    );
  }

  return (
    <View style={styles.flex}>
      <OverlayProvider
        value={{
          style: {
            messageList: {
              container: {
                backgroundColor: 'rgba(0, 0, 0, 0.5)',
              },
            },
          },
        }}
      >
        <Chat client={chatClient}>{children}</Chat>
      </OverlayProvider>
    </View>
  );
};

const ChatChannel = () => {
  const { client } = useChatContext();
  const channel = useRef(client.channel(channelType, channelId));

  return (
    <Channel channel={channel.current}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

const styles = StyleSheet.create({
  flex: {
    flex: 1,
  },
  home: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  chatOverlay: {
    width: '40%',
  },
});

export default App;

Offline support

  • I have enabled offline support.
  • The feature I'm having does not occur when offline support is disabled. (stripe out if not applicable)

Environment

Click To Expand

package.json:

{
  "name": "ReactNativeSandbox",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "start": "react-native start",
    "test": "jest"
  },
  "reanimated": {
    "staticFeatureFlags": {
      "FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": false
    }
  },
  "dependencies": {
    "@react-native-community/netinfo": "12.0.1",
    "@react-native/new-app-screen": "0.85.0",
    "@react-navigation/native": "^7.3.7",
    "@react-navigation/native-stack": "^7.17.9",
    "expo": "~56.0.0",
    "expo-image-manipulator": "~56.0.20",
    "expo-image-picker": "^57.0.2",
    "expo-video": "^57.0.0",
    "react": "19.2.3",
    "react-native": "0.85.0",
    "react-native-gesture-handler": "~2.31.1",
    "react-native-orientation-director": "^3.0.2",
    "react-native-reanimated": "4.3.1",
    "react-native-safe-area-context": "~5.7.0",
    "react-native-screens": "^4.25.2",
    "react-native-svg": "15.15.4",
    "react-native-teleport": "^1.1.10",
    "react-native-worklets": "0.8.3",
    "stream-chat-expo": "^9.6.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@babel/preset-env": "^7.25.3",
    "@babel/runtime": "^7.25.0",
    "@react-native-community/cli": "20.1.0",
    "@react-native-community/cli-platform-android": "20.1.0",
    "@react-native-community/cli-platform-ios": "20.1.0",
    "@react-native/babel-preset": "0.85.0",
    "@react-native/eslint-config": "0.85.0",
    "@react-native/metro-config": "0.85.0",
    "@react-native/typescript-config": "0.85.0",
    "@types/jest": "^29.5.13",
    "@types/react": "^19.2.0",
    "@types/react-test-renderer": "^19.1.0",
    "babel-preset-expo": "~56.0.0",
    "eslint": "^8.19.0",
    "jest": "^29.6.3",
    "prettier": "2.8.8",
    "react-test-renderer": "19.2.3",
    "typescript": "^5.8.3"
  },
  "engines": {
    "node": ">= 22.11.0"
  }
}

react-native info output:

 OUTPUT GOES HERE
  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • stream-chat-react-native version you're using that has this issue:
    • e.g. 5.4.3
  • Device/Emulator info:
    • I am using a physical device
    • OS version: iOS 26.2
    • Device/Emulator: iPhone 16

Additional context

Screenshots

Click To Expand


Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions