@myrjfa/state 2.2.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +20 -3
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/lib/actions/actions.d.ts +13 -13
- package/dist/lib/actions/actions.d.ts.map +1 -1
- package/dist/lib/actions/actions.js +5 -4
- package/dist/lib/actions/auth.d.ts +116 -7
- package/dist/lib/actions/auth.d.ts.map +1 -1
- package/dist/lib/actions/auth.js +59 -10
- package/dist/lib/actions/chat.d.ts +261 -24
- package/dist/lib/actions/chat.d.ts.map +1 -1
- package/dist/lib/actions/chat.js +525 -41
- package/dist/lib/actions/fetcher.d.ts.map +1 -1
- package/dist/lib/actions/fetcher.js +26 -9
- package/dist/lib/actions/serverActions.d.ts +1 -0
- package/dist/lib/actions/serverActions.d.ts.map +1 -1
- package/dist/lib/actions/serverActions.js +15 -0
- package/dist/lib/actions/socket.d.ts.map +1 -1
- package/dist/lib/actions/socket.js +5 -1
- package/dist/lib/actions.d.ts +26 -26
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/actions.js +17 -12
- package/dist/lib/adminDashboardActions.d.ts +15 -3
- package/dist/lib/adminDashboardActions.d.ts.map +1 -1
- package/dist/lib/adminDashboardActions.js +16 -7
- package/dist/lib/authSessionManager.d.ts.map +1 -1
- package/dist/lib/authSessionManager.js +77 -12
- package/dist/lib/authSessionPolicy.d.ts +44 -0
- package/dist/lib/authSessionPolicy.d.ts.map +1 -0
- package/dist/lib/authSessionPolicy.js +61 -0
- package/dist/lib/authorizationActions.d.ts +77 -0
- package/dist/lib/authorizationActions.d.ts.map +1 -0
- package/dist/lib/authorizationActions.js +61 -0
- package/dist/lib/context/ChatContext.d.ts +25 -6
- package/dist/lib/context/ChatContext.d.ts.map +1 -1
- package/dist/lib/context/ChatContext.js +356 -245
- package/dist/lib/discountActions.d.ts +43 -0
- package/dist/lib/discountActions.d.ts.map +1 -0
- package/dist/lib/discountActions.js +69 -0
- package/dist/lib/loyaltyActions.d.ts +32 -0
- package/dist/lib/loyaltyActions.d.ts.map +1 -0
- package/dist/lib/loyaltyActions.js +92 -0
- package/dist/lib/models/adventure.d.ts +0 -2
- package/dist/lib/models/adventure.d.ts.map +1 -1
- package/dist/lib/models/authorization.d.ts +108 -0
- package/dist/lib/models/authorization.d.ts.map +1 -0
- package/dist/lib/models/authorization.js +266 -0
- package/dist/lib/models/blog.d.ts +6 -6
- package/dist/lib/models/booking.d.ts +7 -2
- package/dist/lib/models/booking.d.ts.map +1 -1
- package/dist/lib/models/chat.d.ts +418 -32
- package/dist/lib/models/chat.d.ts.map +1 -1
- package/dist/lib/models/chat.js +5 -1
- package/dist/lib/models/discount.d.ts +589 -138
- package/dist/lib/models/discount.d.ts.map +1 -1
- package/dist/lib/models/discount.js +118 -96
- package/dist/lib/models/guide.d.ts +0 -2
- package/dist/lib/models/guide.d.ts.map +1 -1
- package/dist/lib/models/loyalty.d.ts +88 -0
- package/dist/lib/models/loyalty.d.ts.map +1 -0
- package/dist/lib/models/loyalty.js +1 -0
- package/dist/lib/models/notifications.d.ts +6 -6
- package/dist/lib/models/opportunities/freelance.d.ts +66 -20
- package/dist/lib/models/opportunities/freelance.d.ts.map +1 -1
- package/dist/lib/models/opportunities/internship.d.ts +66 -20
- package/dist/lib/models/opportunities/internship.d.ts.map +1 -1
- package/dist/lib/models/opportunities/job.d.ts +66 -20
- package/dist/lib/models/opportunities/job.d.ts.map +1 -1
- package/dist/lib/models/opportunities/opportunity.d.ts +66 -20
- package/dist/lib/models/opportunities/opportunity.d.ts.map +1 -1
- package/dist/lib/models/opportunities/opportunity.js +2 -0
- package/dist/lib/models/opportunities/volunteerJob.d.ts +66 -20
- package/dist/lib/models/opportunities/volunteerJob.d.ts.map +1 -1
- package/dist/lib/models/package.d.ts +77 -216
- package/dist/lib/models/package.d.ts.map +1 -1
- package/dist/lib/models/package.js +0 -2
- package/dist/lib/models/portfolio.d.ts +2 -2
- package/dist/lib/models/pricing.d.ts +12 -12
- package/dist/lib/models/props.d.ts +1 -1
- package/dist/lib/models/props.d.ts.map +1 -1
- package/dist/lib/models/rental.d.ts +0 -2
- package/dist/lib/models/rental.d.ts.map +1 -1
- package/dist/lib/models/review.d.ts +2 -2
- package/dist/lib/models/stay.d.ts +17 -17
- package/dist/lib/models/store.d.ts +71 -0
- package/dist/lib/models/store.d.ts.map +1 -0
- package/dist/lib/models/store.js +1 -0
- package/dist/lib/models/tile.d.ts +0 -2
- package/dist/lib/models/tile.d.ts.map +1 -1
- package/dist/lib/models/travelPackage.d.ts +211 -310
- package/dist/lib/models/travelPackage.d.ts.map +1 -1
- package/dist/lib/models/travelPackage.js +0 -2
- package/dist/lib/models/user.d.ts +32 -8
- package/dist/lib/models/user.d.ts.map +1 -1
- package/dist/lib/models/user.js +10 -1
- package/dist/lib/opportunityFormPage.d.ts +1 -0
- package/dist/lib/opportunityFormPage.d.ts.map +1 -1
- package/dist/lib/opportunityFormPage.js +1 -0
- package/dist/lib/rentalActions.js +0 -1
- package/dist/lib/rentalDummyData.d.ts.map +1 -1
- package/dist/lib/rentalDummyData.js +0 -35
- package/dist/lib/rentalFormPage.d.ts +0 -2
- package/dist/lib/rentalFormPage.d.ts.map +1 -1
- package/dist/lib/rentalFormPage.js +0 -3
- package/dist/lib/storeActions.d.ts +25 -0
- package/dist/lib/storeActions.d.ts.map +1 -0
- package/dist/lib/storeActions.js +46 -0
- package/dist/lib/storeDummyData.d.ts +2 -26
- package/dist/lib/storeDummyData.d.ts.map +1 -1
- package/dist/lib/userAtom.d.ts +178 -42
- package/dist/lib/userAtom.d.ts.map +1 -1
- package/dist/lib/userAtom.js +80 -52
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/package.json +4 -2
|
@@ -1,318 +1,426 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useEffect,
|
|
4
|
-
import { getSocket } from "../actions/socket";
|
|
5
|
-
import { chatApi } from "../actions/chat";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from "react";
|
|
6
4
|
import { useAtomValue } from "jotai";
|
|
5
|
+
import { getSocket } from "../actions/socket";
|
|
6
|
+
import { chatApi, normalizeChatMessage, } from "../actions/chat";
|
|
7
|
+
import { chatActorKey, isSameChatActor, } from "../models/chat";
|
|
7
8
|
import { userAtom } from "../userAtom";
|
|
8
|
-
const
|
|
9
|
+
const emptyContext = {
|
|
9
10
|
socket: null,
|
|
10
11
|
isConnected: false,
|
|
11
12
|
conversations: [],
|
|
12
13
|
activeConversation: null,
|
|
13
|
-
setActiveConversation: () =>
|
|
14
|
+
setActiveConversation: () => undefined,
|
|
14
15
|
messages: [],
|
|
15
|
-
setMessages: () =>
|
|
16
|
+
setMessages: () => undefined,
|
|
16
17
|
typingUsers: new Map(),
|
|
17
18
|
onlineUsers: new Set(),
|
|
18
|
-
refreshConversations: async () =>
|
|
19
|
-
sendMessage: async () =>
|
|
20
|
-
createPoll: () =>
|
|
21
|
-
votePoll: () =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
refreshConversations: async () => undefined,
|
|
20
|
+
sendMessage: async () => undefined,
|
|
21
|
+
createPoll: () => undefined,
|
|
22
|
+
votePoll: async () => null,
|
|
23
|
+
updatePoll: async () => null,
|
|
24
|
+
editMessage: () => undefined,
|
|
25
|
+
toggleMessageReaction: () => undefined,
|
|
26
|
+
deleteMessage: () => undefined,
|
|
27
|
+
markRead: () => undefined,
|
|
26
28
|
unreadNotificationCount: 0,
|
|
27
29
|
notifications: [],
|
|
28
|
-
clearNotifications: () =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
clearNotifications: () => undefined,
|
|
31
|
+
eventRevision: 0,
|
|
32
|
+
paymentRevision: 0,
|
|
33
|
+
};
|
|
34
|
+
const ChatContext = createContext(emptyContext);
|
|
35
|
+
export const useChatContext = () => useContext(ChatContext);
|
|
33
36
|
export function ChatProvider({ children }) {
|
|
34
37
|
const user = useAtomValue(userAtom);
|
|
38
|
+
const actorId = user?._id || user?.id || "";
|
|
39
|
+
const accountType = user?.accountType || user?.role;
|
|
40
|
+
const actorModel = accountType === "host" ? "HostProfile" : "User";
|
|
35
41
|
const [isConnected, setIsConnected] = useState(false);
|
|
36
42
|
const [conversations, setConversations] = useState([]);
|
|
37
|
-
const [activeConversation,
|
|
43
|
+
const [activeConversation, setActiveConversationState] = useState(null);
|
|
38
44
|
const activeConversationRef = useRef(null);
|
|
39
45
|
const [messages, setMessages] = useState([]);
|
|
40
46
|
const [typingUsers, setTypingUsers] = useState(new Map());
|
|
41
47
|
const [onlineUsers, setOnlineUsers] = useState(new Set());
|
|
42
48
|
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
|
|
43
49
|
const [notifications, setNotifications] = useState([]);
|
|
50
|
+
const [eventRevision, setEventRevision] = useState(0);
|
|
51
|
+
const [paymentRevision, setPaymentRevision] = useState(0);
|
|
44
52
|
const socketRef = useRef(null);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
const conversationListParamsRef = useRef({ filter: "all" });
|
|
54
|
+
const setActiveConversation = useCallback((conversation) => {
|
|
55
|
+
setActiveConversationState(conversation);
|
|
56
|
+
activeConversationRef.current = conversation;
|
|
48
57
|
}, []);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
const refreshConversations = useCallback(async (params) => {
|
|
59
|
+
if (params)
|
|
60
|
+
conversationListParamsRef.current = params;
|
|
61
|
+
const data = await chatApi.getConversations(params || conversationListParamsRef.current);
|
|
62
|
+
setConversations(data);
|
|
63
|
+
const activeId = activeConversationRef.current?._id;
|
|
64
|
+
if (activeId) {
|
|
65
|
+
const refreshed = data.find((conversation) => conversation._id === activeId);
|
|
66
|
+
if (refreshed)
|
|
67
|
+
setActiveConversation(refreshed);
|
|
56
68
|
}
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
}, [setActiveConversation]);
|
|
70
|
+
const sendMessage = useCallback(async (conversationId, text, files, replyTo, options) => {
|
|
71
|
+
const trimmed = text.trim();
|
|
72
|
+
let media = [];
|
|
73
|
+
if (files?.length) {
|
|
74
|
+
const formData = new FormData();
|
|
75
|
+
files.forEach((file) => formData.append("files", file));
|
|
76
|
+
const upload = await chatApi.uploadFiles(formData);
|
|
77
|
+
media = upload?.data?.files || [];
|
|
78
|
+
if (!media.length)
|
|
79
|
+
throw new Error("Unable to upload attachments");
|
|
80
|
+
media = media.map((item, index) => {
|
|
81
|
+
const metadata = files[index]?.chatMediaMetadata;
|
|
82
|
+
return metadata ? { ...item, ...metadata } : item;
|
|
83
|
+
});
|
|
59
84
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
if (!trimmed && !media.length)
|
|
86
|
+
return;
|
|
87
|
+
const socket = socketRef.current;
|
|
88
|
+
if (socket?.connected) {
|
|
89
|
+
await new Promise((resolve, reject) => {
|
|
90
|
+
socket.emit("message:send", {
|
|
91
|
+
conversationId,
|
|
92
|
+
text: trimmed,
|
|
93
|
+
replyTo,
|
|
94
|
+
media,
|
|
95
|
+
type: options?.type || (media.length ? "media" : "text"),
|
|
96
|
+
mentions: options?.mentions || [],
|
|
97
|
+
}, (result) => {
|
|
98
|
+
if (result?.success)
|
|
99
|
+
resolve();
|
|
100
|
+
else
|
|
101
|
+
reject(new Error(result?.error?.message || "Unable to send message"));
|
|
70
102
|
});
|
|
71
|
-
|
|
72
|
-
actualId = res._id;
|
|
73
|
-
// Update active conversation in state to point to the real one
|
|
74
|
-
setActiveConversation(res);
|
|
75
|
-
// Refresh to include in list
|
|
76
|
-
await refreshConversations();
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
console.error("Failed to create conversation during lazy send");
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
console.error("Failed to create conversation during lazy send:", err);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (!socketRef.current)
|
|
103
|
+
});
|
|
89
104
|
return;
|
|
90
|
-
let media = [];
|
|
91
|
-
if (files && files.length > 0) {
|
|
92
|
-
// Upload files first
|
|
93
|
-
const formData = new FormData();
|
|
94
|
-
files.forEach(f => formData.append("chatFiles", f));
|
|
95
|
-
try {
|
|
96
|
-
// We need a dedicated upload endpoint.
|
|
97
|
-
// For now, let's assume /api/v1/chat/upload
|
|
98
|
-
const response = await chatApi.uploadFiles(formData);
|
|
99
|
-
if (response.error) {
|
|
100
|
-
console.error("File upload failed:", response.error);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
media = response.data.files.map((f) => ({
|
|
104
|
-
type: f.mimetype.startsWith("audio") ? "audio" : (f.mimetype.startsWith("image") ? "image" : "file"),
|
|
105
|
-
url: f.location,
|
|
106
|
-
name: f.originalname
|
|
107
|
-
}));
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
console.error("File upload failed:", err);
|
|
111
|
-
}
|
|
112
105
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
content: { text, media },
|
|
116
|
-
type: media.length > 0 ? "media" : "text",
|
|
106
|
+
const message = await chatApi.sendMessage(conversationId, {
|
|
107
|
+
text: trimmed,
|
|
117
108
|
replyTo,
|
|
109
|
+
media,
|
|
110
|
+
type: options?.type || (media.length ? "media" : "text"),
|
|
111
|
+
mentions: options?.mentions || [],
|
|
118
112
|
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
if (message) {
|
|
114
|
+
setMessages((current) => current.some((item) => item._id === message._id)
|
|
115
|
+
? current
|
|
116
|
+
: [...current, message]);
|
|
117
|
+
await refreshConversations();
|
|
118
|
+
}
|
|
119
|
+
}, [refreshConversations]);
|
|
120
|
+
const editMessage = useCallback(async (messageId, text) => {
|
|
121
|
+
if (socketRef.current?.connected) {
|
|
122
|
+
socketRef.current.emit("message:edit", { messageId, text });
|
|
122
123
|
return;
|
|
123
|
-
|
|
124
|
+
}
|
|
125
|
+
const message = await chatApi.editMessage(messageId, text);
|
|
126
|
+
if (message) {
|
|
127
|
+
setMessages((current) => current.map((item) => (item._id === message._id ? message : item)));
|
|
128
|
+
}
|
|
124
129
|
}, []);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
const createPoll = useCallback(async (conversationId, question, options, allowMultiple) => {
|
|
131
|
+
const response = await chatApi.createPoll(conversationId, { question, options, allowMultiple });
|
|
132
|
+
if (response.data) {
|
|
133
|
+
const message = normalizeChatMessage(response.data);
|
|
134
|
+
setMessages((current) => current.some((item) => item._id === message._id) ? current : [...current, message]);
|
|
135
|
+
await refreshConversations();
|
|
136
|
+
}
|
|
137
|
+
}, [refreshConversations]);
|
|
138
|
+
const votePoll = useCallback(async (pollId, optionIndex) => {
|
|
139
|
+
const response = await chatApi.votePoll(pollId, optionIndex);
|
|
140
|
+
return response.data || null;
|
|
129
141
|
}, []);
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
socketRef.current.emit("message:delete", { messageId });
|
|
142
|
+
const updatePoll = useCallback(async (pollId, data) => {
|
|
143
|
+
const response = await chatApi.updatePoll(pollId, data);
|
|
144
|
+
return response.data || null;
|
|
134
145
|
}, []);
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
146
|
+
const toggleMessageReaction = useCallback(async (messageId, emoji) => {
|
|
147
|
+
if (socketRef.current?.connected) {
|
|
148
|
+
socketRef.current.emit("message:react", {
|
|
149
|
+
messageId,
|
|
150
|
+
emoji: emoji || "",
|
|
151
|
+
});
|
|
137
152
|
return;
|
|
138
|
-
|
|
153
|
+
}
|
|
154
|
+
const message = await chatApi.addReaction(messageId, emoji || "");
|
|
155
|
+
if (message) {
|
|
156
|
+
setMessages((current) => current.map((item) => (item._id === message._id ? message : item)));
|
|
157
|
+
}
|
|
139
158
|
}, []);
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
159
|
+
const deleteMessage = useCallback(async (messageId) => {
|
|
160
|
+
if (socketRef.current?.connected) {
|
|
161
|
+
socketRef.current.emit("message:delete", { messageId });
|
|
142
162
|
return;
|
|
143
|
-
|
|
163
|
+
}
|
|
164
|
+
const message = await chatApi.deleteMessage(messageId);
|
|
165
|
+
if (message) {
|
|
166
|
+
setMessages((current) => current.map((item) => (item._id === message._id ? message : item)));
|
|
167
|
+
}
|
|
144
168
|
}, []);
|
|
169
|
+
const markRead = useCallback(async (conversationId, messageIds) => {
|
|
170
|
+
if (!messageIds.length || !actorId)
|
|
171
|
+
return;
|
|
172
|
+
const readAt = new Date().toISOString();
|
|
173
|
+
setMessages((current) => current.map((message) => message.conversationId === conversationId && messageIds.includes(message._id) &&
|
|
174
|
+
!message.readBy.some((receipt) => isSameChatActor(receipt, { actorId, actorModel }))
|
|
175
|
+
? {
|
|
176
|
+
...message,
|
|
177
|
+
readBy: [
|
|
178
|
+
...message.readBy,
|
|
179
|
+
{ actorId, actorModel, readAt },
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
: message));
|
|
183
|
+
setConversations((current) => current.map((conversation) => conversation._id === conversationId
|
|
184
|
+
? {
|
|
185
|
+
...conversation,
|
|
186
|
+
participants: conversation.participants.map((participant) => isSameChatActor(participant, { actorId, actorModel })
|
|
187
|
+
? {
|
|
188
|
+
...participant,
|
|
189
|
+
unreadCount: Math.max(0, participant.unreadCount - messageIds.length),
|
|
190
|
+
}
|
|
191
|
+
: participant),
|
|
192
|
+
preference: {
|
|
193
|
+
...conversation.preference,
|
|
194
|
+
markedUnreadAt: null,
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
: conversation));
|
|
198
|
+
if (socketRef.current?.connected) {
|
|
199
|
+
socketRef.current.emit("message:read", {
|
|
200
|
+
conversationId,
|
|
201
|
+
messageIds,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await chatApi.markRead(conversationId, messageIds);
|
|
206
|
+
await refreshConversations();
|
|
207
|
+
}, [actorId, actorModel, refreshConversations]);
|
|
145
208
|
const clearNotifications = useCallback(() => {
|
|
146
209
|
setNotifications([]);
|
|
147
210
|
setUnreadNotificationCount(0);
|
|
148
211
|
}, []);
|
|
149
|
-
|
|
150
|
-
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const socket = socketRef.current;
|
|
214
|
+
const conversationId = activeConversation?._id;
|
|
215
|
+
if (!socket || !conversationId || conversationId.startsWith("draft_")) {
|
|
151
216
|
return;
|
|
152
|
-
|
|
153
|
-
|
|
217
|
+
}
|
|
218
|
+
socket.emit("conversation:join", { conversationId });
|
|
219
|
+
return () => {
|
|
220
|
+
socket.emit("conversation:leave", { conversationId });
|
|
221
|
+
};
|
|
222
|
+
}, [activeConversation?._id]);
|
|
154
223
|
useEffect(() => {
|
|
155
|
-
if (!user) {
|
|
224
|
+
if (!user || !actorId || accountType === "staff") {
|
|
156
225
|
setIsConnected(false);
|
|
157
226
|
setConversations([]);
|
|
158
227
|
return;
|
|
159
228
|
}
|
|
160
229
|
const socket = getSocket();
|
|
161
230
|
socketRef.current = socket;
|
|
162
|
-
|
|
231
|
+
const onConnect = () => {
|
|
163
232
|
setIsConnected(true);
|
|
164
|
-
refreshConversations();
|
|
165
|
-
|
|
166
|
-
|
|
233
|
+
void refreshConversations();
|
|
234
|
+
const activeId = activeConversationRef.current?._id;
|
|
235
|
+
if (activeId && !activeId.startsWith("draft_")) {
|
|
236
|
+
socket.emit("conversation:join", { conversationId: activeId });
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const onDisconnect = () => {
|
|
167
240
|
setIsConnected(false);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
return prev;
|
|
190
|
-
});
|
|
191
|
-
// Update conversations last message
|
|
192
|
-
setConversations((prev) => prev
|
|
193
|
-
.map((c) => c._id === conversationId
|
|
241
|
+
setOnlineUsers(new Set());
|
|
242
|
+
setTypingUsers(new Map());
|
|
243
|
+
};
|
|
244
|
+
const onNewMessage = (payload) => {
|
|
245
|
+
const message = normalizeChatMessage(payload.message);
|
|
246
|
+
if (activeConversationRef.current?._id === payload.conversationId) {
|
|
247
|
+
setMessages((current) => current.some((item) => item._id === message._id)
|
|
248
|
+
? current
|
|
249
|
+
: [...current, message]);
|
|
250
|
+
}
|
|
251
|
+
if (!isSameChatActor({ actorId: message.senderId, actorModel: message.senderModel }, { actorId, actorModel })) {
|
|
252
|
+
socketRef.current?.emit("message:delivered", { messageId: message._id });
|
|
253
|
+
}
|
|
254
|
+
void refreshConversations();
|
|
255
|
+
};
|
|
256
|
+
const onDelivered = ({ messageId, actorId: deliveredActorId, actorModel: deliveredActorModel, deliveredAt, }) => {
|
|
257
|
+
setMessages((current) => current.map((message) => message._id === messageId && !(message.deliveredTo || []).some((receipt) => isSameChatActor(receipt, {
|
|
258
|
+
actorId: deliveredActorId,
|
|
259
|
+
actorModel: deliveredActorModel,
|
|
260
|
+
}))
|
|
194
261
|
? {
|
|
195
|
-
...
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
262
|
+
...message,
|
|
263
|
+
deliveredTo: [
|
|
264
|
+
...(message.deliveredTo || []),
|
|
265
|
+
{
|
|
266
|
+
actorId: deliveredActorId,
|
|
267
|
+
actorModel: deliveredActorModel,
|
|
268
|
+
deliveredAt,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
202
271
|
}
|
|
203
|
-
:
|
|
204
|
-
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
setMessages((
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
272
|
+
: message));
|
|
273
|
+
};
|
|
274
|
+
const onUpdatedMessage = ({ message: rawMessage }) => {
|
|
275
|
+
const message = normalizeChatMessage(rawMessage);
|
|
276
|
+
setMessages((current) => current.map((item) => (item._id === message._id ? message : item)));
|
|
277
|
+
};
|
|
278
|
+
const onRead = ({ conversationId, actorId: readerId, actorModel: readerModel, messageIds, readAt, }) => {
|
|
279
|
+
const readMessageIds = new Set(messageIds);
|
|
280
|
+
setMessages((current) => current.map((message) => message.conversationId === conversationId &&
|
|
281
|
+
readMessageIds.has(message._id) &&
|
|
282
|
+
!message.readBy.some((receipt) => isSameChatActor(receipt, {
|
|
283
|
+
actorId: readerId,
|
|
284
|
+
actorModel: readerModel,
|
|
285
|
+
}))
|
|
286
|
+
? {
|
|
287
|
+
...message,
|
|
288
|
+
readBy: [
|
|
289
|
+
...message.readBy,
|
|
290
|
+
{ actorId: readerId, actorModel: readerModel, readAt },
|
|
291
|
+
],
|
|
292
|
+
deliveredTo: (message.deliveredTo || []).some((receipt) => isSameChatActor(receipt, {
|
|
293
|
+
actorId: readerId,
|
|
294
|
+
actorModel: readerModel,
|
|
295
|
+
}))
|
|
296
|
+
? message.deliveredTo
|
|
297
|
+
: [
|
|
298
|
+
...(message.deliveredTo || []),
|
|
299
|
+
{
|
|
300
|
+
actorId: readerId,
|
|
301
|
+
actorModel: readerModel,
|
|
302
|
+
deliveredAt: readAt,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
217
305
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
...c,
|
|
225
|
-
participants: c.participants.map((p) => {
|
|
226
|
-
if (p.userId === userId) {
|
|
227
|
-
return {
|
|
228
|
-
...p,
|
|
229
|
-
unreadCounts: {
|
|
230
|
-
messages: 0,
|
|
231
|
-
mentions: 0,
|
|
232
|
-
milestones: 0,
|
|
233
|
-
expenses: 0,
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
return p;
|
|
238
|
-
}),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
return c;
|
|
242
|
-
});
|
|
306
|
+
: message));
|
|
307
|
+
};
|
|
308
|
+
const onTypingStart = ({ conversationId, actorId: typingActorId, actorModel: typingActorModel, }) => {
|
|
309
|
+
const typingActorKey = chatActorKey({
|
|
310
|
+
actorId: typingActorId,
|
|
311
|
+
actorModel: typingActorModel,
|
|
243
312
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// socket.on("poll:created", ({ pollMessage }: { pollMessage: Message }) => {
|
|
247
|
-
// setMessages((prev) => [...prev, pollMessage]);
|
|
248
|
-
// });
|
|
249
|
-
socket.on("poll:updated", ({ pollMessage }) => {
|
|
250
|
-
// Re-fetch messages or update a specific one if referenceId matches
|
|
251
|
-
setMessages(prev => prev.map(m => (m.type === "poll" && m._id === pollMessage._id)
|
|
252
|
-
? pollMessage // Store poll detail in message temporarily for UI
|
|
253
|
-
: m));
|
|
254
|
-
});
|
|
255
|
-
// Message deleted
|
|
256
|
-
socket.on("message:deleted", ({ messageId }) => {
|
|
257
|
-
setMessages((prev) => prev.filter((m) => m._id !== messageId));
|
|
258
|
-
});
|
|
259
|
-
// Typing indicators
|
|
260
|
-
socket.on("typing:start", ({ conversationId, userId }) => {
|
|
261
|
-
setTypingUsers((prev) => {
|
|
262
|
-
const next = new Map(prev);
|
|
313
|
+
setTypingUsers((current) => {
|
|
314
|
+
const next = new Map(current);
|
|
263
315
|
const users = next.get(conversationId) || [];
|
|
264
|
-
if (!users.includes(
|
|
265
|
-
next.set(conversationId, [...users,
|
|
316
|
+
if (!users.includes(typingActorKey)) {
|
|
317
|
+
next.set(conversationId, [...users, typingActorKey]);
|
|
266
318
|
}
|
|
267
319
|
return next;
|
|
268
320
|
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
321
|
+
};
|
|
322
|
+
const onTypingStop = ({ conversationId, actorId: typingActorId, actorModel: typingActorModel, }) => {
|
|
323
|
+
const typingActorKey = chatActorKey({
|
|
324
|
+
actorId: typingActorId,
|
|
325
|
+
actorModel: typingActorModel,
|
|
326
|
+
});
|
|
327
|
+
setTypingUsers((current) => {
|
|
328
|
+
const next = new Map(current);
|
|
329
|
+
next.set(conversationId, (next.get(conversationId) || []).filter((item) => item !== typingActorKey));
|
|
275
330
|
return next;
|
|
276
331
|
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
332
|
+
};
|
|
333
|
+
const onPresenceOnline = ({ actorId: onlineId, actorModel: onlineModel, }) => {
|
|
334
|
+
setOnlineUsers((current) => new Set(current).add(chatActorKey({
|
|
335
|
+
actorId: onlineId,
|
|
336
|
+
actorModel: onlineModel,
|
|
337
|
+
})));
|
|
338
|
+
};
|
|
339
|
+
const onPresenceOffline = ({ actorId: offlineId, actorModel: offlineModel, }) => {
|
|
340
|
+
setOnlineUsers((current) => {
|
|
341
|
+
const next = new Set(current);
|
|
342
|
+
next.delete(chatActorKey({
|
|
343
|
+
actorId: offlineId,
|
|
344
|
+
actorModel: offlineModel,
|
|
345
|
+
}));
|
|
286
346
|
return next;
|
|
287
347
|
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
})
|
|
348
|
+
};
|
|
349
|
+
const onNotification = (notification) => {
|
|
350
|
+
setNotifications((current) => [notification, ...current].slice(0, 50));
|
|
351
|
+
setUnreadNotificationCount((current) => current + 1);
|
|
352
|
+
};
|
|
353
|
+
const onPreferenceUpdated = ({ conversationId, preference, }) => {
|
|
354
|
+
setConversations((current) => current.map((conversation) => conversation._id === conversationId
|
|
355
|
+
? { ...conversation, preference }
|
|
356
|
+
: conversation));
|
|
357
|
+
if (activeConversationRef.current?._id === conversationId) {
|
|
358
|
+
setActiveConversation({
|
|
359
|
+
...activeConversationRef.current,
|
|
360
|
+
preference,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const onConversationMetadataChanged = () => {
|
|
365
|
+
void refreshConversations();
|
|
366
|
+
};
|
|
367
|
+
const onConversationEventChanged = () => {
|
|
368
|
+
setEventRevision((current) => current + 1);
|
|
369
|
+
};
|
|
370
|
+
const onParticipantsChanged = () => {
|
|
371
|
+
void refreshConversations();
|
|
372
|
+
};
|
|
373
|
+
const onPaymentChanged = () => {
|
|
374
|
+
setPaymentRevision((current) => current + 1);
|
|
375
|
+
void refreshConversations();
|
|
376
|
+
};
|
|
377
|
+
socket.on("connect", onConnect);
|
|
378
|
+
socket.on("disconnect", onDisconnect);
|
|
379
|
+
socket.on("message:new", onNewMessage);
|
|
380
|
+
socket.on("message:delivered", onDelivered);
|
|
381
|
+
socket.on("message:updated", onUpdatedMessage);
|
|
382
|
+
socket.on("message:read", onRead);
|
|
383
|
+
socket.on("typing:start", onTypingStart);
|
|
384
|
+
socket.on("typing:stop", onTypingStop);
|
|
385
|
+
socket.on("presence:online", onPresenceOnline);
|
|
386
|
+
socket.on("presence:offline", onPresenceOffline);
|
|
387
|
+
socket.on("notification", onNotification);
|
|
388
|
+
socket.on("conversation:preference.updated", onPreferenceUpdated);
|
|
389
|
+
socket.on("message:pinned", onConversationMetadataChanged);
|
|
390
|
+
socket.on("message:unpinned", onConversationMetadataChanged);
|
|
391
|
+
socket.on("conversation:event.created", onConversationEventChanged);
|
|
392
|
+
socket.on("conversation:event.updated", onConversationEventChanged);
|
|
393
|
+
socket.on("conversation:event.deleted", onConversationEventChanged);
|
|
394
|
+
socket.on("conversation:event.rsvp", onConversationEventChanged);
|
|
395
|
+
socket.on("conversation:participants.updated", onParticipantsChanged);
|
|
396
|
+
socket.on("conversation:payment.updated", onPaymentChanged);
|
|
397
|
+
if (!socket.connected)
|
|
398
|
+
socket.connect();
|
|
399
|
+
else
|
|
400
|
+
onConnect();
|
|
294
401
|
return () => {
|
|
295
402
|
socket.off("connect", onConnect);
|
|
296
403
|
socket.off("disconnect", onDisconnect);
|
|
297
|
-
socket.off("message:new");
|
|
298
|
-
socket.off("message:
|
|
299
|
-
socket.off("message:
|
|
300
|
-
socket.off("
|
|
301
|
-
socket.off("typing:
|
|
302
|
-
socket.off("
|
|
303
|
-
socket.off("presence:
|
|
304
|
-
socket.off("
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
404
|
+
socket.off("message:new", onNewMessage);
|
|
405
|
+
socket.off("message:delivered", onDelivered);
|
|
406
|
+
socket.off("message:updated", onUpdatedMessage);
|
|
407
|
+
socket.off("message:read", onRead);
|
|
408
|
+
socket.off("typing:start", onTypingStart);
|
|
409
|
+
socket.off("typing:stop", onTypingStop);
|
|
410
|
+
socket.off("presence:online", onPresenceOnline);
|
|
411
|
+
socket.off("presence:offline", onPresenceOffline);
|
|
412
|
+
socket.off("notification", onNotification);
|
|
413
|
+
socket.off("conversation:preference.updated", onPreferenceUpdated);
|
|
414
|
+
socket.off("message:pinned", onConversationMetadataChanged);
|
|
415
|
+
socket.off("message:unpinned", onConversationMetadataChanged);
|
|
416
|
+
socket.off("conversation:event.created", onConversationEventChanged);
|
|
417
|
+
socket.off("conversation:event.updated", onConversationEventChanged);
|
|
418
|
+
socket.off("conversation:event.deleted", onConversationEventChanged);
|
|
419
|
+
socket.off("conversation:event.rsvp", onConversationEventChanged);
|
|
420
|
+
socket.off("conversation:participants.updated", onParticipantsChanged);
|
|
421
|
+
socket.off("conversation:payment.updated", onPaymentChanged);
|
|
314
422
|
};
|
|
315
|
-
}, [
|
|
423
|
+
}, [accountType, actorId, actorModel, refreshConversations]);
|
|
316
424
|
return (_jsx(ChatContext.Provider, { value: {
|
|
317
425
|
socket: socketRef.current,
|
|
318
426
|
isConnected,
|
|
@@ -331,8 +439,11 @@ export function ChatProvider({ children }) {
|
|
|
331
439
|
markRead,
|
|
332
440
|
createPoll,
|
|
333
441
|
votePoll,
|
|
442
|
+
updatePoll,
|
|
334
443
|
unreadNotificationCount,
|
|
335
444
|
notifications,
|
|
336
445
|
clearNotifications,
|
|
446
|
+
eventRevision,
|
|
447
|
+
paymentRevision,
|
|
337
448
|
}, children: children }));
|
|
338
449
|
}
|