@progress/kendo-angular-conversational-ui 24.0.0-develop.11 → 24.0.0-develop.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import { InjectionToken, Input, ViewChild, HostBinding, Inject, Directive, Injec
7
7
  import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons';
8
8
  import * as i1$2 from '@progress/kendo-angular-popup';
9
9
  import { PopupComponent, KENDO_POPUP, PopupService } from '@progress/kendo-angular-popup';
10
- import { isPresent, normalizeKeys, Keys, focusableSelector, guid, getter, isDocumentAvailable, hasObservers, EventsOutsideAngularDirective, closest as closest$1, ResizeSensorComponent, isChanged, processCssValue, ResizeBatchService } from '@progress/kendo-angular-common';
10
+ import { isPresent, normalizeKeys, Keys, focusableSelector, guid, getter, isDocumentAvailable, hasObservers, EventsOutsideAngularDirective, closest as closest$1, ResizeSensorComponent, getLicenseMessage, shouldShowValidationUI, isChanged, processCssValue, WatermarkOverlayComponent, ResizeBatchService } from '@progress/kendo-angular-common';
11
11
  import { DialogContainerService, DialogService, WindowService, WindowContainerService } from '@progress/kendo-angular-dialog';
12
12
  import { NgTemplateOutlet, NgClass } from '@angular/common';
13
13
  import { Subject, Subscription, fromEvent } from 'rxjs';
@@ -215,8 +215,8 @@ const packageMetadata = {
215
215
  productName: 'Kendo UI for Angular',
216
216
  productCode: 'KENDOUIANGULAR',
217
217
  productCodes: ['KENDOUIANGULAR'],
218
- publishDate: 1777399640,
219
- version: '24.0.0-develop.11',
218
+ publishDate: 1777479446,
219
+ version: '24.0.0-develop.13',
220
220
  licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/'
221
221
  };
222
222
 
@@ -2349,6 +2349,86 @@ const defaultModelFields = {
2349
2349
  failedField: 'failed'
2350
2350
  };
2351
2351
 
2352
+ /**
2353
+ * @hidden
2354
+ *
2355
+ * Show the scroll-to-bottom button when the user is this many pixels from the bottom.
2356
+ */
2357
+ const scrollButtonThreshold = 100;
2358
+ /**
2359
+ * @hidden
2360
+ *
2361
+ * Pins a scroll container to a computed target across layout shifts.
2362
+ * Observers extend the idle window until layout has settled; the initial and
2363
+ * final pins bracket the settle window so the viewport lands exactly on target.
2364
+ */
2365
+ const settleScroll = (zone, el, computeTarget, onComplete, idleMs = 120, timeoutMs = 1000) => {
2366
+ const pin = () => {
2367
+ const t = computeTarget();
2368
+ if (t !== null) {
2369
+ el.scrollTop = t;
2370
+ }
2371
+ };
2372
+ pin();
2373
+ zone.runOutsideAngular(() => {
2374
+ let idleTimer = null;
2375
+ let safetyTimer = null;
2376
+ let mutationObserver = null;
2377
+ let resizeObserver = null;
2378
+ let finished = false;
2379
+ const finish = () => {
2380
+ if (finished) {
2381
+ return;
2382
+ }
2383
+ finished = true;
2384
+ if (mutationObserver) {
2385
+ mutationObserver.disconnect();
2386
+ }
2387
+ if (resizeObserver) {
2388
+ resizeObserver.disconnect();
2389
+ }
2390
+ clearTimeout(idleTimer);
2391
+ clearTimeout(safetyTimer);
2392
+ pin();
2393
+ zone.run(() => {
2394
+ onComplete();
2395
+ requestAnimationFrame(pin);
2396
+ });
2397
+ };
2398
+ const resetIdle = () => {
2399
+ clearTimeout(idleTimer);
2400
+ idleTimer = setTimeout(finish, idleMs);
2401
+ };
2402
+ const onChange = () => {
2403
+ pin();
2404
+ resetIdle();
2405
+ };
2406
+ if (typeof MutationObserver !== 'undefined') {
2407
+ mutationObserver = new MutationObserver(onChange);
2408
+ mutationObserver.observe(el, {
2409
+ childList: true,
2410
+ subtree: true,
2411
+ characterData: true,
2412
+ attributes: true,
2413
+ attributeFilter: ['style', 'class', 'hidden']
2414
+ });
2415
+ }
2416
+ if (typeof ResizeObserver !== 'undefined') {
2417
+ resizeObserver = new ResizeObserver(onChange);
2418
+ resizeObserver.observe(el);
2419
+ for (const child of Array.from(el.children)) {
2420
+ resizeObserver.observe(child);
2421
+ }
2422
+ }
2423
+ if (!mutationObserver && !resizeObserver) {
2424
+ finished = true;
2425
+ zone.run(onComplete);
2426
+ return;
2427
+ }
2428
+ safetyTimer = setTimeout(finish, timeoutMs);
2429
+ resetIdle();
2430
+ });
2431
+ };
2352
2432
  /**
2353
2433
  * @hidden
2354
2434
  */
@@ -2514,15 +2594,17 @@ const transformActions = (actions) => {
2514
2594
  * @hidden
2515
2595
  */
2516
2596
  class ChatService {
2597
+ zone;
2517
2598
  authorId;
2518
2599
  messageWidthMode;
2519
- messageToolbarActions = [];
2520
2600
  messageContextMenuActions = [];
2521
2601
  calculatedContextMenuActions = [];
2522
2602
  fileActions = [];
2523
2603
  toggleMessageState = false;
2604
+ layoutChangeInProgress = false;
2524
2605
  reply;
2525
2606
  messages = [];
2607
+ repliedToMessages = [];
2526
2608
  chatElement;
2527
2609
  messageElementsMap = new Map();
2528
2610
  messagesContextMenu;
@@ -2540,6 +2622,7 @@ class ChatService {
2540
2622
  _messageBoxSettings = MESSAGE_BOX_SETTINGS;
2541
2623
  _suggestionsLayout = SUGGESTIONS_LAYOUT_DEFAULT_SETTINGS;
2542
2624
  _quickActionsLayout = SUGGESTIONS_LAYOUT_DEFAULT_SETTINGS;
2625
+ _messageToolbarActions = [];
2543
2626
  _authorMessageSettings;
2544
2627
  _receiverMessageSettings;
2545
2628
  _allowMessageCollapse;
@@ -2557,6 +2640,7 @@ class ChatService {
2557
2640
  authorMessageSettingsChange: new Subject(),
2558
2641
  receiverMessageSettingsChange: new Subject(),
2559
2642
  allowMessageCollapseChange: new Subject(),
2643
+ messageToolbarActionsChange: new Subject(),
2560
2644
  };
2561
2645
  toolbarAction$ = this.subjects.toolbarAction.asObservable();
2562
2646
  contextMenuAction$ = this.subjects.contextMenuAction.asObservable();
@@ -2571,6 +2655,10 @@ class ChatService {
2571
2655
  authorMessageSettingsChange$ = this.subjects.authorMessageSettingsChange.asObservable();
2572
2656
  receiverMessageSettingsChange$ = this.subjects.receiverMessageSettingsChange.asObservable();
2573
2657
  allowMessageCollapseChange$ = this.subjects.allowMessageCollapseChange.asObservable();
2658
+ messageToolbarActionsChange$ = this.subjects.messageToolbarActionsChange.asObservable();
2659
+ constructor(zone) {
2660
+ this.zone = zone;
2661
+ }
2574
2662
  set authorMessageSettings(settings) {
2575
2663
  const previousSettings = this._authorMessageSettings;
2576
2664
  if (JSON.stringify(previousSettings) !== JSON.stringify(settings)) {
@@ -2629,6 +2717,13 @@ class ChatService {
2629
2717
  get quickActionsLayout() {
2630
2718
  return this._quickActionsLayout;
2631
2719
  }
2720
+ set messageToolbarActions(value) {
2721
+ this._messageToolbarActions = value;
2722
+ this.emit('messageToolbarActionsChange', this._messageToolbarActions);
2723
+ }
2724
+ get messageToolbarActions() {
2725
+ return this._messageToolbarActions;
2726
+ }
2632
2727
  set allowMessageCollapse(value) {
2633
2728
  const previousValue = this._allowMessageCollapse;
2634
2729
  if (previousValue !== value) {
@@ -2651,7 +2746,8 @@ class ChatService {
2651
2746
  (this.subjects[subjectKey]).next(value);
2652
2747
  }
2653
2748
  getMessageById(id) {
2654
- return this.messages.find(message => message.id === id);
2749
+ return this.messages.find(message => message.id === id)
2750
+ ?? this.repliedToMessages.find(message => message.id === id);
2655
2751
  }
2656
2752
  registerMessageElement(messageId, elementRef) {
2657
2753
  this.messageElementsMap.set(messageId, elementRef);
@@ -2662,26 +2758,25 @@ class ChatService {
2662
2758
  this.reply = null;
2663
2759
  }
2664
2760
  }
2665
- scrollToMessage(messageId) {
2666
- const elementRef = this.messageElementsMap.get(messageId);
2667
- if (!elementRef?.nativeElement) {
2761
+ scrollToMessage(messageId, behavior = 'smooth') {
2762
+ if (!isDocumentAvailable()) {
2668
2763
  return;
2669
2764
  }
2670
- const scrollContainer = this.chatElement?.element?.nativeElement;
2765
+ const scrollContainer = this.getScrollContainer();
2671
2766
  if (!scrollContainer) {
2672
2767
  return;
2673
2768
  }
2674
- const targetElement = elementRef.nativeElement;
2675
- const pinnedElement = scrollContainer.querySelector('.k-message-pinned');
2676
- const pinnedHeight = pinnedElement?.offsetHeight || 0;
2677
- const containerRect = scrollContainer.getBoundingClientRect();
2678
- const targetRect = targetElement.getBoundingClientRect();
2679
- const targetTop = targetRect.top - containerRect.top + scrollContainer.scrollTop;
2680
- const scrollTo = targetTop - pinnedHeight;
2681
- scrollContainer.scrollTo({
2682
- top: scrollTo,
2683
- behavior: 'smooth'
2684
- });
2769
+ const computeTarget = () => this.computeScrollTarget(messageId);
2770
+ const scrollTop = computeTarget();
2771
+ if (scrollTop === null) {
2772
+ return;
2773
+ }
2774
+ if (behavior === 'auto') {
2775
+ settleScroll(this.zone, scrollContainer, computeTarget, () => { });
2776
+ }
2777
+ else {
2778
+ scrollContainer.scrollTo({ top: scrollTop, behavior });
2779
+ }
2685
2780
  }
2686
2781
  focusActiveMessageElement() {
2687
2782
  if (this.activeMessageElement) {
@@ -2691,6 +2786,25 @@ class ChatService {
2691
2786
  isOwnMessage(message) {
2692
2787
  return this.authorId === message.author.id;
2693
2788
  }
2789
+ getScrollContainer() {
2790
+ return this.chatElement?.element?.nativeElement ?? null;
2791
+ }
2792
+ computeScrollTarget(messageId) {
2793
+ const elementRef = this.messageElementsMap.get(messageId);
2794
+ if (!elementRef?.nativeElement) {
2795
+ return null;
2796
+ }
2797
+ const scrollContainer = this.getScrollContainer();
2798
+ if (!scrollContainer) {
2799
+ return null;
2800
+ }
2801
+ const targetElement = elementRef.nativeElement;
2802
+ const pinnedElement = scrollContainer.querySelector('.k-message-pinned');
2803
+ const pinnedHeight = pinnedElement?.offsetHeight || 0;
2804
+ const containerRect = scrollContainer.getBoundingClientRect();
2805
+ const targetRect = targetElement.getBoundingClientRect();
2806
+ return targetRect.top - containerRect.top + scrollContainer.scrollTop - pinnedHeight;
2807
+ }
2694
2808
  updateComponentSettings(property, settings, defaultSettings) {
2695
2809
  if (settings === true) {
2696
2810
  this[property] = defaultSettings;
@@ -2702,17 +2816,17 @@ class ChatService {
2702
2816
  this[property] = { ...defaultSettings, ...settings };
2703
2817
  }
2704
2818
  }
2705
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2819
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
2706
2820
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatService });
2707
2821
  }
2708
2822
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatService, decorators: [{
2709
2823
  type: Injectable
2710
- }] });
2824
+ }], ctorParameters: () => [{ type: i0.NgZone }] });
2711
2825
 
2712
2826
  // Consider scroll to be at the bottom when within this number of pixels from the container height.
2713
2827
  const maxDelta = 2;
2714
- // Show the scroll button when the user is this number of pixels away from the bottom.
2715
- const scrollButtonThreshold = 100;
2828
+ // Edge detection threshold for endless scroll fire loadMore when within this many pixels.
2829
+ const edgeThreshold = 100;
2716
2830
  /**
2717
2831
  * @hidden
2718
2832
  */
@@ -2722,13 +2836,53 @@ class ScrollAnchorDirective {
2722
2836
  renderer;
2723
2837
  cdr;
2724
2838
  autoScroll = true;
2839
+ autoScrollThreshold = '20%';
2840
+ set endlessMode(value) {
2841
+ const changed = this._endlessMode !== value;
2842
+ this._endlessMode = value;
2843
+ this._nearTopLocked = false;
2844
+ this._nearBottomLocked = false;
2845
+ this.scrolling = false;
2846
+ if (changed && !value && this._initialScrollDone && isDocumentAvailable()) {
2847
+ this.element.nativeElement.scrollTo({ top: 0, behavior: 'auto' });
2848
+ this._subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
2849
+ this.performScroll('smooth');
2850
+ }));
2851
+ }
2852
+ }
2853
+ get endlessMode() {
2854
+ return this._endlessMode;
2855
+ }
2856
+ rangeIsAtEnd = true;
2725
2857
  autoScrollChange = new EventEmitter();
2858
+ nearTop = new EventEmitter();
2859
+ nearBottom = new EventEmitter();
2726
2860
  overflowAnchor = 'none';
2861
+ get scrollBehaviorStyle() {
2862
+ return this._endlessMode ? 'auto' : null;
2863
+ }
2727
2864
  showScrollToBottomButton = false;
2728
2865
  showMessageBoxSeparator = false;
2729
2866
  scrolling = false;
2730
2867
  unsubscribe;
2731
2868
  scrollUpdate = null;
2869
+ _previousScrollHeight = 0;
2870
+ _previousScrollTop = 0;
2871
+ _pendingScrollPreservation = false;
2872
+ _endlessMode = false;
2873
+ _nearTopLocked = false;
2874
+ _nearBottomLocked = false;
2875
+ _scrollingToMessage = false;
2876
+ _initialScrollDone = false;
2877
+ _subs = new Subscription();
2878
+ // Need to ensure that the scroll follows a new streaming message
2879
+ // until the scroll threshold is reached.
2880
+ _streamingFollow = false;
2881
+ _thresholdScrollCap = null;
2882
+ _streamingPrevScrollTop = 0;
2883
+ _streamingPrevHeight = 0;
2884
+ _streamingPrevMsgCount = 0;
2885
+ _mutationObserver = null;
2732
2886
  constructor(element, zone, renderer, cdr) {
2733
2887
  this.element = element;
2734
2888
  this.zone = zone;
@@ -2736,15 +2890,54 @@ class ScrollAnchorDirective {
2736
2890
  this.cdr = cdr;
2737
2891
  }
2738
2892
  ngOnInit() {
2893
+ if (!isDocumentAvailable()) {
2894
+ return;
2895
+ }
2739
2896
  this.zone.runOutsideAngular(() => {
2740
- this.unsubscribe = this.renderer.listen(this.element.nativeElement, 'scroll', () => this.setupScrollUpdate(), { passive: true });
2897
+ const el = this.element.nativeElement;
2898
+ this.unsubscribe = this.renderer.listen(el, 'scroll', () => {
2899
+ this.detectStreamingScrollUp();
2900
+ this.setupScrollUpdate();
2901
+ }, { passive: true });
2902
+ this._mutationObserver = new MutationObserver(() => {
2903
+ if (!this._streamingFollow) {
2904
+ return;
2905
+ }
2906
+ const currentMsgCount = el.querySelectorAll('.k-message').length;
2907
+ const currentHeight = el.scrollHeight;
2908
+ if (currentMsgCount === this._streamingPrevMsgCount && currentHeight > this._streamingPrevHeight) {
2909
+ const bottom = currentHeight - el.clientHeight;
2910
+ const cap = this._thresholdScrollCap;
2911
+ const target = cap !== null ? Math.min(bottom, cap) : bottom;
2912
+ if (target > el.scrollTop) {
2913
+ el.scrollTop = target;
2914
+ }
2915
+ this._streamingPrevScrollTop = el.scrollTop;
2916
+ this.updateScrollToBottomButton(el);
2917
+ }
2918
+ this._streamingPrevMsgCount = currentMsgCount;
2919
+ this._streamingPrevHeight = currentHeight;
2920
+ });
2921
+ this._mutationObserver.observe(el, {
2922
+ childList: true,
2923
+ subtree: true,
2924
+ characterData: true,
2925
+ });
2741
2926
  });
2742
2927
  }
2743
2928
  ngAfterViewInit() {
2744
- this.autoScrollToBottom();
2745
- this.zone.onStable.pipe(take(1)).subscribe(() => {
2929
+ this._subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
2930
+ if (this.autoScroll) {
2931
+ if (this._endlessMode) {
2932
+ this.scrollToBottomWithSettle();
2933
+ }
2934
+ else {
2935
+ this.performScroll('smooth');
2936
+ }
2937
+ }
2746
2938
  this.calculateMessageBoxSeparator();
2747
- });
2939
+ this._initialScrollDone = true;
2940
+ }));
2748
2941
  }
2749
2942
  ngOnDestroy() {
2750
2943
  if (this.unsubscribe) {
@@ -2753,8 +2946,16 @@ class ScrollAnchorDirective {
2753
2946
  if (this.scrollUpdate !== null) {
2754
2947
  cancelAnimationFrame(this.scrollUpdate);
2755
2948
  }
2949
+ if (this._mutationObserver) {
2950
+ this._mutationObserver.disconnect();
2951
+ this._mutationObserver = null;
2952
+ }
2953
+ this._subs.unsubscribe();
2756
2954
  }
2757
2955
  onScroll() {
2956
+ if (!isDocumentAvailable() || this._scrollingToMessage) {
2957
+ return;
2958
+ }
2758
2959
  const el = this.element.nativeElement;
2759
2960
  const scrollPosition = el.scrollHeight - el.scrollTop - el.clientHeight;
2760
2961
  const atBottom = scrollPosition < maxDelta;
@@ -2764,7 +2965,8 @@ class ScrollAnchorDirective {
2764
2965
  }
2765
2966
  return;
2766
2967
  }
2767
- const showScrollButton = scrollPosition > scrollButtonThreshold;
2968
+ const showScrollButton = scrollPosition > scrollButtonThreshold
2969
+ || (this.endlessMode && !this.rangeIsAtEnd);
2768
2970
  const autoScrollChanged = this.autoScroll !== atBottom;
2769
2971
  const buttonChanged = this.showScrollToBottomButton !== showScrollButton;
2770
2972
  if (autoScrollChanged || buttonChanged) {
@@ -2779,8 +2981,30 @@ class ScrollAnchorDirective {
2779
2981
  }
2780
2982
  });
2781
2983
  }
2984
+ if (this.endlessMode) {
2985
+ const nearTopEdge = el.scrollTop < edgeThreshold;
2986
+ const nearBottomEdge = !this.rangeIsAtEnd && scrollPosition < edgeThreshold;
2987
+ if (!nearTopEdge) {
2988
+ this._nearTopLocked = false;
2989
+ }
2990
+ if (!nearBottomEdge) {
2991
+ this._nearBottomLocked = false;
2992
+ }
2993
+ if (nearTopEdge && !this._nearTopLocked) {
2994
+ this._nearTopLocked = true;
2995
+ this.zone.run(() => this.nearTop.emit());
2996
+ }
2997
+ if (nearBottomEdge && !this._nearBottomLocked) {
2998
+ this._nearBottomLocked = true;
2999
+ this.zone.run(() => this.nearBottom.emit());
3000
+ }
3001
+ }
2782
3002
  }
2783
3003
  autoScrollToBottom() {
3004
+ if (this._streamingFollow) {
3005
+ this.followStreamingContent();
3006
+ return;
3007
+ }
2784
3008
  if (!this.autoScroll) {
2785
3009
  return;
2786
3010
  }
@@ -2789,9 +3013,80 @@ class ScrollAnchorDirective {
2789
3013
  scrollToBottom() {
2790
3014
  this.autoScroll = true;
2791
3015
  this.showScrollToBottomButton = false;
3016
+ this._streamingFollow = false;
3017
+ this._thresholdScrollCap = null;
2792
3018
  this.performScroll();
2793
3019
  }
3020
+ scrollToBottomWithSettle(onComplete) {
3021
+ if (!isDocumentAvailable()) {
3022
+ onComplete?.();
3023
+ return;
3024
+ }
3025
+ const el = this.element.nativeElement;
3026
+ this._scrollingToMessage = true;
3027
+ this._streamingFollow = false;
3028
+ this._thresholdScrollCap = null;
3029
+ settleScroll(this.zone, el, () => el.scrollHeight - el.clientHeight, () => {
3030
+ this.autoScroll = true;
3031
+ this.showScrollToBottomButton = false;
3032
+ this.scrolling = false;
3033
+ this._scrollingToMessage = false;
3034
+ this.cdr.markForCheck();
3035
+ onComplete?.();
3036
+ });
3037
+ }
3038
+ scrollWithThreshold(messageEl) {
3039
+ if (!isDocumentAvailable() || !messageEl) {
3040
+ this.performScroll();
3041
+ return;
3042
+ }
3043
+ const container = this.element.nativeElement;
3044
+ const pinnedHeight = this.getPinnedBarHeight();
3045
+ const thresholdPx = Math.max(0, this.parseThreshold(pinnedHeight));
3046
+ const containerRect = container.getBoundingClientRect();
3047
+ const messageRect = messageEl.getBoundingClientRect();
3048
+ const msgTopInContent = messageRect.top - containerRect.top + container.scrollTop;
3049
+ const targetScrollTop = Math.max(0, msgTopInContent - thresholdPx - pinnedHeight);
3050
+ const bottomScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
3051
+ const finalScrollTop = Math.min(targetScrollTop, bottomScrollTop);
3052
+ if (finalScrollTop > container.scrollTop) {
3053
+ container.scrollTo({ top: finalScrollTop, behavior: 'smooth' });
3054
+ }
3055
+ this._thresholdScrollCap = targetScrollTop;
3056
+ this._streamingFollow = true;
3057
+ this._streamingPrevScrollTop = container.scrollTop;
3058
+ this._streamingPrevHeight = container.scrollHeight;
3059
+ this._streamingPrevMsgCount = container.querySelectorAll('.k-message').length;
3060
+ const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
3061
+ this.showScrollToBottomButton = distanceFromBottom > scrollButtonThreshold;
3062
+ this.cdr.markForCheck();
3063
+ }
3064
+ recordScrollHeight() {
3065
+ if (!isDocumentAvailable()) {
3066
+ return;
3067
+ }
3068
+ const el = this.element.nativeElement;
3069
+ this._previousScrollHeight = el.scrollHeight;
3070
+ this._previousScrollTop = el.scrollTop;
3071
+ this._pendingScrollPreservation = true;
3072
+ this.cdr.markForCheck();
3073
+ }
3074
+ preserveScrollPosition() {
3075
+ if (!this._pendingScrollPreservation || !isDocumentAvailable()) {
3076
+ return;
3077
+ }
3078
+ const el = this.element.nativeElement;
3079
+ const delta = el.scrollHeight - this._previousScrollHeight;
3080
+ if (delta > 0) {
3081
+ el.scrollTop = this._previousScrollTop + delta;
3082
+ }
3083
+ this._pendingScrollPreservation = false;
3084
+ this.cdr.markForCheck();
3085
+ }
2794
3086
  calculateMessageBoxSeparator() {
3087
+ if (!isDocumentAvailable()) {
3088
+ return;
3089
+ }
2795
3090
  const el = this.element.nativeElement;
2796
3091
  const shouldShow = el.scrollHeight > el.clientHeight;
2797
3092
  if (this.showMessageBoxSeparator !== shouldShow) {
@@ -2799,6 +3094,90 @@ class ScrollAnchorDirective {
2799
3094
  this.cdr.markForCheck();
2800
3095
  }
2801
3096
  }
3097
+ getDistanceFromBottom() {
3098
+ if (!isDocumentAvailable()) {
3099
+ return 0;
3100
+ }
3101
+ const el = this.element.nativeElement;
3102
+ return el.scrollHeight - el.scrollTop - el.clientHeight;
3103
+ }
3104
+ setAriaLive(value) {
3105
+ if (!isDocumentAvailable()) {
3106
+ return;
3107
+ }
3108
+ this.renderer.setAttribute(this.element.nativeElement, 'aria-live', value);
3109
+ }
3110
+ lockForMessageScroll() {
3111
+ this._scrollingToMessage = true;
3112
+ }
3113
+ unlockForMessageScroll() {
3114
+ this._scrollingToMessage = false;
3115
+ this.setupScrollUpdate();
3116
+ }
3117
+ getAutoScrollThresholdPx() {
3118
+ return Math.max(0, this.parseThreshold(this.getPinnedBarHeight()));
3119
+ }
3120
+ get isFollowingThreshold() {
3121
+ return this._streamingFollow;
3122
+ }
3123
+ followStreamingContent() {
3124
+ if (!isDocumentAvailable()) {
3125
+ return;
3126
+ }
3127
+ const el = this.element.nativeElement;
3128
+ const bottom = el.scrollHeight - el.clientHeight;
3129
+ const cap = this._thresholdScrollCap;
3130
+ const target = cap !== null ? Math.min(bottom, cap) : bottom;
3131
+ if (target > el.scrollTop) {
3132
+ el.scrollTop = target;
3133
+ this._streamingPrevScrollTop = el.scrollTop;
3134
+ }
3135
+ this.updateScrollToBottomButton(el);
3136
+ }
3137
+ updateScrollToBottomButton(el) {
3138
+ const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
3139
+ const shouldShow = distance > scrollButtonThreshold
3140
+ || (this.endlessMode && !this.rangeIsAtEnd);
3141
+ if (this.showScrollToBottomButton !== shouldShow) {
3142
+ this.zone.run(() => {
3143
+ this.showScrollToBottomButton = shouldShow;
3144
+ this.cdr.markForCheck();
3145
+ });
3146
+ }
3147
+ }
3148
+ detectStreamingScrollUp() {
3149
+ if (!this._streamingFollow) {
3150
+ return;
3151
+ }
3152
+ const st = this.element.nativeElement.scrollTop;
3153
+ if (st < this._streamingPrevScrollTop) {
3154
+ this._streamingFollow = false;
3155
+ this._thresholdScrollCap = null;
3156
+ }
3157
+ this._streamingPrevScrollTop = st;
3158
+ }
3159
+ parseThreshold(pinnedHeight = 0) {
3160
+ const threshold = this.autoScrollThreshold;
3161
+ if (typeof threshold === 'number') {
3162
+ return threshold;
3163
+ }
3164
+ if (typeof threshold === 'string') {
3165
+ if (threshold.endsWith('%')) {
3166
+ const percent = parseFloat(threshold) / 100;
3167
+ const visibleHeight = this.element.nativeElement.clientHeight - pinnedHeight;
3168
+ return visibleHeight * percent;
3169
+ }
3170
+ const parsed = parseFloat(threshold);
3171
+ if (!isNaN(parsed)) {
3172
+ return parsed;
3173
+ }
3174
+ }
3175
+ return 0;
3176
+ }
3177
+ getPinnedBarHeight() {
3178
+ const pinned = this.element.nativeElement.querySelector('.k-message-pinned');
3179
+ return pinned ? pinned.offsetHeight : 0;
3180
+ }
2802
3181
  setupScrollUpdate() {
2803
3182
  if (this.scrollUpdate !== null) {
2804
3183
  return;
@@ -2808,13 +3187,20 @@ class ScrollAnchorDirective {
2808
3187
  this.onScroll();
2809
3188
  });
2810
3189
  }
2811
- performScroll() {
3190
+ performScroll(behavior = 'auto') {
3191
+ if (!isDocumentAvailable()) {
3192
+ return;
3193
+ }
2812
3194
  const el = this.element.nativeElement;
2813
- el.scrollTop = el.scrollHeight - el.clientHeight;
3195
+ const target = el.scrollHeight - el.clientHeight;
3196
+ if (Math.abs(el.scrollTop - target) < maxDelta) {
3197
+ return;
3198
+ }
3199
+ el.scrollTo({ top: target, behavior });
2814
3200
  this.scrolling = true;
2815
3201
  }
2816
3202
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ScrollAnchorDirective, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
2817
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.20", type: ScrollAnchorDirective, isStandalone: true, selector: "[kendoChatScrollAnchor]", inputs: { autoScroll: "autoScroll" }, outputs: { autoScrollChange: "autoScrollChange" }, host: { properties: { "style.overflow-anchor": "this.overflowAnchor" } }, exportAs: ["scrollAnchor"], ngImport: i0 });
3203
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.20", type: ScrollAnchorDirective, isStandalone: true, selector: "[kendoChatScrollAnchor]", inputs: { autoScroll: "autoScroll", autoScrollThreshold: "autoScrollThreshold", endlessMode: "endlessMode", rangeIsAtEnd: "rangeIsAtEnd" }, outputs: { autoScrollChange: "autoScrollChange", nearTop: "nearTop", nearBottom: "nearBottom" }, host: { properties: { "style.overflow-anchor": "this.overflowAnchor", "style.scroll-behavior": "this.scrollBehaviorStyle" } }, exportAs: ["scrollAnchor"], ngImport: i0 });
2818
3204
  }
2819
3205
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ScrollAnchorDirective, decorators: [{
2820
3206
  type: Directive,
@@ -2825,11 +3211,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2825
3211
  }]
2826
3212
  }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }], propDecorators: { autoScroll: [{
2827
3213
  type: Input
3214
+ }], autoScrollThreshold: [{
3215
+ type: Input
3216
+ }], endlessMode: [{
3217
+ type: Input
3218
+ }], rangeIsAtEnd: [{
3219
+ type: Input
2828
3220
  }], autoScrollChange: [{
2829
3221
  type: Output
3222
+ }], nearTop: [{
3223
+ type: Output
3224
+ }], nearBottom: [{
3225
+ type: Output
2830
3226
  }], overflowAnchor: [{
2831
3227
  type: HostBinding,
2832
3228
  args: ['style.overflow-anchor']
3229
+ }], scrollBehaviorStyle: [{
3230
+ type: HostBinding,
3231
+ args: ['style.scroll-behavior']
2833
3232
  }] } });
