@myrjfa/state 1.1.1 → 2.0.0
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/dist/index.d.ts +34 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -18
- package/dist/lib/actions/actions.d.ts +170 -170
- package/dist/lib/actions/actions.js +307 -307
- package/dist/lib/actions/auth.d.ts +20 -0
- package/dist/lib/actions/auth.d.ts.map +1 -1
- package/dist/lib/actions/booking.d.ts +30 -0
- package/dist/lib/actions/booking.d.ts.map +1 -0
- package/dist/lib/actions/booking.js +77 -0
- package/dist/lib/actions/fetcher.js +84 -84
- package/dist/lib/actions/severActions.js +2 -2
- package/dist/lib/authSessionManager.js +34 -34
- package/dist/lib/context/ChatContext.js +338 -338
- package/dist/lib/models/adventure.d.ts +75 -0
- package/dist/lib/models/adventure.d.ts.map +1 -0
- package/dist/lib/models/adventure.js +1 -0
- package/dist/lib/models/blog.d.ts +4 -4
- package/dist/lib/models/booking.d.ts +47 -0
- package/dist/lib/models/booking.d.ts.map +1 -0
- package/dist/lib/models/booking.js +1 -0
- package/dist/lib/models/guide.d.ts +43 -0
- package/dist/lib/models/guide.d.ts.map +1 -0
- package/dist/lib/models/guide.js +1 -0
- package/dist/lib/models/notfications.d.ts +93 -93
- package/dist/lib/models/opportunities/freelance.d.ts +74 -6
- package/dist/lib/models/opportunities/freelance.d.ts.map +1 -1
- package/dist/lib/models/opportunities/internship.d.ts +74 -6
- package/dist/lib/models/opportunities/internship.d.ts.map +1 -1
- package/dist/lib/models/opportunities/job.d.ts +74 -6
- package/dist/lib/models/opportunities/job.d.ts.map +1 -1
- package/dist/lib/models/opportunities/opportunity.d.ts +74 -6
- package/dist/lib/models/opportunities/opportunity.d.ts.map +1 -1
- package/dist/lib/models/opportunities/volunteerJob.d.ts +74 -6
- package/dist/lib/models/opportunities/volunteerJob.d.ts.map +1 -1
- package/dist/lib/models/package.d.ts +264 -0
- package/dist/lib/models/package.d.ts.map +1 -0
- package/dist/lib/models/package.js +58 -0
- package/dist/lib/models/portfolio.d.ts +42 -42
- package/dist/lib/models/props.d.ts +3 -0
- package/dist/lib/models/props.d.ts.map +1 -1
- package/dist/lib/models/props.js +36 -0
- package/dist/lib/models/rental.d.ts +85 -0
- package/dist/lib/models/rental.d.ts.map +1 -0
- package/dist/lib/models/rental.js +1 -0
- package/dist/lib/models/review.d.ts +1 -1
- package/dist/lib/models/review.d.ts.map +1 -1
- package/dist/lib/models/stay.d.ts +459 -0
- package/dist/lib/models/stay.d.ts.map +1 -0
- package/dist/lib/models/stay.js +214 -0
- package/dist/lib/models/tile.d.ts +53 -28
- package/dist/lib/models/tile.d.ts.map +1 -1
- package/dist/lib/models/user.d.ts +48 -0
- package/dist/lib/models/user.d.ts.map +1 -1
- package/dist/lib/models/user.js +10 -0
- package/dist/lib/userAtom.d.ts +238 -198
- package/dist/lib/userAtom.d.ts.map +1 -1
- package/dist/lib/userAtom.js +127 -127
- package/package.json +6 -1
- package/dist/lib/actions/property.d.ts +0 -77
- package/dist/lib/actions/property.d.ts.map +0 -1
- package/dist/lib/actions/property.js +0 -133
- package/dist/lib/actions.d.ts +0 -141
- package/dist/lib/actions.d.ts.map +0 -1
- package/dist/lib/actions.js +0 -307
- package/dist/lib/auth.d.ts +0 -150
- package/dist/lib/auth.d.ts.map +0 -1
- package/dist/lib/auth.js +0 -125
- package/dist/lib/fetcher.d.ts +0 -9
- package/dist/lib/fetcher.d.ts.map +0 -1
- package/dist/lib/fetcher.js +0 -84
- package/dist/lib/models/property.d.ts +0 -79
- package/dist/lib/models/property.d.ts.map +0 -1
- package/dist/lib/models/property.js +0 -134
- package/dist/lib/models/volunteerJob.d.ts +0 -398
- package/dist/lib/models/volunteerJob.d.ts.map +0 -1
- package/dist/lib/models/volunteerJob.js +0 -152
- package/dist/lib/severActions.d.ts +0 -3
- package/dist/lib/severActions.d.ts.map +0 -1
- package/dist/lib/severActions.js +0 -19
- package/dist/lib/socket.d.ts +0 -7
- package/dist/lib/socket.d.ts.map +0 -1
- package/dist/lib/socket.js +0 -22
- package/dist/lib/utils/socialMediaUrl.d.ts +0 -25
- package/dist/lib/utils/socialMediaUrl.d.ts.map +0 -1
- package/dist/lib/utils/socialMediaUrl.js +0 -97
|
@@ -1,338 +1,338 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useEffect, useState, useCallback, useRef } from "react";
|
|
4
|
-
import { getSocket } from "../actions/socket";
|
|
5
|
-
import { chatApi } from "../actions/chat";
|
|
6
|
-
import { useAtomValue } from "jotai";
|
|
7
|
-
import { userAtom } from "../userAtom";
|
|
8
|
-
const ChatContext = createContext({
|
|
9
|
-
socket: null,
|
|
10
|
-
isConnected: false,
|
|
11
|
-
conversations: [],
|
|
12
|
-
activeConversation: null,
|
|
13
|
-
setActiveConversation: () => { },
|
|
14
|
-
messages: [],
|
|
15
|
-
setMessages: () => { },
|
|
16
|
-
typingUsers: new Map(),
|
|
17
|
-
onlineUsers: new Set(),
|
|
18
|
-
refreshConversations: async () => { },
|
|
19
|
-
sendMessage: async () => { },
|
|
20
|
-
createPoll: () => { },
|
|
21
|
-
votePoll: () => { },
|
|
22
|
-
editMessage: () => { },
|
|
23
|
-
toggleMessageReaction: () => { },
|
|
24
|
-
deleteMessage: () => { },
|
|
25
|
-
markRead: () => { },
|
|
26
|
-
unreadNotificationCount: 0,
|
|
27
|
-
notifications: [],
|
|
28
|
-
clearNotifications: () => { },
|
|
29
|
-
});
|
|
30
|
-
export function useChatContext() {
|
|
31
|
-
return useContext(ChatContext);
|
|
32
|
-
}
|
|
33
|
-
export function ChatProvider({ children }) {
|
|
34
|
-
const user = useAtomValue(userAtom);
|
|
35
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
36
|
-
const [conversations, setConversations] = useState([]);
|
|
37
|
-
const [activeConversation, _setActiveConversation] = useState(null);
|
|
38
|
-
const activeConversationRef = useRef(null);
|
|
39
|
-
const [messages, setMessages] = useState([]);
|
|
40
|
-
const [typingUsers, setTypingUsers] = useState(new Map());
|
|
41
|
-
const [onlineUsers, setOnlineUsers] = useState(new Set());
|
|
42
|
-
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
|
|
43
|
-
const [notifications, setNotifications] = useState([]);
|
|
44
|
-
const socketRef = useRef(null);
|
|
45
|
-
const setActiveConversation = useCallback((conv) => {
|
|
46
|
-
_setActiveConversation(conv);
|
|
47
|
-
activeConversationRef.current = conv;
|
|
48
|
-
}, []);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
activeConversationRef.current = activeConversation;
|
|
51
|
-
}, [activeConversation]);
|
|
52
|
-
const refreshConversations = useCallback(async () => {
|
|
53
|
-
try {
|
|
54
|
-
const data = await chatApi.getConversations();
|
|
55
|
-
setConversations(data);
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
console.error("Failed to fetch conversations:", err);
|
|
59
|
-
}
|
|
60
|
-
}, []);
|
|
61
|
-
const sendMessage = useCallback(async (conversationId, text, files, replyTo) => {
|
|
62
|
-
let actualId = conversationId;
|
|
63
|
-
// If it's a draft conversation, create it first
|
|
64
|
-
if (conversationId.startsWith("draft_")) {
|
|
65
|
-
const otherUserId = conversationId.replace("draft_", "");
|
|
66
|
-
try {
|
|
67
|
-
const res = await chatApi.createConversation({
|
|
68
|
-
type: "individual",
|
|
69
|
-
otherUserId
|
|
70
|
-
});
|
|
71
|
-
if (res._id) {
|
|
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)
|
|
89
|
-
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
|
-
}
|
|
113
|
-
socketRef.current.emit("message:send", {
|
|
114
|
-
conversationId: actualId,
|
|
115
|
-
content: { text, media },
|
|
116
|
-
type: media.length > 0 ? "media" : "text",
|
|
117
|
-
replyTo,
|
|
118
|
-
});
|
|
119
|
-
}, [refreshConversations, setActiveConversation]);
|
|
120
|
-
const editMessage = useCallback((messageId, text) => {
|
|
121
|
-
if (!socketRef.current)
|
|
122
|
-
return;
|
|
123
|
-
socketRef.current.emit("message:edit", { messageId, text });
|
|
124
|
-
}, []);
|
|
125
|
-
const toggleMessageReaction = useCallback((messageId, reaction) => {
|
|
126
|
-
if (!socketRef.current)
|
|
127
|
-
return;
|
|
128
|
-
socketRef.current.emit("message:react", { messageId, reaction });
|
|
129
|
-
}, []);
|
|
130
|
-
const deleteMessage = useCallback((messageId) => {
|
|
131
|
-
if (!socketRef.current)
|
|
132
|
-
return;
|
|
133
|
-
socketRef.current.emit("message:delete", { messageId });
|
|
134
|
-
}, []);
|
|
135
|
-
const createPoll = useCallback((conversationId, question, options, allowMultiple) => {
|
|
136
|
-
if (!socketRef.current)
|
|
137
|
-
return;
|
|
138
|
-
socketRef.current.emit("poll:create", { conversationId, question, options, allowMultiple });
|
|
139
|
-
}, []);
|
|
140
|
-
const votePoll = useCallback((pollId, optionIndex) => {
|
|
141
|
-
if (!socketRef.current)
|
|
142
|
-
return;
|
|
143
|
-
socketRef.current.emit("poll:vote", { pollId, optionIndex });
|
|
144
|
-
}, []);
|
|
145
|
-
const clearNotifications = useCallback(() => {
|
|
146
|
-
setNotifications([]);
|
|
147
|
-
setUnreadNotificationCount(0);
|
|
148
|
-
}, []);
|
|
149
|
-
const markRead = useCallback((conversationId, unreadIds) => {
|
|
150
|
-
if (!socketRef.current)
|
|
151
|
-
return;
|
|
152
|
-
socketRef.current.emit("message:read", { conversationId, unreadIds });
|
|
153
|
-
}, []);
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (!user) {
|
|
156
|
-
setIsConnected(false);
|
|
157
|
-
setConversations([]);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
const socket = getSocket();
|
|
161
|
-
socketRef.current = socket;
|
|
162
|
-
function onConnect() {
|
|
163
|
-
setIsConnected(true);
|
|
164
|
-
refreshConversations();
|
|
165
|
-
}
|
|
166
|
-
function onDisconnect() {
|
|
167
|
-
setIsConnected(false);
|
|
168
|
-
}
|
|
169
|
-
socket.on("connect", onConnect);
|
|
170
|
-
socket.on("disconnect", onDisconnect);
|
|
171
|
-
if (!socket.connected) {
|
|
172
|
-
socket.connect();
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
// Include logic for if already connected
|
|
176
|
-
onConnect();
|
|
177
|
-
}
|
|
178
|
-
// New message
|
|
179
|
-
socket.on("message:new", ({ conversationId, message }) => {
|
|
180
|
-
setMessages((prev) => {
|
|
181
|
-
const currentActiveId = activeConversationRef.current?._id;
|
|
182
|
-
// If this message belongs to the active conversation, add it
|
|
183
|
-
if (currentActiveId === conversationId) {
|
|
184
|
-
// Deduplicate
|
|
185
|
-
if (prev.some((m) => m._id === message._id))
|
|
186
|
-
return prev;
|
|
187
|
-
return [...prev, message];
|
|
188
|
-
}
|
|
189
|
-
return prev;
|
|
190
|
-
});
|
|
191
|
-
// Update conversations last message
|
|
192
|
-
setConversations((prev) => prev
|
|
193
|
-
.map((c) => c._id === conversationId
|
|
194
|
-
? {
|
|
195
|
-
...c,
|
|
196
|
-
lastMessage: {
|
|
197
|
-
content: message.content?.text || `[${message.type}]`,
|
|
198
|
-
senderId: message.senderId,
|
|
199
|
-
sentAt: message.createdAt,
|
|
200
|
-
},
|
|
201
|
-
updatedAt: message.createdAt,
|
|
202
|
-
}
|
|
203
|
-
: c)
|
|
204
|
-
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()));
|
|
205
|
-
});
|
|
206
|
-
// Message updated
|
|
207
|
-
socket.on("message:updated", ({ message }) => {
|
|
208
|
-
setMessages((prev) => prev.map((m) => (m._id === message._id ? message : m)));
|
|
209
|
-
});
|
|
210
|
-
socket.on("message:readed", ({ conversationId, userId, unreadIds }) => {
|
|
211
|
-
setMessages((prev) => prev.map((m) => {
|
|
212
|
-
if (m.conversationId === conversationId && unreadIds.includes(m._id)) {
|
|
213
|
-
return {
|
|
214
|
-
...m,
|
|
215
|
-
isRead: true,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
return m;
|
|
219
|
-
}));
|
|
220
|
-
setConversations((prev) => {
|
|
221
|
-
return prev.map((c) => {
|
|
222
|
-
if (c._id === conversationId) {
|
|
223
|
-
return {
|
|
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
|
-
});
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
// // Poll created/updated (we treat them as messages that might trigger UI updates)
|
|
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);
|
|
263
|
-
const users = next.get(conversationId) || [];
|
|
264
|
-
if (!users.includes(userId)) {
|
|
265
|
-
next.set(conversationId, [...users, userId]);
|
|
266
|
-
}
|
|
267
|
-
return next;
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
socket.on("typing:stop", ({ conversationId, userId }) => {
|
|
271
|
-
setTypingUsers((prev) => {
|
|
272
|
-
const next = new Map(prev);
|
|
273
|
-
const users = next.get(conversationId) || [];
|
|
274
|
-
next.set(conversationId, users.filter((u) => u !== userId));
|
|
275
|
-
return next;
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
// Online/offline
|
|
279
|
-
socket.on("presence:online", ({ userId }) => {
|
|
280
|
-
setOnlineUsers((prev) => new Set(prev).add(userId));
|
|
281
|
-
});
|
|
282
|
-
socket.on("presence:offline", ({ userId }) => {
|
|
283
|
-
setOnlineUsers((prev) => {
|
|
284
|
-
const next = new Set(prev);
|
|
285
|
-
next.delete(userId);
|
|
286
|
-
return next;
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
// Real-time notifications
|
|
290
|
-
socket.on("notification:new", (notification) => {
|
|
291
|
-
setNotifications((prev) => [notification, ...prev].slice(0, 50));
|
|
292
|
-
setUnreadNotificationCount((prev) => prev + 1);
|
|
293
|
-
});
|
|
294
|
-
return () => {
|
|
295
|
-
socket.off("connect", onConnect);
|
|
296
|
-
socket.off("disconnect", onDisconnect);
|
|
297
|
-
socket.off("message:new");
|
|
298
|
-
socket.off("message:updated");
|
|
299
|
-
socket.off("message:deleted");
|
|
300
|
-
socket.off("typing:start");
|
|
301
|
-
socket.off("typing:stop");
|
|
302
|
-
socket.off("presence:online");
|
|
303
|
-
socket.off("presence:offline");
|
|
304
|
-
socket.off("notification:new");
|
|
305
|
-
// Disconnect on unmount? Maybe not if shared across pages.
|
|
306
|
-
// But if specific to chat...
|
|
307
|
-
// For shared state lib, we might want to keep it open?
|
|
308
|
-
// If user navigates away from chat pages, maybe close it?
|
|
309
|
-
// The original logic disconnected on unmount.
|
|
310
|
-
// But if we want global notifications (unread count), we need it open.
|
|
311
|
-
// Let's keep it open but manage listeners.
|
|
312
|
-
// Actually, if we unmount the provider, we should disconnect or remove listeners.
|
|
313
|
-
// Assuming this Provider wraps only Chat pages.
|
|
314
|
-
};
|
|
315
|
-
}, [user, refreshConversations]);
|
|
316
|
-
return (_jsx(ChatContext.Provider, { value: {
|
|
317
|
-
socket: socketRef.current,
|
|
318
|
-
isConnected,
|
|
319
|
-
conversations,
|
|
320
|
-
activeConversation,
|
|
321
|
-
setActiveConversation,
|
|
322
|
-
messages,
|
|
323
|
-
setMessages,
|
|
324
|
-
typingUsers,
|
|
325
|
-
onlineUsers,
|
|
326
|
-
refreshConversations,
|
|
327
|
-
sendMessage,
|
|
328
|
-
editMessage,
|
|
329
|
-
toggleMessageReaction,
|
|
330
|
-
deleteMessage,
|
|
331
|
-
markRead,
|
|
332
|
-
createPoll,
|
|
333
|
-
votePoll,
|
|
334
|
-
unreadNotificationCount,
|
|
335
|
-
notifications,
|
|
336
|
-
clearNotifications,
|
|
337
|
-
}, children: children }));
|
|
338
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useEffect, useState, useCallback, useRef } from "react";
|
|
4
|
+
import { getSocket } from "../actions/socket";
|
|
5
|
+
import { chatApi } from "../actions/chat";
|
|
6
|
+
import { useAtomValue } from "jotai";
|
|
7
|
+
import { userAtom } from "../userAtom";
|
|
8
|
+
const ChatContext = createContext({
|
|
9
|
+
socket: null,
|
|
10
|
+
isConnected: false,
|
|
11
|
+
conversations: [],
|
|
12
|
+
activeConversation: null,
|
|
13
|
+
setActiveConversation: () => { },
|
|
14
|
+
messages: [],
|
|
15
|
+
setMessages: () => { },
|
|
16
|
+
typingUsers: new Map(),
|
|
17
|
+
onlineUsers: new Set(),
|
|
18
|
+
refreshConversations: async () => { },
|
|
19
|
+
sendMessage: async () => { },
|
|
20
|
+
createPoll: () => { },
|
|
21
|
+
votePoll: () => { },
|
|
22
|
+
editMessage: () => { },
|
|
23
|
+
toggleMessageReaction: () => { },
|
|
24
|
+
deleteMessage: () => { },
|
|
25
|
+
markRead: () => { },
|
|
26
|
+
unreadNotificationCount: 0,
|
|
27
|
+
notifications: [],
|
|
28
|
+
clearNotifications: () => { },
|
|
29
|
+
});
|
|
30
|
+
export function useChatContext() {
|
|
31
|
+
return useContext(ChatContext);
|
|
32
|
+
}
|
|
33
|
+
export function ChatProvider({ children }) {
|
|
34
|
+
const user = useAtomValue(userAtom);
|
|
35
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
36
|
+
const [conversations, setConversations] = useState([]);
|
|
37
|
+
const [activeConversation, _setActiveConversation] = useState(null);
|
|
38
|
+
const activeConversationRef = useRef(null);
|
|
39
|
+
const [messages, setMessages] = useState([]);
|
|
40
|
+
const [typingUsers, setTypingUsers] = useState(new Map());
|
|
41
|
+
const [onlineUsers, setOnlineUsers] = useState(new Set());
|
|
42
|
+
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
|
|
43
|
+
const [notifications, setNotifications] = useState([]);
|
|
44
|
+
const socketRef = useRef(null);
|
|
45
|
+
const setActiveConversation = useCallback((conv) => {
|
|
46
|
+
_setActiveConversation(conv);
|
|
47
|
+
activeConversationRef.current = conv;
|
|
48
|
+
}, []);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
activeConversationRef.current = activeConversation;
|
|
51
|
+
}, [activeConversation]);
|
|
52
|
+
const refreshConversations = useCallback(async () => {
|
|
53
|
+
try {
|
|
54
|
+
const data = await chatApi.getConversations();
|
|
55
|
+
setConversations(data);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error("Failed to fetch conversations:", err);
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
const sendMessage = useCallback(async (conversationId, text, files, replyTo) => {
|
|
62
|
+
let actualId = conversationId;
|
|
63
|
+
// If it's a draft conversation, create it first
|
|
64
|
+
if (conversationId.startsWith("draft_")) {
|
|
65
|
+
const otherUserId = conversationId.replace("draft_", "");
|
|
66
|
+
try {
|
|
67
|
+
const res = await chatApi.createConversation({
|
|
68
|
+
type: "individual",
|
|
69
|
+
otherUserId
|
|
70
|
+
});
|
|
71
|
+
if (res._id) {
|
|
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)
|
|
89
|
+
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
|
+
}
|
|
113
|
+
socketRef.current.emit("message:send", {
|
|
114
|
+
conversationId: actualId,
|
|
115
|
+
content: { text, media },
|
|
116
|
+
type: media.length > 0 ? "media" : "text",
|
|
117
|
+
replyTo,
|
|
118
|
+
});
|
|
119
|
+
}, [refreshConversations, setActiveConversation]);
|
|
120
|
+
const editMessage = useCallback((messageId, text) => {
|
|
121
|
+
if (!socketRef.current)
|
|
122
|
+
return;
|
|
123
|
+
socketRef.current.emit("message:edit", { messageId, text });
|
|
124
|
+
}, []);
|
|
125
|
+
const toggleMessageReaction = useCallback((messageId, reaction) => {
|
|
126
|
+
if (!socketRef.current)
|
|
127
|
+
return;
|
|
128
|
+
socketRef.current.emit("message:react", { messageId, reaction });
|
|
129
|
+
}, []);
|
|
130
|
+
const deleteMessage = useCallback((messageId) => {
|
|
131
|
+
if (!socketRef.current)
|
|
132
|
+
return;
|
|
133
|
+
socketRef.current.emit("message:delete", { messageId });
|
|
134
|
+
}, []);
|
|
135
|
+
const createPoll = useCallback((conversationId, question, options, allowMultiple) => {
|
|
136
|
+
if (!socketRef.current)
|
|
137
|
+
return;
|
|
138
|
+
socketRef.current.emit("poll:create", { conversationId, question, options, allowMultiple });
|
|
139
|
+
}, []);
|
|
140
|
+
const votePoll = useCallback((pollId, optionIndex) => {
|
|
141
|
+
if (!socketRef.current)
|
|
142
|
+
return;
|
|
143
|
+
socketRef.current.emit("poll:vote", { pollId, optionIndex });
|
|
144
|
+
}, []);
|
|
145
|
+
const clearNotifications = useCallback(() => {
|
|
146
|
+
setNotifications([]);
|
|
147
|
+
setUnreadNotificationCount(0);
|
|
148
|
+
}, []);
|
|
149
|
+
const markRead = useCallback((conversationId, unreadIds) => {
|
|
150
|
+
if (!socketRef.current)
|
|
151
|
+
return;
|
|
152
|
+
socketRef.current.emit("message:read", { conversationId, unreadIds });
|
|
153
|
+
}, []);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!user) {
|
|
156
|
+
setIsConnected(false);
|
|
157
|
+
setConversations([]);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const socket = getSocket();
|
|
161
|
+
socketRef.current = socket;
|
|
162
|
+
function onConnect() {
|
|
163
|
+
setIsConnected(true);
|
|
164
|
+
refreshConversations();
|
|
165
|
+
}
|
|
166
|
+
function onDisconnect() {
|
|
167
|
+
setIsConnected(false);
|
|
168
|
+
}
|
|
169
|
+
socket.on("connect", onConnect);
|
|
170
|
+
socket.on("disconnect", onDisconnect);
|
|
171
|
+
if (!socket.connected) {
|
|
172
|
+
socket.connect();
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Include logic for if already connected
|
|
176
|
+
onConnect();
|
|
177
|
+
}
|
|
178
|
+
// New message
|
|
179
|
+
socket.on("message:new", ({ conversationId, message }) => {
|
|
180
|
+
setMessages((prev) => {
|
|
181
|
+
const currentActiveId = activeConversationRef.current?._id;
|
|
182
|
+
// If this message belongs to the active conversation, add it
|
|
183
|
+
if (currentActiveId === conversationId) {
|
|
184
|
+
// Deduplicate
|
|
185
|
+
if (prev.some((m) => m._id === message._id))
|
|
186
|
+
return prev;
|
|
187
|
+
return [...prev, message];
|
|
188
|
+
}
|
|
189
|
+
return prev;
|
|
190
|
+
});
|
|
191
|
+
// Update conversations last message
|
|
192
|
+
setConversations((prev) => prev
|
|
193
|
+
.map((c) => c._id === conversationId
|
|
194
|
+
? {
|
|
195
|
+
...c,
|
|
196
|
+
lastMessage: {
|
|
197
|
+
content: message.content?.text || `[${message.type}]`,
|
|
198
|
+
senderId: message.senderId,
|
|
199
|
+
sentAt: message.createdAt,
|
|
200
|
+
},
|
|
201
|
+
updatedAt: message.createdAt,
|
|
202
|
+
}
|
|
203
|
+
: c)
|
|
204
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()));
|
|
205
|
+
});
|
|
206
|
+
// Message updated
|
|
207
|
+
socket.on("message:updated", ({ message }) => {
|
|
208
|
+
setMessages((prev) => prev.map((m) => (m._id === message._id ? message : m)));
|
|
209
|
+
});
|
|
210
|
+
socket.on("message:readed", ({ conversationId, userId, unreadIds }) => {
|
|
211
|
+
setMessages((prev) => prev.map((m) => {
|
|
212
|
+
if (m.conversationId === conversationId && unreadIds.includes(m._id)) {
|
|
213
|
+
return {
|
|
214
|
+
...m,
|
|
215
|
+
isRead: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return m;
|
|
219
|
+
}));
|
|
220
|
+
setConversations((prev) => {
|
|
221
|
+
return prev.map((c) => {
|
|
222
|
+
if (c._id === conversationId) {
|
|
223
|
+
return {
|
|
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
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
// // Poll created/updated (we treat them as messages that might trigger UI updates)
|
|
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);
|
|
263
|
+
const users = next.get(conversationId) || [];
|
|
264
|
+
if (!users.includes(userId)) {
|
|
265
|
+
next.set(conversationId, [...users, userId]);
|
|
266
|
+
}
|
|
267
|
+
return next;
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
socket.on("typing:stop", ({ conversationId, userId }) => {
|
|
271
|
+
setTypingUsers((prev) => {
|
|
272
|
+
const next = new Map(prev);
|
|
273
|
+
const users = next.get(conversationId) || [];
|
|
274
|
+
next.set(conversationId, users.filter((u) => u !== userId));
|
|
275
|
+
return next;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
// Online/offline
|
|
279
|
+
socket.on("presence:online", ({ userId }) => {
|
|
280
|
+
setOnlineUsers((prev) => new Set(prev).add(userId));
|
|
281
|
+
});
|
|
282
|
+
socket.on("presence:offline", ({ userId }) => {
|
|
283
|
+
setOnlineUsers((prev) => {
|
|
284
|
+
const next = new Set(prev);
|
|
285
|
+
next.delete(userId);
|
|
286
|
+
return next;
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
// Real-time notifications
|
|
290
|
+
socket.on("notification:new", (notification) => {
|
|
291
|
+
setNotifications((prev) => [notification, ...prev].slice(0, 50));
|
|
292
|
+
setUnreadNotificationCount((prev) => prev + 1);
|
|
293
|
+
});
|
|
294
|
+
return () => {
|
|
295
|
+
socket.off("connect", onConnect);
|
|
296
|
+
socket.off("disconnect", onDisconnect);
|
|
297
|
+
socket.off("message:new");
|
|
298
|
+
socket.off("message:updated");
|
|
299
|
+
socket.off("message:deleted");
|
|
300
|
+
socket.off("typing:start");
|
|
301
|
+
socket.off("typing:stop");
|
|
302
|
+
socket.off("presence:online");
|
|
303
|
+
socket.off("presence:offline");
|
|
304
|
+
socket.off("notification:new");
|
|
305
|
+
// Disconnect on unmount? Maybe not if shared across pages.
|
|
306
|
+
// But if specific to chat...
|
|
307
|
+
// For shared state lib, we might want to keep it open?
|
|
308
|
+
// If user navigates away from chat pages, maybe close it?
|
|
309
|
+
// The original logic disconnected on unmount.
|
|
310
|
+
// But if we want global notifications (unread count), we need it open.
|
|
311
|
+
// Let's keep it open but manage listeners.
|
|
312
|
+
// Actually, if we unmount the provider, we should disconnect or remove listeners.
|
|
313
|
+
// Assuming this Provider wraps only Chat pages.
|
|
314
|
+
};
|
|
315
|
+
}, [user, refreshConversations]);
|
|
316
|
+
return (_jsx(ChatContext.Provider, { value: {
|
|
317
|
+
socket: socketRef.current,
|
|
318
|
+
isConnected,
|
|
319
|
+
conversations,
|
|
320
|
+
activeConversation,
|
|
321
|
+
setActiveConversation,
|
|
322
|
+
messages,
|
|
323
|
+
setMessages,
|
|
324
|
+
typingUsers,
|
|
325
|
+
onlineUsers,
|
|
326
|
+
refreshConversations,
|
|
327
|
+
sendMessage,
|
|
328
|
+
editMessage,
|
|
329
|
+
toggleMessageReaction,
|
|
330
|
+
deleteMessage,
|
|
331
|
+
markRead,
|
|
332
|
+
createPoll,
|
|
333
|
+
votePoll,
|
|
334
|
+
unreadNotificationCount,
|
|
335
|
+
notifications,
|
|
336
|
+
clearNotifications,
|
|
337
|
+
}, children: children }));
|
|
338
|
+
}
|