From fa8d7e6aa023dfb8d70f8cda707e01ccfb63cee1 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 22 Oct 2025 01:49:09 +0530 Subject: [PATCH 1/2] feat: chat pagination --- .../routes/(protected)/messages/+page.svelte | 180 ++++++++++++++++-- 1 file changed, 167 insertions(+), 13 deletions(-) diff --git a/platforms/pictique/src/routes/(protected)/messages/+page.svelte b/platforms/pictique/src/routes/(protected)/messages/+page.svelte index 8526809e..c0697196 100644 --- a/platforms/pictique/src/routes/(protected)/messages/+page.svelte +++ b/platforms/pictique/src/routes/(protected)/messages/+page.svelte @@ -21,16 +21,36 @@ let groupName = $state(''); let debounceTimer: NodeJS.Timeout; - async function loadMessages() { + // Pagination and loading state + let isLoading = $state(true); + let currentPage = $state(1); + let totalPages = $state(1); + let totalChats = $state(0); + let hasMorePages = $state(false); + + async function loadMessages(page = 1, append = false) { try { - const { data } = await apiClient.get<{ chats: Chat[] }>('/api/chats'); + isLoading = true; + const { data } = await apiClient.get<{ + chats: Chat[]; + total: number; + page: number; + totalPages: number; + }>(`/api/chats?page=${page}&limit=10`); + const { data: userData } = await apiClient.get('/api/users'); currentUserId = userData.id; console.log('Raw chat data from API:', data.chats); - // Show all chats (direct messages and groups) in one unified list - messages = data.chats.map((c) => { + // Update pagination info + currentPage = data.page; + totalPages = data.totalPages; + totalChats = data.total; + hasMorePages = data.page < data.totalPages; + + // Transform chats to messages + const newMessages = data.chats.map((c) => { const members = c.participants.filter((u) => u.id !== userData.id); const memberNames = members.map((m) => m.name ?? m.handle ?? m.ename); const isGroup = members.length > 1; @@ -57,8 +77,35 @@ name: displayName }; }); + + // Append or replace messages based on pagination + if (append) { + messages = [...messages, ...newMessages]; + } else { + messages = newMessages; + } } catch (error) { console.error('Failed to load messages:', error); + } finally { + isLoading = false; + } + } + + async function loadNextPage() { + if (hasMorePages && !isLoading) { + await loadMessages(currentPage + 1, true); + } + } + + async function loadPreviousPage() { + if (currentPage > 1 && !isLoading) { + await loadMessages(currentPage - 1, false); + } + } + + async function goToPage(page: number) { + if (page >= 1 && page <= totalPages && !isLoading) { + await loadMessages(page, false); } } @@ -197,7 +244,7 @@ // Navigate to messages and refresh the feed goto('/messages'); // Refresh the messages to show the newly created group - await loadMessages(); + await loadMessages(1, false); } catch (err) { console.error('Failed to create group:', err); alert('Failed to create group. Please try again.'); @@ -218,7 +265,14 @@ - {#if messages.length > 0} + {#if isLoading && messages.length === 0} +
+
+ Loading chats... +
+ {:else if messages.length > 0} {#each messages as message} {/each} - {/if} - {#if messages.length === 0} + + {#if totalPages > 1} +
+ + +
+ {#if totalPages <= 7} + {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page} + + {/each} + {:else} + + + + {#if currentPage > 3} + ... + {/if} + + + {#each Array.from({ length: Math.min(3, totalPages - 2) }, (_, i) => { + const start = Math.max(2, currentPage - 1); + return Math.min(start + i, totalPages - 1); + }).filter((page, index, arr) => arr.indexOf(page) === index) as page} + + {/each} + + {#if currentPage < totalPages - 2} + ... + {/if} + + + {#if totalPages > 1} + + {/if} + {/if} +
+ + +
+ +
+ Page {currentPage} of {totalPages} • {totalChats} total chats +
+ {/if} + {:else if !isLoading}
You don't have any messages yet. Start a Direct Message by searching a name.
{/if} {#if openNewChatModal} -
+
@@ -250,6 +393,7 @@ @@ -348,7 +494,7 @@ {#if openNewGroupModal} -
+
@@ -357,6 +503,7 @@ From d1f3af233bc5b59406653f00b85d2a295dbdf663 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 22 Oct 2025 01:56:24 +0530 Subject: [PATCH 2/2] feat: blabsy reodering by last chat --- platforms/blabsy/firestore.indexes.json | 14 ++++++ .../blabsy/src/components/chat/chat-list.tsx | 20 ++++---- platforms/blabsy/src/components/chat/chat.tsx | 2 +- .../blabsy/src/lib/context/chat-context.tsx | 19 ++++++- .../pictique-api/src/services/ChatService.ts | 50 +++++++++++++++---- .../src/services/MessageService.ts | 13 ++++- 6 files changed, 95 insertions(+), 23 deletions(-) diff --git a/platforms/blabsy/firestore.indexes.json b/platforms/blabsy/firestore.indexes.json index e60d2a14..00b8636e 100644 --- a/platforms/blabsy/firestore.indexes.json +++ b/platforms/blabsy/firestore.indexes.json @@ -83,6 +83,20 @@ "order": "ASCENDING" } ] + }, + { + "collectionGroup": "chats", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "participants", + "arrayConfig": "CONTAINS" + }, + { + "fieldPath": "updatedAt", + "order": "DESCENDING" + } + ] } ], "fieldOverrides": [ diff --git a/platforms/blabsy/src/components/chat/chat-list.tsx b/platforms/blabsy/src/components/chat/chat-list.tsx index ccab5e0f..9f340e6c 100644 --- a/platforms/blabsy/src/components/chat/chat-list.tsx +++ b/platforms/blabsy/src/components/chat/chat-list.tsx @@ -92,8 +92,8 @@ export function ChatList(): JSX.Element { } return ( -
-
+
+
{chats.map((chat) => { const otherParticipant = chat.participants.find( (p) => p !== user?.id @@ -160,13 +160,15 @@ export function ChatList(): JSX.Element { ); })}
- +
+ +
setOpenCreateNewChatModal(false)} diff --git a/platforms/blabsy/src/components/chat/chat.tsx b/platforms/blabsy/src/components/chat/chat.tsx index 5d690d3b..1b7d24af 100644 --- a/platforms/blabsy/src/components/chat/chat.tsx +++ b/platforms/blabsy/src/components/chat/chat.tsx @@ -5,7 +5,7 @@ export function Chat(): JSX.Element { return (
-
+
diff --git a/platforms/blabsy/src/lib/context/chat-context.tsx b/platforms/blabsy/src/lib/context/chat-context.tsx index 4d6801db..e2fae190 100644 --- a/platforms/blabsy/src/lib/context/chat-context.tsx +++ b/platforms/blabsy/src/lib/context/chat-context.tsx @@ -94,14 +94,29 @@ export function ChatContextProvider({ const chatsQuery = query( chatsCollection, - where('participants', 'array-contains', user.id) + where('participants', 'array-contains', user.id), + orderBy('updatedAt', 'desc') ); const unsubscribe = onSnapshot( chatsQuery, (snapshot) => { const chatsData = snapshot.docs.map((doc) => doc.data()); - setChats(chatsData); + + // Sort chats by last message timestamp (most recent first) + const sortedChats = chatsData.sort((a, b) => { + // If both have lastMessage, sort by timestamp + if (a.lastMessage?.timestamp && b.lastMessage?.timestamp) { + return b.lastMessage.timestamp.toMillis() - a.lastMessage.timestamp.toMillis(); + } + // If only one has lastMessage, prioritize it + if (a.lastMessage?.timestamp && !b.lastMessage?.timestamp) return -1; + if (!a.lastMessage?.timestamp && b.lastMessage?.timestamp) return 1; + // If neither has lastMessage, sort by updatedAt + return b.updatedAt.toMillis() - a.updatedAt.toMillis(); + }); + + setChats(sortedChats); setLoading(false); }, (error) => { diff --git a/platforms/pictique-api/src/services/ChatService.ts b/platforms/pictique-api/src/services/ChatService.ts index 2111ba8c..e3172bdd 100644 --- a/platforms/pictique-api/src/services/ChatService.ts +++ b/platforms/pictique-api/src/services/ChatService.ts @@ -162,6 +162,11 @@ export class ChatService { }); const savedMessage = await this.messageRepository.save(message); + + // Update the chat's updatedAt timestamp to reflect the latest message + chat.updatedAt = new Date(); + await this.chatRepository.save(chat); + console.log("Sent event", `chat:${chatId}`); this.eventEmitter.emit(`chat:${chatId}`, [savedMessage]); @@ -280,20 +285,32 @@ export class ChatService { page: number; totalPages: number; }> { - // First, get the chat IDs that the user is part of - const [chatIds, total] = await this.chatRepository + // Get chats ordered by the most recent message timestamp + const queryBuilder = this.chatRepository .createQueryBuilder("chat") - .select(["chat.id", "chat.updatedAt"]) + .leftJoin("chat.messages", "message") .innerJoin("chat.participants", "participants") .where("participants.id = :userId", { userId }) - .orderBy("chat.updatedAt", "DESC") + .groupBy("chat.id") + .addGroupBy("chat.name") + .addGroupBy("chat.ename") + .addGroupBy("chat.createdAt") + .addGroupBy("chat.updatedAt") + .orderBy("MAX(message.createdAt)", "DESC") + .addOrderBy("chat.createdAt", "DESC"); // Fallback for chats without messages + + // Get total count for pagination + const total = await queryBuilder.getCount(); + + // Apply pagination + const chats = await queryBuilder .skip((page - 1) * limit) .take(limit) - .getManyAndCount(); + .getMany(); - // Then, load the full chat data with all relations - const chats = await this.chatRepository.find({ - where: { id: In(chatIds.map((chat) => chat.id)) }, + // Load full chat data with all relations for the paginated results + const chatsWithRelations = await this.chatRepository.find({ + where: { id: In(chats.map((chat) => chat.id)) }, relations: [ "participants", "messages", @@ -301,12 +318,25 @@ export class ChatService { "messages.readStatuses", "messages.readStatuses.user", ], - order: { updatedAt: "DESC" }, + }); + + // Sort the chats by latest message timestamp (since we loaded relations, we need to sort again) + const sortedChats = chatsWithRelations.sort((a, b) => { + const aLatestMessage = a.messages[a.messages.length - 1]; + const bLatestMessage = b.messages[b.messages.length - 1]; + + if (!aLatestMessage && !bLatestMessage) { + return b.createdAt.getTime() - a.createdAt.getTime(); + } + if (!aLatestMessage) return 1; + if (!bLatestMessage) return -1; + + return bLatestMessage.createdAt.getTime() - aLatestMessage.createdAt.getTime(); }); // For each chat, get the latest message and its read status const chatsWithLatestMessage = await Promise.all( - chats.map(async (chat) => { + sortedChats.map(async (chat) => { const latestMessage = chat.messages[chat.messages.length - 1]; if (!latestMessage) { return { ...chat, latestMessage: undefined }; diff --git a/platforms/pictique-api/src/services/MessageService.ts b/platforms/pictique-api/src/services/MessageService.ts index a2732876..7e024673 100644 --- a/platforms/pictique-api/src/services/MessageService.ts +++ b/platforms/pictique-api/src/services/MessageService.ts @@ -1,8 +1,10 @@ import { AppDataSource } from "../database/data-source"; import { Message } from "../database/entities/Message"; +import { Chat } from "../database/entities/Chat"; export class MessageService { public messageRepository = AppDataSource.getRepository(Message); + private chatRepository = AppDataSource.getRepository(Chat); async findById(id: string): Promise { return await this.messageRepository.findOneBy({ id }); @@ -17,7 +19,16 @@ export class MessageService { isArchived: false }); - return await this.messageRepository.save(message); + const savedMessage = await this.messageRepository.save(message); + + // Update the chat's updatedAt timestamp to reflect the latest message + const chat = await this.chatRepository.findOneBy({ id: chatId }); + if (chat) { + chat.updatedAt = new Date(); + await this.chatRepository.save(chat); + } + + return savedMessage; } async createSystemMessage(chatId: string, text: string): Promise {