-
Notifications
You must be signed in to change notification settings - Fork 345
Open
Labels
bugSomething isn't workingSomething isn't working
Description
Current behavior
Currently when I added item to top then never rendered first time that with glitch on scrolling
Expected behavior
smooth scrolling while rendering new item never rendered before
To Reproduce
Already add keyExtractor and more
Platform:
- iOS
- Android
Environment
"dependencies": {
"@lodev09/react-native-true-sheet": "^2.0.6",
"@react-native-vector-icons/icomoon": "^12.2.0",
"@react-native/new-app-screen": "0.80.2",
"@react-navigation/bottom-tabs": "^7.4.6",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.25",
"@shopify/flash-list": "^2.0.3",
"clsx": "^2.1.1",
"formik": "^2.4.6",
"nativewind": "^4.1.23",
"react": "19.1.0",
"react-native": "0.80.2",
"react-native-edge-to-edge": "^1.6.2",
"react-native-flash-message": "^0.4.2",
"react-native-image-picker": "^8.2.1",
"react-native-keyboard-controller": "^1.18.5",
"react-native-reanimated": "^3",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.15.2",
"yup": "^1.7.0"
},
List
import { ChatListing } from "@/src/features/chat/components/molecule";
import { ChatMessage } from "@/src/features/chat/types";
import { faker } from "@faker-js/faker";
import { FlashList, FlashListRef, ListRenderItem } from "@shopify/flash-list";
import React, { useState } from "react";
import { useSafeAreaInsets } from "react-native-safe-area-context";
// Generate initial 50 dummy items
const generateItems = (start: number, count: number) => {
const timestamp = faker.date.recent({ days: 7 });
return Array.from({ length: count }, (_, i) => ({
id: String(start + i),
text: faker.lorem.sentence(),
sender: Math.random() > 0.5 ? "u1" : "u2",
timestamp: timestamp.toISOString(),
}));
};
const CURRENT_USER = "u1";
interface Props {
ref?: React.RefObject<FlashListRef<ChatMessage> | null>;
}
export const ChatList: React.FC<Props> = ({ ref }) => {
const [items, setItems] = useState<ChatMessage[]>(generateItems(1, 50));
const { bottom } = useSafeAreaInsets();
const timestamp = faker.date.recent({ days: 7 });
const handleStartReached = async () => {
// Generate 15 new messages
const newMessages = Array.from({ length: 115 }, (_, i) => ({
id: `${Date.now()}-${i}`, // unique ID
text: faker.lorem.sentence(),
sender: Math.random() > 0.5 ? "u1" : "u2",
timestamp: timestamp.toISOString(),
}));
// Prepend them to the existing items
setItems((prev) => [...newMessages, ...prev]);
};
const renderItem: ListRenderItem<ChatMessage> = ({ index, item }) => {
const prevUser = index > 0 ? items[index - 1].sender : null;
const nextUser = index < items.length - 1 ? items[index + 1].sender : null;
const prevIsMe = prevUser === item.sender;
const nextIsMe = nextUser === item.sender;
return (
<ChatListing
prevIsMe={prevIsMe}
nextIsMe={nextIsMe}
isMe={item.sender === CURRENT_USER}
message={item}
/>
);
};
return (
<FlashList
onStartReachedThreshold={0.6}
onStartReached={handleStartReached}
ref={ref}
data={items}
showsVerticalScrollIndicator
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={{ paddingTop: 16, paddingBottom: bottom + 16 }}
maintainVisibleContentPosition={{
autoscrollToBottomThreshold: 0.2,
startRenderingFromBottom: true, // keep chat starting from bottom
}}
getItemType={(item) => {
return item.sender;
}}
drawDistance={600}
/>
);
};
- Card
import { ChatMessage } from "@/src/features/chat/types";
import colors from "@/src/theme/colors";
import { Image } from "expo-image";
import React, { useState } from "react";
import { Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
interface Props {
message: ChatMessage;
isMe: boolean;
prevIsMe?: boolean;
nextIsMe?: boolean;
}
const formatTime = (isoTimestamp: string): string => {
return new Date(isoTimestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
hour12: true,
});
};
const MESSAGE_AVATAR_SIZE = 25;
const GAP_BETWEEN_AVATAR_AND_CARD = 8;
const GAP_BETWEEN_CARD_AND_TIME = 4;
export const ChatListing: React.FC<Props> = ({
message,
isMe,
prevIsMe,
nextIsMe,
}) => {
const [showTime, setShowTime] = useState(false);
styles.useVariants({
isMe: isMe ? "true" : "false",
prevIsMe: prevIsMe ? "true" : "false",
nextIsMe: nextIsMe ? "true" : "false",
});
return (
<View style={[styles.container]}>
{!isMe && !nextIsMe && (
<Image
style={{ width: 25, height: 25 }}
source={require("@/assets/images/avatar.png")}
cachePolicy={"memory-disk"}
/>
)}
<View style={styles.inner}>
<View style={[styles.card]}>
<Text style={styles.text}>{message.text}</Text>
</View>
{showTime ||
(!nextIsMe && (
<Text style={styles.time}>{formatTime(message.timestamp)}</Text>
))}
</View>
</View>
);
};
const styles = StyleSheet.create(() => ({
container: {
flex: 1,
flexDirection: "row",
gap: GAP_BETWEEN_AVATAR_AND_CARD,
paddingHorizontal: 16,
variants: {
isMe: {
true: {
maxWidth: "65%",
alignSelf: "flex-end",
},
false: {
maxWidth: "70%",
alignSelf: "flex-start",
alignItems: "flex-end",
},
},
prevIsMe: {
true: {
paddingTop: 4,
},
false: {
paddingTop: 16,
},
},
},
},
inner: {
gap: GAP_BETWEEN_CARD_AND_TIME,
variants: {
nextIsMe: {
true: {
paddingLeft: MESSAGE_AVATAR_SIZE + GAP_BETWEEN_AVATAR_AND_CARD,
},
false: {},
},
},
},
card: {
paddingHorizontal: 12,
paddingVertical: 8,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
variants: {
isMe: {
true: {
backgroundColor: colors.blue[600],
borderBottomLeftRadius: 12,
},
false: {
backgroundColor: colors.gray[200],
borderBottomRightRadius: 12,
},
},
},
},
text: {
fontSize: 15,
lineHeight: 20,
variants: {
isMe: {
true: {
color: colors.white,
},
false: {
color: colors.black,
},
},
},
},
time: {
fontSize: 12,
color: colors.gray[500],
variants: {
isMe: {
true: {
alignSelf: "flex-end",
},
false: {
alignSelf: "flex-start",
},
},
},
},
}));
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working