You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
const [currentDate, setCurrentDate] = useState<string | null>(null);
const dateIndicatorOpacity = useSharedValue(0);
const textOpacity = useSharedValue(1);
const flashListRef = useRef<FlashListRef>(null);
const scrollToBottomButtonOpacity = useSharedValue(0);
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom since newest messages are at the top
// Scroll to bottom function
const scrollToBottom = useCallback(() => {
if (flashListRef.current && messages.length > 0) {
try {
// Scroll to the last index (newest message in reversed array)
flashListRef.current.scrollToEnd({ animated: true });
// Reset unseen messages count when scrolling to bottom
updateUnReadMessagesCount(0);
setIsAtBottom(true);
} catch (error) {
console.warn('Failed to scroll to bottom:', error);
}
}
}, [messages.length, updateUnReadMessagesCount]);
// Handle scroll events to detect when user is at bottom
const handleScrollEnd = useCallback((event: any) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
// Check if we're at the bottom (newest messages)
const isAtBottomNow = contentOffset.y + layoutMeasurement.height >= contentSize.height - 50; // 50px threshold
setIsAtBottom(isAtBottomNow);
if (isAtBottomNow) {
// User has reached the bottom, reset unseen messages count
updateUnReadMessagesCount(0);
}
}, [updateUnReadMessagesCount]);
// Update button visibility based on unseen messages count and scroll position
useEffect(() => {
if (unReadMessagesCount >= 1 && !isAtBottom) {
scrollToBottomButtonOpacity.value = withSpring(1);
} else {
scrollToBottomButtonOpacity.value = withSpring(0);
}
}, [unReadMessagesCount, isAtBottom, scrollToBottomButtonOpacity]);
// Reset unseen messages count when user is at bottom and new messages arrive
useEffect(() => {
if (isAtBottom && unReadMessagesCount > 0) {
updateUnReadMessagesCount(0);
}
}, [isAtBottom, unReadMessagesCount, updateUnReadMessagesCount]);
const renderMessage = useCallback(
({ item: message, index }: { item: MessageItemD; index: number }) => {
// Show avatar for last message in a group OR when next message has different sender
const showAvatar =
index === reversedMessages.length - 1 || (reversedMessages[index + 1]?.send_by !== message.send_by);
// Show date for first message OR when current message date differs from previous
const showDate =
index === 0 ||
formatMessageDate(new Date(message.createdAt || new Date())) !==
formatMessageDate(
new Date(reversedMessages[index - 1]?.createdAt || new Date()),
);
if (message.content_type === 'feedback') {
return (
<FeedbackItem message={message} />
)
}
if (message.type && message.type in MESSAGE_TYPE_COMPONENTS) {
const MessageComponent = MESSAGE_TYPE_COMPONENTS[message.type as MessageType];
if (MessageComponent) {
return <MessageComponent message={message} />
}
}
return (
<View style={{ flexDirection: showDate ? 'column' : 'column-reverse' }}>
{showDate && (
<DateSeparator
date={formatMessageDate(
new Date(message.createdAt || new Date()),
)}
/>
)}
<TouchableOpacity
onLongPress={() => onMessageLongPress?.(message)}
activeOpacity={0.8}>
<MessageBox
message={message}
showAvatar={showAvatar}
deleteMessage={handleDeleteMessage}
sessionUid={sessionUid}
/>
</TouchableOpacity>
</View>
);
},
[messages, handleDeleteMessage, onMessageLongPress, sessionUid],
// If we have a localId, use it with a prefix
if (item.localId !== undefined && item.localId !== null) {
baseKey = `local_${item.localId}`;
}
// If we have message_id, use it
else if (item.message_id !== undefined && item.message_id !== null) {
baseKey = `msg_${item.message_id}`;
}
// If we have id, use it
else if (item.id !== undefined && item.id !== null) {
baseKey = `id_${item.id}`;
}
// Fallback to timestamp
else {
const timestamp = item.createdAt
? new Date(item.createdAt).getTime()
: Date.now();
baseKey = `time_${timestamp}`;
}
// Always append the index to ensure uniqueness during fast scrolling
return `${baseKey}_idx_${index}`;
Here is my ChatMessagesList component, i am having scrolling issue here as i recently upgraded to v2, i had to remove 'inverted' prop and reverse the messages array
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
View,
RefreshControl,
ActivityIndicator,
TouchableOpacity,
ViewToken,
} from 'react-native';
import { createStyleSheet, useStyles } from 'react-native-unistyles';
import { MessageItemD, MessageType } from '../../../../types/conversations/message';
import MessageBox from './MessageBox';
import DateSeparator from './components/DateSeparator';
import { formatMessageDate } from './utils/dateUtils';
import { deleteMessageApi } from '../../../../network/api/conversations';
import useProjectStore from '../../../../store/projectStore';
import { useAuth } from '../../../../context/AuthContext';
import useMessagesStore from '../../../../store/messagesStore';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSequence,
withSpring,
withTiming,
} from 'react-native-reanimated';
import { isToday } from 'date-fns';
import { Typography } from '../../../../components/v2';
import FeedbackItem from './components/message-types/FeedbackItem';
import { ArrowDown } from 'lucide-react-native';
import ParticipantAdded from './components/(message_types)/ParticipantAdded';
import Resolved from './components/(message_types)/Resolved';
import IntentDetected from './components/(message_types)/IntentDetected';
import SystemFunction from './components/(message_types)/SystemFunction';
import ApiCall from './components/(message_types)/ApiCall';
import AiAction from './components/(message_types)/AiAction';
import CustomCode from './components/(message_types)/CustomCode';
import KnowledgeBase from './components/(message_types)/KnowledgeBase';
import useChatStore from '../../../../store/chatStore';
import { FlashList,FlashListRef } from '@shopify/flash-list';
interface Props {
messages: MessageItemD[];
onLoadMore: () => void;
sessionUid: string;
onMessageLongPress?: (message: MessageItemD) => void;
}
export default function MessageList({
messages,
onLoadMore,
sessionUid,
onMessageLongPress,
}: Props) {
const { styles, theme } = useStyles(stylesheet);
const { token } = useAuth();
const { activeProjectUId } = useProjectStore();
const { deleteMessage, loading, unReadMessagesCount, updateUnReadMessagesCount } = useMessagesStore();
const { getLiveTranslation } = useChatStore();
const transltationState = getLiveTranslation(sessionUid)
const [currentDate, setCurrentDate] = useState<string | null>(null);
const dateIndicatorOpacity = useSharedValue(0);
const textOpacity = useSharedValue(1);
const flashListRef = useRef<FlashListRef>(null);
const scrollToBottomButtonOpacity = useSharedValue(0);
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom since newest messages are at the top
// Scroll to bottom function
const scrollToBottom = useCallback(() => {
if (flashListRef.current && messages.length > 0) {
try {
// Scroll to the last index (newest message in reversed array)
flashListRef.current.scrollToEnd({ animated: true });
// Reset unseen messages count when scrolling to bottom
updateUnReadMessagesCount(0);
setIsAtBottom(true);
} catch (error) {
console.warn('Failed to scroll to bottom:', error);
}
}
}, [messages.length, updateUnReadMessagesCount]);
// Handle scroll events to detect when user is at bottom
const handleScrollEnd = useCallback((event: any) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
}, [updateUnReadMessagesCount]);
// Update button visibility based on unseen messages count and scroll position
useEffect(() => {
if (unReadMessagesCount >= 1 && !isAtBottom) {
scrollToBottomButtonOpacity.value = withSpring(1);
} else {
scrollToBottomButtonOpacity.value = withSpring(0);
}
}, [unReadMessagesCount, isAtBottom, scrollToBottomButtonOpacity]);
// Reset unseen messages count when user is at bottom and new messages arrive
useEffect(() => {
if (isAtBottom && unReadMessagesCount > 0) {
updateUnReadMessagesCount(0);
}
}, [isAtBottom, unReadMessagesCount, updateUnReadMessagesCount]);
const handleDeleteMessage = useCallback(
async (messageId: string) => {
console.log('messageId', messageId, activeProjectUId, sessionUid);
if (!token || !activeProjectUId || !sessionUid) return;
);
// Handle viewable items changed
const onViewableItemsChanged = useCallback(
({ viewableItems }: { viewableItems: ViewToken[] }) => {
if (viewableItems.length > 0) {
const topMessage = viewableItems[viewableItems.length - 1].item as MessageItemD;
const messageDate = new Date(topMessage.createdAt || Date.now());
const dateStr = formatMessageDate(messageDate);
);
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 50,
minimumViewTime: 100,
}).current;
const MESSAGE_TYPE_COMPONENTS: Partial<Record<MessageType, React.ComponentType<{ message: MessageItemD }>>> = {
[MessageType.intent_detected]: IntentDetected,
[MessageType.added_participants]: ParticipantAdded,
[MessageType.mark_resolved]: Resolved,
} as const;
// Create reversed messages array for consistent indexing
const reversedMessages = [...messages].reverse();
const renderMessage = useCallback(
({ item: message, index }: { item: MessageItemD; index: number }) => {
// Show avatar for last message in a group OR when next message has different sender
const showAvatar =
index === reversedMessages.length - 1 || (reversedMessages[index + 1]?.send_by !== message.send_by);
);
const getUniqueKey = useCallback((item: MessageItemD, index: number) => {
let baseKey = '';
}, []);
// const keyExtractor = useCallback((item: MessageItemD) => item.message_id?.toString() || item.id?.toString() || item.createdAt?.toString() || Date.now().toString(), []);
);
// Animated style for the floating date indicator
const dateIndicatorStyle = useAnimatedStyle(() => ({
opacity: dateIndicatorOpacity.value,
transform: [{ scale: dateIndicatorOpacity.value }],
}));
const dateTextStyle = useAnimatedStyle(() => ({
opacity: textOpacity.value,
}));
// Animated style for the scroll to bottom button
const scrollToBottomButtonStyle = useAnimatedStyle(() => ({
opacity: scrollToBottomButtonOpacity.value,
transform: [{ scale: scrollToBottomButtonOpacity.value }],
}));
return (
{!transltationState?.show && <Animated.View style={[styles.dateIndicator, dateIndicatorStyle]}>
<Animated.View style={dateTextStyle}>
{currentDate}
</Animated.View>
</Animated.View>
}
);
}
const stylesheet = createStyleSheet(theme => ({
container: {
flex: 1,
width: '100%',
},
list: {
flex: 1,
},
contentContainer: {
paddingVertical: theme.spacing.md,
flexGrow: 1,
},
loadingContainer: {
paddingVertical: theme.spacing.md,
alignItems: 'center',
},
dateIndicator: {
position: 'absolute',
top: 16,
alignSelf: 'center',
backgroundColor: theme.colors.surface,
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
zIndex: 100,
shadowColor: theme.colors.textSecondary,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
borderWidth: 1,
borderColor: theme.colors.border,
},
dateIndicatorText: {
color: theme.colors.textPrimary,
fontSize: 13,
fontWeight: '600',
},
scrollToBottomButton: {
position: 'absolute',
bottom: 20,
right: 20,
zIndex: 100,
},
scrollToBottomButtonContent: {
// backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.full,
shadowColor: theme.colors.textSecondary,
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 5,
elevation: 8,
},
scrollToBottomButtonInner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor:theme.colors.primary,
borderRadius: theme.borderRadius.full,
paddingHorizontal: 12,
paddingVertical: 8,
// gap: 8,
},
messageCountBadge: {
position: 'absolute',
right: -4,
top: -4,
backgroundColor: theme.colors.surface,
borderRadius: theme.borderRadius.full,
minWidth: 20,
height: 20,
paddingHorizontal: 6,
justifyContent: 'center',
alignItems: 'center',
},
messageCountText: {
color: theme.colors.primary,
fontSize: 11,
fontWeight: '700',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
minHeight:'100%',
}
}));
Here is my ChatMessagesList component, i am having scrolling issue here as i recently upgraded to v2, i had to remove 'inverted' prop and reverse the messages array
Beta Was this translation helpful? Give feedback.
All reactions