Skip to content

Commit 44522f0

Browse files
committed
chore: fix unRead type and re-enable markAsRead
1 parent 6264817 commit 44522f0

7 files changed

Lines changed: 85 additions & 73 deletions

File tree

examples/demo/src/ChatScreen.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ export default function ChatScreen({ currentUser }: Props) {
5454
<ChatProvider currentUser={currentUser}>
5555
<LibChatScreen
5656
conversationId={activeConversation}
57-
currentUserId={currentUser.id}
58-
memberIds={[currentUser.id, recipientId.trim()]}
57+
partners={[{ id: recipientId.trim(), name: recipientId.trim() }]}
5958
/>
6059
</ChatProvider>
6160
</div>

postcss.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module.exports = {
22
plugins: [
3+
require('postcss-import'),
34
require('tailwindcss'),
45
require('autoprefixer'),
56
],

src/components/ChatList.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ export const ChatList: React.FC<ChatListProps> = ({
3131
{conversations.map((c) => (
3232
<button
3333
key={c.id}
34-
className={`conversation-item ${
35-
selectedConversationId === c.id ? "active" : ""
36-
}`}
34+
className={`conversation-item ${selectedConversationId === c.id ? "active" : ""
35+
}`}
3736
onClick={() => {
3837
handleSelectConversation(c);
3938
}}
@@ -60,8 +59,8 @@ export const ChatList: React.FC<ChatListProps> = ({
6059
{c?.latestMessage?.text || ""}
6160
</div>
6261
</div>
63-
{(c.unRead || 0) > 0 && (
64-
<span className="unread-badge">{c.unRead || 0}</span>
62+
{(c.unRead?.[currentUser?.id] || 0) > 0 && (
63+
<span className="unread-badge">{c.unRead?.[currentUser?.id] || 0}</span>
6564
)}
6665
</button>
6766
))}

src/components/ChatScreen.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
4949

5050
// Keep a ref so the subscription callback always reads the latest key
5151
const derivedKeyRef = useRef(derivedKey);
52+
// Track whether we've already auto-selected the first conversation
53+
const hasAutoSelectedRef = useRef(!!conversationId);
5254
useEffect(() => {
5355
derivedKeyRef.current = derivedKey;
5456
}, [derivedKey]);
@@ -74,7 +76,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
7476
const chatName = useMemo(
7577
() =>
7678
isGroup
77-
? `group_${currentUser.id},${selectedPartners.map((p) => p.name).join(",")}`
79+
? `group_${currentUser.name},${selectedPartners.map((p) => p.name).join(",")}`
7880
: selectedPartners.find((p) => p.id !== currentUser.id)?.name || selectedName,
7981
[isGroup, currentUser.id, selectedPartners, selectedName]
8082
);
@@ -133,22 +135,30 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
133135
})
134136
);
135137
setConversations(decrypted);
136-
// If nothing selected, pick first
137-
if (!selectedConversationId && decrypted.length > 0) {
138-
setSelectedConversationId(decrypted[0].id);
139-
setSelectedName(decrypted[0].name || "");
138+
// Auto-select first conversation only once
139+
if (!hasAutoSelectedRef.current && decrypted.length > 0) {
140+
hasAutoSelectedRef.current = true;
141+
const first = decrypted[0];
142+
setSelectedConversationId(first.id);
143+
setSelectedName(first.name || "");
144+
setSelectedPartners(
145+
(first.members ?? [])
146+
.filter((m: string) => m !== `${currentUser?.id}`)
147+
.map((m: string) => ({ id: m }))
148+
);
140149
}
141150
}
142151
);
143152
return () => unsubscribe?.();
144153
// eslint-disable-next-line react-hooks/exhaustive-deps
145154
}, [currentUser?.id]);
146155

156+
// Reset unread count when a conversation is selected
147157
useEffect(() => {
148158
if (selectedConversationId) {
149159
markAsRead();
150160
}
151-
}, [selectedConversationId, markAsRead]);
161+
}, [selectedConversationId]); // eslint-disable-line react-hooks/exhaustive-deps
152162

153163
const handleSendMessage = useCallback(
154164
async (text: string) => {
@@ -190,7 +200,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
190200
console.error("Failed to start chat", e);
191201
}
192202
},
193-
[currentUser?.id]
203+
[currentUser?.id, currentUser?.name]
194204
);
195205

196206
const handleFileUpload = useCallback(

src/hooks/useChat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export const useChat = ({ user, conversationId, memberIds, name }: UseChatProps)
130130
setError(err instanceof Error ? err.message : 'Failed to mark as read');
131131
throw err;
132132
}
133-
}, [conversationId, user.id]);
133+
}, [conversationId, user.id, chatService]);
134134

