Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[🐛] 🔥 KeyboardCompatibleView causes chat input and message list to float away upwards on input blur on iPhone 15 Pro Max iOS 18.2 physical device #2876

Open
2 of 8 tasks
amamenko opened this issue Jan 8, 2025 · 3 comments

Comments

@amamenko
Copy link

amamenko commented Jan 8, 2025

Issue

Chat channel messages and chat input are sliding to the top and out of view after chat input is focused on and then blurred specifically on iPhone 15 Pro Max iOS 18.2 physical device. Adding the prop disableKeyboardCompatibleView to the <Channel /> component (import { Channel } from "stream-chat-expo";) fixes the issue but then keyboard hides input and needs additional configuration. Notably the custom StickyHeader on the MessageList component stays right where it is.

I have two completely separate and different chat components in my application and the same behavior occurs for the user on both components regardless of customization - again, disabling the KeyboardCompatibleView fixes the floating away issue.

Steps to reproduce

Steps to reproduce the behavior:

  1. Enter chat channel with message list and chat input on iPhone 15 Pro Max iOS 18.2 physical device.
  2. Focus on chat input so that native keyboard is shown.
  3. Remove focus (blur) chat input so that keyboard is dismissed.
  4. Chat input and message list float away upwards and out of sight.

Expected behavior

Chat input and chat view/chat message list should not slide away on input blur.

Project Related Information

Customization

Click To Expand

Wrapping the InboxStackNavigator I have the following:

import React, { useCallback, useState } from "react";
import {
  StackNavigationProp,
  createStackNavigator,
} from "@react-navigation/stack";
import { InboxScreen } from "../../screens/InboxScreen";
import InboxChannelScreen from "../../screens/InboxChannelScreen";
import {
  Chat,
  OverlayProvider,
  OverlayReactionListProps,
} from "stream-chat-expo";
import { useChatClient } from "../../hooks/stream/useChatClient";
import InboxHeader from "../Header/InboxHeader";
import { RouteProp, useFocusEffect } from "@react-navigation/native";
import { RootStackParamList } from "./StackNavigator";
import ProfileScreen from "../../screens/ProfileScreen";
import CustomMessageActionList from "../InboxChannel/CustomComponents/CustomMessageActionList";
import { CustomOverlayReactionList } from "../InboxChannel/CustomComponents/CustomOverlayReactionsList";
import CustomOverlayReactionsAvatar from "../InboxChannel/CustomComponents/CustomOverlayReactionsAvatar";
import InboxSearchUsersScreen from "../../screens/InboxSearchUsersScreen";
import SearchHeader from "../Header/SearchHeader";
import { User } from "../../interfaces/User.interface";
import { View } from "react-native";

...

return (
 <OverlayProvider
        value={{
          style: {
            overlay: {
              reactions: {
                title: {
                  display: "none",
                },
                avatarName: {
                  display: "none",
                },
              },
            },
            messageInput: {
              container: {
                borderColor: "transparent",
                display: "flex",
                flexDirection: "row",
                backgroundColor: "transparent",
                alignItems: "center",
                justifyContent: "center",
              },
            },
            messageSimple: {
              content: {
                containerInner: {
                  borderColor: "transparent",
                },
                textContainer: {
                  backgroundColor: "#fff",
                  borderColor: "transparent",
                },
                container: {
                  borderRadiusS: 15,
                },
              },
            },
          },
        }}
        MessageActionList={CustomMessageActionList}
        OverlayReactionList={(props: OverlayReactionListProps) => (
          <CustomOverlayReactionList {...props} />
        )}
        OverlayReactionsAvatar={CustomOverlayReactionsAvatar}
      >
        <Chat client={chatClient as any}>
          <Stack.Navigator>
          ...
          <Stack.Screen
              name="InboxChannel"
              options={{
                headerShown: false,
              }}
              children={({ navigation }) => (
                <InboxChannelScreen
                  navigation={navigation}
                  route={route}
                  chatClient={chatClient}
                />
              )}
            />
          ...
          </Stack.Navigator>
        </Chat>
      </OverlayProvider>
   );
}