2834
3233
 
2835
3234
  const DEFAULT_SCROLL_BEHAVIOR = 'smooth';
@@ -2933,6 +3332,93 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2933
3332
  type: Injectable
2934
3333
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i1.LocalizationService }] });
2935
3334
 
3335
+ /**
3336
+ * @hidden
3337
+ */
3338
+ class EndlessScrollState {
3339
+ startIndex = 0;
3340
+ endIndex = 0;
3341
+ isLoading = false;
3342
+ get isAtStart() {
3343
+ return this.startIndex === 0;
3344
+ }
3345
+ get isAtEnd() {
3346
+ return this.endIndex >= this._total;
3347
+ }
3348
+ _total = 0;
3349
+ _pageSize = 50;
3350
+ get pageSize() {
3351
+ return this._pageSize;
3352
+ }
3353
+ init(total, pageSize) {
3354
+ this._total = total;
3355
+ this._pageSize = pageSize;
3356
+ return this.jumpToEnd();
3357
+ }
3358
+ extendUp() {
3359
+ if (this.isAtStart || this.isLoading) {
3360
+ return null;
3361
+ }
3362
+ const newStart = Math.max(0, this.startIndex - this._pageSize);
3363
+ if (newStart === this.startIndex) {
3364
+ return null;
3365
+ }
3366
+ this.startIndex = newStart;
3367
+ return { start: this.startIndex, end: this.endIndex };
3368
+ }
3369
+ extendDown() {
3370
+ if (this.isAtEnd || this.isLoading) {
3371
+ return null;
3372
+ }
3373
+ const newEnd = Math.min(this._total, this.endIndex + this._pageSize);
3374
+ if (newEnd === this.endIndex) {
3375
+ return null;
3376
+ }
3377
+ this.endIndex = newEnd;
3378
+ return { start: this.startIndex, end: this.endIndex };
3379
+ }
3380
+ jumpTo(targetIndex) {
3381
+ const half = Math.floor(this._pageSize / 2);
3382
+ let start = targetIndex - half;
3383
+ let end = start + this._pageSize;
3384
+ if (start < 0) {
3385
+ start = 0;
3386
+ end = Math.min(this._pageSize, this._total);
3387
+ }
3388
+ if (end > this._total) {
3389
+ end = this._total;
3390
+ start = Math.max(0, end - this._pageSize);
3391
+ }
3392
+ this.startIndex = start;
3393
+ this.endIndex = end;
3394
+ return { start, end };
3395
+ }
3396
+ jumpToEnd() {
3397
+ const start = Math.max(0, this._total - this._pageSize);
3398
+ const end = this._total;
3399
+ this.startIndex = start;
3400
+ this.endIndex = end;
3401
+ return { start, end };
3402
+ }
3403
+ updateTotal(total) {
3404
+ this._total = total;
3405
+ }
3406
+ reset() {
3407
+ this.startIndex = 0;
3408
+ this.endIndex = 0;
3409
+ this.isLoading = false;
3410
+ this._total = 0;
3411
+ }
3412
+ contains(index) {
3413
+ return index >= this.startIndex && index < this.endIndex;
3414
+ }
3415
+ syncFromInputs(start, end, total) {
3416
+ this.startIndex = start;
3417
+ this.endIndex = end;
3418
+ this._total = total;
3419
+ }
3420
+ }
3421
+
2936
3422
  /**
2937
3423
  * @hidden
2938
3424
  */
