Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,7 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6"
integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==

"@expo/react-native-action-sheet@^4.1.1":
"@expo/react-native-action-sheet@^4.1.0":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@expo/react-native-action-sheet/-/react-native-action-sheet-4.1.1.tgz#a94a11088b8f146ac86b3ae710deef609a922d82"
integrity sha512-4KRaba2vhqDRR7ObBj6nrD5uJw8ePoNHdIOMETTpgGTX7StUbrF4j/sfrP1YUyaPEa1P8FXdwG6pB+2WtrJd1A==
Expand Down Expand Up @@ -1538,9 +1538,9 @@
fastq "^1.6.0"

"@qteab/react-native-firebase-chat@file:..":
version "0.5.4"
version "0.5.12"
dependencies:
react-native-gifted-chat "^2.1.0"
react-native-gifted-chat "npm:@qte/react-native-gifted-chat@^2.7.14"

"@react-native-community/cli-clean@^10.1.1":
version "10.1.1"
Expand Down Expand Up @@ -6294,12 +6294,12 @@ react-native-communications@^2.2.1:
resolved "https://registry.yarnpkg.com/react-native-communications/-/react-native-communications-2.2.1.tgz#7883b56b20a002eeb790c113f8616ea8692ca795"
integrity sha512-5+C0X9mopI0+qxyQHzOPEi5v5rxNBQjxydPPiKMQSlX1RBIcJ8uTcqUPssQ9Mo8p6c1IKIWJUSqCj4jAmD0qVQ==

