hazo_chat 2.1.0 → 3.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.
Files changed (33) hide show
  1. package/README.md +182 -67
  2. package/SETUP_CHECKLIST.md +773 -67
  3. package/dist/api/index.d.ts +17 -2
  4. package/dist/api/index.d.ts.map +1 -1
  5. package/dist/api/index.js +16 -1
  6. package/dist/api/index.js.map +1 -1
  7. package/dist/api/messages.d.ts +34 -1
  8. package/dist/api/messages.d.ts.map +1 -1
  9. package/dist/api/messages.js +340 -47
  10. package/dist/api/messages.js.map +1 -1
  11. package/dist/api/types.d.ts +50 -2
  12. package/dist/api/types.d.ts.map +1 -1
  13. package/dist/api/unread_count.d.ts +19 -10
  14. package/dist/api/unread_count.d.ts.map +1 -1
  15. package/dist/api/unread_count.js +54 -30
  16. package/dist/api/unread_count.js.map +1 -1
  17. package/dist/components/hazo_chat/hazo_chat.d.ts.map +1 -1
  18. package/dist/components/hazo_chat/hazo_chat.js +23 -15
  19. package/dist/components/hazo_chat/hazo_chat.js.map +1 -1
  20. package/dist/components/hazo_chat/hazo_chat_header.d.ts.map +1 -1
  21. package/dist/components/hazo_chat/hazo_chat_header.js +17 -4
  22. package/dist/components/hazo_chat/hazo_chat_header.js.map +1 -1
  23. package/dist/components/hazo_chat/hazo_chat_messages.d.ts +5 -4
  24. package/dist/components/hazo_chat/hazo_chat_messages.d.ts.map +1 -1
  25. package/dist/components/hazo_chat/hazo_chat_messages.js +48 -8
  26. package/dist/components/hazo_chat/hazo_chat_messages.js.map +1 -1
  27. package/dist/hooks/use_chat_messages.d.ts +5 -5
  28. package/dist/hooks/use_chat_messages.d.ts.map +1 -1
  29. package/dist/hooks/use_chat_messages.js +247 -148
  30. package/dist/hooks/use_chat_messages.js.map +1 -1
  31. package/dist/types/index.d.ts +162 -7
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/package.json +1 -1
@@ -7,17 +7,33 @@
7
7
  * - Optimistic updates for sent messages
8
8
  * - Soft delete functionality
9
9
  * - Exponential backoff on errors
10
+ * - Abstracted transport layer for future WebSocket/SSE support
10
11
  *
11
12
  * Uses fetch() to call API endpoints instead of direct database access.
12
13
  * This allows the hook to work in client components without Node.js dependencies.
13
14
  */
14
15
  'use client';
