stream-chat-angular 5.0.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/version.d.ts +1 -1
  2. package/esm2020/assets/version.mjs +2 -2
  3. package/esm2020/lib/avatar/avatar.component.mjs +4 -1
  4. package/esm2020/lib/channel-list/channel-list.component.mjs +2 -2
  5. package/esm2020/lib/channel.service.mjs +10 -26
  6. package/esm2020/lib/chat-client.service.mjs +2 -3
  7. package/esm2020/lib/message-actions.service.mjs +4 -4
  8. package/esm2020/lib/message-list/message-list.component.mjs +181 -249
  9. package/esm2020/lib/types.mjs +1 -1
  10. package/esm2020/lib/virtualized-list.service.mjs +271 -0
  11. package/esm2020/lib/virtualized-message-list.service.mjs +73 -0
  12. package/esm2020/lib/voice-recording/voice-recording.component.mjs +2 -2
  13. package/esm2020/public-api.mjs +3 -1
  14. package/fesm2015/stream-chat-angular.mjs +550 -288
  15. package/fesm2015/stream-chat-angular.mjs.map +1 -1
  16. package/fesm2020/stream-chat-angular.mjs +536 -277
  17. package/fesm2020/stream-chat-angular.mjs.map +1 -1
  18. package/lib/avatar/avatar.component.d.ts +3 -2
  19. package/lib/channel-list/channel-list.component.d.ts +1 -1
  20. package/lib/channel.service.d.ts +6 -12
  21. package/lib/message-list/message-list.component.d.ts +12 -18
  22. package/lib/types.d.ts +7 -0
  23. package/lib/virtualized-list.service.d.ts +58 -0
  24. package/lib/virtualized-message-list.service.d.ts +15 -0
  25. package/package.json +1 -1
  26. package/public-api.d.ts +2 -0
  27. package/src/assets/styles/css/index.css +1 -1
  28. package/src/assets/styles/css/index.layout.css +1 -1
  29. package/src/assets/styles/scss/LoadingIndicator/LoadingIndicator-layout.scss +16 -0
  30. package/src/assets/version.ts +1 -1
@@ -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.1';
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.
@@ -241,9 +241,8 @@ class ChatClientService {
241
241
  { id: { $autocomplete: searchTerm } },
242
242
  { name: { $autocomplete: searchTerm } },
243
243
  ],
244
- id: { $ne: this.chatClient.userID },
245
244
  }); // TODO: find out why we need this typecast
246
- return result.users;
245
+ return result.users.filter((u) => u.id !== this.chatClient?.user?.id);
247
246
  }
