hazo_chat 2.1.0 → 2.1.1

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.
@@ -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({ 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, }) {
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
+ receiver_user_id,
61
+ reference_id,
62
+ reference_type,
63
+ api_base_url,
64
+ realtime_mode,
65
+ polling_interval,
66
+ messages_per_page,
67
+ }), [receiver_user_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,8 +152,8 @@ 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
  // -------------------------------------------------------------------------
@@ -95,24 +172,44 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
95
172
  sender_profile: profiles.get(msg.sender_user_id),
96
173
  receiver_profile: profiles.get(msg.receiver_user_id),
97
174
  is_sender: msg.sender_user_id === user_id,
98
- send_status: 'sent'
175
+ send_status: 'sent',
99
176
  }));
100
177
  }, [fetch_user_profiles]);
101
178
  // -------------------------------------------------------------------------
102
- // Fetch messages via API
179
+ // Calculate polling delay with exponential backoff
180
+ // -------------------------------------------------------------------------
181
+ const get_poll_delay = useCallback(() => {
182
+ if (retry_count_ref.current === 0) {
183
+ return config.polling_interval;
184
+ }
185
+ // Exponential backoff: interval * 2^retryCount, capped at MAX_POLLING_DELAY
186
+ const delay = config.polling_interval * Math.pow(2, retry_count_ref.current);
187
+ return Math.min(delay, MAX_POLLING_DELAY);
188
+ }, [config.polling_interval]);
189
+ // -------------------------------------------------------------------------
190
+ // Fetch messages via API with optional pagination
103
191
  // -------------------------------------------------------------------------
104
- const fetch_messages_from_api = useCallback(async () => {
105
- if (!receiver_user_id) {
106
- return { messages: [], user_id: null };
192
+ const fetch_messages_from_api = useCallback(async (options) => {
193
+ if (!config.receiver_user_id) {
194
+ return { messages: [], user_id: null, has_more: false, next_cursor: null };
107
195
  }
108
196
  try {
109
197
  const params = new URLSearchParams({
110
- receiver_user_id,
111
- ...(reference_id && { reference_id }),
112
- ...(reference_type && { reference_type }),
198
+ receiver_user_id: config.receiver_user_id,
199
+ limit: String(options?.limit || config.messages_per_page),
113
200
  });
114
- const response = await fetch(`${api_base_url}/messages?${params.toString()}`, {
115
- credentials: 'include'
201
+ if (config.reference_id) {
202
+ params.set('reference_id', config.reference_id);
203
+ }
204
+ if (config.reference_type) {
205
+ params.set('reference_type', config.reference_type);
206
+ }
207
+ if (options?.cursor) {
208
+ params.set('cursor', options.cursor);
209
+ params.set('direction', options.direction || 'older');
210
+ }
211
+ const response = await fetch(`${config.api_base_url}/messages?${params.toString()}`, {
212
+ credentials: 'include',
116
213
  });
117
214
  if (!response.ok) {
118
215
  throw new Error(`HTTP ${response.status}`);
@@ -121,7 +218,9 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
121
218
  if (data.success) {
122
219
  return {
123
220
  messages: data.messages || [],
124
- user_id: data.current_user_id || null
221
+ user_id: data.current_user_id || null,
222
+ has_more: data.pagination?.has_more ?? false,
223
+ next_cursor: data.pagination?.next_cursor ?? null,
125
224
  };
126
225
  }
127
226
  else {
@@ -132,31 +231,35 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
132
231
  console.error('[useChatMessages] Fetch error:', err);
133
232
  throw err;
134
233
  }
135
- }, [receiver_user_id, reference_id, reference_type, api_base_url]);
234
+ }, [config]);
136
235
  // -------------------------------------------------------------------------
137
236
  // Initial load
138
237
  // -------------------------------------------------------------------------
139
238
  const load_initial = useCallback(async () => {
140
- if (!receiver_user_id) {
239
+ if (!config.receiver_user_id) {
141
240
  set_is_loading(false);
142
241
  return;
143
242
  }
144
243
  set_is_loading(true);
145
244
  set_error(null);
146
245
  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);
246
+ const result = await fetch_messages_from_api();
247
+ if (result.user_id && is_mounted_ref.current) {
248
+ set_current_user_id(result.user_id);
150
249
  }
151
250
  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' }));
251
+ const transformed = result.user_id
252
+ ? await transform_messages(result.messages, result.user_id)
253
+ : result.messages.map((msg) => ({
254
+ ...msg,
255
+ is_sender: false,
256
+ send_status: 'sent',
257
+ }));
155
258
  // Sort messages in ascending order (oldest first, newest last)
156
259
  const sorted = transformed.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
157
260
  set_messages(sorted);
158
- set_has_more(db_messages.length >= messages_per_page);
159
- cursor_ref.current = db_messages.length;
261
+ set_has_more(result.has_more);
262
+ cursor_ref.current = result.next_cursor;
160
263
  retry_count_ref.current = 0;
161
264
  set_polling_status('connected');
162
265
  }
@@ -172,25 +275,32 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
172
275
  set_is_loading(false);
173
276
  }
174
277
  }
175
- }, [receiver_user_id, fetch_messages_from_api, transform_messages, messages_per_page]);
278
+ }, [config.receiver_user_id, fetch_messages_from_api, transform_messages]);
176
279
  // -------------------------------------------------------------------------