15
- import { useState, useEffect, useCallback, useRef } from 'react';
16
- import { DEFAULT_REALTIME_MODE, DEFAULT_POLLING_INTERVAL, DEFAULT_MESSAGES_PER_PAGE, MAX_RETRY_ATTEMPTS, RETRY_BASE_DELAY } from '../lib/constants.js';
16
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
17
+ import { DEFAULT_REALTIME_MODE, DEFAULT_POLLING_INTERVAL, DEFAULT_MESSAGES_PER_PAGE, MAX_RETRY_ATTEMPTS, } from '../lib/constants.js';
18
+ // ============================================================================
19
+ // Constants
20
+ // ============================================================================
21
+ /** Maximum entries in the user profile cache */
22
+ const PROFILE_CACHE_MAX_SIZE = 200;
23
+ /** Cache TTL in milliseconds (30 minutes) */
24
+ const PROFILE_CACHE_TTL = 1000 * 60 * 30;
25
+ /** Maximum polling delay cap in milliseconds */
26
+ const MAX_POLLING_DELAY = 30000;
27
+ // ============================================================================
28
+ // Helper: Generate unique ID for optimistic messages
29
+ // ============================================================================
30
+ function generateOptimisticId() {
31
+ return `optimistic-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
32
+ }
17
33
  // ============================================================================
18
34
  // Hook Implementation
19
35
  // ============================================================================
20
- export function useChatMessages({ receiver_user_id, reference_id = '', reference_type = 'chat', api_base_url = '/api/hazo_chat', realtime_mode = DEFAULT_REALTIME_MODE, polling_interval = DEFAULT_POLLING_INTERVAL, messages_per_page = DEFAULT_MESSAGES_PER_PAGE }) {
36
+ export function useChatMessages({ chat_group_id, reference_id = '', reference_type = 'chat', api_base_url = '/api/hazo_chat', realtime_mode = DEFAULT_REALTIME_MODE, polling_interval = DEFAULT_POLLING_INTERVAL, messages_per_page = DEFAULT_MESSAGES_PER_PAGE, }) {
21
37
  // -------------------------------------------------------------------------
22
38
  // State
23
39
  // -------------------------------------------------------------------------
@@ -29,13 +45,26 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
29
45
  const [polling_status, set_polling_status] = useState('connected');
30
46
  const [current_user_id, set_current_user_id] = useState(null);
31
47
  // -------------------------------------------------------------------------
32
- // Refs
48
+ // Refs (stable across renders)
33
49
  // -------------------------------------------------------------------------
34
- const cursor_ref = useRef(0);
50
+ const cursor_ref = useRef(null);
35
51
  const retry_count_ref = useRef(0);
36
52
  const user_profiles_cache_ref = useRef(new Map());
37
53
  const polling_timer_ref = useRef(null);
38
54
  const is_mounted_ref = useRef(true);
55
+ const is_polling_ref = useRef(false);
56
+ // -------------------------------------------------------------------------
57
+ // Memoized config to prevent effect re-runs
58
+ // -------------------------------------------------------------------------
59
+ const config = useMemo(() => ({
60
+ chat_group_id,
61
+ reference_id,
62
+ reference_type,
63
+ api_base_url,
64
+ realtime_mode,
65
+ polling_interval,
66
+ messages_per_page,
67
+ }), [chat_group_id, reference_id, reference_type, api_base_url, realtime_mode, polling_interval, messages_per_page]);
39
68
  // -------------------------------------------------------------------------
40
69
  // Cleanup on unmount
41
70
  // -------------------------------------------------------------------------
@@ -44,29 +73,77 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
44
73
  return () => {
45
74
  is_mounted_ref.current = false;
46
75
  if (polling_timer_ref.current) {
47
- clearInterval(polling_timer_ref.current);
76
+ clearTimeout(polling_timer_ref.current);
77
+ polling_timer_ref.current = null;
48
78
  }
49
79
  };
50
80
  }, []);
51
81
  // -------------------------------------------------------------------------
52
- // Fetch user profiles via API
82
+ // User profile cache with TTL and eviction
83
+ // -------------------------------------------------------------------------
84
+ const get_cached_profile = useCallback((user_id) => {
85
+ const entry = user_profiles_cache_ref.current.get(user_id);
86
+ if (!entry)
87
+ return null;
88
+ // Check TTL
89
+ if (Date.now() - entry.timestamp > PROFILE_CACHE_TTL) {
90
+ user_profiles_cache_ref.current.delete(user_id);
91
+ return null;
92
+ }
93
+ return entry.value;
94
+ }, []);
95
+ const set_cached_profile = useCallback((profile) => {
96
+ // Evict oldest entries if cache is too large
97
+ if (user_profiles_cache_ref.current.size >= PROFILE_CACHE_MAX_SIZE) {
98
+ // Find and remove oldest entry
99
+ let oldest_key = null;
100
+ let oldest_timestamp = Infinity;
101
+ user_profiles_cache_ref.current.forEach((entry, key) => {
102
+ if (entry.timestamp < oldest_timestamp) {
103
+ oldest_timestamp = entry.timestamp;
104
+ oldest_key = key;
105
+ }
106
+ });
107
+ if (oldest_key) {
108
+ user_profiles_cache_ref.current.delete(oldest_key);
109
+ }
110
+ }
111
+ user_profiles_cache_ref.current.set(profile.id, {
112
+ value: profile,
113
+ timestamp: Date.now(),
114
+ });
115
+ }, []);
116
+ // -------------------------------------------------------------------------
117
+ // Fetch user profiles via API (with cache)
53
118
  // -------------------------------------------------------------------------
54
119
  const fetch_user_profiles = useCallback(async (user_ids) => {
55
- const uncached_ids = user_ids.filter((id) => !user_profiles_cache_ref.current.has(id));
120
+ const result = new Map();
121
+ const uncached_ids = [];
122
+ // Check cache first
123
+ user_ids.forEach((id) => {
124
+ const cached = get_cached_profile(id);
125
+ if (cached) {
126
+ result.set(id, cached);
127
+ }
128
+ else {
129
+ uncached_ids.push(id);
130
+ }
131
+ });
132
+ // Fetch uncached profiles
56
133
  if (uncached_ids.length > 0) {
57
134
  try {
58
- // Use the hazo_auth profiles endpoint
59
135
  const response = await fetch('/api/hazo_auth/profiles', {
60
136
  method: 'POST',
61
137
  headers: { 'Content-Type': 'application/json' },
62
138
  body: JSON.stringify({ user_ids: uncached_ids }),
63
- credentials: 'include'
139
+ credentials: 'include',
64
140
  });
65
141
  if (response.ok) {
66
142
  const data = await response.json();
67
143
  if (data.success && data.profiles) {
68
144
  data.profiles.forEach((profile) => {
69
- user_profiles_cache_ref.current.set(profile.id, profile);
145
+ set_cached_profile(profile);
146
+ result.set(profile.id, profile);
70
147
  });
71
148
  }
72
149
  }
@@ -75,17 +152,16 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
75
152
  console.error('[useChatMessages] Failed to fetch user profiles:', err);
76
153
  }
77
154
  }
78
- return user_profiles_cache_ref.current;
79
- }, []);
155
+ return result;
156
+ }, [get_cached_profile, set_cached_profile]);
80
157
  // -------------------------------------------------------------------------
81
158
  // Transform DB messages to ChatMessage
82
159
  // -------------------------------------------------------------------------
83
160
  const transform_messages = useCallback(async (db_messages, user_id) => {
84
- // Collect all unique user IDs
161
+ // Collect all unique sender IDs (no longer need receiver since it's group-based)
85
162
  const user_ids = new Set();
86
163
  db_messages.forEach((msg) => {
87
164
  user_ids.add(msg.sender_user_id);
88
- user_ids.add(msg.receiver_user_id);
89
165
  });
90
166
  // Fetch profiles
91
167
  const profiles = await fetch_user_profiles(Array.from(user_ids));
@@ -93,26 +169,45 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
93
169
  return db_messages.map((msg) => ({
94
170
  ...msg,
95
171
  sender_profile: profiles.get(msg.sender_user_id),
96
- receiver_profile: profiles.get(msg.receiver_user_id),
97
172
  is_sender: msg.sender_user_id === user_id,
98
- send_status: 'sent'
173
+ send_status: 'sent',
99
174
  }));
100
175
  }, [fetch_user_profiles]);
101
176
  // -------------------------------------------------------------------------
102
- // Fetch messages via API
177
+ // Calculate polling delay with exponential backoff
178
+ // -------------------------------------------------------------------------
179
+ const get_poll_delay = useCallback(() => {
180
+ if (retry_count_ref.current === 0) {
181
+ return config.polling_interval;
182
+ }
183
+ // Exponential backoff: interval * 2^retryCount, capped at MAX_POLLING_DELAY
184
+ const delay = config.polling_interval * Math.pow(2, retry_count_ref.current);
185
+ return Math.min(delay, MAX_POLLING_DELAY);
186
+ }, [config.polling_interval]);
187
+ // -------------------------------------------------------------------------
188
+ // Fetch messages via API with optional pagination
103
189
  // -------------------------------------------------------------------------
104
- const fetch_messages_from_api = useCallback(async () => {
105
- if (!receiver_user_id) {
106
- return { messages: [], user_id: null };
190
+ const fetch_messages_from_api = useCallback(async (options) => {
191
+ if (!config.chat_group_id) {
192
+ return { messages: [], user_id: null, has_more: false, next_cursor: null };
107
193
  }
108
194
  try {
109
195
  const params = new URLSearchParams({
110
- receiver_user_id,
111
- ...(reference_id && { reference_id }),
112
- ...(reference_type && { reference_type }),
196
+ chat_group_id: config.chat_group_id,
197
+ limit: String(options?.limit || config.messages_per_page),
113
198
  });
114
- const response = await fetch(`${api_base_url}/messages?${params.toString()}`, {
115
- credentials: 'include'
199
+ if (config.reference_id) {
200
+ params.set('reference_id', config.reference_id);
201
+ }
202
+ if (config.reference_type) {
203
+ params.set('reference_type', config.reference_type);
204
+ }
205
+ if (options?.cursor) {
206
+ params.set('cursor', options.cursor);
207
+ params.set('direction', options.direction || 'older');
208
+ }
209
+ const response = await fetch(`${config.api_base_url}/messages?${params.toString()}`, {
210
+ credentials: 'include',
116
211
  });
117
212
  if (!response.ok) {
118
213
  throw new Error(`HTTP ${response.status}`);
@@ -121,7 +216,9 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
121
216
  if (data.success) {
122
217
  return {
123
218
  messages: data.messages || [],
124
- user_id: data.current_user_id || null
219
+ user_id: data.current_user_id || null,
220
+ has_more: data.pagination?.has_more ?? false,
221
+ next_cursor: data.pagination?.next_cursor ?? null,
125
222
  };
126
223
  }
127
224
  else {
@@ -132,31 +229,35 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
132
229
  console.error('[useChatMessages] Fetch error:', err);
133
230
  throw err;
134
231
  }
135
- }, [receiver_user_id, reference_id, reference_type, api_base_url]);
232
+ }, [config]);
136
233
  // -------------------------------------------------------------------------
137
234
  // Initial load
138
235
  // -------------------------------------------------------------------------
139
236
  const load_initial = useCallback(async () => {
140
- if (!receiver_user_id) {
237
+ if (!config.chat_group_id) {
141
238
  set_is_loading(false);
142
239
  return;
143
240
  }
144
241
  set_is_loading(true);
145
242
  set_error(null);
146
243
  try {
147
- const { messages: db_messages, user_id } = await fetch_messages_from_api();
148
- if (user_id && is_mounted_ref.current) {
149
- set_current_user_id(user_id);
244
+ const result = await fetch_messages_from_api();
245
+ if (result.user_id && is_mounted_ref.current) {
246
+ set_current_user_id(result.user_id);
150
247
  }
151
248
  if (is_mounted_ref.current) {
152
- const transformed = user_id
153
- ? await transform_messages(db_messages, user_id)
154
- : db_messages.map(msg => ({ ...msg, is_sender: false, send_status: 'sent' }));
249
+ const transformed = result.user_id
250
+ ? await transform_messages(result.messages, result.user_id)
251
+ : result.messages.map((msg) => ({
252
+ ...msg,
253
+ is_sender: false,
254
+ send_status: 'sent',
255
+ }));
155
256
  // Sort messages in ascending order (oldest first, newest last)
156
257
  const sorted = transformed.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
157
258
  set_messages(sorted);
158
- set_has_more(db_messages.length >= messages_per_page);
159
- cursor_ref.current = db_messages.length;
259
+ set_has_more(result.has_more);
260
+ cursor_ref.current = result.next_cursor;
160
261
  retry_count_ref.current = 0;
161
262
  set_polling_status('connected');
162
263
  }
@@ -172,25 +273,32 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
172
273
  set_is_loading(false);
173
274
  }
174
275
  }
175
- }, [receiver_user_id, fetch_messages_from_api, transform_messages, messages_per_page]);
276
+ }, [config.chat_group_id, fetch_messages_from_api, transform_messages]);
176
277
  // -------------------------------------------------------------------------
177
- // Load more (pagination) - Note: simplified, API should support pagination params
278
+ // Load more (pagination)
178
279
  // -------------------------------------------------------------------------
179
280
  const load_more = useCallback(async () => {
180
- if (!current_user_id || !has_more || is_loading_more) {
281
+ if (!current_user_id || !has_more || is_loading_more || !cursor_ref.current) {
181
282
  return;
182
283
  }
183
284
  set_is_loading_more(true);
184
285
  try {
185
- // For now, reload all messages - pagination can be added to API later
186
- const { messages: db_messages } = await fetch_messages_from_api();
187
- const transformed = await transform_messages(db_messages, current_user_id);
286
+ const result = await fetch_messages_from_api({
287
+ cursor: cursor_ref.current,
288
+ direction: 'older',
289
+ });
290
+ const transformed = await transform_messages(result.messages, current_user_id);
188
291
  if (is_mounted_ref.current) {
189
- // Sort messages in ascending order (oldest first, newest last)
190
- const sorted = transformed.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
191
- set_messages(sorted);
192
- set_has_more(false); // Simplified - loaded all
193
- cursor_ref.current = db_messages.length;
292
+ set_messages((prev) => {
293
+ // Prepend older messages and sort
294
+ const combined = [...transformed, ...prev];
295
+ return combined.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
296
+ });
297
+ set_has_more(result.has_more);
298
+ // Update cursor to oldest message for next load_more
299
+ if (result.messages.length > 0) {
300
+ cursor_ref.current = result.messages[0].created_at;
301
+ }
194
302
  }
195
303
  }
196
304
  catch (err) {
@@ -203,88 +311,87 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
203
311
  }
204
312
  }, [current_user_id, has_more, is_loading_more, fetch_messages_from_api, transform_messages]);
205
313
  // -------------------------------------------------------------------------
206
- // Poll for new messages
314
+ // Poll for new messages (uses setTimeout for proper backoff)
207
315
  // -------------------------------------------------------------------------
208
- const poll_for_new_messages = useCallback(async () => {
209
- if (!current_user_id || !receiver_user_id) {
316
+ const schedule_next_poll = useCallback(() => {
317
+ if (!is_mounted_ref.current || config.realtime_mode !== 'polling' || !config.chat_group_id) {
210
318
  return;
211
319
  }
212
- try {
213
- const { messages: db_messages } = await fetch_messages_from_api();
214
- if (db_messages.length > 0 && is_mounted_ref.current) {
215
- const transformed = await transform_messages(db_messages, current_user_id);
216
- set_messages((prev) => {
217
- // Merge new messages, avoiding duplicates
218
- const existing_ids = new Set(prev.map(m => m.id));
219
- const new_messages = transformed.filter(m => !existing_ids.has(m.id));
220
- if (new_messages.length > 0) {
221
- // Combine and sort by created_at
222
- const combined = [...prev, ...new_messages];
223
- combined.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
224
- return combined;
225
- }
226
- return prev;
227
- });
320
+ const delay = get_poll_delay();
321
+ polling_timer_ref.current = setTimeout(async () => {
322
+ if (!is_mounted_ref.current || is_polling_ref.current) {
323
+ return;
228
324
  }
229
- retry_count_ref.current = 0;
230
- if (is_mounted_ref.current) {
231
- set_polling_status('connected');
325
+ is_polling_ref.current = true;
326
+ try {
327
+ // Fetch only newer messages since our latest
328
+ const latest_message = messages[messages.length - 1];
329
+ const result = await fetch_messages_from_api(latest_message
330
+ ? { cursor: latest_message.created_at, direction: 'newer', limit: 50 }
331
+ : undefined);
332
+ if (result.messages.length > 0 && is_mounted_ref.current && current_user_id) {
333
+ const transformed = await transform_messages(result.messages, current_user_id);
334
+ set_messages((prev) => {
335
+ // Merge new messages, avoiding duplicates
336
+ const existing_ids = new Set(prev.map((m) => m.id));
337
+ const new_messages = transformed.filter((m) => !existing_ids.has(m.id));
338
+ if (new_messages.length > 0) {
339
+ const combined = [...prev, ...new_messages];
340
+ return combined.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
341
+ }
342
+ return prev;
343
+ });
344
+ }
345
+ // Reset retry count on success
346
+ retry_count_ref.current = 0;
347
+ if (is_mounted_ref.current) {
348
+ set_polling_status('connected');
349
+ }
232
350
  }
233
- }
234
- catch (err) {
235
- console.error('[useChatMessages] Polling error:', err);
236
- retry_count_ref.current += 1;
237
- if (is_mounted_ref.current) {
238
- if (retry_count_ref.current >= MAX_RETRY_ATTEMPTS) {
239
- set_polling_status('error');
351
+ catch (err) {
352
+ console.error('[useChatMessages] Polling error:', err);
353
+ retry_count_ref.current += 1;
354
+ if (is_mounted_ref.current) {
355
+ if (retry_count_ref.current >= MAX_RETRY_ATTEMPTS) {
356
+ set_polling_status('error');
357
+ }
358
+ else {
359
+ set_polling_status('reconnecting');
360
+ }
240
361
  }
241
- else {
242
- set_polling_status('reconnecting');
362
+ }
363
+ finally {
364
+ is_polling_ref.current = false;
365
+ // Schedule next poll with updated delay (backoff applied via get_poll_delay)
366
+ if (is_mounted_ref.current && config.realtime_mode === 'polling') {
367
+ schedule_next_poll();
243
368
  }
244
369
  }
245
- }
246
- }, [current_user_id, receiver_user_id, fetch_messages_from_api, transform_messages]);
370
+ }, delay);
371
+ }, [config.realtime_mode, config.chat_group_id, get_poll_delay, fetch_messages_from_api, transform_messages, messages, current_user_id]);
247
372
  // -------------------------------------------------------------------------
248
- // Start polling (only if realtime_mode is 'polling')
373
+ // Start/stop polling based on realtime_mode
249
374
  // -------------------------------------------------------------------------
250
375
  useEffect(() => {
251
- // Only start polling if mode is 'polling'
252
- if (realtime_mode !== 'polling' || !receiver_user_id) {
253
- // Clear any existing timer if switching to manual mode
254
- if (polling_timer_ref.current) {
255
- clearInterval(polling_timer_ref.current);
256
- polling_timer_ref.current = null;
257
- }
258
- // Set status to connected for manual mode (no polling needed)
259
- if (realtime_mode === 'manual') {
260
- set_polling_status('connected');
261
- }
262
- return;
263
- }
264
376
  // Clear any existing timer
265
377
  if (polling_timer_ref.current) {
266
- clearInterval(polling_timer_ref.current);
378
+ clearTimeout(polling_timer_ref.current);
379
+ polling_timer_ref.current = null;
380
+ }
381
+ // Only start polling if mode is 'polling' and we have a chat group
382
+ if (config.realtime_mode === 'polling' && config.chat_group_id && current_user_id) {
383
+ schedule_next_poll();
384
+ }
385
+ else if (config.realtime_mode === 'manual') {
386
+ set_polling_status('connected');
267
387
  }
268
- // Calculate delay with exponential backoff
269
- const get_poll_delay = () => {
270
- if (retry_count_ref.current === 0) {
271
- return polling_interval;
272
- }
273
- return Math.min(polling_interval * Math.pow(2, retry_count_ref.current), RETRY_BASE_DELAY * 30 // Cap at 30 seconds
274
- );
275
- };
276
- const start_polling = () => {
277
- polling_timer_ref.current = setInterval(() => {
278
- poll_for_new_messages();
279
- }, get_poll_delay());
280
- };
281
- start_polling();
282
388
  return () => {
283
389
  if (polling_timer_ref.current) {
284
- clearInterval(polling_timer_ref.current);
390
+ clearTimeout(polling_timer_ref.current);
391
+ polling_timer_ref.current = null;
285
392
  }
286
393
  };
287
- }, [receiver_user_id, realtime_mode, polling_interval, poll_for_new_messages]);
394
+ }, [config.realtime_mode, config.chat_group_id, current_user_id, schedule_next_poll]);
288
395
  // -------------------------------------------------------------------------
289
396
  // Initial load effect
290
397
  // -------------------------------------------------------------------------
@@ -299,38 +406,37 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
299
406
  set_error('Not authenticated');
300
407
  return false;
301
408
  }
302
- // Create optimistic message
303
- const optimistic_id = `optimistic-${Date.now()}`;
409
+ // Create optimistic message with unique ID
410
+ const optimistic_id = generateOptimisticId();
411
+ const sender_profile = get_cached_profile(current_user_id);
304
412
  const optimistic_message = {
305
413
  id: optimistic_id,
306
414
  reference_id: payload.reference_id,
307
415
  reference_type: payload.reference_type,
308
416
  sender_user_id: current_user_id,
309
- receiver_user_id: payload.receiver_user_id,
417
+ chat_group_id: payload.chat_group_id,
310
418
  message_text: payload.message_text,
311
419
  reference_list: payload.reference_list || null,
312
420
  read_at: null,
313
421
  deleted_at: null,
314
422
  created_at: new Date().toISOString(),
315
423
  changed_at: new Date().toISOString(),
316
- sender_profile: user_profiles_cache_ref.current.get(current_user_id),
317
- receiver_profile: user_profiles_cache_ref.current.get(payload.receiver_user_id),
424
+ sender_profile: sender_profile || undefined,
318
425
  is_sender: true,
319
- send_status: 'sending'
426
+ send_status: 'sending',
320
427
  };
321
- // Add optimistic message to state (will be sorted when real message arrives)
428
+ // Add optimistic message to state
322
429
  set_messages((prev) => {
323
430
  const updated = [...prev, optimistic_message];
324
- // Sort to maintain chronological order
325
431
  return updated.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
326
432
  });
327
433
  try {
328
- const response = await fetch(`${api_base_url}/messages`, {
434
+ const response = await fetch(`${config.api_base_url}/messages`, {
329
435
  method: 'POST',
330
436
  headers: { 'Content-Type': 'application/json' },
331
437
  credentials: 'include',
332
438
  body: JSON.stringify({
333
- receiver_user_id: payload.receiver_user_id,
439
+ chat_group_id: payload.chat_group_id,
334
440
  message_text: payload.message_text,
335
441
  reference_id: payload.reference_id,
336
442
  reference_type: payload.reference_type,
@@ -341,27 +447,24 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
341
447
  // Replace optimistic message with real one
342
448
  const real_message = {
343
449
  ...data.message,
344
- // Ensure all required fields are set with proper defaults
345
450
  reference_list: data.message.reference_list ?? null,
346
451
  read_at: data.message.read_at ?? null,
347
452
  deleted_at: data.message.deleted_at ?? null,
348
453
  changed_at: data.message.changed_at ?? data.message.created_at,
349
- sender_profile: user_profiles_cache_ref.current.get(current_user_id),
350
- receiver_profile: user_profiles_cache_ref.current.get(payload.receiver_user_id),
454
+ sender_profile: sender_profile || undefined,
351
455
  is_sender: true,
352
- send_status: 'sent'
456
+ send_status: 'sent',
353
457
  };
354
458
  set_messages((prev) => {
355
459
  // Check if real message already exists (from polling)
356
- const real_message_exists = prev.some(msg => msg.id === real_message.id);
460
+ const real_message_exists = prev.some((msg) => msg.id === real_message.id);
357
461
  if (real_message_exists) {
358
462
  // Real message already exists from polling, just remove optimistic one
359
- return prev.filter(msg => msg.id !== optimistic_id);
463
+ return prev.filter((msg) => msg.id !== optimistic_id);
360
464
  }
361
465
  else {
362
466
  // Replace optimistic message with real one
363
467
  const replaced = prev.map((msg) => msg.id === optimistic_id ? real_message : msg);
364
- // Sort to ensure correct chronological order
365
468
  return replaced.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
366
469
  }
367
470
  });
@@ -375,13 +478,11 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
375
478
  console.error('[useChatMessages] Send error:', err);
376
479
  // Mark optimistic message as failed
377
480
  if (is_mounted_ref.current) {
378
- set_messages((prev) => prev.map((msg) => msg.id === optimistic_id
379
- ? { ...msg, send_status: 'failed' }
380
- : msg));
481
+ set_messages((prev) => prev.map((msg) => msg.id === optimistic_id ? { ...msg, send_status: 'failed' } : msg));
381
482
  }
382
483
  return false;
383
484
  }
384
- }, [current_user_id, api_base_url]);
485
+ }, [current_user_id, config.api_base_url, get_cached_profile]);
385
486
  // -------------------------------------------------------------------------
386
487
  // Delete message (soft delete) via API
387
488
  // -------------------------------------------------------------------------
@@ -395,14 +496,17 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
395
496
  set_error('Cannot delete this message');
396
497
  return false;
397
498
  }
499
+ // Store original values for rollback
500
+ const original_deleted_at = message.deleted_at;
501
+ const original_message_text = message.message_text;
398
502
  // Optimistic update
399
503
  set_messages((prev) => prev.map((msg) => msg.id === message_id
400
504
  ? { ...msg, deleted_at: new Date().toISOString(), message_text: null }
401
505
  : msg));
402
506
  try {
403
- const response = await fetch(`${api_base_url}/messages/${message_id}`, {
507
+ const response = await fetch(`${config.api_base_url}/messages/${message_id}`, {
404
508
  method: 'DELETE',
405
- credentials: 'include'
509
+ credentials: 'include',
406
510
  });
407
511
  if (!response.ok) {
408
512
  throw new Error('Failed to delete message');
@@ -414,12 +518,12 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
414
518
  // Rollback on error
415
519
  if (is_mounted_ref.current) {
416
520
  set_messages((prev) => prev.map((msg) => msg.id === message_id
417
- ? { ...msg, deleted_at: message.deleted_at, message_text: message.message_text }
521
+ ? { ...msg, deleted_at: original_deleted_at, message_text: original_message_text }
418
522
  : msg));
419
523
  }
420
524
  return false;
421
525
  }
422
- }, [current_user_id, messages, api_base_url]);
526
+ }, [current_user_id, messages, config.api_base_url]);
423
527
  // -------------------------------------------------------------------------
424
528
  // Mark as read via API
425
529
  // -------------------------------------------------------------------------
@@ -432,36 +536,31 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
432
536
  return;
433
537
  }
434
538
  try {
435
- console.log('[useChatMessages] Marking message as read:', message_id);
436
- const response = await fetch(`${api_base_url}/messages/${message_id}/read`, {
539
+ const response = await fetch(`${config.api_base_url}/messages/${message_id}/read`, {
437
540
  method: 'PATCH',
438
- credentials: 'include'
541
+ credentials: 'include',
439
542
  });
440
543
  if (!response.ok) {
441
- const error_text = await response.text();
442
- console.error('[useChatMessages] Mark as read failed:', response.status, error_text);
544
+ console.error('[useChatMessages] Mark as read failed:', response.status);
443
545
  return;
444
546
  }
445
547
  const data = await response.json();
446
548
  if (data.success && is_mounted_ref.current) {
447
- console.log('[useChatMessages] Message marked as read successfully:', message_id, data.message?.read_at);
448
549
  set_messages((prev) => prev.map((msg) => msg.id === message_id
449
550
  ? { ...msg, read_at: data.message?.read_at || new Date().toISOString() }
450
551
  : msg));
451
552
  }
452
- else {
453
- console.error('[useChatMessages] Mark as read response not successful:', data);
454
- }
455
553
  }
456
554
  catch (err) {
457
555
  console.error('[useChatMessages] Mark as read error:', err);
458
556
  }
459
- }, [current_user_id, messages, api_base_url]);
557
+ }, [current_user_id, messages, config.api_base_url]);
460
558
  // -------------------------------------------------------------------------
461
559
  // Refresh
462
560
  // -------------------------------------------------------------------------
463
561
  const refresh = useCallback(() => {
464
- cursor_ref.current = 0;
562
+ cursor_ref.current = null;
563
+ retry_count_ref.current = 0;
465
564
  set_messages([]);
466
565
  load_initial();
467
566
  }, [load_initial]);