stream-chat-angular 5.0.0 → 5.1.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.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, Component, Input, EventEmitter, Output, ViewChild, HostBinding, ChangeDetectionStrategy, InjectionToken, Directive, Inject, NgModule } from '@angular/core';
3
- import { BehaviorSubject, ReplaySubject, combineLatest, take as take$1, Subject, timer } from 'rxjs';
3
+ import { BehaviorSubject, ReplaySubject, combineLatest, take as take$1, Subject, timer, merge, switchMap, distinctUntilChanged, pairwise, filter as filter$1, of, map as map$1 } from 'rxjs';
4
4
  import { StreamChat } from 'stream-chat';
5
5
  import { take, shareReplay, map, first, filter, tap, debounceTime, throttleTime } from 'rxjs/operators';
6
6
  import { v4 } from 'uuid';
@@ -19,7 +19,7 @@ import transliterate from '@stream-io/transliterate';
19
19
  import * as i8$1 from 'angular-mentions';
20
20
  import { MentionModule } from 'angular-mentions';
21
21
 
22
- const version = '5.0.0';
22
+ const version = '5.1.0';
23
23
 
24
24
  /**
25
25
  * The `NotificationService` can be used to add or remove notifications. By default the [`NotificationList`](../components/NotificationListComponent.mdx) component displays the currently active notifications.
@@ -447,6 +447,7 @@ class ChannelService {
447
447
  this.chatClientService = chatClientService;
448
448
  this.ngZone = ngZone;
449
449
  this.notificationService = notificationService;
450
+ this.messagePageSize = 25;
450
451
  this.channelsSubject = new BehaviorSubject(undefined);
451
452
  this.activeChannelSubject = new BehaviorSubject(undefined);
452
453
  this.activeChannelMessagesSubject = new BehaviorSubject([]);
@@ -458,7 +459,6 @@ class ChannelService {
458
459
  this.activeThreadMessagesSubject = new BehaviorSubject([]);
459
460
  this.jumpToMessageSubject = new BehaviorSubject({ id: undefined, parentId: undefined });
460
461
  this.latestMessageDateByUserByChannelsSubject = new BehaviorSubject({});
461
- this.messagePageSize = 25;
462
462
  this.attachmentMaxSizeFallbackInMB = 100;
463
463
  this.messageToQuoteSubject = new BehaviorSubject(undefined);
464
464
  this.usersTypingInChannelSubject = new BehaviorSubject([]);
@@ -567,23 +567,6 @@ class ChannelService {
567
567
  .asObservable()
568
568
  .pipe(shareReplay(1));
569
569
  }
570
- /**
571
- * internal
572
- */
573
- removeOldMessageFromMessageList() {
574
- const channel = this.activeChannelSubject.getValue();
575
- const channelMessages = channel?.state.latestMessages;
576
- const targetLength = Math.ceil(ChannelService.MAX_MESSAGE_COUNT_IN_MESSAGE_LIST / 2);
577
- if (!channel ||
578
- !channelMessages ||
579
- channelMessages !== channel?.state.latestMessages ||
580
- channelMessages.length <= targetLength) {
581
- return;
582
- }
583
- const messages = channelMessages;
584
- messages.splice(0, messages.length - targetLength);
585
- this.activeChannelMessagesSubject.next(messages);
586
- }
587
570
  /**
588
571
  * If set to false, read events won't be sent as new messages are received. If set to true active channel (if any) will immediately be marked as read.
589
572
  */
@@ -1466,6 +1449,12 @@ class ChannelService {
1466
1449
  get activeChannelMessages() {
1467
1450
  return this.activeChannelMessagesSubject.getValue() || [];
1468
1451
  }
1452
+ /**
1453
+ * The current thread replies
1454
+ */
1455
+ get activeChannelThreadReplies() {
1456
+ return this.activeThreadMessagesSubject.getValue() || [];
1457
+ }
1469
1458
  /**
1470
1459
  * Get the last 1200 reactions of a message in the current active channel. If you need to fetch more reactions please use the [following endpoint](https://getstream.io/chat/docs/javascript/send_reaction/?language=javascript#paginating-reactions).
1471
1460
  * @param messageId
@@ -1547,7 +1536,7 @@ class ChannelService {
1547
1536
  return;
1548
1537
  }
1549
1538
  const messageIndex = messages.findIndex((m) => m.id === event?.message?.id);
1550
- if (messageIndex !== -1) {
1539
+ if (messageIndex !== -1 || event.type === 'message.deleted') {
1551
1540
  isThreadReply
1552
1541
  ? this.activeThreadMessagesSubject.next([...messages])
1553
1542
  : this.activeChannelMessagesSubject.next([...messages]);
@@ -1928,10 +1917,6 @@ class ChannelService {
1928
1917
  }
1929
1918
  }
1930
1919
  }
1931
- /**
1932
- * @internal
1933
- */
1934
- ChannelService.MAX_MESSAGE_COUNT_IN_MESSAGE_LIST = 250;
1935
1920
  /**
1936
1921
  * @internal
1937
1922
  */
@@ -2692,6 +2677,9 @@ class AvatarComponent {
2692
2677
  this.setFallbackChannelImage();
2693
2678
  }
2694
2679
  }
