stream-chat 8.38.0 → 8.40.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 (42) hide show
  1. package/README.md +15 -2
  2. package/dist/browser.es.js +1667 -372
  3. package/dist/browser.es.js.map +1 -1
  4. package/dist/browser.full-bundle.min.js +1 -1
  5. package/dist/browser.full-bundle.min.js.map +1 -1
  6. package/dist/browser.js +1668 -371
  7. package/dist/browser.js.map +1 -1
  8. package/dist/index.es.js +1667 -372
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.js +1668 -371
  11. package/dist/index.js.map +1 -1
  12. package/dist/types/channel.d.ts +6 -8
  13. package/dist/types/channel.d.ts.map +1 -1
  14. package/dist/types/channel_state.d.ts +14 -22
  15. package/dist/types/channel_state.d.ts.map +1 -1
  16. package/dist/types/client.d.ts +3 -1
  17. package/dist/types/client.d.ts.map +1 -1
  18. package/dist/types/constants.d.ts +7 -0
  19. package/dist/types/constants.d.ts.map +1 -0
  20. package/dist/types/index.d.ts +2 -0
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/types/store.d.ts +14 -0
  23. package/dist/types/store.d.ts.map +1 -0
  24. package/dist/types/thread.d.ts +93 -29
  25. package/dist/types/thread.d.ts.map +1 -1
  26. package/dist/types/thread_manager.d.ts +51 -0
  27. package/dist/types/thread_manager.d.ts.map +1 -0
  28. package/dist/types/types.d.ts +35 -18
  29. package/dist/types/types.d.ts.map +1 -1
  30. package/dist/types/utils.d.ts +48 -7
  31. package/dist/types/utils.d.ts.map +1 -1
  32. package/package.json +7 -6
  33. package/src/channel.ts +28 -13
  34. package/src/channel_state.ts +30 -27
  35. package/src/client.ts +182 -104
  36. package/src/constants.ts +4 -0
  37. package/src/index.ts +2 -0
  38. package/src/store.ts +57 -0
  39. package/src/thread.ts +470 -107
  40. package/src/thread_manager.ts +297 -0
  41. package/src/types.ts +34 -19
  42. package/src/utils.ts +362 -43
package/src/utils.ts CHANGED
@@ -3,12 +3,15 @@ import {
3
3
  AscDesc,
4
4
  ExtendableGenerics,
5
5
  DefaultGenerics,
6
+ Logger,
6
7
  OwnUserBase,
7
8
  OwnUserResponse,
8
9
  UserResponse,
9
10
  MessageResponse,
10
11
  FormatMessageResponse,
11
12
  ReactionGroupResponse,
13
+ MessageSet,
14
+ MessagePaginationOptions,
12
15
  } from './types';
13
16
  import { AxiosRequestConfig } from 'axios';
14
17
 
@@ -277,14 +280,13 @@ export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (pa
277
280
  };
278
281
 
279
282
  /**
280
- * formatMessage - Takes the message object. Parses the dates, sets __html
281
- * and sets the status to received if missing. Returns a message object
282
- *
283
- * @param {MessageResponse<StreamChatGenerics>} message a message object
283
+ * Takes the message object, parses the dates, sets `__html`
284
+ * and sets the status to `received` if missing; returns a new message object.
284
285
  *
286
+ * @param {MessageResponse<StreamChatGenerics>} message `MessageResponse` object
285
287
  */
286
288
  export function formatMessage<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