@@ -3113,7 +3599,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3113
3599
  * ```html
3114
3600
  * <kendo-promptbox>
3115
3601
  * <kendo-promptbox-end-affix>
3116
- * <button kendoButton look="clear" icon="image"></button>
3602
+ * <button kendoButton fillMode="clear" [svgIcon]="imageIcon"></button>
3117
3603
  * </kendo-promptbox-end-affix>
3118
3604
  * </kendo-promptbox>
3119
3605
  * ```
@@ -3146,7 +3632,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3146
3632
  * ```html
3147
3633
  * <kendo-promptbox>
3148
3634
  * <kendo-promptbox-start-affix>
3149
- * <button kendoButton look="clear" icon="image"></button>
3635
+ * <button kendoButton fillMode="clear" [svgIcon]="imageIcon"></button>
3150
3636
  * </kendo-promptbox-start-affix>
3151
3637
  * </kendo-promptbox>
3152
3638
  * ```
@@ -3179,7 +3665,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3179
3665
  * ```html
3180
3666
  * <kendo-promptbox>
3181
3667
  * <kendo-promptbox-top-affix>
3182
- * <button kendoButton look="clear" icon="image"></button>
3668
+ * <button kendoButton fillMode="clear" [svgIcon]="imageIcon"></button>
3183
3669
  * </kendo-promptbox-top-affix>
3184
3670
  * </kendo-promptbox>
3185
3671
  * ```
@@ -5538,7 +6024,7 @@ const groupMessages = (acc, msg, isLastMessage) => {
5538
6024
  messages: [msg],
5539
6025
  author: msg.author,
5540
6026
  timestamp: msg.timestamp,
5541
- trackBy: msg
6027
+ trackBy: msg.id
5542
6028
  });
5543
6029
  }
5544
6030
  };