2680
+ ngOnDestroy() {
2681
+ this.subscriptions.forEach((s) => s.unsubscribe());
2682
+ }
2695
2683
  setFallbackChannelImage() {
2696
2684
  if (this.type !== 'channel') {
2697
2685
  this.fallbackChannelImage = undefined;
@@ -3166,7 +3154,7 @@ class MessageActionsService {
3166
3154
  actionHandler: (message) => {
3167
3155
  void this.channelService.markMessageUnread(message.id);
3168
3156
  },
3169
- isVisible: (enabledActions, isMine, message) => enabledActions.indexOf('read-events') !== -1 && !message.parent_id,
3157
+ isVisible: (enabledActions, _, message) => enabledActions.indexOf('read-events') !== -1 && !message.parent_id,
3170
3158
  },
3171
3159
  {
3172
3160
  actionName: 'quote',
@@ -3182,7 +3170,7 @@ class MessageActionsService {
3182
3170
  actionHandler: (message) => {
3183
3171
  void this.channelService.setAsActiveParentMessage(message);
3184
3172
  },
3185
- isVisible: (enabledActions, isMine, message) => enabledActions.indexOf('send-reply') !== -1 && !message.parent_id,
3173
+ isVisible: (enabledActions, _, message) => enabledActions.indexOf('send-reply') !== -1 && !message.parent_id,
3186
3174
  },
3187
3175
  {
3188
3176
  actionName: 'pin',
@@ -3203,7 +3191,7 @@ class MessageActionsService {
3203
3191
  await this.chatClientService.flagMessage(message.id);
3204
3192
  this.notificationService.addTemporaryNotification('streamChat.Message has been successfully flagged', 'success');
3205
3193
  }
3206
- catch (err) {
3194
+ catch (error) {
3207
3195
  this.notificationService.addTemporaryNotification('streamChat.Error adding flag');
3208
3196
  }
3209
3197
  },
@@ -4097,7 +4085,7 @@ class ChannelListComponent {
4097
4085
  await this.channelService.loadMoreChannels();
4098
4086
  this.isLoadingMoreChannels = false;
4099
4087
  }
4100
- trackByChannelId(index, item) {
4088
+ trackByChannelId(_, item) {
4101
4089
  return item.cid;
4102
4090
  }
4103
4091
  }
@@ -4337,7 +4325,7 @@ class VoiceRecordingComponent {
4337
4325
  : this.audioElement.nativeElement.pause();
4338
4326
  this.isError = false;
4339
4327
  }
4340
- catch (e) {
4328
+ catch (error) {
4341
4329
  this.isError = true;
4342
4330
  }
4343
4331
  }
@@ -6271,6 +6259,347 @@ const isOnSameDay = (date1, date2) => {
6271
6259
  date1.getDate() === date2.getDate());
6272
6260
  };
6273
6261
 
6262
+ /**
6263
+ * The `VirtualizedListService` removes items from a list that are not currently displayed. This is a high-level overview of how it works:
6264
+ * - Create a new instance for each list that needs virtualization
6265
+ * - Input: Provide a reactive stream that emits all items in the list
6266
+ * - Input: Provide a reactive stream that emit the current scroll position (top, middle or bottom)
6267
+ * - Input: maximum number of items that are allowed in the list (in practice the service can make the virtualized list half this number, you should take this into account when choosing the value)
6268
+ * - Output: The service will emit the current list of displayed items via the virtualized items reactive stream
6269
+ * - For simplicity, the service won't track the height of the items, nor it needs an exact scroll location -> this is how removing items work:
6270
+ * - If scroll location is bottom/top items around the current bottom/top item will be emitted in the virtualized items stream
6271
+ * - If scroll location is middle, the service won't remove items, if new items are received, those will be appended to the virtualized list (this means that in theory the list can grow very big if a lot of new items are received while the user is scrolled somewhere, this is a trade-off for the simplicity of no height tracking)
6272
+ * - Since there is no height tracking, you should make sure to provide a maximum number that is big enough to fill the biggest expected screen size twice
6273
+ * - If the user scrolls to the bottom/top and there are no more local items to show, the service will trigger a query to load more items
6274
+ * - Input: you should provide the page size to use, in order for the service to determine if loading is necessary
6275
+ *
6276
+ * The `VirtualizedMessageListService` provides an implementation for the message list component.
6277
+ */
6278
+ class VirtualizedListService {
6279
+ constructor(allItems$, scrollPosition$, jumpToItem$, pageSize = 25, maxItemCount = pageSize * 4) {
6280
+ this.allItems$ = allItems$;
6281
+ this.scrollPosition$ = scrollPosition$;
6282
+ this.jumpToItem$ = jumpToItem$;
6283
+ this.pageSize = pageSize;
6284
+ this.maxItemCount = maxItemCount;
6285
+ this.queryStateSubject = new BehaviorSubject({
6286
+ state: 'success',
6287
+ });
6288
+ this.bufferOnTop = 0;
6289
+ this.bufferOnBottom = 0;
6290
+ this.loadFromBuffer$ = new Subject();
6291
+ this.virtualizedItemsSubject = new BehaviorSubject([]);
6292
+ this.subscriptions = [];
6293
+ this.virtualizedItems$ = this.virtualizedItemsSubject.asObservable();
6294
+ this.queryState$ = this.queryStateSubject.asObservable();
6295
+ this.subscriptions.push(this.virtualizedItems$.subscribe((virtaluzedItems) => {
6296
+ this.allItems$.pipe(take$1(1)).subscribe((allItems) => {
6297
+ if (virtaluzedItems.length === allItems.length) {
6298
+ this.bufferOnTop = 0;
6299
+ this.bufferOnBottom = 0;
6300
+ }
6301
+ else if (virtaluzedItems.length === 0) {
6302
+ this.bufferOnTop = allItems.length;
6303
+ this.bufferOnBottom = 0;
6304
+ }
6305
+ else {
6306
+ this.bufferOnTop = allItems.indexOf(virtaluzedItems[0]);
6307
+ this.bufferOnBottom =
6308
+ allItems.length -
6309
+ allItems.indexOf(virtaluzedItems[virtaluzedItems.length - 1]) -
6310
+ 1;
6311
+ }
6312
+ });
6313
+ }));
6314
+ this.subscriptions.push(merge(this.allItems$, this.loadFromBuffer$)
6315
+ .pipe(switchMap(() => {
6316
+ return combineLatest([
6317
+ this.allItems$.pipe(take$1(1)),
6318
+ this.scrollPosition$.pipe(take$1(1)),
6319
+ ]);
6320
+ }))
6321
+ .subscribe(([items, scrollPosition]) => {
6322
+ if (scrollPosition === 'middle') {
6323
+ return;
6324
+ }
6325
+ const currentItems = this.virtualizedItemsSubject.getValue();
6326
+ if (items.length <= this.maxItemCount) {
6327
+ this.virtualizedItemsSubject.next(items);
6328
+ }
6329
+ else {
6330
+ let startIndex = 0;
6331
+ let endIndex = undefined;
6332
+ const numberOfItemsToRemove = items.length - Math.round(this.maxItemCount / 2);
6333
+ const numberOfItemsAfterRemove = items.length - numberOfItemsToRemove;
6334
+ switch (scrollPosition) {
6335
+ case 'top':
6336
+ if (currentItems.length > 0) {
6337
+ const middleIndex = items.findIndex((i) => this.isEqual(i, currentItems[0]));
6338
+ if (middleIndex !== -1) {
6339
+ startIndex = Math.max(0, middleIndex - Math.ceil(numberOfItemsAfterRemove / 2));
6340
+ endIndex = startIndex + numberOfItemsAfterRemove;
6341
+ }
6342
+ }
6343
+ else {
6344
+ endIndex = numberOfItemsAfterRemove;
6345
+ }
6346
+ break;
6347
+ case 'bottom':
6348
+ if (currentItems.length > 0) {
6349
+ const middleIndex = items.findIndex((i) => this.isEqual(i, currentItems[currentItems.length - 1]));
6350
+ if (middleIndex !== -1) {
6351
+ endIndex = Math.min(items.length, middleIndex + Math.floor(numberOfItemsAfterRemove / 2) + 1);
6352
+ startIndex = endIndex - numberOfItemsAfterRemove;
6353
+ }
6354
+ }
6355
+ else {
6356
+ startIndex = items.length - numberOfItemsAfterRemove;
6357
+ }
6358
+ break;
6359
+ }
6360
+ const virtualizedItems = items.slice(startIndex, endIndex);
6361
+ this.virtualizedItemsSubject.next(virtualizedItems);
6362
+ }
6363
+ }));
6364
+ this.subscriptions.push(this.scrollPosition$
6365
+ .pipe(distinctUntilChanged())
6366
+ .subscribe((position) => {
6367
+ if (this.queryStateSubject.getValue().state === `loading-${position}`) {
6368
+ return;
6369
+ }
6370
+ if (position === 'top') {
6371
+ if (this.bufferOnTop < this.pageSize) {
6372
+ void this.loadMore(position);
6373
+ }
6374
+ else {
6375
+ this.loadMoreFromBuffer('top');
6376
+ }
6377
+ }
6378
+ else if (position === 'bottom') {
6379
+ if (this.bufferOnBottom < this.pageSize) {
6380
+ void this.loadMore(position);
6381
+ }
6382
+ else {
6383
+ this.loadMoreFromBuffer('bottom');
6384
+ }
6385
+ }
6386
+ }));
6387
+ this.subscriptions.push(this.allItems$
6388
+ .pipe(pairwise(), filter$1(() => {
6389
+ let scrollPosition;
6390
+ this.scrollPosition$
6391
+ .pipe(take$1(1))
6392
+ .subscribe((s) => (scrollPosition = s));
6393
+ return scrollPosition === 'middle';
6394
+ }))
6395
+ .subscribe(([prevItems, currentItems]) => {
6396
+ if (currentItems.length < this.maxItemCount ||
6397
+ this.virtualizedItems.length === 0) {
6398
+ this.virtualizedItemsSubject.next(currentItems);
6399
+ }
6400
+ else {
6401
+ const currentFirstItem = this.virtualizedItems[0];
6402
+ const currentLastItem = this.virtualizedItems[this.virtualizedItems.length - 1];
6403
+ const prevStartIndex = prevItems.findIndex((i) => this.isEqual(i, currentFirstItem));
6404
+ const prevEndIndex = prevItems.findIndex((i) => this.isEqual(i, currentLastItem));
6405
+ const isStartRemainedSame = currentItems[prevStartIndex]
6406
+ ? this.isEqual(currentItems[prevStartIndex], currentFirstItem)
6407
+ : false;
6408
+ const isEndRemainedSame = currentItems[prevEndIndex]
6409
+ ? this.isEqual(currentItems[prevEndIndex], currentLastItem)
6410
+ : false;
6411
+ const hasNewItemsBottom = prevEndIndex === prevItems.length - 1 && isEndRemainedSame
6412
+ ? prevItems.length !== currentItems.length
6413
+ : false;
6414
+ if (isStartRemainedSame && isEndRemainedSame) {
6415
+ const endIndex = hasNewItemsBottom ? undefined : prevEndIndex + 1;
6416
+ this.virtualizedItemsSubject.next(currentItems.slice(prevStartIndex, endIndex));
6417
+ }
6418
+ let currentStartIndex = isStartRemainedSame ? prevStartIndex : -1;
6419
+ let currentEndIndex = isEndRemainedSame ? prevEndIndex : -1;
6420
+ if (!isStartRemainedSame) {
6421
+ currentStartIndex = currentItems.findIndex((i) => this.isEqual(i, currentFirstItem));
6422
+ }
6423
+ if (!isEndRemainedSame) {
6424
+ currentEndIndex = currentItems.findIndex((i) => this.isEqual(i, currentLastItem));
6425
+ }
6426
+ const hasNewItemsTop = prevStartIndex === 0 && !isStartRemainedSame
6427
+ ? currentStartIndex !== 0
6428
+ : false;
6429
+ if (currentStartIndex !== -1 && currentEndIndex !== -1) {
6430
+ const startIndex = hasNewItemsTop ? 0 : currentStartIndex;
6431
+ this.virtualizedItemsSubject.next(currentItems.slice(startIndex, currentEndIndex + 1));
6432
+ }
6433
+ else {
6434
+ if (currentStartIndex === -1 && currentEndIndex !== -1) {
6435
+ currentStartIndex = Math.max(0, currentEndIndex - (prevEndIndex - prevStartIndex));
6436
+ }
6437
+ if (currentEndIndex === -1 && currentStartIndex !== -1) {
6438
+ currentEndIndex = Math.min(currentItems.length - 1, currentStartIndex + (prevEndIndex - prevStartIndex));
6439
+ }
6440
+ this.virtualizedItemsSubject.next(currentItems.slice(currentStartIndex, currentEndIndex + 1));
6441
+ }
6442
+ }
6443
+ }));
6444
+ if (this.jumpToItem$) {
6445
+ this.subscriptions.push(this.jumpToItem$
6446
+ .pipe(switchMap((jumpToItem) => combineLatest([this.allItems$.pipe(take$1(1)), of(jumpToItem)])))
6447
+ .subscribe(([allItems, jumpToItem]) => {
6448
+ if (jumpToItem.item) {
6449
+ if (allItems.length < this.maxItemCount) {
6450
+ this.virtualizedItemsSubject.next(allItems);
6451
+ }
6452
+ else {
6453
+ const itemIndex = allItems.findIndex((i) =>
6454
+ // @ts-expect-error TODO: do we know a better typing here?
6455
+ this.isEqual(i, jumpToItem.item));
6456
+ if (itemIndex === -1) {
6457
+ return;
6458
+ }
6459
+ else {
6460
+ const position = jumpToItem.position || 'middle';
6461
+ const numberOfItemsToRemove = allItems.length - Math.round(this.maxItemCount / 2);
6462
+ const numberOfItemsAfterRemove = allItems.length - numberOfItemsToRemove;
6463
+ let startIndex = -1;
6464
+ let endIndex = -1;
6465
+ switch (position) {
6466
+ case 'top':
6467
+ startIndex = itemIndex;
6468
+ endIndex = Math.min(allItems.length, startIndex + numberOfItemsAfterRemove);
6469
+ break;
6470
+ case 'bottom':
6471
+ endIndex = itemIndex + 1;
6472
+ startIndex = Math.max(0, endIndex - numberOfItemsAfterRemove);
6473
+ break;
6474
+ case 'middle': {
6475
+ const itemsOnTop = itemIndex;
6476
+ const itemsOnBottom = allItems.length - itemIndex;
6477
+ if (itemsOnTop < Math.ceil(numberOfItemsAfterRemove / 2)) {
6478
+ startIndex = 0;
6479
+ }
6480
+ if (itemsOnBottom <
6481
+ Math.floor(numberOfItemsAfterRemove / 2) + 1) {
6482
+ endIndex = allItems.length;
6483
+ }
6484
+ if (startIndex === -1) {
6485
+ if (endIndex !== -1) {
6486
+ startIndex = endIndex - numberOfItemsAfterRemove;
6487
+ }
6488
+ else {
6489
+ startIndex =
6490
+ itemIndex - Math.ceil(numberOfItemsAfterRemove / 2);
6491
+ }
6492
+ }
6493
+ if (endIndex === -1) {
6494
+ endIndex = startIndex + numberOfItemsAfterRemove;
6495
+ }
6496
+ }
6497
+ }
6498
+ this.virtualizedItemsSubject.next(allItems.slice(startIndex, endIndex));
6499
+ }
6500
+ }
6501
+ }
6502
+ }));
6503
+ }
6504
+ }
6505
+ /**
6506
+ * The current value of virtualized items
6507
+ */
6508
+ get virtualizedItems() {
6509
+ return this.virtualizedItemsSubject.getValue();
6510
+ }
6511
+ /**
6512
+ * Remove all subscriptions, call this once you're done using an instance of this service
6513
+ */
6514
+ dispose() {
6515
+ this.subscriptions.forEach((s) => s.unsubscribe());
6516
+ }
6517
+ loadMoreFromBuffer(_) {
6518
+ this.loadFromBuffer$.next();
6519
+ }
6520
+ async loadMore(direction) {
6521
+ this.queryStateSubject.next({ state: `loading-${direction}` });
6522
+ try {
6523
+ await this.query(direction);
6524
+ this.queryStateSubject.next({ state: 'success' });
6525
+ }
6526
+ catch (e) {
6527
+ this.queryStateSubject.next({ state: 'error', error: e });
6528
+ }
6529
+ }
6530
+ }
6531
+
6532
+ /**
6533
+ * The `VirtualizedMessageListService` removes messages from the message list that are currently not in view
6534
+ */
6535
+ class VirtualizedMessageListService extends VirtualizedListService {
6536
+ constructor(mode, scrollPosition$, channelService) {
6537
+ const jumpToMessage$ = channelService.jumpToMessage$.pipe(map$1((jumpToMessage) => {
6538
+ let result = {
6539
+ item: undefined,
6540
+ };
6541
+ let targetMessageId;
6542
+ if (mode === 'main') {
6543
+ targetMessageId = jumpToMessage.parentId
6544
+ ? jumpToMessage.parentId
6545
+ : jumpToMessage.id;
6546
+ }
6547
+ else {
6548
+ targetMessageId = jumpToMessage.parentId
6549
+ ? jumpToMessage.id
6550
+ : undefined;
6551
+ }
6552
+ if (targetMessageId) {
6553
+ const messages = mode === 'main'
6554
+ ? channelService.activeChannelMessages
6555
+ : channelService.activeChannelThreadReplies;
6556
+ const id = targetMessageId === 'latest'
6557
+ ? messages[messages.length - 1]?.id
6558
+ : targetMessageId;
6559
+ if (id) {
6560
+ result = {
6561
+ item: { id },
6562
+ position: jumpToMessage.id === 'latest' ? 'bottom' : 'middle',
6563
+ };
6564
+ }
6565
+ channelService.clearMessageJump();
6566
+ }
6567
+ return result;
6568
+ }));
6569
+ const messages$ = mode === 'main'
6570
+ ? channelService.activeChannelMessages$
6571
+ : channelService.activeThreadMessages$;
6572
+ super(messages$, scrollPosition$, jumpToMessage$, channelService.messagePageSize);
6573
+ this.mode = mode;
6574
+ this.channelService = channelService;
6575
+ this.isEqual = (t1, t2) => t1.id === t2.id;
6576
+ this.query = (direction) => {
6577
+ const request = this.mode === 'main'
6578
+ ? (direction) => this.channelService.loadMoreMessages(direction)
6579
+ : (direction) => this.channelService.loadMoreThreadReplies(direction);
6580
+ const result = request(direction === 'top' ? 'older' : 'newer');
6581
+ if (result) {
6582
+ return result;
6583
+ }
6584
+ else {
6585
+ this.queryStateSubject.next({ state: 'success' });
6586
+ if ((direction === 'top' && this.bufferOnTop > 0) ||
6587
+ (direction === 'bottom' && this.bufferOnBottom > 0)) {
6588
+ this.loadFromBuffer$.next();
6589
+ }
6590
+ return Promise.resolve();
6591
+ }
6592
+ };
6593
+ }
6594
+ loadMoreFromBuffer(direction) {
6595
+ this.queryStateSubject.next({ state: `loading-${direction}` });
6596
+ setTimeout(() => {
6597
+ this.loadFromBuffer$.next();
6598
+ this.queryStateSubject.next({ state: 'success' });
6599
+ });
6600
+ }
6601
+ }
6602
+
6274
6603
  /**
6275
6604
  * The `MessageList` component renders a scrollable list of messages.
6276
6605
  */
@@ -6321,10 +6650,6 @@ class MessageListComponent {
6321
6650
  * You can turn on and off the loading indicator that signals to users that more messages are being loaded to the message list
6322
6651
  */
6323
6652
  this.displayLoadingIndicator = true;
6324
- /**
6325
- * @internal
6326
- */
6327
- this.limitNumberOfMessagesInList = true;
6328
6653
  this.emptyMainMessageListTemplate = null;
6329
6654
  this.emptyThreadMessageListTemplate = null;
6330
6655
  this.enabledMessageActions = [];
@@ -6332,17 +6657,20 @@ class MessageListComponent {
6332
6657
  this.newMessageCountWhileBeingScrolled = 0;
6333
6658
  this.groupStyles = [];
6334
6659
  this.isNextMessageOnSeparateDate = [];
6335
- this.isLoading = false;
6660
+ this.loadingState = 'idle';
6336
6661
  this.isUnreadNotificationVisible = true;
6337
6662
  this.isJumpingToLatestUnreadMessage = false;
6338
6663
  this.isJumpToLatestButtonVisible = true;
6664
+ this.isJumpingToMessage = false;
6339
6665
  this.scroll$ = new Subject();
6666
+ this.isNewMessageSentByUser = false;
6340
6667
  this.subscriptions = [];
6341
6668
  this.isLatestMessageInList = true;
6342
6669
  this.parsedDates = new Map();
6343
6670
  this.isViewInited = false;
6344
6671
  this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
6345
6672
  this.forceRepaintSubject = new Subject();
6673
+ this.scrollPosition$ = new BehaviorSubject('bottom');
6346
6674
  this.messageNotificationJumpClicked = () => {
6347
6675
  this.jumpToFirstUnreadMessage();
6348
6676
  this.isUnreadNotificationVisible = false;
@@ -6361,7 +6689,6 @@ class MessageListComponent {
6361
6689
  this.forceRepaint();
6362
6690
  }));
6363
6691
  this.subscriptions.push(this.channelService.activeChannel$.subscribe((channel) => {
6364
- this.chatClientService.chatClient?.logger?.('info', `${channel?.cid || 'undefined'} selected`, { tags: `message list ${this.mode}` });
6365
6692
  let isNewChannel = false;
6366
6693
  if (this.channelId !== channel?.id) {
6367
6694
  isNewChannel = true;
@@ -6369,12 +6696,9 @@ class MessageListComponent {
6369
6696
  clearTimeout(this.checkIfUnreadNotificationIsVisibleTimeout);
6370
6697
  }
6371
6698
  this.isUnreadNotificationVisible = false;
6372
- this.chatClientService?.chatClient?.logger?.('info', `new channel is different from prev channel, reseting scroll state`, { tags: `message list ${this.mode}` });
6373
6699
  this.parsedDates = new Map();
6374
- if (this.messageRemoveTimeout) {
6375
- clearTimeout(this.messageRemoveTimeout);
6376
- }
6377
6700
  this.resetScrollState();
6701
+ this.setMessages$();
6378
6702
  this.channelId = channel?.id;
6379
6703
  if (this.isViewInited) {
6380
6704
  this.cdRef.detectChanges();
@@ -6434,17 +6758,12 @@ class MessageListComponent {
6434
6758
  this.newMessageSubscription?.unsubscribe();
6435
6759
  if (channel) {
6436
6760
  this.newMessageSubscription = channel.on('message.new', (event) => {
6437
- // If we display main channel messages and we're switched to an older message set -> use message.new event to update unread count and detect new messages sent by current user
6438
- if (!event.message ||
6439
- channel.state.messages === channel.state.latestMessages ||
6440
- this.mode === 'thread') {
6761
+ if (!event.message) {
6441
6762
  return;
6442
6763
  }
6443
- this.newMessageReceived({
6444
- id: event.message.id,
6445
- user: event.message.user,
6446
- created_at: new Date(event.message.created_at || ''),
6447
- });
6764
+ else {
6765
+ this.newMessageReceived(event.message);
6766
+ }
6448
6767
  });
6449
6768
  }
6450
6769
  }));
@@ -6454,6 +6773,7 @@ class MessageListComponent {
6454
6773
  message.id !== this.parentMessage.id &&
6455
6774
  this.mode === 'thread') {
6456
6775
  this.resetScrollState();
6776
+ this.setMessages$();
6457
6777
  }
6458
6778
  if (this.parentMessage === message) {
6459
6779
  return;
@@ -6508,41 +6828,6 @@ class MessageListComponent {
6508
6828
  this.cdRef.detectChanges();
6509
6829
  }
6510
6830
  }));
6511
- this.subscriptions.push(this.channelService.jumpToMessage$
6512
- .pipe(filter((config) => !!config.id))
6513
- .subscribe((config) => {
6514
- let messageId = undefined;
6515
- if (this.messageRemoveTimeout) {
6516
- clearTimeout(this.messageRemoveTimeout);
6517
- }
6518
- if (this.mode === 'main') {
6519
- messageId = config.parentId || config.id;
6520
- }
6521
- else if (config.parentId) {
6522
- messageId = config.id;
6523
- }
6524
- this.chatClientService.chatClient?.logger?.('info', `Jumping to ${messageId || ''}`, { tags: `message list ${this.mode}` });
6525
- if (messageId) {
6526
- if (messageId === 'latest') {
6527
- this.scrollToLatestMessage();
6528
- if (this.isViewInited) {
6529
- this.cdRef.detectChanges();
6530
- }
6531
- }
6532
- else {
6533
- if (this.isJumpingToLatestUnreadMessage) {
6534
- this.scrollMessageIntoView(this.firstUnreadMessageId || messageId);
6535
- this.highlightedMessageId =
6536
- this.firstUnreadMessageId || messageId;
6537
- }
6538
- else {
6539
- this.scrollMessageIntoView(messageId);
6540
- this.highlightedMessageId = messageId;
6541
- }
6542
- }
6543
- }
6544
- this.channelService.clearMessageJump();
6545
- }));
6546
6831
  this.subscriptions.push(this.customTemplatesService.emptyMainMessageListPlaceholder$.subscribe((template) => {
6547
6832
  const isChanged = this.emptyMainMessageListTemplate !== template;
6548
6833
  this.emptyMainMessageListTemplate = template || null;
@@ -6561,6 +6846,7 @@ class MessageListComponent {
6561
6846
  }
6562
6847
  ngOnChanges(changes) {
6563
6848
  if (changes.mode || changes.direction) {
6849
+ this.resetScrollState();
6564
6850
  this.setMessages$();
6565
6851
  }
6566
6852
  if (changes.direction) {
@@ -6572,51 +6858,27 @@ class MessageListComponent {
6572
6858
  ngAfterViewInit() {
6573
6859
  this.isViewInited = true;
6574
6860
  this.ngZone.runOutsideAngular(() => {
6575
- this.scrollContainer.nativeElement.addEventListener('scroll', () => this.scrolled());
6861
+ this.scrollContainer?.nativeElement?.addEventListener('scroll', () => this.scrolled());
6576
6862
  });
6577
6863
  }
6578
6864
  ngAfterViewChecked() {
6579
- if (this.highlightedMessageId) {
6580
- // Turn off programatic scroll adjustments while jump to message is in progress
6581
- this.hasNewMessages = false;
6582
- this.olderMassagesLoaded = false;
6865
+ if (this.isJumpingToMessage) {
6866
+ this.isNewMessageSentByUser = false;
6867
+ this.messageIdToAnchorTo = undefined;
6868
+ this.anchorMessageTopOffset = undefined;
6869
+ return;
6583
6870
  }
6584
- if (this.direction === 'top-to-bottom') {
6585
- if (this.hasNewMessages &&
6586
- (this.isNewMessageSentByUser || !this.isUserScrolled)) {
6587
- this.isLatestMessageInList
6588
- ? this.scrollToTop()
6589
- : this.jumpToLatestMessage();
6590
- this.hasNewMessages = false;
6591
- this.containerHeight = this.scrollContainer.nativeElement.scrollHeight;
6592
- }
6871
+ if (this.messageIdToAnchorTo && this.loadingState === 'idle') {
6872
+ this.preserveScrollbarPosition();
6593
6873
  }
6594
- else {
6595
- if (this.hasNewMessages) {
6596
- if (!this.isUserScrolled || this.isNewMessageSentByUser) {
6597
- this.chatClientService.chatClient?.logger?.('info', `User has new messages, and not scrolled or sent new messages, therefore we ${this.isLatestMessageInList ? 'scroll' : 'jump'} to latest message`, { tags: `message list ${this.mode}` });
6598
- this.isLatestMessageInList
6599
- ? this.scrollToBottom()
6600
- : this.jumpToLatestMessage();
6601
- }
6602
- this.hasNewMessages = false;
6603
- this.containerHeight = this.scrollContainer.nativeElement.scrollHeight;
6604
- }
6605
- else if (this.olderMassagesLoaded) {
6606
- this.chatClientService.chatClient?.logger?.('info', `Older messages are loaded, we preserve the scroll position`, { tags: `message list ${this.mode}` });
6607
- this.preserveScrollbarPosition();
6608
- this.containerHeight = this.scrollContainer.nativeElement.scrollHeight;
6609
- this.olderMassagesLoaded = false;
6610
- }
6611
- else if (this.getScrollPosition() !== 'bottom' &&
6612
- !this.isUserScrolled &&
6613
- !this.highlightedMessageId) {
6614
- this.chatClientService.chatClient?.logger?.('info', `Container grew and user didn't scroll therefore we ${this.isLatestMessageInList ? 'scroll' : 'jump'} to latest message`, { tags: `message list ${this.mode}` });
6615
- this.isLatestMessageInList
6616
- ? this.scrollToBottom()
6617
- : this.jumpToLatestMessage();
6618
- this.containerHeight = this.scrollContainer.nativeElement.scrollHeight;
6619
- }
6874
+ else if ((!this.isUserScrolled &&
6875
+ this.scrollContainer.nativeElement?.scrollHeight >
6876
+ this.scrollContainer?.nativeElement.clientHeight &&
6877
+ this.getScrollPosition() !==
6878
+ (this.direction === 'bottom-to-top' ? 'bottom' : 'top')) ||
6879
+ (this.isUserScrolled && this.isNewMessageSentByUser)) {
6880
+ this.isNewMessageSentByUser = false;
6881
+ this.jumpToLatestMessage();
6620
6882
  }
6621
6883
  }
6622
6884
  ngOnDestroy() {
@@ -6631,19 +6893,23 @@ class MessageListComponent {
6631
6893
  if (this.jumpToLatestButtonVisibilityTimeout) {
6632
6894
  clearTimeout(this.jumpToLatestButtonVisibilityTimeout);
6633
6895
  }
6634
- if (this.messageRemoveTimeout) {
6635
- clearTimeout(this.messageRemoveTimeout);
6636
- }
6637
- this.removeOldMessagesSubscription?.unsubscribe();
6896
+ this.disposeVirtualizedList();
6638
6897
  }
6639
- trackByMessageId(index, item) {
6898
+ trackByMessageId(_, item) {
6640
6899
  return item.id;
6641
6900
  }
6642
- trackByUserId(index, user) {
6901
+ trackByUserId(_, user) {
6643
6902
  return user.id;
6644
6903
  }
6645
6904
  jumpToLatestMessage() {
6646
- void this.channelService.jumpToMessage('latest', this.mode === 'thread' ? this.parentMessage?.id : undefined);
6905
+ if (this.isLatestMessageInList) {
6906
+ this.direction === 'bottom-to-top'
6907
+ ? this.scrollToBottom()
6908
+ : this.scrollToTop();
6909
+ }
6910
+ else {
6911
+ void this.channelService.jumpToMessage('latest', this.mode === 'thread' ? this.parentMessage?.id : undefined);
6912
+ }
6647
6913
  }
6648
6914
  scrollToBottom() {
6649
6915
  this.scrollContainer.nativeElement.scrollTop =
@@ -6666,8 +6932,7 @@ class MessageListComponent {
6666
6932
  return;
6667
6933
  }
6668
6934
  this.scroll$.next();
6669
- const scrollPosition = this.getScrollPosition();
6670
- this.chatClientService.chatClient?.logger?.('info', `Scrolled - scroll position: ${scrollPosition}, container height: ${this.scrollContainer.nativeElement.scrollHeight}`, { tags: `message list ${this.mode}` });
6935
+ let scrollPosition = this.getScrollPosition();
6671
6936
  const isUserScrolled = (this.direction === 'bottom-to-top'
6672
6937
  ? scrollPosition !== 'bottom'
6673
6938
  : scrollPosition !== 'top') || !this.isLatestMessageInList;
@@ -6696,30 +6961,33 @@ class MessageListComponent {
6696
6961
  }
6697
6962
  }, 100);
6698
6963
  }
6699
- if (this.shouldLoadMoreMessages(scrollPosition)) {
6964
+ const prevScrollPosition = this.scrollPosition$.getValue();
6965
+ if (this.direction === 'top-to-bottom') {
6966
+ if (scrollPosition === 'top') {
6967
+ scrollPosition = 'bottom';
6968
+ }
6969
+ else if (scrollPosition === 'bottom') {
6970
+ scrollPosition = 'top';
6971
+ }
6972
+ }
6973
+ if (prevScrollPosition !== scrollPosition && !this.isJumpingToMessage) {
6974
+ if (scrollPosition === 'top' || scrollPosition === 'bottom') {
6975
+ this.virtualizedList?.virtualizedItems$
6976
+ .pipe(take(1))
6977
+ .subscribe((items) => {
6978
+ this.messageIdToAnchorTo =
6979
+ scrollPosition === 'top'
6980
+ ? items[0]?.id
6981
+ : items[items.length - 1]?.id;
6982
+ this.anchorMessageTopOffset = document
6983
+ .getElementById(this.messageIdToAnchorTo)
6984
+ ?.getBoundingClientRect()?.top;
6985
+ });
6986
+ }
6700
6987
  this.ngZone.run(() => {
6701
- this.containerHeight = this.scrollContainer.nativeElement.scrollHeight;
6702
- let direction;
6703
- if (this.direction === 'top-to-bottom') {
6704
- direction = scrollPosition === 'top' ? 'newer' : 'older';
6705
- }
6706
- else {
6707
- direction = scrollPosition === 'top' ? 'older' : 'newer';
6708
- }
6709
- const result = this.mode === 'main'
6710
- ? this.channelService.loadMoreMessages(direction)
6711
- : this.channelService.loadMoreThreadReplies(direction);
6712
- if (result) {
6713
- this.chatClientService.chatClient?.logger?.('info', `Displaying loading indicator`, { tags: `message list ${this.mode}` });
6714
- this.isLoading = true;
6715
- result.catch?.(() => {
6716
- this.isLoading = false;
6717
- });
6718
- }
6719
- this.cdRef.detectChanges();
6988
+ this.scrollPosition$.next(scrollPosition);
6720
6989
  });
6721
6990
  }
6722
- this.prevScrollTop = this.scrollContainer.nativeElement.scrollTop;
6723
6991
  }
6724
6992
  jumpToFirstUnreadMessage() {
6725
6993
  if (!this.lastReadMessageId) {
@@ -6760,9 +7028,18 @@ class MessageListComponent {
6760
7028
  : this.emptyThreadMessageListTemplate;
6761
7029
  }
6762
7030
  preserveScrollbarPosition() {
6763
- this.scrollContainer.nativeElement.scrollTop =
6764
- (this.prevScrollTop || 0) +
6765
- (this.scrollContainer.nativeElement.scrollHeight - this.containerHeight);
7031
+ if (!this.messageIdToAnchorTo) {
7032
+ return;
7033
+ }
7034
+ const messageToAlignTo = document.getElementById(this.messageIdToAnchorTo);
7035
+ this.messageIdToAnchorTo = undefined;
7036
+ this.scrollContainer.nativeElement.scrollTop +=
7037
+ (messageToAlignTo?.getBoundingClientRect()?.top || 0) -
7038
+ (this.anchorMessageTopOffset || 0);
7039
+ this.anchorMessageTopOffset = undefined;
7040
+ if (this.isSafari) {
7041
+ this.forceRepaintSubject.next();
7042
+ }
6766
7043
  }
6767
7044
  forceRepaint() {
6768
7045
  // Solves the issue of empty screen on Safari when scrolling
@@ -6773,10 +7050,7 @@ class MessageListComponent {
6773
7050
  getScrollPosition() {
6774
7051
  let position = 'middle';
6775
7052
  if (Math.floor(this.scrollContainer.nativeElement.scrollTop) <=
6776
- (this.parentMessageElement?.nativeElement.clientHeight || 0) &&
6777
- (this.prevScrollTop === undefined ||
6778
- this.prevScrollTop >
6779
- (this.parentMessageElement?.nativeElement.clientHeight || 0))) {
7053
+ (this.parentMessageElement?.nativeElement.clientHeight || 0)) {
6780
7054
  position = 'top';
6781
7055
  }
6782
7056
  else if (Math.ceil(this.scrollContainer.nativeElement.scrollTop) +
@@ -6787,22 +7061,23 @@ class MessageListComponent {
6787
7061
  }
6788
7062
  return position;
6789
7063
  }
6790
- shouldLoadMoreMessages(scrollPosition) {
6791
- return (scrollPosition !== 'middle' &&
6792
- !this.highlightedMessageId &&
6793
- !this.isLoading);
6794
- }
6795
7064
  setMessages$() {
6796
- this.messages$ = (this.mode === 'main'
6797
- ? this.channelService.activeChannelMessages$
6798
- : this.channelService.activeThreadMessages$).pipe(tap((messages) => {
6799
- if (this.isLoading) {
6800
- this.isLoading = false;
7065
+ this.disposeVirtualizedList();
7066
+ this.virtualizedList = new VirtualizedMessageListService(this.mode, this.scrollPosition$, this.channelService);
7067
+ this.queryStateSubscription = this.virtualizedList.queryState$.subscribe((queryState) => {
7068
+ let mappedState = 'idle';
7069
+ if (queryState.state.includes('loading')) {
7070
+ mappedState = queryState.state || 'loading-bottom';
7071
+ }
7072
+ if (mappedState !== this.loadingState) {
7073
+ this.loadingState = mappedState;
7074
+ if (this.isViewInited) {
7075
+ this.cdRef.detectChanges();
7076
+ }
6801
7077
  }
7078
+ });
7079
+ this.messages$ = this.virtualizedList.virtualizedItems$.pipe(tap((messages) => {
6802
7080
  if (messages.length === 0) {
6803
- this.chatClientService.chatClient?.logger?.('info', `Empty messages array, reseting scroll state`, {
6804
- tags: `message list ${this.mode}`,
6805
- });
6806
7081
  this.resetScrollState();
6807
7082
  return;
6808
7083
  }
@@ -6810,21 +7085,6 @@ class MessageListComponent {
6810
7085
  // cdRef.detectChanges() isn't enough here, test will fail
6811
7086
  setTimeout(() => (this.isEmpty = false), 0);
6812
7087
  }
6813
- this.chatClientService.chatClient?.logger?.('info', `Received one or more messages`, {
6814
- tags: `message list ${this.mode}`,
6815
- });
6816
- const currentLatestMessageInState = messages[messages.length - 1];
6817
- this.newMessageReceived(currentLatestMessageInState);
6818
- const currentOldestMessage = messages[0];
6819
- if (!this.oldestMessage ||
6820
- !messages.find((m) => m.id === this.oldestMessage.id)) {
6821
- this.oldestMessage = currentOldestMessage;
6822
- }
6823
- else if (this.oldestMessage.created_at.getTime() >
6824
- currentOldestMessage.created_at.getTime()) {
6825
- this.oldestMessage = currentOldestMessage;
6826
- this.olderMassagesLoaded = true;
6827
- }
6828
7088
  }), tap((messages) => {
6829
7089
  if (this.isJumpingToLatestUnreadMessage &&
6830
7090
  !this.firstUnreadMessageId &&
@@ -6837,82 +7097,98 @@ class MessageListComponent {
6837
7097
  .reverse()
6838
7098
  .find((m) => m.user?.id === this.chatClientService.chatClient?.user?.id &&
6839
7099
  m.status !== 'sending')?.id)), tap((messages) => {
7100
+ const latestMessageInList = messages[messages.length - 1];
7101
+ const channel = this.channelService.activeChannel;
7102
+ const messagesFromState = (this.mode === 'main'
7103
+ ? channel?.state.latestMessages
7104
+ : channel?.state.threads[this.parentMessage?.id || '']) || [];
6840
7105
  this.isLatestMessageInList =
6841
- !this.latestMessage ||
6842
- messages.length === 0 ||
6843
- messages[messages.length - 1].id === this.latestMessage.id ||
6844
- this.mode === 'thread';
7106
+ !latestMessageInList ||
7107
+ latestMessageInList.cid !== channel?.cid ||
7108
+ latestMessageInList?.id ===
7109
+ messagesFromState[messagesFromState.length - 1]?.id;
6845
7110
  if (!this.isLatestMessageInList) {
6846
7111
  this.isUserScrolled = true;
6847
7112
  }
6848
- }), map((messages) => this.direction === 'bottom-to-top' ? messages : [...messages].reverse()), tap((messages) => {
7113
+ }), map((messages) => {
7114
+ return this.direction === 'bottom-to-top'
7115
+ ? messages
7116
+ : [...messages].reverse();
7117
+ }), tap((messages) => {
6849
7118
  this.groupStyles = messages.map((m, i) => getGroupStyles(m, messages[i - 1], messages[i + 1], {
6850
7119
  lastReadMessageId: this.lastReadMessageId,
6851
7120
  }));
6852
7121
  this.isNextMessageOnSeparateDate = messages.map((m, i) => this.checkIfOnSeparateDates(m, messages[i + 1]));
6853
7122
  }), shareReplay(1));
6854
- this.removeOldMessagesSubscription?.unsubscribe();
6855
- this.removeOldMessagesSubscription = combineLatest([
6856
- this.channelService.jumpToMessage$,
6857
- this.messages$,
6858
- ]).subscribe(([jumpToMessage, messages]) => {
6859
- if (this.limitNumberOfMessagesInList &&
6860
- this.mode === 'main' &&
6861
- messages.length >
6862
- ChannelService.MAX_MESSAGE_COUNT_IN_MESSAGE_LIST * 0.5 &&
6863
- !this.isUserScrolled &&
6864
- !jumpToMessage?.id &&
6865
- this.isLatestMessageInList) {
6866
- if (this.messageRemoveTimeout) {
6867
- clearTimeout(this.messageRemoveTimeout);
6868
- }
6869
- if (messages.length >= ChannelService.MAX_MESSAGE_COUNT_IN_MESSAGE_LIST) {
6870
- this.channelService.removeOldMessageFromMessageList();
6871
- }
6872
- else {
6873
- this.messageRemoveTimeout = setTimeout(() => {
6874
- if (this.limitNumberOfMessagesInList &&
6875
- this.mode === 'main' &&
6876
- messages.length >
6877
- ChannelService.MAX_MESSAGE_COUNT_IN_MESSAGE_LIST * 0.5 &&
6878
- !this.isUserScrolled &&
6879
- !this.highlightedMessageId &&
6880
- this.isLatestMessageInList) {
6881
- this.channelService.removeOldMessageFromMessageList();
6882
- }
6883
- }, 1500);
7123
+ if (this.virtualizedList?.jumpToItem$) {
7124
+ this.jumpToItemSubscription = this.virtualizedList.jumpToItem$
7125
+ .pipe(filter((jumpToMessage) => !!jumpToMessage.item?.id))
7126
+ .subscribe((jumpToMessage) => {
7127
+ let messageId = jumpToMessage.item?.id;
7128
+ if (messageId) {
7129
+ if (this.isJumpingToLatestUnreadMessage) {
7130
+ messageId = this.firstUnreadMessageId || messageId;
7131
+ }
7132
+ if (jumpToMessage.position !== 'bottom') {
7133
+ this.highlightedMessageId = messageId;
7134
+ }
7135
+ else if (this.direction === 'top-to-bottom') {
7136
+ jumpToMessage.position = 'top';
7137
+ }
7138
+ this.isJumpingToMessage = true;
7139
+ this.scrollMessageIntoView({
7140
+ messageId: this.firstUnreadMessageId || messageId,
7141
+ position: jumpToMessage.position || 'middle',
7142
+ });
6884
7143
  }
6885
- }
6886
- });
7144
+ });
7145
+ }
6887
7146
  }
6888
7147
  resetScrollState() {
6889
7148
  this.isEmpty = true;
6890
- this.latestMessage = undefined;
6891
- this.hasNewMessages = true;
6892
7149
  this.isUserScrolled = false;
6893
- this.containerHeight = undefined;
6894
- this.olderMassagesLoaded = false;
6895
- this.oldestMessage = undefined;
7150
+ this.messageIdToAnchorTo = undefined;
7151
+ this.anchorMessageTopOffset = undefined;
6896
7152
  this.newMessageCountWhileBeingScrolled = 0;
6897
- this.prevScrollTop = undefined;
6898
- this.isNewMessageSentByUser = undefined;
7153
+ this.isNewMessageSentByUser = false;
6899
7154
  this.isLatestMessageInList = true;
7155
+ this.isJumpingToMessage = false;
7156
+ this.scrollPosition$.next('bottom');
7157
+ this.loadingState = 'idle';
7158
+ }
7159
+ disposeVirtualizedList() {
7160
+ this.virtualizedList?.dispose();
7161
+ this.jumpToItemSubscription?.unsubscribe();
7162
+ this.queryStateSubscription?.unsubscribe();
6900
7163
  }
6901
7164
  get usersTyping$() {
6902
7165
  return this.mode === 'thread'
6903
7166
  ? this.usersTypingInThread$
6904
7167
  : this.usersTypingInChannel$;
6905
7168
  }
6906
- scrollMessageIntoView(messageId, withRetry = true) {
6907
- const element = document.getElementById(messageId);
7169
+ scrollMessageIntoView(options, withRetry = true) {
7170
+ const element = document.getElementById(options.messageId);
6908
7171
  if (!element && withRetry) {
6909
7172
  // If the message was newly inserted into activeChannelMessages$, the message will be rendered after the current change detection cycle -> wait for this cycle to complete
6910
- setTimeout(() => this.scrollMessageIntoView(messageId, false));
7173
+ setTimeout(() => this.scrollMessageIntoView(options, false));
6911
7174
  }
6912
7175
  else if (element) {
7176
+ const blockMapping = {
7177
+ top: 'start',
7178
+ bottom: 'end',
7179
+ middle: 'center',
7180
+ };
6913
7181
  element.scrollIntoView({
6914
- block: 'center',
7182
+ block: blockMapping[options.position],
6915
7183
  });
7184
+ if (options.position !== 'middle') {
7185
+ options.position === 'bottom'
7186
+ ? this.scrollToBottom()
7187
+ : this.scrollToTop();
7188
+ }
7189
+ setTimeout(() => {
7190
+ this.isJumpingToMessage = false;
7191
+ }, 0);
6916
7192
  setTimeout(() => {
6917
7193
  this.highlightedMessageId = undefined;
6918
7194
  this.firstUnreadMessageId = undefined;
@@ -6921,39 +7197,26 @@ class MessageListComponent {
6921
7197
  }, 1000);
6922
7198
  }
6923
7199
  }
6924
- scrollToLatestMessage(withRetry = true) {
6925
- if (document.getElementById(this.latestMessage.id)) {
6926
- this.direction === 'bottom-to-top'
6927
- ? this.scrollToBottom()
6928
- : this.scrollToTop();
7200
+ newMessageReceived(message) {
7201
+ if ((this.mode === 'main' && message.parent_id) ||
7202
+ (this.mode === 'thread' && message.parent_id !== this.parentMessage?.id)) {
7203
+ return;
6929
7204
  }
6930
- else if (withRetry) {
6931
- // If the message was newly inserted into activeChannelMessages$, the message will be rendered after the current change detection cycle -> wait for this cycle to complete
6932
- setTimeout(() => this.scrollToLatestMessage(false), 0);
7205
+ const isNewMessageSentByCurrentUser = message.user?.id === this.chatClientService.chatClient?.user?.id;
7206
+ let shouldDetectChanges = false;
7207
+ if (!this.isNewMessageSentByUser && isNewMessageSentByCurrentUser) {
7208
+ this.isNewMessageSentByUser = true;
7209
+ shouldDetectChanges = true;
6933
7210
  }
6934
- }
6935
- newMessageReceived(message) {
6936
- const latestMessages = this.channelService.activeChannel?.state?.latestMessages;
6937
- if (!this.latestMessage ||
6938
- this.latestMessage.created_at?.getTime() < message.created_at.getTime() ||
6939
- (this.mode === 'main' &&
6940
- latestMessages &&
6941
- this.latestMessage &&
6942
- latestMessages[latestMessages.length - 1]?.id !== this.latestMessage.id)) {
6943
- this.chatClientService.chatClient?.logger?.('info', `Received new message`, { tags: `message list ${this.mode}` });
6944
- const isNewChannel = !this.latestMessage;
6945
- this.latestMessage = message;
6946
- this.hasNewMessages = true;
6947
- this.isNewMessageSentByUser =
6948
- message.user?.id === this.chatClientService.chatClient?.user?.id;
6949
- if (this.isUserScrolled) {
6950
- this.newMessageCountWhileBeingScrolled++;
6951
- }
6952
- if (!this.isNewMessageSentByUser &&
6953
- this.unreadCount !== undefined &&
6954
- !isNewChannel) {
6955
- this.unreadCount++;
6956
- }
7211
+ if (this.isUserScrolled) {
7212
+ this.newMessageCountWhileBeingScrolled++;
7213
+ shouldDetectChanges = true;
7214
+ }
7215
+ if (!this.isNewMessageSentByUser && this.unreadCount !== undefined) {
7216
+ this.unreadCount++;
7217
+ shouldDetectChanges = true;
7218
+ }
7219
+ if (shouldDetectChanges && this.isViewInited) {
6957
7220
  this.cdRef.detectChanges();
6958
7221
  }
6959
7222
  }
@@ -6965,10 +7228,10 @@ class MessageListComponent {
6965
7228
  }
6966
7229
  }
6967
7230
  MessageListComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: MessageListComponent, deps: [{ token: ChannelService }, { token: ChatClientService }, { token: CustomTemplatesService }, { token: DateParserService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
6968
- MessageListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.4", type: MessageListComponent, selector: "stream-message-list", inputs: { mode: "mode", direction: "direction", hideJumpToLatestButtonDuringScroll: "hideJumpToLatestButtonDuringScroll", displayDateSeparator: "displayDateSeparator", displayUnreadSeparator: "displayUnreadSeparator", dateSeparatorTextPos: "dateSeparatorTextPos", openMessageListAt: "openMessageListAt", hideUnreadCountForNotificationAndIndicator: "hideUnreadCountForNotificationAndIndicator", displayLoadingIndicator: "displayLoadingIndicator", limitNumberOfMessagesInList: "limitNumberOfMessagesInList" }, host: { properties: { "class": "this.class" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "parentMessageElement", first: true, predicate: ["parentMessageElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container\n *ngIf=\"\n lastReadMessageId &&\n isUnreadNotificationVisible &&\n openMessageListAt === 'last-message' &&\n displayUnreadSeparator\n \"\n>\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesNotificationTemplate ||\n defaultUnreadMessagesNotification;\n context: {\n unreadCount: unreadCount,\n onDismiss: messageNotificationDismissClicked,\n onJump: messageNotificationJumpClicked\n }\n \"\n ></ng-container>\n</ng-container>\n<ng-template\n #defaultUnreadMessagesNotification\n let-unreadCount=\"unreadCount\"\n let-onDismiss=\"onDismiss\"\n let-onJump=\"onJump\"\n>\n <div\n class=\"str-chat__unread-messages-notification\"\n data-testid=\"unread-messages-notification\"\n >\n <button\n data-testid=\"unread-messages-notification-jump-to-message\"\n (click)=\"onJump()\"\n >\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </button>\n <button\n data-testid=\"unread-messages-notification-dismiss\"\n (click)=\"onDismiss()\"\n >\n <stream-icon-placeholder icon=\"close\"></stream-icon-placeholder>\n </button>\n </div>\n</ng-template>\n<div #scrollContainer data-testid=\"scroll-container\" class=\"str-chat__list\">\n <ng-container *ngIf=\"mode === 'main' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <div class=\"str-chat__reverse-infinite-scroll str-chat__message-list-scroll\">\n <ul class=\"str-chat__ul\">\n <li\n *ngIf=\"mode === 'thread' && parentMessage\"\n #parentMessageElement\n data-testid=\"parent-message\"\n class=\"str-chat__parent-message-li\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: parentMessage, index: 'parent' }\n \"\n ></ng-container>\n <div data-testid=\"reply-count\" class=\"str-chat__thread-start\">\n {{parentMessage.reply_count === 1 ? ('streamChat.1 reply' | translate) : ('streamChat.{{ replyCount }}\n replies' | translate:replyCountParam)}}\n </div>\n </li>\n <ng-container *ngIf=\"mode === 'thread' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <stream-loading-indicator\n *ngIf=\"\n isLoading && direction === 'bottom-to-top' && displayLoadingIndicator\n \"\n data-testid=\"top-loading-indicator\"\n ></stream-loading-indicator>\n <ng-container *ngIf=\"messages$ | async as messages\">\n <ng-container\n *ngFor=\"\n let message of messages;\n let i = index;\n let isFirst = first;\n let isLast = last;\n trackBy: trackByMessageId\n \"\n >\n <ng-container *ngIf=\"isFirst\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: message.created_at,\n parsedDate: parseDate(message.created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n <li\n tabindex=\"0\"\n data-testclass=\"message\"\n class=\"str-chat__li str-chat__li--{{ groupStyles[i] }}\"\n id=\"{{ message.id }}\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: message, index: i }\n \"\n ></ng-container>\n </li>\n <ng-container\n *ngIf=\"\n (lastReadMessageId === message?.id &&\n direction === 'bottom-to-top') ||\n (direction === 'top-to-bottom' &&\n lastReadMessageId === messages[i + 1]?.id)\n \"\n >\n <li\n *ngIf=\"displayUnreadSeparator\"\n id=\"stream-chat-new-message-indicator\"\n data-testid=\"new-messages-indicator\"\n class=\"str-chat__li str-chat__unread-messages-separator-wrapper\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesIndicatorTemplate ||\n defaultNewMessagesIndicator;\n context: { unreadCount: unreadCount }\n \"\n ></ng-container>\n </li>\n </ng-container>\n <ng-container *ngIf=\"isNextMessageOnSeparateDate[i]\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: messages[i + 1].created_at,\n parsedDate: parseDate(messages[i + 1].created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n </ng-container>\n </ng-container>\n <stream-loading-indicator\n *ngIf=\"\n isLoading && direction === 'top-to-bottom' && displayLoadingIndicator\n \"\n data-testid=\"bottom-loading-indicator\"\n ></stream-loading-indicator>\n </ul>\n <ng-template #defaultTypingIndicator let-usersTyping$=\"usersTyping$\">\n <!-- eslint-disable-next-line @angular-eslint/template/no-any -->\n <ng-container *ngIf=\"$any(usersTyping$ | async) as users\">\n <div\n *ngIf=\"users.length > 0\"\n data-testid=\"typing-indicator\"\n class=\"str-chat__typing-indicator str-chat__typing-indicator--typing\"\n >\n <div class=\"str-chat__typing-indicator__dots\">\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n </div>\n <div\n data-testid=\"typing-users\"\n class=\"str-chat__typing-indicator__users\"\n >\n {{\n users.length === 1\n ? (\"streamChat.user is typing\"\n | translate: { user: getTypingIndicatorText(users) })\n : (\"streamChat.users are typing\"\n | translate: { users: getTypingIndicatorText(users) })\n }}\n </div>\n </div>\n </ng-container>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n typingIndicatorTemplate || defaultTypingIndicator;\n context: getTypingIndicatorContext()\n \"\n ></ng-container>\n </div>\n</div>\n<div class=\"str-chat__jump-to-latest-message\">\n <button\n *ngIf=\"isUserScrolled && isJumpToLatestButtonVisible\"\n data-testid=\"scroll-to-latest\"\n class=\"\n str-chat__message-notification-scroll-to-latest\n str-chat__message-notification-scroll-to-latest-right\n str-chat__circle-fab\n \"\n (keyup.enter)=\"jumpToLatestMessage()\"\n (click)=\"jumpToLatestMessage()\"\n >\n <stream-icon\n class=\"str-chat__jump-to-latest-icon str-chat__circle-fab-icon\"\n [icon]=\"direction === 'bottom-to-top' ? 'arrow-down' : 'arrow-up'\"\n ></stream-icon>\n <div\n *ngIf=\"newMessageCountWhileBeingScrolled > 0\"\n class=\"\n str-chat__message-notification\n str-chat__message-notification-scroll-to-latest-unread-count\n str-chat__jump-to-latest-unread-count\n \"\n >\n {{ newMessageCountWhileBeingScrolled }}\n </div>\n </button>\n</div>\n\n<ng-template #messageTemplateContainer let-message=\"message\" let-index=\"index\">\n <ng-template\n #defaultMessageTemplate\n let-messageInput=\"message\"\n let-isLastSentMessage=\"isLastSentMessage\"\n let-enabledMessageActions=\"enabledMessageActions\"\n let-mode=\"mode\"\n let-isHighlighted=\"isHighlighted\"\n let-scroll$=\"scroll$\"\n >\n <stream-message\n [message]=\"messageInput\"\n [isLastSentMessage]=\"isLastSentMessage\"\n [enabledMessageActions]=\"enabledMessageActions\"\n [mode]=\"mode\"\n [isHighlighted]=\"isHighlighted\"\n [scroll$]=\"scroll$\"\n ></stream-message>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplate || defaultMessageTemplate;\n context: {\n message: message,\n isLastSentMessage: !!(\n lastSentMessageId && message?.id === lastSentMessageId\n ),\n enabledMessageActions: enabledMessageActions,\n mode: mode,\n isHighlighted: message?.id === highlightedMessageId,\n scroll$: scroll$\n }\n \"\n ></ng-container>\n</ng-template>\n\n<ng-template #dateSeparator let-date=\"date\" let-parsedDate=\"parsedDate\">\n <ng-container *ngIf=\"displayDateSeparator\">\n <ng-container\n *ngTemplateOutlet=\"\n customDateSeparatorTemplate || defaultDateSeparator;\n context: {\n date: date,\n parsedDate: parsedDate\n }\n \"\n ></ng-container>\n </ng-container>\n\n <ng-template\n #defaultDateSeparator\n let-date=\"date\"\n let-parsedDate=\"parsedDate\"\n >\n <div data-testid=\"date-separator\" class=\"str-chat__date-separator\">\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'right' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n <div class=\"str-chat__date-separator-date\">\n {{ parsedDate }}\n </div>\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'left' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n </div>\n </ng-template>\n</ng-template>\n\n<ng-template #defaultNewMessagesIndicator let-unreadCount=\"unreadCount\">\n <div class=\"str-chat__unread-messages-separator\">\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MessageComponent, selector: "stream-message", inputs: ["message", "enabledMessageActions", "isLastSentMessage", "mode", "isHighlighted", "scroll$"] }, { kind: "component", type: LoadingIndicatorComponent, selector: "stream-loading-indicator" }, { kind: "component", type: IconComponent, selector: "stream-icon", inputs: ["icon"] }, { kind: "component", type: IconPlaceholderComponent, selector: "stream-icon-placeholder", inputs: ["icon"] }, { kind: "pipe", type: i4.AsyncPipe, name: "async" }, { kind: "pipe", type: i10.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7231
+ MessageListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.0.4", type: MessageListComponent, selector: "stream-message-list", inputs: { mode: "mode", direction: "direction", hideJumpToLatestButtonDuringScroll: "hideJumpToLatestButtonDuringScroll", displayDateSeparator: "displayDateSeparator", displayUnreadSeparator: "displayUnreadSeparator", dateSeparatorTextPos: "dateSeparatorTextPos", openMessageListAt: "openMessageListAt", hideUnreadCountForNotificationAndIndicator: "hideUnreadCountForNotificationAndIndicator", displayLoadingIndicator: "displayLoadingIndicator" }, host: { properties: { "class": "this.class" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "parentMessageElement", first: true, predicate: ["parentMessageElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container\n *ngIf=\"\n lastReadMessageId &&\n isUnreadNotificationVisible &&\n openMessageListAt === 'last-message' &&\n displayUnreadSeparator\n \"\n>\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesNotificationTemplate ||\n defaultUnreadMessagesNotification;\n context: {\n unreadCount: unreadCount,\n onDismiss: messageNotificationDismissClicked,\n onJump: messageNotificationJumpClicked\n }\n \"\n ></ng-container>\n</ng-container>\n<ng-template\n #defaultUnreadMessagesNotification\n let-unreadCount=\"unreadCount\"\n let-onDismiss=\"onDismiss\"\n let-onJump=\"onJump\"\n>\n <div\n class=\"str-chat__unread-messages-notification\"\n data-testid=\"unread-messages-notification\"\n >\n <button\n data-testid=\"unread-messages-notification-jump-to-message\"\n (click)=\"onJump()\"\n >\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </button>\n <button\n data-testid=\"unread-messages-notification-dismiss\"\n (click)=\"onDismiss()\"\n >\n <stream-icon-placeholder icon=\"close\"></stream-icon-placeholder>\n </button>\n </div>\n</ng-template>\n<div\n #scrollContainer\n data-testid=\"scroll-container\"\n class=\"str-chat__list\"\n style=\"overscroll-behavior-y: none\"\n>\n <ng-container *ngIf=\"mode === 'main' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <div class=\"str-chat__reverse-infinite-scroll str-chat__message-list-scroll\">\n <ul class=\"str-chat__ul\">\n <li\n *ngIf=\"mode === 'thread' && parentMessage\"\n #parentMessageElement\n data-testid=\"parent-message\"\n class=\"str-chat__parent-message-li\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: parentMessage, index: 'parent' }\n \"\n ></ng-container>\n <div data-testid=\"reply-count\" class=\"str-chat__thread-start\">\n {{parentMessage.reply_count === 1 ? ('streamChat.1 reply' | translate) : ('streamChat.{{ replyCount }}\n replies' | translate:replyCountParam)}}\n </div>\n </li>\n <ng-container *ngIf=\"mode === 'thread' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <stream-loading-indicator-placeholder\n *ngIf=\"\n ((loadingState === 'loading-top' && direction === 'bottom-to-top') ||\n (loadingState === 'loading-bottom' &&\n direction === 'top-to-bottom')) &&\n displayLoadingIndicator;\n else loadingIndicatorPlaceholder\n \"\n data-testid=\"top-loading-indicator\"\n ></stream-loading-indicator-placeholder>\n <ng-container *ngIf=\"messages$ | async as messages\">\n <ng-container\n *ngFor=\"\n let message of messages;\n let i = index;\n let isFirst = first;\n let isLast = last;\n trackBy: trackByMessageId\n \"\n >\n <ng-container *ngIf=\"isFirst\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: message.created_at,\n parsedDate: parseDate(message.created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n <li\n tabindex=\"0\"\n data-testclass=\"message\"\n class=\"str-chat__li str-chat__li--{{ groupStyles[i] }}\"\n id=\"{{ message.id }}\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: message, index: i }\n \"\n ></ng-container>\n </li>\n <ng-container\n *ngIf=\"\n (lastReadMessageId === message?.id &&\n direction === 'bottom-to-top') ||\n (direction === 'top-to-bottom' &&\n lastReadMessageId === messages[i + 1]?.id)\n \"\n >\n <li\n *ngIf=\"displayUnreadSeparator\"\n id=\"stream-chat-new-message-indicator\"\n data-testid=\"new-messages-indicator\"\n class=\"str-chat__li str-chat__unread-messages-separator-wrapper\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesIndicatorTemplate ||\n defaultNewMessagesIndicator;\n context: { unreadCount: unreadCount }\n \"\n ></ng-container>\n </li>\n </ng-container>\n <ng-container *ngIf=\"isNextMessageOnSeparateDate[i]\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: messages[i + 1].created_at,\n parsedDate: parseDate(messages[i + 1].created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n </ng-container>\n </ng-container>\n <stream-loading-indicator-placeholder\n *ngIf=\"\n ((loadingState === 'loading-bottom' &&\n direction === 'bottom-to-top') ||\n (loadingState === 'loading-top' &&\n direction === 'top-to-bottom')) &&\n displayLoadingIndicator;\n else loadingIndicatorPlaceholder\n \"\n data-testid=\"bottom-loading-indicator\"\n ></stream-loading-indicator-placeholder>\n <ng-template #loadingIndicatorPlaceholder>\n <div class=\"str-chat__loading-indicator-placeholder\"></div>\n </ng-template>\n </ul>\n <ng-template #defaultTypingIndicator let-usersTyping$=\"usersTyping$\">\n <!-- eslint-disable-next-line @angular-eslint/template/no-any -->\n <ng-container *ngIf=\"$any(usersTyping$ | async) as users\">\n <div\n *ngIf=\"users.length > 0\"\n data-testid=\"typing-indicator\"\n class=\"str-chat__typing-indicator str-chat__typing-indicator--typing\"\n >\n <div class=\"str-chat__typing-indicator__dots\">\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n </div>\n <div\n data-testid=\"typing-users\"\n class=\"str-chat__typing-indicator__users\"\n >\n {{\n users.length === 1\n ? (\"streamChat.user is typing\"\n | translate: { user: getTypingIndicatorText(users) })\n : (\"streamChat.users are typing\"\n | translate: { users: getTypingIndicatorText(users) })\n }}\n </div>\n </div>\n </ng-container>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n typingIndicatorTemplate || defaultTypingIndicator;\n context: getTypingIndicatorContext()\n \"\n ></ng-container>\n </div>\n</div>\n<div class=\"str-chat__jump-to-latest-message\">\n <button\n *ngIf=\"isUserScrolled && isJumpToLatestButtonVisible\"\n data-testid=\"scroll-to-latest\"\n class=\"\n str-chat__message-notification-scroll-to-latest\n str-chat__message-notification-scroll-to-latest-right\n str-chat__circle-fab\n \"\n (keyup.enter)=\"jumpToLatestMessage()\"\n (click)=\"jumpToLatestMessage()\"\n >\n <stream-icon\n class=\"str-chat__jump-to-latest-icon str-chat__circle-fab-icon\"\n [icon]=\"direction === 'bottom-to-top' ? 'arrow-down' : 'arrow-up'\"\n ></stream-icon>\n <div\n *ngIf=\"newMessageCountWhileBeingScrolled > 0\"\n class=\"\n str-chat__message-notification\n str-chat__message-notification-scroll-to-latest-unread-count\n str-chat__jump-to-latest-unread-count\n \"\n >\n {{ newMessageCountWhileBeingScrolled }}\n </div>\n </button>\n</div>\n\n<ng-template #messageTemplateContainer let-message=\"message\" let-index=\"index\">\n <ng-template\n #defaultMessageTemplate\n let-messageInput=\"message\"\n let-isLastSentMessage=\"isLastSentMessage\"\n let-enabledMessageActions=\"enabledMessageActions\"\n let-mode=\"mode\"\n let-isHighlighted=\"isHighlighted\"\n let-scroll$=\"scroll$\"\n >\n <stream-message\n [message]=\"messageInput\"\n [isLastSentMessage]=\"isLastSentMessage\"\n [enabledMessageActions]=\"enabledMessageActions\"\n [mode]=\"mode\"\n [isHighlighted]=\"isHighlighted\"\n [scroll$]=\"scroll$\"\n ></stream-message>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplate || defaultMessageTemplate;\n context: {\n message: message,\n isLastSentMessage: !!(\n lastSentMessageId && message?.id === lastSentMessageId\n ),\n enabledMessageActions: enabledMessageActions,\n mode: mode,\n isHighlighted: message?.id === highlightedMessageId,\n scroll$: scroll$\n }\n \"\n ></ng-container>\n</ng-template>\n\n<ng-template #dateSeparator let-date=\"date\" let-parsedDate=\"parsedDate\">\n <ng-container *ngIf=\"displayDateSeparator\">\n <ng-container\n *ngTemplateOutlet=\"\n customDateSeparatorTemplate || defaultDateSeparator;\n context: {\n date: date,\n parsedDate: parsedDate\n }\n \"\n ></ng-container>\n </ng-container>\n\n <ng-template\n #defaultDateSeparator\n let-date=\"date\"\n let-parsedDate=\"parsedDate\"\n >\n <div data-testid=\"date-separator\" class=\"str-chat__date-separator\">\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'right' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n <div class=\"str-chat__date-separator-date\">\n {{ parsedDate }}\n </div>\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'left' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n </div>\n </ng-template>\n</ng-template>\n\n<ng-template #defaultNewMessagesIndicator let-unreadCount=\"unreadCount\">\n <div class=\"str-chat__unread-messages-separator\">\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MessageComponent, selector: "stream-message", inputs: ["message", "enabledMessageActions", "isLastSentMessage", "mode", "isHighlighted", "scroll$"] }, { kind: "component", type: IconComponent, selector: "stream-icon", inputs: ["icon"] }, { kind: "component", type: IconPlaceholderComponent, selector: "stream-icon-placeholder", inputs: ["icon"] }, { kind: "component", type: LoadingIndicatorPlaceholderComponent, selector: "stream-loading-indicator-placeholder" }, { kind: "pipe", type: i4.AsyncPipe, name: "async" }, { kind: "pipe", type: i10.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6969
7232
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: MessageListComponent, decorators: [{
6970
7233
  type: Component,
6971
- args: [{ selector: 'stream-message-list', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container\n *ngIf=\"\n lastReadMessageId &&\n isUnreadNotificationVisible &&\n openMessageListAt === 'last-message' &&\n displayUnreadSeparator\n \"\n>\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesNotificationTemplate ||\n defaultUnreadMessagesNotification;\n context: {\n unreadCount: unreadCount,\n onDismiss: messageNotificationDismissClicked,\n onJump: messageNotificationJumpClicked\n }\n \"\n ></ng-container>\n</ng-container>\n<ng-template\n #defaultUnreadMessagesNotification\n let-unreadCount=\"unreadCount\"\n let-onDismiss=\"onDismiss\"\n let-onJump=\"onJump\"\n>\n <div\n class=\"str-chat__unread-messages-notification\"\n data-testid=\"unread-messages-notification\"\n >\n <button\n data-testid=\"unread-messages-notification-jump-to-message\"\n (click)=\"onJump()\"\n >\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </button>\n <button\n data-testid=\"unread-messages-notification-dismiss\"\n (click)=\"onDismiss()\"\n >\n <stream-icon-placeholder icon=\"close\"></stream-icon-placeholder>\n </button>\n </div>\n</ng-template>\n<div #scrollContainer data-testid=\"scroll-container\" class=\"str-chat__list\">\n <ng-container *ngIf=\"mode === 'main' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <div class=\"str-chat__reverse-infinite-scroll str-chat__message-list-scroll\">\n <ul class=\"str-chat__ul\">\n <li\n *ngIf=\"mode === 'thread' && parentMessage\"\n #parentMessageElement\n data-testid=\"parent-message\"\n class=\"str-chat__parent-message-li\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: parentMessage, index: 'parent' }\n \"\n ></ng-container>\n <div data-testid=\"reply-count\" class=\"str-chat__thread-start\">\n {{parentMessage.reply_count === 1 ? ('streamChat.1 reply' | translate) : ('streamChat.{{ replyCount }}\n replies' | translate:replyCountParam)}}\n </div>\n </li>\n <ng-container *ngIf=\"mode === 'thread' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <stream-loading-indicator\n *ngIf=\"\n isLoading && direction === 'bottom-to-top' && displayLoadingIndicator\n \"\n data-testid=\"top-loading-indicator\"\n ></stream-loading-indicator>\n <ng-container *ngIf=\"messages$ | async as messages\">\n <ng-container\n *ngFor=\"\n let message of messages;\n let i = index;\n let isFirst = first;\n let isLast = last;\n trackBy: trackByMessageId\n \"\n >\n <ng-container *ngIf=\"isFirst\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: message.created_at,\n parsedDate: parseDate(message.created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n <li\n tabindex=\"0\"\n data-testclass=\"message\"\n class=\"str-chat__li str-chat__li--{{ groupStyles[i] }}\"\n id=\"{{ message.id }}\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: message, index: i }\n \"\n ></ng-container>\n </li>\n <ng-container\n *ngIf=\"\n (lastReadMessageId === message?.id &&\n direction === 'bottom-to-top') ||\n (direction === 'top-to-bottom' &&\n lastReadMessageId === messages[i + 1]?.id)\n \"\n >\n <li\n *ngIf=\"displayUnreadSeparator\"\n id=\"stream-chat-new-message-indicator\"\n data-testid=\"new-messages-indicator\"\n class=\"str-chat__li str-chat__unread-messages-separator-wrapper\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesIndicatorTemplate ||\n defaultNewMessagesIndicator;\n context: { unreadCount: unreadCount }\n \"\n ></ng-container>\n </li>\n </ng-container>\n <ng-container *ngIf=\"isNextMessageOnSeparateDate[i]\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: messages[i + 1].created_at,\n parsedDate: parseDate(messages[i + 1].created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n </ng-container>\n </ng-container>\n <stream-loading-indicator\n *ngIf=\"\n isLoading && direction === 'top-to-bottom' && displayLoadingIndicator\n \"\n data-testid=\"bottom-loading-indicator\"\n ></stream-loading-indicator>\n </ul>\n <ng-template #defaultTypingIndicator let-usersTyping$=\"usersTyping$\">\n <!-- eslint-disable-next-line @angular-eslint/template/no-any -->\n <ng-container *ngIf=\"$any(usersTyping$ | async) as users\">\n <div\n *ngIf=\"users.length > 0\"\n data-testid=\"typing-indicator\"\n class=\"str-chat__typing-indicator str-chat__typing-indicator--typing\"\n >\n <div class=\"str-chat__typing-indicator__dots\">\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n </div>\n <div\n data-testid=\"typing-users\"\n class=\"str-chat__typing-indicator__users\"\n >\n {{\n users.length === 1\n ? (\"streamChat.user is typing\"\n | translate: { user: getTypingIndicatorText(users) })\n : (\"streamChat.users are typing\"\n | translate: { users: getTypingIndicatorText(users) })\n }}\n </div>\n </div>\n </ng-container>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n typingIndicatorTemplate || defaultTypingIndicator;\n context: getTypingIndicatorContext()\n \"\n ></ng-container>\n </div>\n</div>\n<div class=\"str-chat__jump-to-latest-message\">\n <button\n *ngIf=\"isUserScrolled && isJumpToLatestButtonVisible\"\n data-testid=\"scroll-to-latest\"\n class=\"\n str-chat__message-notification-scroll-to-latest\n str-chat__message-notification-scroll-to-latest-right\n str-chat__circle-fab\n \"\n (keyup.enter)=\"jumpToLatestMessage()\"\n (click)=\"jumpToLatestMessage()\"\n >\n <stream-icon\n class=\"str-chat__jump-to-latest-icon str-chat__circle-fab-icon\"\n [icon]=\"direction === 'bottom-to-top' ? 'arrow-down' : 'arrow-up'\"\n ></stream-icon>\n <div\n *ngIf=\"newMessageCountWhileBeingScrolled > 0\"\n class=\"\n str-chat__message-notification\n str-chat__message-notification-scroll-to-latest-unread-count\n str-chat__jump-to-latest-unread-count\n \"\n >\n {{ newMessageCountWhileBeingScrolled }}\n </div>\n </button>\n</div>\n\n<ng-template #messageTemplateContainer let-message=\"message\" let-index=\"index\">\n <ng-template\n #defaultMessageTemplate\n let-messageInput=\"message\"\n let-isLastSentMessage=\"isLastSentMessage\"\n let-enabledMessageActions=\"enabledMessageActions\"\n let-mode=\"mode\"\n let-isHighlighted=\"isHighlighted\"\n let-scroll$=\"scroll$\"\n >\n <stream-message\n [message]=\"messageInput\"\n [isLastSentMessage]=\"isLastSentMessage\"\n [enabledMessageActions]=\"enabledMessageActions\"\n [mode]=\"mode\"\n [isHighlighted]=\"isHighlighted\"\n [scroll$]=\"scroll$\"\n ></stream-message>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplate || defaultMessageTemplate;\n context: {\n message: message,\n isLastSentMessage: !!(\n lastSentMessageId && message?.id === lastSentMessageId\n ),\n enabledMessageActions: enabledMessageActions,\n mode: mode,\n isHighlighted: message?.id === highlightedMessageId,\n scroll$: scroll$\n }\n \"\n ></ng-container>\n</ng-template>\n\n<ng-template #dateSeparator let-date=\"date\" let-parsedDate=\"parsedDate\">\n <ng-container *ngIf=\"displayDateSeparator\">\n <ng-container\n *ngTemplateOutlet=\"\n customDateSeparatorTemplate || defaultDateSeparator;\n context: {\n date: date,\n parsedDate: parsedDate\n }\n \"\n ></ng-container>\n </ng-container>\n\n <ng-template\n #defaultDateSeparator\n let-date=\"date\"\n let-parsedDate=\"parsedDate\"\n >\n <div data-testid=\"date-separator\" class=\"str-chat__date-separator\">\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'right' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n <div class=\"str-chat__date-separator-date\">\n {{ parsedDate }}\n </div>\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'left' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n </div>\n </ng-template>\n</ng-template>\n\n<ng-template #defaultNewMessagesIndicator let-unreadCount=\"unreadCount\">\n <div class=\"str-chat__unread-messages-separator\">\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </div>\n</ng-template>\n" }]
7234
+ args: [{ selector: 'stream-message-list', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container\n *ngIf=\"\n lastReadMessageId &&\n isUnreadNotificationVisible &&\n openMessageListAt === 'last-message' &&\n displayUnreadSeparator\n \"\n>\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesNotificationTemplate ||\n defaultUnreadMessagesNotification;\n context: {\n unreadCount: unreadCount,\n onDismiss: messageNotificationDismissClicked,\n onJump: messageNotificationJumpClicked\n }\n \"\n ></ng-container>\n</ng-container>\n<ng-template\n #defaultUnreadMessagesNotification\n let-unreadCount=\"unreadCount\"\n let-onDismiss=\"onDismiss\"\n let-onJump=\"onJump\"\n>\n <div\n class=\"str-chat__unread-messages-notification\"\n data-testid=\"unread-messages-notification\"\n >\n <button\n data-testid=\"unread-messages-notification-jump-to-message\"\n (click)=\"onJump()\"\n >\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </button>\n <button\n data-testid=\"unread-messages-notification-dismiss\"\n (click)=\"onDismiss()\"\n >\n <stream-icon-placeholder icon=\"close\"></stream-icon-placeholder>\n </button>\n </div>\n</ng-template>\n<div\n #scrollContainer\n data-testid=\"scroll-container\"\n class=\"str-chat__list\"\n style=\"overscroll-behavior-y: none\"\n>\n <ng-container *ngIf=\"mode === 'main' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <div class=\"str-chat__reverse-infinite-scroll str-chat__message-list-scroll\">\n <ul class=\"str-chat__ul\">\n <li\n *ngIf=\"mode === 'thread' && parentMessage\"\n #parentMessageElement\n data-testid=\"parent-message\"\n class=\"str-chat__parent-message-li\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: parentMessage, index: 'parent' }\n \"\n ></ng-container>\n <div data-testid=\"reply-count\" class=\"str-chat__thread-start\">\n {{parentMessage.reply_count === 1 ? ('streamChat.1 reply' | translate) : ('streamChat.{{ replyCount }}\n replies' | translate:replyCountParam)}}\n </div>\n </li>\n <ng-container *ngIf=\"mode === 'thread' && isEmpty && emptyListTemplate\">\n <ng-container *ngTemplateOutlet=\"emptyListTemplate\"></ng-container>\n </ng-container>\n <stream-loading-indicator-placeholder\n *ngIf=\"\n ((loadingState === 'loading-top' && direction === 'bottom-to-top') ||\n (loadingState === 'loading-bottom' &&\n direction === 'top-to-bottom')) &&\n displayLoadingIndicator;\n else loadingIndicatorPlaceholder\n \"\n data-testid=\"top-loading-indicator\"\n ></stream-loading-indicator-placeholder>\n <ng-container *ngIf=\"messages$ | async as messages\">\n <ng-container\n *ngFor=\"\n let message of messages;\n let i = index;\n let isFirst = first;\n let isLast = last;\n trackBy: trackByMessageId\n \"\n >\n <ng-container *ngIf=\"isFirst\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: message.created_at,\n parsedDate: parseDate(message.created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n <li\n tabindex=\"0\"\n data-testclass=\"message\"\n class=\"str-chat__li str-chat__li--{{ groupStyles[i] }}\"\n id=\"{{ message.id }}\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplateContainer;\n context: { message: message, index: i }\n \"\n ></ng-container>\n </li>\n <ng-container\n *ngIf=\"\n (lastReadMessageId === message?.id &&\n direction === 'bottom-to-top') ||\n (direction === 'top-to-bottom' &&\n lastReadMessageId === messages[i + 1]?.id)\n \"\n >\n <li\n *ngIf=\"displayUnreadSeparator\"\n id=\"stream-chat-new-message-indicator\"\n data-testid=\"new-messages-indicator\"\n class=\"str-chat__li str-chat__unread-messages-separator-wrapper\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n customnewMessagesIndicatorTemplate ||\n defaultNewMessagesIndicator;\n context: { unreadCount: unreadCount }\n \"\n ></ng-container>\n </li>\n </ng-container>\n <ng-container *ngIf=\"isNextMessageOnSeparateDate[i]\">\n <ng-container\n *ngTemplateOutlet=\"\n dateSeparator;\n context: {\n date: messages[i + 1].created_at,\n parsedDate: parseDate(messages[i + 1].created_at)\n }\n \"\n ></ng-container>\n </ng-container>\n </ng-container>\n </ng-container>\n <stream-loading-indicator-placeholder\n *ngIf=\"\n ((loadingState === 'loading-bottom' &&\n direction === 'bottom-to-top') ||\n (loadingState === 'loading-top' &&\n direction === 'top-to-bottom')) &&\n displayLoadingIndicator;\n else loadingIndicatorPlaceholder\n \"\n data-testid=\"bottom-loading-indicator\"\n ></stream-loading-indicator-placeholder>\n <ng-template #loadingIndicatorPlaceholder>\n <div class=\"str-chat__loading-indicator-placeholder\"></div>\n </ng-template>\n </ul>\n <ng-template #defaultTypingIndicator let-usersTyping$=\"usersTyping$\">\n <!-- eslint-disable-next-line @angular-eslint/template/no-any -->\n <ng-container *ngIf=\"$any(usersTyping$ | async) as users\">\n <div\n *ngIf=\"users.length > 0\"\n data-testid=\"typing-indicator\"\n class=\"str-chat__typing-indicator str-chat__typing-indicator--typing\"\n >\n <div class=\"str-chat__typing-indicator__dots\">\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n <span class=\"str-chat__typing-indicator__dot\"></span>\n </div>\n <div\n data-testid=\"typing-users\"\n class=\"str-chat__typing-indicator__users\"\n >\n {{\n users.length === 1\n ? (\"streamChat.user is typing\"\n | translate: { user: getTypingIndicatorText(users) })\n : (\"streamChat.users are typing\"\n | translate: { users: getTypingIndicatorText(users) })\n }}\n </div>\n </div>\n </ng-container>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n typingIndicatorTemplate || defaultTypingIndicator;\n context: getTypingIndicatorContext()\n \"\n ></ng-container>\n </div>\n</div>\n<div class=\"str-chat__jump-to-latest-message\">\n <button\n *ngIf=\"isUserScrolled && isJumpToLatestButtonVisible\"\n data-testid=\"scroll-to-latest\"\n class=\"\n str-chat__message-notification-scroll-to-latest\n str-chat__message-notification-scroll-to-latest-right\n str-chat__circle-fab\n \"\n (keyup.enter)=\"jumpToLatestMessage()\"\n (click)=\"jumpToLatestMessage()\"\n >\n <stream-icon\n class=\"str-chat__jump-to-latest-icon str-chat__circle-fab-icon\"\n [icon]=\"direction === 'bottom-to-top' ? 'arrow-down' : 'arrow-up'\"\n ></stream-icon>\n <div\n *ngIf=\"newMessageCountWhileBeingScrolled > 0\"\n class=\"\n str-chat__message-notification\n str-chat__message-notification-scroll-to-latest-unread-count\n str-chat__jump-to-latest-unread-count\n \"\n >\n {{ newMessageCountWhileBeingScrolled }}\n </div>\n </button>\n</div>\n\n<ng-template #messageTemplateContainer let-message=\"message\" let-index=\"index\">\n <ng-template\n #defaultMessageTemplate\n let-messageInput=\"message\"\n let-isLastSentMessage=\"isLastSentMessage\"\n let-enabledMessageActions=\"enabledMessageActions\"\n let-mode=\"mode\"\n let-isHighlighted=\"isHighlighted\"\n let-scroll$=\"scroll$\"\n >\n <stream-message\n [message]=\"messageInput\"\n [isLastSentMessage]=\"isLastSentMessage\"\n [enabledMessageActions]=\"enabledMessageActions\"\n [mode]=\"mode\"\n [isHighlighted]=\"isHighlighted\"\n [scroll$]=\"scroll$\"\n ></stream-message>\n </ng-template>\n <ng-container\n *ngTemplateOutlet=\"\n messageTemplate || defaultMessageTemplate;\n context: {\n message: message,\n isLastSentMessage: !!(\n lastSentMessageId && message?.id === lastSentMessageId\n ),\n enabledMessageActions: enabledMessageActions,\n mode: mode,\n isHighlighted: message?.id === highlightedMessageId,\n scroll$: scroll$\n }\n \"\n ></ng-container>\n</ng-template>\n\n<ng-template #dateSeparator let-date=\"date\" let-parsedDate=\"parsedDate\">\n <ng-container *ngIf=\"displayDateSeparator\">\n <ng-container\n *ngTemplateOutlet=\"\n customDateSeparatorTemplate || defaultDateSeparator;\n context: {\n date: date,\n parsedDate: parsedDate\n }\n \"\n ></ng-container>\n </ng-container>\n\n <ng-template\n #defaultDateSeparator\n let-date=\"date\"\n let-parsedDate=\"parsedDate\"\n >\n <div data-testid=\"date-separator\" class=\"str-chat__date-separator\">\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'right' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n <div class=\"str-chat__date-separator-date\">\n {{ parsedDate }}\n </div>\n <hr\n *ngIf=\"\n dateSeparatorTextPos === 'left' || dateSeparatorTextPos === 'center'\n \"\n class=\"str-chat__date-separator-line\"\n />\n </div>\n </ng-template>\n</ng-template>\n\n<ng-template #defaultNewMessagesIndicator let-unreadCount=\"unreadCount\">\n <div class=\"str-chat__unread-messages-separator\">\n <ng-container\n *ngIf=\"\n unreadCount > 0 && !hideUnreadCountForNotificationAndIndicator;\n else noUnreadCount\n \"\n >\n {{\n (unreadCount === 1\n ? \"streamChat.\\{\\{count\\}\\} unread message\"\n : \"streamChat.\\{\\{count\\}\\} unread messages\"\n ) | translate: { count: unreadCount }\n }}\n </ng-container>\n <ng-template #noUnreadCount>\n {{ \"streamChat.Unread messages\" | translate }}\n </ng-template>\n </div>\n</ng-template>\n" }]
6972
7235
  }], ctorParameters: function () { return [{ type: ChannelService }, { type: ChatClientService }, { type: CustomTemplatesService }, { type: DateParserService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { mode: [{
6973
7236
  type: Input
6974
7237
  }], direction: [{
@@ -6987,8 +7250,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImpor
6987
7250
  type: Input
6988
7251
  }], displayLoadingIndicator: [{
6989
7252
  type: Input
6990
- }], limitNumberOfMessagesInList: [{
6991
- type: Input
6992
7253
  }], scrollContainer: [{
6993
7254
  type: ViewChild,
6994
7255
  args: ['scrollContainer']
@@ -7235,5 +7496,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImpor
7235
7496
  * Generated bundle index. Do not edit.
7236
7497
  */
7237
7498
 
7238
- export { AttachmentConfigurationService, AttachmentListComponent, AttachmentPreviewListComponent, AttachmentService, AutocompleteTextareaComponent, AvatarComponent, AvatarPlaceholderComponent, ChannelComponent, ChannelHeaderComponent, ChannelListComponent, ChannelPreviewComponent, ChannelQuery, ChannelService, ChatClientService, CustomTemplatesService, DateParserService, EmojiInputService, IconComponent, IconPlaceholderComponent, LoadingIndicatorComponent, LoadingIndicatorPlaceholderComponent, MessageActionsBoxComponent, MessageActionsService, MessageBouncePromptComponent, MessageComponent, MessageInputComponent, MessageInputConfigService, MessageListComponent, MessageReactionsComponent, MessageReactionsSelectorComponent, MessageReactionsService, MessageService, ModalComponent, NotificationComponent, NotificationListComponent, NotificationService, StreamAutocompleteTextareaModule, StreamAvatarModule, StreamChatModule, StreamI18nService, StreamTextareaModule, TextareaComponent, TextareaDirective, ThemeService, ThreadComponent, TransliterationService, VoiceRecordingComponent, VoiceRecordingWavebarComponent, createMessagePreview, getChannelDisplayText, getGroupStyles, getMessageTranslation, getReadBy, isImageAttachment, isImageFile, isOnSeparateDate, listUsers, parseDate, textareaInjectionToken };
7499
+ export { AttachmentConfigurationService, AttachmentListComponent, AttachmentPreviewListComponent, AttachmentService, AutocompleteTextareaComponent, AvatarComponent, AvatarPlaceholderComponent, ChannelComponent, ChannelHeaderComponent, ChannelListComponent, ChannelPreviewComponent, ChannelQuery, ChannelService, ChatClientService, CustomTemplatesService, DateParserService, EmojiInputService, IconComponent, IconPlaceholderComponent, LoadingIndicatorComponent, LoadingIndicatorPlaceholderComponent, MessageActionsBoxComponent, MessageActionsService, MessageBouncePromptComponent, MessageComponent, MessageInputComponent, MessageInputConfigService, MessageListComponent, MessageReactionsComponent, MessageReactionsSelectorComponent, MessageReactionsService, MessageService, ModalComponent, NotificationComponent, NotificationListComponent, NotificationService, StreamAutocompleteTextareaModule, StreamAvatarModule, StreamChatModule, StreamI18nService, StreamTextareaModule, TextareaComponent, TextareaDirective, ThemeService, ThreadComponent, TransliterationService, VirtualizedListService, VirtualizedMessageListService, VoiceRecordingComponent, VoiceRecordingWavebarComponent, createMessagePreview, getChannelDisplayText, getGroupStyles, getMessageTranslation, getReadBy, isImageAttachment, isImageFile, isOnSeparateDate, listUsers, parseDate, textareaInjectionToken };
7239
7500
  //# sourceMappingURL=stream-chat-angular.mjs.map