177
- // Load more (pagination) - Note: simplified, API should support pagination params
280
+ // Load more (pagination)
178
281
  // -------------------------------------------------------------------------
179
282
  const load_more = useCallback(async () => {
180
- if (!current_user_id || !has_more || is_loading_more) {
283
+ if (!current_user_id || !has_more || is_loading_more || !cursor_ref.current) {
181
284
  return;
182
285
  }
183
286
  set_is_loading_more(true);
184
287
  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);
288
+ const result = await fetch_messages_from_api({
289
+ cursor: cursor_ref.current,
290
+ direction: 'older',
291
+ });
292
+ const transformed = await transform_messages(result.messages, current_user_id);
188
293
  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;
294
+ set_messages((prev) => {
295
+ // Prepend older messages and sort
296
+ const combined = [...transformed, ...prev];
297
+ return combined.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
298
+ });
299
+ set_has_more(result.has_more);
300
+ // Update cursor to oldest message for next load_more
301
+ if (result.messages.length > 0) {
302
+ cursor_ref.current = result.messages[0].created_at;
303
+ }
194
304
  }
195
305
  }
196
306
  catch (err) {
@@ -203,88 +313,87 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
203
313
  }
204
314
  }, [current_user_id, has_more, is_loading_more, fetch_messages_from_api, transform_messages]);
205
315
  // -------------------------------------------------------------------------
206
- // Poll for new messages
316
+ // Poll for new messages (uses setTimeout for proper backoff)
207
317
  // -------------------------------------------------------------------------
208
- const poll_for_new_messages = useCallback(async () => {
209
- if (!current_user_id || !receiver_user_id) {
318
+ const schedule_next_poll = useCallback(() => {
319
+ if (!is_mounted_ref.current || config.realtime_mode !== 'polling' || !config.receiver_user_id) {
210
320
  return;
211
321
  }
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
- });
322
+ const delay = get_poll_delay();
323
+ polling_timer_ref.current = setTimeout(async () => {
324
+ if (!is_mounted_ref.current || is_polling_ref.current) {
325
+ return;
228
326
  }
229
- retry_count_ref.current = 0;
230
- if (is_mounted_ref.current) {
231
- set_polling_status('connected');
327
+ is_polling_ref.current = true;
328
+ try {
329
+ // Fetch only newer messages since our latest
330
+ const latest_message = messages[messages.length - 1];
331
+ const result = await fetch_messages_from_api(latest_message
332
+ ? { cursor: latest_message.created_at, direction: 'newer', limit: 50 }
333
+ : undefined);
334
+ if (result.messages.length > 0 && is_mounted_ref.current && current_user_id) {
335
+ const transformed = await transform_messages(result.messages, current_user_id);
336
+ set_messages((prev) => {
337
+ // Merge new messages, avoiding duplicates
338
+ const existing_ids = new Set(prev.map((m) => m.id));
339
+ const new_messages = transformed.filter((m) => !existing_ids.has(m.id));
340
+ if (new_messages.length > 0) {
341
+ const combined = [...prev, ...new_messages];
342
+ return combined.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
343
+ }
344
+ return prev;
345
+ });
346
+ }
347
+ // Reset retry count on success
348
+ retry_count_ref.current = 0;
349
+ if (is_mounted_ref.current) {
350
+ set_polling_status('connected');
351
+ }
232
352
  }
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');
353
+ catch (err) {
354
+ console.error('[useChatMessages] Polling error:', err);
355
+ retry_count_ref.current += 1;
356
+ if (is_mounted_ref.current) {
357
+ if (retry_count_ref.current >= MAX_RETRY_ATTEMPTS) {
358
+ set_polling_status('error');
359
+ }
360
+ else {
361
+ set_polling_status('reconnecting');
362
+ }
240
363
  }
241
- else {
242
- set_polling_status('reconnecting');
364
+ }
365
+ finally {
366
+ is_polling_ref.current = false;
367
+ // Schedule next poll with updated delay (backoff applied via get_poll_delay)
368
+ if (is_mounted_ref.current && config.realtime_mode === 'polling') {
369
+ schedule_next_poll();
243
370
  }
244
371
  }
245
- }
246
- }, [current_user_id, receiver_user_id, fetch_messages_from_api, transform_messages]);
372
+ }, delay);
373
+ }, [config.realtime_mode, config.receiver_user_id, get_poll_delay, fetch_messages_from_api, transform_messages, messages, current_user_id]);
247
374
  // -------------------------------------------------------------------------