287
- message: MessageResponse<StreamChatGenerics>,
289
+ message: MessageResponse<StreamChatGenerics> | FormatMessageResponse<StreamChatGenerics>,
288
290
  ): FormatMessageResponse<StreamChatGenerics> {
289
291
  return {
290
292
  ...message,
@@ -292,10 +294,11 @@ export function formatMessage<StreamChatGenerics extends ExtendableGenerics = De
292
294
  * @deprecated please use `html`
293
295
  */
294
296
  __html: message.html,
295
- // parse the date..
297
+ // parse the dates
296
298
  pinned_at: message.pinned_at ? new Date(message.pinned_at) : null,
297
299
  created_at: message.created_at ? new Date(message.created_at) : new Date(),
298
300
  updated_at: message.updated_at ? new Date(message.updated_at) : new Date(),
301
+ deleted_at: message.deleted_at ? new Date(message.deleted_at) : null,
299
302
  status: message.status || 'received',
300
303
  reaction_groups: maybeGetReactionGroupsFallback(
301
304
  message.reaction_groups,
@@ -305,71 +308,124 @@ export function formatMessage<StreamChatGenerics extends ExtendableGenerics = De
305
308
  };
306
309
  }
307
310
 
308
- export function addToMessageList<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
309
- messages: Array<FormatMessageResponse<StreamChatGenerics>>,
310
- message: FormatMessageResponse<StreamChatGenerics>,
311
+ export const findIndexInSortedArray = <T, L>({
312
+ needle,
313
+ sortedArray,
314
+ selectValueToCompare = (e) => e,
315
+ sortDirection = 'ascending',
316
+ }: {
317
+ needle: T;
318
+ sortedArray: readonly T[];
319
+ /**
320
+ * In array of objects (like messages), pick a specific
321
+ * property to compare needle value to.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * selectValueToCompare: (message) => message.created_at.getTime()
326
+ * ```
327
+ */
328
+ selectValueToCompare?: (arrayElement: T) => L | T;
329
+ /**
330
+ * @default ascending
331
+ * @description
332
+ * ```md
333
+ * ascending - [1,2,3,4,5...]
334
+ * descending - [...5,4,3,2,1]
335
+ * ```
336
+ */
337
+ sortDirection?: 'ascending' | 'descending';
338
+ }) => {
339
+ if (!sortedArray.length) return 0;
340
+
341
+ let left = 0;
342
+ let right = sortedArray.length - 1;
343
+ let middle = 0;
344
+
345
+ const recalculateMiddle = () => {
346
+ middle = Math.round((left + right) / 2);
347
+ };
348
+
349
+ const actualNeedle = selectValueToCompare(needle);
350
+ recalculateMiddle();
351
+
352
+ while (left <= right) {
353
+ // if (actualNeedle === selectValueToCompare(sortedArray[middle])) return middle;
354
+
355
+ if (
356
+ (sortDirection === 'ascending' && actualNeedle < selectValueToCompare(sortedArray[middle])) ||
357
+ (sortDirection === 'descending' && actualNeedle > selectValueToCompare(sortedArray[middle]))
358
+ ) {
359
+ right = middle - 1;
360
+ } else {
361
+ left = middle + 1;
362
+ }
363
+
364
+ recalculateMiddle();
365
+ }
366
+
367
+ return left;
368
+ };
369
+
370
+ export function addToMessageList<T extends FormatMessageResponse>(
371
+ messages: readonly T[],
372
+ newMessage: T,
311
373
  timestampChanged = false,
312
374
  sortBy: 'pinned_at' | 'created_at' = 'created_at',
313
375
  addIfDoesNotExist = true,
314
376
  ) {
315
377
  const addMessageToList = addIfDoesNotExist || timestampChanged;
316
- let messageArr = messages;
378
+ let newMessages = [...messages];
317
379
 
318
380
  // if created_at has changed, message should be filtered and re-inserted in correct order
319
381
  // slow op but usually this only happens for a message inserted to state before actual response with correct timestamp
320
382
  if (timestampChanged) {
321
- messageArr = messageArr.filter((msg) => !(msg.id && message.id === msg.id));
383
+ newMessages = newMessages.filter((message) => !(message.id && newMessage.id === message.id));
322
384
  }
323
385
 
324
- // Get array length after filtering
325
- const messageArrayLength = messageArr.length;
326
-
327
386
  // for empty list just concat and return unless it's an update or deletion
328
- if (messageArrayLength === 0 && addMessageToList) {
329
- return messageArr.concat(message);
330
- } else if (messageArrayLength === 0) {
331
- return [...messageArr];
387
+ if (!newMessages.length && addMessageToList) {
388
+ return newMessages.concat(newMessage);
332
389
  }
333
390
 
334
- const messageTime = (message[sortBy] as Date).getTime();
335
- const messageIsNewest = (messageArr[messageArrayLength - 1][sortBy] as Date).getTime() < messageTime;
391
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
392
+ const messageTime = newMessage[sortBy]!.getTime();
393
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
394
+ const messageIsNewest = newMessages.at(-1)![sortBy]!.getTime() < messageTime;
336
395
 
337
396
  // if message is newer than last item in the list concat and return unless it's an update or deletion
338
397
  if (messageIsNewest && addMessageToList) {
339
- return messageArr.concat(message);
340
- } else if (messageIsNewest) {
341
- return [...messageArr];
398
+ return newMessages.concat(newMessage);
342
399
  }
343
400
 
344
401
  // find the closest index to push the new message
345
- let left = 0;
346
- let middle = 0;
347
- let right = messageArrayLength - 1;
348
- while (left <= right) {
349
- middle = Math.floor((right + left) / 2);
350
- if ((messageArr[middle][sortBy] as Date).getTime() <= messageTime) left = middle + 1;
351
- else right = middle - 1;
352
- }
402
+ const insertionIndex = findIndexInSortedArray({
403
+ needle: newMessage,
404
+ sortedArray: messages,
405
+ sortDirection: 'ascending',
406
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
407
+ selectValueToCompare: (m) => m[sortBy]!.getTime(),
408
+ });
353
409
 
354
- // message already exists and not filtered due to timestampChanged, update and return
355
- if (!timestampChanged && message.id) {
356
- if (messageArr[left] && message.id === messageArr[left].id) {
357
- messageArr[left] = message;
358
- return [...messageArr];
410
+ // message already exists and not filtered with timestampChanged, update and return
411
+ if (!timestampChanged && newMessage.id) {
412
+ if (newMessages[insertionIndex] && newMessage.id === newMessages[insertionIndex].id) {
413
+ newMessages[insertionIndex] = newMessage;
414
+ return newMessages;
359
415
  }
360
416
 
361
- if (messageArr[left - 1] && message.id === messageArr[left - 1].id) {
362
- messageArr[left - 1] = message;
363
- return [...messageArr];
417
+ if (newMessages[insertionIndex - 1] && newMessage.id === newMessages[insertionIndex - 1].id) {
418
+ newMessages[insertionIndex - 1] = newMessage;
419
+ return newMessages;
364
420
  }
365
421
  }
366
422
 
367
- // Do not add updated or deleted messages to the list if they do not already exist
368
- // or have a timestamp change.
423
+ // do not add updated or deleted messages to the list if they already exist or come with a timestamp change
369
424
  if (addMessageToList) {
370
- messageArr.splice(left, 0, message);
425
+ newMessages.splice(insertionIndex, 0, newMessage);
371
426
  }
372
- return [...messageArr];
427
+
428
+ return newMessages;
373
429
  }
374
430
 
375
431
  function maybeGetReactionGroupsFallback(
@@ -396,3 +452,266 @@ function maybeGetReactionGroupsFallback(
396
452
 
397
453
  return null;
398
454
  }
455
+
456
+ // works exactly the same as lodash.throttle
457
+ export const throttle = <T extends (...args: unknown[]) => unknown>(
458
+ fn: T,
459
+ timeout = 200,
460
+ { leading = true, trailing = false }: { leading?: boolean; trailing?: boolean } = {},
461
+ ) => {
462
+ let runningTimeout: null | NodeJS.Timeout = null;
463
+ let storedArgs: Parameters<T> | null = null;
464
+
465
+ return (...args: Parameters<T>) => {
466
+ if (runningTimeout) {
467
+ if (trailing) storedArgs = args;
468
+ return;
469
+ }
470
+
471
+ if (leading) fn(...args);
472
+
473
+ const timeoutHandler = () => {
474
+ if (storedArgs) {
475
+ fn(...storedArgs);
476
+ storedArgs = null;
477
+ runningTimeout = setTimeout(timeoutHandler, timeout);
478
+
479
+ return;
480
+ }
481
+
482
+ runningTimeout = null;
483
+ };
484
+
485
+ runningTimeout = setTimeout(timeoutHandler, timeout);
486
+ };
487
+ };
488
+
489
+ type MessagePaginationUpdatedParams<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
490
+ parentSet: MessageSet;
491
+ requestedPageSize: number;
492
+ returnedPage: MessageResponse<StreamChatGenerics>[];
493
+ logger?: Logger;
494
+ messagePaginationOptions?: MessagePaginationOptions;
495
+ };
496
+
497
+ export function binarySearchByDateEqualOrNearestGreater(
498
+ array: {
499
+ created_at?: string;
500
+ }[],
501
+ targetDate: Date,
502
+ ): number {
503
+ let left = 0;
504
+ let right = array.length - 1;
505
+
506
+ while (left <= right) {
507
+ const mid = Math.floor((left + right) / 2);
508
+ const midCreatedAt = array[mid].created_at;
509
+ if (!midCreatedAt) {
510
+ left += 1;
511
+ continue;
512
+ }
513
+ const midDate = new Date(midCreatedAt);
514
+
515
+ if (midDate.getTime() === targetDate.getTime()) {
516
+ return mid;
517
+ } else if (midDate.getTime() < targetDate.getTime()) {
518
+ left = mid + 1;
519
+ } else {
520
+ right = mid - 1;
521
+ }
522
+ }
523
+
524
+ return left;
525
+ }
526
+
527
+ const messagePaginationCreatedAtAround = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
528
+ parentSet,
529
+ requestedPageSize,
530
+ returnedPage,
531
+ messagePaginationOptions,
532
+ }: MessagePaginationUpdatedParams<StreamChatGenerics>) => {
533
+ const newPagination = { ...parentSet.pagination };
534
+ if (!messagePaginationOptions?.created_at_around) return newPagination;
535
+ let hasPrev;
536
+ let hasNext;
537
+ let updateHasPrev;
538
+ let updateHasNext;
539
+ const createdAtAroundDate = new Date(messagePaginationOptions.created_at_around);
540
+ const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]];
541
+
542
+ // expect ASC order (from oldest to newest)
543
+ const wholePageHasNewerMessages =
544
+ !!firstPageMsg?.created_at && new Date(firstPageMsg.created_at) > createdAtAroundDate;
545
+ const wholePageHasOlderMessages = !!lastPageMsg?.created_at && new Date(lastPageMsg.created_at) < createdAtAroundDate;
546
+
547
+ const requestedPageSizeNotMet =
548
+ requestedPageSize > parentSet.messages.length && requestedPageSize > returnedPage.length;
549
+ const noMoreMessages =
550
+ (requestedPageSize > parentSet.messages.length || parentSet.messages.length >= returnedPage.length) &&
551
+ requestedPageSize > returnedPage.length;
552
+
553
+ if (wholePageHasNewerMessages) {
554
+ hasPrev = false;
555
+ updateHasPrev = true;
556
+ if (requestedPageSizeNotMet) {
557
+ hasNext = false;
558
+ updateHasNext = true;
559
+ }
560
+ } else if (wholePageHasOlderMessages) {
561
+ hasNext = false;
562
+ updateHasNext = true;
563
+ if (requestedPageSizeNotMet) {
564
+ hasPrev = false;
565
+ updateHasPrev = true;
566
+ }
567
+ } else if (noMoreMessages) {
568
+ hasNext = hasPrev = false;
569
+ updateHasPrev = updateHasNext = true;
570
+ } else {
571
+ const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
572
+ firstPageMsg?.id && firstPageMsg.id === parentSet.messages[0]?.id,
573
+ lastPageMsg?.id && lastPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
574
+ ];
575
+ updateHasPrev = firstPageMsgIsFirstInSet;
576
+ updateHasNext = lastPageMsgIsLastInSet;
577
+ const midPointByCount = Math.floor(returnedPage.length / 2);
578
+ const midPointByCreationDate = binarySearchByDateEqualOrNearestGreater(returnedPage, createdAtAroundDate);
579
+
580
+ if (midPointByCreationDate !== -1) {
581
+ hasPrev = midPointByCount <= midPointByCreationDate;
582
+ hasNext = midPointByCount >= midPointByCreationDate;
583
+ }
584
+ }
585
+
586
+ if (updateHasPrev && typeof hasPrev !== 'undefined') newPagination.hasPrev = hasPrev;
587
+ if (updateHasNext && typeof hasNext !== 'undefined') newPagination.hasNext = hasNext;
588
+
589
+ return newPagination;
590
+ };
591
+
592
+ const messagePaginationIdAround = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
593
+ parentSet,
594
+ requestedPageSize,
595
+ returnedPage,
596
+ messagePaginationOptions,
597
+ }: MessagePaginationUpdatedParams<StreamChatGenerics>) => {
598
+ const newPagination = { ...parentSet.pagination };
599
+ const { id_around } = messagePaginationOptions || {};
600
+ if (!id_around) return newPagination;
601
+ let hasPrev;
602
+ let hasNext;
603
+
604
+ const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]];
605
+ const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
606
+ firstPageMsg?.id === parentSet.messages[0]?.id,
607
+ lastPageMsg?.id === parentSet.messages.slice(-1)[0]?.id,
608
+ ];
609
+ let updateHasPrev = firstPageMsgIsFirstInSet;
610
+ let updateHasNext = lastPageMsgIsLastInSet;
611
+
612
+ const midPoint = Math.floor(returnedPage.length / 2);
613
+ const noMoreMessages =
614
+ (requestedPageSize > parentSet.messages.length || parentSet.messages.length >= returnedPage.length) &&
615
+ requestedPageSize > returnedPage.length;
616
+
617
+ if (noMoreMessages) {
618
+ hasNext = hasPrev = false;
619
+ updateHasPrev = updateHasNext = true;
620
+ } else if (!returnedPage[midPoint]) {
621
+ return newPagination;
622
+ } else if (returnedPage[midPoint].id === id_around) {
623
+ hasPrev = hasNext = true;
624
+ } else {
625
+ let targetMsg;
626
+ const halves = [returnedPage.slice(0, midPoint), returnedPage.slice(midPoint)];
627
+ hasPrev = hasNext = true;
628
+ for (let i = 0; i < halves.length; i++) {
629
+ targetMsg = halves[i].find((message) => message.id === id_around);
630
+ if (targetMsg && i === 0) {
631
+ hasPrev = false;
632
+ }
633
+ if (targetMsg && i === 1) {
634
+ hasNext = false;
635
+ }
636
+ }
637
+ }
638
+
639
+ if (updateHasPrev && typeof hasPrev !== 'undefined') newPagination.hasPrev = hasPrev;
640
+ if (updateHasNext && typeof hasNext !== 'undefined') newPagination.hasNext = hasNext;
641
+
642
+ return newPagination;
643
+ };
644
+
645
+ const messagePaginationLinear = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
646
+ parentSet,
647
+ requestedPageSize,
648
+ returnedPage,
649
+ messagePaginationOptions,
650
+ }: MessagePaginationUpdatedParams<StreamChatGenerics>) => {
651
+ const newPagination = { ...parentSet.pagination };
652
+
653
+ let hasPrev;
654
+ let hasNext;
655
+
656
+ const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]];
657
+ const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
658
+ firstPageMsg?.id && firstPageMsg.id === parentSet.messages[0]?.id,
659
+ lastPageMsg?.id && lastPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
660
+ ];
661
+
662
+ const queriedNextMessages =
663
+ messagePaginationOptions &&
664
+ (messagePaginationOptions.created_at_after_or_equal ||
665
+ messagePaginationOptions.created_at_after ||
666
+ messagePaginationOptions.id_gt ||
667
+ messagePaginationOptions.id_gte);
668
+
669
+ const queriedPrevMessages =
670
+ typeof messagePaginationOptions === 'undefined'
671
+ ? true
672
+ : messagePaginationOptions.created_at_before_or_equal ||
673
+ messagePaginationOptions.created_at_before ||
674
+ messagePaginationOptions.id_lt ||
675
+ messagePaginationOptions.id_lte ||
676
+ messagePaginationOptions.offset;
677
+
678
+ const containsUnrecognizedOptionsOnly =
679
+ !queriedNextMessages &&
680
+ !queriedPrevMessages &&
681
+ !messagePaginationOptions?.id_around &&
682
+ !messagePaginationOptions?.created_at_around;
683
+
684
+ const hasMore = returnedPage.length >= requestedPageSize;
685
+
686
+ if (typeof queriedPrevMessages !== 'undefined' || containsUnrecognizedOptionsOnly) {
687
+ hasPrev = hasMore;
688
+ }
689
+ if (typeof queriedNextMessages !== 'undefined') {
690
+ hasNext = hasMore;
691
+ }
692
+ const returnedPageIsEmpty = returnedPage.length === 0;
693
+
694
+ if ((firstPageMsgIsFirstInSet || returnedPageIsEmpty) && typeof hasPrev !== 'undefined')
695
+ newPagination.hasPrev = hasPrev;
696
+ if ((lastPageMsgIsLastInSet || returnedPageIsEmpty) && typeof hasNext !== 'undefined')
697
+ newPagination.hasNext = hasNext;
698
+
699
+ return newPagination;
700
+ };
701
+
702
+ export const messageSetPagination = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
703
+ params: MessagePaginationUpdatedParams<StreamChatGenerics>,
704
+ ) => {
705
+ if (params.parentSet.messages.length < params.returnedPage.length) {
706
+ params.logger?.('error', 'Corrupted message set state: parent set size < returned page size');
707
+ return params.parentSet.pagination;
708
+ }
709
+
710
+ if (params.messagePaginationOptions?.created_at_around) {
711
+ return messagePaginationCreatedAtAround(params);
712
+ } else if (params.messagePaginationOptions?.id_around) {
713
+ return messagePaginationIdAround(params);
714
+ } else {
715
+ return messagePaginationLinear(params);
716
+ }
717
+ };