Skip to content

Layout shifts on android when loading chat #2602

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

Open
muazazhar opened this issue Mar 18, 2025 · 7 comments
Open

Layout shifts on android when loading chat #2602

muazazhar opened this issue Mar 18, 2025 · 7 comments

Comments

@muazazhar
Copy link

muazazhar commented Mar 18, 2025

I'm having this issue with gifted chat. when a chat is opened, everything moves downward including header before correcting their positions and even though chats are statically defined, it takes times to load UI and chats. What am i doing wrong? This header issue happens only on android.
If i comment out giftedchat component in screen, UI doesn't move downwards

Image

Versions:
RN-gifted-chats: "^2.8.0"
expo: "^52.0.38"

import { View, SafeAreaView, TouchableOpacity, Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import { useState, useCallback, useEffect } from 'react';
import {
  GiftedChat,
  IMessage,
  InputToolbar,
  Send,
  Actions,
  Composer,
  Time,
  Bubble,
} from 'react-native-gifted-chat';
import { useUserStore } from '~/store/userStore';
import { Ionicons } from '@expo/vector-icons';
import * as DocumentPicker from 'expo-document-picker';

export default function ChatScreen() {
  const { id, chatUser } = useLocalSearchParams();
  const { user } = useUserStore();
  const [messages, setMessages] = useState<IMessage[]>([]);
  const [text, setText] = useState('');
  const [isRecording, setIsRecording] = useState(false);
  const [selectedFile, setSelectedFile] = useState<DocumentPicker.DocumentResult | null>(null);
  const otherUser = chatUser ? JSON.parse(chatUser as string) : null;

  useEffect(() => {
    setMessages([
      {
        _id: 1,
        text: "Good morning! That's great to hear. Could you share the details with me?",
        createdAt: new Date(),
        user: {
          _id: 2,
          name: 'Daniella Russel',
          avatar: otherUser?.avatar,
        },
      },
      {
        _id: 2,
        text: 'Please review this',
        createdAt: new Date(),
        user: {
          _id: 2,
          name: 'Daniella Russel',
          avatar: otherUser?.avatar,
        },
        file: {
          name: 'Portfolio.zip',
          size: '54 KB',
          type: 'application/zip',
        },
      },
      {
        _id: 3,
        text: 'Sure! For starters, I recommend reallocating funds from low-yield bonds to high-growth mutual funds.',
        createdAt: new Date(),
        user: {
          _id: 2,
          name: 'Daniella Russel',
          avatar: otherUser?.avatar,
        },
      },
    ]);
  }, []);

  const onSend = useCallback((newMessages: IMessage[] = []) => {
    setMessages((previousMessages) => GiftedChat.append(previousMessages, newMessages));
  }, []);

  const handleAttachment = async () => {
    try {
      const result = await DocumentPicker.getDocumentAsync();
      if (result.assets && result.assets.length > 0) {
        const file = result.assets[0];
        setSelectedFile(result);
        // Send file message
        const fileMessage: IMessage = {
          _id: Math.random().toString(),
          text: file.name,
          createdAt: new Date(),
          user: {
            _id: user?._id || '',
            name: user?.username,
            avatar: user?.avatar,
          },
          file: {
            name: file.name,
            size: `${Math.round(file.size / 1024)} KB`,
            type: file.mimeType,
          },
        };
        onSend([fileMessage]);
      }
    } catch (error) {
      console.error('Error picking document:', error);
    }
  };

  const renderInputToolbar = (props: any) => (
    <InputToolbar
      {...props}
      containerStyle={{
        backgroundColor: '#f8f9fa',
        borderTopWidth: 0,
        paddingVertical: 8,
        paddingHorizontal: 10,
      }}
      primaryStyle={{ alignItems: 'center' }}
    />
  );

  const renderActions = (props: any) => (
    <Actions
      {...props}
      containerStyle={{
        width: 40,
        height: 40,
        alignItems: 'center',
        justifyContent: 'center',
        marginLeft: 4,
        marginRight: 4,
      }}
      icon={() => <Ionicons name="attach" size={24} color="#666" />}
      onPressActionButton={handleAttachment}
    />
  );

  const renderSend = (props: any) => (
    <Send
      {...props}
      disabled={!props.text && !isRecording}
      containerStyle={{
        width: 40,
        height: 40,
        alignItems: 'center',
        justifyContent: 'center',
        marginHorizontal: 4,
      }}>
      <TouchableOpacity
        onPress={() => {
          if (!props.text) {
            setIsRecording(!isRecording);
          } else {
            props.onSend({ text: props.text.trim() }, true);
          }
        }}
        className="h-8 w-8 items-center justify-center rounded-full bg-primary">
        <Ionicons
          name={props.text ? 'send' : isRecording ? 'stop' : 'mic'}
          size={20}
          color="white"
        />
      </TouchableOpacity>
    </Send>
  );

  const renderComposer = (props: any) => (
    <Composer
      {...props}
      textInputStyle={{
        backgroundColor: 'white',
        borderRadius: 20,
        borderWidth: 1,
        borderColor: '#e2e8f0',
        paddingHorizontal: 12,
        paddingVertical: 8,
        maxHeight: 100,
        fontSize: 16,
      }}
      placeholderTextColor="#94a3b8"
    />
  );

  const renderBubble = (props: any) => (
    <Bubble
      {...props}
      wrapperStyle={{
        left: {
          backgroundColor: 'white',
          borderRadius: 12,
          padding: 4,
          marginBottom: 4,
        },
        right: {
          backgroundColor: '#007AFF',
          borderRadius: 12,
          padding: 4,
          marginBottom: 4,
        },
      }}
      textStyle={{
        left: {
          color: '#1a1a1a',
        },
        right: {
          color: 'white',
        },
      }}
    />
  );

  const renderTime = (props: any) => (
    <Time
      {...props}
      timeTextStyle={{
        left: {
          color: '#94a3b8',
          fontSize: 12,
        },
        right: {
          color: '#94a3b8',
          fontSize: 12,
        },
      }}
    />
  );

  const renderMessageFile = (props: any) => {
    const { currentMessage } = props;
    if (currentMessage.file) {
      return (
        <View className="mt-2 rounded-lg bg-gray-100 p-3">
          <View className="flex-row items-center">
            <Ionicons name="document-outline" size={24} color="#666" />
            <View className="ml-3">
              <Text className="font-medium text-sm text-gray-900">{currentMessage.file.name}</Text>
              <Text className="text-xs text-gray-500">{currentMessage.file.size}</Text>
            </View>
          </View>
        </View>
      );
    }
    return null;
  };

  return (
    <SafeAreaView className="flex-1 bg-gray-50">
      <View className="flex-1">
        <GiftedChat
          messages={messages}
          onSend={onSend}
          user={{
            _id: user?._id || '',
            name: user?.username,
            avatar: user?.avatar,
          }}
          text={text}
          onInputTextChanged={setText}
          renderInputToolbar={renderInputToolbar}
          renderActions={renderActions}
          renderSend={renderSend}
          renderComposer={renderComposer}
          renderBubble={renderBubble}
          renderTime={renderTime}
          renderMessageFile={renderMessageFile}
          placeholder="Write a message..."
          showAvatarForEveryMessage={false}
          alwaysShowSend
          minInputToolbarHeight={60}
          timeFormat="HH:mm"
          dateFormat="ll"
          scrollToBottom
          infiniteScroll
        />
      </View>
    </SafeAreaView>
  );
}
@evilimkova
Copy link

Hi @muazazhar, I have the same problem, did you found a solution?

@muazazhar
Copy link
Author

No

@aalekh0695
Copy link

Facing the same problem only on android device.
No solution found yet

@Kjagd
Copy link

Kjagd commented Apr 8, 2025

I added a comment about our setup here: #2613 (comment) might be helpful

@younseoryu
Copy link

same issue here. @Kjagd 's approach didn't help

@hamzazaheer
Copy link

@Kjagd Getting the same issue. Has anyone found a solution for this?

@Kjagd
Copy link

Kjagd commented Apr 17, 2025

And you are positively using the edge-to-edge library? And updated your code to use it for the Status bar? It just looks related to that, but it would make sense to have a small example app in the repo here where is verified works.

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

No branches or pull requests

6 participants