248
- // Start polling (only if realtime_mode is 'polling')
375
+ // Start/stop polling based on realtime_mode
249
376
  // -------------------------------------------------------------------------
250
377
  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
378
  // Clear any existing timer
265
379
  if (polling_timer_ref.current) {
266
- clearInterval(polling_timer_ref.current);
380
+ clearTimeout(polling_timer_ref.current);
381
+ polling_timer_ref.current = null;
382
+ }
383
+ // Only start polling if mode is 'polling' and we have a receiver
384
+ if (config.realtime_mode === 'polling' && config.receiver_user_id && current_user_id) {
385
+ schedule_next_poll();
386
+ }
387
+ else if (config.realtime_mode === 'manual') {
388
+ set_polling_status('connected');
267
389
  }
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
390
  return () => {
283
391
  if (polling_timer_ref.current) {
284
- clearInterval(polling_timer_ref.current);
392
+ clearTimeout(polling_timer_ref.current);
393
+ polling_timer_ref.current = null;
285
394
  }
286
395
  };
287
- }, [receiver_user_id, realtime_mode, polling_interval, poll_for_new_messages]);
396
+ }, [config.realtime_mode, config.receiver_user_id, current_user_id, schedule_next_poll]);
288
397
  // -------------------------------------------------------------------------
289
398
  // Initial load effect
290
399
  // -------------------------------------------------------------------------
@@ -299,8 +408,10 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
299
408
  set_error('Not authenticated');
300
409
  return false;
301
410
  }
302
- // Create optimistic message
303
- const optimistic_id = `optimistic-${Date.now()}`;
411
+ // Create optimistic message with unique ID
412
+ const optimistic_id = generateOptimisticId();
413
+ const sender_profile = get_cached_profile(current_user_id);
414
+ const receiver_profile = get_cached_profile(payload.receiver_user_id);
304
415
  const optimistic_message = {
305
416
  id: optimistic_id,
306
417
  reference_id: payload.reference_id,
@@ -313,19 +424,18 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
313
424
  deleted_at: null,
314
425
  created_at: new Date().toISOString(),
315
426
  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),
427
+ sender_profile: sender_profile || undefined,
428
+ receiver_profile: receiver_profile || undefined,
318
429
  is_sender: true,
319
- send_status: 'sending'
430
+ send_status: 'sending',
320
431
  };
321
- // Add optimistic message to state (will be sorted when real message arrives)
432
+ // Add optimistic message to state
322
433
  set_messages((prev) => {
323
434
  const updated = [...prev, optimistic_message];
324
- // Sort to maintain chronological order
325
435
  return updated.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
326
436
  });
327
437
  try {
328
- const response = await fetch(`${api_base_url}/messages`, {
438
+ const response = await fetch(`${config.api_base_url}/messages`, {
329
439
  method: 'POST',
330
440
  headers: { 'Content-Type': 'application/json' },
331
441
  credentials: 'include',
@@ -341,27 +451,25 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
341
451
  // Replace optimistic message with real one
342
452
  const real_message = {
343
453
  ...data.message,
344
- // Ensure all required fields are set with proper defaults
345
454
  reference_list: data.message.reference_list ?? null,
346
455
  read_at: data.message.read_at ?? null,
347
456
  deleted_at: data.message.deleted_at ?? null,
348
457
  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),
458
+ sender_profile: sender_profile || undefined,
459
+ receiver_profile: receiver_profile || undefined,
351
460
  is_sender: true,
352
- send_status: 'sent'
461
+ send_status: 'sent',
353
462
  };
354
463
  set_messages((prev) => {
355
464
  // Check if real message already exists (from polling)
356
- const real_message_exists = prev.some(msg => msg.id === real_message.id);
465
+ const real_message_exists = prev.some((msg) => msg.id === real_message.id);
357
466
  if (real_message_exists) {
358
467
  // Real message already exists from polling, just remove optimistic one
359
- return prev.filter(msg => msg.id !== optimistic_id);
468
+ return prev.filter((msg) => msg.id !== optimistic_id);
360
469
  }
361
470
  else {
362
471
  // Replace optimistic message with real one
363
472
  const replaced = prev.map((msg) => msg.id === optimistic_id ? real_message : msg);
364
- // Sort to ensure correct chronological order
365
473
  return replaced.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
366
474
  }
367
475
  });
@@ -375,13 +483,11 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
375
483
  console.error('[useChatMessages] Send error:', err);
376
484
  // Mark optimistic message as failed
377
485
  if (is_mounted_ref.current) {
378
- set_messages((prev) => prev.map((msg) => msg.id === optimistic_id
379
- ? { ...msg, send_status: 'failed' }
380
- : msg));
486
+ set_messages((prev) => prev.map((msg) => msg.id === optimistic_id ? { ...msg, send_status: 'failed' } : msg));
381
487
  }
382
488
  return false;
383
489
  }
384
- }, [current_user_id, api_base_url]);
490
+ }, [current_user_id, config.api_base_url, get_cached_profile]);
385
491
  // -------------------------------------------------------------------------