react-native-gifted-chat@^2.1.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/react-native-gifted-chat/-/react-native-gifted-chat-2.8.1.tgz#92380aceb56024103de7c0003e3b43572c5a5641"
integrity sha512-x4Kq0YvmaHqQg/ENAmFzwcjJyH31cGrCWETFzUMmTZgWsXkkiJ1MamTnkLGQp6deVxM6G0QNxrF7IZnBOeMbsw==
"react-native-gifted-chat@npm:@qte/react-native-gifted-chat@^2.7.14":
version "2.7.14"
resolved "https://registry.yarnpkg.com/@qte/react-native-gifted-chat/-/react-native-gifted-chat-2.7.14.tgz#29f7e77af796af43c23c91759d660a0dea73fe1c"
integrity sha512-7is52zdzOCi7XoUGzyqgxvvH61/cVLvS4vuw97MW6mf5qbIxVKS+tndCVWoDTfK5hZMNpMi/lNBKABzEKRQuXw==
dependencies:
"@expo/react-native-action-sheet" "^4.1.1"
"@expo/react-native-action-sheet" "^4.1.0"
"@types/lodash.isequal" "^4.5.8"
dayjs "^1.11.13"
lodash.isequal "^4.5.0"
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@
"access": "public"
},
"devDependencies": {
"@commitlint/config-conventional": "^17.0.2",
"@commitlint/config-angular": "^17.0.2",
"@evilmartians/lefthook": "^1.2.2",
"@react-native-community/eslint-config": "^3.0.2",
"@react-native-firebase/app": "^22.0.0",
"@react-native-firebase/firestore": "^22.0.0",
"@release-it/conventional-changelog": "^5.0.0",
"@shopify/flash-list": "^2.0.1",
"@types/jest": "^28.1.2",
"@types/react": "18.2.0",
"commitlint": "^17.0.2",
Expand All @@ -83,7 +84,8 @@
"@react-native-firebase/app": "^22.0.0",
"@react-native-firebase/firestore": "^22.0.0",
"react": "*",
"react-native": "*"
"react-native": "*",
"@shopify/flash-list": "^2.0.1"
},
"engines": {
"node": ">= 16.0.0"
Expand All @@ -97,7 +99,7 @@
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
"@commitlint/config-angular"
]
},
"release-it": {
Expand Down Expand Up @@ -162,6 +164,6 @@
]
},
"dependencies": {
"react-native-gifted-chat": "^2.1.0"
"react-native-gifted-chat": "npm:@qte/react-native-gifted-chat@^2.7.14"
}
}
157 changes: 69 additions & 88 deletions src/CuteChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
query,
startAfter,
} from '@react-native-firebase/firestore';
import { FlashListRef } from '@shopify/flash-list';
import React, {
useCallback,
useEffect,
Expand All @@ -21,22 +22,13 @@ import React, {
useRef,
useState,
} from 'react';
import {
Alert,
FlatList,
NativeScrollEvent,
StyleProp,
ViewStyle,
ScrollViewProps,
FlatListProps,
} from 'react-native';
import type { IMessage } from 'react-native-gifted-chat';
import { GiftedChat, GiftedChatProps } from 'react-native-gifted-chat';
import { Alert, StyleProp, ViewStyle } from 'react-native';
import { GiftedChat, IMessage } from 'react-native-gifted-chat';
import { GiftedChatProps } from 'react-native-gifted-chat/lib/GiftedChat/types';
import { ChatFooter } from './components/ChatFooter/ChatFooter';
import { appendSnapshot } from './utils/appendSnapshot';
import { prepareSnapshot } from './utils/prepareSnapshot';
import { isCloseToBottom } from './utils/isCloseToBottom';
import { isCloseToTop } from './utils/isCloseToTop';
import { ChatFooter } from './components/ChatFooter/ChatFooter';
import { prepareSnapshot } from './utils/prepareSnapshot';

interface CustomCuteChatProps {
chatId: string;
Expand All @@ -45,8 +37,6 @@ interface CustomCuteChatProps {
setIsLoading?: (isLoading: boolean) => void;
newMessagesBannerComponent?: () => React.ReactNode;
newMessagesBannerStyles?: StyleProp<ViewStyle>;
maintainVisibleContentPosition?: ScrollViewProps['maintainVisibleContentPosition'];
getItemLayout?: FlatListProps<IMessage>['getItemLayout'];
}

interface User {
Expand All @@ -56,10 +46,13 @@ interface User {
avatar: string;
}

type CuteChatProps = Omit<GiftedChatProps, 'messages' | 'user' | 'onSend'> &
export type CuteChatProps = Omit<
GiftedChatProps<IMessage>,
'messages' | 'user' | 'onSend'
> &
CustomCuteChatProps;

type CuteChatRef = {
export type CuteChatRef = {
scrollToMessage: (messageId: string) => Promise<void>;
};

Expand All @@ -71,19 +64,23 @@ export const CuteChat = React.forwardRef<CuteChatRef, CuteChatProps>(function (
) {
const { chatId, user, setIsLoading } = props;

const [closeToTop, setCloseToTop] = useState(true);
const [closeToBottom, setCloseToBottom] = useState(true);
const [messages, setMessages] = useState<IMessage[]>([]);
const [loading, setLoading] = useState(false);
const [initializing, setInitializing] = useState(true);
const [lastMessageDoc, setLastMessageDoc] =
useState<FirebaseFirestoreTypes.DocumentData | null>(null);
const [hasNewMessages, setHasNewMessages] = useState(false);
const [scrollToIndex, setScrollToIndex] = useState<number | null>(null);
const [scrollToMessageId, setScrollToMessageId] = useState<string | null>(
null
);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [allMessagesLoaded, setAllMessagesLoaded] = useState(false);

const memoizedUser = useMemo(() => ({ _id: user.id, ...user }), [user]);
const startDate = useMemo(() => new Date(), []);

const chatListRef = useRef<FlatList<IMessage>>(null);
const chatListRef = useRef<FlashListRef<IMessage>>(null);
const timeout = React.useRef<ReturnType<typeof setTimeout>>();

const setIsLoadingBool = useCallback(
Expand Down Expand Up @@ -271,7 +268,7 @@ export const CuteChat = React.forwardRef<CuteChatRef, CuteChatProps>(function (
);
}

setIsLoadingBool(true);
setIsLoadingEarlier(true);
try {
console.log('Fetching more messages...');
const messagesRef = collection(
Expand All @@ -292,28 +289,27 @@ export const CuteChat = React.forwardRef<CuteChatRef, CuteChatProps>(function (
setMessages((old) => appendSnapshot(old, snapshotChanges));
} else {
console.log('Snapshot empty');
setAllMessagesLoaded(true);
}

setIsLoadingBool(false);
});
} catch (error) {
console.error('Error fetching more messages: ', error);
} finally {
setIsLoadingEarlier(false);
}
}, [chatId, lastMessageDoc, setIsLoadingBool, initializing, loading]);
}, [chatId, lastMessageDoc, setIsLoadingEarlier, initializing, loading]);

const scrollToMessage = useCallback(
async (messageId: string) => {
console.log('Scrolling to message:', messageId);

const messageIndex = messages.findIndex(
(message) => message._id === messageId
);
const message = messages.find((m) => m._id === messageId);

if (messageIndex !== -1) {
console.log('Message found at index:', messageIndex);
chatListRef.current?.scrollToIndex({
index: messageIndex,
animated: true,
if (message !== undefined) {
console.log('Message found:', message);
chatListRef.current?.scrollToItem({
item: message,
viewPosition: 0.5, // Center the item in the view
});

return;
Expand Down Expand Up @@ -341,28 +337,13 @@ export const CuteChat = React.forwardRef<CuteChatRef, CuteChatProps>(function (
onSnapshot(scrollToMessageQuery, async (snapshot) => {
if (!snapshot.empty) {
const snapshotChanges = await prepareSnapshot(snapshot, chatId);
let newMessageIndex = -1;
setMessages((old) => {
const newMessages = appendSnapshot(old, snapshotChanges);
newMessageIndex = newMessages.findIndex(
(message) => message._id === messageId
);
console.log('Message index after fetching:', newMessageIndex);
return newMessages;
});

if (newMessageIndex !== -1) {
console.log('New message found at index:', newMessageIndex);
setScrollToIndex(newMessageIndex);
chatListRef.current?.scrollToIndex({
index: newMessageIndex,
animated: true,
});
} else {
console.warn(
`New message with ID ${messageId} not found in messages after fetching`
);
}
console.log('Setting scrollToMessageId:', messageId);
setScrollToMessageId(messageId);
} else {
console.warn('No messages found after fetching');
}
Expand Down Expand Up @@ -409,6 +390,36 @@ export const CuteChat = React.forwardRef<CuteChatRef, CuteChatProps>(function (
};
});

useEffect(() => {
console.log('scrollToMessageId changed:', scrollToMessageId);
if (scrollToMessageId === null || chatListRef.current === null) {
return;
}

// Clear any existing timeout to prevent multiple scrolls
if (timeout.current) {
clearTimeout(timeout.current);
}

const message = messages.find((msg) => msg._id === scrollToMessageId);

if (!message) {
console.warn(`Message with ID ${scrollToMessageId} not found.`);
setScrollToMessageId(null);
return;
}

// Use a timeout to ensure the scroll happens after the component has rendered
timeout.current = setTimeout(() => {
console.log('Scrolling to message', message);
chatListRef.current?.scrollToItem({
item: message,
viewPosition: 0.5, // Center the item in the view
});
setScrollToMessageId(null); // Reset after scrolling
}, 500);
}, [scrollToMessageId, messages]);

// Clear the timeout if it still exists when the component unmounts.
React.useEffect(() => {
return () => timeout.current && clearTimeout(timeout.current);
Expand All @@ -426,7 +437,7 @@ export const CuteChat = React.forwardRef<CuteChatRef, CuteChatProps>(function (
scrollToBottomStyle={props.scrollToBottomStyle}
hasNewMessages={hasNewMessages}
markNewMessagesAsSeen={() => setHasNewMessages(false)}
closeToTop={closeToTop}
closeToBottom={closeToBottom}
chatRef={chatListRef}
/>
{props.renderChatFooter?.()}
Expand All @@ -435,44 +446,14 @@ export const CuteChat = React.forwardRef<CuteChatRef, CuteChatProps>(function (
messages={messages}
onSend={props.onSend || onSend}
user={memoizedUser}
messageContainerRef={chatListRef}
onLoadEarlier={fetchMoreMessages}
loadEarlier={!allMessagesLoaded}
infiniteScroll={true}
isLoadingEarlier={isLoadingEarlier}
inverted={true}
listViewProps={{
ref: chatListRef,
onScroll: ({ nativeEvent }: { nativeEvent: NativeScrollEvent }) => {
if (isCloseToBottom(nativeEvent)) fetchMoreMessages();

if (isCloseToTop(nativeEvent)) setCloseToTop(true);
else setCloseToTop(false);
},
scrollEventThrottle: 500,
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
getItemLayout: props.getItemLayout,
onScrollToIndexFailed: (info: {
index: number;
highestMeasuredFrameIndex: number;
averageItemLength: number;
}) => {
console.log('onScrollToIndexFailed', info);

// Calculate the possible position of the item and scroll there using the internal scroll responder.
const offset = info.averageItemLength * info.index;
chatListRef.current?.scrollToOffset({
animated: true,
offset: offset,
});

// If we know exactly where we want to scroll to, we can just scroll now since the item is likely visible.
// Otherwise it'll call this function recursively again.
if (scrollToIndex) {
timeout.current = setTimeout(() => {
console.log('Retrying scroll to index:', scrollToIndex);
chatListRef.current?.scrollToIndex({
index: scrollToIndex,
animated: true,
});
}, 100);
}
},
handleOnScroll={({ nativeEvent }) => {
setCloseToBottom(isCloseToBottom(nativeEvent));
}}
/>
);
Expand Down
15 changes: 8 additions & 7 deletions src/components/ChatFooter/ChatFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import React, { ReactNode, RefObject } from 'react';
import { FlatList, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { IMessage } from 'react-native-gifted-chat';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { RightSection } from './RightSection';
import { MiddleSection } from './MiddleSection';
import { LeftSection } from './LeftSection';
import { FlashListRef } from '@shopify/flash-list';
import { IMessage } from 'react-native-gifted-chat';

export const ChatFooter = (props: {
newMessagesBannerComponent?: () => ReactNode;
newMessagesBannerStyles?: StyleProp<ViewStyle>;
scrollToBottomComponent?: () => ReactNode;
scrollToBottomStyle?: StyleProp<ViewStyle>;

closeToTop: boolean;
closeToBottom: boolean;
hasNewMessages: boolean;
markNewMessagesAsSeen: () => void;
chatRef: RefObject<FlatList<IMessage>>;
chatRef: RefObject<FlashListRef<IMessage>>;
}) => {
const scrollToBottom = () => {
props.chatRef.current?.scrollToOffset({ offset: 0, animated: true });
props.chatRef.current?.scrollToEnd();
};

const scrollDownAndMarkAsRead = () => {
Expand All @@ -33,15 +34,15 @@ export const ChatFooter = (props: {
newMessagesBannerComponent={props.newMessagesBannerComponent}
newMessagesBannerStyles={props.newMessagesBannerStyles}
onNewMessagesBannerPress={scrollDownAndMarkAsRead}
closeToTop={props.closeToTop}
closeToBottom={props.closeToBottom}
hasNewMessages={props.hasNewMessages}
/>

<RightSection
scrollToBottom={scrollDownAndMarkAsRead}
scrollToBottomComponent={props.scrollToBottomComponent}
scrollToBottomStyle={props.scrollToBottomStyle}
closeToTop={props.closeToTop}
closeToBottom={props.closeToBottom}
/>
</View>
);
Expand Down
Loading