Here's my InboxChannelScreen component:

import React, { useContext, useEffect } from "react";
import { ChatChannelContext } from "../context/ChatChannelContext";
import { Channel, MessageList, MessageInput } from "stream-chat-expo";
import { useHeaderHeight } from "@react-navigation/elements";
import { Dimensions, Text } from "react-native";
import StickyChannelHeader from "../components/InboxChannel/StickyChannelHeader";
import { StackNavigationProp } from "@react-navigation/stack";
import CustomMessage from "../components/InboxChannel/CustomComponents/CustomMessage";
import MessageAvatar from "../components/InboxChannel/MessageAvatar";
import { format } from "date-fns";
import SendButton from "../components/InboxChannel/SendButton";
import CustomMessageInput from "../components/InboxChannel/CustomComponents/CustomMessageInput";
import InboxChannelSkeleton from "../components/InboxChannel/InboxChannelSkeleton";
import { RouteProp, useIsFocused } from "@react-navigation/native";
import { StreamChat } from "stream-chat";
import { RootStackParamList } from "../components/Navigation/StackNavigator";
import ChannelEmptyState from "../components/InboxChannel/ChannelEmptyState";
import useGetRemoteUser from "../hooks/queries/useGetRemoteUser";
import { User } from "../interfaces/User.interface";
import { InboxStackParamList } from "../components/Navigation/InboxStackNavigator";
import useFindOrCreateChatChannel from "../hooks/stream/useFindOrCreateChatChannel";
import CustomInputReplyStateHeader from "../components/InboxChannel/CustomComponents/CustomInputReplyStateHeader";
import CustomReply from "../components/InboxChannel/CustomComponents/CustomReply";
import CustomMessageSimple from "../components/InboxChannel/CustomComponents/CustomMessageSimple";
import { SafeAreaView } from "react-native-safe-area-context";

interface InboxChannelScreenProps {
  navigation: StackNavigationProp<InboxStackParamList>;
  route: RouteProp<RootStackParamList, "Inbox">;
  chatClient: StreamChat<any>;
}

