1- import { useState , useEffect , useContext , createContext , useMemo } from 'react' ;
1+ import { useState , useEffect , useContext , createContext , useMemo , useCallback } from 'react' ;
22import {
33 collection ,
44 query ,
55 where ,
66 orderBy ,
77 onSnapshot ,
88 limit ,
9- Timestamp
9+ Timestamp ,
10+ getDocs ,
11+ startAfter ,
12+ QueryDocumentSnapshot
1013} from 'firebase/firestore' ;
1114import { db } from '@lib/firebase/app' ;
1215import {
@@ -25,18 +28,23 @@ import type { ReactNode } from 'react';
2528import type { Chat } from '@lib/types/chat' ;
2629import type { Message } from '@lib/types/message' ;
2730
31+ const MESSAGES_PER_PAGE = 30 ;
32+
2833type ChatContext = {
2934 chats : Chat [ ] | null ;
3035 currentChat : Chat | null ;
3136 messages : Message [ ] | null ;
3237 loading : boolean ;
3338 error : Error | null ;
39+ hasMoreMessages : boolean ;
40+ loadingOlderMessages : boolean ;
3441 setCurrentChat : ( chat : Chat | null ) => void ;
35- createNewChat : ( participants : string [ ] , name ?: string ) => Promise < string > ;
42+ createNewChat : ( participants : string [ ] , name ?: string , description ?: string ) => Promise < string > ;
3643 sendNewMessage : ( text : string ) => Promise < void > ;
3744 markAsRead : ( messageId : string ) => Promise < void > ;
3845 addParticipant : ( userId : string ) => Promise < void > ;
3946 removeParticipant : ( userId : string ) => Promise < void > ;
47+ loadOlderMessages : ( ) => Promise < void > ;
4048} ;
4149
4250const ChatContext = createContext < ChatContext | null > ( null ) ;
@@ -51,15 +59,41 @@ export function ChatContextProvider({
5159 const { user } = useAuth ( ) ;
5260 const [ chats , setChats ] = useState < Chat [ ] | null > ( null ) ;
5361 const [ currentChat , setCurrentChat ] = useState < Chat | null > ( null ) ;
54- const [ messages , setMessages ] = useState < Message [ ] | null > ( null ) ;
62+ const [ realtimeMessages , setRealtimeMessages ] = useState < Message [ ] | null > ( null ) ;
63+ const [ olderMessages , setOlderMessages ] = useState < Message [ ] > ( [ ] ) ;
5564 const [ loading , setLoading ] = useState ( true ) ;
5665 const [ error , setError ] = useState < Error | null > ( null ) ;
66+ const [ hasMoreMessages , setHasMoreMessages ] = useState ( true ) ;
67+ const [ loadingOlderMessages , setLoadingOlderMessages ] = useState ( false ) ;
68+ const [ oldestDocSnapshot , setOldestDocSnapshot ] = useState < QueryDocumentSnapshot | null > ( null ) ;
69+
70+ // Merge realtime + older messages, deduplicating by id
71+ const messages = useMemo ( ( ) => {
72+ if ( ! realtimeMessages ) return null ;
73+
74+ const messageMap = new Map < string , Message > ( ) ;
75+
76+ // Add older messages first
77+ for ( const msg of olderMessages ) {
78+ messageMap . set ( msg . id , msg ) ;
79+ }
80+
81+ // Realtime messages overwrite any overlapping older ones
82+ for ( const msg of realtimeMessages ) {
83+ messageMap . set ( msg . id , msg ) ;
84+ }
85+
86+ // Sort descending by createdAt (matching the existing convention; UI reverses for display)
87+ return Array . from ( messageMap . values ( ) ) . sort ( ( a , b ) => {
88+ const aTime = a . createdAt ?. toMillis ?.( ) ?? 0 ;
89+ const bTime = b . createdAt ?. toMillis ?.( ) ?? 0 ;
90+ return bTime - aTime ;
91+ } ) ;
92+ } , [ realtimeMessages , olderMessages ] ) ;
5793
5894 // Listen to user's chats
5995 useEffect ( ( ) => {
6096 if ( ! user ) {
61- // setChats(null);
62- // setLoading(false);
6397 setChats ( [
6498 {
6599 id : 'dummy-chat-1' ,
@@ -82,7 +116,7 @@ export function ChatContextProvider({
82116 updatedAt : Timestamp . fromDate ( new Date ( ) ) ,
83117 lastMessage : {
84118 senderId : 'user_4' ,
85- text : ' Let’ s meet tomorrow.' ,
119+ text : " Let' s meet tomorrow." ,
86120 timestamp : Timestamp . fromDate ( new Date ( ) )
87121 } ,
88122 name : 'Project Team'
@@ -103,31 +137,23 @@ export function ChatContextProvider({
103137 ( snapshot ) => {
104138 const chatsData = snapshot . docs . map ( ( doc ) => doc . data ( ) ) ;
105139
106- // Sort chats by most recent activity (most recent first)
107- // Priority: lastMessage timestamp > updatedAt > createdAt
108140 const sortedChats = chatsData . sort ( ( a , b ) => {
109- // Get the most recent activity timestamp for each chat
110141 const getMostRecentTimestamp = ( chat : typeof a ) : number => {
111- // Priority 1: lastMessage timestamp (most recent activity)
112142 if ( chat . lastMessage ?. timestamp ) {
113143 return chat . lastMessage . timestamp . toMillis ( ) ;
114144 }
115- // Priority 2: updatedAt (for updated chats without messages)
116145 if ( chat . updatedAt ) {
117146 return chat . updatedAt . toMillis ( ) ;
118147 }
119- // Priority 3: createdAt (for new chats)
120148 if ( chat . createdAt ) {
121149 return chat . createdAt . toMillis ( ) ;
122150 }
123- // Fallback: 0 for chats with no timestamps
124151 return 0 ;
125152 } ;
126153
127154 const aTimestamp = getMostRecentTimestamp ( a ) ;
128155 const bTimestamp = getMostRecentTimestamp ( b ) ;
129156
130- // Sort by most recent timestamp (descending)
131157 return bTimestamp - aTimestamp ;
132158 } ) ;
133159
@@ -147,24 +173,43 @@ export function ChatContextProvider({
147173 } ;
148174 } , [ user ] ) ;
149175
150- // Listen to current chat messages
176+ // Listen to current chat messages (realtime — most recent batch)
151177 useEffect ( ( ) => {
152178 if ( ! currentChat ) {
153- setMessages ( null ) ;
179+ setRealtimeMessages ( null ) ;
180+ setOlderMessages ( [ ] ) ;
181+ setHasMoreMessages ( true ) ;
182+ setOldestDocSnapshot ( null ) ;
154183 return ;
155184 }
156185
186+ // Reset pagination state on chat change
187+ setOlderMessages ( [ ] ) ;
188+ setHasMoreMessages ( true ) ;
189+ setOldestDocSnapshot ( null ) ;
190+
157191 const messagesQuery = query (
158192 chatMessagesCollection ( currentChat . id ) ,
159193 orderBy ( 'createdAt' , 'desc' ) ,
160- limit ( 50 )
194+ limit ( MESSAGES_PER_PAGE )
161195 ) ;
162196
163197 const unsubscribe = onSnapshot (
164198 messagesQuery ,
165199 ( snapshot ) => {
166200 const messagesData = snapshot . docs . map ( ( doc ) => doc . data ( ) ) ;
167- setMessages ( messagesData ) ;
201+ setRealtimeMessages ( messagesData ) ;
202+
203+ // Store the oldest doc snapshot as cursor for pagination (only on first load)
204+ if ( snapshot . docs . length > 0 ) {
205+ const lastDoc = snapshot . docs [ snapshot . docs . length - 1 ] ;
206+ setOldestDocSnapshot ( ( prev ) => prev ?? lastDoc ) ;
207+ }
208+
209+ // If we got fewer than the limit, there are no more messages
210+ if ( snapshot . docs . length < MESSAGES_PER_PAGE ) {
211+ setHasMoreMessages ( false ) ;
212+ }
168213 } ,
169214 ( error ) => {
170215 setError ( error as Error ) ;
@@ -176,6 +221,38 @@ export function ChatContextProvider({
176221 } ;
177222 } , [ currentChat ] ) ;
178223
224+ const loadOlderMessages = useCallback ( async ( ) : Promise < void > => {
225+ if ( ! currentChat || ! hasMoreMessages || loadingOlderMessages || ! oldestDocSnapshot ) return ;
226+
227+ setLoadingOlderMessages ( true ) ;
228+
229+ try {
230+ const olderQuery = query (
231+ chatMessagesCollection ( currentChat . id ) ,
232+ orderBy ( 'createdAt' , 'desc' ) ,
233+ startAfter ( oldestDocSnapshot ) ,
234+ limit ( MESSAGES_PER_PAGE )
235+ ) ;
236+
237+ const snapshot = await getDocs ( olderQuery ) ;
238+ const olderData = snapshot . docs . map ( ( doc ) => doc . data ( ) ) ;
239+
240+ if ( olderData . length > 0 ) {
241+ setOlderMessages ( ( prev ) => [ ...prev , ...olderData ] ) ;
242+ setOldestDocSnapshot ( snapshot . docs [ snapshot . docs . length - 1 ] ) ;
243+ }
244+
245+ if ( olderData . length < MESSAGES_PER_PAGE ) {
246+ setHasMoreMessages ( false ) ;
247+ }
248+ } catch ( error ) {
249+ console . error ( 'Error loading older messages:' , error ) ;
250+ setError ( error as Error ) ;
251+ }
252+
253+ setLoadingOlderMessages ( false ) ;
254+ } , [ currentChat , hasMoreMessages , loadingOlderMessages , oldestDocSnapshot ] ) ;
255+
179256 const createNewChat = async (
180257 participants : string [ ] ,
181258 name ?: string ,
@@ -249,12 +326,15 @@ export function ChatContextProvider({
249326 messages,
250327 loading,
251328 error,
329+ hasMoreMessages,
330+ loadingOlderMessages,
252331 setCurrentChat,
253332 createNewChat,
254333 sendNewMessage,
255334 markAsRead,
256335 addParticipant,
257- removeParticipant
336+ removeParticipant,
337+ loadOlderMessages
258338 } ;
259339
260340 return (
0 commit comments