Skip to content

Commit 757e768

Browse files
authored
feat: chat pagination (#378)
* feat: chat pagination * feat: blabsy reodering by last chat
1 parent 60cb73a commit 757e768

File tree

7 files changed

+262
-36
lines changed

7 files changed

+262
-36
lines changed

platforms/blabsy/firestore.indexes.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@
8383
"order": "ASCENDING"
8484
}
8585
]
86+
},
87+
{
88+
"collectionGroup": "chats",
89+
"queryScope": "COLLECTION",
90+
"fields": [
91+
{
92+
"fieldPath": "participants",
93+
"arrayConfig": "CONTAINS"
94+
},
95+
{
96+
"fieldPath": "updatedAt",
97+
"order": "DESCENDING"
98+
}
99+
]
86100
}
87101
],
88102
"fieldOverrides": [

platforms/blabsy/src/components/chat/chat-list.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ export function ChatList(): JSX.Element {
9292
}
9393

9494
return (
95-
<div className='flex h-full flex-col gap-4'>
96-
<div className='flex h-full flex-col gap-2 overflow-y-auto p-4'>
95+
<div className='flex h-full flex-col'>
96+
<div className='flex-1 overflow-y-auto p-4 space-y-2'>
9797
{chats.map((chat) => {
9898
const otherParticipant = chat.participants.find(
9999
(p) => p !== user?.id
@@ -160,13 +160,15 @@ export function ChatList(): JSX.Element {
160160
);
161161
})}
162162
</div>
163-
<button
164-
type='button'
165-
onClick={() => setOpenCreateNewChatModal(true)}
166-
className='flex items-center justify-center gap-3 bg-main-accent rounded-lg p-3 transition-colors hover:brightness-90 mx-4 mb-4'
167-
>
168-
New Chat
169-
</button>
163+
<div className='flex-shrink-0 p-4'>
164+
<button
165+
type='button'
166+
onClick={() => setOpenCreateNewChatModal(true)}
167+
className='w-full flex items-center justify-center gap-3 bg-main-accent rounded-lg p-3 transition-colors hover:brightness-90'
168+
>
169+
New Chat
170+
</button>
171+
</div>
170172
<AddMembers
171173
open={openCreateNewChatModal}
172174
onClose={() => setOpenCreateNewChatModal(false)}

platforms/blabsy/src/components/chat/chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function Chat(): JSX.Element {
55
return (
66
<main className='min-h-full w-full max-w-5xl pt-8'>
77
<div className='grid h-[calc(100vh-4rem)] grid-cols-1 gap-4 md:grid-cols-[350px_1fr]'>
8-
<div className='max-h-full rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-black'>
8+
<div className='h-full rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-black flex flex-col'>
99
<ChatList />
1010
</div>
1111
<div className='h-[calc(100vh-4rem)] rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-black'>

platforms/blabsy/src/lib/context/chat-context.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,29 @@ export function ChatContextProvider({
9494

9595
const chatsQuery = query(
9696
chatsCollection,
97-
where('participants', 'array-contains', user.id)
97+
where('participants', 'array-contains', user.id),
98+
orderBy('updatedAt', 'desc')
9899
);
99100

100101
const unsubscribe = onSnapshot(
101102
chatsQuery,
102103
(snapshot) => {
103104
const chatsData = snapshot.docs.map((doc) => doc.data());
104-
setChats(chatsData);
105+
106+
// Sort chats by last message timestamp (most recent first)
107+
const sortedChats = chatsData.sort((a, b) => {
108+
// If both have lastMessage, sort by timestamp
109+
if (a.lastMessage?.timestamp && b.lastMessage?.timestamp) {
110+
return b.lastMessage.timestamp.toMillis() - a.lastMessage.timestamp.toMillis();
111+
}
112+
// If only one has lastMessage, prioritize it
113+
if (a.lastMessage?.timestamp && !b.lastMessage?.timestamp) return -1;
114+
if (!a.lastMessage?.timestamp && b.lastMessage?.timestamp) return 1;
115+
// If neither has lastMessage, sort by updatedAt
116+
return b.updatedAt.toMillis() - a.updatedAt.toMillis();
117+
});
118+
119+
setChats(sortedChats);
105120
setLoading(false);
106121
},
107122
(error) => {

platforms/pictique-api/src/services/ChatService.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ export class ChatService {
162162
});
163163

164164
const savedMessage = await this.messageRepository.save(message);
165+
166+
// Update the chat's updatedAt timestamp to reflect the latest message
167+
chat.updatedAt = new Date();
168+
await this.chatRepository.save(chat);
169+
165170
console.log("Sent event", `chat:${chatId}`);
166171
this.eventEmitter.emit(`chat:${chatId}`, [savedMessage]);
167172

@@ -280,33 +285,58 @@ export class ChatService {
280285
page: number;
281286
totalPages: number;
282287
}> {
283-
// First, get the chat IDs that the user is part of
284-
const [chatIds, total] = await this.chatRepository
288+
// Get chats ordered by the most recent message timestamp
289+
const queryBuilder = this.chatRepository
285290
.createQueryBuilder("chat")
286-
.select(["chat.id", "chat.updatedAt"])
291+
.leftJoin("chat.messages", "message")
287292
.innerJoin("chat.participants", "participants")
288293
.where("participants.id = :userId", { userId })
289-
.orderBy("chat.updatedAt", "DESC")
294+
.groupBy("chat.id")
295+
.addGroupBy("chat.name")
296+
.addGroupBy("chat.ename")
297+
.addGroupBy("chat.createdAt")
298+
.addGroupBy("chat.updatedAt")
299+
.orderBy("MAX(message.createdAt)", "DESC")
300+
.addOrderBy("chat.createdAt", "DESC"); // Fallback for chats without messages
301+
302+
// Get total count for pagination
303+
const total = await queryBuilder.getCount();
304+
305+
// Apply pagination
306+
const chats = await queryBuilder
290307
.skip((page - 1) * limit)
291308
.take(limit)
292-
.getManyAndCount();
309+
.getMany();
293310

294-
// Then, load the full chat data with all relations
295-
const chats = await this.chatRepository.find({
296-
where: { id: In(chatIds.map((chat) => chat.id)) },
311+
// Load full chat data with all relations for the paginated results
312+
const chatsWithRelations = await this.chatRepository.find({
313+
where: { id: In(chats.map((chat) => chat.id)) },
297314
relations: [
298315
"participants",
299316
"messages",
300317
"messages.sender",
301318
"messages.readStatuses",
302319
"messages.readStatuses.user",
303320
],
304-
order: { updatedAt: "DESC" },
321+
});
322+
323+
// Sort the chats by latest message timestamp (since we loaded relations, we need to sort again)
324+
const sortedChats = chatsWithRelations.sort((a, b) => {
325+
const aLatestMessage = a.messages[a.messages.length - 1];
326+
const bLatestMessage = b.messages[b.messages.length - 1];
327+
328+
if (!aLatestMessage && !bLatestMessage) {
329+
return b.createdAt.getTime() - a.createdAt.getTime();
330+
}
331+
if (!aLatestMessage) return 1;
332+
if (!bLatestMessage) return -1;
333+
334+
return bLatestMessage.createdAt.getTime() - aLatestMessage.createdAt.getTime();
305335
});
306336

307337
// For each chat, get the latest message and its read status
308338
const chatsWithLatestMessage = await Promise.all(
309-
chats.map(async (chat) => {
339+
sortedChats.map(async (chat) => {
310340
const latestMessage = chat.messages[chat.messages.length - 1];
311341
if (!latestMessage) {
312342
return { ...chat, latestMessage: undefined };

platforms/pictique-api/src/services/MessageService.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { AppDataSource } from "../database/data-source";
22
import { Message } from "../database/entities/Message";
3+
import { Chat } from "../database/entities/Chat";
34

45
export class MessageService {
56
public messageRepository = AppDataSource.getRepository(Message);
7+
private chatRepository = AppDataSource.getRepository(Chat);
68

79
async findById(id: string): Promise<Message | null> {
810
return await this.messageRepository.findOneBy({ id });
@@ -17,7 +19,16 @@ export class MessageService {
1719
isArchived: false
1820
});
1921

20-
return await this.messageRepository.save(message);
22+
const savedMessage = await this.messageRepository.save(message);
23+
24+
// Update the chat's updatedAt timestamp to reflect the latest message
25+
const chat = await this.chatRepository.findOneBy({ id: chatId });
26+
if (chat) {
27+
chat.updatedAt = new Date();
28+
await this.chatRepository.save(chat);
29+
}
30+
31+
return savedMessage;
2132
}
2233

2334
async createSystemMessage(chatId: string, text: string): Promise<Message> {

0 commit comments

Comments
 (0)