export const InboxChannelScreen = ({
  navigation,
  route,
  chatClient,
}: InboxChannelScreenProps) => {
  const remoteUserId = route?.params?.id || "";
  const isFocused = useIsFocused();
  const { chatChannel, setChatChannel } = useContext(ChatChannelContext);
  const headerHeight = useHeaderHeight();
  const { data: remoteUser, refetch: refetchRemoteUser } = useGetRemoteUser({
    userId: remoteUserId || "",
  });
  const { findOrCreateChatChannel } = useFindOrCreateChatChannel({
    chatClient,
  });

  useEffect(() => {
    if (isFocused && !chatChannel && remoteUserId) {
      findOrCreateChatChannel(remoteUserId);
    }
  }, [isFocused, chatChannel, remoteUserId]);

  useEffect(() => {
    if (!isFocused) {
      setChatChannel(null);
    }
  }, [isFocused, setChatChannel]);

  useEffect(() => {
    if (isFocused && remoteUserId) {
      refetchRemoteUser();
    }
  }, [isFocused, remoteUserId]);

  if (!chatChannel) return <InboxChannelSkeleton navigation={navigation} />;

  const windowWidth = Dimensions.get("window").width / 1.5;

  return (
    <Channel
      disableKeyboardCompatibleView={true}
      channel={chatChannel as any}
      keyboardVerticalOffset={headerHeight}
      MessageSimple={CustomMessageSimple}
      MessageText={CustomMessage}
      MessageAvatar={(props) => <MessageAvatar navigation={navigation} />}
      MessageFooter={() => <></>}
      StickyHeader={() => <StickyChannelHeader navigation={navigation} />}
      Reply={(props) => <CustomReply {...props} />}
      InlineDateSeparator={({ date }) => (
        <Text className="text-center font-medium py-6 text-gray-500">
          {format(date, "MMM dd, yyyy hh:mm a")}
        </Text>
      )}
      InputReplyStateHeader={CustomInputReplyStateHeader}
      myMessageTheme={{
        overlay: {
          container: {
            backgroundColor: "rgba(255,255,255,0.9)",
          },
          reactionsList: {
            reactionList: {
              maxWidth: 300,
              overflow: "scroll",
              display: "flex",
              flexDirection: "row",
              alignItems: "flex-start",
            },
            reactionSize: 29,
          },
        },
        reply: {
          container: {
            backgroundColor: "rgb(215, 215, 215)",
            borderRadius: 16,
            borderWidth: 0,
          },
          messageContainer: {
            backgroundColor: "rgb(215, 215, 215)",
            borderRadius: 16,
            borderWidth: 0,
          },
          textContainer: {
            backgroundColor: "rgb(215, 215, 215)",
            borderRadius: 16,
            borderWidth: 0,
          },
        },
        messageSimple: {
          content: {
            textContainer: {
              backgroundColor: "#00d26c",
              maxWidth: windowWidth,
              borderWidth: 0,
              onlyEmojiMarkdown: {
                text: {
                  color: "#000000", // Set the text color for emoji-only messages
                },
              },
            },
            wrapper: {
              backgroundColor: "transparent",
              maxWidth: windowWidth,
              borderWidth: 0,
            },
            messageUser: {
              color: "#000",
            },
            replyBorder: {
              borderWidth: 0,
            },
            container: {
              borderWidth: 0,
              borderRadiusS: 15,
            },
            containerInner: {
              borderWidth: 0,
            },
            deletedContainerInner: {
              borderWidth: 0,
            },
          },
          reactionList: {
            reactionSize: 40,
            reactionCount: {
              display: "none",
              fontSize: 15,
            },
          },
        },
        messageInput: {
          container: {
            display: "flex",
            flexDirection: "row",
            justifyContent: "center",
            alignItems: "center",
          },
        },
      }}
    >
      <SafeAreaView
        className="flex-1"
        style={[
          {
            backgroundColor: "rgb(248,248,248)",
          },
        ]}
      >
        <MessageList
          StickyHeader={() => <StickyChannelHeader navigation={navigation} />}
          EmptyStateIndicator={() => (
            <ChannelEmptyState
              navigation={navigation}
              remoteUser={remoteUser as User}
            />
          )}
          ScrollToBottomButton={() => <></>}
        />
        <MessageInput
          Input={CustomMessageInput}
          InputButtons={() => <></>}
          SendButton={SendButton}
        />
      </SafeAreaView>
    </Channel>
  );
};

export default InboxChannelScreen;

And here's my CustomMessageInput:

import React, { useEffect, useRef } from "react";
import { Animated, Keyboard, View } from "react-native";
import {
  AutoCompleteInput,
  useMessageInputContext,
  useOverlayContext,
} from "stream-chat-expo";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import PressableOpacity from "../../PressableOpacity";

const CustomMessageInput = () => {
  const { sendMessage, text } = useMessageInputContext();
  const { overlay } = useOverlayContext();
  const animateTranslateY = useRef(new Animated.Value(0)).current;
  const overlayIsActive = !overlay || overlay !== "none";

  useEffect(() => {
    Animated.timing(animateTranslateY, {
      duration: overlayIsActive ? 500 : 0,
      toValue: overlayIsActive ? 80 : 0,
      useNativeDriver: true,
    }).start();
  }, [overlayIsActive]);

  const handleSendMessage = async () => {
    await sendMessage();
    Keyboard.dismiss();
  };

  const translateY = animateTranslateY.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 85],
    extrapolate: "clamp",
  });

  return (
    <Animated.View
      className="flex-row items-center gap-4"
      style={[
        {
          transform: [{ translateY }],
        },
      ]}
    >
      <View className="relative w-full flex-1 flex-row items-center justify-center">
        <AutoCompleteInput
          additionalTextInputProps={{
            onPressIn: (e) => e.preventDefault(),
            placeholder: "Send a message...",
            placeholderTextColor: "rgb(131,132,137)",
            returnKeyType: "send",
            style: {
              flex: 1,
              backgroundColor: "rgb(254,255,254)",
              color: "#000",
              fontSize: 18,
              padding: 5,
              paddingLeft: 15,
              paddingTop: 12,
              paddingBottom: 12,
              borderRadius: 12,
              width: "100%",
            },
          }}
        />
      </View>
      <View className="flex items-center justify-center absolute right-2 top-0 bottom-0 h-fit">
        <PressableOpacity
          className={`h-9 w-9 items-center justify-center rounded-full ${
            text ? "bg-green-600" : "bg-stone-400/60"
          }`}
          onPress={handleSendMessage}
        >
          <FontAwesome name="send" size={18} color={"#fff"} />
        </PressableOpacity>
      </View>
    </Animated.View>
  );
};