248
247
  updatePendingInvites(e) {
249
248
  if (!this.trackPendingChannelInvites) {
@@ -447,6 +446,7 @@ class ChannelService {
447
446
  this.chatClientService = chatClientService;
448
447
  this.ngZone = ngZone;
449
448
  this.notificationService = notificationService;
449
+ this.messagePageSize = 25;
450
450
  this.channelsSubject = new BehaviorSubject(undefined);
451
451
  this.activeChannelSubject = new BehaviorSubject(undefined);
452
452
  this.activeChannelMessagesSubject = new BehaviorSubject([]);
@@ -458,7 +458,6 @@ class ChannelService {
458
458
  this.activeThreadMessagesSubject = new BehaviorSubject([]);
459
459
  this.jumpToMessageSubject = new BehaviorSubject({ id: undefined, parentId: undefined });
460
460
  this.latestMessageDateByUserByChannelsSubject = new BehaviorSubject({});
461
- this.messagePageSize = 25;
462
461
  this.attachmentMaxSizeFallbackInMB = 100;
463
462
  this.messageToQuoteSubject = new BehaviorSubject(undefined);
464
463
  this.usersTypingInChannelSubject = new BehaviorSubject([]);
@@ -567,23 +566,6 @@ class ChannelService {
567
566
  .asObservable()
568
567
  .pipe(shareReplay(1));
569
568
  }
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
569
  /**
588
570
  * 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
571
  */
@@ -1030,9 +1012,8 @@ class ChannelService {
1030
1012
  }
1031
1013
  const result = await activeChannel.queryMembers({
1032
1014
  name: { $autocomplete: searchTerm },
1033
- id: { $ne: this.chatClientService.chatClient.userID },
1034
1015
  }); // TODO: find out why we need typecast here
1035
- return Object.values(result.members);
1016
+ return result.members.filter((m) => m.user_id !== this.chatClientService.chatClient?.user?.id);
1036
1017
  }
1037
1018
  }
1038
1019
  /**
@@ -1466,6 +1447,12 @@ class ChannelService {
1466
1447
  get activeChannelMessages() {
1467
1448
  return this.activeChannelMessagesSubject.getValue() || [];
1468
1449
  }
1450
+ /**
1451
+ * The current thread replies
1452
+ */
1453
+ get activeChannelThreadReplies() {
1454
+ return this.activeThreadMessagesSubject.getValue() || [];
1455
+ }
1469
1456
  /**
1470
1457
  * 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
1458
  * @param messageId
@@ -1547,7 +1534,7 @@ class ChannelService {
1547
1534
  return;
1548
1535
  }
1549
1536
  const messageIndex = messages.findIndex((m) => m.id === event?.message?.id);
1550
- if (messageIndex !== -1) {
1537
+ if (messageIndex !== -1 || event.type === 'message.deleted') {
1551
1538
  isThreadReply
1552
1539
  ? this.activeThreadMessagesSubject.next([...messages])
1553
1540
  : this.activeChannelMessagesSubject.next([...messages]);
@@ -1928,10 +1915,6 @@ class ChannelService {
1928
1915
  }
1929
1916
  }
1930
1917
  }
1931
- /**
1932
- * @internal
1933
- */
1934
- ChannelService.MAX_MESSAGE_COUNT_IN_MESSAGE_LIST = 250;
1935
1918
  /**
1936
1919
  * @internal
1937
1920
  */
@@ -2692,6 +2675,9 @@ class AvatarComponent {
2692
2675
  this.setFallbackChannelImage();
2693
2676
  }
2694
2677
  }
2678
+ ngOnDestroy() {
2679
+ this.subscriptions.forEach((s) => s.unsubscribe());
2680
+ }
2695
2681
  setFallbackChannelImage() {
2696
2682
  if (this.type !== 'channel') {
2697
2683
  this.fallbackChannelImage = undefined;
@@ -3166,7 +3152,7 @@ class MessageActionsService {
3166
3152
  actionHandler: (message) => {
3167
3153
  void this.channelService.markMessageUnread(message.id);
3168
3154
  },
3169
- isVisible: (enabledActions, isMine, message) => enabledActions.indexOf('read-events') !== -1 && !message.parent_id,
3155
+ isVisible: (enabledActions, _, message) => enabledActions.indexOf('read-events') !== -1 && !message.parent_id,
3170
3156
  },
3171
3157
  {
3172
3158
  actionName: 'quote',
@@ -3182,7 +3168,7 @@ class MessageActionsService {
3182
3168
  actionHandler: (message) => {
3183
3169
  void this.channelService.setAsActiveParentMessage(message);
3184
3170
  },
3185
- isVisible: (enabledActions, isMine, message) => enabledActions.indexOf('send-reply') !== -1 && !message.parent_id,
3171
+ isVisible: (enabledActions, _, message) => enabledActions.indexOf('send-reply') !== -1 && !message.parent_id,
3186
3172
  },
3187
3173
  {
3188
3174
  actionName: 'pin',
@@ -3203,7 +3189,7 @@ class MessageActionsService {
3203
3189
  await this.chatClientService.flagMessage(message.id);
3204
3190
  this.notificationService.addTemporaryNotification('streamChat.Message has been successfully flagged', 'success');
3205
3191
  }
3206
- catch (err) {
3192
+ catch (error) {
3207
3193
  this.notificationService.addTemporaryNotification('streamChat.Error adding flag');
3208
3194
  }
3209
3195
  },
@@ -4097,7 +4083,7 @@ class ChannelListComponent {
4097
4083
  await this.channelService.loadMoreChannels();
4098
4084
  this.isLoadingMoreChannels = false;
4099
4085
  }
4100
- trackByChannelId(index, item) {
4086
+ trackByChannelId(_, item) {
4101
4087
  return item.cid;
4102
4088
  }
4103
4089
  }
@@ -4337,7 +4323,7 @@ class VoiceRecordingComponent {
4337
4323
  : this.audioElement.nativeElement.pause();
4338
4324
  this.isError = false;
4339
4325
  }
4340
- catch (e) {
4326
+ catch (error) {
4341
4327
  this.isError = true;
4342
4328
  }
4343
4329
  }
@@ -6271,6 +6257,347 @@ const isOnSameDay = (date1, date2) => {
6271
6257
  date1.getDate() === date2.getDate());
6272
6258
  };
6273
6259
 
6260
+ /**
6261
+ * The `VirtualizedListService` removes items from a list that are not currently displayed. This is a high-level overview of how it works:
6262
+ * - Create a new instance for each list that needs virtualization
6263
+ * - Input: Provide a reactive stream that emits all items in the list
6264
+ * - Input: Provide a reactive stream that emit the current scroll position (top, middle or bottom)
6265
+ * - 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)
6266
+ * - Output: The service will emit the current list of displayed items via the virtualized items reactive stream
6267
+ * - 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:
6268
+ * - If scroll location is bottom/top items around the current bottom/top item will be emitted in the virtualized items stream
6269
+ * - 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)
6270
+ * - 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
6271
+ * - 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
6272
+ * - Input: you should provide the page size to use, in order for the service to determine if loading is necessary
6273
+ *
6274
+ * The `VirtualizedMessageListService` provides an implementation for the message list component.
6275
+ */
6276
+ class VirtualizedListService {
6277
+ constructor(allItems$, scrollPosition$, jumpToItem$, pageSize = 25, maxItemCount = pageSize * 4) {
6278
+ this.allItems$ = allItems$;
6279
+ this.scrollPosition$ = scrollPosition$;
6280
+ this.jumpToItem$ = jumpToItem$;
6281
+ this.pageSize = pageSize;
6282
+ this.maxItemCount = maxItemCount;
6283
+ this.queryStateSubject = new BehaviorSubject({
6284
+ state: 'success',
6285
+ });
6286
+ this.bufferOnTop = 0;
6287
+ this.bufferOnBottom = 0;
6288
+ this.loadFromBuffer$ = new Subject();
6289
+ this.virtualizedItemsSubject = new BehaviorSubject([]);
6290
+ this.subscriptions = [];
6291
+ this.virtualizedItems$ = this.virtualizedItemsSubject.asObservable();
6292
+ this.queryState$ = this.queryStateSubject.asObservable();
6293
+ this.subscriptions.push(this.virtualizedItems$.subscribe((virtaluzedItems) => {
6294
+ this.allItems$.pipe(take$1(1)).subscribe((allItems) => {
6295
+ if (virtaluzedItems.length === allItems.length) {
6296
+ this.bufferOnTop = 0;
6297
+ this.bufferOnBottom = 0;
6298
+ }
6299
+ else if (virtaluzedItems.length === 0) {
6300
+ this.bufferOnTop = allItems.length;
6301
+ this.bufferOnBottom = 0;
6302
+ }
6303
+ else {
6304
+ this.bufferOnTop = allItems.indexOf(virtaluzedItems[0]);
6305
+ this.bufferOnBottom =
6306
+ allItems.length -
6307
+ allItems.indexOf(virtaluzedItems[virtaluzedItems.length - 1]) -
6308
+ 1;
6309
+ }
6310
+ });
6311
+ }));
6312
+ this.subscriptions.push(merge(this.allItems$, this.loadFromBuffer$)
6313
+ .pipe(switchMap(() => {
6314
+ return combineLatest([
6315
+ this.allItems$.pipe(take$1(1)),
6316
+ this.scrollPosition$.pipe(take$1(1)),
6317
+ ]);
6318
+ }))
6319
+ .subscribe(([items, scrollPosition]) => {
6320
+ if (scrollPosition === 'middle') {
6321
+ return;
6322
+ }
6323
+ const currentItems = this.virtualizedItemsSubject.getValue();
6324
+ if (items.length <= this.maxItemCount) {
6325
+ this.virtualizedItemsSubject.next(items);
6326
+ }
6327
+ else {
6328
+ let startIndex = 0;
6329
+ let endIndex = undefined;
6330
+ const numberOfItemsToRemove = items.length - Math.round(this.maxItemCount / 2);
6331
+ const numberOfItemsAfterRemove = items.length - numberOfItemsToRemove;
6332
+ switch (scrollPosition) {
6333
+ case 'top':
6334
+ if (currentItems.length > 0) {
6335
+ const middleIndex = items.findIndex((i) => this.isEqual(i, currentItems[0]));
6336
+ if (middleIndex !== -1) {
6337
+ startIndex = Math.max(0, middleIndex - Math.ceil(numberOfItemsAfterRemove / 2));
6338
+ endIndex = startIndex + numberOfItemsAfterRemove;
6339
+ }
6340
+ }
6341
+ else {
6342
+ endIndex = numberOfItemsAfterRemove;
6343
+ }
6344
+ break;
6345
+ case 'bottom':
6346
+ if (currentItems.length > 0) {
6347
+ const middleIndex = items.findIndex((i) => this.isEqual(i, currentItems[currentItems.length - 1]));
6348
+ if (middleIndex !== -1) {
6349
+ endIndex = Math.min(items.length, middleIndex + Math.floor(numberOfItemsAfterRemove / 2) + 1);
6350
+ startIndex = endIndex - numberOfItemsAfterRemove;
6351
+ }
6352
+ }
6353
+ else {
6354
+ startIndex = items.length - numberOfItemsAfterRemove;
6355
+ }
6356
+ break;
6357
+ }
6358
+ const virtualizedItems = items.slice(startIndex, endIndex);
6359
+ this.virtualizedItemsSubject.next(virtualizedItems);
6360
+ }
6361
+ }));
6362
+ this.subscriptions.push(this.scrollPosition$
6363
+ .pipe(distinctUntilChanged())
6364
+ .subscribe((position) => {
6365
+ if (this.queryStateSubject.getValue().state === `loading-${position}`) {
6366
+ return;
6367
+ }
6368
+ if (position === 'top') {
6369
+ if (this.bufferOnTop < this.pageSize) {
6370
+ void this.loadMore(position);
6371
+ }
6372
+ else {
6373
+ this.loadMoreFromBuffer('top');
6374
+ }
6375
+ }
6376
+ else if (position === 'bottom') {
6377
+ if (this.bufferOnBottom < this.pageSize) {
6378
+ void this.loadMore(position);
6379
+ }
6380
+ else {
6381
+ this.loadMoreFromBuffer('bottom');
6382
+ }
6383
+ }
6384
+ }));
6385
+ this.subscriptions.push(this.allItems$
6386
+ .pipe(pairwise(), filter$1(() => {
6387
+ let scrollPosition;
6388
+ this.scrollPosition$
6389
+ .pipe(take$1(1))
6390
+ .subscribe((s) => (scrollPosition = s));
6391
+ return scrollPosition === 'middle';
6392
+ }))
6393
+ .subscribe(([prevItems, currentItems]) => {
6394
+ if (currentItems.length < this.maxItemCount ||
6395
+ this.virtualizedItems.length === 0) {
6396
+ this.virtualizedItemsSubject.next(currentItems);
6397
+ }
6398
+ else {
6399
+ const currentFirstItem = this.virtualizedItems[0];
6400
+ const currentLastItem = this.virtualizedItems[this.virtualizedItems.length - 1];
6401
+ const prevStartIndex = prevItems.findIndex((i) => this.isEqual(i, currentFirstItem));
6402
+ const prevEndIndex = prevItems.findIndex((i) => this.isEqual(i, currentLastItem));
6403
+ const isStartRemainedSame = currentItems[prevStartIndex]
6404
+ ? this.isEqual(currentItems[prevStartIndex], currentFirstItem)
6405
+ : false;
6406
+ const isEndRemainedSame = currentItems[prevEndIndex]
6407
+ ? this.isEqual(currentItems[prevEndIndex], currentLastItem)
6408
+ : false;
6409
+ const hasNewItemsBottom = prevEndIndex === prevItems.length - 1 && isEndRemainedSame
6410
+ ? prevItems.length !== currentItems.length
6411
+ : false;
6412
+ if (isStartRemainedSame && isEndRemainedSame) {
6413
+ const endIndex = hasNewItemsBottom ? undefined : prevEndIndex + 1;
6414
+ this.virtualizedItemsSubject.next(currentItems.slice(prevStartIndex, endIndex));
6415
+ }
6416
+ let currentStartIndex = isStartRemainedSame ? prevStartIndex : -1;
6417
+ let currentEndIndex = isEndRemainedSame ? prevEndIndex : -1;
6418
+ if (!isStartRemainedSame) {
6419
+ currentStartIndex = currentItems.findIndex((i) => this.isEqual(i, currentFirstItem));
6420
+ }
6421
+ if (!isEndRemainedSame) {
6422
+ currentEndIndex = currentItems.findIndex((i) => this.isEqual(i, currentLastItem));
6423
+ }
6424
+ const hasNewItemsTop = prevStartIndex === 0 && !isStartRemainedSame
6425
+ ? currentStartIndex !== 0
6426
+ : false;
6427
+ if (currentStartIndex !== -1 && currentEndIndex !== -1) {
6428
+ const startIndex = hasNewItemsTop ? 0 : currentStartIndex;
6429
+ this.virtualizedItemsSubject.next(currentItems.slice(startIndex, currentEndIndex + 1));
6430
+ }
6431
+ else {
6432
+ if (currentStartIndex === -1 && currentEndIndex !== -1) {
6433
+ currentStartIndex = Math.max(0, currentEndIndex - (prevEndIndex - prevStartIndex));
6434
+ }
6435
+ if (currentEndIndex === -1 && currentStartIndex !== -1) {
6436
+ currentEndIndex = Math.min(currentItems.length - 1, currentStartIndex + (prevEndIndex - prevStartIndex));
6437
+ }
6438
+ this.virtualizedItemsSubject.next(currentItems.slice(currentStartIndex, currentEndIndex + 1));
6439
+ }
6440
+ }
6441
+ }));
6442
+ if (this.jumpToItem$) {
6443
+ this.subscriptions.push(this.jumpToItem$
6444
+ .pipe(switchMap((jumpToItem) => combineLatest([this.allItems$.pipe(take$1(1)), of(jumpToItem)])))
6445
+ .subscribe(([allItems, jumpToItem]) => {
6446
+ if (jumpToItem.item) {
6447
+ if (allItems.length < this.maxItemCount) {
6448
+ this.virtualizedItemsSubject.next(allItems);
6449
+ }
6450
+ else {
6451
+ const itemIndex = allItems.findIndex((i) =>
6452
+ // @ts-expect-error TODO: do we know a better typing here?
6453
+ this.isEqual(i, jumpToItem.item));
6454
+ if (itemIndex === -1) {
6455
+ return;
6456
+ }
6457
+ else {
6458
+ const position = jumpToItem.position || 'middle';
6459
+ const numberOfItemsToRemove = allItems.length - Math.round(this.maxItemCount / 2);
6460
+ const numberOfItemsAfterRemove = allItems.length - numberOfItemsToRemove;
6461
+ let startIndex = -1;
6462
+ let endIndex = -1;
6463
+ switch (position) {
6464
+ case 'top':
6465
+ startIndex = itemIndex;
6466
+ endIndex = Math.min(allItems.length, startIndex + numberOfItemsAfterRemove);
6467
+ break;
6468
+ case 'bottom':
6469
+ endIndex = itemIndex + 1;
6470
+ startIndex = Math.max(0, endIndex - numberOfItemsAfterRemove);
6471
+ break;
6472
+ case 'middle': {
6473
+ const itemsOnTop = itemIndex;
6474
+ const itemsOnBottom = allItems.length - itemIndex;
6475
+ if (itemsOnTop < Math.ceil(numberOfItemsAfterRemove / 2)) {
6476
+ startIndex = 0;
6477
+ }
6478
+ if (itemsOnBottom <
6479
+ Math.floor(numberOfItemsAfterRemove / 2) + 1) {
6480
+ endIndex = allItems.length;
6481
+ }
6482
+ if (startIndex === -1) {
6483
+ if (endIndex !== -1) {
6484
+ startIndex = endIndex - numberOfItemsAfterRemove;
6485
+ }
6486
+ else {
6487
+ startIndex =
6488
+ itemIndex - Math.ceil(numberOfItemsAfterRemove / 2);
6489
+ }
6490
+ }
6491
+ if (endIndex === -1) {
6492
+ endIndex = startIndex + numberOfItemsAfterRemove;
6493
+ }
6494
+ }
6495
+ }
6496
+ this.virtualizedItemsSubject.next(allItems.slice(startIndex, endIndex));
6497
+ }
6498
+ }
6499
+ }
6500
+ }));
6501
+ }
6502
+ }
6503
+ /**
6504
+ * The current value of virtualized items
6505
+ */
6506
+ get virtualizedItems() {
6507
+ return this.virtualizedItemsSubject.getValue();
6508
+ }
6509
+ /**
6510
+ * Remove all subscriptions, call this once you're done using an instance of this service
6511
+ */
6512
+ dispose() {
6513
+ this.subscriptions.forEach((s) => s.unsubscribe());
6514
+ }
6515
+ loadMoreFromBuffer(_) {
6516
+ this.loadFromBuffer$.next();
6517
+ }
6518
+ async loadMore(direction) {
6519
+ this.queryStateSubject.next({ state: `loading-${direction}` });
6520
+ try {
6521
+ await this.query(direction);
6522
+ this.queryStateSubject.next({ state: 'success' });
6523
+ }
6524
+ catch (e) {
6525
+ this.queryStateSubject.next({ state: 'error', error: e });
6526
+ }
6527
+ }
6528
+ }
6529
+
6530
+ /**
6531
+ * The `VirtualizedMessageListService` removes messages from the message list that are currently not in view
6532
+ */
6533
+ class VirtualizedMessageListService extends VirtualizedListService {
6534
+ constructor(mode, scrollPosition$, channelService) {
6535
+ const jumpToMessage$ = channelService.jumpToMessage$.pipe(map$1((jumpToMessage) => {
6536
+ let result = {
6537
+ item: undefined,
6538
+ };
6539
+ let targetMessageId;
6540
+ if (mode === 'main') {
6541
+ targetMessageId = jumpToMessage.parentId
6542
+ ? jumpToMessage.parentId
6543
+ : jumpToMessage.id;
6544
+ }
6545
+ else {
6546
+ targetMessageId = jumpToMessage.parentId
6547
+ ? jumpToMessage.id
6548
+ : undefined;
6549
+ }
6550
+ if (targetMessageId) {
6551
+ const messages = mode === 'main'
6552
+ ? channelService.activeChannelMessages
6553
+ : channelService.activeChannelThreadReplies;
6554
+ const id = targetMessageId === 'latest'
6555
+ ? messages[messages.length - 1]?.id
6556
+ : targetMessageId;
6557
+ if (id) {
6558
+ result = {
6559
+ item: { id },
6560
+ position: jumpToMessage.id === 'latest' ? 'bottom' : 'middle',
6561
+ };
6562
+ }
6563
+ channelService.clearMessageJump();
6564
+ }
6565
+ return result;
6566
+ }));
6567
+ const messages$ = mode === 'main'
6568
+ ? channelService.activeChannelMessages$
6569
+ : channelService.activeThreadMessages$;
6570
+ super(messages$, scrollPosition$, jumpToMessage$, channelService.messagePageSize);
6571
+ this.mode = mode;
6572
+ this.channelService = channelService;
6573
+ this.isEqual = (t1, t2) => t1.id === t2.id;
6574
+ this.query = (direction) => {
6575
+ const request = this.mode === 'main'
6576
+ ? (direction) => this.channelService.loadMoreMessages(direction)
6577
+ : (direction) => this.channelService.loadMoreThreadReplies(direction);
6578
+ const result = request(direction === 'top' ? 'older' : 'newer');
6579
+ if (result) {
6580
+ return result;
6581
+ }
6582
+ else {
6583
+ this.queryStateSubject.next({ state: 'success' });
6584
+ if ((direction === 'top' && this.bufferOnTop > 0) ||
6585
+ (direction === 'bottom' && this.bufferOnBottom > 0)) {
6586
+ this.loadFromBuffer$.next();
6587
+ }
6588
+ return Promise.resolve();
6589
+ }
6590
+ };
6591
+ }
6592
+ loadMoreFromBuffer(direction) {
6593
+ this.queryStateSubject.next({ state: `loading-${direction}` });
6594
+ setTimeout(() => {
6595
+ this.loadFromBuffer$.next();
6596
+ this.queryStateSubject.next({ state: 'success' });
6597
+ });
6598
+ }
6599
+ }
6600
+
6274
6601
  /**
6275
6602
  * The `MessageList` component renders a scrollable list of messages.
6276
6603
  */
@@ -6321,10 +6648,6 @@ class MessageListComponent {
6321
6648
  * You can turn on and off the loading indicator that signals to users that more messages are being loaded to the message list
6322
6649
  */
6323
6650
  this.displayLoadingIndicator = true;
6324
- /**
6325
- * @internal
6326
- */
6327
- this.limitNumberOfMessagesInList = true;
6328
6651
  this.emptyMainMessageListTemplate = null;
6329
6652
  this.emptyThreadMessageListTemplate = null;
6330
6653
  this.enabledMessageActions = [];
@@ -6332,17 +6655,20 @@ class MessageListComponent {
6332
6655
  this.newMessageCountWhileBeingScrolled = 0;
6333
6656
  this.groupStyles = [];
6334
6657
  this.isNextMessageOnSeparateDate = [];
6335
- this.isLoading = false;
6658
+ this.loadingState = 'idle';
6336
6659
  this.isUnreadNotificationVisible = true;
6337
6660
  this.isJumpingToLatestUnreadMessage = false;
6338
6661
  this.isJumpToLatestButtonVisible = true;
6662
+ this.isJumpingToMessage = false;
6339
6663
  this.scroll$ = new Subject();
6664
+ this.isNewMessageSentByUser = false;
6340
6665
  this.subscriptions = [];
6341
6666
  this.isLatestMessageInList = true;
6342
6667
  this.parsedDates = new Map();
6343
6668
  this.isViewInited = false;
6344
6669
  this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
6345
6670
  this.forceRepaintSubject = new Subject();
6671
+ this.scrollPosition$ = new BehaviorSubject('bottom');
6346
6672
  this.messageNotificationJumpClicked = () => {
6347
6673
  this.jumpToFirstUnreadMessage();
6348
6674
  this.isUnreadNotificationVisible = false;
@@ -6361,7 +6687,6 @@ class MessageListComponent {
6361
6687
  this.forceRepaint();
6362
6688
  }));
6363
6689
  this.subscriptions.push(this.channelService.activeChannel$.subscribe((channel) => {
6364
- this.chatClientService.chatClient?.logger?.('info', `${channel?.cid || 'undefined'} selected`, { tags: `message list ${this.mode}` });
6365
6690
  let isNewChannel = false;
6366
6691
  if (this.channelId !== channel?.id) {
6367
6692
  isNewChannel = true;
@@ -6369,12 +6694,9 @@ class MessageListComponent {
6369
6694
  clearTimeout(this.checkIfUnreadNotificationIsVisibleTimeout);
6370
6695
  }
6371
6696
  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
6697
  this.parsedDates = new Map();
6374
- if (this.messageRemoveTimeout) {
6375
- clearTimeout(this.messageRemoveTimeout);
6376
- }
6377
6698
  this.resetScrollState();
6699
+ this.setMessages$();
6378
6700
  this.channelId = channel?.id;
6379
6701
  if (this.isViewInited) {
6380
6702
  this.cdRef.detectChanges();
@@ -6434,17 +6756,12 @@ class MessageListComponent {
6434
6756
  this.newMessageSubscription?.unsubscribe();
6435
6757
  if (channel) {
6436
6758
  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') {
6759
+ if (!event.message) {
6441
6760
  return;
6442
6761
  }
6443
- this.newMessageReceived({
6444
- id: event.message.id,
6445
- user: event.message.user,
6446
- created_at: new Date(event.message.created_at || ''),
6447
- });
6762
+ else {
6763
+ this.newMessageReceived(event.message);
6764
+ }
6448
6765
  });
6449
6766
  }
6450
6767
  }));
@@ -6454,6 +6771,7 @@ class MessageListComponent {
6454
6771
  message.id !== this.parentMessage.id &&
6455
6772
  this.mode === 'thread') {
6456
6773
  this.resetScrollState();
6774
+ this.setMessages$();
6457
6775
  }
6458
6776
  if (this.parentMessage === message) {
6459
6777
  return;
@@ -6508,41 +6826,6 @@ class MessageListComponent {
6508
6826
  this.cdRef.detectChanges();
6509
6827
  }
6510
6828
  }));
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
6829
  this.subscriptions.push(this.customTemplatesService.emptyMainMessageListPlaceholder$.subscribe((template) => {
6547
6830
  const isChanged = this.emptyMainMessageListTemplate !== template;
6548
6831
  this.emptyMainMessageListTemplate = template || null;
@@ -6561,6 +6844,7 @@ class MessageListComponent {
6561
6844
  }
6562
6845
  ngOnChanges(changes) {
6563
6846
  if (changes.mode || changes.direction) {
6847
+ this.resetScrollState();
6564
6848
  this.setMessages$();
6565
6849
  }
6566
6850
  if (changes.direction) {
@@ -6572,51 +6856,27 @@ class MessageListComponent {
6572
6856
  ngAfterViewInit() {
6573
6857
  this.isViewInited = true;
6574
6858
  this.ngZone.runOutsideAngular(() => {
6575
- this.scrollContainer.nativeElement.addEventListener('scroll', () => this.scrolled());
6859
+ this.scrollContainer?.nativeElement?.addEventListener('scroll', () => this.scrolled());
6576
6860
  });
6577
6861
  }
6578
6862
  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;
6863
+ if (this.isJumpingToMessage) {
6864
+ this.isNewMessageSentByUser = false;
6865
+ this.messageIdToAnchorTo = undefined;
6866
+ this.anchorMessageTopOffset = undefined;
6867
+ return;
6583
6868
  }
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
- }
6869
+ if (this.messageIdToAnchorTo && this.loadingState === 'idle') {
6870
+ this.preserveScrollbarPosition();
6593
6871
  }
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
- }
6872
+ else if ((!this.isUserScrolled &&
6873
+ this.scrollContainer.nativeElement?.scrollHeight >
6874
+ this.scrollContainer?.nativeElement.clientHeight &&
6875
+ this.getScrollPosition() !==
6876
+ (this.direction === 'bottom-to-top' ? 'bottom' : 'top')) ||
6877
+ (this.isUserScrolled && this.isNewMessageSentByUser)) {
6878
+ this.isNewMessageSentByUser = false;
6879
+ this.jumpToLatestMessage();
6620
6880
  }
6621
6881
  }
6622
6882
  ngOnDestroy() {
@@ -6631,19 +6891,23 @@ class MessageListComponent {
6631
6891
  if (this.jumpToLatestButtonVisibilityTimeout) {
6632
6892
  clearTimeout(this.jumpToLatestButtonVisibilityTimeout);
6633
6893
  }
6634
- if (this.messageRemoveTimeout) {
6635
- clearTimeout(this.messageRemoveTimeout);
6636
- }
6637
- this.removeOldMessagesSubscription?.unsubscribe();
6894
+ this.disposeVirtualizedList();
6638
6895
  }
6639
- trackByMessageId(index, item) {
6896
+ trackByMessageId(_, item) {
6640
6897
  return item.id;
6641
6898
  }
6642
- trackByUserId(index, user) {
6899
+ trackByUserId(_, user) {
6643
6900
  return user.id;
6644
6901
  }
6645
6902
  jumpToLatestMessage() {
6646
- void this.channelService.jumpToMessage('latest', this.mode === 'thread' ? this.parentMessage?.id : undefined);
6903
+ if (this.isLatestMessageInList) {
6904
+ this.direction === 'bottom-to-top'
6905
+ ? this.scrollToBottom()
6906
+ : this.scrollToTop();
6907
+ }
6908
+ else {
6909
+ void this.channelService.jumpToMessage('latest', this.mode === 'thread' ? this.parentMessage?.id : undefined);
6910
+ }
6647
6911
  }
6648
6912
  scrollToBottom() {
6649
6913
  this.scrollContainer.nativeElement.scrollTop =
@@ -6666,8 +6930,7 @@ class MessageListComponent {
6666
6930
  return;
6667
6931
  }
6668
6932
  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}` });
6933
+ let scrollPosition = this.getScrollPosition();
6671
6934
  const isUserScrolled = (this.direction === 'bottom-to-top'
6672
6935
  ? scrollPosition !== 'bottom'
6673
6936
  : scrollPosition !== 'top') || !this.isLatestMessageInList;
@@ -6696,30 +6959,33 @@ class MessageListComponent {
6696
6959
  }
6697
6960
  }, 100);
6698
6961
  }
6699
- if (this.shouldLoadMoreMessages(scrollPosition)) {
6962
+ const prevScrollPosition = this.scrollPosition$.getValue();
6963
+ if (this.direction === 'top-to-bottom') {
6964
+ if (scrollPosition === 'top') {
6965
+ scrollPosition = 'bottom';
6966
+ }
6967
+ else if (scrollPosition === 'bottom') {
6968
+ scrollPosition = 'top';
6969
+ }
6970
+ }
6971
+ if (prevScrollPosition !== scrollPosition && !this.isJumpingToMessage) {
6972
+ if (scrollPosition === 'top' || scrollPosition === 'bottom') {
6973
+ this.virtualizedList?.virtualizedItems$
6974
+ .pipe(take(1))
6975
+ .subscribe((items) => {
6976
+ this.messageIdToAnchorTo =
6977
+ scrollPosition === 'top'
6978
+ ? items[0]?.id
6979
+ : items[items.length - 1]?.id;
6980
+ this.anchorMessageTopOffset = document
6981
+ .getElementById(this.messageIdToAnchorTo)
6982
+ ?.getBoundingClientRect()?.top;
6983
+ });
6984
+ }
6700
6985
  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();
6986
+ this.scrollPosition$.next(scrollPosition);
6720
6987
  });
6721
6988
  }
6722
- this.prevScrollTop = this.scrollContainer.nativeElement.scrollTop;
6723
6989
  }
6724
6990
  jumpToFirstUnreadMessage() {
6725
6991
  if (!this.lastReadMessageId) {
@@ -6760,9 +7026,18 @@ class MessageListComponent {
6760
7026
  : this.emptyThreadMessageListTemplate;
6761
7027
  }
6762
7028
  preserveScrollbarPosition() {
6763
- this.scrollContainer.nativeElement.scrollTop =
6764
- (this.prevScrollTop || 0) +
6765
- (this.scrollContainer.nativeElement.scrollHeight - this.containerHeight);
7029
+ if (!this.messageIdToAnchorTo) {
7030
+ return;
7031
+ }
7032
+ const messageToAlignTo = document.getElementById(this.messageIdToAnchorTo);
7033
+ this.messageIdToAnchorTo = undefined;
7034
+ this.scrollContainer.nativeElement.scrollTop +=
7035
+ (messageToAlignTo?.getBoundingClientRect()?.top || 0) -
7036
+ (this.anchorMessageTopOffset || 0);
7037
+ this.anchorMessageTopOffset = undefined;
7038
+ if (this.isSafari) {
7039
+ this.forceRepaintSubject.next();
7040
+ }
6766
7041
  }
6767
7042
  forceRepaint() {
6768
7043
  // Solves the issue of empty screen on Safari when scrolling
@@ -6773,10 +7048,7 @@ class MessageListComponent {
6773
7048
  getScrollPosition() {
6774
7049
  let position = 'middle';
6775
7050
  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))) {
7051
+ (this.parentMessageElement?.nativeElement.clientHeight || 0)) {
6780
7052
  position = 'top';
6781
7053
  }
6782
7054
  else if (Math.ceil(this.scrollContainer.nativeElement.scrollTop) +
@@ -6787,22 +7059,23 @@ class MessageListComponent {
6787
7059
  }
6788
7060
  return position;
6789
7061
  }
6790
- shouldLoadMoreMessages(scrollPosition) {
6791
- return (scrollPosition !== 'middle' &&
6792
- !this.highlightedMessageId &&
6793
- !this.isLoading);
6794
- }
6795
7062
  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;
7063
+ this.disposeVirtualizedList();
7064
+ this.virtualizedList = new VirtualizedMessageListService(this.mode, this.scrollPosition$, this.channelService);
7065
+ this.queryStateSubscription = this.virtualizedList.queryState$.subscribe((queryState) => {
7066
+ let mappedState = 'idle';
7067
+ if (queryState.state.includes('loading')) {
7068
+ mappedState = queryState.state || 'loading-bottom';
7069
+ }
7070
+ if (mappedState !== this.loadingState) {
7071
+ this.loadingState = mappedState;
7072
+ if (this.isViewInited) {
7073
+ this.cdRef.detectChanges();
7074
+ }
6801
7075
  }
7076
+ });
7077
+ this.messages$ = this.virtualizedList.virtualizedItems$.pipe(tap((messages) => {
6802
7078
  if (messages.length === 0) {
6803
- this.chatClientService.chatClient?.logger?.('info', `Empty messages array, reseting scroll state`, {
6804
- tags: `message list ${this.mode}`,
6805
- });
6806
7079
  this.resetScrollState();
6807
7080
  return;
6808
7081
  }
@@ -6810,21 +7083,6 @@ class MessageListComponent {
6810
7083
  // cdRef.detectChanges() isn't enough here, test will fail
6811
7084
  setTimeout(() => (this.isEmpty = false), 0);
6812
7085
  }
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
7086
  }), tap((messages) => {
6829
7087
  if (this.isJumpingToLatestUnreadMessage &&
6830
7088
  !this.firstUnreadMessageId &&
@@ -6837,82 +7095,98 @@ class MessageListComponent {
6837
7095
  .reverse()
6838
7096
  .find((m) => m.user?.id === this.chatClientService.chatClient?.user?.id &&
6839
7097
  m.status !== 'sending')?.id)), tap((messages) => {
7098
+ const latestMessageInList = messages[messages.length - 1];
7099
+ const channel = this.channelService.activeChannel;
7100
+ const messagesFromState = (this.mode === 'main'
7101
+ ? channel?.state.latestMessages
7102
+ : channel?.state.threads[this.parentMessage?.id || '']) || [];
6840
7103
  this.isLatestMessageInList =
6841
- !this.latestMessage ||
6842
- messages.length === 0 ||
6843
- messages[messages.length - 1].id === this.latestMessage.id ||
6844
- this.mode === 'thread';
7104
+ !latestMessageInList ||
7105
+ latestMessageInList.cid !== channel?.cid ||
7106
+ latestMessageInList?.id ===
7107
+ messagesFromState[messagesFromState.length - 1]?.id;
6845
7108
  if (!this.isLatestMessageInList) {
6846
7109
  this.isUserScrolled = true;
6847
7110
  }
6848
- }), map((messages) => this.direction === 'bottom-to-top' ? messages : [...messages].reverse()), tap((messages) => {
7111
+ }), map((messages) => {
7112
+ return this.direction === 'bottom-to-top'
7113
+ ? messages
7114
+ : [...messages].reverse();
7115
+ }), tap((messages) => {
6849
7116
  this.groupStyles = messages.map((m, i) => getGroupStyles(m, messages[i - 1], messages[i + 1], {
6850
7117
  lastReadMessageId: this.lastReadMessageId,
6851
7118
  }));
6852
7119
  this.isNextMessageOnSeparateDate = messages.map((m, i) => this.checkIfOnSeparateDates(m, messages[i + 1]));
6853
7120
  }), 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);
7121
+ if (this.virtualizedList?.jumpToItem$) {
7122
+ this.jumpToItemSubscription = this.virtualizedList.jumpToItem$
7123
+ .pipe(filter((jumpToMessage) => !!jumpToMessage.item?.id))
7124
+ .subscribe((jumpToMessage) => {
7125
+ let messageId = jumpToMessage.item?.id;
7126
+ if (messageId) {
7127
+ if (this.isJumpingToLatestUnreadMessage) {
7128
+ messageId = this.firstUnreadMessageId || messageId;
7129
+ }
7130
+ if (jumpToMessage.position !== 'bottom') {
7131
+ this.highlightedMessageId = messageId;
7132
+ }
7133
+ else if (this.direction === 'top-to-bottom') {
7134
+ jumpToMessage.position = 'top';
7135
+ }
7136
+ this.isJumpingToMessage = true;
7137
+ this.scrollMessageIntoView({
7138
+ messageId: this.firstUnreadMessageId || messageId,
7139
+ position: jumpToMessage.position || 'middle',
7140
+ });
6884
7141
  }
6885
- }
6886
- });
7142
+ });
7143
+ }
6887
7144
  }
6888
7145
  resetScrollState() {
6889
7146
  this.isEmpty = true;
6890
- this.latestMessage = undefined;
6891
- this.hasNewMessages = true;
6892
7147
  this.isUserScrolled = false;
6893
- this.containerHeight = undefined;
6894
- this.olderMassagesLoaded = false;
6895
- this.oldestMessage = undefined;
7148
+ this.messageIdToAnchorTo = undefined;
7149
+ this.anchorMessageTopOffset = undefined;
6896
7150
  this.newMessageCountWhileBeingScrolled = 0;
6897
- this.prevScrollTop = undefined;
6898
- this.isNewMessageSentByUser = undefined;
7151
+ this.isNewMessageSentByUser = false;
6899
7152
  this.isLatestMessageInList = true;
7153
+ this.isJumpingToMessage = false;
7154
+ this.scrollPosition$.next('bottom');
7155
+ this.loadingState = 'idle';
7156
+ }
7157
+ disposeVirtualizedList() {
7158
+ this.virtualizedList?.dispose();
7159
+ this.jumpToItemSubscription?.unsubscribe();
7160
+ this.queryStateSubscription?.unsubscribe();
6900
7161
  }
6901
7162
  get usersTyping$() {
6902
7163
  return this.mode === 'thread'
6903
7164
  ? this.usersTypingInThread$
6904
7165
  : this.usersTypingInChannel$;
6905
7166
  }
6906
- scrollMessageIntoView(messageId, withRetry = true) {
6907
- const element = document.getElementById(messageId);
7167
+ scrollMessageIntoView(options, withRetry = true) {
7168
+ const element = document.getElementById(options.messageId);
6908
7169
  if (!element && withRetry) {
6909
7170
  // 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));
7171
+ setTimeout(() => this.scrollMessageIntoView(options, false));
6911
7172
  }
6912
7173
  else if (element) {
7174
+ const blockMapping = {
7175
+ top: 'start',
7176
+ bottom: 'end',
7177
+ middle: 'center',
7178
+ };
6913
7179
  element.scrollIntoView({
6914
- block: 'center',
7180
+ block: blockMapping[options.position],
6915
7181
  });
7182
+ if (options.position !== 'middle') {
7183
+ options.position === 'bottom'
7184
+ ? this.scrollToBottom()
7185
+ : this.scrollToTop();
7186
+ }
7187
+ setTimeout(() => {
7188
+ this.isJumpingToMessage = false;
7189
+ }, 0);
6916
7190
  setTimeout(() => {
6917
7191
  this.highlightedMessageId = undefined;
6918
7192
  this.firstUnreadMessageId = undefined;
@@ -6921,39 +7195,26 @@ class MessageListComponent {
6921
7195
  }, 1000);
6922
7196
  }
6923
7197
  }
6924
- scrollToLatestMessage(withRetry = true) {
6925
- if (document.getElementById(this.latestMessage.id)) {
6926
- this.direction === 'bottom-to-top'
6927
- ? this.scrollToBottom()
6928
- : this.scrollToTop();
7198
+ newMessageReceived(message) {
7199
+ if ((this.mode === 'main' && message.parent_id) ||
7200
+ (this.mode === 'thread' && message.parent_id !== this.parentMessage?.id)) {
7201
+ return;
6929
7202
  }
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);
7203
+ const isNewMessageSentByCurrentUser = message.user?.id === this.chatClientService.chatClient?.user?.id;
7204
+ let shouldDetectChanges = false;
7205
+ if (!this.isNewMessageSentByUser && isNewMessageSentByCurrentUser) {
7206
+ this.isNewMessageSentByUser = true;
7207
+ shouldDetectChanges = true;
6933
7208
  }
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
- }
7209
+ if (this.isUserScrolled) {
7210
+ this.newMessageCountWhileBeingScrolled++;
7211
+ shouldDetectChanges = true;
7212
+ }
7213
+ if (!this.isNewMessageSentByUser && this.unreadCount !== undefined) {
7214
+ this.unreadCount++;
7215
+ shouldDetectChanges = true;
7216
+ }
7217
+ if (shouldDetectChanges && this.isViewInited) {
6957
7218
  this.cdRef.detectChanges();
6958
7219
  }
6959
7220
  }
@@ -6965,10 +7226,10 @@ class MessageListComponent {
6965
7226
  }
6966
7227
  }
6967
7228
  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 });
7229
+ 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
7230
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImport: i0, type: MessageListComponent, decorators: [{
6970
7231
  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" }]
7232
+ 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
7233
  }], ctorParameters: function () { return [{ type: ChannelService }, { type: ChatClientService }, { type: CustomTemplatesService }, { type: DateParserService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { mode: [{
6973
7234
  type: Input
6974
7235
  }], direction: [{
@@ -6987,8 +7248,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImpor
6987
7248
  type: Input
6988
7249
  }], displayLoadingIndicator: [{
6989
7250
  type: Input
6990
- }], limitNumberOfMessagesInList: [{
6991
- type: Input
6992
7251
  }], scrollContainer: [{
6993
7252
  type: ViewChild,
6994
7253
  args: ['scrollContainer']
@@ -7235,5 +7494,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.4", ngImpor
7235
7494
  * Generated bundle index. Do not edit.
7236
7495
  */
7237
7496
 
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 };
7497
+ 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
7498
  //# sourceMappingURL=stream-chat-angular.mjs.map