135135
return {
136136
messages,

src/services/chat.ts

Lines changed: 58 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
deleteDoc,
77
setDoc,
88
query,
9+
where,
910
orderBy,
1011
limit,
1112
startAfter,
@@ -21,6 +22,7 @@ import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
2122
import { getFirebaseFirestore, getFirebaseStorage } from './firebase';
2223
import { UserService } from './user';
2324
import {
25+
ConversationProps,
2426
FireStoreCollection,
2527
IMessage,
2628
MediaType,
@@ -67,62 +69,50 @@ export class ChatService {
6769
conversationId?: string,
6870
): Promise<string> {
6971
try {
72+
// Per-user names: initiator sees otherName, others see initiator's name
73+
const names: Record<string, string> = {};
74+
const unRead: Record<string, number> = {};
75+
memberIds.forEach((id) => {
76+
names[id] = id === initiatorId ? (otherName || '') : (name || '');
77+
unRead[id] = 0;
78+
});
79+
7080
const conversationData = {
7181
members: memberIds,
7282
type,
73-
name: otherName || '',
83+
names,
84+
unRead,
7485
createdAt: Date.now(),
7586
updatedAt: Date.now(),
7687
latestMessage: null,
7788
latestMessageTime: null,
78-
createdBy: initiatorId
89+
createdBy: initiatorId,
7990
};
8091

8192
let docRefId: string;
8293

8394
if (conversationId) {
84-
// Create the document with the specified conversationId
8595
await updateDoc(
8696
doc(this.db, COLLECTIONS.CONVERSATIONS, conversationId),
8797
conversationData
88-
).catch(async (_err) => {
89-
// If doc does not exist, set it
98+
).catch(async () => {
9099
await setDoc(
91100
doc(this.db, COLLECTIONS.CONVERSATIONS, conversationId),
92101
conversationData
93102
);
94103
});
95104
docRefId = conversationId;
96105
} else {
97-
// Add a new document with auto-generated ID
98106
const docRef = await addDoc(
99107
collection(this.db, COLLECTIONS.CONVERSATIONS),
100108
conversationData
101109
);
102110
docRefId = docRef.id;
103111
}
104112

105-
// Create user conversation references for each member
106-
const promises = memberIds.map(async (memberId) => {
107-
// Ensure user document exists
108-
await this.userService.createUserIfNotExists(memberId);
109-
110-
// Get the chat name based on the memberId and initiatorId
111-
const chatName = memberId === initiatorId ? otherName : name;
112-
// Add conversation reference to user's conversations subcollection
113-
await setDoc(
114-
doc(this.db, COLLECTIONS.USERS, memberId, COLLECTIONS.CONVERSATIONS, docRefId),
115-
{
116-
joinedAt: Date.now(),
117-
unRead: 0,
118-
updatedAt: Date.now(),
119-
members: memberIds,
120-
name: chatName || '',
121-
}
122-
);
123-
});
113+
// Ensure user documents exist for all members
114+
await Promise.all(memberIds.map((id) => this.userService.createUserIfNotExists(id)));
124115

125-
await Promise.all(promises);
126116
return docRefId;
127117
} catch (error) {
128118
console.error('Error creating conversation:', error);
@@ -167,7 +157,9 @@ export class ChatService {
167157
try {
168158
// Check if conversation exists, create if it doesn't
169159
const conversationExists = await this.conversationExists(conversationId);
170-
const memberIds = conversationOptions?.memberIds || [String(message.senderId)];
160+
const memberIds = conversationOptions?.memberIds && conversationOptions.memberIds.length > 1
161+
? conversationOptions.memberIds
162+
: [String(message.senderId)];
171163

172164
if (!conversationExists) {
173165
// Use provided options or defaults
@@ -206,30 +198,27 @@ export class ChatService {
206198
}
207199
);
208200

209-
// Update unread counts for other members
201+
// Update unread counts for other members on the conversation document
210202
const conversationDoc = await getDoc(
211203
doc(this.db, COLLECTIONS.CONVERSATIONS, conversationId)
212204
);
213205

214206
if (conversationDoc.exists()) {
215-
// Use members from the conversation document so all participants are updated,
216-
// regardless of what memberIds was passed by the caller
217207
const allMembers: string[] = conversationDoc.data()?.members || memberIds;
218-
const updatePromises = allMembers.map(async (memberId: string) => {
219-
const isSender = memberId === message.senderId;
220-
// Update unread count in user's conversations subcollection
221-
await setDoc(
222-
doc(this.db, COLLECTIONS.USERS, memberId, COLLECTIONS.CONVERSATIONS, conversationId),
223-
{
224-
unRead: increment(isSender ? 0 : 1),
225-
updatedAt: Date.now(),
226-
latestMessage: convertToLatestMessage(`${message.senderId}`, conversationOptions?.name || '', message.text || ''),
227-
},
228-
{ merge: true } // Merge with existing data if document exists
229-
);
208+
const unreadUpdates: Record<string, unknown> = {};
209+
allMembers.forEach((memberId: string) => {
210+
if (memberId !== message.senderId) {
211+
unreadUpdates[`unRead.${memberId}`] = increment(1);
212+
}
230213
});
231214

232-
await Promise.all(updatePromises);
215+
await updateDoc(
216+
doc(this.db, COLLECTIONS.CONVERSATIONS, conversationId),
217+
{
218+
...unreadUpdates,
219+
latestMessage: convertToLatestMessage(`${message.senderId}`, conversationOptions?.name || '', message.text || ''),
220+
}
221+
);
233222
}
234223
} catch (error) {
235224
console.error('Error sending message:', error);
@@ -279,32 +268,39 @@ export class ChatService {
279268
}
280269

281270
/**
282-
* Get user's conversations from the user's conversations subcollection
271+
* Get user's conversations from the top-level /conversations collection
272+
* filtered by membership.
283273
* @param userId - The ID of the user
284274
* @param callback - Callback function to receive conversations
285275
* @returns Unsubscribe function
286276
*/
287277
subscribeToUserConversations(
288278
userId: string,
289-
callback: (userConversations: any[]) => void
279+
callback: (userConversations: ConversationProps[]) => void
290280
): () => void {
291281
// First ensure user document exists
292282
this.userService.createUserIfNotExists(userId).catch(console.error);
293283

294284
const userConversationsQuery = query(
295-
collection(this.db, COLLECTIONS.USERS, userId, 'conversations'),
285+
collection(this.db, COLLECTIONS.CONVERSATIONS),
286+
where('members', 'array-contains', userId),
296287
orderBy('updatedAt', 'desc')
297288
);
298289

299290
return onSnapshot(userConversationsQuery, (snapshot) => {
300-
const userConversations: any[] = [];
301-
snapshot.forEach((doc) => {
302-
const data = doc.data();
291+
const userConversations: ConversationProps[] = [];
292+
snapshot.forEach((docSnap) => {
293+
const data = docSnap.data();
294+
const names: Record<string, string> = data.names || {};
295+
const unreadCounts: Record<string, number> = data.unRead || {};
303296
userConversations.push({
304-
id: doc.id,
305-
...data,
297+
id: docSnap.id,
298+
name: names[userId] || '',
299+
members: data.members || [],
300+
unRead: unreadCounts,
306301
updatedAt: data.updatedAt ? new Date(data.updatedAt).valueOf() : Date.now(),
307-
joinedAt: data.joinedAt ? new Date(data.joinedAt).valueOf() : Date.now(),
302+
joinedAt: data.createdAt ? new Date(data.createdAt).valueOf() : Date.now(),
303+
latestMessage: data.latestMessage || undefined,
308304
});
309305
});
310306
callback(userConversations);
@@ -359,12 +355,18 @@ export class ChatService {
359355
});
360356
}
361357

362-
// Update unread count
358+
// Update unread count on the main conversation document
363359
async updateUnread(conversationId: string, userId: string): Promise<void> {
364360
try {
365-
const conversationRef = doc(this.db, FireStoreCollection.users, userId, FireStoreCollection.conversations, conversationId);
366-
await setDoc(conversationRef, { unRead: 0 }, { merge: true });
367-
} catch (error) {
361+
await updateDoc(
362+
doc(this.db, COLLECTIONS.CONVERSATIONS, conversationId),
363+
{ [`unRead.${userId}`]: 0 }
364+
);
365+
} catch (error: any) {
366+
if (error?.code === 'not-found') {
367+
// Conversation document doesn't exist yet, we can safely ignore
368+
return;
369+
}
368370
console.error('Error updating unread count:', error);
369371
throw new Error('Failed to update unread count');
370372
}

src/types/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface IConversation {
4040
members: string[];
4141
latestMessage?: IMessage;
4242
latestMessageTime?: number;
43-
unRead?: number;
43+
unRead?: Record<string, number>;
4444
title?: string;
4545
type: 'private' | 'group';
4646
createdAt: number;
@@ -112,8 +112,9 @@ export interface ConversationProps {
112112
name?: string;
113113
image?: string;
114114
members: string[];
115-
unRead?: number;
115+
unRead?: Record<string, number>;
116116
updatedAt: number;
117+
joinedAt?: number;
117118
latestMessage?: LatestMessageProps;
118119
}
119120

0 commit comments

Comments
 (0)