386
492
  // Delete message (soft delete) via API
387
493
  // -------------------------------------------------------------------------
@@ -395,14 +501,17 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
395
501
  set_error('Cannot delete this message');
396
502
  return false;
397
503
  }
504
+ // Store original values for rollback
505
+ const original_deleted_at = message.deleted_at;
506
+ const original_message_text = message.message_text;
398
507
  // Optimistic update
399
508
  set_messages((prev) => prev.map((msg) => msg.id === message_id
400
509
  ? { ...msg, deleted_at: new Date().toISOString(), message_text: null }
401
510
  : msg));
402
511
  try {
403
- const response = await fetch(`${api_base_url}/messages/${message_id}`, {
512
+ const response = await fetch(`${config.api_base_url}/messages/${message_id}`, {
404
513
  method: 'DELETE',
405
- credentials: 'include'
514
+ credentials: 'include',
406
515
  });
407
516
  if (!response.ok) {
408
517
  throw new Error('Failed to delete message');
@@ -414,12 +523,12 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
414
523
  // Rollback on error
415
524
  if (is_mounted_ref.current) {
416
525
  set_messages((prev) => prev.map((msg) => msg.id === message_id
417
- ? { ...msg, deleted_at: message.deleted_at, message_text: message.message_text }
526
+ ? { ...msg, deleted_at: original_deleted_at, message_text: original_message_text }
418
527
  : msg));
419
528
  }
420
529
  return false;
421
530
  }
422
- }, [current_user_id, messages, api_base_url]);
531
+ }, [current_user_id, messages, config.api_base_url]);
423
532
  // -------------------------------------------------------------------------
424
533
  // Mark as read via API
425
534
  // -------------------------------------------------------------------------
@@ -432,36 +541,31 @@ export function useChatMessages({ receiver_user_id, reference_id = '', reference
432
541
  return;
433
542
  }
434
543
  try {
435
- console.log('[useChatMessages] Marking message as read:', message_id);
436
- const response = await fetch(`${api_base_url}/messages/${message_id}/read`, {
544
+ const response = await fetch(`${config.api_base_url}/messages/${message_id}/read`, {
437
545
  method: 'PATCH',
438
- credentials: 'include'
546
+ credentials: 'include',
439
547
  });
440
548
  if (!response.ok) {
441
- const error_text = await response.text();
442
- console.error('[useChatMessages] Mark as read failed:', response.status, error_text);
549
+ console.error('[useChatMessages] Mark as read failed:', response.status);
443
550
  return;
444
551
  }
445
552
  const data = await response.json();
446
553
  if (data.success && is_mounted_ref.current) {
447
- console.log('[useChatMessages] Message marked as read successfully:', message_id, data.message?.read_at);
448
554
  set_messages((prev) => prev.map((msg) => msg.id === message_id
449
555
  ? { ...msg, read_at: data.message?.read_at || new Date().toISOString() }
450
556
  : msg));
451
557
  }
452
- else {
453
- console.error('[useChatMessages] Mark as read response not successful:', data);
454
- }
455
558
  }
456
559
  catch (err) {
457
560
  console.error('[useChatMessages] Mark as read error:', err);
458
561
  }
459
- }, [current_user_id, messages, api_base_url]);
562
+ }, [current_user_id, messages, config.api_base_url]);
460
563
  // -------------------------------------------------------------------------
461
564
  // Refresh
462
565
  // -------------------------------------------------------------------------
463
566
  const refresh = useCallback(() => {
464
- cursor_ref.current = 0;
567
+ cursor_ref.current = null;
568
+ retry_count_ref.current = 0;
465
569
  set_messages([]);
466
570
  load_initial();
467
571
  }, [load_initial]);