@@ -6763,8 +7249,16 @@ class MessageComponent extends ChatItem {
6763
7249
  chatService;
6764
7250
  localization;
6765
7251
  cdr;
7252
+ zone;
6766
7253
  set message(value) {
7254
+ const textChanged = this._message && value && this._message.text !== value.text;
6767
7255
  this._message = value;
7256
+ if (textChanged) {
7257
+ this.parts = this.getFormattedTextParts(value.text);
7258
+ if (this.isMessageExpandable && !this.showExpandCollapseIcon) {
7259
+ this.updateExpandCollapseIconAfterStable();
7260
+ }
7261
+ }
6768
7262
  }
6769
7263
  get message() {
6770
7264
  return this._message;
@@ -6856,13 +7350,14 @@ class MessageComponent extends ChatItem {
6856
7350
  }
6857
7351
  subs = new Subscription();
6858
7352
  _message;
6859
- constructor(element, intl, chatService, localization, cdr) {
7353
+ constructor(element, intl, chatService, localization, cdr, zone) {
6860
7354
  super();
6861
7355
  this.element = element;
6862
7356
  this.intl = intl;
6863
7357
  this.chatService = chatService;
6864
7358
  this.localization = localization;
6865
7359
  this.cdr = cdr;
7360
+ this.zone = zone;
6866
7361
  }
6867
7362
  ngOnInit() {
6868
7363
  this.fileActions = this.getFileActions();
@@ -6873,14 +7368,20 @@ class MessageComponent extends ChatItem {
6873
7368
  this.subs.add(settingsChange$.subscribe(() => {
6874
7369
  this.fileActions = this.getFileActions();
6875
7370
  this.toolbarActions = this.getToolbarActions();
6876
- setTimeout(() => {
6877
- this.showExpandCollapseIcon = this.calculateExpandCollapseIconVisibility();
6878
- });
7371
+ if (this.isMessageExpandable) {
7372
+ this.updateExpandCollapseIconAfterStable();
7373
+ }
6879
7374
  }));
6880
7375
  this.subs.add(this.chatService.allowMessageCollapseChange$.subscribe(() => {
6881
- setTimeout(() => {
6882
- this.showExpandCollapseIcon = this.calculateExpandCollapseIconVisibility();
6883
- });
7376
+ if (this.isMessageExpandable) {
7377
+ this.updateExpandCollapseIconAfterStable();
7378
+ }
7379
+ else {
7380
+ this.showExpandCollapseIcon = false;
7381
+ }
7382
+ }));
7383
+ this.subs.add(this.chatService.messageToolbarActionsChange$.subscribe(() => {
7384
+ this.toolbarActions = this.getToolbarActions();
6884
7385
  }));
6885
7386
  if (this.message.id) {
6886
7387
  this.chatService.registerMessageElement(this.message.id, this.element);
@@ -6888,8 +7389,9 @@ class MessageComponent extends ChatItem {
6888
7389
  this.parts = this.getFormattedTextParts(this.message.text);
6889
7390
  }
6890
7391
  ngAfterViewInit() {
6891
- this.showExpandCollapseIcon = this.calculateExpandCollapseIconVisibility();
6892
- this.cdr.detectChanges();
7392
+ if (this.isMessageExpandable) {
7393
+ this.updateExpandCollapseIconAfterStable();
7394
+ }
6893
7395
  }
6894
7396
  ngOnDestroy() {
6895
7397
  if (this.message.id) {
@@ -6960,6 +7462,7 @@ class MessageComponent extends ChatItem {
6960
7462
  event.stopImmediatePropagation();
6961
7463
  this.isMessageExpanded = !this.isMessageExpanded;
6962
7464
  this.chatService.toggleMessageState = false;
7465
+ this.cdr.detectChanges();
6963
7466
  }
6964
7467
  onExpandableKeydown(event) {
6965
7468
  const key = normalizeKeys(event);
@@ -7074,7 +7577,13 @@ class MessageComponent extends ChatItem {
7074
7577
  : this.chatService.fileActions || [];
7075
7578
  return transformActions(actions);
7076
7579
  }
7077
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MessageComponent, deps: [{ token: i0.ElementRef }, { token: i1$1.IntlService }, { token: ChatService }, { token: i1.LocalizationService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
7580
+ updateExpandCollapseIconAfterStable() {
7581
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
7582
+ this.showExpandCollapseIcon = this.calculateExpandCollapseIconVisibility();
7583
+ this.cdr.detectChanges();
7584
+ }));
7585
+ }
7586
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MessageComponent, deps: [{ token: i0.ElementRef }, { token: i1$1.IntlService }, { token: ChatService }, { token: i1.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
7078
7587
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: MessageComponent, isStandalone: true, selector: "kendo-chat-message", inputs: { message: "message", tabbable: "tabbable", authorMessageContentTemplate: "authorMessageContentTemplate", receiverMessageContentTemplate: "receiverMessageContentTemplate", messageContentTemplate: "messageContentTemplate", authorMessageTemplate: "authorMessageTemplate", receiverMessageTemplate: "receiverMessageTemplate", messageTemplate: "messageTemplate", statusTemplate: "statusTemplate", showMessageTime: "showMessageTime", authorId: "authorId" }, host: { listeners: { "keydown": "onKeyDown($event)" }, properties: { "class.k-message": "this.cssClass", "class.k-message-failed": "this.failedClass", "class.k-message-removed": "this.removedClass", "attr.tabIndex": "this.tabIndex" } }, providers: [
7079
7588
  {
7080
7589
  provide: ChatItem,
@@ -7159,7 +7668,7 @@ class MessageComponent extends ChatItem {
7159
7668
  </span>
7160
7669
  } @if (!message.isDeleted && parts.length > 0) {
7161
7670
  <span class="k-chat-bubble-text">
7162
- @for (part of parts; track part) { @if (part.type === 'text') {{{part.content}}} @if (part.type ===
7671
+ @for (part of parts; track $index) { @if (part.type === 'text') {{{part.content}}} @if (part.type ===
7163
7672
  'link') {<a [href]="part.href" target="_blank">{{ part.content }}</a
7164
7673
  >} }
7165
7674
  </span>
@@ -7172,7 +7681,7 @@ class MessageComponent extends ChatItem {
7172
7681
  'k-files-vertical': chatService.messageFilesLayout === 'vertical'
7173
7682
  }"
7174
7683
  >
7175
- @for (file of message.files; track file) {
7684
+ @for (file of message.files; track file.id) {
7176
7685
  <li
7177
7686
  class="k-file-box"
7178
7687
  [chatFile]="file"
@@ -7186,7 +7695,7 @@ class MessageComponent extends ChatItem {
7186
7695
  } @else {
7187
7696
  <ul class="k-file-box-wrapper">
7188
7697
  <div class="k-files-scroll">
7189
- @for (file of message.files; track file) {
7698
+ @for (file of message.files; track file.id) {
7190
7699
  <li
7191
7700
  class="k-file-box"
7192
7701
  [chatFile]="file"
@@ -7272,7 +7781,7 @@ class MessageComponent extends ChatItem {
7272
7781
  </div>
7273
7782
  } } @if (showToolbar) {
7274
7783
  <kendo-toolbar class="k-chat-message-toolbar" fillMode="flat">
7275
- @for (action of toolbarActions; track action) {
7784
+ @for (action of toolbarActions; track action.id) {
7276
7785
  <kendo-toolbar-button
7277
7786
  fillMode="flat"
7278
7787
  [icon]="action.icon"
@@ -7376,7 +7885,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
7376
7885
  </span>
7377
7886
  } @if (!message.isDeleted && parts.length > 0) {
7378
7887
  <span class="k-chat-bubble-text">
7379
- @for (part of parts; track part) { @if (part.type === 'text') {{{part.content}}} @if (part.type ===
7888
+ @for (part of parts; track $index) { @if (part.type === 'text') {{{part.content}}} @if (part.type ===
7380
7889
  'link') {<a [href]="part.href" target="_blank">{{ part.content }}</a
7381
7890
  >} }
7382
7891
  </span>
@@ -7389,7 +7898,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
7389
7898
  'k-files-vertical': chatService.messageFilesLayout === 'vertical'
7390
7899
  }"
7391
7900
  >
7392
- @for (file of message.files; track file) {
7901
+ @for (file of message.files; track file.id) {
7393
7902
  <li
7394
7903
  class="k-file-box"
7395
7904
  [chatFile]="file"
@@ -7403,7 +7912,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
7403
7912
  } @else {
7404
7913
  <ul class="k-file-box-wrapper">
7405
7914
  <div class="k-files-scroll">
7406
- @for (file of message.files; track file) {
7915
+ @for (file of message.files; track file.id) {
7407
7916
  <li
7408
7917
  class="k-file-box"
7409
7918
  [chatFile]="file"
@@ -7489,7 +7998,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
7489
7998
  </div>
7490
7999
  } } @if (showToolbar) {
7491
8000
  <kendo-toolbar class="k-chat-message-toolbar" fillMode="flat">
7492
- @for (action of toolbarActions; track action) {
8001
+ @for (action of toolbarActions; track action.id) {
7493
8002
  <kendo-toolbar-button
7494
8003
  fillMode="flat"
7495
8004
  [icon]="action.icon"
@@ -7516,7 +8025,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
7516
8025
  ButtonComponent,
7517
8026
  ],
7518
8027
  }]
7519
- }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i1$1.IntlService }, { type: ChatService }, { type: i1.LocalizationService }, { type: i0.ChangeDetectorRef }], propDecorators: { message: [{
8028
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i1$1.IntlService }, { type: ChatService }, { type: i1.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { message: [{
7520
8029
  type: Input
7521
8030
  }], tabbable: [{
7522
8031
  type: Input
@@ -8074,6 +8583,7 @@ class MessageListComponent {
8074
8583
  item.selected = true;
8075
8584
  this.selectedItem = item;
8076
8585
  }
8586
+ this.chatService.layoutChangeInProgress = true;
8077
8587
  this.cdr.detectChanges();
8078
8588
  }
8079
8589
  this.chatService.toggleMessageState = false;
@@ -8238,7 +8748,7 @@ class MessageListComponent {
8238
8748
  <div class="k-message-group-content">
8239
8749
  @if (showGroupAuthor(group)) {
8240
8750
  <p class="k-message-author">{{ group.author.name }}</p>
8241
- } @for (msg of group.messages; track msg; let firstMessage = $first; let lastMessage = $last) { @if
8751
+ } @for (msg of group.messages; track msg.id; let firstMessage = $first; let lastMessage = $last) { @if
8242
8752
  (msg.user?.avatarUrl) {
8243
8753
  <div class="k-avatar">
8244
8754
  <span class="k-avatar-image">
@@ -8346,7 +8856,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
8346
8856
  <div class="k-message-group-content">
8347
8857
  @if (showGroupAuthor(group)) {
8348
8858
  <p class="k-message-author">{{ group.author.name }}</p>
8349
- } @for (msg of group.messages; track msg; let firstMessage = $first; let lastMessage = $last) { @if
8859
+ } @for (msg of group.messages; track msg.id; let firstMessage = $first; let lastMessage = $last) { @if
8350
8860
  (msg.user?.avatarUrl) {
8351
8861
  <div class="k-avatar">
8352
8862
  <span class="k-avatar-image">
@@ -8666,6 +9176,86 @@ class ChatComponent {
8666
9176
  get sendButtonSettings() {
8667
9177
  return this.sendButton;
8668
9178
  }
9179
+ /**
9180
+ * Controls the scrolling behavior of the message list.
9181
+ *
9182
+ * @default 'scrollable'
9183
+ */
9184
+ scrollMode = 'scrollable';
9185
+ /**
9186
+ * Sets the number of messages loaded per page in endless scroll mode.
9187
+ * Ignored in `scrollable` mode.
9188
+ *
9189
+ * @default 50
9190
+ */
9191
+ pageSize = 50;
9192
+ /**
9193
+ * Sets the minimum distance between a new receiver message and the top of the visible
9194
+ * message area when auto-scrolling. Keeps older messages partially visible instead of
9195
+ * scrolling them out of view.
9196
+ *
9197
+ * Accepts a percentage string relative to the visible area height (for example, `'30%'`)
9198
+ * or a pixel value as a number. Negative values scroll the message fully to the bottom.
9199
+ *
9200
+ * Has no effect on author messages, which always scroll to the bottom.
9201
+ *
9202
+ * @default '20%'
9203
+ */
9204
+ autoScrollThreshold = '20%';
9205
+ /**
9206
+ * Sets the total number of messages in the conversation.
9207
+ *
9208
+ * When `messages.length` equals `total` (or `total` is not set), the Chat operates in
9209
+ * **built-in mode** and manages rendering internally. When `messages.length < total`,
9210
+ * the Chat operates in **remote mode** — the consumer manages data via
9211
+ * [`loadMore`](slug:api_conversational-ui_chatcomponent#loadMore) events.
9212
+ *
9213
+ * Only relevant when [`scrollMode`](slug:api_conversational-ui_chatcomponent#scrollMode)
9214
+ * is `'endless'`.
9215
+ */
9216
+ total;
9217
+ /**
9218
+ * Sets the index in the full conversation that `messages[0]` corresponds to.
9219
+ *
9220
+ * Only relevant in remote mode (`messages.length < total`). As the user scrolls up
9221
+ * and the consumer prepends messages, `startIndex` decreases. The Chat uses this to
9222
+ * compute the correct range for the next
9223
+ * [`loadMore`](slug:api_conversational-ui_chatcomponent#loadMore) call.
9224
+ *
9225
+ * Ignored in built-in mode.
9226
+ */
9227
+ startIndex;
9228
+ /**
9229
+ * Sets the exclusive end index of the current batch in the full conversation.
9230
+ *
9231
+ * Only relevant in remote mode (`messages.length < total`). As the user scrolls down
9232
+ * and the consumer appends messages, `endIndex` increases. The Chat uses this to know
9233
+ * whether more messages exist below the current batch.
9234
+ *
9235
+ * Ignored in built-in mode.
9236
+ */
9237
+ endIndex;
9238
+ /**
9239
+ * Sets the full set of pinned messages in the conversation.
9240
+ *
9241
+ * Only needed in remote mode (`messages.length < total`). The Chat uses this to
9242
+ * render the pinned message indicator when the pinned message is outside the current
9243
+ * batch. In built-in mode, the Chat has all messages and can look them up directly.
9244
+ *
9245
+ * @default []
9246
+ */
9247
+ pinnedMessages = [];
9248
+ /**
9249
+ * Sets the reply targets that live outside the current batch.
9250
+ *
9251
+ * Only needed in remote mode (`messages.length < total`). The Chat uses this to
9252
+ * render reply previews when the replied-to message is not in the current batch.
9253
+ * The consumer resolves this after each data fetch by finding which `replyToId`
9254
+ * targets are missing from the batch and fetching only those.
9255
+ *
9256
+ * @default []
9257
+ */
9258
+ repliedToMessages = [];
8669
9259
  /**
8670
9260
  * Sets the names of the model fields from which the Chat reads its data.
8671
9261
  * Lets you map custom data types to the expected `Message` format.
@@ -8743,6 +9333,14 @@ class ChatComponent {
8743
9333
  * Fires when the user types in the message input box.
8744
9334
  */
8745
9335
  inputValueChange = new EventEmitter();
9336
+ /**
9337
+ * Fires when the user scrolls near the edge of the rendered message window in endless scroll mode.
9338
+ */
9339
+ loadMore = new EventEmitter();
9340
+ /**
9341
+ * Fires when the user clicks a referenced message (pinned indicator or reply preview).
9342
+ */
9343
+ referencedMessageClick = new EventEmitter();
8746
9344
  get className() {
8747
9345
  return 'k-chat';
8748
9346
  }
@@ -8793,6 +9391,43 @@ class ChatComponent {
8793
9391
  }
8794
9392
  return this.messages;
8795
9393
  }
9394
+ /**
9395
+ * @hidden
9396
+ * Returns `true` when the Chat is in remote endless scroll mode.
9397
+ * Remote mode is active whenever the consumer provides a `total`.
9398
+ */
9399
+ get isRemoteMode() {
9400
+ return this.scrollMode === 'endless'
9401
+ && isPresent(this.total);
9402
+ }
9403
+ /**
9404
+ * @hidden
9405
+ * Returns the messages to render — sliced in endless mode, full in scrollable mode.
9406
+ * In remote mode, returns processedMessages as-is (consumer already provides only the current batch).
9407
+ */
9408
+ get renderedMessages() {
9409
+ const all = this.processedMessages;
9410
+ if (!all || all.length === 0) {
9411
+ return [];
9412
+ }
9413
+ if (this.scrollMode === 'endless') {
9414
+ if (this.isRemoteMode) {
9415
+ return all;
9416
+ }
9417
+ const start = this.endlessState.startIndex;
9418
+ const end = this.endlessState.endIndex;
9419
+ if (all !== this._renderedMessagesSource ||
9420
+ start !== this._renderedMessagesStart ||
9421
+ end !== this._renderedMessagesEnd) {
9422
+ this._cachedRenderedMessages = all.slice(start, end);
9423
+ this._renderedMessagesSource = all;
9424
+ this._renderedMessagesStart = start;
9425
+ this._renderedMessagesEnd = end;
9426
+ }
9427
+ return this._cachedRenderedMessages;
9428
+ }
9429
+ return all;
9430
+ }
8796
9431
  /**
8797
9432
  * @hidden
8798
9433
  */
@@ -8838,6 +9473,19 @@ class ChatComponent {
8838
9473
  * @hidden
8839
9474
  */
8840
9475
  scrollToBottomIcon = arrowDownOutlineIcon;
9476
+ /**
9477
+ * @hidden
9478
+ */
9479
+ endlessState = new EndlessScrollState();
9480
+ /**
9481
+ * @hidden
9482
+ */
9483
+ showLicenseWatermark = false;
9484
+ /**
9485
+ * @hidden
9486
+ */
9487
+ licenseMessage;
9488
+ anchor;
8841
9489
  direction;
8842
9490
  subs = new Subscription();
8843
9491
  _modelFields = defaultModelFields;
@@ -8848,13 +9496,25 @@ class ChatComponent {
8848
9496
  _lastModelFields = null;
8849
9497
  _cachedContextMenuActions = [];
8850
9498
  _lastContextMenuActionsReference = null;
9499
+ _previousMessagesLength = 0;
9500
+ _pendingScrollAction = null;
9501
+ _scrollHandledBeforePaint = false;
9502
+ _lastNewMessageId = null;
9503
+ _cachedRenderedMessages = [];
9504
+ _renderedMessagesSource = null;
9505
+ _renderedMessagesStart = -1;
9506
+ _renderedMessagesEnd = -1;
9507
+ _pendingRemoteScrollToMessageId = null;
9508
+ _remoteScrollToBottom = false;
8851
9509
  constructor(localization, zone, renderer, element, chatService) {
8852
9510
  this.localization = localization;
8853
9511
  this.zone = zone;
8854
9512
  this.renderer = renderer;
8855
9513
  this.element = element;
8856
9514
  this.chatService = chatService;
8857
- validatePackage(packageMetadata);
9515
+ const isValid = validatePackage(packageMetadata);
9516
+ this.licenseMessage = getLicenseMessage(packageMetadata);
9517
+ this.showLicenseWatermark = shouldShowValidationUI(isValid);
8858
9518
  this.direction = localization.rtl ? 'rtl' : 'ltr';
8859
9519
  this.subs.add(localization.changes.subscribe(({ rtl }) => {
8860
9520
  this.direction = rtl ? 'rtl' : 'ltr';
@@ -8900,17 +9560,53 @@ class ChatComponent {
8900
9560
  }));
8901
9561
  this.pinnedMessage = this.findLastPinnedMessage();
8902
9562
  this.chatService.authorId = this.authorId;
9563
+ this.chatService.repliedToMessages = this.repliedToMessages || [];
9564
+ this.initEndlessScroll();
9565
+ this.subs.add(this.chatService.replyReferenceClick$.subscribe((messageId) => {
9566
+ this.handleReferencedMessageClick(messageId, 'reply');
9567
+ }));
8903
9568
  }
8904
9569
  /**
8905
9570
  * @hidden
8906
9571
  */
8907
9572
  ngOnChanges(changes) {
8908
- this.zone.runOutsideAngular(() => setTimeout(() => {
8909
- this.messageList.element.nativeElement.style.flex = '1 1 auto';
8910
- }));
8911
9573
  if (isChanged('messages', changes, false)) {
8912
9574
  this.pinnedMessage = this.findLastPinnedMessage();
8913
9575
  this.chatService.messages = this.processedMessages;
9576
+ if (this.isRemoteMode) {
9577
+ const skipDetectAndSchedule = this.endlessState.isLoading || !!this._pendingRemoteScrollToMessageId;
9578
+ if (!skipDetectAndSchedule) {
9579
+ this.detectNewMessageScrollAction();
9580
+ }
9581
+ this.handleRemoteMessagesChange();
9582
+ if (!skipDetectAndSchedule) {
9583
+ this.scheduleNewMessageScroll();
9584
+ }
9585
+ }
9586
+ else {
9587
+ this.detectNewMessageScrollAction();
9588
+ this.handleMessagesChange();
9589
+ this.scheduleNewMessageScroll();
9590
+ }
9591
+ this.handlePendingRemoteScroll();
9592
+ }
9593
+ if (isChanged('pinnedMessages', changes, false)) {
9594
+ this.pinnedMessage = this.findLastPinnedMessage();
9595
+ }
9596
+ if (isChanged('repliedToMessages', changes, false)) {
9597
+ this.chatService.repliedToMessages = this.repliedToMessages || [];
9598
+ }
9599
+ this.validateRemoteInputs();
9600
+ if (isChanged('scrollMode', changes, false)) {
9601
+ this.initEndlessScroll();
9602
+ }
9603
+ if (isChanged('pageSize', changes, false) && this.scrollMode === 'endless') {
9604
+ this.endlessState.init(this.processedMessages?.length || 0, this.pageSize);
9605
+ }
9606
+ if (this.isRemoteMode && (isChanged('startIndex', changes, false) ||
9607
+ isChanged('endIndex', changes, false) ||
9608
+ isChanged('total', changes, false))) {
9609
+ this.endlessState.syncFromInputs(this.startIndex, this.endIndex, this.total);
8914
9610
  }
8915
9611
  if (isChanged('height', changes, false)) {
8916
9612
  this.renderer.setStyle(this.element.nativeElement, 'height', `${processCssValue(this.height)}`);
@@ -8980,9 +9676,130 @@ class ChatComponent {
8980
9676
  /**
8981
9677
  * @hidden
8982
9678
  */
8983
- scrollToPinnedMessage() {
9679
+ scrollToPinnedMessage(event) {
9680
+ if (event && event.target?.closest('button')) {
9681
+ return;
9682
+ }
8984
9683
  if (this.pinnedMessage) {
8985
- this.chatService.scrollToMessage(this.pinnedMessage?.id);
9684
+ this.handleReferencedMessageClick(this.pinnedMessage.id, 'pinned');
9685
+ }
9686
+ }
9687
+ /**
9688
+ * @hidden
9689
+ */
9690
+ onNearTop() {
9691
+ if (this.scrollMode !== 'endless' || this.endlessState.isLoading) {
9692
+ return;
9693
+ }
9694
+ if (this.isRemoteMode) {
9695
+ if (this.startIndex <= 0) {
9696
+ return;
9697
+ }
9698
+ this.endlessState.isLoading = true;
9699
+ this.anchor?.recordScrollHeight();
9700
+ this.anchor?.setAriaLive('off');
9701
+ const reqStart = Math.max(0, this.startIndex - this.pageSize);
9702
+ this.loadMore.emit({ startIndex: reqStart, endIndex: this.startIndex });
9703
+ return;
9704
+ }
9705
+ const range = this.endlessState.extendUp();
9706
+ if (range) {
9707
+ this.endlessState.isLoading = true;
9708
+ this.anchor?.recordScrollHeight();
9709
+ this.anchor?.setAriaLive('off');
9710
+ this.loadMore.emit({ startIndex: range.start, endIndex: range.end });
9711
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
9712
+ this.anchor?.preserveScrollPosition();
9713
+ this.anchor?.setAriaLive('polite');
9714
+ this.endlessState.isLoading = false;
9715
+ }));
9716
+ }
9717
+ }
9718
+ /**
9719
+ * @hidden
9720
+ */
9721
+ onNearBottom() {
9722
+ if (this.scrollMode !== 'endless' || this.endlessState.isLoading) {
9723
+ return;
9724
+ }
9725
+ if (this.isRemoteMode) {
9726
+ if (this.endIndex >= this.total) {
9727
+ return;
9728
+ }
9729
+ this.endlessState.isLoading = true;
9730
+ this.anchor?.setAriaLive('off');
9731
+ const reqEnd = Math.min(this.total, this.endIndex + this.pageSize);
9732
+ this.loadMore.emit({ startIndex: this.endIndex, endIndex: reqEnd });
9733
+ return;
9734
+ }
9735
+ const range = this.endlessState.extendDown();
9736
+ if (range) {
9737
+ this.endlessState.isLoading = true;
9738
+ this.loadMore.emit({ startIndex: range.start, endIndex: range.end });
9739
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
9740
+ this.endlessState.isLoading = false;
9741
+ }));
9742
+ }
9743
+ }
9744
+ /**
9745
+ * @hidden
9746
+ */
9747
+ onScrollToBottomClick() {
9748
+ if (this.scrollMode === 'endless' && !this.endlessState.isAtEnd) {
9749
+ if (this.isRemoteMode) {
9750
+ this.endlessState.isLoading = true;
9751
+ this._remoteScrollToBottom = true;
9752
+ this.autoScroll = true;
9753
+ const reqStart = Math.max(0, this.total - this.pageSize);
9754
+ this.loadMore.emit({ startIndex: reqStart, endIndex: this.total });
9755
+ return;
9756
+ }
9757
+ this.anchor?.lockForMessageScroll();
9758
+ const range = this.endlessState.jumpToEnd();
9759
+ this.loadMore.emit({ startIndex: range.start, endIndex: range.end });
9760
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
9761
+ this.anchor?.scrollToBottomWithSettle(() => {
9762
+ this.anchor?.unlockForMessageScroll();
9763
+ });
9764
+ }));
9765
+ }
9766
+ else {
9767
+ this.anchor?.scrollToBottom();
9768
+ }
9769
+ }
9770
+ /**
9771
+ * @hidden
9772
+ */
9773
+ handleReferencedMessageClick(messageId, type) {
9774
+ this.referencedMessageClick.emit({ id: messageId, type });
9775
+ this.autoScroll = false;
9776
+ if (this.scrollMode === 'endless') {
9777
+ if (this.isRemoteMode) {
9778
+ this.handleRemoteReferencedMessageClick(messageId);
9779
+ return;
9780
+ }
9781
+ const allMessages = this.processedMessages;
9782
+ const targetIndex = allMessages.findIndex(m => m.id === messageId);
9783
+ if (targetIndex === -1) {
9784
+ return;
9785
+ }
9786
+ if (this.endlessState.contains(targetIndex)) {
9787
+ this.anchor?.lockForMessageScroll();
9788
+ this.chatService.scrollToMessage(messageId, 'auto');
9789
+ this.anchor?.unlockForMessageScroll();
9790
+ }
9791
+ else {
9792
+ this.anchor?.lockForMessageScroll();
9793
+ const range = this.endlessState.jumpTo(targetIndex);
9794
+ this.loadMore.emit({ startIndex: range.start, endIndex: range.end });
9795
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
9796
+ this.chatService.scrollToMessage(messageId, 'auto');
9797
+ this.anchor?.unlockForMessageScroll();
9798
+ }));
9799
+ }
9800
+ }
9801
+ else {
9802
+ this.chatService.scrollToMessage(messageId);
8986
9803
  }
8987
9804
  }
8988
9805
  /**
@@ -9022,8 +9839,203 @@ class ChatComponent {
9022
9839
  this.chatService.selectOnMenuClose = false;
9023
9840
  }
9024
9841
  }
9842
+ /**
9843
+ * @hidden
9844
+ */
9845
+ onMessageListResize() {
9846
+ this.anchor?.calculateMessageBoxSeparator();
9847
+ const layoutChange = this.chatService.layoutChangeInProgress;
9848
+ const skipAutoScroll = !!this._pendingScrollAction || this._scrollHandledBeforePaint || layoutChange;
9849
+ if (skipAutoScroll) {
9850
+ this._scrollHandledBeforePaint = false;
9851
+ if (layoutChange) {
9852
+ this.chatService.layoutChangeInProgress = false;
9853
+ this.anchor?.onScroll();
9854
+ }
9855
+ return;
9856
+ }
9857
+ this.anchor?.autoScrollToBottom();
9858
+ }
9859
+ /**
9860
+ * Schedules the new-message scroll adjustment on zone.onStable so it runs
9861
+ * after Angular renders the new DOM but before the browser paints,
9862
+ * preventing a visible intermediate frame.
9863
+ */
9864
+ scheduleNewMessageScroll() {
9865
+ if (!this._pendingScrollAction) {
9866
+ return;
9867
+ }
9868
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
9869
+ this.zone.run(() => {
9870
+ const action = this._pendingScrollAction;
9871
+ this._pendingScrollAction = null;
9872
+ this._scrollHandledBeforePaint = true;
9873
+ if (action === 'author') {
9874
+ this._lastNewMessageId = null;
9875
+ this.autoScroll = true;
9876
+ this.anchor?.scrollToBottomWithSettle();
9877
+ return;
9878
+ }
9879
+ if (action === 'receiver') {
9880
+ const messageEl = this.getLastNewMessageElement();
9881
+ this._lastNewMessageId = null;
9882
+ this.autoScroll = true;
9883
+ if (messageEl) {
9884
+ this.anchor?.scrollWithThreshold(messageEl);
9885
+ }
9886
+ else {
9887
+ this.anchor?.scrollToBottom();
9888
+ }
9889
+ }
9890
+ });
9891
+ }));
9892
+ }
9893
+ detectNewMessageScrollAction() {
9894
+ const messages = this.processedMessages;
9895
+ const prevLength = this._previousMessagesLength;
9896
+ const hasNoMessages = !messages;
9897
+ const hasNoNewMessages = messages && messages.length <= prevLength;
9898
+ const isInitialLoad = prevLength === 0;
9899
+ if (hasNoMessages || hasNoNewMessages || isInitialLoad) {
9900
+ this._pendingScrollAction = null;
9901
+ this._lastNewMessageId = null;
9902
+ return;
9903
+ }
9904
+ const lastMessage = messages[messages.length - 1];
9905
+ this._lastNewMessageId = lastMessage?.id ?? null;
9906
+ const isAuthor = this.isOwnMessage(lastMessage);
9907
+ if (isAuthor) {
9908
+ this._pendingScrollAction = 'author';
9909
+ return;
9910
+ }
9911
+ if (!this.anchor) {
9912
+ this._pendingScrollAction = null;
9913
+ return;
9914
+ }
9915
+ const distance = this.anchor.getDistanceFromBottom();
9916
+ const threshold = this.anchor.getAutoScrollThresholdPx();
9917
+ const isNearBottom = distance <= threshold || this.anchor.isFollowingThreshold;
9918
+ this._pendingScrollAction = isNearBottom ? 'receiver' : null;
9919
+ }
9920
+ getLastNewMessageElement() {
9921
+ if (this._lastNewMessageId == null) {
9922
+ return null;
9923
+ }
9924
+ const ref = this.chatService.messageElementsMap.get(this._lastNewMessageId);
9925
+ return ref?.nativeElement ?? null;
9926
+ }
9025
9927
  findLastPinnedMessage() {
9026
- return [...this.processedMessages].reverse().find((message) => message.isPinned);
9928
+ const fromMessages = [...this.processedMessages].reverse().find((message) => message.isPinned);
9929
+ if (fromMessages) {
9930
+ return fromMessages;
9931
+ }
9932
+ if (this.isRemoteMode && this.pinnedMessages?.length) {
9933
+ return [...this.pinnedMessages].reverse().find((message) => message.isPinned);
9934
+ }
9935
+ return undefined;
9936
+ }
9937
+ initEndlessScroll() {
9938
+ if (this.scrollMode === 'endless') {
9939
+ if (this.isRemoteMode) {
9940
+ this.endlessState.syncFromInputs(this.startIndex ?? 0, this.endIndex ?? 0, this.total ?? 0);
9941
+ }
9942
+ else {
9943
+ this.endlessState.init(this.processedMessages?.length || 0, this.pageSize);
9944
+ }
9945
+ this._previousMessagesLength = this.processedMessages?.length || 0;
9946
+ }
9947
+ else {
9948
+ this.endlessState.reset();
9949
+ this._remoteScrollToBottom = false;
9950
+ this._pendingRemoteScrollToMessageId = null;
9951
+ }
9952
+ }
9953
+ handleMessagesChange() {
9954
+ if (this.scrollMode !== 'endless') {
9955
+ this._previousMessagesLength = this.processedMessages?.length || 0;
9956
+ return;
9957
+ }
9958
+ const allMessages = this.processedMessages || [];
9959
+ const wasAtEnd = this.endlessState.isAtEnd;
9960
+ const isNewMessageAppended = allMessages.length > this._previousMessagesLength && this._previousMessagesLength > 0;
9961
+ const isSameLength = allMessages.length === this._previousMessagesLength;
9962
+ const shouldScroll = this._pendingScrollAction !== null;
9963
+ if (isNewMessageAppended) {
9964
+ this.endlessState.updateTotal(allMessages.length);
9965
+ if (shouldScroll) {
9966
+ if (!wasAtEnd) {
9967
+ const range = this.endlessState.jumpToEnd();
9968
+ this.loadMore.emit({ startIndex: range.start, endIndex: range.end });
9969
+ }
9970
+ else {
9971
+ this.endlessState.endIndex = allMessages.length;
9972
+ }
9973
+ }
9974
+ }
9975
+ else if (!isSameLength) {
9976
+ this.endlessState.init(allMessages.length, this.pageSize);
9977
+ }
9978
+ this._previousMessagesLength = allMessages.length;
9979
+ }
9980
+ handleRemoteMessagesChange() {
9981
+ const wasLoading = this.endlessState.isLoading;
9982
+ const prevLength = this._previousMessagesLength;
9983
+ const currentLength = this.processedMessages?.length || 0;
9984
+ const isInitialBatch = prevLength === 0 && currentLength > 0;
9985
+ if (wasLoading) {
9986
+ this.endlessState.isLoading = false;
9987
+ if (this._remoteScrollToBottom) {
9988
+ this._remoteScrollToBottom = false;
9989
+ this._scrollHandledBeforePaint = true;
9990
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
9991
+ this.anchor?.scrollToBottomWithSettle();
9992
+ }));
9993
+ }
9994
+ else {
9995
+ this._scrollHandledBeforePaint = true;
9996
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
9997
+ this.anchor?.preserveScrollPosition();
9998
+ this.anchor?.setAriaLive('polite');
9999
+ }));
10000
+ }
10001
+ }
10002
+ else if (isInitialBatch) {
10003
+ this.autoScroll = true;
10004
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
10005
+ this.anchor?.scrollToBottom();
10006
+ }));
10007
+ }
10008
+ else if (currentLength > 0 && !this._pendingScrollAction) {
10009
+ const lastMessage = this.processedMessages[currentLength - 1];
10010
+ const isNowAtEnd = this.endIndex >= this.total;
10011
+ if (isNowAtEnd && this.isOwnMessage(lastMessage)) {
10012
+ this._pendingScrollAction = 'author';
10013
+ }
10014
+ }
10015
+ this._previousMessagesLength = currentLength;
10016
+ }
10017
+ handleRemoteReferencedMessageClick(messageId) {
10018
+ const messageEl = this.chatService.messageElementsMap.get(messageId);
10019
+ if (messageEl) {
10020
+ this.anchor?.lockForMessageScroll();
10021
+ this.chatService.scrollToMessage(messageId, 'auto');
10022
+ this.anchor?.unlockForMessageScroll();
10023
+ return;
10024
+ }
10025
+ this._pendingRemoteScrollToMessageId = messageId;
10026
+ }
10027
+ handlePendingRemoteScroll() {
10028
+ if (!this._pendingRemoteScrollToMessageId) {
10029
+ return;
10030
+ }
10031
+ const messageId = this._pendingRemoteScrollToMessageId;
10032
+ this._pendingRemoteScrollToMessageId = null;
10033
+ this._scrollHandledBeforePaint = true;
10034
+ this.subs.add(this.zone.onStable.pipe(take(1)).subscribe(() => {
10035
+ this.anchor?.lockForMessageScroll();
10036
+ this.chatService.scrollToMessage(messageId, 'auto');
10037
+ this.anchor?.unlockForMessageScroll();
10038
+ }));
9027
10039
  }
9028
10040
  updateChatServiceProperties(propNames, changes) {
9029
10041
  propNames.forEach(propName => {
@@ -9032,6 +10044,34 @@ class ChatComponent {
9032
10044
  }
9033
10045
  });
9034
10046
  }
10047
+ validateRemoteInputs() {
10048
+ if (!isDevMode()) {
10049
+ return;
10050
+ }
10051
+ const hasRemoteInputs = isPresent(this.total) || isPresent(this.startIndex) || isPresent(this.endIndex);
10052
+ if (hasRemoteInputs && this.scrollMode !== 'endless') {
10053
+ console.warn('[kendo-chat] [total], [startIndex], and [endIndex] are only used when [scrollMode] is \'endless\'.');
10054
+ return;
10055
+ }
10056
+ if (isPresent(this.total) && (!isPresent(this.startIndex) || !isPresent(this.endIndex))) {
10057
+ console.warn('[kendo-chat] Remote mode requires [startIndex] and [endIndex] when [total] is set.');
10058
+ }
10059
+ if (isPresent(this.startIndex) && this.startIndex < 0) {
10060
+ console.warn('[kendo-chat] [startIndex] must not be negative.');
10061
+ }
10062
+ if (isPresent(this.startIndex) && isPresent(this.endIndex) && this.startIndex > this.endIndex) {
10063
+ console.warn(`[kendo-chat] [startIndex] (${this.startIndex}) must not exceed [endIndex] (${this.endIndex}).`);
10064
+ }
10065
+ if (isPresent(this.endIndex) && isPresent(this.total) && this.endIndex > this.total) {
10066
+ console.warn(`[kendo-chat] [endIndex] (${this.endIndex}) must not exceed [total] (${this.total}).`);
10067
+ }
10068
+ if (isPresent(this.startIndex) && isPresent(this.endIndex) && this.messages) {
10069
+ const expectedLength = this.endIndex - this.startIndex;
10070
+ if (this.messages.length !== expectedLength) {
10071
+ console.warn(`[kendo-chat] messages.length (${this.messages.length}) does not match the range [startIndex]–[endIndex] (expected ${expectedLength}).`);
10072
+ }
10073
+ }
10074
+ }
9035
10075
  mergeWithDefaultActions(actions, defaultActions) {
9036
10076
  if (!actions || actions.length === 0) {
9037
10077
  return [];
@@ -9045,7 +10085,7 @@ class ChatComponent {
9045
10085
  });
9046
10086
  }
9047
10087
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatComponent, deps: [{ token: i1.LocalizationService }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: ChatService }], target: i0.ɵɵFactoryTarget.Component });
9048
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: ChatComponent, isStandalone: true, selector: "kendo-chat", inputs: { messages: "messages", authorId: "authorId", messageBoxType: "messageBoxType", height: "height", width: "width", placeholder: "placeholder", messageWidthMode: "messageWidthMode", timestampVisibility: "timestampVisibility", showUsername: "showUsername", showAvatar: "showAvatar", allowMessageCollapse: "allowMessageCollapse", speechToTextButton: "speechToTextButton", fileSelectButton: "fileSelectButton", messageBoxSettings: "messageBoxSettings", messageToolbarActions: "messageToolbarActions", inputValue: "inputValue", authorMessageSettings: "authorMessageSettings", receiverMessageSettings: "receiverMessageSettings", messageContextMenuActions: "messageContextMenuActions", fileActions: "fileActions", messageFilesLayout: "messageFilesLayout", suggestionsLayout: "suggestionsLayout", quickActionsLayout: "quickActionsLayout", suggestions: "suggestions", sendButton: "sendButton", sendButtonSettings: "sendButtonSettings", modelFields: "modelFields", scrollToBottomButton: "scrollToBottomButton", loading: "loading" }, outputs: { sendMessage: "sendMessage", resendMessage: "resendMessage", toolbarActionClick: "toolbarActionClick", contextMenuActionClick: "contextMenuActionClick", fileActionClick: "fileActionClick", download: "download", executeAction: "executeAction", suggestionExecute: "suggestionExecute", fileSelect: "fileSelect", fileRemove: "fileRemove", unpin: "unpin", inputValueChange: "inputValueChange" }, host: { properties: { "class": "this.className", "attr.dir": "this.dirAttr" } }, providers: [
10088
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: ChatComponent, isStandalone: true, selector: "kendo-chat", inputs: { messages: "messages", authorId: "authorId", messageBoxType: "messageBoxType", height: "height", width: "width", placeholder: "placeholder", messageWidthMode: "messageWidthMode", timestampVisibility: "timestampVisibility", showUsername: "showUsername", showAvatar: "showAvatar", allowMessageCollapse: "allowMessageCollapse", speechToTextButton: "speechToTextButton", fileSelectButton: "fileSelectButton", messageBoxSettings: "messageBoxSettings", messageToolbarActions: "messageToolbarActions", inputValue: "inputValue", authorMessageSettings: "authorMessageSettings", receiverMessageSettings: "receiverMessageSettings", messageContextMenuActions: "messageContextMenuActions", fileActions: "fileActions", messageFilesLayout: "messageFilesLayout", suggestionsLayout: "suggestionsLayout", quickActionsLayout: "quickActionsLayout", suggestions: "suggestions", sendButton: "sendButton", sendButtonSettings: "sendButtonSettings", scrollMode: "scrollMode", pageSize: "pageSize", autoScrollThreshold: "autoScrollThreshold", total: "total", startIndex: "startIndex", endIndex: "endIndex", pinnedMessages: "pinnedMessages", repliedToMessages: "repliedToMessages", modelFields: "modelFields", scrollToBottomButton: "scrollToBottomButton", loading: "loading" }, outputs: { sendMessage: "sendMessage", resendMessage: "resendMessage", toolbarActionClick: "toolbarActionClick", contextMenuActionClick: "contextMenuActionClick", fileActionClick: "fileActionClick", download: "download", executeAction: "executeAction", suggestionExecute: "suggestionExecute", fileSelect: "fileSelect", fileRemove: "fileRemove", unpin: "unpin", inputValueChange: "inputValueChange", loadMore: "loadMore", referencedMessageClick: "referencedMessageClick" }, host: { properties: { "class": "this.className", "attr.dir": "this.dirAttr" } }, providers: [
9049
10089
  LocalizationService,
9050
10090
  ChatService,
9051
10091
  SuggestionsScrollService,
@@ -9053,7 +10093,7 @@ class ChatComponent {
9053
10093
  provide: L10N_PREFIX,
9054
10094
  useValue: 'kendo.chat',
9055
10095
  },
9056
- ], queries: [{ propertyName: "attachmentTemplate", first: true, predicate: AttachmentTemplateDirective, descendants: true }, { propertyName: "chatHeaderTemplate", first: true, predicate: ChatHeaderTemplateDirective, descendants: true }, { propertyName: "chatNoDataTemplate", first: true, predicate: NoDataTemplateDirective, descendants: true }, { propertyName: "authorMessageContentTemplate", first: true, predicate: AuthorMessageContentTemplateDirective, descendants: true }, { propertyName: "receiverMessageContentTemplate", first: true, predicate: ReceiverMessageContentTemplateDirective, descendants: true }, { propertyName: "messageContentTemplate", first: true, predicate: MessageContentTemplateDirective, descendants: true }, { propertyName: "authorMessageTemplate", first: true, predicate: AuthorMessageTemplateDirective, descendants: true }, { propertyName: "receiverMessageTemplate", first: true, predicate: ReceiverMessageTemplateDirective, descendants: true }, { propertyName: "messageTemplate", first: true, predicate: MessageTemplateDirective, descendants: true }, { propertyName: "timestampTemplate", first: true, predicate: ChatTimestampTemplateDirective, descendants: true }, { propertyName: "suggestionTemplate", first: true, predicate: ChatSuggestionTemplateDirective, descendants: true }, { propertyName: "statusTemplate", first: true, predicate: ChatStatusTemplateDirective, descendants: true }, { propertyName: "messageBoxTemplate", first: true, predicate: ChatMessageBoxTemplateDirective, descendants: true }, { propertyName: "messageBoxStartAffixTemplate", first: true, predicate: ChatMessageBoxStartAffixTemplateDirective, descendants: true }, { propertyName: "messageBoxEndAffixTemplate", first: true, predicate: ChatMessageBoxEndAffixTemplateDirective, descendants: true }, { propertyName: "messageBoxTopAffixTemplate", first: true, predicate: ChatMessageBoxTopAffixTemplateDirective, descendants: true }, { propertyName: "userStatusTemplate", first: true, predicate: ChatUserStatusTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "messagesContextMenu", first: true, predicate: ["messagesContextMenu"], descendants: true }, { propertyName: "messageBox", first: true, predicate: ["messageBox"], descendants: true }, { propertyName: "messageList", first: true, predicate: ["messageList"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `
10096
+ ], queries: [{ propertyName: "attachmentTemplate", first: true, predicate: AttachmentTemplateDirective, descendants: true }, { propertyName: "chatHeaderTemplate", first: true, predicate: ChatHeaderTemplateDirective, descendants: true }, { propertyName: "chatNoDataTemplate", first: true, predicate: NoDataTemplateDirective, descendants: true }, { propertyName: "authorMessageContentTemplate", first: true, predicate: AuthorMessageContentTemplateDirective, descendants: true }, { propertyName: "receiverMessageContentTemplate", first: true, predicate: ReceiverMessageContentTemplateDirective, descendants: true }, { propertyName: "messageContentTemplate", first: true, predicate: MessageContentTemplateDirective, descendants: true }, { propertyName: "authorMessageTemplate", first: true, predicate: AuthorMessageTemplateDirective, descendants: true }, { propertyName: "receiverMessageTemplate", first: true, predicate: ReceiverMessageTemplateDirective, descendants: true }, { propertyName: "messageTemplate", first: true, predicate: MessageTemplateDirective, descendants: true }, { propertyName: "timestampTemplate", first: true, predicate: ChatTimestampTemplateDirective, descendants: true }, { propertyName: "suggestionTemplate", first: true, predicate: ChatSuggestionTemplateDirective, descendants: true }, { propertyName: "statusTemplate", first: true, predicate: ChatStatusTemplateDirective, descendants: true }, { propertyName: "messageBoxTemplate", first: true, predicate: ChatMessageBoxTemplateDirective, descendants: true }, { propertyName: "messageBoxStartAffixTemplate", first: true, predicate: ChatMessageBoxStartAffixTemplateDirective, descendants: true }, { propertyName: "messageBoxEndAffixTemplate", first: true, predicate: ChatMessageBoxEndAffixTemplateDirective, descendants: true }, { propertyName: "messageBoxTopAffixTemplate", first: true, predicate: ChatMessageBoxTopAffixTemplateDirective, descendants: true }, { propertyName: "userStatusTemplate", first: true, predicate: ChatUserStatusTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "messagesContextMenu", first: true, predicate: ["messagesContextMenu"], descendants: true }, { propertyName: "messageBox", first: true, predicate: ["messageBox"], descendants: true }, { propertyName: "messageList", first: true, predicate: ["messageList"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "anchor", first: true, predicate: ["anchor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
9057
10097
  <ng-container
9058
10098
  kendoChatLocalizedMessages
9059
10099
  i18n-deletedMessageSenderText="
@@ -9123,9 +10163,7 @@ class ChatComponent {
9123
10163
  kendo.chat.nextSuggestionsButtonTitle|The title of the button that scrolls to the next suggestions
9124
10164
  "
9125
10165
  nextSuggestionsButtonTitle="Scroll right"
9126
- i18n-unpinMessageTitle="
9127
- kendo.chat.unpinMessageTitle|The title of the button that unpins a pinned message
9128
- "
10166
+ i18n-unpinMessageTitle="kendo.chat.unpinMessageTitle|The title of the button that unpins a pinned message"
9129
10167
  unpinMessageTitle="Unpin message"
9130
10168
  >
9131
10169
  </ng-container>
@@ -9144,18 +10182,30 @@ class ChatComponent {
9144
10182
  [attr.aria-label]="textFor('messageListLabel')"
9145
10183
  #anchor="scrollAnchor"
9146
10184
  [(autoScroll)]="autoScroll"
10185
+ [autoScrollThreshold]="autoScrollThreshold"
10186
+ [endlessMode]="scrollMode === 'endless'"
10187
+ [rangeIsAtEnd]="endlessState.isAtEnd"
10188
+ (nearTop)="onNearTop()"
10189
+ (nearBottom)="onNearBottom()"
9147
10190
  >
9148
10191
  @if (pinnedMessage) {
9149
10192
  <div
9150
10193
  class="k-message-reference k-message-pinned"
9151
10194
  [class.k-message-reference-receiver]="!isOwnMessage(pinnedMessage)"
9152
10195
  [class.k-message-reference-sender]="isOwnMessage(pinnedMessage)"
9153
- (click)="scrollToPinnedMessage()"
10196
+ (click)="scrollToPinnedMessage($event)"
9154
10197
  >
9155
10198
  <kendo-icon-wrapper name="pin" [svgIcon]="pinIcon"> </kendo-icon-wrapper>
9156
10199
  <chat-message-reference-content [message]="pinnedMessage"></chat-message-reference-content>
9157
10200
  <span class="k-spacer"></span>
9158
- <button kendoButton icon="x" [svgIcon]="deleteIcon" [attr.title]="textFor('unpinMessageTitle')" (click)="unpin.emit(pinnedMessage)" fillMode="flat"></button>
10201
+ <button
10202
+ kendoButton
10203
+ icon="x"
10204
+ [svgIcon]="deleteIcon"
10205
+ [attr.title]="textFor('unpinMessageTitle')"
10206
+ (click)="unpin.emit(pinnedMessage)"
10207
+ fillMode="flat"
10208
+ ></button>
9159
10209
  </div>
9160
10210
  } @if (processedMessages && processedMessages.length === 0) {
9161
10211
  <div class="k-message-list-content k-message-list-content-empty">
@@ -9163,7 +10213,7 @@ class ChatComponent {
9163
10213
  </div>
9164
10214
  } @else {
9165
10215
  <kendo-chat-message-list
9166
- [messages]="processedMessages"
10216
+ [messages]="renderedMessages"
9167
10217
  [authorMessageContentTemplate]="authorMessageContentTemplate"
9168
10218
  [receiverMessageContentTemplate]="receiverMessageContentTemplate"
9169
10219
  [messageContentTemplate]="messageContentTemplate"
@@ -9177,7 +10227,7 @@ class ChatComponent {
9177
10227
  [attachmentTemplate]="attachmentTemplate"
9178
10228
  [authorId]="authorId"
9179
10229
  (executeAction)="dispatchAction($event)"
9180
- (resize)="anchor.autoScrollToBottom(); anchor.calculateMessageBoxSeparator()"
10230
+ (resize)="onMessageListResize()"
9181
10231
  (navigate)="this.autoScroll = false"
9182
10232
  >
9183
10233
  </kendo-chat-message-list>
@@ -9190,7 +10240,7 @@ class ChatComponent {
9190
10240
  [align]="{ horizontal: 'center' }"
9191
10241
  icon="arrow-down-outline"
9192
10242
  [svgIcon]="scrollToBottomIcon"
9193
- (click)="anchor.scrollToBottom()"
10243
+ (click)="onScrollToBottomClick()"
9194
10244
  ></kendo-floatingactionbutton>
9195
10245
  </div>
9196
10246
  }
@@ -9227,7 +10277,11 @@ class ChatComponent {
9227
10277
  (popupClose)="handleMenuClose($event)"
9228
10278
  (select)="onContextMenuAction($event.item.originalAction)"
9229
10279
  ></kendo-contextmenu>
9230
- `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective$2, selector: "[kendoChatLocalizedMessages]" }, { kind: "directive", type: ScrollAnchorDirective, selector: "[kendoChatScrollAnchor]", inputs: ["autoScroll"], outputs: ["autoScrollChange"], exportAs: ["scrollAnchor"] }, { kind: "component", type: MessageListComponent, selector: "kendo-chat-message-list", inputs: ["messages", "attachmentTemplate", "authorMessageContentTemplate", "receiverMessageContentTemplate", "messageContentTemplate", "authorMessageTemplate", "receiverMessageTemplate", "messageTemplate", "timestampTemplate", "statusTemplate", "userStatusTemplate", "localization", "authorId"], outputs: ["executeAction", "navigate", "resize"] }, { kind: "component", type: MessageBoxComponent, selector: "kendo-message-box", inputs: ["authorId", "autoScroll", "suggestions", "placeholder", "inputValue", "localization", "messageBoxTemplate", "messageBoxStartAffixTemplate", "messageBoxEndAffixTemplate", "messageBoxTopAffixTemplate", "suggestionTemplate", "loading"], outputs: ["sendMessage", "executeSuggestion", "fileSelect", "fileRemove"] }, { kind: "component", type: MessageReferenceComponent, selector: "chat-message-reference-content", inputs: ["message"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "component", type: ContextMenuComponent, selector: "kendo-contextmenu", inputs: ["showOn", "target", "filter", "alignToAnchor", "vertical", "popupAnimate", "popupAlign", "anchorAlign", "collision", "appendTo", "ariaLabel"], outputs: ["popupOpen", "popupClose", "select", "open", "close"], exportAs: ["kendoContextMenu"] }, { kind: "component", type: FloatingActionButtonComponent, selector: "kendo-floatingactionbutton", inputs: ["themeColor", "size", "rounded", "disabled", "align", "offset", "positionMode", "icon", "svgIcon", "iconClass", "buttonClass", "dialClass", "text", "dialItemAnimation", "tabIndex", "dialItems"], outputs: ["blur", "focus", "dialItemClick", "open", "close"] }] });
10280
+
10281
+ @if (showLicenseWatermark) {
10282
+ <div kendoWatermarkOverlay [licenseMessage]="licenseMessage"></div>
10283
+ }
10284
+ `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective$2, selector: "[kendoChatLocalizedMessages]" }, { kind: "directive", type: ScrollAnchorDirective, selector: "[kendoChatScrollAnchor]", inputs: ["autoScroll", "autoScrollThreshold", "endlessMode", "rangeIsAtEnd"], outputs: ["autoScrollChange", "nearTop", "nearBottom"], exportAs: ["scrollAnchor"] }, { kind: "component", type: MessageListComponent, selector: "kendo-chat-message-list", inputs: ["messages", "attachmentTemplate", "authorMessageContentTemplate", "receiverMessageContentTemplate", "messageContentTemplate", "authorMessageTemplate", "receiverMessageTemplate", "messageTemplate", "timestampTemplate", "statusTemplate", "userStatusTemplate", "localization", "authorId"], outputs: ["executeAction", "navigate", "resize"] }, { kind: "component", type: MessageBoxComponent, selector: "kendo-message-box", inputs: ["authorId", "autoScroll", "suggestions", "placeholder", "inputValue", "localization", "messageBoxTemplate", "messageBoxStartAffixTemplate", "messageBoxEndAffixTemplate", "messageBoxTopAffixTemplate", "suggestionTemplate", "loading"], outputs: ["sendMessage", "executeSuggestion", "fileSelect", "fileRemove"] }, { kind: "component", type: MessageReferenceComponent, selector: "chat-message-reference-content", inputs: ["message"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "component", type: ContextMenuComponent, selector: "kendo-contextmenu", inputs: ["showOn", "target", "filter", "alignToAnchor", "vertical", "popupAnimate", "popupAlign", "anchorAlign", "collision", "appendTo", "ariaLabel"], outputs: ["popupOpen", "popupClose", "select", "open", "close"], exportAs: ["kendoContextMenu"] }, { kind: "component", type: FloatingActionButtonComponent, selector: "kendo-floatingactionbutton", inputs: ["themeColor", "size", "rounded", "disabled", "align", "offset", "positionMode", "icon", "svgIcon", "iconClass", "buttonClass", "dialClass", "text", "dialItemAnimation", "tabIndex", "dialItems"], outputs: ["blur", "focus", "dialItemClick", "open", "close"] }, { kind: "component", type: WatermarkOverlayComponent, selector: "div[kendoWatermarkOverlay], kendo-watermark-overlay", inputs: ["licenseMessage"] }] });
9231
10285
  }
9232
10286
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ChatComponent, decorators: [{
9233
10287
  type: Component,
@@ -9312,9 +10366,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9312
10366
  kendo.chat.nextSuggestionsButtonTitle|The title of the button that scrolls to the next suggestions
9313
10367
  "
9314
10368
  nextSuggestionsButtonTitle="Scroll right"
9315
- i18n-unpinMessageTitle="
9316
- kendo.chat.unpinMessageTitle|The title of the button that unpins a pinned message
9317
- "
10369
+ i18n-unpinMessageTitle="kendo.chat.unpinMessageTitle|The title of the button that unpins a pinned message"
9318
10370
  unpinMessageTitle="Unpin message"
9319
10371
  >
9320
10372
  </ng-container>
@@ -9333,18 +10385,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9333
10385
  [attr.aria-label]="textFor('messageListLabel')"
9334
10386
  #anchor="scrollAnchor"
9335
10387
  [(autoScroll)]="autoScroll"
10388
+ [autoScrollThreshold]="autoScrollThreshold"
10389
+ [endlessMode]="scrollMode === 'endless'"
10390
+ [rangeIsAtEnd]="endlessState.isAtEnd"
10391
+ (nearTop)="onNearTop()"
10392
+ (nearBottom)="onNearBottom()"
9336
10393
  >
9337
10394
  @if (pinnedMessage) {
9338
10395
  <div
9339
10396
  class="k-message-reference k-message-pinned"
9340
10397
  [class.k-message-reference-receiver]="!isOwnMessage(pinnedMessage)"
9341
10398
  [class.k-message-reference-sender]="isOwnMessage(pinnedMessage)"
9342
- (click)="scrollToPinnedMessage()"
10399
+ (click)="scrollToPinnedMessage($event)"
9343
10400
  >
9344
10401
  <kendo-icon-wrapper name="pin" [svgIcon]="pinIcon"> </kendo-icon-wrapper>
9345
10402
  <chat-message-reference-content [message]="pinnedMessage"></chat-message-reference-content>
9346
10403
  <span class="k-spacer"></span>
9347
- <button kendoButton icon="x" [svgIcon]="deleteIcon" [attr.title]="textFor('unpinMessageTitle')" (click)="unpin.emit(pinnedMessage)" fillMode="flat"></button>
10404
+ <button
10405
+ kendoButton
10406
+ icon="x"
10407
+ [svgIcon]="deleteIcon"
10408
+ [attr.title]="textFor('unpinMessageTitle')"
10409
+ (click)="unpin.emit(pinnedMessage)"
10410
+ fillMode="flat"
10411
+ ></button>
9348
10412
  </div>
9349
10413
  } @if (processedMessages && processedMessages.length === 0) {
9350
10414
  <div class="k-message-list-content k-message-list-content-empty">
@@ -9352,7 +10416,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9352
10416
  </div>
9353
10417
  } @else {
9354
10418
  <kendo-chat-message-list
9355
- [messages]="processedMessages"
10419
+ [messages]="renderedMessages"
9356
10420
  [authorMessageContentTemplate]="authorMessageContentTemplate"
9357
10421
  [receiverMessageContentTemplate]="receiverMessageContentTemplate"
9358
10422
  [messageContentTemplate]="messageContentTemplate"
@@ -9366,7 +10430,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9366
10430
  [attachmentTemplate]="attachmentTemplate"
9367
10431
  [authorId]="authorId"
9368
10432
  (executeAction)="dispatchAction($event)"
9369
- (resize)="anchor.autoScrollToBottom(); anchor.calculateMessageBoxSeparator()"
10433
+ (resize)="onMessageListResize()"
9370
10434
  (navigate)="this.autoScroll = false"
9371
10435
  >
9372
10436
  </kendo-chat-message-list>
@@ -9379,7 +10443,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9379
10443
  [align]="{ horizontal: 'center' }"
9380
10444
  icon="arrow-down-outline"
9381
10445
  [svgIcon]="scrollToBottomIcon"
9382
- (click)="anchor.scrollToBottom()"
10446
+ (click)="onScrollToBottomClick()"
9383
10447
  ></kendo-floatingactionbutton>
9384
10448
  </div>
9385
10449
  }
@@ -9416,6 +10480,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9416
10480
  (popupClose)="handleMenuClose($event)"
9417
10481
  (select)="onContextMenuAction($event.item.originalAction)"
9418
10482
  ></kendo-contextmenu>
10483
+
10484
+ @if (showLicenseWatermark) {
10485
+ <div kendoWatermarkOverlay [licenseMessage]="licenseMessage"></div>
10486
+ }
9419
10487
  `,
9420
10488
  standalone: true,
9421
10489
  imports: [
@@ -9429,6 +10497,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9429
10497
  ButtonComponent,
9430
10498
  ContextMenuComponent,
9431
10499
  FloatingActionButtonComponent,
10500
+ WatermarkOverlayComponent,
9432
10501
  ],
9433
10502
  }]
9434
10503
  }], ctorParameters: () => [{ type: i1.LocalizationService }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: ChatService }], propDecorators: { messages: [{
@@ -9483,6 +10552,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9483
10552
  type: Input
9484
10553
  }], sendButtonSettings: [{
9485
10554
  type: Input
10555
+ }], scrollMode: [{
10556
+ type: Input
10557
+ }], pageSize: [{
10558
+ type: Input
10559
+ }], autoScrollThreshold: [{
10560
+ type: Input
10561
+ }], total: [{
10562
+ type: Input
10563
+ }], startIndex: [{
10564
+ type: Input
10565
+ }], endIndex: [{
10566
+ type: Input
10567
+ }], pinnedMessages: [{
10568
+ type: Input
10569
+ }], repliedToMessages: [{
10570
+ type: Input
9486
10571
  }], modelFields: [{
9487
10572
  type: Input
9488
10573
  }], scrollToBottomButton: [{
@@ -9513,6 +10598,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9513
10598
  type: Output
9514
10599
  }], inputValueChange: [{
9515
10600
  type: Output
10601
+ }], loadMore: [{
10602
+ type: Output
10603
+ }], referencedMessageClick: [{
10604
+ type: Output
9516
10605
  }], className: [{
9517
10606
  type: HostBinding,
9518
10607
  args: ['class']
@@ -9579,6 +10668,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9579
10668
  }], messageList: [{
9580
10669
  type: ViewChild,
9581
10670
  args: ['messageList', { static: true, read: ViewContainerRef }]
10671
+ }], anchor: [{
10672
+ type: ViewChild,
10673
+ args: ['anchor']
9582
10674
  }] } });
9583
10675
 
9584
10676
  // eslint-disable no-forward-ref