Skip to content

Commit 4084969

Browse files
author
HuyDo
committed
feat: [FC-18] leave group have silent and no silent
1 parent b340bf5 commit 4084969

File tree

6 files changed

+246
-45
lines changed

6 files changed

+246
-45
lines changed

src/chat/ChatProvider.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
chatReducer,
66
setListConversation,
77
updateConversation,
8+
updateListConversation,
89
} from '../reducer';
910

1011
const firestoreServices = FirestoreServices.getInstance();
@@ -36,7 +37,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
3637
dispatch(setListConversation(res));
3738
});
3839
unsubscribeListener = firestoreServices.listenConversationUpdate(
39-
(data) => {
40+
(data, type) => {
41+
if (type === 'removed') {
42+
dispatch(updateListConversation(data));
43+
return;
44+
}
4045
dispatch(updateConversation(data));
4146
}
4247
);

src/interfaces/message.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface LatestMessageProps {
1717
type?: MediaType;
1818
path?: string;
1919
extension?: string;
20+
system?: boolean;
2021
}
2122

2223
interface MessageProps extends BaseEntity, IMessage {
@@ -29,6 +30,7 @@ interface MessageProps extends BaseEntity, IMessage {
2930
type?: MediaType;
3031
path?: string;
3132
extension?: string;
33+
system?: boolean;
3234
}
3335

3436
interface SendMessageProps {
@@ -42,6 +44,7 @@ interface SendMessageProps {
4244
type?: MediaType;
4345
path?: string;
4446
extension?: string;
47+
system?: boolean;
4548
}
4649

4750
type MediaType = 'image' | 'video' | 'text' | undefined;

src/reducer/action.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ export enum ChatActionKind {
55
SET_CONVERSATION = 'SET_CONVERSATION',
66
CLEAR_CONVERSATION = 'CLEAR_CONVERSATION',
77
UPDATE_CONVERSATION = 'UPDATE_CONVERSATION',
8+
UPDATE_LIST_CONVERSATION = 'UPDATE_LIST_CONVERSATION',
89
}
910

1011
export const setListConversation = (payload: ConversationProps[]) => ({
1112
type: ChatActionKind.SET_LIST_CONVERSATION,
1213
payload,
1314
});
1415

16+
export const updateListConversation = (payload: ConversationProps) => ({
17+
type: ChatActionKind.UPDATE_LIST_CONVERSATION,
18+
payload,
19+
});
20+
1521
export const setConversation = (payload: ConversationProps) => ({
1622
type: ChatActionKind.SET_CONVERSATION,
1723
payload,

src/reducer/chat.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ export const chatReducer = (
2121
...state,
2222
listConversation: action.payload as ConversationProps[],
2323
};
24+
case ChatActionKind.UPDATE_LIST_CONVERSATION: {
25+
const message = action.payload as ConversationProps;
26+
const listConversation = state.listConversation?.filter(
27+
(e) => e.id !== message.id
28+
);
29+
30+
return {
31+
...state,
32+
listConversation: listConversation as ConversationProps[],
33+
};
34+
}
2435
case ChatActionKind.SET_CONVERSATION:
2536
return {
2637
...state,
@@ -31,7 +42,7 @@ export const chatReducer = (
3142
...state,
3243
conversation: undefined,
3344
};
34-
case ChatActionKind.UPDATE_CONVERSATION:
45+
case ChatActionKind.UPDATE_CONVERSATION: {
3546
const message = action.payload as ConversationProps;
3647
const isExistID = state.listConversation?.some(
3748
(item) => item.id === message.id
@@ -52,5 +63,9 @@ export const chatReducer = (
5263
...state,
5364
listConversation: newListConversation,
5465
};
66+
}
67+
68+
default:
69+
return state;
5570
}
5671
};

src/services/firebase/firestore.ts

Lines changed: 174 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,15 @@ export class FirestoreServices {
246246
this.memberIds?.forEach((memberId) => {
247247
this.updateUserConversation(
248248
memberId,
249-
formatLatestMessage(
250-
this.userId,
251-
this.userInfo?.name || '',
252-
'',
249+
formatLatestMessage({
250+
userId: this.userId,
251+
name: this.userInfo?.name || '',
252+
message: '',
253253
type,
254254
path,
255-
extension
256-
)
255+
extension,
256+
system: false,
257+
})
257258
);
258259
});
259260
} catch (error) {
@@ -278,16 +279,30 @@ export class FirestoreServices {
278279
message.type === MessageTypes.image ||
279280
message.type === MessageTypes.video
280281
) {
281-
messageData = formatSendMessage(this.userId, text, type, path, extension);
282+
messageData = formatSendMessage({
283+
userId: this.userId,
284+
text,
285+
type,
286+
path,
287+
extension,
288+
});
282289
this.sendMessageWithFile(messageData);
283290
} else {
284-
/** Format message */
285-
messageData = formatSendMessage(this.userId, text);
286-
/** Encrypt the message before store to firestore */
287-
if (this.enableEncrypt && this.encryptKey) {
288-
messageData.text = this.encryptFunctionProp
289-
? await this.encryptFunctionProp(text)
290-
: await encryptData(text, this.encryptKey);
291+
if (message.system) {
292+
messageData = formatSendMessage({
293+
userId: this.userId,
294+
text,
295+
system: true,
296+
});
297+
} else {
298+
/** Format message text */
299+
messageData = formatSendMessage({ userId: this.userId, text });
300+
/** Encrypt the message before store to firestore */
301+
if (this.enableEncrypt && this.encryptKey) {
302+
messageData.text = this.encryptFunctionProp
303+
? await this.encryptFunctionProp(text)
304+
: await encryptData(text, this.encryptKey);
305+
}
291306
}
292307

293308
try {
@@ -301,11 +316,12 @@ export class FirestoreServices {
301316
.add(messageData);
302317

303318
/** Format latest message data */
304-
const latestMessageData = formatLatestMessage(
305-
this.userId,
306-
this.userInfo?.name || '',
307-
messageData.text
308-
);
319+
const latestMessageData = formatLatestMessage({
320+
userId: this.userId,
321+
name: this.userInfo?.name || '',
322+
message: messageData.text,
323+
system: messageData.system,
324+
});
309325
this.memberIds?.forEach((memberId) => {
310326
this.updateUserConversation(memberId, latestMessageData);
311327
});
@@ -324,6 +340,8 @@ export class FirestoreServices {
324340
'Please create conversation before send the first message!'
325341
);
326342
}
343+
if (userId === this.userId && latestMessageData.system) return;
344+
327345
const userConversationRef = firestore()
328346
.collection<Partial<ConversationProps>>(
329347
this.getUrlWithPrefix(
@@ -611,7 +629,9 @@ export class FirestoreServices {
611629
);
612630
};
613631

614-
listenConversationUpdate = (callback: (_: ConversationProps) => void) => {
632+
listenConversationUpdate = (
633+
callback: (_: ConversationProps, type: string) => void
634+
) => {
615635
const regex = this.regexBlacklist;
616636

617637
return firestore()
@@ -623,11 +643,11 @@ export class FirestoreServices {
623643
.onSnapshot(async (snapshot) => {
624644
if (snapshot) {
625645
for (const change of snapshot.docChanges()) {
646+
const data = {
647+
...(change.doc.data() as ConversationProps),
648+
id: change.doc.id,
649+
};
626650
if (change.type === 'modified') {
627-
const data = {
628-
...(change.doc.data() as ConversationProps),
629-
id: change.doc.id,
630-
};
631651
const message = {
632652
...data,
633653
latestMessage: data.latestMessage
@@ -639,10 +659,139 @@ export class FirestoreServices {
639659
)
640660
: data.latestMessage,
641661
} as ConversationProps;
642-
callback?.(message);
662+
callback?.(message, change.type);
663+
} else if (change.type === 'removed') {
664+
callback?.(data, change.type);
643665
}
644666
}
645667
}
646668
});
647669
};
670+
671+
private deleteUnreadAndTypingUser = async (userId: string) => {
672+
if (!userId || !this.conversationId) {
673+
console.error('User ID or conversation ID is missing');
674+
return;
675+
}
676+
677+
const conversationRef = firestore()
678+
.collection<ConversationData>(
679+
this.getUrlWithPrefix(`${FireStoreCollection.conversations}`)
680+
)
681+
.doc(this.conversationId);
682+
683+
try {
684+
const conversationDoc = await conversationRef.get();
685+
if (conversationDoc.exists) {
686+
const conversationData = conversationDoc.data();
687+
if (
688+
conversationData &&
689+
conversationData.unRead &&
690+
conversationData.unRead[userId]
691+
) {
692+
const updatedUnread = { ...conversationData.unRead };
693+
delete updatedUnread[userId];
694+
695+
await conversationRef.update({ unRead: updatedUnread });
696+
const updatedTyping = { ...conversationData.typing };
697+
delete updatedTyping[userId];
698+
await conversationRef.update({ typing: updatedTyping });
699+
} else {
700+
console.log('User not found in unRead or no unRead field present');
701+
}
702+
} else {
703+
console.log('Conversation document does not exist');
704+
}
705+
} catch (error) {
706+
console.error('Error removing user from unRead: ', error);
707+
}
708+
};
709+
710+
private updateConversationMembers = async (
711+
conversationId: string,
712+
userId: string
713+
): Promise<ConversationProps | null> => {
714+
const leftConversation = firestore()
715+
.collection(
716+
this.getUrlWithPrefix(
717+
`${FireStoreCollection.users}/${userId}/${FireStoreCollection.conversations}`
718+
)
719+
)
720+
.doc(conversationId);
721+
722+
try {
723+
const leftConversationDoc = await leftConversation.get();
724+
if (!leftConversationDoc.exists) {
725+
console.error('Conversation document does not exist');
726+
return null;
727+
}
728+
729+
const leftConversationData =
730+
leftConversationDoc.data() as ConversationProps;
731+
const newMembers = leftConversationData?.members?.filter(
732+
(e) => e !== userId
733+
);
734+
735+
const batch = firestore().batch();
736+
newMembers?.forEach((id) => {
737+
if (id) {
738+
const doc = firestore()
739+
.collection(
740+
this.getUrlWithPrefix(
741+
`${FireStoreCollection.users}/${id}/${FireStoreCollection.conversations}`
742+
)
743+
)
744+
.doc(conversationId);
745+
batch.set(doc, { members: newMembers }, { merge: true });
746+
}
747+
});
748+
749+
await batch.commit();
750+
return leftConversationData;
751+
} catch (error) {
752+
console.error('Error updating conversation members: ', error);
753+
return null;
754+
}
755+
};
756+
757+
leaveConversation = async (isSilent: boolean = false): Promise<boolean> => {
758+
if (!this.conversationId) {
759+
throw new Error(
760+
'Please create a conversation before sending the first message!'
761+
);
762+
}
763+
764+
try {
765+
await this.deleteUnreadAndTypingUser(this.userId);
766+
767+
const leftConversationData = await this.updateConversationMembers(
768+
this.conversationId,
769+
this.userId
770+
);
771+
772+
if (!leftConversationData) return false;
773+
774+
if (!isSilent) {
775+
await this.sendMessage({
776+
text: `${this.userInfo?.name} left the conversation`,
777+
system: true,
778+
} as MessageProps);
779+
}
780+
781+
const leftConversation = firestore()
782+
.collection(
783+
this.getUrlWithPrefix(
784+
`${FireStoreCollection.users}/${this.userId}/${FireStoreCollection.conversations}`
785+
)
786+
)
787+
.doc(this.conversationId);
788+
789+
await leftConversation.delete();
790+
791+
return true;
792+
} catch (e) {
793+
console.error('Error leaving conversation: ', e);
794+
return false;
795+
}
796+
};
648797
}

0 commit comments

Comments
 (0)