export default CustomMessageInput;

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": "coolthing",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start --dev-client",
    "android": "expo run:android",
    "device:android": "expo run:android --device",
    "ios": "expo run:ios"
  },
  "dependencies": {
    "@config-plugins/react-native-webrtc": "^9.0.0",
    "@expo/config-plugins": "~8.0.0",
    "@expo/react-native-action-sheet": "^4.1.0",
    "@expo/vector-icons": "^14.0.4",
    "@gorhom/bottom-sheet": "^4.6.4",
    "@notifee/react-native": "^9.1.8",
    "@react-native-async-storage/async-storage": "1.23.1",
    "@react-native-community/netinfo": "11.3.1",
    "@react-native-google-signin/google-signin": "^13.1.0",
    "@react-native-masked-view/masked-view": "0.3.1",
    "@react-native-picker/picker": "2.7.5",
    "@react-navigation/bottom-tabs": "^6.6.1",
    "@react-navigation/drawer": "^6.7.2",
    "@react-navigation/native": "^6.1.18",
    "@react-navigation/stack": "^6.4.1",
    "@stream-io/flat-list-mvcp": "^0.10.3",
    "@stream-io/react-native-webrtc": "^118.1.2",
    "@stream-io/video-react-native-sdk": "^1.2.12",
    "@tanstack/react-query": "^5.59.0",
    "axios": "^1.7.7",
    "bson": "^6.8.0",
    "date-fns": "^4.1.0",
    "expo": "~51.0.39",
    "expo-av": "~14.0.7",
    "expo-brightness": "~12.0.1",
    "expo-build-properties": "~0.12.5",
    "expo-camera": "~15.0.16",
    "expo-clipboard": "~6.0.3",
    "expo-constants": "~16.0.2",
    "expo-dev-client": "~4.0.29",
    "expo-device": "~6.0.2",
    "expo-document-picker": "~12.0.2",
    "expo-file-system": "~17.0.1",
    "expo-haptics": "~13.0.1",
    "expo-image": "~1.13.0",
    "expo-image-manipulator": "~12.0.5",
    "expo-image-picker": "~15.0.7",
    "expo-linear-gradient": "~13.0.2",
    "expo-linking": "~6.3.1",
    "expo-media-library": "~16.0.5",
    "expo-notifications": "~0.28.19",
    "expo-sharing": "~12.0.1",
    "expo-status-bar": "~1.12.1",
    "expo-updates": "~0.25.27",
    "expo-web-browser": "~13.0.3",
    "firebase": "^10.14.0",
    "lodash.capitalize": "^4.2.1",
    "lodash.debounce": "^4.0.8",
    "lodash.delay": "^4.1.1",
    "lodash.random": "^3.2.0",
    "lottie-react-native": "6.7.0",
    "nativewind": "^2.0.11",
    "react": "18.2.0",
    "react-native": "0.74.5",
    "react-native-dotenv": "^3.4.11",
    "react-native-gesture-handler": "~2.16.1",
    "react-native-incall-manager": "^4.2.0",
    "react-native-keyboard-aware-scroll-view": "^0.9.5",
    "react-native-modal": "^13.0.1",
    "react-native-paper": "^5.12.5",
    "react-native-purchases": "^8.5.0",
    "react-native-reanimated": "~3.10.1",
    "react-native-reanimated-carousel": "^3.5.1",
    "react-native-safe-area-context": "4.10.5",
    "react-native-screens": "3.31.1",
    "react-native-shimmer-placeholder": "^2.0.9",
    "react-native-svg": "15.2.0",
    "react-native-uuid": "^2.0.2",
    "react-native-vector-icons": "^10.2.0",
    "react-native-vision-camera": "^4.6.3",
    "stream-chat": "^8.42.0",
    "stream-chat-expo": "^5.44.2",
    "stream-chat-react-native": "^5.44.2",
    "stream-chat-react-native-core": "^5.44.2",
    "validator": "^13.12.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.7",
    "@types/lodash.capitalize": "^4.2.9",
    "@types/lodash.debounce": "^4.0.9",
    "@types/lodash.delay": "^4.1.9",
    "@types/lodash.random": "^3.2.9",
    "@types/react": "~18.2.79",
    "@types/validator": "^13.12.2",
    "babel-plugin-module-resolver": "^5.0.2",
    "expo-atlas": "^0.3.0",
    "tailwindcss": "3.3.2",
    "typescript": "~5.3.3"
  },
  "private": true
}

react-native info output:

info Fetching system and libraries information...
(node:54174) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
System:
  OS: macOS 15.0.1
  CPU: (8) arm64 Apple M2
  Memory: 118.19 MB / 8.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 20.10.0
    path: ~/.nvm/versions/node/v20.10.0/bin/node
  Yarn:
    version: 1.22.19
    path: ~/.nvm/versions/node/v16.17.0/bin/yarn
  npm:
    version: 10.2.3
    path: ~/.nvm/versions/node/v20.10.0/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.15.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  Android SDK: Not Found
IDEs:
  Android Studio: 2022.3 AI-223.8836.35.2231.11005911
  Xcode:
    version: 16.2/16C5032a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 20.0.1
    path: /opt/homebrew/opt/openjdk/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.74.5
    wanted: 0.74.5
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: true
  newArchEnabled: false

info React Native v0.76.5 is now available (your project is running on v0.74.5).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.76.5
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.74.5
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".
  • 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:
    • ^5.44.2
  • Device/Emulator info:
    • I am using a physical device
    • OS version: iOS 18.2 (22C152)
    • Device/Emulator: iPhone 15 Pro Max

Additional context

Screenshots

Click To Expand

https://drive.google.com/file/d/1hX5LBB4VSAs40Kt-4V-USE6VeAZLlsRe/view?usp=drive_link


@khushal87
Copy link
Member

Hey @amamenko, is this very specific to this device(iPhone 15 pro max), and doesn't happen on any other platforms? Also, I suspect this has something to do with your customization and doesn't seem to happen with our default SDK components. You can easily confirm that by adding our components as is and with minimal customization. Maybe that would help us point out the problem.

@khushal87
Copy link
Member

Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-01-16.at.16.15.51.mp4

I don't have a 15 pro max real device handy, but I am testing it on the iPhone 16 pro max simulator and don't see an issue just with your applied theme. Although i didn't add every component customization that you have, but can you relate to it?

@amamenko
Copy link
Author

@khushal87 This is probably a transient isolated issue. We were able to debug this today via an Expo ngrok tunnel on the basic ExpoMessaging example.

We are able to reproduce the bug when using some of the same custom components:

ScreenRecording_01-16-2025.18-35-22_1.MOV

While debugging, though, I saw the following warning in the terminal logs from the user's device:

[Reanimated] Reduced motion setting is enabled on this device. This warning is visible only in the development mode. Some animations will be disabled by default. You can override the behavior for individual animations, see https:/ /docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#reduced-motion-setting-is-enabled-on-this-device.

Which was odd so I asked the user to disable the iOS reduced motion setting (they didn't even know they had it on.) Once turned off, the issue was mitigated and the chat was working normally again - no issues.

The weird thing is that when reduced motion was re-enabled, the chat was still working fine. Really weird issue. Probably just a physical device glitch of some sort.

Fixed running minimal example:

ScreenRecording_01-16-2025.20-59-20_1.MOV

Custom components were also working just fine again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants